orizu 0.5.24 → 0.5.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +23 -3
  2. package/dist/credentials.d.ts +15 -0
  3. package/dist/credentials.d.ts.map +1 -1
  4. package/dist/credentials.js +279 -59
  5. package/dist/help.d.ts.map +1 -1
  6. package/dist/help.js +5 -2
  7. package/dist/http.d.ts +20 -0
  8. package/dist/http.d.ts.map +1 -1
  9. package/dist/http.js +239 -19
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +153 -213
  12. package/dist/keyboard-select.d.ts +7 -0
  13. package/dist/keyboard-select.d.ts.map +1 -0
  14. package/dist/keyboard-select.js +72 -0
  15. package/dist/logout-diagnostic.d.ts +4 -0
  16. package/dist/logout-diagnostic.d.ts.map +1 -0
  17. package/dist/logout-diagnostic.js +26 -0
  18. package/dist/setup-account.d.ts +11 -0
  19. package/dist/setup-account.d.ts.map +1 -0
  20. package/dist/setup-account.js +106 -0
  21. package/dist/setup-onboarding.d.ts +27 -1
  22. package/dist/setup-onboarding.d.ts.map +1 -1
  23. package/dist/setup-onboarding.js +95 -11
  24. package/dist/setup-summary.d.ts +12 -0
  25. package/dist/setup-summary.d.ts.map +1 -0
  26. package/dist/setup-summary.js +56 -0
  27. package/package.json +1 -1
  28. package/src/credentials.ts +326 -57
  29. package/src/help.ts +7 -2
  30. package/src/http.ts +325 -20
  31. package/src/index.ts +193 -279
  32. package/src/keyboard-select.ts +97 -0
  33. package/src/logout-diagnostic.ts +39 -0
  34. package/src/setup-account.ts +129 -0
  35. package/src/setup-onboarding.ts +133 -10
  36. package/src/setup-summary.ts +77 -0
  37. package/vendor/skills/orizu/SKILL.md +51 -111
  38. package/vendor/skills/orizu/flows/expand.md +27 -0
  39. package/vendor/skills/orizu/flows/first-win.md +31 -0
  40. package/vendor/skills/orizu/flows/onboard.md +31 -0
  41. package/vendor/skills/orizu/flows/promote.md +33 -0
  42. package/vendor/skills/orizu/flows/recurse.md +23 -0
  43. package/vendor/skills/orizu/flows/triage.md +37 -0
  44. package/vendor/skills/orizu/references/assess-and-plan.md +5 -5
  45. package/vendor/skills/orizu/references/authority-map.md +20 -0
  46. package/vendor/skills/orizu/references/building-apps.md +7 -1
  47. package/vendor/skills/orizu/references/building-judges.md +15 -16
  48. package/vendor/skills/orizu/references/cli-reference.md +17 -8
  49. package/vendor/skills/orizu/references/dataset-design.md +16 -6
  50. package/vendor/skills/orizu/references/eval-strategy.md +18 -15
  51. package/vendor/skills/orizu/references/instructions-after-prompts.md +2 -2
  52. package/vendor/skills/orizu/references/migrate-existing-gepa-setup.md +10 -6
  53. package/vendor/skills/orizu/references/optimization-reports.md +11 -11
  54. package/vendor/skills/orizu/references/optimization-with-gepa.md +4 -2
  55. package/vendor/skills/orizu/references/primer.md +18 -22
  56. package/vendor/skills/orizu/references/prompt-control-plane.md +5 -5
  57. package/vendor/skills/orizu/references/vocabulary.md +27 -0
package/src/index.ts CHANGED
@@ -5,7 +5,6 @@ import { fileURLToPath } from 'url'
5
5
  import { createServer } from 'http'
6
6
  import { gunzipSync, gzipSync } from 'zlib'
7
7
  import {
8
- existsSync,
9
8
  mkdirSync,
10
9
  mkdtempSync,
11
10
  readFileSync,
@@ -15,7 +14,7 @@ import {
15
14
  writeFileSync,
16
15
  } from 'fs'
17
16
  import { spawn, spawnSync } from 'child_process'
18
- import { homedir, tmpdir } from 'os'
17
+ import { tmpdir } from 'os'
19
18
  import { createInterface } from 'readline/promises'
20
19
  import { emitKeypressEvents } from 'readline'
21
20
  import { stdin as input, stdout as output } from 'process'
@@ -33,6 +32,10 @@ import { parseDatasetReference } from './dataset-download.js'
33
32
  import { editDatasetRows as runEditDatasetRows } from './dataset-edit-rows.js'
34
33
  import { ensureDatasetUploadSnapshot } from './dataset-upload-snapshot.js'
35
34
  import { extractErrorMessage } from './error-response.js'
35
+ import {
36
+ describeLogoutHttpFailure,
37
+ describeLogoutTransportFailure,
38
+ } from './logout-diagnostic.js'
36
39
  import { parseGlobalFlags } from './global-flags.js'
37
40
  import { shouldUseHeadlessLogin, waitForHeadlessAuthorization } from './headless-login.js'
38
41
  import { getCapabilities, renderHelpForArgs, renderRootHelp } from './help.js'
@@ -41,10 +44,12 @@ import { readAssignmentManifestJsonlFile } from './task-assignment-manifest.js'
41
44
  import {
42
45
  assertSecureTokenTransport,
43
46
  authedFetch,
47
+ captureAuthenticatedRequestContext,
44
48
  credentialRequestRedirectPolicy,
45
49
  getBaseUrl,
46
50
  resolveLoginBaseUrl,
47
51
  setGlobalFlags,
52
+ type AuthenticatedRequestContext,
48
53
  } from './http.js'
49
54
  import {
50
55
  computeSkillContentHash,
@@ -57,7 +62,6 @@ import {
57
62
  resolveSkillSource,
58
63
  SKILL_INSTALL_AGENTS,
59
64
  SKILL_INSTALL_TARGETS,
60
- SkillInstallAgent,
61
65
  SkillInstallMode,
62
66
  SkillInstallScope,
63
67
  SkillInstallTarget,
@@ -67,7 +71,20 @@ import {
67
71
  import { renderBanner } from './banner.js'
68
72
  import { formatTaskCreateError } from './task-create-error.js'
69
73
  import { renderAgentSetupPrompt } from './setup-prompt.js'
70
- import { findExecutable, LAUNCHABLE_AGENTS, setupLaunchPrompt, validateSetupAgentArgs, validateSetupFlagValues, validateSetupSelectionArgs } from './setup-onboarding.js'
74
+ import {
75
+ AGENT_LAUNCH_SPECS,
76
+ detectedSetupSkillChoices,
77
+ findExecutable,
78
+ resolveSetupSkillHome,
79
+ setupLaunchPrompt,
80
+ setupNativeTargetForAgent,
81
+ validateSetupAgentArgs,
82
+ validateSetupFlagValues,
83
+ validateSetupSelectionArgs,
84
+ } from './setup-onboarding.js'
85
+ import { promptKeyboardSelect } from './keyboard-select.js'
86
+ import { confirmExistingSetupAccount, describeSetupAuthState } from './setup-account.js'
87
+ import { renderSetupSkillSummary, sanitizeSetupHumanInlineText } from './setup-summary.js'
71
88
  import { LoginResponse } from './types.js'
72
89
  import {
73
90
  getWorkspaceRoot,
@@ -666,24 +683,26 @@ async function promptSelect<T>(
666
683
  }
667
684
  }
668
685
 
669
- async function fetchTeams(): Promise<Team[]> {
670
- const response = await authedFetch('/api/cli/teams')
686
+ type AuthenticatedRequest = AuthenticatedRequestContext['fetch']
687
+ async function fetchTeams(request: AuthenticatedRequest = authedFetch): Promise<Team[]> {
688
+ const response = await request('/api/cli/teams')
671
689
  if (!response.ok) {
672
690
  throw new Error(`Failed to fetch teams: ${await response.text()}`)
673
691
  }
674
-
675
692
  const data = await parseJsonResponse<{ teams: Team[] }>(response, 'Teams list')
676
693
  return data.teams
677
694
  }
678
695
 
679
- async function fetchProjects(teamSlug?: string): Promise<Project[]> {
696
+ async function fetchProjects(
697
+ teamSlug?: string,
698
+ request: AuthenticatedRequest = authedFetch
699
+ ): Promise<Project[]> {
680
700
  const normalizedTeamSlug = teamSlug ? normalizeSlugInput(teamSlug) : undefined
681
701
  const query = normalizedTeamSlug ? `?teamSlug=${encodeURIComponent(normalizedTeamSlug)}` : ''
682
- const response = await authedFetch(`/api/cli/projects${query}`)
702
+ const response = await request(`/api/cli/projects${query}`)
683
703
  if (!response.ok) {
684
704
  throw new Error(`Failed to fetch projects: ${await response.text()}`)
685
705
  }
686
-
687
706
  const data = await parseJsonResponse<{ projects: Project[] }>(response, 'Projects list')
688
707
  return data.projects
689
708
  }
@@ -736,32 +755,35 @@ async function fetchTeamMembers(teamSlug: string): Promise<TeamMember[]> {
736
755
  return data.members
737
756
  }
738
757
 
739
- async function createTeamOnServer(name: string): Promise<Team> {
740
- const response = await authedFetch('/api/cli/teams', {
758
+ async function createTeamOnServer(
759
+ name: string,
760
+ request: AuthenticatedRequest = authedFetch
761
+ ): Promise<Team> {
762
+ const response = await request('/api/cli/teams', {
741
763
  method: 'POST',
742
764
  headers: { 'Content-Type': 'application/json' },
743
765
  body: JSON.stringify({ name }),
744
766
  })
745
-
746
767
  if (!response.ok) {
747
768
  throw new Error(`Failed to create team: ${await response.text()}`)
748
769
  }
749
-
750
770
  const data = await parseJsonResponse<{ team: Team }>(response, 'Team create')
751
771
  return data.team
752
772
  }
753
773
 
754
- async function createProjectOnServer(teamSlug: string, name: string): Promise<Project> {
755
- const response = await authedFetch('/api/cli/projects', {
774
+ async function createProjectOnServer(
775
+ teamSlug: string,
776
+ name: string,
777
+ request: AuthenticatedRequest = authedFetch
778
+ ): Promise<Project> {
779
+ const response = await request('/api/cli/projects', {
756
780
  method: 'POST',
757
781
  headers: { 'Content-Type': 'application/json' },
758
782
  body: JSON.stringify({ teamSlug, name }),
759
783
  })
760
-
761
784
  if (!response.ok) {
762
785
  throw new Error(`Failed to create project: ${await response.text()}`)
763
786
  }
764
-
765
787
  const data = await parseJsonResponse<{
766
788
  project: {
767
789
  id: string
@@ -773,7 +795,6 @@ async function createProjectOnServer(teamSlug: string, name: string): Promise<Pr
773
795
  role?: string
774
796
  }
775
797
  }>(response, 'Project create')
776
-
777
798
  return {
778
799
  id: data.project.id,
779
800
  name: data.project.name,
@@ -1052,8 +1073,9 @@ function resolveAuthCallbackPort(): number {
1052
1073
  return parsed
1053
1074
  }
1054
1075
 
1055
- async function login() {
1056
- const baseUrl = hasArg('--no-prompt-if-logged-in') ? getBaseUrl() : resolveLoginBaseUrl()
1076
+ async function login(baseUrlOverride?: string) {
1077
+ const baseUrl = baseUrlOverride
1078
+ ?? (hasArg('--no-prompt-if-logged-in') ? getBaseUrl() : resolveLoginBaseUrl())
1057
1079
  assertSecureTokenTransport(baseUrl)
1058
1080
 
1059
1081
  // hasResolvableAuth (ALI-1090): an env bearer (ORIZU_TOKEN / ORIZU_TOKEN_FILE)
@@ -1184,7 +1206,6 @@ async function whoami() {
1184
1206
  if (!response.ok) {
1185
1207
  throw new Error(`whoami failed: ${await response.text()}`)
1186
1208
  }
1187
-
1188
1209
  const data = await response.json() as { user: { id: string; email: string | null } }
1189
1210
  if (hasJsonFlag()) {
1190
1211
  printJson({ user: data.user, server: getBaseUrl() })
@@ -1193,8 +1214,8 @@ async function whoami() {
1193
1214
  printLine(sanitizeTerminalText(data.user.email ?? data.user.id))
1194
1215
  }
1195
1216
 
1196
- async function logout() {
1197
- const baseUrl = getBaseUrl()
1217
+ async function logout(baseUrlOverride?: string) {
1218
+ const baseUrl = baseUrlOverride || getBaseUrl()
1198
1219
  const credentials = getServerCredentials(baseUrl)
1199
1220
  if (!credentials) {
1200
1221
  if (hasJsonFlag()) {
@@ -1204,7 +1225,13 @@ async function logout() {
1204
1225
  printLine(`Already logged out for ${sanitizeTerminalText(baseUrl)}.`)
1205
1226
  return
1206
1227
  }
1207
-
1228
+ const authorizationToken = 'accessToken' in credentials
1229
+ ? credentials.accessToken
1230
+ : credentials.apiKey
1231
+ const logoutSecrets = [
1232
+ authorizationToken,
1233
+ ...('refreshToken' in credentials ? [credentials.refreshToken] : []),
1234
+ ]
1208
1235
  let remoteLogoutError: string | null = null
1209
1236
  try {
1210
1237
  assertSecureTokenTransport(baseUrl)
@@ -1212,22 +1239,19 @@ async function logout() {
1212
1239
  method: 'POST',
1213
1240
  headers: {
1214
1241
  'Content-Type': 'application/json',
1215
- Authorization: `Bearer ${'accessToken' in credentials ? credentials.accessToken : credentials.apiKey}`,
1242
+ Authorization: `Bearer ${authorizationToken}`,
1216
1243
  },
1217
1244
  body: 'refreshToken' in credentials
1218
1245
  ? JSON.stringify({ refreshToken: credentials.refreshToken })
1219
1246
  : undefined,
1220
1247
  ...credentialRequestRedirectPolicy(),
1221
1248
  })
1222
-
1223
1249
  if (!response.ok) {
1224
- remoteLogoutError = sanitizeTerminalText(await response.text()).slice(0, 180)
1250
+ remoteLogoutError = await describeLogoutHttpFailure(response, logoutSecrets)
1225
1251
  }
1226
1252
  } catch (error) {
1227
- const message = error instanceof Error ? error.message : String(error)
1228
- remoteLogoutError = sanitizeTerminalText(message).slice(0, 180)
1253
+ remoteLogoutError = describeLogoutTransportFailure(error, logoutSecrets)
1229
1254
  }
1230
-
1231
1255
  clearServerCredentials(baseUrl)
1232
1256
  if (hasJsonFlag()) {
1233
1257
  printJson({ status: 'logged-out', server: baseUrl, remoteLogoutError })
@@ -2754,87 +2778,6 @@ async function waitForAnyKeyOrEscape(message: string): Promise<boolean> {
2754
2778
  })
2755
2779
  }
2756
2780
 
2757
- async function promptKeyboardSelect<T>(
2758
- title: string,
2759
- items: T[],
2760
- label: (item: T, index: number) => string
2761
- ): Promise<T> {
2762
- if (items.length === 0) {
2763
- throw new Error(`No options available for ${title.toLowerCase()}`)
2764
- }
2765
- if (!isInteractiveTerminal()) {
2766
- throw new Error(`${title} selection requires interactive terminal. Provide flags explicitly instead.`)
2767
- }
2768
-
2769
- printLine(`\n${sanitizeTerminalText(title)}`)
2770
- printLine('Use ↑/↓ to choose, Enter to confirm, or Esc to cancel.')
2771
-
2772
- let selected = 0
2773
- let rendered = false
2774
- const rawInput = input as RawInput
2775
- const wasRaw = Boolean(rawInput.isRaw)
2776
- emitKeypressEvents(input)
2777
-
2778
- function render() {
2779
- if (rendered) {
2780
- output.write(`\x1b[${items.length}A`)
2781
- }
2782
- items.forEach((item, index) => {
2783
- output.write('\x1b[2K')
2784
- const marker = index === selected ? '●' : '○'
2785
- output.write(` ${marker} ${sanitizeTerminalText(label(item, index))}\n`)
2786
- })
2787
- rendered = true
2788
- }
2789
-
2790
- render()
2791
- output.write('\x1b[?25l')
2792
-
2793
- return new Promise((resolvePromise, rejectPromise) => {
2794
- function cleanup() {
2795
- input.off('keypress', onKeypress)
2796
- if (rawInput.setRawMode && !wasRaw) {
2797
- rawInput.setRawMode(false)
2798
- }
2799
- input.pause()
2800
- output.write('\x1b[?25h')
2801
- printLine('')
2802
- }
2803
-
2804
- function onKeypress(_chunk: string, key: KeypressInfo) {
2805
- if (key.ctrl && key.name === 'c') {
2806
- cleanup()
2807
- rejectPromise(new Error('Setup cancelled.'))
2808
- return
2809
- }
2810
- if (key.name === 'escape') {
2811
- cleanup()
2812
- rejectPromise(new Error('Setup cancelled.'))
2813
- return
2814
- }
2815
- if (key.name === 'up') {
2816
- selected = (selected - 1 + items.length) % items.length
2817
- render()
2818
- return
2819
- }
2820
- if (key.name === 'down') {
2821
- selected = (selected + 1) % items.length
2822
- render()
2823
- return
2824
- }
2825
- if (key.name === 'return' || key.name === 'enter') {
2826
- const item = items[selected]
2827
- cleanup()
2828
- resolvePromise(item)
2829
- }
2830
- }
2831
-
2832
- input.on('keypress', onKeypress)
2833
- input.resume()
2834
- rawInput.setRawMode?.(true)
2835
- })
2836
- }
2837
-
2838
2781
  async function promptKeyboardMultiSelect<T>(
2839
2782
  title: string,
2840
2783
  items: T[],
@@ -2984,14 +2927,14 @@ function writeWorkspaceValidationLog(
2984
2927
 
2985
2928
  function printWorkspaceFindings(findings: { severity: string, code: string, path?: string, message: string }[]) {
2986
2929
  if (findings.length === 0) {
2987
- printLine(' Validation passed.')
2930
+ printLine('Validation passed.')
2988
2931
  return
2989
2932
  }
2990
2933
 
2991
- printLine(' Validation findings:')
2934
+ printLine('Validation findings:')
2992
2935
  for (const finding of findings) {
2993
2936
  const path = finding.path ? ` ${finding.path}` : ''
2994
- printLine(` - ${finding.severity.toUpperCase()} ${finding.code}:${path} ${finding.message}`)
2937
+ printLine(`- ${finding.severity.toUpperCase()} ${finding.code}:${path} ${finding.message}`)
2995
2938
  }
2996
2939
  }
2997
2940
 
@@ -3367,15 +3310,6 @@ function skillsPathCommand() {
3367
3310
  printLine(hasArg('--skill-md') ? source.skillMd : source.root)
3368
3311
  }
3369
3312
 
3370
- function describeSetupAuthState(): { state: 'signed-in' | 'signed-out', baseUrl: string } {
3371
- const baseUrl = getBaseUrl()
3372
- return {
3373
- // ALI-1090: an env bearer (ORIZU_TOKEN / ORIZU_TOKEN_FILE) is signed-in too.
3374
- state: hasResolvableAuth(baseUrl) ? 'signed-in' : 'signed-out',
3375
- baseUrl,
3376
- }
3377
- }
3378
-
3379
3313
  type SetupTeamChoice =
3380
3314
  | { kind: 'team', team: Team }
3381
3315
  | { kind: 'create' }
@@ -3385,7 +3319,8 @@ async function resolveSetupTeam(
3385
3319
  createTeamName: string | null,
3386
3320
  noInput: boolean,
3387
3321
  dryRun: boolean,
3388
- workspaceTeamSlug: string | null
3322
+ workspaceTeamSlug: string | null,
3323
+ request: AuthenticatedRequest = authedFetch
3389
3324
  ): Promise<Team> {
3390
3325
  if (teamSlugArg && createTeamName) {
3391
3326
  throw new Error('Use either --team <slug> or --create-team <name>, not both.')
@@ -3397,14 +3332,12 @@ async function resolveSetupTeam(
3397
3332
  if (workspaceTeamSlug) throw new Error(`This directory is already an Orizu workspace for team '${workspaceTeamSlug}'. Create the new team from another directory.`)
3398
3333
  const name = createTeamName.trim()
3399
3334
  if (!name) throw new Error('--create-team requires a non-empty team name.')
3400
- const team = await createTeamOnServer(name)
3401
- printLine(` Created team ${sanitizeTerminalText(team.slug)}`)
3335
+ const team = await createTeamOnServer(name, request)
3336
+ printLine(`Created team ${sanitizeTerminalText(team.slug)}`)
3402
3337
  return team
3403
3338
  }
3404
-
3405
- const teams = await fetchTeams()
3339
+ const teams = await fetchTeams(request)
3406
3340
  const normalizedTeamSlug = teamSlugArg ? normalizeSlugInput(teamSlugArg) : null
3407
-
3408
3341
  if (normalizedTeamSlug) {
3409
3342
  const team = teams.find(candidate => candidate.slug === normalizedTeamSlug)
3410
3343
  if (!team) {
@@ -3412,11 +3345,9 @@ async function resolveSetupTeam(
3412
3345
  }
3413
3346
  return team
3414
3347
  }
3415
-
3416
3348
  if (noInput || dryRun) {
3417
3349
  throw new Error('Non-interactive setup requires --team <slug> or --create-team <name>.')
3418
3350
  }
3419
-
3420
3351
  const choices: SetupTeamChoice[] = [
3421
3352
  ...teams.map(team => ({ kind: 'team' as const, team })),
3422
3353
  ...(teams.some(team => team.role === 'agent') ? [] : [{ kind: 'create' as const }]),
@@ -3428,18 +3359,16 @@ async function resolveSetupTeam(
3428
3359
  ? 'Create a new team'
3429
3360
  : `${item.team.name} (${item.team.slug})`
3430
3361
  )
3431
-
3432
3362
  if (choice.kind === 'team') {
3433
3363
  return choice.team
3434
3364
  }
3435
-
3436
- const name = await askText(' Team name?', '')
3365
+ const name = await askText('Team name?', '')
3437
3366
  if (!name) {
3438
3367
  throw new Error('Team name is required to create a team.')
3439
3368
  }
3440
3369
  if (workspaceTeamSlug) throw new Error(`This directory is already an Orizu workspace for team '${workspaceTeamSlug}'. Create the new team from another directory.`)
3441
- const team = await createTeamOnServer(name)
3442
- printLine(` Created team ${sanitizeTerminalText(team.slug)}`)
3370
+ const team = await createTeamOnServer(name, request)
3371
+ printLine(`Created team ${sanitizeTerminalText(team.slug)}`)
3443
3372
  return team
3444
3373
  }
3445
3374
 
@@ -3456,7 +3385,8 @@ async function resolveSetupProjects(
3456
3385
  projectSlugArg: string | null,
3457
3386
  createProjectName: string | null,
3458
3387
  noInput: boolean,
3459
- dryRun: boolean
3388
+ dryRun: boolean,
3389
+ request: AuthenticatedRequest
3460
3390
  ): Promise<SetupProjectSelection> {
3461
3391
  if (projectSlugArg && createProjectName) {
3462
3392
  throw new Error('Use either --project <slug> or --create-project <name>, not both.')
@@ -3464,23 +3394,21 @@ async function resolveSetupProjects(
3464
3394
  if ((noInput || dryRun) && !projectSlugArg && !createProjectName) {
3465
3395
  throw new Error('Non-interactive setup requires --project <slug> or --create-project <name>.')
3466
3396
  }
3467
-
3468
3397
  const canCreateProject = team.role === 'admin' || team.role === 'agent'
3469
3398
  if (createProjectName && !canCreateProject) {
3470
3399
  throw new Error(`Only team admins can create projects in '${team.slug}'. Choose an existing project or ask a team admin to create one.`)
3471
3400
  }
3472
- const projects = await fetchProjects(team.slug)
3401
+ const projects = await fetchProjects(team.slug, request)
3473
3402
  if (createProjectName) {
3474
3403
  if (dryRun) {
3475
3404
  throw new Error('--create-project cannot be used with --dry-run because setup cannot preview a server-generated project slug.')
3476
3405
  }
3477
3406
  const name = createProjectName.trim()
3478
3407
  if (!name) throw new Error('--create-project requires a non-empty project name.')
3479
- const selected = await createProjectOnServer(team.slug, name)
3480
- printLine(` Created project ${sanitizeTerminalText(`${selected.teamSlug}/${selected.slug}`)}`)
3408
+ const selected = await createProjectOnServer(team.slug, name, request)
3409
+ printLine(`Created project ${sanitizeTerminalText(`${selected.teamSlug}/${selected.slug}`)}`)
3481
3410
  return { selected, projects: [...projects, selected] }
3482
3411
  }
3483
-
3484
3412
  const normalizedProjectSlug = projectSlugArg ? normalizeSlugInput(projectSlugArg) : null
3485
3413
  if (normalizedProjectSlug) {
3486
3414
  const selected = projects.find(project => project.slug === normalizedProjectSlug)
@@ -3489,17 +3417,15 @@ async function resolveSetupProjects(
3489
3417
  }
3490
3418
  return { selected, projects }
3491
3419
  }
3492
-
3493
3420
  if (projects.length === 0) {
3494
3421
  if (!canCreateProject) throw new Error(`Team '${team.slug}' has no accessible projects, and only team admins can create one.`)
3495
- printLine(` Team '${sanitizeTerminalText(team.slug)}' has no projects yet. Let's create the first one.`)
3496
- const name = await askText(' Project name?', '')
3422
+ printLine(`Team '${sanitizeTerminalText(team.slug)}' has no projects yet. Let's create the first one.`)
3423
+ const name = await askText('Project name?', '')
3497
3424
  if (!name) throw new Error('Project name is required to create a project.')
3498
- const selected = await createProjectOnServer(team.slug, name)
3499
- printLine(` Created project ${sanitizeTerminalText(`${selected.teamSlug}/${selected.slug}`)}`)
3425
+ const selected = await createProjectOnServer(team.slug, name, request)
3426
+ printLine(`Created project ${sanitizeTerminalText(`${selected.teamSlug}/${selected.slug}`)}`)
3500
3427
  return { selected, projects: [selected] }
3501
3428
  }
3502
-
3503
3429
  const choices: SetupProjectChoice[] = [
3504
3430
  ...projects.map(project => ({ kind: 'project' as const, project })),
3505
3431
  ...(canCreateProject ? [{ kind: 'create' as const }] : []),
@@ -3514,11 +3440,10 @@ async function resolveSetupProjects(
3514
3440
  if (choice.kind === 'project') {
3515
3441
  return { selected: choice.project, projects }
3516
3442
  }
3517
-
3518
- const name = await askText(' Project name?', '')
3443
+ const name = await askText('Project name?', '')
3519
3444
  if (!name) throw new Error('Project name is required to create a project.')
3520
- const selected = await createProjectOnServer(team.slug, name)
3521
- printLine(` Created project ${sanitizeTerminalText(`${selected.teamSlug}/${selected.slug}`)}`)
3445
+ const selected = await createProjectOnServer(team.slug, name, request)
3446
+ printLine(`Created project ${sanitizeTerminalText(`${selected.teamSlug}/${selected.slug}`)}`)
3522
3447
  return { selected, projects: [...projects, selected] }
3523
3448
  }
3524
3449
 
@@ -3534,47 +3459,6 @@ function formatProjectSetupProgress(count: number): string {
3534
3459
  return `${count} project${count === 1 ? '' : 's'} being set up in workspace...`
3535
3460
  }
3536
3461
 
3537
- interface SetupSkillChoice {
3538
- agent: SkillInstallAgent
3539
- label: string
3540
- target: SkillInstallTarget
3541
- }
3542
-
3543
- const SETUP_NATIVE_SKILL_CHOICES: ReadonlyArray<{
3544
- agent: SkillInstallAgent
3545
- label: string
3546
- directory: string[]
3547
- binary?: string
3548
- }> = [
3549
- { agent: 'claude', label: 'Claude Code', directory: ['.claude'], binary: 'claude' },
3550
- { agent: 'devin', label: 'Devin', directory: ['.devin', 'skills'] },
3551
- { agent: 'droid', label: 'Droid', directory: ['.factory', 'skills'] },
3552
- { agent: 'grok', label: 'Grok Build', directory: ['.grok', 'skills'] },
3553
- { agent: 'windsurf', label: 'Windsurf', directory: ['.windsurf', 'skills'] },
3554
- { agent: 'opencode', label: 'OpenCode', directory: ['.config', 'opencode', 'skills'], binary: 'opencode' },
3555
- ]
3556
-
3557
- function setupNativeTargetForAgent(agent: SkillInstallAgent): SkillInstallTarget | null {
3558
- if (agent === 'codex' || agent === 'pi') return null
3559
- return getTargetForAgent(agent, 'global')
3560
- }
3561
- function resolveSetupSkillHome(): string {
3562
- return resolve(process.env.HOME || homedir())
3563
- }
3564
-
3565
- function detectedSetupSkillChoices(homeDir: string): SetupSkillChoice[] {
3566
- return SETUP_NATIVE_SKILL_CHOICES
3567
- .filter(choice =>
3568
- existsSync(join(homeDir, ...choice.directory))
3569
- || Boolean(choice.binary && findExecutable(choice.binary))
3570
- )
3571
- .map(choice => ({
3572
- agent: choice.agent,
3573
- label: choice.label,
3574
- target: getTargetForAgent(choice.agent, 'global'),
3575
- }))
3576
- }
3577
-
3578
3462
  function setupSkillTargetsFromArgs(): SkillInstallTarget[] {
3579
3463
  const targets: SkillInstallTarget[] = []
3580
3464
  for (const agent of getArgs('--agent')) {
@@ -3626,26 +3510,68 @@ async function setupCommand() {
3626
3510
  if (workspaceManifest.state === 'invalid' && !validateOnly && !fix) throw new Error('The existing orizu.team.json is invalid or unreadable. Repair it before creating remote resources.')
3627
3511
  const workspaceTeamSlug = workspaceManifest.state === 'valid' ? workspaceManifest.slug : null
3628
3512
  if (createTeamArg !== null && workspaceTeamSlug) throw new Error(`This directory is already an Orizu workspace for team '${workspaceTeamSlug}'. Create the new team from another directory.`)
3629
-
3630
3513
  printBannerIfInteractive()
3631
3514
  printLine('Orizu setup')
3632
3515
  printLine('')
3633
-
3634
3516
  // Step 1: login
3635
3517
  printLine('Step 1: Login')
3636
3518
  let auth = describeSetupAuthState()
3519
+ let setupAuthContext: AuthenticatedRequestContext | null = null
3637
3520
  if (hasArg('--skip-login')) {
3638
- printLine(' Skipped (--skip-login).')
3521
+ printLine('Skipped (--skip-login).')
3639
3522
  } else if (auth.state === 'signed-in') {
3640
- printLine(` Already authenticated with ${sanitizeTerminalText(auth.baseUrl)}.`)
3523
+ if (!validateOnly) {
3524
+ setupAuthContext = captureAuthenticatedRequestContext({ allowRefresh: !dryRun })
3525
+ }
3526
+ if (!noInput && !validateOnly) {
3527
+ const confirmation = await confirmExistingSetupAccount(
3528
+ setupAuthContext!,
3529
+ email => promptKeyboardSelect(
3530
+ 'Confirm the account to use for setup',
3531
+ ['continue', 'switch'] as const,
3532
+ item => item === 'continue'
3533
+ ? `Continue as ${email}`
3534
+ : 'Log in with a different account'
3535
+ ),
3536
+ async () => {
3537
+ const context = setupAuthContext!
3538
+ const logoutResult = await context.logout()
3539
+ if (logoutResult.remoteError) {
3540
+ const localOutcome = logoutResult.localCredentialCleared
3541
+ ? 'Local credentials were cleared.'
3542
+ : 'Local credentials were not cleared because a newer login is active.'
3543
+ console.warn(
3544
+ `Warning: remote logout failed: ${logoutResult.remoteError} ${localOutcome}`
3545
+ )
3546
+ } else if (!logoutResult.localCredentialCleared) {
3547
+ console.warn('Warning: local credentials were not cleared because a newer login is active.')
3548
+ }
3549
+ printLine(logoutResult.localCredentialCleared
3550
+ ? `Logged out from ${sanitizeTerminalText(context.baseUrl)}.`
3551
+ : `Kept the newer login for ${sanitizeTerminalText(context.baseUrl)}.`)
3552
+ await login(context.baseUrl)
3553
+ },
3554
+ { dryRun }
3555
+ )
3556
+ if (confirmation === 'switched') {
3557
+ setupAuthContext = captureAuthenticatedRequestContext({ allowRefresh: !dryRun })
3558
+ }
3559
+ auth = { state: 'signed-in', baseUrl: setupAuthContext!.baseUrl }
3560
+ } else {
3561
+ printLine(`Already authenticated with ${sanitizeTerminalText(auth.baseUrl)}.`)
3562
+ }
3641
3563
  } else if (noInput || dryRun) {
3642
- printLine(' Not signed in. Run `orizu login` to authenticate.')
3643
- } else if (await waitForAnyKeyOrEscape(' Press any key to connect your account, or Esc to cancel.')) {
3564
+ printLine('Not signed in. Run `orizu login` to authenticate.')
3565
+ } else if (await waitForAnyKeyOrEscape('Press any key to connect your account, or Esc to cancel.')) {
3644
3566
  await login()
3645
3567
  auth = describeSetupAuthState()
3646
3568
  } else {
3647
3569
  throw new Error('Setup cancelled.')
3648
3570
  }
3571
+ if (auth.state === 'signed-in' && !validateOnly && !setupAuthContext) {
3572
+ setupAuthContext = captureAuthenticatedRequestContext({ allowRefresh: !dryRun })
3573
+ }
3574
+ if (setupAuthContext) auth = { state: 'signed-in', baseUrl: setupAuthContext.baseUrl }
3649
3575
  const serverCreateRequested = createTeamArg !== null || createProjectArg !== null
3650
3576
  if ((noInput || serverCreateRequested) && auth.state === 'signed-out' && !dryRun && !validateOnly && !fix) {
3651
3577
  throw new Error(
@@ -3653,20 +3579,20 @@ async function setupCommand() {
3653
3579
  )
3654
3580
  }
3655
3581
  printLine('')
3656
-
3657
3582
  let setupTeam: Team | null = null
3658
3583
  let selectedProject: Project | null = null
3659
3584
  let setupProjects: Project[] = []
3660
3585
  if (auth.state === 'signed-in' && !validateOnly) {
3661
- setupTeam = await resolveSetupTeam(teamArg, createTeamArg, noInput, dryRun, workspaceTeamSlug)
3586
+ const setupRequest = setupAuthContext!.fetch.bind(setupAuthContext)
3587
+ setupTeam = await resolveSetupTeam(teamArg, createTeamArg, noInput, dryRun, workspaceTeamSlug, setupRequest)
3662
3588
  if (workspaceTeamSlug && setupTeam.slug !== workspaceTeamSlug) {
3663
3589
  throw new Error(`This directory is already an Orizu workspace for team '${workspaceTeamSlug}'. Run setup in another directory to set up team '${setupTeam.slug}'.`)
3664
3590
  }
3665
3591
  if (fix) {
3666
- setupProjects = await fetchProjects(setupTeam.slug)
3592
+ setupProjects = await fetchProjects(setupTeam.slug, setupRequest)
3667
3593
  if (setupProjects.length === 0) throw new Error(`Team '${setupTeam.slug}' has no projects to restore.`)
3668
3594
  } else {
3669
- const selection = await resolveSetupProjects(setupTeam, projectArg, createProjectArg, noInput, dryRun)
3595
+ const selection = await resolveSetupProjects(setupTeam, projectArg, createProjectArg, noInput, dryRun, setupRequest)
3670
3596
  selectedProject = selection.selected
3671
3597
  setupProjects = selection.projects
3672
3598
  printLine(`Selected project ${sanitizeTerminalText(`${setupTeam.slug}/${selectedProject.slug}`)}`)
@@ -3680,7 +3606,7 @@ async function setupCommand() {
3680
3606
  let workspaceResult: ReturnType<typeof initOrizuWorkspace> | null = null
3681
3607
  let validationLogPath: string | null = null
3682
3608
  if (hasArg('--no-workspace') && !validateOnly && !fix) {
3683
- printLine(' Skipped (--no-workspace).')
3609
+ printLine('Skipped (--no-workspace).')
3684
3610
  } else {
3685
3611
  const wantsWorkspace = validateOnly
3686
3612
  || fix
@@ -3693,26 +3619,24 @@ async function setupCommand() {
3693
3619
  const projectSlug = projectArg
3694
3620
  let teamId: string | null = null
3695
3621
  let projects: WorkspaceProjectSeed[] | undefined
3696
-
3697
3622
  if (setupTeam && !validateOnly) {
3698
3623
  teamSlug = setupTeam.slug
3699
3624
  teamId = setupTeam.id
3700
3625
  projects = projectSeedsFromProjects(setupProjects)
3701
- printLine(` ${formatProjectSetupProgress(projects.length)}`)
3626
+ printLine(formatProjectSetupProgress(projects.length))
3702
3627
  } else if (!teamSlug && !noInput && !validateOnly) {
3703
- teamSlug = await askText(' Team slug?', 'local-team')
3628
+ teamSlug = await askText('Team slug?', 'local-team')
3704
3629
  }
3705
3630
  if (auth.state !== 'signed-in' && (!teamSlug || !projectSlug) && noInput && hasArg('--workspace')) {
3706
- printLine(' Using starter local-only team/project. Pass --team with authentication to materialize server projects.')
3631
+ printLine('Using starter local-only team/project. Pass --team with authentication to materialize server projects.')
3707
3632
  }
3708
3633
  if (projectSlug && auth.state !== 'signed-in') {
3709
3634
  projects = [{ slug: projectSlug }]
3710
3635
  }
3711
3636
  if (auth.state !== 'signed-in' && !validateOnly) {
3712
3637
  const localProjectCount = projects?.length || 1
3713
- printLine(` ${formatProjectSetupProgress(localProjectCount)}`)
3638
+ printLine(formatProjectSetupProgress(localProjectCount))
3714
3639
  }
3715
-
3716
3640
  const result = initOrizuWorkspace({
3717
3641
  workspaceRoot,
3718
3642
  teamSlug,
@@ -3732,12 +3656,12 @@ async function setupCommand() {
3732
3656
  workspaceState = result.state
3733
3657
  if (dryRun || verbose) {
3734
3658
  for (const action of result.actions) {
3735
- printLine(` ${dryRun ? 'Would ' : 'Did '}${action}`)
3659
+ printLine(`${dryRun ? 'Would ' : 'Did '}${action}`)
3736
3660
  }
3737
3661
  } else if (result.actions.length > 0) {
3738
- printLine(validateOnly ? ' Workspace validation complete' : ' Workspace setup complete')
3662
+ printLine(validateOnly ? 'Workspace validation complete' : 'Workspace setup complete')
3739
3663
  } else if (!validateOnly) {
3740
- printLine(' Workspace setup complete')
3664
+ printLine('Workspace setup complete')
3741
3665
  }
3742
3666
  validationLogPath = writeWorkspaceValidationLog(result.root, result.findings, dryRun)
3743
3667
  if (verbose) {
@@ -3747,40 +3671,32 @@ async function setupCommand() {
3747
3671
  process.exitCode = 1
3748
3672
  }
3749
3673
  } else {
3750
- printLine(` Skipped. Rerun with --workspace to initialize the workspace contract.`)
3674
+ printLine('Skipped. Rerun with --workspace to initialize the workspace contract.')
3751
3675
  }
3752
3676
  }
3753
3677
  printLine('')
3754
-
3755
3678
  // Step 3: global coding-agent skills
3756
3679
  let installOutcomes: SkillInstallOutcome[] = []
3757
3680
  printLine('Step 3: Install coding agent skills')
3758
3681
  if (hasArg('--no-install') || validateOnly || fix) {
3759
- printLine(hasArg('--no-install') ? ' Skipped (--no-install).' : ' Skipped for validation/repair.')
3682
+ printLine(hasArg('--no-install') ? 'Skipped (--no-install).' : 'Skipped for validation/repair.')
3760
3683
  } else {
3761
3684
  let targets = setupSkillTargetsFromArgs()
3762
3685
  if (targets.length === 0 && !noInput && !dryRun) {
3763
- if (await askYesNo(' Install the Orizu skill for your coding agents?', true)) {
3764
- targets = ['agent-user']
3765
- const choices = detectedSetupSkillChoices(setupSkillHome)
3766
- if (choices.length > 0) {
3767
- const selectedChoices = await promptKeyboardMultiSelect(
3768
- 'Choose additional native agent installs',
3769
- choices,
3770
- choice => choice.label,
3771
- () => true
3772
- )
3773
- for (const choice of selectedChoices) {
3774
- if (!targets.includes(choice.target)) targets.push(choice.target)
3775
- }
3776
- }
3686
+ if (await askYesNo('Install the Orizu skill for your coding agents?', true)) {
3687
+ const selectedChoices = await promptKeyboardMultiSelect(
3688
+ 'Choose skill installation destinations',
3689
+ detectedSetupSkillChoices(setupSkillHome),
3690
+ choice => choice.label,
3691
+ () => true
3692
+ )
3693
+ targets = selectedChoices.map(choice => choice.target)
3777
3694
  }
3778
3695
  }
3779
-
3780
3696
  if (targets.length === 0) {
3781
- printLine(' Skipped skill install.')
3697
+ printLine('Skipped skill install.')
3782
3698
  } else {
3783
- printLine(' Installing skills...')
3699
+ printLine('Installing skills...')
3784
3700
  installOutcomes = await applySkillInstallTargets(targets, {
3785
3701
  mode: installMode,
3786
3702
  skipConfirm: true,
@@ -3791,22 +3707,21 @@ async function setupCommand() {
3791
3707
  printResults: false,
3792
3708
  })
3793
3709
  if (dryRun) {
3794
- printLine(' Dry run: no skill files were changed.')
3710
+ printLine('Dry run: no skill files were changed.')
3795
3711
  }
3796
3712
  for (const outcome of installOutcomes) {
3797
3713
  const label = setupSkillLabel(outcome.target)
3798
3714
  if (outcome.action === 'failed') {
3799
- printLine(` ${label} failed: ${outcome.error}`)
3715
+ printLine(`${label} failed: ${sanitizeSetupHumanInlineText(outcome.error)}`)
3800
3716
  } else if (outcome.action === 'skipped') {
3801
- printLine(` ${label} skipped`)
3717
+ printLine(`${label} skipped`)
3802
3718
  } else {
3803
- printLine(` ${label} installed`)
3719
+ printLine(`${label} installed`)
3804
3720
  }
3805
3721
  }
3806
3722
  }
3807
3723
  }
3808
3724
  printLine('')
3809
-
3810
3725
  // Phase 4: coding-agent handoff
3811
3726
  const handoffTeamSlug = setupTeam?.slug || workspaceResult?.teamSlug || null
3812
3727
  const handoffProjectSlug = selectedProject?.slug || (workspaceResult?.projectSlugs.length === 1 ? workspaceResult.projectSlugs[0] : null)
@@ -3820,55 +3735,65 @@ async function setupCommand() {
3820
3735
  printLine('--- prompt end ---')
3821
3736
  printLine('')
3822
3737
  }
3823
-
3824
- const detected = Object.keys(LAUNCHABLE_AGENTS)
3825
- .filter(agent => findExecutable(LAUNCHABLE_AGENTS[agent]))
3738
+ const detected = Object.entries(AGENT_LAUNCH_SPECS)
3739
+ .filter(([, spec]) => findExecutable(spec.command))
3740
+ .map(([agent]) => agent)
3826
3741
  let launchAgent = getArg('--launch')
3827
- if (launchAgent && !(launchAgent in LAUNCHABLE_AGENTS)) {
3828
- throw new Error(`Unknown --launch agent '${launchAgent}'. Choices: ${Object.keys(LAUNCHABLE_AGENTS).join(', ')}.`)
3742
+ let completedInteractiveHandoff = false
3743
+ if (launchAgent && !(launchAgent in AGENT_LAUNCH_SPECS)) {
3744
+ throw new Error(`Unknown --launch agent '${launchAgent}'. Choices: ${Object.keys(AGENT_LAUNCH_SPECS).join(', ')}.`)
3829
3745
  }
3830
3746
 
3831
- if (!launchAgent && !noInput && isInteractiveTerminal() && detected.length > 0 && !dryRun) {
3747
+ if (!launchAgent && !noInput && isInteractiveTerminal() && !dryRun) {
3832
3748
  const launchChoice = await promptKeyboardSelect(
3833
3749
  'Choose an agent to launch',
3834
- [null, ...detected],
3835
- agent => agent === null ? "Don't launch an agent" : LAUNCHABLE_AGENTS[agent]
3750
+ [...detected, 'other'],
3751
+ agent => agent === 'other' ? 'Other' : AGENT_LAUNCH_SPECS[agent].displayLabel,
3752
+ { onEscape: () => 'escape', escapeLabel: 'skip' }
3836
3753
  )
3837
- launchAgent = launchChoice
3754
+ completedInteractiveHandoff = true
3755
+ if (launchChoice === 'other') {
3756
+ if (!handoffRequested) {
3757
+ printLine('--- prompt start ---')
3758
+ printLine(handoffPrompt)
3759
+ printLine('--- prompt end ---')
3760
+ printLine('')
3761
+ }
3762
+ } else if (launchChoice !== 'escape') {
3763
+ launchAgent = launchChoice
3764
+ }
3838
3765
  }
3839
-
3840
3766
  if (launchAgent) {
3841
- const binary = findExecutable(LAUNCHABLE_AGENTS[launchAgent])
3767
+ const launchSpec = AGENT_LAUNCH_SPECS[launchAgent]
3768
+ const binary = findExecutable(launchSpec.command)
3842
3769
  if (inputDisabled) {
3843
- printLine(' Not launching: setup is non-interactive.')
3770
+ printLine('Not launching: setup is non-interactive.')
3844
3771
  } else if (!isInteractiveTerminal()) {
3845
- printLine(' Not launching: --launch requires an interactive terminal.')
3772
+ printLine('Not launching: --launch requires an interactive terminal.')
3846
3773
  } else if (!binary) {
3847
- printLine(` Not launching: '${LAUNCHABLE_AGENTS[launchAgent]}' was not found on PATH.`)
3774
+ printLine(`Not launching: '${launchSpec.command}' was not found on PATH.`)
3848
3775
  } else if (dryRun) {
3849
- printLine(` Would launch ${binary} with the setup prompt.`)
3850
- } else if (!getArg('--launch') || skipConfirm || await askYesNo(` Launch ${LAUNCHABLE_AGENTS[launchAgent]} with the setup prompt now?`, true)) {
3851
- printLine(` Launching ${binary}…`)
3852
- const launched = spawnSync(binary, [handoffPrompt], { cwd: workspaceRoot, stdio: 'inherit' })
3776
+ printLine(`Would launch ${binary} with the setup prompt.`)
3777
+ } else if (!getArg('--launch') || skipConfirm || await askYesNo(`Launch ${launchSpec.displayLabel} with the setup prompt now?`, true)) {
3778
+ printLine(`Launching ${binary}…`)
3779
+ const launched = spawnSync(binary, launchSpec.buildPromptArgs(handoffPrompt), {
3780
+ cwd: workspaceRoot,
3781
+ stdio: 'inherit',
3782
+ shell: false,
3783
+ })
3853
3784
  if (!launched.error) launchedAgent = launchAgent
3854
3785
  if (launched.error || (launched.status !== null && launched.status !== 0)) {
3855
- printLine(` ${LAUNCHABLE_AGENTS[launchAgent]} exited without completing successfully.`)
3786
+ printLine(`${launchSpec.displayLabel} exited without completing successfully.`)
3856
3787
  }
3857
3788
  }
3858
- } else if (detected.length === 0) {
3859
- printLine(` No launchable coding agent was found. Read https://orizu.ai/llms.txt to get started.`)
3789
+ } else if (detected.length === 0 && !completedInteractiveHandoff) {
3790
+ printLine('No launchable coding agent was found. Read https://orizu.ai/llms.txt to get started.')
3860
3791
  }
3861
3792
  printLine('')
3862
3793
  }
3863
3794
 
3864
3795
  // Setup summary (success criteria checklist)
3865
- const summaryTargets = installOutcomes.length > 0
3866
- ? installOutcomes
3867
- .filter(outcome => outcome.action !== 'failed')
3868
- .map(outcome => ({ outcome, status: getSkillTargetStatus(outcome.target, { cwd: workspaceRoot, homeDir: setupSkillHome }) }))
3869
- : []
3870
3796
  const source = resolveSkillSource()
3871
-
3872
3797
  if (hasJsonFlag()) {
3873
3798
  printJson({
3874
3799
  auth: { state: auth.state, server: auth.baseUrl },
@@ -3911,21 +3836,10 @@ async function setupCommand() {
3911
3836
  })
3912
3837
  return
3913
3838
  }
3914
-
3915
3839
  printLine('Setup summary')
3916
3840
  printLine(` Auth: ${auth.state === 'signed-in' ? `signed in (${sanitizeTerminalText(auth.baseUrl)})` : 'not signed in — run `orizu login`'}`)
3917
- if (summaryTargets.length > 0) {
3918
- printLine(' Skills:')
3919
- for (const { outcome, status } of summaryTargets) {
3920
- const modeLabel = status.mode ? `, ${status.mode}` : ''
3921
- printLine(` ${outcome.target.padEnd(16)} ${status.state}${modeLabel} ${outcome.path}`)
3922
- }
3923
- } else {
3924
- printLine(' Skills: none installed this run')
3925
- }
3926
- const failed = installOutcomes.filter(outcome => outcome.action === 'failed')
3927
- for (const failure of failed) {
3928
- printLine(` ${failure.target.padEnd(16)} failed: ${failure.error}`)
3841
+ for (const line of renderSetupSkillSummary(installOutcomes, setupSkillHome)) {
3842
+ printLine(line)
3929
3843
  }
3930
3844
  printLine(` Workspace: ${workspaceState === 'skipped' ? 'skipped' : getWorkspaceRoot(workspaceRoot)}`)
3931
3845
  if (workspaceResult) {