diff --git a/lib/installers/atl.js b/lib/installers/atl.js index 6f68b01..96ac1b2 100644 --- a/lib/installers/atl.js +++ b/lib/installers/atl.js @@ -1,6 +1,6 @@ import chalk from 'chalk'; import inquirer from 'inquirer'; -import { execSync } from 'child_process'; +import { execFileSync, execSync } from 'child_process'; import fs from 'fs'; import os from 'os'; import path from 'path'; @@ -50,7 +50,7 @@ export const hasOAuthCredentials = () => { if (binary) { let output; try { - output = execSync(`${binary} auth status`, { + output = execFileSync(binary, ['auth', 'status'], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8', }); @@ -79,18 +79,52 @@ export const hasOAuthCredentials = () => { /** * Check if atl-cli is authenticated (has valid token) */ -export const isAtlAuthenticated = () => { +export const parseAtlAuthStatus = (output, hostname = '') => { + const statuses = JSON.parse(output); + if (!Array.isArray(statuses)) return false; + return hostname + ? statuses.length === 1 && statuses[0]?.authenticated === true + : statuses.some((status) => status?.authenticated === true); +}; + +export const isAtlAuthenticated = (hostname = '') => { const binary = findAtlBinary(); if (!binary) return false; try { - const output = execSync(`${binary} auth status`, { stdio: 'pipe', encoding: 'utf8' }); - return output.includes('Authenticated'); + const args = ['auth', 'status', '--json']; + if (hostname) args.push('--hostname', hostname); + const output = execFileSync(binary, args, { stdio: 'pipe', encoding: 'utf8' }); + return parseAtlAuthStatus(output, hostname); } catch { return false; } }; +export const loginAtlTargets = ( + binary, + targets, + { login = execFileSync, isAuthenticated = isAtlAuthenticated } = {} +) => { + const failures = []; + for (const hostname of targets) { + try { + const args = ['auth', 'login']; + if (hostname) args.push('--hostname', hostname); + login(binary, args, { stdio: 'inherit' }); + if (!isAuthenticated(hostname)) { + failures.push(hostname || 'default host'); + } + } catch { + failures.push(hostname || 'default host'); + } + } + return failures; +}; + +export const unauthenticatedAtlTargets = (hosts, isAuthenticated = isAtlAuthenticated) => + hosts.filter((hostname) => !isAuthenticated(hostname)); + /** * Configure git and Go for private repo access */ @@ -191,7 +225,15 @@ const installAtl = async () => { /** * Configure Atlassian CLI */ -export const configureAtlassianCli = async () => { +export const configureAtlassianCli = async ({ loginHosts = [] } = {}) => { + if ( + !Array.isArray(loginHosts) || + loginHosts.some((hostname) => typeof hostname !== 'string' || hostname.trim() === '') + ) { + console.error(chalk.red('✗ loginHosts must contain non-empty hostnames or aliases')); + return false; + } + console.log(chalk.cyan('\n=== Atlassian CLI Configuration ===\n')); const binary = findAtlBinary(); @@ -243,7 +285,9 @@ export const configureAtlassianCli = async () => { // Check current state const hasOAuth = hasOAuthCredentials(); - const isAuthenticated = isAtlAuthenticated(); + const missingHosts = unauthenticatedAtlTargets(loginHosts); + const isAuthenticated = loginHosts.length > 0 ? missingHosts.length === 0 : isAtlAuthenticated(); + let loginTargets = loginHosts.length > 0 ? missingHosts : ['']; console.log( chalk.blue('OAuth credentials:'), @@ -255,7 +299,7 @@ export const configureAtlassianCli = async () => { ); // If already fully set up, offer to reconfigure - if (isAuthenticated) { + if (hasOAuth && isAuthenticated) { console.log(chalk.green('\n✓ Already authenticated with Atlassian')); const { reconfigure } = await inquirer.prompt([ @@ -268,6 +312,9 @@ export const configureAtlassianCli = async () => { ]); if (!reconfigure) return true; + // This branch is reachable only after every requested host passed the + // authentication check; explicit re-authentication intentionally refreshes all. + loginTargets = loginHosts.length > 0 ? loginHosts : ['']; } // Step 1: OAuth setup (only if credentials don't exist) @@ -287,7 +334,8 @@ export const configureAtlassianCli = async () => { if (runSetup) { try { - execSync(`${atlBinary} auth setup`, { stdio: 'inherit' }); + execFileSync(atlBinary, ['auth', 'setup'], { stdio: 'inherit' }); + loginTargets = loginHosts.length > 0 ? loginHosts : ['']; console.log(chalk.green('\n✓ OAuth setup completed')); } catch (error) { console.error(chalk.red(`\n✗ OAuth setup failed: ${error.message}`)); @@ -305,7 +353,13 @@ export const configureAtlassianCli = async () => { // Step 2: Login (always run if not authenticated) console.log(chalk.blue('\n--- Step 2: Login ---')); - console.log(chalk.gray('This will open a browser window for authentication.\n')); + console.log( + chalk.gray( + loginHosts.length > 0 + ? `This will authenticate: ${loginTargets.join(', ')}.\n` + : 'This will open a browser window for authentication.\n' + ) + ); const { runLogin } = await inquirer.prompt([ { @@ -322,13 +376,15 @@ export const configureAtlassianCli = async () => { return false; } - try { - execSync(`${atlBinary} auth login`, { stdio: 'inherit' }); - console.log(chalk.green('\n✓ Atlassian CLI authenticated successfully')); - return true; - } catch (error) { - console.error(chalk.red(`\n✗ Authentication failed: ${error.message}`)); - console.log(chalk.gray(`Try again with: ${atlBinary} auth login`)); + const failures = loginAtlTargets(atlBinary, loginTargets); + if (failures.length > 0) { + console.error(chalk.red(`\n✗ Authentication failed for: ${failures.join(', ')}`)); + for (const hostname of failures) { + const retry = hostname === 'default host' ? '' : ` --hostname ${hostname}`; + console.log(chalk.gray(`Try again with: ${atlBinary} auth login${retry}`)); + } return false; } + console.log(chalk.green('\n✓ Atlassian CLI authenticated successfully')); + return true; }; diff --git a/lib/llm/index.js b/lib/llm/index.js index bf15c45..b936932 100644 --- a/lib/llm/index.js +++ b/lib/llm/index.js @@ -159,13 +159,14 @@ git show origin/main:path/to/file content: `## Atlassian CLI (atl) Command-line tool for Jira and Confluence. Use \`--json\` for structured output. +The context and Assets commands below require atl-cli v1.13.0 or newer. ### Authentication \`\`\`bash -atl auth status # Check authentication +atl auth status # Check every configured site atl auth setup # First-time OAuth setup (required once) -atl auth login # Authenticate (opens browser) +atl auth login --hostname mycompany.atlassian.net \`\`\` ### Context Switching (Multi-Environment) @@ -177,7 +178,7 @@ Switch between Atlassian instances (e.g., production vs sandbox) using aliases: atl config set-alias prod # alias "prod" → current host atl config set-alias sandbox mycompany-sandbox.atlassian.net # alias "sandbox" → specific host -# Switch active host +# Switch the persistent default for an interactive shell only atl config use-context prod # switch by alias atl config use-context sandbox atl config use-context mycompany.atlassian.net # or by full hostname @@ -194,92 +195,121 @@ atl config list # shows Aliases section with (current) marker Aliases can be used with the \`--hostname\` flag to target an environment for a single command: \`atl auth status --hostname prod\` +Auth commands use \`--hostname\`; Jira, Confluence, and Assets operations use +the root \`--context\` option. + +**Always pass \`--context \` on every Jira, Confluence, or +Assets operation.** The persistent default is shared across processes, so an +agent must never rely on it or run \`atl config use-context\`. A required guard +in managed agent sessions blocks context-less API commands. + +\`\`\`bash +atl --context prod jira issue view PROJ-1234 +atl --context sandbox confluence space list +\`\`\` + +Inline \`ATLASSIAN_CONTEXT=prod atl ...\` is equivalent, but the flag is preferred. + Jira commands are under \`atl jira\` (\`atl jira issue\`, \`atl jira board\`, \`atl jira sm\`, \`atl jira sprint\`). The bare \`atl issue\`/\`atl board\`/\`atl sm\` forms still work as deprecated aliases (they warn) and may be removed. +### Jira Assets + +Assets uses the explicitly selected host's OAuth token and auto-discovers its +workspace. Re-authenticate each hostname after CMDB scopes are added to the app. + +\`\`\`bash +atl --context sandbox jira assets count +atl --context sandbox jira assets aql 'objectType = Customer' --limit 25 +atl --context sandbox jira assets object 9244 --json +\`\`\` + ### Jira Issues +The examples below use \`prod\` as a placeholder. Resolve and substitute the +intended alias or hostname before running any command, especially a write. + \`\`\`bash # View and list -atl jira issue view PROJ-1234 # View issue details (includes custom fields) -atl jira issue view PROJ-1234 --json # View as JSON (custom_fields section) -atl jira issue list --assignee @me # Your assigned issues -atl jira issue list --jql "status = Open" # Custom JQL query +atl --context prod jira issue view PROJ-1234 # View issue details (includes custom fields) +atl --context prod jira issue view PROJ-1234 --json # View as JSON (custom_fields section) +atl --context prod jira issue list --assignee @me # Your assigned issues +atl --context prod jira issue list --jql "status = Open" # Custom JQL query # Create -atl jira issue create --project PROJ --type Bug --summary "Title" -atl jira issue create --project PROJ --type Task --summary "Title" --description "Details" -atl jira issue create --project PROJ --parent PROJ-123 --summary "Subtask" # Auto-discovers subtask type -atl jira issue create --project PROJ --type Bug --summary "Title" --security "Developer only" # Restrict visibility +atl --context prod jira issue create --project PROJ --type Bug --summary "Title" +atl --context prod jira issue create --project PROJ --type Task --summary "Title" --description "Details" +atl --context prod jira issue create --project PROJ --parent PROJ-123 --summary "Subtask" # Auto-discovers subtask type +atl --context prod jira issue create --project PROJ --type Bug --summary "Title" --security "Developer only" # Restrict visibility # Edit -atl jira issue edit PROJ-1234 --summary "New summary" -atl jira issue edit PROJ-1234 --assignee @me -atl jira issue edit PROJ-1234 --description "New description" -atl jira issue edit PROJ-1234 --description "Appended text" --append -atl jira issue edit PROJ-1234 --add-label bug --remove-label wontfix -atl jira issue edit PROJ-1234 --priority High -atl jira issue edit PROJ-1234 --field "Story Points=8" # Custom field by name -atl jira issue edit PROJ-1234 --field "customfield_10016=8" # Custom field by ID -atl jira issue edit PROJ-1234 --field "Custom Field=Some **markdown** text" # Auto-converts to ADF -atl jira issue edit PROJ-1234 --field-file fields.json # Complex values from JSON file -atl jira issue edit PROJ-1234 --security "Developer only" # Set issue security level (by name or id) -atl jira issue edit PROJ-1234 --security "" # Clear issue security level +atl --context prod jira issue edit PROJ-1234 --summary "New summary" +atl --context prod jira issue edit PROJ-1234 --assignee @me +atl --context prod jira issue edit PROJ-1234 --description "New description" +atl --context prod jira issue edit PROJ-1234 --description "Appended text" --append +atl --context prod jira issue edit PROJ-1234 --add-label bug --remove-label wontfix +atl --context prod jira issue edit PROJ-1234 --priority High +atl --context prod jira issue edit PROJ-1234 --field "Story Points=8" # Custom field by name +atl --context prod jira issue edit PROJ-1234 --field "customfield_10016=8" # Custom field by ID +atl --context prod jira issue edit PROJ-1234 --field "Custom Field=Some **markdown** text" # Auto-converts to ADF +atl --context prod jira issue edit PROJ-1234 --field-file fields.json # Complex values from JSON file +atl --context prod jira issue edit PROJ-1234 --security "Developer only" # Set issue security level (by name or id) +atl --context prod jira issue edit PROJ-1234 --security "" # Clear issue security level # Workflow -atl jira issue transition PROJ-1234 "In Progress" -atl jira issue transition PROJ-1234 --list # List available transitions -atl jira issue transition PROJ-1234 "Done" --field "Resolution=Fixed" # Transition with required fields +atl --context prod jira issue transition PROJ-1234 "In Progress" +atl --context prod jira issue transition PROJ-1234 --list # List available transitions +atl --context prod jira issue transition PROJ-1234 "Done" --field "Resolution=Fixed" # Transition with required fields # Issue links -atl jira issue link PROJ-1 PROJ-2 --type Blocks -atl jira issue link PROJ-1 --list # List links on an issue -atl jira issue link PROJ-1 --delete # Delete a link by ID -atl jira issue link --list-types # List available link types +atl --context prod jira issue link PROJ-1 PROJ-2 --type Blocks +atl --context prod jira issue link PROJ-1 --list # List links on an issue +atl --context prod jira issue link PROJ-1 --delete # Delete a link by ID +atl --context prod jira issue link --list-types # List available link types # Web links -atl jira issue weblink PROJ-1234 --url "https://..." --title "Title" +atl --context prod jira issue weblink PROJ-1234 --url "https://..." --title "Title" # Sprint management -atl jira issue sprint PROJ-1234 --sprint-id 123 -atl jira issue sprint PROJ-1234 --backlog -atl jira issue sprint --list-sprints --board 42 +atl --context prod jira issue sprint PROJ-1234 --sprint-id 123 +atl --context prod jira issue sprint PROJ-1234 --backlog +atl --context prod jira issue sprint --list-sprints --board 42 # Sprint lifecycle (atl jira sprint) -atl jira sprint create --board 42 --name "Sprint 30" --goal "..." # future sprint -atl jira sprint create --board 42 --name "Sprint 30" --start --duration 14d # create + start -atl jira sprint edit 123 --goal "Updated goal" -atl jira sprint start 123 --duration 14d -atl jira sprint close 123 # prompts unless --force -atl jira sprint list --board 42 [--state active,future,closed] -atl jira sprint move NX-1 NX-2 --to 123 # or --sprint "name" --board 42 -atl jira sprint backlog NX-1 +atl --context prod jira sprint create --board 42 --name "Sprint 30" --goal "..." # future sprint +atl --context prod jira sprint create --board 42 --name "Sprint 30" --start --duration 14d # create + start +atl --context prod jira sprint edit 123 --goal "Updated goal" +atl --context prod jira sprint start 123 --duration 14d +atl --context prod jira sprint close 123 # prompts unless --force +atl --context prod jira sprint list --board 42 [--state active,future,closed] +atl --context prod jira sprint move NX-1 NX-2 --to 123 # or --sprint "name" --board 42 +atl --context prod jira sprint backlog NX-1 # Comments (subcommand pattern, supports Markdown) -atl jira issue comment list PROJ-1234 # List comments -atl jira issue comment add PROJ-1234 --body "Comment with **bold** and \`code\`" -atl jira issue comment edit PROJ-1234 --id --body "Updated text" -atl jira issue comment delete PROJ-1234 --id +atl --context prod jira issue comment list PROJ-1234 # List comments +atl --context prod jira issue comment add PROJ-1234 --body "Comment with **bold** and \`code\`" +atl --context prod jira issue comment edit PROJ-1234 --id --body "Updated text" +atl --context prod jira issue comment delete PROJ-1234 --id # Attachments -atl jira issue attachment PROJ-1234 --list # List attachments -atl jira issue attachment PROJ-1234 --download # Download attachment +atl --context prod jira issue attachment PROJ-1234 --list # List attachments +atl --context prod jira issue attachment PROJ-1234 --download # Download attachment # Metadata discovery -atl jira issue types --project PROJ # List issue types -atl jira issue priorities # List available priorities -atl jira issue fields --search "story points" # Search for field by name -atl jira issue fields --custom --json # List all custom fields -atl jira issue field-options --project PROJ --type Bug # Allowed values for select/radio fields -atl jira issue field-options --project PROJ --type Bug --field "Repo" # Specific field options -atl jira issue field-options --project PROJ --type Bug --field security # Security levels (for --security) +atl --context prod jira issue types --project PROJ # List issue types +atl --context prod jira issue priorities # List available priorities +atl --context prod jira issue fields --search "story points" # Search for field by name +atl --context prod jira issue fields --custom --json # List all custom fields +atl --context prod jira issue field-options --project PROJ --type Bug # Allowed values for select/radio fields +atl --context prod jira issue field-options --project PROJ --type Bug --field "Repo" # Specific field options +atl --context prod jira issue field-options --project PROJ --type Bug --field security # Security levels (for --security) # Read-only REST passthrough (atl v1.12.0+; GET only, path relative to /rest/api/3) -atl jira api GET issue/PROJ-1234/editmeta # Endpoints atl doesn't model -atl jira api project/PROJ/securitylevel # Method arg optional; defaults to GET +atl --context prod jira api GET issue/PROJ-1234/editmeta # Endpoints atl doesn't model +atl --context prod jira api project/PROJ/securitylevel # Method arg optional; defaults to GET # Board sorting / ranking -atl jira issue list --jql 'project = PROJ AND statusCategory = Done ORDER BY statuscategorychangedate DESC' --limit 50 --json -atl jira board rank PROJ-124 PROJ-125 --after PROJ-123 --board-id # Rank relative to another issue +atl --context prod jira issue list --jql 'project = PROJ AND statusCategory = Done ORDER BY statuscategorychangedate DESC' --limit 50 --json +atl --context prod jira board rank PROJ-124 PROJ-125 --after PROJ-123 --board-id # Rank relative to another issue \`\`\` **Board ranking tips**: @@ -292,58 +322,58 @@ atl jira board rank PROJ-124 PROJ-125 --after PROJ-123 --board-id # \`\`\`bash # Spaces -atl confluence space list # List all spaces -atl confluence space list --all # Fetch all (follows pagination) +atl --context prod confluence space list # List all spaces +atl --context prod confluence space list --all # Fetch all (follows pagination) # View pages -atl confluence page view # View by ID -atl confluence page view -s DOCS -t "Title" # View by space + exact title -atl confluence page view --raw # Get storage format (XHTML) -atl confluence page view --web # Open in browser +atl --context prod confluence page view # View by ID +atl --context prod confluence page view -s DOCS -t "Title" # View by space + exact title +atl --context prod confluence page view --raw # Get storage format (XHTML) +atl --context prod confluence page view --web # Open in browser # List pages -atl confluence page list -s DOCS # List pages in space -atl confluence page list -s DOCS --status draft # List drafts -atl confluence page list -s DOCS --status archived # List archived -atl confluence page list -s DOCS --all # Fetch all pages +atl --context prod confluence page list -s DOCS # List pages in space +atl --context prod confluence page list -s DOCS --status draft # List drafts +atl --context prod confluence page list -s DOCS --status archived # List archived +atl --context prod confluence page list -s DOCS --all # Fetch all pages # Search (uses v1 API - different scopes than v2) -atl confluence page search -q "term" # Search by title -atl confluence page search -q "term" -s DOCS # Search within space -atl confluence page search --cql "ancestor = " # Search in hierarchy -atl confluence page search --cql "parent = " # Direct children only -atl confluence page search --cql "type = page AND text ~ 'keyword'" +atl --context prod confluence page search -q "term" # Search by title +atl --context prod confluence page search -q "term" -s DOCS # Search within space +atl --context prod confluence page search --cql "ancestor = " # Search in hierarchy +atl --context prod confluence page search --cql "parent = " # Direct children only +atl --context prod confluence page search --cql "type = page AND text ~ 'keyword'" # Create and edit -atl confluence page create -s DOCS -t "Title" -b "

Content

" -atl confluence page create -s DOCS -t "Title" --parent # Child page -atl confluence page create -s DOCS -t "Title" --draft # Create as draft -atl confluence page edit --title "New Title" -atl confluence page edit --body "

New content

" +atl --context prod confluence page create -s DOCS -t "Title" -b "

Content

" +atl --context prod confluence page create -s DOCS -t "Title" --parent # Child page +atl --context prod confluence page create -s DOCS -t "Title" --draft # Create as draft +atl --context prod confluence page edit --title "New Title" +atl --context prod confluence page edit --body "

New content

" # Hierarchy navigation -atl confluence page children # List immediate children -atl confluence page children --descendants # All descendants with depth -atl confluence page children --type folder # Only folders -atl confluence page children --type page # Only pages +atl --context prod confluence page children # List immediate children +atl --context prod confluence page children --descendants # All descendants with depth +atl --context prod confluence page children --type folder # Only folders +atl --context prod confluence page children --type page # Only pages # Move pages -atl confluence page move --target # Move as child of target -atl confluence page move --target --position before # Reorder siblings -atl confluence page move --target --position after -atl confluence page move --space NEWSPACE # Move to different space +atl --context prod confluence page move --target # Move as child of target +atl --context prod confluence page move --target --position before # Reorder siblings +atl --context prod confluence page move --target --position after +atl --context prod confluence page move --space NEWSPACE # Move to different space # Archive and delete -atl confluence page archive # Archive page -atl confluence page delete --force # Delete (skip confirmation) -atl confluence page publish # Publish draft +atl --context prod confluence page archive # Archive page +atl --context prod confluence page delete --force # Delete (skip confirmation) +atl --context prod confluence page publish # Publish draft # Templates (v1 API - requires Space Admin or Confluence Admin) -atl confluence template view # View template -atl confluence template view --raw # View raw storage format -atl confluence template create -s DOCS --name "Meeting Notes" --body "

Notes

" -atl confluence template create --name "Global Template" --body "

Content

" # Global (admin only) -atl confluence template update --name "New Name" --body "

Updated

" +atl --context prod confluence template view # View template +atl --context prod confluence template view --raw # View raw storage format +atl --context prod confluence template create -s DOCS --name "Meeting Notes" --body "

Notes

" +atl --context prod confluence template create --name "Global Template" --body "

Content

" # Global (admin only) +atl --context prod confluence template update --name "New Name" --body "

Updated

" \`\`\` **API version notes**: @@ -375,11 +405,11 @@ Folders are distinct from pages (containers without content, not pages with chil **To rename a folder** (workaround - create new, move children, delete old): \`\`\`bash # Create new page that will become the folder -atl confluence page create -s DOCS --parent -t "New Name" -b "

Folder

" +atl --context prod confluence page create -s DOCS --parent -t "New Name" -b "

Folder

" # Move each child to new parent -atl confluence page move --target +atl --context prod confluence page move --target # Delete old folder -atl confluence page delete --force +atl --context prod confluence page delete --force \`\`\` ### Jira Formatting (Extended Markdown via CLI) @@ -430,7 +460,7 @@ Hidden content that can be expanded **Output format:** - When viewing issues, descriptions render as Markdown - Embedded images show as \`[Image: filename]\` placeholders -- Use \`atl jira issue attachment PROJ-1234 --list\` to see attachments +- Use \`atl --context prod jira issue attachment PROJ-1234 --list\` to see attachments **Important**: When a Jira issue description contains image references (e.g., \`[Image: filename.png]\`), always download and inspect attachments to understand the full context. Visual information is often essential to understanding requirements. @@ -438,8 +468,8 @@ Hidden content that can be expanded **Textarea custom fields** (\`--field\`): Automatically converts Markdown to ADF. Use literal \`\\n\` for newlines: \`\`\`bash -atl jira issue edit PROJ-1234 --field 'Kontext=Line 1\\n\\nLine 2\\n- Bullet A\\n- Bullet B' -atl jira issue edit PROJ-1234 --field 'Kontext=+++Expand Title\\nHidden content\\n+++' +atl --context prod jira issue edit PROJ-1234 --field 'Kontext=Line 1\\n\\nLine 2\\n- Bullet A\\n- Bullet B' +atl --context prod jira issue edit PROJ-1234 --field 'Kontext=+++Expand Title\\nHidden content\\n+++' \`\`\` **Code names with underscores**: Use backticks in descriptions (\`MY_TABLE_NAME\`). Bare underscores render as italic (\`MY_TABLE_NAME\` → MY*TABLE*NAME). Backslash-escaping (\`MY\\_TABLE\\_NAME\`) renders backslashes literally. @@ -457,7 +487,7 @@ Plain \`--field "Name=value"\` is string-only. Complex Jira field types need \`- | Multi-select | \`"Name": [{"value": "A"}, {"value": "B"}]\` | No | | Issue security level | use \`--security ""\` instead | No — \`--field\` can't set it | -**Issue security level** has a dedicated flag (atl v1.12.0+) — don't reach for \`--field-file\`. Set it on create/edit with \`--security "Developer only"\` (name or numeric id); \`--security ""\` on edit clears it. Discover a project's levels with \`atl jira issue field-options --project PROJ --type Bug --field security\`. +**Issue security level** has a dedicated flag (atl v1.12.0+) — don't reach for \`--field-file\`. Set it on create/edit with \`--security "Developer only"\` (name or numeric id); \`--security ""\` on edit clears it. Discover a project's levels with \`atl --context prod jira issue field-options --project PROJ --type Bug --field security\`. Example combining labels + select + radio (placeholder field names — the actual fields and allowed values depend on your project's schema, not on this example): @@ -469,10 +499,10 @@ Example combining labels + select + radio (placeholder field names — the actua } \`\`\` -Apply with \`atl jira issue edit PROJ-123 --field-file fields.json\` or include in \`atl jira issue transition\`. Discover allowed values for any select/radio field before guessing: +Apply with \`atl --context prod jira issue edit PROJ-123 --field-file fields.json\` or include in \`atl --context prod jira issue transition\`. Discover allowed values for any select/radio field before guessing: \`\`\`bash -atl jira issue field-options --project PROJ --type Bug --field "Severity" +atl --context prod jira issue field-options --project PROJ --type Bug --field "Severity" \`\`\` ### Markdown→ADF Converter Hang Traps @@ -492,7 +522,7 @@ Workaround: flatten nested code into inline single-backtick fragments; replace \ ### Service Desk Comment De-duplication -\`atl jira issue comment add\` against an SD ticket can return success with a comment ID that never actually surfaces in the UI. Always verify with \`atl jira issue comment list \` before assuming a post landed. +\`atl --context prod jira issue comment add\` against an SD ticket can return success with a comment ID that never actually surfaces in the UI. Always verify with \`atl --context prod jira issue comment list \` before assuming a post landed. ### Underlying Jira API Notes @@ -504,7 +534,7 @@ Workaround: flatten nested code into inline single-backtick fragments; replace \ ### Jira Workflow Transitions -Transition names vary by Jira instance and language. Use \`atl jira issue transition PROJ-123 --list\` to see available transitions for a specific issue. +Transition names vary by Jira instance and language. Use \`atl --context prod jira issue transition PROJ-123 --list\` to see available transitions for a specific issue. ### Confluence Formatting (HTML) @@ -1132,6 +1162,10 @@ ${rows} 2. Show the context name and query to the user 3. Ask for explicit confirmation before executing +**Safety**: Every Jira, Confluence, or Assets operation must pass an explicit +\`atl --context \`. Never rely on or mutate atl's shared +persistent context from an agent session. + **Safety**: Before executing any M365 write or delete operation (add, set, remove, copy, move): 1. Show the full command and target URL to the user 2. Ask for explicit confirmation before executing diff --git a/package-lock.json b/package-lock.json index 20162ba..c2b0c57 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "llm-cli-setup", - "version": "1.12.0", + "name": "@enthus-appdev/llm-cli-setup", + "version": "1.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "llm-cli-setup", - "version": "1.12.0", + "name": "@enthus-appdev/llm-cli-setup", + "version": "1.13.0", "license": "MIT", "dependencies": { "chalk": "^5.3.0", @@ -559,6 +559,7 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -604,16 +605,16 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/chalk": { @@ -702,6 +703,7 @@ "integrity": "sha512-5KeEOJZBfEVA47boFiBsf+6MmmJpffM7qEBg4pLla2e4nlKgdKlqCW0oSLOGsT8Wl5uCGJptLV1bkaiShj90Gw==", "dev": true, "license": "MIT", + "peer": true, "workspaces": [ "packages/*" ], diff --git a/package.json b/package.json index 31a9a31..c05d911 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@enthus-appdev/llm-cli-setup", - "version": "1.12.0", + "version": "1.13.0", "description": "CLI tools setup with LLM integration - installs and configures sqlcmd, gh, atl, n8nctl, gcx, m365, esq, discordctl, playwright, and hcloud with Claude Code, Antigravity, and Codex support", "type": "module", "main": "./lib/index.js", @@ -15,6 +15,7 @@ }, "scripts": { "start": "node bin/cli.js", + "test": "node --test", "lint": "eslint .", "format": "prettier --write .", "check-format": "prettier --check ." diff --git a/test/atl.test.js b/test/atl.test.js new file mode 100644 index 0000000..cf463f3 --- /dev/null +++ b/test/atl.test.js @@ -0,0 +1,71 @@ +import assert from 'node:assert/strict'; +import { describe, test } from 'node:test'; + +import { + loginAtlTargets, + parseAtlAuthStatus, + unauthenticatedAtlTargets, +} from '../lib/installers/atl.js'; + +describe('ATL authentication helpers', () => { + test('parseAtlAuthStatus requires the scoped host to be authenticated', () => { + assert.equal(parseAtlAuthStatus('[{"authenticated":true}]', 'sandbox'), true); + assert.equal(parseAtlAuthStatus('[{"authenticated":false}]', 'sandbox'), false); + assert.equal(parseAtlAuthStatus('[]', 'sandbox'), false); + assert.equal(parseAtlAuthStatus('{"authenticated":true}', 'sandbox'), false); + }); + + test('parseAtlAuthStatus accepts any authenticated host for the legacy bare call', () => { + const output = '[{"authenticated":false},{"authenticated":true}]'; + assert.equal(parseAtlAuthStatus(output), true); + }); + + test('unauthenticatedAtlTargets skips hosts with valid tokens', () => { + const targets = unauthenticatedAtlTargets( + ['sandbox', 'prod', 'staging'], + (hostname) => hostname !== 'sandbox' + ); + + assert.deepEqual(targets, ['sandbox']); + }); + + test('loginAtlTargets continues after a failed host and reports it', () => { + const attempted = []; + const failures = loginAtlTargets('atl', ['sandbox', 'prod'], { + login: (_binary, args) => { + const hostname = args.at(-1); + attempted.push(hostname); + if (hostname === 'sandbox') throw new Error('cancelled'); + }, + isAuthenticated: (hostname) => hostname === 'prod', + }); + + assert.deepEqual(attempted, ['sandbox', 'prod']); + assert.deepEqual(failures, ['sandbox']); + }); + + test('loginAtlTargets verifies each successful login', () => { + const checked = []; + const failures = loginAtlTargets('atl', ['sandbox', 'prod'], { + login: () => {}, + isAuthenticated: (hostname) => { + checked.push(hostname); + return hostname === 'sandbox'; + }, + }); + + assert.deepEqual(checked, ['sandbox', 'prod']); + assert.deepEqual(failures, ['prod']); + }); + + test('loginAtlTargets preserves the default-host login path', () => { + const calls = []; + const failures = loginAtlTargets('atl', [''], { + login: (binary, args) => calls.push([binary, args]), + isAuthenticated: () => true, + }); + + assert.deepEqual(calls, [['atl', ['auth', 'login']]]); + assert.deepEqual(failures, []); + }); +});