orizu 0.4.0 → 0.5.0

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 (76) hide show
  1. package/README.md +22 -13
  2. package/dist/artifact-archive.js +152 -0
  3. package/dist/connectors-cli.js +80 -0
  4. package/dist/credentials.js +4 -1
  5. package/dist/daytona-slice-rehearsal.js +255 -0
  6. package/dist/daytona-slice.js +441 -0
  7. package/dist/egress-allowlist-cli.js +132 -0
  8. package/dist/egress-policy.js +159 -0
  9. package/dist/git-credential.js +131 -0
  10. package/dist/github-setup.js +226 -0
  11. package/dist/help.js +276 -21
  12. package/dist/hosted-bootstrap-rehearsal.js +235 -0
  13. package/dist/hosted-bootstrap.js +365 -0
  14. package/dist/hosted-broker-server.js +220 -0
  15. package/dist/hosted-git-http-server.js +136 -0
  16. package/dist/hosted-harness-claude.js +493 -0
  17. package/dist/hosted-harness-opencode.js +729 -0
  18. package/dist/hosted-harness.js +60 -0
  19. package/dist/hosted-loop.js +477 -0
  20. package/dist/hosted-run-event-sink.js +499 -0
  21. package/dist/hosted-runtime-assets.js +216 -0
  22. package/dist/hosted-session-cli.js +642 -0
  23. package/dist/index.js +945 -258
  24. package/dist/kill-switch-cli.js +89 -0
  25. package/dist/manifests-cli.js +143 -0
  26. package/dist/preview-runtime.js +4 -1
  27. package/dist/sandbox-provider.js +238 -0
  28. package/dist/secret-redaction.js +112 -0
  29. package/dist/session-timeline.js +103 -0
  30. package/dist/setup-prompt.js +4 -2
  31. package/dist/skill-installer.js +9 -1
  32. package/dist/smoke.js +250 -0
  33. package/dist/task-assignment-manifest.js +96 -0
  34. package/dist/vercel-sandbox-provider.js +260 -0
  35. package/dist/workbench-cli.js +493 -0
  36. package/dist/workspace-sync.js +924 -0
  37. package/dist/workspace.js +868 -57
  38. package/package.json +4 -1
  39. package/src/artifact-archive.ts +182 -0
  40. package/src/connectors-cli.ts +115 -0
  41. package/src/credentials.ts +6 -2
  42. package/src/daytona-slice-rehearsal.ts +320 -0
  43. package/src/daytona-slice.ts +598 -0
  44. package/src/egress-allowlist-cli.ts +147 -0
  45. package/src/egress-policy.ts +188 -0
  46. package/src/git-credential.ts +155 -0
  47. package/src/github-setup.ts +366 -0
  48. package/src/help.ts +277 -21
  49. package/src/hosted-bootstrap-rehearsal.ts +312 -0
  50. package/src/hosted-bootstrap.ts +571 -0
  51. package/src/hosted-broker-server.ts +278 -0
  52. package/src/hosted-git-http-server.ts +164 -0
  53. package/src/hosted-harness-claude.ts +621 -0
  54. package/src/hosted-harness-opencode.ts +823 -0
  55. package/src/hosted-harness.ts +124 -0
  56. package/src/hosted-loop.ts +680 -0
  57. package/src/hosted-run-event-sink.ts +632 -0
  58. package/src/hosted-runtime-assets.ts +272 -0
  59. package/src/hosted-session-cli.ts +844 -0
  60. package/src/index.ts +1102 -317
  61. package/src/kill-switch-cli.ts +113 -0
  62. package/src/manifests-cli.ts +168 -0
  63. package/src/preview-runtime.ts +14 -9
  64. package/src/sandbox-provider.ts +440 -0
  65. package/src/secret-redaction.ts +124 -0
  66. package/src/session-timeline.ts +152 -0
  67. package/src/setup-prompt.ts +4 -2
  68. package/src/skill-installer.ts +13 -1
  69. package/src/smoke.ts +342 -0
  70. package/src/task-assignment-manifest.ts +164 -0
  71. package/src/vercel-sandbox-provider.ts +388 -0
  72. package/src/workbench-cli.ts +701 -0
  73. package/src/workspace-sync.ts +1148 -0
  74. package/src/workspace.ts +1051 -64
  75. package/vendor/skills/orizu-cli/SKILL.md +8 -4
  76. package/vendor/skills/orizu-cli/references/cli-reference.md +12 -4
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash, randomBytes, randomUUID } from 'crypto'
3
- import { basename, delimiter, dirname, extname, isAbsolute, join, normalize } from 'path'
3
+ import { basename, delimiter, dirname, extname, isAbsolute, join, normalize, relative, resolve } from 'path'
4
4
  import { fileURLToPath } from 'url'
5
5
  import { createServer } from 'http'
6
6
  import { gunzipSync, gzipSync } from 'zlib'
@@ -8,7 +8,6 @@ import {
8
8
  existsSync,
9
9
  mkdirSync,
10
10
  mkdtempSync,
11
- readdirSync,
12
11
  readFileSync,
13
12
  realpathSync,
14
13
  rmSync,
@@ -16,8 +15,9 @@ import {
16
15
  writeFileSync,
17
16
  } from 'fs'
18
17
  import { spawn, spawnSync } from 'child_process'
19
- import { tmpdir } from 'os'
18
+ import { homedir, tmpdir } from 'os'
20
19
  import { createInterface } from 'readline/promises'
20
+ import { emitKeypressEvents } from 'readline'
21
21
  import { stdin as input, stdout as output } from 'process'
22
22
  import { clearServerCredentials, getServerCredentials, saveServerCredentials } from './credentials.js'
23
23
  import { parseDatasetFile } from './file-parser.js'
@@ -26,14 +26,10 @@ import { parseDatasetReference } from './dataset-download.js'
26
26
  import { extractErrorMessage } from './error-response.js'
27
27
  import { parseGlobalFlags } from './global-flags.js'
28
28
  import { getCapabilities, renderHelpForArgs, renderRootHelp } from './help.js'
29
+ import { zipDirectoryToBase64 } from './artifact-archive.js'
29
30
  import { runLocalAppPreview } from './preview-runtime.js'
30
- import {
31
- assertSecureTokenTransport,
32
- authedFetch,
33
- getBaseUrl,
34
- resolveLoginBaseUrl,
35
- setGlobalFlags,
36
- } from './http.js'
31
+ import { readAssignmentManifestJsonlFile } from './task-assignment-manifest.js'
32
+ import { assertSecureTokenTransport, authedFetch, getBaseUrl, resolveLoginBaseUrl, setGlobalFlags } from './http.js'
37
33
  import {
38
34
  computeSkillContentHash,
39
35
  getSkillInstallPath,
@@ -45,6 +41,7 @@ import {
45
41
  resolveSkillSource,
46
42
  SKILL_INSTALL_AGENTS,
47
43
  SKILL_INSTALL_TARGETS,
44
+ SkillInstallAgent,
48
45
  SkillInstallMode,
49
46
  SkillInstallScope,
50
47
  SkillInstallTarget,
@@ -58,9 +55,26 @@ import { LoginResponse } from './types.js'
58
55
  import {
59
56
  getWorkspaceRoot,
60
57
  initOrizuWorkspace,
61
- WORKSPACE_DIR_NAME,
58
+ type WorkspaceProjectSeed,
62
59
  workspaceExists,
63
60
  } from './workspace.js'
61
+ import { connectorsCommand } from './connectors-cli.js'
62
+ import { killSwitchCommand } from './kill-switch-cli.js'
63
+ import { egressAllowlistCommand } from './egress-allowlist-cli.js'
64
+ import { manifestsCommand } from './manifests-cli.js'
65
+ import { workbenchCommand } from './workbench-cli.js'
66
+ import { hostedCommand } from './hosted-session-cli.js'
67
+ import { workspaceSyncCommand } from './workspace-sync.js'
68
+ import { runGitCredential } from './git-credential.js'
69
+ import { type GithubLinkResult, runGithubLink, runInteractiveHostedSetup } from './github-setup.js'
70
+
71
+ function getErrorMessage(error: unknown): string {
72
+ return error instanceof Error ? error.message : String(error)
73
+ }
74
+
75
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
76
+ return error instanceof Error
77
+ }
64
78
 
65
79
  interface Team {
66
80
  id: string
@@ -278,6 +292,21 @@ interface TaskStatusPayload {
278
292
  }
279
293
  }
280
294
 
295
+ const TASK_STATUS_ACTION_LABELS = {
296
+ paused: {
297
+ verb: 'pause',
298
+ pastTense: 'Paused',
299
+ },
300
+ active: {
301
+ verb: 'unpause',
302
+ pastTense: 'Unpaused',
303
+ },
304
+ completed: {
305
+ verb: 'complete',
306
+ pastTense: 'Completed',
307
+ },
308
+ } as const
309
+
281
310
  function getCliVersion(): string {
282
311
  const packageJson = JSON.parse(
283
312
  readFileSync(new URL('../package.json', import.meta.url), 'utf8')
@@ -311,14 +340,6 @@ function printUsage() {
311
340
  printLine(renderRootHelp())
312
341
  }
313
342
 
314
- function printPreviewUsage() {
315
- printLine(`\nLocal app preview:\n\n orizu apps preview --file <path> --input-schema <json-path> --output-schema <json-path> --sample-row <json-path> [--screenshot <png-path>] [--headed] [--keep-open] [--component <name>]`)
316
- }
317
-
318
- function printOptimizationUsage() {
319
- printLine(`\nOptimization lifecycle commands:\n\n orizu optimizations start --project <team/project> --optimizer-version <id> --prompt-version <id[,id]> --selection-scorer <id> [--reflection-scorer <id>] [--pareto-scorer <id>] [--best-scorer <id>] --dataset-version <id> --split-set <id> [--train-split <name>] [--validation-split <name>] [--metadata <json|@file>] [--json]\n orizu optimizations run-gepa --project <team/project> --optimizer-version-id <id> --candidate-version-id <id> --runner-version-id <id> --candidate-runner-dir <dir> --scorer-version-id <id> --scorer-runner-version-id <id> --scorer-runner-dir <dir> --dataset-version-id <id> --split-set-id <id> [--train-split train] [--val-split validation] [--budget auto|light|medium|heavy | --max-metric-calls <n> | --max-full-evals <n> | --max-iterations <n>] [--num-threads auto|N] [--reflection-retry-attempts N] [--reflection-http-timeout-seconds N] [--log-dir logs] [--no-skip-perfect-parent-reflection]\n orizu optimizations export <run-id> [--out <path>] [--json]\n orizu optimizations pause <run-id> [--reason <text>] [--json]\n orizu optimizations resume <run-id> [--json]\n orizu optimizations finish <run-id> [--best-score <n>] [--best-candidate <id>] [--result-prompt-version <id>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations fail <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--metadata <json|@file>] [--json]\n orizu optimizations cancel <run-id> [--reason <text>] [--report <markdown|@file> | --report-file <path>] [--json]`)
320
- }
321
-
322
343
  let cliArgs = process.argv.slice(2)
323
344
  let cliJsonOutput = false
324
345
 
@@ -350,6 +371,20 @@ function getArgs(name: string): string[] {
350
371
  return values
351
372
  }
352
373
 
374
+ function getOptionalArgValue(name: string): string | null {
375
+ const index = cliArgs.indexOf(name)
376
+ if (index === -1 || index + 1 >= cliArgs.length) {
377
+ return null
378
+ }
379
+
380
+ const value = cliArgs[index + 1]
381
+ if (!value || value.startsWith('-')) {
382
+ return null
383
+ }
384
+
385
+ return value
386
+ }
387
+
353
388
  function rejectDashPrefixedOptionValue(name: string, value: string | null) {
354
389
  if (value && value.startsWith('-')) {
355
390
  throw new Error(`Invalid value for ${name}: option values cannot start with a dash`)
@@ -408,6 +443,10 @@ export function sanitizeTerminalText(value: unknown): string {
408
443
  return String(value).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '')
409
444
  }
410
445
 
446
+ function printError(message: string): void {
447
+ console.error(sanitizeTerminalText(message))
448
+ }
449
+
411
450
  export function validateBrowserUrl(url: string, expectedOrigin?: string): URL {
412
451
  let parsed: URL
413
452
  try {
@@ -575,8 +614,8 @@ function readJsonObjectArg(valueArg: string | null, label: string): Record<strin
575
614
  throw new Error('JSON root must be an object')
576
615
  }
577
616
  return parsed as Record<string, unknown>
578
- } catch (error: any) {
579
- throw new Error(`Invalid ${label} '${valueArg}': ${error?.message || String(error)}`)
617
+ } catch (error: unknown) {
618
+ throw new Error(`Invalid ${label} '${valueArg}': ${getErrorMessage(error)}`)
580
619
  }
581
620
  }
582
621
 
@@ -688,6 +727,55 @@ async function fetchTeamMembers(teamSlug: string): Promise<TeamMember[]> {
688
727
  return data.members
689
728
  }
690
729
 
730
+ async function createTeamOnServer(name: string): Promise<Team> {
731
+ const response = await authedFetch('/api/cli/teams', {
732
+ method: 'POST',
733
+ headers: { 'Content-Type': 'application/json' },
734
+ body: JSON.stringify({ name }),
735
+ })
736
+
737
+ if (!response.ok) {
738
+ throw new Error(`Failed to create team: ${await response.text()}`)
739
+ }
740
+
741
+ const data = await parseJsonResponse<{ team: Team }>(response, 'Team create')
742
+ return data.team
743
+ }
744
+
745
+ async function createProjectOnServer(teamSlug: string, name: string): Promise<Project> {
746
+ const response = await authedFetch('/api/cli/projects', {
747
+ method: 'POST',
748
+ headers: { 'Content-Type': 'application/json' },
749
+ body: JSON.stringify({ teamSlug, name }),
750
+ })
751
+
752
+ if (!response.ok) {
753
+ throw new Error(`Failed to create project: ${await response.text()}`)
754
+ }
755
+
756
+ const data = await parseJsonResponse<{
757
+ project: {
758
+ id: string
759
+ name: string
760
+ slug: string
761
+ teamId?: string
762
+ teamName?: string
763
+ teamSlug: string
764
+ role?: string
765
+ }
766
+ }>(response, 'Project create')
767
+
768
+ return {
769
+ id: data.project.id,
770
+ name: data.project.name,
771
+ slug: data.project.slug,
772
+ teamId: data.project.teamId || '',
773
+ teamName: data.project.teamName || data.project.teamSlug,
774
+ teamSlug: data.project.teamSlug,
775
+ role: data.project.role || 'admin',
776
+ }
777
+ }
778
+
691
779
  async function resolveProjectSelection(projectArg: string | null): Promise<Project> {
692
780
  projectArg = projectArg || process.env.ORIZU_PROJECT || null
693
781
  const teams = await fetchTeams()
@@ -723,7 +811,7 @@ async function resolveProjectSelection(projectArg: string | null): Promise<Proje
723
811
 
724
812
  const matchedTeam = teams.find(team => team.slug === teamSlug)
725
813
  if (!matchedTeam) {
726
- console.error(`Team '${sanitizeTerminalText(teamSlug)}' not found in your accessible teams.`)
814
+ printError(`Team '${teamSlug}' not found in your accessible teams.`)
727
815
  const selectedTeam = await promptSelect(
728
816
  'Select a team',
729
817
  teams,
@@ -744,7 +832,7 @@ async function resolveProjectSelection(projectArg: string | null): Promise<Proje
744
832
  const matchedProject = projects.find(project => project.slug === projectSlug)
745
833
 
746
834
  if (!matchedProject) {
747
- console.error(`Project '${sanitizeTerminalText(projectSlug)}' not found in team '${sanitizeTerminalText(matchedTeam.slug)}'.`)
835
+ printError(`Project '${projectSlug}' not found in team '${matchedTeam.slug}'.`)
748
836
  const selectedProject = await promptSelect(
749
837
  `Select a project in ${matchedTeam.slug}`,
750
838
  projects,
@@ -2770,22 +2858,12 @@ async function createTeam() {
2770
2858
  throw new Error('Usage: orizu teams create --name <name>')
2771
2859
  }
2772
2860
 
2773
- const response = await authedFetch('/api/cli/teams', {
2774
- method: 'POST',
2775
- headers: { 'Content-Type': 'application/json' },
2776
- body: JSON.stringify({ name }),
2777
- })
2778
-
2779
- if (!response.ok) {
2780
- throw new Error(`Failed to create team: ${await response.text()}`)
2781
- }
2782
-
2783
- const data = await parseJsonResponse<{ team: Team }>(response, 'Team create')
2861
+ const team = await createTeamOnServer(name)
2784
2862
  if (hasJsonFlag()) {
2785
- printJson({ team: data.team })
2863
+ printJson({ team })
2786
2864
  return
2787
2865
  }
2788
- printLine(`Created team: ${sanitizeTerminalText(data.team.name)} (${sanitizeTerminalText(data.team.slug)})`)
2866
+ printLine(`Created team: ${sanitizeTerminalText(team.name)} (${sanitizeTerminalText(team.slug)})`)
2789
2867
  }
2790
2868
 
2791
2869
  async function listProjects() {
@@ -2818,24 +2896,12 @@ async function createProject() {
2818
2896
  teamSlug = team.slug
2819
2897
  }
2820
2898
 
2821
- const response = await authedFetch('/api/cli/projects', {
2822
- method: 'POST',
2823
- headers: { 'Content-Type': 'application/json' },
2824
- body: JSON.stringify({ teamSlug, name }),
2825
- })
2826
-
2827
- if (!response.ok) {
2828
- throw new Error(`Failed to create project: ${await response.text()}`)
2829
- }
2830
-
2831
- const data = await parseJsonResponse<{
2832
- project: { id: string; name: string; slug: string; teamSlug: string }
2833
- }>(response, 'Project create')
2899
+ const project = await createProjectOnServer(teamSlug, name)
2834
2900
  if (hasJsonFlag()) {
2835
- printJson({ project: data.project })
2901
+ printJson({ project })
2836
2902
  return
2837
2903
  }
2838
- printLine(`Created project ${sanitizeTerminalText(`${data.project.teamSlug}/${data.project.slug}`)}`)
2904
+ printLine(`Created project ${sanitizeTerminalText(`${project.teamSlug}/${project.slug}`)}`)
2839
2905
  }
2840
2906
 
2841
2907
  async function listTasks() {
@@ -2862,11 +2928,11 @@ function readSourceFile(pathArg: string): string {
2862
2928
  const expandedPath = expandHomePath(pathArg)
2863
2929
  try {
2864
2930
  return readFileSync(expandedPath, 'utf-8')
2865
- } catch (error: any) {
2866
- if (error?.code === 'ENOENT') {
2931
+ } catch (error: unknown) {
2932
+ if (isNodeError(error) && error.code === 'ENOENT') {
2867
2933
  throw new Error(`File not found: ${expandedPath}`)
2868
2934
  }
2869
- throw new Error(`Failed to read file '${expandedPath}': ${error?.message || String(error)}`)
2935
+ throw new Error(`Failed to read file '${expandedPath}': ${getErrorMessage(error)}`)
2870
2936
  }
2871
2937
  }
2872
2938
 
@@ -2874,11 +2940,11 @@ function readSourceBytes(pathArg: string): Buffer {
2874
2940
  const expandedPath = expandHomePath(pathArg)
2875
2941
  try {
2876
2942
  return readFileSync(expandedPath)
2877
- } catch (error: any) {
2878
- if (error?.code === 'ENOENT') {
2943
+ } catch (error: unknown) {
2944
+ if (isNodeError(error) && error.code === 'ENOENT') {
2879
2945
  throw new Error(`File not found: ${expandedPath}`)
2880
2946
  }
2881
- throw new Error(`Failed to read file '${expandedPath}': ${error?.message || String(error)}`)
2947
+ throw new Error(`Failed to read file '${expandedPath}': ${getErrorMessage(error)}`)
2882
2948
  }
2883
2949
  }
2884
2950
 
@@ -2920,6 +2986,304 @@ async function askYesNo(question: string, defaultYes: boolean): Promise<boolean>
2920
2986
  }
2921
2987
  }
2922
2988
 
2989
+ async function askText(question: string, defaultValue: string): Promise<string> {
2990
+ const rl = createInterface({ input, output })
2991
+ try {
2992
+ const suffix = defaultValue ? ` [${defaultValue}]` : ''
2993
+ const answer = (await rl.question(`${question}${suffix} `)).trim()
2994
+ return answer || defaultValue
2995
+ } finally {
2996
+ rl.close()
2997
+ }
2998
+ }
2999
+
3000
+ type KeypressInfo = {
3001
+ name?: string
3002
+ sequence?: string
3003
+ ctrl?: boolean
3004
+ }
3005
+
3006
+ type RawInput = typeof input & {
3007
+ setRawMode?: (mode: boolean) => void
3008
+ isRaw?: boolean
3009
+ }
3010
+
3011
+ async function waitForAnyKeyOrEscape(message: string): Promise<boolean> {
3012
+ if (!isInteractiveTerminal()) {
3013
+ throw new Error('Interactive terminal required.')
3014
+ }
3015
+
3016
+ printLine(message)
3017
+ const rawInput = input as RawInput
3018
+ const wasRaw = Boolean(rawInput.isRaw)
3019
+ emitKeypressEvents(input)
3020
+
3021
+ return new Promise((resolvePromise, rejectPromise) => {
3022
+ function cleanup() {
3023
+ input.off('keypress', onKeypress)
3024
+ if (rawInput.setRawMode && !wasRaw) {
3025
+ rawInput.setRawMode(false)
3026
+ }
3027
+ input.pause()
3028
+ printLine('')
3029
+ }
3030
+
3031
+ function onKeypress(_chunk: string, key: KeypressInfo) {
3032
+ cleanup()
3033
+ if (key.ctrl && key.name === 'c') {
3034
+ rejectPromise(new Error('Setup cancelled.'))
3035
+ return
3036
+ }
3037
+ resolvePromise(key.name !== 'escape')
3038
+ }
3039
+
3040
+ input.on('keypress', onKeypress)
3041
+ input.resume()
3042
+ rawInput.setRawMode?.(true)
3043
+ })
3044
+ }
3045
+
3046
+ async function promptKeyboardSelect<T>(
3047
+ title: string,
3048
+ items: T[],
3049
+ label: (item: T, index: number) => string
3050
+ ): Promise<T> {
3051
+ if (items.length === 0) {
3052
+ throw new Error(`No options available for ${title.toLowerCase()}`)
3053
+ }
3054
+ if (!isInteractiveTerminal()) {
3055
+ throw new Error(`${title} selection requires interactive terminal. Provide flags explicitly instead.`)
3056
+ }
3057
+
3058
+ printLine(`\n${sanitizeTerminalText(title)}`)
3059
+ printLine('Use ↑/↓ to choose, Enter to confirm, or Esc to cancel.')
3060
+
3061
+ let selected = 0
3062
+ let rendered = false
3063
+ const rawInput = input as RawInput
3064
+ const wasRaw = Boolean(rawInput.isRaw)
3065
+ emitKeypressEvents(input)
3066
+
3067
+ function render() {
3068
+ if (rendered) {
3069
+ output.write(`\x1b[${items.length}A`)
3070
+ }
3071
+ items.forEach((item, index) => {
3072
+ output.write('\x1b[2K')
3073
+ const marker = index === selected ? '●' : '○'
3074
+ output.write(` ${marker} ${sanitizeTerminalText(label(item, index))}\n`)
3075
+ })
3076
+ rendered = true
3077
+ }
3078
+
3079
+ render()
3080
+ output.write('\x1b[?25l')
3081
+
3082
+ return new Promise((resolvePromise, rejectPromise) => {
3083
+ function cleanup() {
3084
+ input.off('keypress', onKeypress)
3085
+ if (rawInput.setRawMode && !wasRaw) {
3086
+ rawInput.setRawMode(false)
3087
+ }
3088
+ input.pause()
3089
+ output.write('\x1b[?25h')
3090
+ printLine('')
3091
+ }
3092
+
3093
+ function onKeypress(_chunk: string, key: KeypressInfo) {
3094
+ if (key.ctrl && key.name === 'c') {
3095
+ cleanup()
3096
+ rejectPromise(new Error('Setup cancelled.'))
3097
+ return
3098
+ }
3099
+ if (key.name === 'escape') {
3100
+ cleanup()
3101
+ rejectPromise(new Error('Setup cancelled.'))
3102
+ return
3103
+ }
3104
+ if (key.name === 'up') {
3105
+ selected = (selected - 1 + items.length) % items.length
3106
+ render()
3107
+ return
3108
+ }
3109
+ if (key.name === 'down') {
3110
+ selected = (selected + 1) % items.length
3111
+ render()
3112
+ return
3113
+ }
3114
+ if (key.name === 'return' || key.name === 'enter') {
3115
+ const item = items[selected]
3116
+ cleanup()
3117
+ resolvePromise(item)
3118
+ }
3119
+ }
3120
+
3121
+ input.on('keypress', onKeypress)
3122
+ input.resume()
3123
+ rawInput.setRawMode?.(true)
3124
+ })
3125
+ }
3126
+
3127
+ async function promptKeyboardMultiSelect<T>(
3128
+ title: string,
3129
+ items: T[],
3130
+ label: (item: T, index: number) => string,
3131
+ defaultSelected: (item: T, index: number) => boolean
3132
+ ): Promise<T[]> {
3133
+ if (items.length === 0) {
3134
+ return []
3135
+ }
3136
+ if (!isInteractiveTerminal()) {
3137
+ throw new Error(`${title} selection requires interactive terminal. Provide flags explicitly instead.`)
3138
+ }
3139
+
3140
+ printLine(`\n${sanitizeTerminalText(title)}`)
3141
+ printLine('Use ↑/↓ to move, Space to toggle, Enter to confirm, or Esc to skip.')
3142
+
3143
+ let selected = 0
3144
+ let rendered = false
3145
+ const checked = new Set<number>()
3146
+ items.forEach((item, index) => {
3147
+ if (defaultSelected(item, index)) {
3148
+ checked.add(index)
3149
+ }
3150
+ })
3151
+ const rawInput = input as RawInput
3152
+ const wasRaw = Boolean(rawInput.isRaw)
3153
+ emitKeypressEvents(input)
3154
+
3155
+ function render() {
3156
+ if (rendered) {
3157
+ output.write(`\x1b[${items.length}A`)
3158
+ }
3159
+ items.forEach((item, index) => {
3160
+ output.write('\x1b[2K')
3161
+ const cursor = index === selected ? '›' : ' '
3162
+ const marker = checked.has(index) ? '◉' : '○'
3163
+ output.write(`${cursor} ${marker} ${sanitizeTerminalText(label(item, index))}\n`)
3164
+ })
3165
+ rendered = true
3166
+ }
3167
+
3168
+ render()
3169
+ output.write('\x1b[?25l')
3170
+
3171
+ return new Promise((resolvePromise, rejectPromise) => {
3172
+ function cleanup() {
3173
+ input.off('keypress', onKeypress)
3174
+ if (rawInput.setRawMode && !wasRaw) {
3175
+ rawInput.setRawMode(false)
3176
+ }
3177
+ input.pause()
3178
+ output.write('\x1b[?25h')
3179
+ printLine('')
3180
+ }
3181
+
3182
+ function onKeypress(_chunk: string, key: KeypressInfo) {
3183
+ if (key.ctrl && key.name === 'c') {
3184
+ cleanup()
3185
+ rejectPromise(new Error('Setup cancelled.'))
3186
+ return
3187
+ }
3188
+ if (key.name === 'escape') {
3189
+ cleanup()
3190
+ resolvePromise([])
3191
+ return
3192
+ }
3193
+ if (key.name === 'up') {
3194
+ selected = (selected - 1 + items.length) % items.length
3195
+ render()
3196
+ return
3197
+ }
3198
+ if (key.name === 'down') {
3199
+ selected = (selected + 1) % items.length
3200
+ render()
3201
+ return
3202
+ }
3203
+ if (key.name === 'space') {
3204
+ if (checked.has(selected)) {
3205
+ checked.delete(selected)
3206
+ } else {
3207
+ checked.add(selected)
3208
+ }
3209
+ render()
3210
+ return
3211
+ }
3212
+ if (key.name === 'return' || key.name === 'enter') {
3213
+ const selectedItems = items.filter((_item, index) => checked.has(index))
3214
+ cleanup()
3215
+ resolvePromise(selectedItems)
3216
+ }
3217
+ }
3218
+
3219
+ input.on('keypress', onKeypress)
3220
+ input.resume()
3221
+ rawInput.setRawMode?.(true)
3222
+ })
3223
+ }
3224
+
3225
+ function formatFindingCount(count: number, label: string): string {
3226
+ return `${count} ${label}${count === 1 ? '' : 's'}`
3227
+ }
3228
+
3229
+ function validationSummary(findings: { severity: string }[], logPath: string | null): string {
3230
+ if (findings.length === 0) {
3231
+ return 'passed'
3232
+ }
3233
+
3234
+ const errors = findings.filter(finding => finding.severity === 'error').length
3235
+ const warnings = findings.filter(finding => finding.severity === 'warning').length
3236
+ const infos = findings.filter(finding => finding.severity === 'info').length
3237
+ const parts = [
3238
+ errors > 0 ? formatFindingCount(errors, 'error') : null,
3239
+ warnings > 0 ? formatFindingCount(warnings, 'warning') : null,
3240
+ infos > 0 ? formatFindingCount(infos, 'info') : null,
3241
+ ].filter(Boolean)
3242
+ const location = logPath ? ` (printed to ${logPath})` : ''
3243
+ return `${parts.join(', ')}${location}`
3244
+ }
3245
+
3246
+ function writeWorkspaceValidationLog(
3247
+ workspaceRoot: string,
3248
+ findings: { severity: string, code: string, path?: string, message: string }[],
3249
+ dryRun: boolean
3250
+ ): string | null {
3251
+ if (findings.length === 0 || dryRun) {
3252
+ return null
3253
+ }
3254
+
3255
+ const logsDir = join(workspaceRoot, '.logs')
3256
+ mkdirSync(logsDir, { recursive: true })
3257
+ const logPath = join(logsDir, `${randomBytes(6).toString('hex')}.log`)
3258
+ const lines = [
3259
+ 'Orizu workspace validation',
3260
+ `Generated at: ${new Date().toISOString()}`,
3261
+ `Workspace: ${workspaceRoot}`,
3262
+ `Summary: ${validationSummary(findings, null)}`,
3263
+ '',
3264
+ ...findings.map(finding => {
3265
+ const path = finding.path ? ` ${finding.path}` : ''
3266
+ return `${finding.severity.toUpperCase()} ${finding.code}:${path} ${finding.message}`
3267
+ }),
3268
+ '',
3269
+ ]
3270
+ writeFileSync(logPath, lines.join('\n'), 'utf8')
3271
+ return relative(workspaceRoot, logPath)
3272
+ }
3273
+
3274
+ function printWorkspaceFindings(findings: { severity: string, code: string, path?: string, message: string }[]) {
3275
+ if (findings.length === 0) {
3276
+ printLine(' Validation passed.')
3277
+ return
3278
+ }
3279
+
3280
+ printLine(' Validation findings:')
3281
+ for (const finding of findings) {
3282
+ const path = finding.path ? ` ${finding.path}` : ''
3283
+ printLine(` - ${finding.severity.toUpperCase()} ${finding.code}:${path} ${finding.message}`)
3284
+ }
3285
+ }
3286
+
2923
3287
  const SKILL_INSTALL_USAGE =
2924
3288
  `Usage: orizu install-skill [--agent <${SKILL_INSTALL_AGENTS.join('|')}>]... [--scope global|project] [--mode auto|link|copy] [--target <${SKILL_INSTALL_TARGETS.join('|')}>]... [--yes] [--dry-run]`
2925
3289
 
@@ -2932,6 +3296,7 @@ interface SkillInstallChoice {
2932
3296
  function describeSkillTarget(target: SkillInstallTarget): string {
2933
3297
  if (target === 'claude-user') return 'Claude Code, for you across all projects'
2934
3298
  if (target === 'claude-project') return 'Claude Code, only for this project'
3299
+ if (target === 'codex-user') return 'Codex, for you across all projects'
2935
3300
  if (target === 'agent-user') return 'Codex / Open Agent Skills, for you across all projects'
2936
3301
  if (target === 'agents-project') return 'Codex / Open Agent Skills, only for this project'
2937
3302
  if (target === 'codex-project') return 'Codex legacy project folder (.codex/skills)'
@@ -3052,15 +3417,31 @@ interface SkillInstallOutcome {
3052
3417
 
3053
3418
  async function applySkillInstallTargets(
3054
3419
  targets: SkillInstallTarget[],
3055
- options: { mode: SkillInstallMode, skipConfirm: boolean, dryRun: boolean }
3420
+ options: {
3421
+ mode: SkillInstallMode
3422
+ skipConfirm: boolean
3423
+ dryRun: boolean
3424
+ cwd?: string
3425
+ homeDir?: string
3426
+ showPlan?: boolean
3427
+ printResults?: boolean
3428
+ }
3056
3429
  ): Promise<SkillInstallOutcome[]> {
3057
3430
  const cliVersion = getCliVersion()
3058
3431
  const jsonMode = hasJsonFlag()
3059
3432
  const planned = targets.map(target =>
3060
- installSkillTarget(target, { dryRun: true, mode: options.mode, cliVersion })
3433
+ installSkillTarget(target, {
3434
+ cwd: options.cwd,
3435
+ homeDir: options.homeDir,
3436
+ dryRun: true,
3437
+ mode: options.mode,
3438
+ cliVersion,
3439
+ })
3061
3440
  )
3441
+ const showPlan = options.showPlan ?? true
3442
+ const printResults = options.printResults ?? true
3062
3443
 
3063
- if (!jsonMode) {
3444
+ if (showPlan && !jsonMode) {
3064
3445
  printLine('Planned writes:')
3065
3446
  for (const plan of planned) {
3066
3447
  printLine(describePlannedWrite(plan.target, plan.action, plan.mode, plan.path))
@@ -3069,7 +3450,7 @@ async function applySkillInstallTargets(
3069
3450
  }
3070
3451
 
3071
3452
  if (options.dryRun) {
3072
- if (!jsonMode) {
3453
+ if (printResults && !jsonMode) {
3073
3454
  printLine('Dry run: no files were changed.')
3074
3455
  }
3075
3456
  return planned.map(plan => ({
@@ -3083,8 +3464,8 @@ async function applySkillInstallTargets(
3083
3464
  const outcomes: SkillInstallOutcome[] = []
3084
3465
  for (const target of targets) {
3085
3466
  let overwrite = options.skipConfirm
3086
- const installPath = getSkillInstallPath(target)
3087
- if (!overwrite && targetNeedsOverwrite(target)) {
3467
+ const installPath = getSkillInstallPath(target, { cwd: options.cwd, homeDir: options.homeDir })
3468
+ if (!overwrite && targetNeedsOverwrite(target, { cwd: options.cwd, homeDir: options.homeDir })) {
3088
3469
  if (!isInteractiveTerminal()) {
3089
3470
  throw new Error(`${installPath} already exists. Pass --yes to replace it.`)
3090
3471
  }
@@ -3100,11 +3481,13 @@ async function applySkillInstallTargets(
3100
3481
 
3101
3482
  try {
3102
3483
  const result = installSkillTarget(target, {
3484
+ cwd: options.cwd,
3485
+ homeDir: options.homeDir,
3103
3486
  overwrite,
3104
3487
  mode: options.mode,
3105
3488
  cliVersion,
3106
3489
  })
3107
- if (!jsonMode) {
3490
+ if (printResults && !jsonMode) {
3108
3491
  const suffix = result.mode === 'link' ? ' (symlink)' : ''
3109
3492
  printLine(`${formatSkillInstallAction(result.action)} ${result.path}${suffix}`)
3110
3493
  }
@@ -3114,9 +3497,9 @@ async function applySkillInstallTargets(
3114
3497
  action: result.action as 'created' | 'updated',
3115
3498
  mode: result.mode,
3116
3499
  })
3117
- } catch (error: any) {
3118
- const message = error?.message || String(error)
3119
- if (!jsonMode) {
3500
+ } catch (error: unknown) {
3501
+ const message = getErrorMessage(error)
3502
+ if (printResults && !jsonMode) {
3120
3503
  printLine(`Failed ${installPath}: ${message}`)
3121
3504
  }
3122
3505
  outcomes.push({ target, path: installPath, action: 'failed', mode: null, error: message })
@@ -3294,17 +3677,161 @@ function describeSetupAuthState(): { state: 'signed-in' | 'signed-out', baseUrl:
3294
3677
  }
3295
3678
  }
3296
3679
 
3680
+ type SetupTeamChoice =
3681
+ | { kind: 'team', team: Team }
3682
+ | { kind: 'create' }
3683
+
3684
+ async function resolveSetupTeam(teamSlugArg: string | null, noInput: boolean, dryRun: boolean): Promise<Team> {
3685
+ const teams = await fetchTeams()
3686
+ const normalizedTeamSlug = teamSlugArg ? normalizeSlugInput(teamSlugArg) : null
3687
+
3688
+ if (normalizedTeamSlug) {
3689
+ const team = teams.find(candidate => candidate.slug === normalizedTeamSlug)
3690
+ if (!team) {
3691
+ throw new Error(`Team '${normalizedTeamSlug}' was not found in your accessible teams.`)
3692
+ }
3693
+ return team
3694
+ }
3695
+
3696
+ if (noInput || dryRun) {
3697
+ throw new Error('Authenticated setup requires --team <slug> when running non-interactively.')
3698
+ }
3699
+
3700
+ const choices: SetupTeamChoice[] = [
3701
+ ...teams.map(team => ({ kind: 'team' as const, team })),
3702
+ { kind: 'create' },
3703
+ ]
3704
+ const choice = await promptKeyboardSelect(
3705
+ 'Choose the team to set up in this directory',
3706
+ choices,
3707
+ item => item.kind === 'create'
3708
+ ? 'Create a new team'
3709
+ : `${item.team.name} (${item.team.slug})`
3710
+ )
3711
+
3712
+ if (choice.kind === 'team') {
3713
+ return choice.team
3714
+ }
3715
+
3716
+ const name = await askText(' Team name?', '')
3717
+ if (!name) {
3718
+ throw new Error('Team name is required to create a team.')
3719
+ }
3720
+ return createTeamOnServer(name)
3721
+ }
3722
+
3723
+ async function resolveSetupProjects(team: Team): Promise<Project[]> {
3724
+ const projects = await fetchProjects(team.slug)
3725
+
3726
+ if (projects.length === 0) {
3727
+ throw new Error(`Team '${team.slug}' has no projects. Create a project before running setup.`)
3728
+ }
3729
+
3730
+ return projects
3731
+ }
3732
+
3733
+ function projectSeedsFromProjects(projects: Project[]): WorkspaceProjectSeed[] {
3734
+ return projects.map(project => ({
3735
+ slug: project.slug,
3736
+ id: project.id,
3737
+ name: project.name,
3738
+ }))
3739
+ }
3740
+
3741
+ function formatProjectSetupProgress(count: number): string {
3742
+ return `${count} project${count === 1 ? '' : 's'} being set up in workspace...`
3743
+ }
3744
+
3745
+ interface SetupSkillChoice {
3746
+ agent: SkillInstallAgent
3747
+ label: string
3748
+ target: SkillInstallTarget
3749
+ }
3750
+
3751
+ function setupTargetForAgent(agent: SkillInstallAgent): SkillInstallTarget {
3752
+ return agent === 'codex' ? 'codex-user' : 'claude-user'
3753
+ }
3754
+
3755
+ function resolveSetupSkillHome(): string {
3756
+ return resolve(process.env.HOME || homedir())
3757
+ }
3758
+
3759
+ function detectedSetupSkillChoices(homeDir: string): SetupSkillChoice[] {
3760
+ const choices: SetupSkillChoice[] = []
3761
+ if (existsSync(join(homeDir, '.codex'))) {
3762
+ choices.push({
3763
+ agent: 'codex',
3764
+ label: 'Codex',
3765
+ target: 'codex-user',
3766
+ })
3767
+ }
3768
+
3769
+ if (existsSync(join(homeDir, '.claude'))) {
3770
+ choices.push({
3771
+ agent: 'claude',
3772
+ label: 'Claude Code',
3773
+ target: 'claude-user',
3774
+ })
3775
+ }
3776
+
3777
+ return choices
3778
+ }
3779
+
3780
+ function setupSkillTargetsFromArgs(): SkillInstallTarget[] {
3781
+ const targets: SkillInstallTarget[] = []
3782
+ for (const agent of getArgs('--agent')) {
3783
+ if (!isSkillInstallAgent(agent)) {
3784
+ throw new Error(
3785
+ `Unknown agent '${agent}'. Available agents: ${SKILL_INSTALL_AGENTS.join(', ')}`
3786
+ )
3787
+ }
3788
+ const target = setupTargetForAgent(agent)
3789
+ if (!targets.includes(target)) {
3790
+ targets.push(target)
3791
+ }
3792
+ }
3793
+ for (const target of getArgs('--target')) {
3794
+ if (!isSkillInstallTarget(target)) {
3795
+ throw new Error(
3796
+ `Unknown skill install target '${target}'. Available targets: ${SKILL_INSTALL_TARGETS.join(', ')}`
3797
+ )
3798
+ }
3799
+ if (!targets.includes(target)) {
3800
+ targets.push(target)
3801
+ }
3802
+ }
3803
+ return targets
3804
+ }
3805
+
3806
+ function setupSkillLabel(target: SkillInstallTarget): string {
3807
+ if (target === 'codex-user' || target === 'codex-project' || target === 'agent-user' || target === 'agents-project') {
3808
+ return 'Codex skill'
3809
+ }
3810
+ if (target === 'claude-user' || target === 'claude-project') return 'Claude Code skill'
3811
+ return 'Orizu skill'
3812
+ }
3813
+
3297
3814
  async function setupCommand() {
3298
3815
  const dryRun = hasArg('--dry-run')
3816
+ const validateOnly = hasArg('--validate') && !hasArg('--fix')
3817
+ const fix = hasArg('--fix')
3818
+ const verbose = hasArg('--verbose')
3819
+ const handoffRequested = hasArg('--handoff') || Boolean(getArg('--launch'))
3299
3820
  const skipConfirm = hasArg('--yes')
3300
- const noInput = hasArg('--no-input') || !isInteractiveTerminal()
3821
+ const noInput = hasArg('--no-input') || hasArg('--non-interactive') || !isInteractiveTerminal()
3822
+ const workspaceArg = getOptionalArgValue('--workspace')
3823
+ const workspaceRoot = workspaceArg ? resolve(expandHomePath(workspaceArg)) : process.cwd()
3824
+ const teamArg = getOptionalArgValue('--team')
3825
+ const projectArg = getOptionalArgValue('--project')
3826
+ const setupSkillHome = resolveSetupSkillHome()
3827
+ const installMode: SkillInstallMode = getArg('--mode') ? parseSkillInstallMode() : 'link'
3301
3828
 
3302
3829
  printBannerIfInteractive()
3303
3830
  printLine('Orizu setup')
3304
3831
  printLine('')
3305
3832
 
3306
- // Phase 1: login
3307
- printLine('1. Authentication')
3833
+ // Step 1: login
3834
+ printLine('Step 1: Login')
3308
3835
  let auth = describeSetupAuthState()
3309
3836
  if (hasArg('--skip-login')) {
3310
3837
  printLine(' Skipped (--skip-login).')
@@ -3312,70 +3839,159 @@ async function setupCommand() {
3312
3839
  printLine(` Already authenticated with ${sanitizeTerminalText(auth.baseUrl)}.`)
3313
3840
  } else if (noInput || dryRun) {
3314
3841
  printLine(' Not signed in. Run `orizu login` to authenticate.')
3315
- } else if (await askYesNo(' Sign in to Orizu now?', true)) {
3842
+ } else if (await waitForAnyKeyOrEscape(' Press any key to open a browser and connect to your account, or Esc to cancel.')) {
3316
3843
  await login()
3317
3844
  auth = describeSetupAuthState()
3318
3845
  } else {
3319
- printLine(' Skipped. Run `orizu login` when ready.')
3846
+ throw new Error('Setup cancelled.')
3320
3847
  }
3321
3848
  printLine('')
3322
3849
 
3323
- // Phase 2: agent integration
3324
- printLine('2. Agent integration')
3325
- const scope = parseSkillInstallScope()
3326
- const mode = parseSkillInstallMode()
3327
- let targets = hasArg('--no-install') ? [] : parseSkillInstallTargets(scope)
3328
- if (targets.length === 0 && !hasArg('--no-install')) {
3329
- if (noInput) {
3330
- printLine(' No agents selected. Pass --agent claude and/or --agent codex, or rerun interactively.')
3331
- } else {
3332
- targets = await promptSkillInstallTargets(scope)
3333
- }
3334
- }
3335
-
3336
- let installOutcomes: SkillInstallOutcome[] = []
3337
- if (targets.length > 0) {
3338
- installOutcomes = await applySkillInstallTargets(targets, { mode, skipConfirm, dryRun })
3339
- } else if (hasArg('--no-install')) {
3340
- printLine(' Skipped (--no-install).')
3341
- }
3342
- printLine('')
3343
-
3344
- // Phase 3: local workspace
3345
- printLine('3. Local workspace')
3346
- let workspaceState: 'created' | 'exists' | 'skipped' | 'would-create' = 'skipped'
3347
- if (hasArg('--no-workspace')) {
3850
+ // Step 2: local workspace
3851
+ printLine('Step 2: Setup your workspace')
3852
+ let workspaceState: 'created' | 'exists' | 'skipped' | 'would-create' | 'validated' | 'invalid' | 'repaired' = 'skipped'
3853
+ let workspaceResult: ReturnType<typeof initOrizuWorkspace> | null = null
3854
+ let validationLogPath: string | null = null
3855
+ let hostedAttached = false
3856
+ if (hasArg('--no-workspace') && !validateOnly && !fix) {
3348
3857
  printLine(' Skipped (--no-workspace).')
3349
- } else if (workspaceExists()) {
3350
- workspaceState = 'exists'
3351
- printLine(` ${WORKSPACE_DIR_NAME}/ already initialized.`)
3352
3858
  } else {
3353
- const wantsWorkspace = hasArg('--workspace') ||
3354
- (!noInput && await askYesNo(` Create a gitignored ${WORKSPACE_DIR_NAME}/ workspace for local Orizu artifacts?`, true))
3859
+ const wantsWorkspace = validateOnly
3860
+ || fix
3861
+ || hasArg('--workspace')
3862
+ || Boolean(teamArg || projectArg)
3863
+ || workspaceExists(workspaceRoot)
3864
+ || !noInput
3355
3865
  if (wantsWorkspace) {
3866
+ let teamSlug = teamArg
3867
+ const projectSlug = projectArg
3868
+ let teamId: string | null = null
3869
+ let projects: WorkspaceProjectSeed[] | undefined
3870
+
3871
+ if (auth.state === 'signed-in' && !validateOnly) {
3872
+ const team = await resolveSetupTeam(teamSlug, noInput, dryRun)
3873
+ const serverProjects = await resolveSetupProjects(team)
3874
+ teamSlug = team.slug
3875
+ teamId = team.id
3876
+ projects = projectSeedsFromProjects(serverProjects)
3877
+ printLine(` ${formatProjectSetupProgress(projects.length)}`)
3878
+ if (!hasArg('--local') && !noInput && !dryRun) { let link: GithubLinkResult | null = null; try { link = await runGithubLink(team.slug, { print: printLine, fetcher: authedFetch, openUrl: openInBrowser, confirm: (org) => askYesNo(` Connect this team to GitHub org '${org}'?`, true) }) } catch (error) { printLine(` GitHub link skipped: ${getErrorMessage(error)} Run \`orizu github link --team ${team.slug}\` to finish.`) }
3879
+ if (link) { try { hostedAttached = (await runInteractiveHostedSetup({ teamSlug: team.slug, targetDir: workspaceRoot, local: hasArg('--local'), activeInstallation: link.status === 'active' }, { print: printLine, fetcher: authedFetch })).attached } catch (error) { throw new Error(`Hosted setup failed: ${getErrorMessage(error)} Remove any partial clone in ${workspaceRoot} if one exists, then re-run \`orizu setup\` (or pass --local to scaffold a local-only workspace).`) } } }
3880
+ } else if (!teamSlug && !noInput && !validateOnly) {
3881
+ teamSlug = await askText(' Team slug?', 'local-team')
3882
+ }
3883
+ if (auth.state !== 'signed-in' && (!teamSlug || !projectSlug) && noInput && hasArg('--workspace')) {
3884
+ printLine(' Using starter local-only team/project. Pass --team with authentication to materialize server projects.')
3885
+ }
3886
+ if (projectSlug && auth.state !== 'signed-in') {
3887
+ projects = [{ slug: projectSlug }]
3888
+ }
3889
+ if (auth.state !== 'signed-in' && !validateOnly) {
3890
+ const localProjectCount = projects?.length || 1
3891
+ printLine(` ${formatProjectSetupProgress(localProjectCount)}`)
3892
+ }
3893
+
3894
+ if (hostedAttached) { workspaceState = 'exists' } else {
3356
3895
  const result = initOrizuWorkspace({
3896
+ workspaceRoot,
3897
+ teamSlug,
3898
+ teamId,
3899
+ projectSlug: auth.state === 'signed-in' ? null : projectSlug,
3900
+ projects,
3357
3901
  baseUrl: auth.baseUrl,
3902
+ serviceOrigin: getOptionalArgValue('--service-origin') || auth.baseUrl,
3903
+ attachWorkspaceId: getOptionalArgValue('--attach-workspace'),
3358
3904
  cliVersion: getCliVersion(),
3359
3905
  dryRun,
3906
+ validateOnly,
3907
+ fix,
3908
+ noSymlinks: hasArg('--no-symlinks'),
3360
3909
  })
3361
- workspaceState = result.state === 'would-create' ? 'would-create' : result.state
3362
- for (const action of result.actions) {
3363
- printLine(` ${dryRun ? 'Would ' : 'Did '}${action}`)
3910
+ workspaceResult = result
3911
+ workspaceState = result.state
3912
+ if (dryRun || verbose) {
3913
+ for (const action of result.actions) {
3914
+ printLine(` ${dryRun ? 'Would ' : 'Did '}${action}`)
3915
+ }
3916
+ } else if (result.actions.length > 0) {
3917
+ printLine(validateOnly ? ' Workspace validation complete' : ' Workspace setup complete')
3918
+ } else if (!validateOnly) {
3919
+ printLine(' Workspace setup complete')
3920
+ }
3921
+ validationLogPath = writeWorkspaceValidationLog(result.root, result.findings, dryRun)
3922
+ if (verbose) {
3923
+ printWorkspaceFindings(result.findings)
3924
+ }
3925
+ if ((validateOnly || fix) && result.findings.some(finding => finding.severity === 'error')) {
3926
+ process.exitCode = 1
3927
+ }
3928
+ }
3929
+ } else {
3930
+ printLine(` Skipped. Rerun with --workspace to initialize the workspace contract.`)
3931
+ }
3932
+ }
3933
+ printLine('')
3934
+
3935
+ // Step 3: global coding-agent skills
3936
+ let installOutcomes: SkillInstallOutcome[] = []
3937
+ printLine('Step 3: Install coding agent skills')
3938
+ if (hasArg('--no-install') || validateOnly || fix) {
3939
+ printLine(hasArg('--no-install') ? ' Skipped (--no-install).' : ' Skipped for validation/repair.')
3940
+ } else {
3941
+ let targets = setupSkillTargetsFromArgs()
3942
+ if (targets.length === 0 && !noInput && !dryRun) {
3943
+ const choices = detectedSetupSkillChoices(setupSkillHome)
3944
+ if (choices.length > 0) {
3945
+ const selectedChoices = await promptKeyboardMultiSelect(
3946
+ 'Choose the coding agents that should learn how to use Orizu',
3947
+ choices,
3948
+ choice => choice.label,
3949
+ () => true
3950
+ )
3951
+ targets = selectedChoices.map(choice => choice.target)
3952
+ } else {
3953
+ printLine(' No Codex or Claude Code agent folders were detected.')
3364
3954
  }
3955
+ }
3956
+
3957
+ if (targets.length === 0) {
3958
+ printLine(' Skipped skill install.')
3365
3959
  } else {
3366
- printLine(` Skipped. Rerun with --workspace to create ${WORKSPACE_DIR_NAME}/.`)
3960
+ printLine(' Installing skills...')
3961
+ installOutcomes = await applySkillInstallTargets(targets, {
3962
+ mode: installMode,
3963
+ skipConfirm,
3964
+ dryRun,
3965
+ cwd: workspaceRoot,
3966
+ homeDir: setupSkillHome,
3967
+ showPlan: false,
3968
+ printResults: false,
3969
+ })
3970
+ if (dryRun) {
3971
+ printLine(' Dry run: no skill files were changed.')
3972
+ }
3973
+ for (const outcome of installOutcomes) {
3974
+ const label = setupSkillLabel(outcome.target)
3975
+ if (outcome.action === 'failed') {
3976
+ printLine(` ${label} failed: ${outcome.error}`)
3977
+ } else if (outcome.action === 'skipped') {
3978
+ printLine(` ${label} skipped`)
3979
+ } else {
3980
+ printLine(` ${label} installed`)
3981
+ }
3982
+ }
3367
3983
  }
3368
3984
  }
3369
3985
  printLine('')
3370
3986
 
3371
3987
  // Phase 4: coding-agent handoff
3372
3988
  const handoffPrompt = renderAgentSetupPrompt({
3373
- workspacePath: workspaceState === 'created' || workspaceState === 'exists'
3374
- ? getWorkspaceRoot()
3989
+ workspacePath: workspaceResult && workspaceState !== 'invalid'
3990
+ ? getWorkspaceRoot(workspaceRoot)
3375
3991
  : null,
3376
3992
  })
3377
- if (!hasArg('--no-handoff')) {
3378
- printLine('4. Coding-agent handoff')
3993
+ if (handoffRequested && !hasArg('--no-handoff')) {
3994
+ printLine('Step 4: Coding-agent handoff')
3379
3995
  printLine(' Give your coding agent this prompt to plan repo-specific Orizu adoption:')
3380
3996
  printLine('')
3381
3997
  printLine('--- prompt start ---')
@@ -3415,7 +4031,7 @@ async function setupCommand() {
3415
4031
  const summaryTargets = installOutcomes.length > 0
3416
4032
  ? installOutcomes
3417
4033
  .filter(outcome => outcome.action !== 'failed')
3418
- .map(outcome => ({ outcome, status: getSkillTargetStatus(outcome.target) }))
4034
+ .map(outcome => ({ outcome, status: getSkillTargetStatus(outcome.target, { cwd: workspaceRoot, homeDir: setupSkillHome }) }))
3419
4035
  : []
3420
4036
  const source = resolveSkillSource()
3421
4037
 
@@ -3435,7 +4051,21 @@ async function setupCommand() {
3435
4051
  cliVersion: getCliVersion(),
3436
4052
  skillHash: computeSkillContentHash(source.root),
3437
4053
  },
3438
- workspace: { state: workspaceState, path: getWorkspaceRoot() },
4054
+ workspace: {
4055
+ state: workspaceState,
4056
+ path: getWorkspaceRoot(workspaceRoot),
4057
+ validationLogPath,
4058
+ operations: workspaceResult?.operations.map(op => ({
4059
+ action: op.action,
4060
+ path: op.path,
4061
+ safe: op.safe,
4062
+ reason: op.reason,
4063
+ entries: op.entries,
4064
+ sourcePath: op.sourcePath,
4065
+ target: op.target,
4066
+ })) || [],
4067
+ findings: workspaceResult?.findings || [],
4068
+ },
3439
4069
  })
3440
4070
  return
3441
4071
  }
@@ -3443,21 +4073,23 @@ async function setupCommand() {
3443
4073
  printLine('Setup summary')
3444
4074
  printLine(` Auth: ${auth.state === 'signed-in' ? `signed in (${sanitizeTerminalText(auth.baseUrl)})` : 'not signed in — run `orizu login`'}`)
3445
4075
  if (summaryTargets.length > 0) {
3446
- printLine(' Integrations:')
4076
+ printLine(' Skills:')
3447
4077
  for (const { outcome, status } of summaryTargets) {
3448
4078
  const modeLabel = status.mode ? `, ${status.mode}` : ''
3449
4079
  printLine(` ${outcome.target.padEnd(16)} ${status.state}${modeLabel} ${outcome.path}`)
3450
4080
  }
3451
4081
  } else {
3452
- printLine(' Integrations: none installed this run — `orizu install-skill` sets them up.')
4082
+ printLine(' Skills: none installed this run')
3453
4083
  }
3454
4084
  const failed = installOutcomes.filter(outcome => outcome.action === 'failed')
3455
4085
  for (const failure of failed) {
3456
4086
  printLine(` ${failure.target.padEnd(16)} failed: ${failure.error}`)
3457
4087
  }
3458
- printLine(` Skill source: ${source.root} (${source.source})`)
3459
- printLine(` Workspace: ${workspaceState === 'skipped' ? 'skipped' : `${getWorkspaceRoot()} (${workspaceState})`}`)
3460
- printLine(' Next: hand the prompt above to your coding agent to plan repo-specific Orizu adoption.')
4088
+ printLine(` Workspace: ${workspaceState === 'skipped' ? 'skipped' : getWorkspaceRoot(workspaceRoot)}`)
4089
+ if (workspaceResult) {
4090
+ printLine(` Validation: ${validationSummary(workspaceResult.findings, validationLogPath)}`)
4091
+ }
4092
+ printLine(' Next: run `orizu setup prompt` if you want a coding-agent handoff prompt.')
3461
4093
  }
3462
4094
 
3463
4095
  function setupPromptCommand() {
@@ -3480,8 +4112,8 @@ function readJsonFile(pathArg: string): Record<string, unknown> {
3480
4112
  }
3481
4113
 
3482
4114
  return parsed as Record<string, unknown>
3483
- } catch (error: any) {
3484
- throw new Error(`Invalid JSON file '${pathArg}': ${error?.message || String(error)}`)
4115
+ } catch (error: unknown) {
4116
+ throw new Error(`Invalid JSON file '${pathArg}': ${getErrorMessage(error)}`)
3485
4117
  }
3486
4118
  }
3487
4119
 
@@ -3558,141 +4190,6 @@ function writeTextFileEnsuringDir(pathArg: string, content: string) {
3558
4190
  writeFileSync(pathArg, content)
3559
4191
  }
3560
4192
 
3561
- function shouldExcludeArtifactPath(relativePath: string): boolean {
3562
- const parts = relativePath.split('/')
3563
- return parts.some(part =>
3564
- part === '.git' ||
3565
- part === '.DS_Store' ||
3566
- part === '__pycache__' ||
3567
- part === '.pytest_cache'
3568
- )
3569
- }
3570
-
3571
- function collectArtifactFiles(sourceDir: string, relativeDir = ''): string[] {
3572
- const absoluteDir = relativeDir ? join(sourceDir, relativeDir) : sourceDir
3573
- return readdirSync(absoluteDir, { withFileTypes: true })
3574
- .flatMap(entry => {
3575
- const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name
3576
- if (shouldExcludeArtifactPath(relativePath)) {
3577
- return []
3578
- }
3579
- if (entry.isDirectory()) {
3580
- return collectArtifactFiles(sourceDir, relativePath)
3581
- }
3582
- return entry.isFile() ? [relativePath] : []
3583
- })
3584
- .sort((left, right) => left < right ? -1 : left > right ? 1 : 0)
3585
- }
3586
-
3587
- const ZIP_GENERAL_PURPOSE_UTF8 = 0x0800
3588
- const ZIP_DOS_TIME_2000_01_01 = 0
3589
- const ZIP_DOS_DATE_2000_01_01 = ((2000 - 1980) << 9) | (1 << 5) | 1
3590
- const CRC32_TABLE = Array.from({ length: 256 }, (_, index) => {
3591
- let value = index
3592
- for (let bit = 0; bit < 8; bit++) {
3593
- value = (value & 1) ? (0xedb88320 ^ (value >>> 1)) : (value >>> 1)
3594
- }
3595
- return value >>> 0
3596
- })
3597
-
3598
- function crc32(bytes: Buffer): number {
3599
- let crc = 0xffffffff
3600
- for (const byte of bytes) {
3601
- crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8)
3602
- }
3603
- return (crc ^ 0xffffffff) >>> 0
3604
- }
3605
-
3606
- function createStoredZip(entries: Array<{ path: string; data: Buffer }>): Buffer {
3607
- const localParts: Buffer[] = []
3608
- const centralParts: Buffer[] = []
3609
- let offset = 0
3610
-
3611
- for (const entry of entries) {
3612
- const name = Buffer.from(entry.path, 'utf8')
3613
- const crc = crc32(entry.data)
3614
- const localHeader = Buffer.alloc(30)
3615
- localHeader.writeUInt32LE(0x04034b50, 0)
3616
- localHeader.writeUInt16LE(10, 4)
3617
- localHeader.writeUInt16LE(ZIP_GENERAL_PURPOSE_UTF8, 6)
3618
- localHeader.writeUInt16LE(0, 8)
3619
- localHeader.writeUInt16LE(ZIP_DOS_TIME_2000_01_01, 10)
3620
- localHeader.writeUInt16LE(ZIP_DOS_DATE_2000_01_01, 12)
3621
- localHeader.writeUInt32LE(crc, 14)
3622
- localHeader.writeUInt32LE(entry.data.length, 18)
3623
- localHeader.writeUInt32LE(entry.data.length, 22)
3624
- localHeader.writeUInt16LE(name.length, 26)
3625
- localHeader.writeUInt16LE(0, 28)
3626
-
3627
- localParts.push(localHeader, name, entry.data)
3628
-
3629
- const centralHeader = Buffer.alloc(46)
3630
- centralHeader.writeUInt32LE(0x02014b50, 0)
3631
- centralHeader.writeUInt16LE(20, 4)
3632
- centralHeader.writeUInt16LE(10, 6)
3633
- centralHeader.writeUInt16LE(ZIP_GENERAL_PURPOSE_UTF8, 8)
3634
- centralHeader.writeUInt16LE(0, 10)
3635
- centralHeader.writeUInt16LE(ZIP_DOS_TIME_2000_01_01, 12)
3636
- centralHeader.writeUInt16LE(ZIP_DOS_DATE_2000_01_01, 14)
3637
- centralHeader.writeUInt32LE(crc, 16)
3638
- centralHeader.writeUInt32LE(entry.data.length, 20)
3639
- centralHeader.writeUInt32LE(entry.data.length, 24)
3640
- centralHeader.writeUInt16LE(name.length, 28)
3641
- centralHeader.writeUInt16LE(0, 30)
3642
- centralHeader.writeUInt16LE(0, 32)
3643
- centralHeader.writeUInt16LE(0, 34)
3644
- centralHeader.writeUInt16LE(0, 36)
3645
- centralHeader.writeUInt32LE(0, 38)
3646
- centralHeader.writeUInt32LE(offset, 42)
3647
- centralParts.push(centralHeader, name)
3648
-
3649
- offset += localHeader.length + name.length + entry.data.length
3650
- }
3651
-
3652
- const centralDirectoryOffset = offset
3653
- const centralDirectory = Buffer.concat(centralParts)
3654
- const endOfCentralDirectory = Buffer.alloc(22)
3655
- endOfCentralDirectory.writeUInt32LE(0x06054b50, 0)
3656
- endOfCentralDirectory.writeUInt16LE(0, 4)
3657
- endOfCentralDirectory.writeUInt16LE(0, 6)
3658
- endOfCentralDirectory.writeUInt16LE(entries.length, 8)
3659
- endOfCentralDirectory.writeUInt16LE(entries.length, 10)
3660
- endOfCentralDirectory.writeUInt32LE(centralDirectory.length, 12)
3661
- endOfCentralDirectory.writeUInt32LE(centralDirectoryOffset, 16)
3662
- endOfCentralDirectory.writeUInt16LE(0, 20)
3663
-
3664
- return Buffer.concat([...localParts, centralDirectory, endOfCentralDirectory])
3665
- }
3666
-
3667
- function zipDirectoryToBase64(dirArg: string): { zipBase64: string; contentSha256: string } {
3668
- const sourceDir = expandHomePath(dirArg)
3669
- try {
3670
- const stats = statSync(sourceDir)
3671
- if (!stats.isDirectory()) {
3672
- throw new Error(`${sourceDir} is not a directory`)
3673
- }
3674
- } catch (error: any) {
3675
- if (error?.code === 'ENOENT') {
3676
- throw new Error(`Directory not found: ${sourceDir}`)
3677
- }
3678
- throw error
3679
- }
3680
-
3681
- const files = collectArtifactFiles(sourceDir)
3682
- if (files.length === 0) {
3683
- throw new Error(`Directory contains no artifact files: ${sourceDir}`)
3684
- }
3685
-
3686
- const bytes = createStoredZip(files.map(relativePath => ({
3687
- path: relativePath,
3688
- data: readFileSync(join(sourceDir, relativePath)),
3689
- })))
3690
- return {
3691
- zipBase64: bytes.toString('base64'),
3692
- contentSha256: createHash('sha256').update(bytes).digest('hex'),
3693
- }
3694
- }
3695
-
3696
4193
  function readManifestFile(dirArg: string): Record<string, unknown> {
3697
4194
  return readJsonFile(join(expandHomePath(dirArg), 'manifest.json'))
3698
4195
  }
@@ -3908,43 +4405,111 @@ function parseCommaSeparated(value: string | null): string[] {
3908
4405
  .filter(Boolean)
3909
4406
  }
3910
4407
 
4408
+ function readAssignmentManifest(path: string | null): ReturnType<typeof readAssignmentManifestJsonlFile> | null {
4409
+ if (!path) {
4410
+ return null
4411
+ }
4412
+
4413
+ return readAssignmentManifestJsonlFile(expandHomePath(path))
4414
+ }
4415
+
4416
+ function readOptionalTaskTextArg(
4417
+ valueFlag: string,
4418
+ fileFlag: string
4419
+ ): string | undefined {
4420
+ const value = getArg(valueFlag)
4421
+ const file = getArg(fileFlag)
4422
+ rejectDashPrefixedOptionValue(valueFlag, value)
4423
+ rejectDashPrefixedOptionValue(fileFlag, file)
4424
+
4425
+ if (value !== null && file !== null) {
4426
+ throw new Error(`Use either ${valueFlag} or ${fileFlag}, not both.`)
4427
+ }
4428
+
4429
+ if (file !== null) {
4430
+ return readSourceFile(file)
4431
+ }
4432
+
4433
+ return value ?? undefined
4434
+ }
4435
+
4436
+ function readPositiveIntegerFlag(name: string): number | undefined {
4437
+ const value = getArg(name)
4438
+ rejectDashPrefixedOptionValue(name, value)
4439
+ if (value === null) {
4440
+ return undefined
4441
+ }
4442
+
4443
+ const parsed = Number(value)
4444
+ if (!Number.isInteger(parsed) || parsed < 1) {
4445
+ throw new Error(`${name} must be a positive integer`)
4446
+ }
4447
+
4448
+ return parsed
4449
+ }
4450
+
3911
4451
  async function createTask() {
3912
4452
  const projectSlug = getArg('--project')
3913
4453
  const datasetId = getArg('--dataset')
3914
4454
  const appId = getArg('--app')
3915
4455
  const title = getArg('--title')
3916
4456
  const assignees = parseCommaSeparated(getArg('--assignees'))
4457
+ const assignmentFile = getArg('--assignment-file')
3917
4458
  const publish = hasArg('--publish')
3918
4459
  const versionArg = getArg('--version')
3919
4460
  const instructions = getArg('--instructions')
3920
4461
  const labelsPerItemArg = getArg('--labels-per-item')
3921
- const labelsPerItem = labelsPerItemArg ? Number(labelsPerItemArg) : 1
4462
+ const parsedLabelsPerItem = labelsPerItemArg ? Number(labelsPerItemArg) : 1
4463
+ const labelsPerItem =
4464
+ Number.isInteger(parsedLabelsPerItem) && parsedLabelsPerItem > 0
4465
+ ? parsedLabelsPerItem
4466
+ : null
3922
4467
  const parsedVersionNum = versionArg ? Number(versionArg) : Number.NaN
3923
4468
  const versionNum =
3924
4469
  Number.isInteger(parsedVersionNum) && parsedVersionNum > 0 ? parsedVersionNum : null
3925
4470
 
3926
- if (!projectSlug || !datasetId || !appId || !title || (publish && assignees.length === 0)) {
3927
- throw new Error('Usage: orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> [--assignees <userIdOrEmail1,userIdOrEmail2>] [--publish] [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]')
4471
+ if (assignmentFile && assignees.length > 0) {
4472
+ throw new Error('Use either --assignment-file or --assignees, not both.')
4473
+ }
4474
+
4475
+ const explicitAssignments = readAssignmentManifest(assignmentFile)
4476
+
4477
+ if (!projectSlug || !datasetId || !appId || !title || (publish && assignees.length === 0 && !explicitAssignments)) {
4478
+ throw new Error('Usage: orizu tasks create --project <team/project> --dataset <datasetId> --app <appId> --title <title> [--assignees <userIdOrEmail1,userIdOrEmail2> | --assignment-file <path>] [--publish] [--version <n>] [--instructions <text>] [--labels-per-item <n>] [--json]')
3928
4479
  }
3929
4480
 
3930
4481
  if (versionArg && versionNum === null) {
3931
4482
  throw new Error('--version must be a positive integer')
3932
4483
  }
3933
4484
 
4485
+ if (labelsPerItemArg && labelsPerItem === null) {
4486
+ throw new Error('--labels-per-item must be a positive integer')
4487
+ }
4488
+
4489
+ const requestBody: Record<string, unknown> = {
4490
+ projectSlug,
4491
+ datasetId,
4492
+ appId,
4493
+ versionNum,
4494
+ title,
4495
+ publish,
4496
+ instructions,
4497
+ }
4498
+
4499
+ if (explicitAssignments) {
4500
+ requestBody.explicitAssignments = explicitAssignments
4501
+ } else {
4502
+ requestBody.memberIds = assignees
4503
+ }
4504
+
4505
+ if (!explicitAssignments || labelsPerItemArg) {
4506
+ requestBody.requiredAssignmentsPerRow = labelsPerItem
4507
+ }
4508
+
3934
4509
  const response = await authedFetch('/api/cli/tasks', {
3935
4510
  method: 'POST',
3936
4511
  headers: { 'Content-Type': 'application/json' },
3937
- body: JSON.stringify({
3938
- projectSlug,
3939
- datasetId,
3940
- appId,
3941
- versionNum,
3942
- title,
3943
- memberIds: assignees,
3944
- publish,
3945
- instructions,
3946
- requiredAssignmentsPerRow: labelsPerItem,
3947
- }),
4512
+ body: JSON.stringify(requestBody),
3948
4513
  })
3949
4514
 
3950
4515
  if (!response.ok) {
@@ -3982,13 +4547,16 @@ async function createTask() {
3982
4547
  taskUrl: `${getBaseUrl()}/d/${projectSlug}/tasks/${data.task.id}`,
3983
4548
  title: data.task.title,
3984
4549
  status: data.task.status,
4550
+ assignmentMode: explicitAssignments ? 'custom' : 'auto',
3985
4551
  requiredAssignmentsPerRow: data.task.requiredAssignmentsPerRow,
3986
4552
  assignmentsCreated: data.assignmentsCreated,
3987
4553
  draft: data.task.status === 'draft',
3988
4554
  ...(data.task.status === 'draft'
3989
4555
  ? {
3990
4556
  message: 'Draft created. Test it manually before assigning.',
3991
- publishCommand: `orizu tasks publish --task ${data.task.id} --assignees <userId1,userId2>`,
4557
+ publishCommand: explicitAssignments
4558
+ ? `orizu tasks publish --task ${data.task.id} --assignment-file ${assignmentFile ? shellQuote(assignmentFile) : '<path>'}`
4559
+ : `orizu tasks publish --task ${data.task.id} --assignees <userId1,userId2>`,
3992
4560
  }
3993
4561
  : {}),
3994
4562
  ...(data.assignmentShortfall !== undefined ? { assignmentShortfall: data.assignmentShortfall } : {}),
@@ -4005,28 +4573,193 @@ async function createTask() {
4005
4573
  `\n Task ID: ${sanitizeTerminalText(data.task.id)}` +
4006
4574
  `\n Dataset ID: ${sanitizeTerminalText(datasetId)}` +
4007
4575
  `\n Version: v${data.task.versionNum} (${sanitizeTerminalText(data.task.versionId)})` +
4576
+ `\n Mode: ${explicitAssignments ? 'custom row map' : 'auto distribute'}` +
4008
4577
  `\n Labels/row: ${data.task.requiredAssignmentsPerRow}` +
4009
4578
  `\n Assignments: ${data.assignmentsCreated}` +
4010
4579
  (data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : '') +
4011
4580
  `\n URL: ${sanitizeTerminalText(taskUrl)}` +
4012
4581
  (isDraft
4013
- ? `\n\nThis task is a draft. Test it manually before assigning.\nPublish after approval: orizu tasks publish --task ${sanitizeTerminalText(data.task.id)} --assignees <userId1,userId2>`
4582
+ ? `\n\nThis task is a draft. Test it manually before assigning.\nPublish after approval: ${
4583
+ explicitAssignments
4584
+ ? `orizu tasks publish --task ${sanitizeTerminalText(data.task.id)} --assignment-file ${sanitizeTerminalText(assignmentFile || '<path>')}`
4585
+ : `orizu tasks publish --task ${sanitizeTerminalText(data.task.id)} --assignees <userId1,userId2>`
4586
+ }`
4014
4587
  : '')
4015
4588
  )
4016
4589
  }
4017
4590
 
4591
+ async function updateTask() {
4592
+ const taskId = getArg('--task')
4593
+ rejectDashPrefixedOptionValue('--task', taskId)
4594
+ if (!taskId) {
4595
+ throw new Error('Usage: orizu tasks update --task <taskId> [--title <text>] [--description <text>|--description-file <path>] [--instructions <text>|--instructions-file <path>] [--dataset <datasetId>] [--app <appId> [--version <n>]] [--labels-per-item <n>] [--assignees <userIdOrEmail1,userIdOrEmail2> | --assignment-file <path>] [--json]')
4596
+ }
4597
+
4598
+ const title = getArg('--title')
4599
+ const description = readOptionalTaskTextArg('--description', '--description-file')
4600
+ const instructions = readOptionalTaskTextArg('--instructions', '--instructions-file')
4601
+ const datasetId = getArg('--dataset')
4602
+ const appId = getArg('--app')
4603
+ const versionNum = readPositiveIntegerFlag('--version')
4604
+ const requiredAssignmentsPerRow = readPositiveIntegerFlag('--labels-per-item')
4605
+ const assignees = parseCommaSeparated(getArg('--assignees'))
4606
+ const assignmentFile = getArg('--assignment-file')
4607
+
4608
+ rejectDashPrefixedOptionValue('--title', title)
4609
+ rejectDashPrefixedOptionValue('--dataset', datasetId)
4610
+ rejectDashPrefixedOptionValue('--app', appId)
4611
+ rejectDashPrefixedOptionValue('--assignment-file', assignmentFile)
4612
+
4613
+ if (title !== null && title.trim().length === 0) {
4614
+ throw new Error('--title must be a non-empty string')
4615
+ }
4616
+
4617
+ if (assignmentFile && assignees.length > 0) {
4618
+ throw new Error('Use either --assignment-file or --assignees, not both.')
4619
+ }
4620
+
4621
+ if (hasArg('--assignees') && assignees.length === 0) {
4622
+ throw new Error('--assignees requires at least one user ID or email')
4623
+ }
4624
+
4625
+ if (versionNum !== undefined && !appId) {
4626
+ throw new Error('--version requires --app')
4627
+ }
4628
+
4629
+ const explicitAssignments = readAssignmentManifest(assignmentFile)
4630
+ const requestBody: Record<string, unknown> = {}
4631
+ if (title !== null) requestBody.title = title.trim()
4632
+ if (description !== undefined) requestBody.description = description
4633
+ if (instructions !== undefined) requestBody.instructions = instructions
4634
+ if (datasetId !== null) requestBody.datasetId = datasetId
4635
+ if (appId !== null) requestBody.appId = appId
4636
+ if (versionNum !== undefined) requestBody.versionNum = versionNum
4637
+ if (requiredAssignmentsPerRow !== undefined) {
4638
+ requestBody.requiredAssignmentsPerRow = requiredAssignmentsPerRow
4639
+ }
4640
+ if (explicitAssignments) {
4641
+ requestBody.explicitAssignments = explicitAssignments
4642
+ } else if (hasArg('--assignees')) {
4643
+ requestBody.memberIds = assignees
4644
+ }
4645
+
4646
+ if (Object.keys(requestBody).length === 0) {
4647
+ throw new Error('Usage: orizu tasks update --task <taskId> [--title <text>] [--description <text>|--description-file <path>] [--instructions <text>|--instructions-file <path>] [--dataset <datasetId>] [--app <appId> [--version <n>]] [--labels-per-item <n>] [--assignees <userIdOrEmail1,userIdOrEmail2> | --assignment-file <path>] [--json]')
4648
+ }
4649
+
4650
+ const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}`, {
4651
+ method: 'PATCH',
4652
+ headers: { 'Content-Type': 'application/json' },
4653
+ body: JSON.stringify(requestBody),
4654
+ })
4655
+
4656
+ if (!response.ok) {
4657
+ throw new Error(`Failed to update task: ${await extractErrorMessage(response)}`)
4658
+ }
4659
+
4660
+ const data = await parseJsonResponse<{
4661
+ task: {
4662
+ id: string
4663
+ title?: string | null
4664
+ status?: string | null
4665
+ dataset_id?: string | null
4666
+ app_id?: string | null
4667
+ version_id?: string | null
4668
+ required_assignments_per_row?: number | null
4669
+ }
4670
+ assignmentMode?: string
4671
+ assignmentsCreated?: number
4672
+ }>(response, 'Task update')
4673
+
4674
+ if (hasJsonFlag()) {
4675
+ printJson({
4676
+ taskId: data.task.id,
4677
+ title: data.task.title ?? null,
4678
+ status: data.task.status ?? null,
4679
+ datasetId: data.task.dataset_id ?? null,
4680
+ appId: data.task.app_id ?? null,
4681
+ versionId: data.task.version_id ?? null,
4682
+ requiredAssignmentsPerRow: data.task.required_assignments_per_row ?? null,
4683
+ ...(data.assignmentMode ? { assignmentMode: data.assignmentMode } : {}),
4684
+ ...(data.assignmentsCreated !== undefined
4685
+ ? { assignmentsCreated: data.assignmentsCreated }
4686
+ : {}),
4687
+ })
4688
+ return
4689
+ }
4690
+
4691
+ printLine(
4692
+ `Updated draft task ${sanitizeTerminalText(data.task.title || data.task.id)} (${sanitizeTerminalText(data.task.id)})` +
4693
+ (data.task.status ? ` [${sanitizeTerminalText(data.task.status)}]` : '') +
4694
+ (data.task.dataset_id ? `\n Dataset ID: ${sanitizeTerminalText(data.task.dataset_id)}` : '') +
4695
+ (data.task.version_id ? `\n Version ID: ${sanitizeTerminalText(data.task.version_id)}` : '') +
4696
+ (data.task.required_assignments_per_row
4697
+ ? `\n Labels/row: ${data.task.required_assignments_per_row}`
4698
+ : '') +
4699
+ (data.assignmentMode ? `\n Mode: ${sanitizeTerminalText(data.assignmentMode === 'custom' ? 'custom row map' : 'auto distribute')}` : '') +
4700
+ (data.assignmentsCreated !== undefined ? `\n Assignments: ${data.assignmentsCreated}` : '')
4701
+ )
4702
+ }
4703
+
4704
+ async function discardTask() {
4705
+ const taskId = getArg('--task')
4706
+ rejectDashPrefixedOptionValue('--task', taskId)
4707
+ if (!taskId) {
4708
+ throw new Error('Usage: orizu tasks discard --task <taskId> [--yes] [--json]')
4709
+ }
4710
+
4711
+ if (!hasArg('--yes')) {
4712
+ if (!isInteractiveTerminal()) {
4713
+ throw new Error('Refusing to discard a draft in non-interactive mode without --yes')
4714
+ }
4715
+
4716
+ const confirmed = await askYesNo(`Discard draft task ${taskId}? This cannot be undone.`, false)
4717
+ if (!confirmed) {
4718
+ printLine('Discard cancelled.')
4719
+ return
4720
+ }
4721
+ }
4722
+
4723
+ const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}`, {
4724
+ method: 'DELETE',
4725
+ })
4726
+
4727
+ if (!response.ok) {
4728
+ throw new Error(`Failed to discard draft task: ${await extractErrorMessage(response)}`)
4729
+ }
4730
+
4731
+ const data = await parseJsonResponse<{ success: boolean }>(response, 'Task discard')
4732
+ if (hasJsonFlag()) {
4733
+ printJson({ taskId, discarded: data.success })
4734
+ return
4735
+ }
4736
+
4737
+ printLine(`Discarded draft task ${sanitizeTerminalText(taskId)}.`)
4738
+ }
4739
+
4018
4740
  async function publishTask() {
4019
4741
  const taskId = getArg('--task')
4020
4742
  const assignees = parseCommaSeparated(getArg('--assignees'))
4743
+ const assignmentFile = getArg('--assignment-file')
4744
+
4745
+ if (assignmentFile && assignees.length > 0) {
4746
+ throw new Error('Use either --assignment-file or --assignees, not both.')
4747
+ }
4748
+
4749
+ const explicitAssignments = readAssignmentManifest(assignmentFile)
4021
4750
 
4022
- if (!taskId || assignees.length === 0) {
4023
- throw new Error('Usage: orizu tasks publish --task <taskId> --assignees <userId1,userId2> [--json]')
4751
+ if (!taskId || (assignees.length === 0 && !explicitAssignments)) {
4752
+ throw new Error('Usage: orizu tasks publish --task <taskId> (--assignees <userId1,userId2> | --assignment-file <path>) [--json]')
4024
4753
  }
4025
4754
 
4026
4755
  const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/publish`, {
4027
4756
  method: 'POST',
4028
4757
  headers: { 'Content-Type': 'application/json' },
4029
- body: JSON.stringify({ memberIds: assignees }),
4758
+ body: JSON.stringify(
4759
+ explicitAssignments
4760
+ ? { explicitAssignments }
4761
+ : { memberIds: assignees }
4762
+ ),
4030
4763
  })
4031
4764
 
4032
4765
  if (!response.ok) {
@@ -4049,6 +4782,7 @@ async function publishTask() {
4049
4782
  printLine(JSON.stringify({
4050
4783
  taskId: data.task.id,
4051
4784
  status: data.task.status,
4785
+ assignmentMode: explicitAssignments ? 'custom' : 'auto',
4052
4786
  assignmentsCreated: data.assignmentsCreated,
4053
4787
  assignmentsProcessed: data.assignmentsProcessed,
4054
4788
  ...(data.assignmentsRemoved !== undefined
@@ -4064,6 +4798,7 @@ async function publishTask() {
4064
4798
 
4065
4799
  printLine(
4066
4800
  `Published task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]` +
4801
+ `\n Mode: ${explicitAssignments ? 'custom row map' : 'auto distribute'}` +
4067
4802
  `\n Assignments: ${data.assignmentsCreated}` +
4068
4803
  (data.assignmentsRemoved !== undefined
4069
4804
  ? `\n Replaced: ${data.assignmentsRemoved} old assignments removed`
@@ -4075,27 +4810,65 @@ async function publishTask() {
4075
4810
  async function assignTask() {
4076
4811
  const taskId = getArg('--task')
4077
4812
  const assignees = parseCommaSeparated(getArg('--assignees'))
4813
+ const assignmentFile = getArg('--assignment-file')
4078
4814
 
4079
- if (!taskId || assignees.length === 0) {
4080
- throw new Error('Usage: orizu tasks assign --task <taskId> --assignees <userId1,userId2>')
4815
+ if (assignmentFile && assignees.length > 0) {
4816
+ throw new Error('Use either --assignment-file or --assignees, not both.')
4817
+ }
4818
+
4819
+ const explicitAssignments = readAssignmentManifest(assignmentFile)
4820
+
4821
+ if (!taskId || (assignees.length === 0 && !explicitAssignments)) {
4822
+ throw new Error('Usage: orizu tasks assign --task <taskId> (--assignees <userId1,userId2> | --assignment-file <path>) [--replace-existing] [--json]')
4823
+ }
4824
+
4825
+ const requestBody: Record<string, unknown> = explicitAssignments
4826
+ ? { explicitAssignments }
4827
+ : { memberIds: assignees }
4828
+ if (hasArg('--replace-existing')) {
4829
+ requestBody.replaceExisting = true
4081
4830
  }
4082
4831
 
4083
4832
  const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/assign`, {
4084
4833
  method: 'POST',
4085
4834
  headers: { 'Content-Type': 'application/json' },
4086
- body: JSON.stringify({ memberIds: assignees }),
4835
+ body: JSON.stringify(requestBody),
4087
4836
  })
4088
4837
 
4089
4838
  if (!response.ok) {
4090
4839
  throw new Error(`Failed to assign task: ${await response.text()}`)
4091
4840
  }
4092
4841
 
4093
- const data = await parseJsonResponse<{ assignmentsCreated: number }>(response, 'Task assign')
4842
+ const data = await parseJsonResponse<{
4843
+ assignmentsProcessed?: number
4844
+ assignmentsCreated: number
4845
+ assignmentsRemoved?: number
4846
+ assignmentShortfall?: number
4847
+ warning?: string
4848
+ }>(response, 'Task assign')
4094
4849
  if (hasJsonFlag()) {
4095
- printJson({ assignmentsCreated: data.assignmentsCreated })
4850
+ printJson({
4851
+ assignmentMode: explicitAssignments ? 'custom' : 'auto',
4852
+ assignmentsProcessed: data.assignmentsProcessed ?? data.assignmentsCreated,
4853
+ assignmentsCreated: data.assignmentsCreated,
4854
+ ...(data.assignmentsRemoved !== undefined
4855
+ ? { assignmentsRemoved: data.assignmentsRemoved }
4856
+ : {}),
4857
+ ...(data.assignmentShortfall !== undefined
4858
+ ? { assignmentShortfall: data.assignmentShortfall }
4859
+ : {}),
4860
+ ...(data.warning ? { warning: data.warning } : {}),
4861
+ })
4096
4862
  return
4097
4863
  }
4098
- printLine(`Created ${data.assignmentsCreated} assignments.`)
4864
+ printLine(
4865
+ `Created ${data.assignmentsCreated} assignments.` +
4866
+ `\n Mode: ${explicitAssignments ? 'custom row map' : 'auto distribute'}` +
4867
+ (data.assignmentsRemoved !== undefined
4868
+ ? `\n Replaced: ${data.assignmentsRemoved} old assignments removed`
4869
+ : '') +
4870
+ (data.warning ? `\n Warning: ${sanitizeTerminalText(data.warning)}` : '')
4871
+ )
4099
4872
  }
4100
4873
 
4101
4874
  async function taskStatus() {
@@ -4142,11 +4915,12 @@ async function taskStatus() {
4142
4915
  printTaskStatusSummary(data)
4143
4916
  }
4144
4917
 
4145
- async function updateTaskStatus(targetStatus: 'paused' | 'active') {
4918
+ async function updateTaskStatus(targetStatus: 'paused' | 'active' | 'completed') {
4146
4919
  const taskId = getArg('--task')
4920
+ const labels = TASK_STATUS_ACTION_LABELS[targetStatus]
4921
+
4147
4922
  if (!taskId) {
4148
- const verb = targetStatus === 'paused' ? 'pause' : 'unpause'
4149
- throw new Error(`Usage: orizu tasks ${verb} --task <taskId>`)
4923
+ throw new Error(`Usage: orizu tasks ${labels.verb} --task <taskId>`)
4150
4924
  }
4151
4925
 
4152
4926
  const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/status`, {
@@ -4156,8 +4930,7 @@ async function updateTaskStatus(targetStatus: 'paused' | 'active') {
4156
4930
  })
4157
4931
 
4158
4932
  if (!response.ok) {
4159
- const verb = targetStatus === 'paused' ? 'pause' : 'unpause'
4160
- throw new Error(`Failed to ${verb} task: ${await response.text()}`)
4933
+ throw new Error(`Failed to ${labels.verb} task: ${await response.text()}`)
4161
4934
  }
4162
4935
 
4163
4936
  const data = await parseJsonResponse<{ task: { id: string; status: string } }>(response, 'Task status update')
@@ -4165,8 +4938,7 @@ async function updateTaskStatus(targetStatus: 'paused' | 'active') {
4165
4938
  printJson({ task: data.task })
4166
4939
  return
4167
4940
  }
4168
- const action = targetStatus === 'paused' ? 'Paused' : 'Unpaused'
4169
- printLine(`${action} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`)
4941
+ printLine(`${labels.pastTense} task ${sanitizeTerminalText(data.task.id)} [${sanitizeTerminalText(data.task.status)}]`)
4170
4942
  }
4171
4943
 
4172
4944
  async function setTaskReport() {
@@ -4429,10 +5201,10 @@ async function changeTeamMemberRole() {
4429
5201
  const email = getArg('--email')
4430
5202
  const role = getArg('--role')
4431
5203
  if (!teamSlug || !email || !role) {
4432
- throw new Error('Usage: orizu teams members role --team <teamSlug> --email <email> --role <admin|member>')
5204
+ throw new Error('Usage: orizu teams members role --team <teamSlug> --email <email> --role <admin|curator|judge>')
4433
5205
  }
4434
- if (!['admin', 'member'].includes(role)) {
4435
- throw new Error('role must be one of: admin, member')
5206
+ if (!['admin', 'curator', 'judge'].includes(role)) {
5207
+ throw new Error('role must be one of: admin, curator, judge')
4436
5208
  }
4437
5209
 
4438
5210
  const members = await fetchTeamMembers(teamSlug)
@@ -5414,12 +6186,16 @@ async function runnersExec() {
5414
6186
  }
5415
6187
 
5416
6188
  const runnerOutput = JSON.parse(readFileSync(outputPath, 'utf8')) as Record<string, unknown>
6189
+ // Spread runner output first: the scorer/prompt context identity fields
6190
+ // (row_id, version ids, metric_key) are authoritative, never runner-overridable.
5417
6191
  resultLines.push(JSON.stringify({
6192
+ ...runnerOutput,
5418
6193
  row_id: row.id,
5419
6194
  prompt_version_id: context.prompt.promptVersionId,
5420
6195
  runner_version_id: context.prompt.runnerVersionId,
5421
- ...(scorerVersion ? { scorer_version_id: context.scorer?.versionId || scorerVersion } : {}),
5422
- ...runnerOutput,
6196
+ ...(scorerVersion
6197
+ ? { scorer_version_id: context.scorer?.versionId || scorerVersion, metric_key: context.scorer?.metricKey || 'score' }
6198
+ : {}),
5423
6199
  }))
5424
6200
  } finally {
5425
6201
  rmSync(tempDir, { recursive: true, force: true })
@@ -5785,6 +6561,16 @@ export async function main(rawArgs = process.argv.slice(2)) {
5785
6561
  return
5786
6562
  }
5787
6563
 
6564
+ if (command === 'tasks' && subcommand === 'update') {
6565
+ await updateTask()
6566
+ return
6567
+ }
6568
+
6569
+ if (command === 'tasks' && subcommand === 'discard') {
6570
+ await discardTask()
6571
+ return
6572
+ }
6573
+
5788
6574
  if (command === 'tasks' && subcommand === 'publish') {
5789
6575
  await publishTask()
5790
6576
  return
@@ -5809,15 +6595,9 @@ export async function main(rawArgs = process.argv.slice(2)) {
5809
6595
  return
5810
6596
  }
5811
6597
 
5812
- if (command === 'tasks' && subcommand === 'pause') {
5813
- await updateTaskStatus('paused')
5814
- return
5815
- }
5816
-
5817
- if (command === 'tasks' && subcommand === 'unpause') {
5818
- await updateTaskStatus('active')
5819
- return
5820
- }
6598
+ if (command === 'tasks' && subcommand === 'pause') { await updateTaskStatus('paused'); return }
6599
+ if (command === 'tasks' && subcommand === 'unpause') { await updateTaskStatus('active'); return }
6600
+ if (command === 'tasks' && subcommand === 'complete') { await updateTaskStatus('completed'); return }
5821
6601
 
5822
6602
  if (command === 'datasets' && subcommand === 'upload') {
5823
6603
  await uploadDataset()
@@ -5833,55 +6613,60 @@ export async function main(rawArgs = process.argv.slice(2)) {
5833
6613
  await downloadDataset()
5834
6614
  return
5835
6615
  }
5836
-
5837
6616
  if (command === 'datasets' && subcommand === 'append') {
5838
6617
  await appendDatasetRows()
5839
6618
  return
5840
6619
  }
5841
-
5842
6620
  const datasetsAction = cliArgs[2]
5843
6621
  if (command === 'datasets' && subcommand === 'readme' && datasetsAction === 'set') {
5844
6622
  await setDatasetReadme()
5845
6623
  return
5846
6624
  }
5847
-
5848
6625
  if (command === 'datasets' && subcommand === 'versions' && datasetsAction === 'create') {
5849
6626
  await createDatasetVersion()
5850
6627
  return
5851
6628
  }
5852
-
5853
6629
  if (command === 'datasets' && subcommand === 'splits' && datasetsAction === 'create') {
5854
6630
  await createDatasetSplitSet()
5855
6631
  return
5856
6632
  }
5857
-
5858
6633
  if (command === 'datasets' && subcommand === 'edit-rows') {
5859
6634
  await editDatasetRows()
5860
6635
  return
5861
6636
  }
5862
-
5863
6637
  if (command === 'datasets' && subcommand === 'delete-rows') {
5864
6638
  await deleteDatasetRows()
5865
6639
  return
5866
6640
  }
5867
-
5868
6641
  if (command === 'datasets' && subcommand === 'delete') {
5869
6642
  await deleteDataset()
5870
6643
  return
5871
6644
  }
5872
-
5873
- if (command === 'datasets' && subcommand === 'lock') {
5874
- await lockDataset()
6645
+ if (command === 'datasets' && subcommand === 'lock') { await lockDataset(); return }
6646
+ if (command === 'datasets' && subcommand === 'clone') { await cloneDataset(); return }
6647
+ if (command === 'tasks' && subcommand === 'export') { await downloadAnnotations(); return }
6648
+ if (command === 'internal' || (command === 'session' && subcommand === 'start' && hasArg('--hosted'))) { process.exitCode = await hostedCommand(cliArgs, { json: hasJsonFlag(), print: printLine, printErr: printError }); return }
6649
+ if (command === 'session' || command === 'run') {
6650
+ process.exitCode = await workbenchCommand(cliArgs, { json: hasJsonFlag(), print: printLine })
5875
6651
  return
5876
6652
  }
5877
-
5878
- if (command === 'datasets' && subcommand === 'clone') {
5879
- await cloneDataset()
6653
+ if (command === 'workspace') {
6654
+ process.exitCode = await workspaceSyncCommand(cliArgs.slice(1), { json: hasJsonFlag(), print: printLine })
5880
6655
  return
5881
6656
  }
5882
-
5883
- if (command === 'tasks' && subcommand === 'export') {
5884
- await downloadAnnotations()
6657
+ if (command === 'git-credential') { process.exitCode = await runGitCredential(subcommand ?? 'get', { stdin: readFileSync(0, 'utf8'), cwd: process.cwd(), print: printLine, printErr: printError }); return }
6658
+ if (command === 'github' && subcommand === 'link') { const noInput = hasArg('--no-input') || hasArg('--non-interactive') || !isInteractiveTerminal(); const team = getOptionalArgValue('--team') || (await resolveSetupTeam(null, noInput, false)).slug; await runGithubLink(team, { print: printLine, fetcher: authedFetch, openUrl: openInBrowser, confirm: (org) => askYesNo(`Connect this team to GitHub org '${org}'?`, true) }); return }
6659
+ if (command === 'connectors' || command === 'manifests') {
6660
+ const moduleCommand = command === 'connectors' ? connectorsCommand : manifestsCommand
6661
+ process.exitCode = await moduleCommand(cliArgs.slice(1), { json: hasJsonFlag(), print: printLine, resolveProjectSlug })
6662
+ return
6663
+ }
6664
+ if (command === 'team' && (subcommand === 'kill-agents' || subcommand === 'release-agents')) {
6665
+ process.exitCode = await killSwitchCommand(cliArgs.slice(1), { json: hasJsonFlag(), print: printLine, printErr: printError })
6666
+ return
6667
+ }
6668
+ if (command === 'team' && subcommand === 'egress-allowlist') {
6669
+ process.exitCode = await egressAllowlistCommand(cliArgs.slice(1), { json: hasJsonFlag(), print: printLine, printErr: printError })
5885
6670
  return
5886
6671
  }
5887
6672
 
@@ -5904,7 +6689,7 @@ function isCliEntrypoint(): boolean {
5904
6689
 
5905
6690
  if (isCliEntrypoint()) {
5906
6691
  main().catch(error => {
5907
- console.error(sanitizeTerminalText(error instanceof Error ? error.message : 'Unknown error'))
6692
+ printError(error instanceof Error ? error.message : 'Unknown error')
5908
6693
  process.exit(1)
5909
6694
  })
5910
6695
  }