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.
- package/README.md +22 -13
- package/dist/artifact-archive.js +152 -0
- package/dist/connectors-cli.js +80 -0
- package/dist/credentials.js +4 -1
- package/dist/daytona-slice-rehearsal.js +255 -0
- package/dist/daytona-slice.js +441 -0
- package/dist/egress-allowlist-cli.js +132 -0
- package/dist/egress-policy.js +159 -0
- package/dist/git-credential.js +131 -0
- package/dist/github-setup.js +226 -0
- package/dist/help.js +276 -21
- package/dist/hosted-bootstrap-rehearsal.js +235 -0
- package/dist/hosted-bootstrap.js +365 -0
- package/dist/hosted-broker-server.js +220 -0
- package/dist/hosted-git-http-server.js +136 -0
- package/dist/hosted-harness-claude.js +493 -0
- package/dist/hosted-harness-opencode.js +729 -0
- package/dist/hosted-harness.js +60 -0
- package/dist/hosted-loop.js +477 -0
- package/dist/hosted-run-event-sink.js +499 -0
- package/dist/hosted-runtime-assets.js +216 -0
- package/dist/hosted-session-cli.js +642 -0
- package/dist/index.js +945 -258
- package/dist/kill-switch-cli.js +89 -0
- package/dist/manifests-cli.js +143 -0
- package/dist/preview-runtime.js +4 -1
- package/dist/sandbox-provider.js +238 -0
- package/dist/secret-redaction.js +112 -0
- package/dist/session-timeline.js +103 -0
- package/dist/setup-prompt.js +4 -2
- package/dist/skill-installer.js +9 -1
- package/dist/smoke.js +250 -0
- package/dist/task-assignment-manifest.js +96 -0
- package/dist/vercel-sandbox-provider.js +260 -0
- package/dist/workbench-cli.js +493 -0
- package/dist/workspace-sync.js +924 -0
- package/dist/workspace.js +868 -57
- package/package.json +4 -1
- package/src/artifact-archive.ts +182 -0
- package/src/connectors-cli.ts +115 -0
- package/src/credentials.ts +6 -2
- package/src/daytona-slice-rehearsal.ts +320 -0
- package/src/daytona-slice.ts +598 -0
- package/src/egress-allowlist-cli.ts +147 -0
- package/src/egress-policy.ts +188 -0
- package/src/git-credential.ts +155 -0
- package/src/github-setup.ts +366 -0
- package/src/help.ts +277 -21
- package/src/hosted-bootstrap-rehearsal.ts +312 -0
- package/src/hosted-bootstrap.ts +571 -0
- package/src/hosted-broker-server.ts +278 -0
- package/src/hosted-git-http-server.ts +164 -0
- package/src/hosted-harness-claude.ts +621 -0
- package/src/hosted-harness-opencode.ts +823 -0
- package/src/hosted-harness.ts +124 -0
- package/src/hosted-loop.ts +680 -0
- package/src/hosted-run-event-sink.ts +632 -0
- package/src/hosted-runtime-assets.ts +272 -0
- package/src/hosted-session-cli.ts +844 -0
- package/src/index.ts +1102 -317
- package/src/kill-switch-cli.ts +113 -0
- package/src/manifests-cli.ts +168 -0
- package/src/preview-runtime.ts +14 -9
- package/src/sandbox-provider.ts +440 -0
- package/src/secret-redaction.ts +124 -0
- package/src/session-timeline.ts +152 -0
- package/src/setup-prompt.ts +4 -2
- package/src/skill-installer.ts +13 -1
- package/src/smoke.ts +342 -0
- package/src/task-assignment-manifest.ts +164 -0
- package/src/vercel-sandbox-provider.ts +388 -0
- package/src/workbench-cli.ts +701 -0
- package/src/workspace-sync.ts +1148 -0
- package/src/workspace.ts +1051 -64
- package/vendor/skills/orizu-cli/SKILL.md +8 -4
- 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
|
-
|
|
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
|
-
|
|
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:
|
|
579
|
-
throw new Error(`Invalid ${label} '${valueArg}': ${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2863
|
+
printJson({ team })
|
|
2786
2864
|
return
|
|
2787
2865
|
}
|
|
2788
|
-
printLine(`Created team: ${sanitizeTerminalText(
|
|
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
|
|
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
|
|
2901
|
+
printJson({ project })
|
|
2836
2902
|
return
|
|
2837
2903
|
}
|
|
2838
|
-
printLine(`Created project ${sanitizeTerminalText(`${
|
|
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:
|
|
2866
|
-
if (error
|
|
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}': ${
|
|
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:
|
|
2878
|
-
if (error
|
|
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}': ${
|
|
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: {
|
|
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, {
|
|
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:
|
|
3118
|
-
const message =
|
|
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
|
-
//
|
|
3307
|
-
printLine('1
|
|
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
|
|
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
|
-
|
|
3846
|
+
throw new Error('Setup cancelled.')
|
|
3320
3847
|
}
|
|
3321
3848
|
printLine('')
|
|
3322
3849
|
|
|
3323
|
-
//
|
|
3324
|
-
printLine('2
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
let
|
|
3328
|
-
|
|
3329
|
-
|
|
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 =
|
|
3354
|
-
|
|
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
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
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(
|
|
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:
|
|
3374
|
-
? getWorkspaceRoot()
|
|
3989
|
+
workspacePath: workspaceResult && workspaceState !== 'invalid'
|
|
3990
|
+
? getWorkspaceRoot(workspaceRoot)
|
|
3375
3991
|
: null,
|
|
3376
3992
|
})
|
|
3377
|
-
if (!hasArg('--no-handoff')) {
|
|
3378
|
-
printLine('4
|
|
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: {
|
|
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('
|
|
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('
|
|
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(`
|
|
3459
|
-
|
|
3460
|
-
|
|
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:
|
|
3484
|
-
throw new Error(`Invalid JSON file '${pathArg}': ${
|
|
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
|
|
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 (
|
|
3927
|
-
throw new Error('
|
|
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:
|
|
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:
|
|
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(
|
|
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 (
|
|
4080
|
-
throw new Error('
|
|
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(
|
|
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<{
|
|
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({
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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|
|
|
5204
|
+
throw new Error('Usage: orizu teams members role --team <teamSlug> --email <email> --role <admin|curator|judge>')
|
|
4433
5205
|
}
|
|
4434
|
-
if (!['admin', '
|
|
4435
|
-
throw new Error('role must be one of: admin,
|
|
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
|
|
5422
|
-
|
|
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
|
-
|
|
5814
|
-
|
|
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 === '
|
|
5874
|
-
|
|
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
|
-
|
|
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 === '
|
|
5884
|
-
|
|
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
|
-
|
|
6692
|
+
printError(error instanceof Error ? error.message : 'Unknown error')
|
|
5908
6693
|
process.exit(1)
|
|
5909
6694
|
})
|
|
5910
6695
|
}
|