orizu 0.5.38 → 0.5.39
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 +18 -3
- package/dist/app-commands.d.ts.map +1 -1
- package/dist/app-commands.js +215 -57
- package/dist/app-publish-failure.d.ts +6 -0
- package/dist/app-publish-failure.d.ts.map +1 -0
- package/dist/app-publish-failure.js +9 -0
- package/dist/app-publish-paths.d.ts +4 -0
- package/dist/app-publish-paths.d.ts.map +1 -0
- package/dist/app-publish-paths.js +32 -0
- package/dist/app-publish-workspace.d.ts +16 -0
- package/dist/app-publish-workspace.d.ts.map +1 -0
- package/dist/app-publish-workspace.js +175 -0
- package/dist/artifacts-git-runtime.d.ts +5 -0
- package/dist/artifacts-git-runtime.d.ts.map +1 -1
- package/dist/artifacts-git-runtime.js +70 -25
- package/dist/artifacts-session-merge.d.ts +62 -0
- package/dist/artifacts-session-merge.d.ts.map +1 -0
- package/dist/artifacts-session-merge.js +123 -0
- package/dist/dataset-append-workflow.d.ts +12 -0
- package/dist/dataset-append-workflow.d.ts.map +1 -0
- package/dist/dataset-append-workflow.js +198 -0
- package/dist/dataset-commands.d.ts.map +1 -1
- package/dist/dataset-commands.js +11 -130
- package/dist/dataset-transfer-download.js +6 -6
- package/dist/dataset-transfer-upload.js +8 -8
- package/dist/git-credential.d.ts +1 -1
- package/dist/git-credential.d.ts.map +1 -1
- package/dist/git-credential.js +8 -1
- package/dist/help.js +3 -3
- package/dist/hosted-boot.d.ts +11 -0
- package/dist/hosted-boot.d.ts.map +1 -1
- package/dist/hosted-boot.js +46 -1
- package/dist/hosted-session-cli.d.ts.map +1 -1
- package/dist/hosted-session-cli.js +6 -1
- package/dist/hosted-session-seed.d.ts +45 -0
- package/dist/hosted-session-seed.d.ts.map +1 -0
- package/dist/hosted-session-seed.js +61 -0
- package/dist/jsonl-stream.d.ts +1 -0
- package/dist/jsonl-stream.d.ts.map +1 -1
- package/dist/jsonl-stream.js +7 -0
- package/dist/provider-list-workflow.d.ts +3 -0
- package/dist/provider-list-workflow.d.ts.map +1 -0
- package/dist/provider-list-workflow.js +95 -0
- package/dist/provider-wire-record.d.ts +3 -0
- package/dist/provider-wire-record.d.ts.map +1 -0
- package/dist/provider-wire-record.js +4 -0
- package/dist/providers-cli.d.ts.map +1 -1
- package/dist/providers-cli.js +5 -15
- package/dist/publish-merge-job.d.ts +68 -0
- package/dist/publish-merge-job.d.ts.map +1 -0
- package/dist/publish-merge-job.js +320 -0
- package/dist/publish-merge-timing.d.ts +18 -0
- package/dist/publish-merge-timing.d.ts.map +1 -0
- package/dist/publish-merge-timing.js +23 -0
- package/dist/workbench-cli.d.ts +4 -0
- package/dist/workbench-cli.d.ts.map +1 -1
- package/dist/workbench-cli.js +11 -1
- package/dist/workspace-bootstrap-job.d.ts +1 -0
- package/dist/workspace-bootstrap-job.d.ts.map +1 -1
- package/dist/workspace-bootstrap-job.js +1 -1
- package/package.json +5 -2
- package/src/app-commands.ts +234 -66
- package/src/app-publish-failure.ts +7 -0
- package/src/app-publish-paths.ts +27 -0
- package/src/app-publish-workspace.ts +207 -0
- package/src/artifacts-git-runtime.ts +73 -31
- package/src/artifacts-session-merge.ts +253 -0
- package/src/dataset-append-workflow.ts +205 -0
- package/src/dataset-commands.ts +10 -170
- package/src/dataset-transfer-download.ts +5 -5
- package/src/dataset-transfer-upload.ts +7 -7
- package/src/git-credential.ts +8 -1
- package/src/help.ts +3 -3
- package/src/hosted-boot.ts +56 -1
- package/src/hosted-session-cli.ts +6 -1
- package/src/hosted-session-seed.ts +78 -0
- package/src/jsonl-stream.ts +8 -0
- package/src/provider-list-workflow.ts +108 -0
- package/src/provider-wire-record.ts +4 -0
- package/src/providers-cli.ts +5 -13
- package/src/publish-merge-job.ts +350 -0
- package/src/publish-merge-timing.ts +27 -0
- package/src/workbench-cli.ts +15 -1
- package/src/workspace-bootstrap-job.ts +1 -1
- package/vendor/skills/orizu/flows/onboard.md +1 -1
- package/vendor/skills/orizu/references/authority-map.md +2 -2
- package/vendor/skills/orizu/references/building-apps.md +24 -0
- package/vendor/skills/orizu/references/cli-reference.md +13 -4
- package/vendor/skills/orizu/references/optimization-reports.md +1 -1
- package/vendor/skills/orizu/references/primer.md +1 -0
- package/vendor/skills/orizu/references/prompt-control-plane.md +9 -5
package/src/dataset-commands.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// ORI-2044: moved from index.ts to keep dataset commands together.
|
|
2
2
|
|
|
3
|
-
import { readFileSync,
|
|
3
|
+
import { readFileSync, writeFileSync } from 'fs'
|
|
4
4
|
import { basename, extname } from 'path'
|
|
5
5
|
import { createInterface } from 'readline/promises'
|
|
6
6
|
import { stdin as input, stdout as output } from 'process'
|
|
@@ -28,7 +28,6 @@ import { printDatasetSummaries } from './archive-artifacts-cli.js'
|
|
|
28
28
|
import { formatTerminalLink } from './auth-commands.js'
|
|
29
29
|
import { parseDatasetFile } from './file-parser.js'
|
|
30
30
|
import { parseDatasetReference } from './dataset-download.js'
|
|
31
|
-
import { streamJsonlRowChunks } from './jsonl-stream.js'
|
|
32
31
|
import { editDatasetRows as runEditDatasetRows } from './dataset-edit-rows.js'
|
|
33
32
|
import { parseCommaSeparated } from './task-commands.js'
|
|
34
33
|
import type { DatasetSelection } from './account-directory.js'
|
|
@@ -432,175 +431,16 @@ async function downloadDataset() {
|
|
|
432
431
|
printLine(`Saved dataset ${sanitizeTerminalText(datasetId)} (${format.toUpperCase()}) to ${sanitizeTerminalText(filename)}`)
|
|
433
432
|
}
|
|
434
433
|
|
|
435
|
-
const MAX_INPUT_FILE_SIZE_BYTES = 50 * 1024 * 1024 // 50 MB
|
|
436
|
-
const APPEND_CHUNK_SIZE_ROWS = 500
|
|
437
|
-
async function appendChunk(
|
|
438
|
-
datasetId: string,
|
|
439
|
-
rows: Array<Record<string, unknown>>
|
|
440
|
-
): Promise<{ dataset: { id: string; name: string; rowCount: number }; appendedCount: number }> {
|
|
441
|
-
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/rows`, {
|
|
442
|
-
method: 'POST',
|
|
443
|
-
headers: { 'Content-Type': 'application/json' },
|
|
444
|
-
body: JSON.stringify({ rows }),
|
|
445
|
-
})
|
|
446
|
-
|
|
447
|
-
if (!response.ok) {
|
|
448
|
-
throw new Error(`Append failed: ${await response.text()}`)
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
return parseJsonResponse<{
|
|
452
|
-
dataset: { id: string; name: string; rowCount: number }
|
|
453
|
-
appendedCount: number
|
|
454
|
-
}>(response, 'Dataset append')
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
async function appendJsonlDatasetRowsInChunks(datasetId: string, file: string) {
|
|
458
|
-
let totalAppended = 0
|
|
459
|
-
let lastResult: { dataset: { id: string; name: string; rowCount: number } } | null = null
|
|
460
|
-
let chunkIndex = 0
|
|
461
|
-
const chunks = streamJsonlRowChunks(file)[Symbol.asyncIterator]()
|
|
462
|
-
|
|
463
|
-
while (true) {
|
|
464
|
-
let nextChunk: IteratorResult<Array<Record<string, unknown>>>
|
|
465
|
-
try {
|
|
466
|
-
nextChunk = await chunks.next()
|
|
467
|
-
} catch (error) {
|
|
468
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
469
|
-
throw new Error(
|
|
470
|
-
`Append stopped while reading the next JSONL chunk: ${message}\n` +
|
|
471
|
-
`${totalAppended} rows from ${chunkIndex} chunk(s) were already appended. ` +
|
|
472
|
-
`Fix the file, remove the first ${totalAppended} rows, and re-run the command.`
|
|
473
|
-
)
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (nextChunk.done) {
|
|
477
|
-
break
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const chunk = nextChunk.value
|
|
481
|
-
chunkIndex += 1
|
|
482
|
-
printLine(`Uploading chunk ${chunkIndex} (${chunk.length} rows)...`)
|
|
483
|
-
|
|
484
|
-
try {
|
|
485
|
-
const data = await appendChunk(datasetId, chunk)
|
|
486
|
-
totalAppended += data.appendedCount
|
|
487
|
-
lastResult = data
|
|
488
|
-
} catch (error) {
|
|
489
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
490
|
-
throw new Error(
|
|
491
|
-
`Chunk ${chunkIndex} failed: ${message}\n` +
|
|
492
|
-
`${totalAppended} rows from ${chunkIndex - 1} chunk(s) were already appended. ` +
|
|
493
|
-
`To retry, remove the first ${totalAppended} rows from your file and re-run the command.`
|
|
494
|
-
)
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if (!lastResult) {
|
|
499
|
-
throw new Error('Dataset append file must contain at least one row')
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
printLine(
|
|
503
|
-
`Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`
|
|
504
|
-
)
|
|
505
|
-
}
|
|
506
|
-
|
|
507
434
|
async function appendDatasetRows() {
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
throw new Error('Usage: orizu datasets append [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>')
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (datasetInput) {
|
|
518
|
-
datasetId = parseDatasetReference(datasetInput).datasetId
|
|
519
|
-
} else {
|
|
520
|
-
const selected = await selectDatasetInteractively(projectArg)
|
|
521
|
-
datasetId = selected.datasetId
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
const file = expandHomePath(fileArg)
|
|
525
|
-
|
|
526
|
-
if (extname(file).toLowerCase() === '.jsonl') {
|
|
527
|
-
await appendJsonlDatasetRowsInChunks(datasetId, file)
|
|
528
|
-
return
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// Check file size before reading to prevent OOM on large files (ALI-565).
|
|
532
|
-
// Wrap statSync in try/catch so missing/inaccessible files get friendly
|
|
533
|
-
// errors instead of raw Node.js ENOENT/EPERM (ALI-554).
|
|
534
|
-
let fileSizeBytes: number
|
|
535
|
-
try {
|
|
536
|
-
fileSizeBytes = statSync(file).size
|
|
537
|
-
} catch (error) {
|
|
538
|
-
const maybeError = error as NodeJS.ErrnoException
|
|
539
|
-
if (maybeError.code === 'ENOENT') {
|
|
540
|
-
throw new Error(
|
|
541
|
-
`File not found: ${file}. Check the path and filename, then retry.`
|
|
542
|
-
)
|
|
543
|
-
}
|
|
544
|
-
if (maybeError.code === 'EPERM' || maybeError.code === 'EACCES') {
|
|
545
|
-
throw new Error(
|
|
546
|
-
`Cannot read file: ${file}. Grant folder permission to your terminal app and retry.`
|
|
547
|
-
)
|
|
548
|
-
}
|
|
549
|
-
throw new Error(`Failed to access file ${file}: ${maybeError.message}`)
|
|
550
|
-
}
|
|
551
|
-
if (fileSizeBytes > MAX_INPUT_FILE_SIZE_BYTES) {
|
|
552
|
-
const sizeMb = (fileSizeBytes / (1024 * 1024)).toFixed(1)
|
|
553
|
-
throw new Error(
|
|
554
|
-
`Input file is ${sizeMb} MB, which exceeds the 50 MB limit. Split the file into smaller parts and append each separately.`
|
|
555
|
-
)
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
const { rows } = parseDatasetFile(file)
|
|
559
|
-
if (!Array.isArray(rows) || rows.length === 0) {
|
|
560
|
-
throw new Error('Dataset append file must contain at least one row')
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if (rows.length <= APPEND_CHUNK_SIZE_ROWS) {
|
|
564
|
-
const data = await appendChunk(datasetId, rows)
|
|
565
|
-
printLine(
|
|
566
|
-
`Appended ${data.appendedCount} rows to dataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}). New row count: ${data.dataset.rowCount}`
|
|
567
|
-
)
|
|
568
|
-
return
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// Chunked upload for large row counts (ALI-555: track partial progress)
|
|
572
|
-
let totalAppended = 0
|
|
573
|
-
let lastResult: { dataset: { id: string; name: string; rowCount: number } } | null = null
|
|
574
|
-
const totalChunks = Math.ceil(rows.length / APPEND_CHUNK_SIZE_ROWS)
|
|
575
|
-
|
|
576
|
-
for (let offset = 0; offset < rows.length; offset += APPEND_CHUNK_SIZE_ROWS) {
|
|
577
|
-
const chunk = rows.slice(offset, offset + APPEND_CHUNK_SIZE_ROWS)
|
|
578
|
-
const chunkIndex = Math.floor(offset / APPEND_CHUNK_SIZE_ROWS) + 1
|
|
579
|
-
|
|
580
|
-
printLine(`Uploading chunk ${chunkIndex}/${totalChunks} (${chunk.length} rows)...`)
|
|
581
|
-
try {
|
|
582
|
-
const data = await appendChunk(datasetId, chunk)
|
|
583
|
-
totalAppended += data.appendedCount
|
|
584
|
-
lastResult = data
|
|
585
|
-
} catch (error) {
|
|
586
|
-
const msg = error instanceof Error ? error.message : String(error)
|
|
587
|
-
throw new Error(
|
|
588
|
-
`Chunk ${chunkIndex}/${totalChunks} failed: ${msg}\n` +
|
|
589
|
-
`${totalAppended} rows from ${chunkIndex - 1} chunk(s) were already appended. ` +
|
|
590
|
-
`To retry, remove the first ${totalAppended} rows from your file and re-run the command.`
|
|
591
|
-
)
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
if (lastResult) {
|
|
596
|
-
if (hasJsonFlag()) {
|
|
597
|
-
printJson({ dataset: lastResult.dataset, appendedCount: totalAppended })
|
|
598
|
-
return
|
|
599
|
-
}
|
|
600
|
-
printLine(
|
|
601
|
-
`Appended ${totalAppended} rows to dataset ${sanitizeTerminalText(lastResult.dataset.name)} (${sanitizeTerminalText(lastResult.dataset.id)}). New row count: ${lastResult.dataset.rowCount}`
|
|
602
|
-
)
|
|
603
|
-
}
|
|
435
|
+
const { appendDatasetWorkflow } = await import('./dataset-append-workflow.js')
|
|
436
|
+
process.exitCode = await appendDatasetWorkflow(async () => {
|
|
437
|
+
const projectArg = getArg('--project')
|
|
438
|
+
const datasetInput = getDatasetReferenceInput()
|
|
439
|
+
const fileArg = getArg('--file')
|
|
440
|
+
if (!fileArg) throw new Error('Usage: orizu datasets append [--dataset <datasetId|datasetUrl>] [--project <team/project>] --file <path>')
|
|
441
|
+
const datasetId = datasetInput ? parseDatasetReference(datasetInput).datasetId : (await selectDatasetInteractively(projectArg)).datasetId
|
|
442
|
+
return { datasetId, file: expandHomePath(fileArg) }
|
|
443
|
+
}, { json: hasJsonFlag(), print: printLine })
|
|
604
444
|
}
|
|
605
445
|
|
|
606
446
|
async function editDatasetRows() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect,
|
|
1
|
+
import { Effect, Result, Schedule } from 'effect'
|
|
2
2
|
import { writeFileSync } from 'node:fs'
|
|
3
3
|
import { datasetTransferTimeoutMs } from './dataset-transfer-policy.js'
|
|
4
4
|
import { authedFetch } from './http.js'
|
|
@@ -29,9 +29,9 @@ export async function transferDatasetDownload(datasetId: string, format: string,
|
|
|
29
29
|
if (cause instanceof TypeError) return new DownloadTransportError(cause)
|
|
30
30
|
return cause instanceof Error ? cause : new Error('Dataset download request failed', { cause })
|
|
31
31
|
},
|
|
32
|
-
}).pipe(Effect.
|
|
32
|
+
}).pipe(Effect.timeoutOrElse({
|
|
33
33
|
duration: timeoutMs,
|
|
34
|
-
|
|
34
|
+
orElse: () => Effect.fail(new Error('DATASET_TRANSFER_TIMEOUT: Dataset download exceeded its deadline')),
|
|
35
35
|
}), Effect.retry({
|
|
36
36
|
times: 2,
|
|
37
37
|
schedule: Schedule.spaced(100),
|
|
@@ -49,8 +49,8 @@ export async function transferDatasetDownload(datasetId: string, format: string,
|
|
|
49
49
|
process.once('SIGINT', handleInterrupt)
|
|
50
50
|
process.once('SIGTERM', handleInterrupt)
|
|
51
51
|
try {
|
|
52
|
-
const result = await Effect.runPromise(Effect.
|
|
53
|
-
if (
|
|
52
|
+
const result = await Effect.runPromise(Effect.result(Effect.flatMap(download, write)), { signal: controller.signal })
|
|
53
|
+
if (Result.isFailure(result)) throw result.failure
|
|
54
54
|
} catch (error) {
|
|
55
55
|
if (controller.signal.aborted) throw new Error('DATASET_TRANSFER_INTERRUPTED: Dataset download cancelled')
|
|
56
56
|
throw error
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Effect,
|
|
1
|
+
import { Context, Effect, Layer, Result } from 'effect'
|
|
2
2
|
|
|
3
3
|
import { ensureDatasetUploadSnapshot } from './dataset-upload-snapshot.js'
|
|
4
4
|
import { datasetTransferTimeoutMs } from './dataset-transfer-policy.js'
|
|
@@ -43,7 +43,7 @@ interface DatasetUploadApi {
|
|
|
43
43
|
append: (id: string, rows: UploadRows) => Effect.Effect<DatasetAppendResponse, DatasetWriteFailure>
|
|
44
44
|
snapshot: (id: string) => Effect.Effect<DatasetSnapshot, DatasetWriteFailure>
|
|
45
45
|
}
|
|
46
|
-
const DatasetUploadApi = Context.
|
|
46
|
+
const DatasetUploadApi = Context.Service<DatasetUploadApi>('orizu/DatasetUploadApi')
|
|
47
47
|
|
|
48
48
|
// Only the existing authentication client's rejected-token refresh may replay a
|
|
49
49
|
// request. No transient/transport retries belong around these non-idempotent writes.
|
|
@@ -64,9 +64,9 @@ function post<T>(path: string, body: unknown, context: string, prefix: string, t
|
|
|
64
64
|
catch: cause => cause instanceof DatasetWriteFailure ? cause : new DatasetWriteFailure(
|
|
65
65
|
cause instanceof Error ? cause.message : String(cause), requestStarted ? 'unknown' : 'rejected', { cause },
|
|
66
66
|
),
|
|
67
|
-
}).pipe(Effect.
|
|
67
|
+
}).pipe(Effect.timeoutOrElse({
|
|
68
68
|
duration: timeoutMs,
|
|
69
|
-
|
|
69
|
+
orElse: () => Effect.fail(new DatasetWriteFailure(`DATASET_TRANSFER_TIMEOUT: ${context} exceeded its deadline`, requestStarted ? 'unknown' : 'rejected')),
|
|
70
70
|
}))
|
|
71
71
|
})
|
|
72
72
|
}
|
|
@@ -84,9 +84,9 @@ function liveUploadApi(options: DatasetUploadOptions) {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
async function expectedPromise<A, E>(program: Effect.Effect<A, E>): Promise<A> {
|
|
87
|
-
const result = await Effect.runPromise(Effect.
|
|
88
|
-
if (
|
|
89
|
-
return result.
|
|
87
|
+
const result = await Effect.runPromise(Effect.result(program))
|
|
88
|
+
if (Result.isFailure(result)) throw result.failure
|
|
89
|
+
return result.success
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
function uploadProgram(options: DatasetUploadOptions) {
|
package/src/git-credential.ts
CHANGED
|
@@ -63,6 +63,9 @@ const SAFE_REMOTE_COMPONENT = /^[^\u0000-\u0020\u007f\\?#]+$/
|
|
|
63
63
|
export const REPO_TOKEN_PURPOSES = [
|
|
64
64
|
'read',
|
|
65
65
|
'write',
|
|
66
|
+
// ORI-2280: a hosted session's read key for its team copy, used by the
|
|
67
|
+
// `orizu-team` remote a publish conflict adds. Artifacts remotes only.
|
|
68
|
+
'team_read',
|
|
66
69
|
] as const
|
|
67
70
|
|
|
68
71
|
export type RepoTokenPurpose =
|
|
@@ -375,7 +378,7 @@ async function mintArtifactsCredential(
|
|
|
375
378
|
{
|
|
376
379
|
method: 'POST',
|
|
377
380
|
headers: { 'Content-Type': 'application/json' },
|
|
378
|
-
body: JSON.stringify({ purpose }),
|
|
381
|
+
body: JSON.stringify({ purpose, ...(purpose === 'team_read' ? { sessionId: process.env.ORIZU_SESSION_ID?.trim() } : {}) }),
|
|
379
382
|
}
|
|
380
383
|
)
|
|
381
384
|
|
|
@@ -541,6 +544,10 @@ export async function runGitCredential(op: string, io: GitCredentialIo): Promise
|
|
|
541
544
|
)
|
|
542
545
|
return 1
|
|
543
546
|
}
|
|
547
|
+
if (io.purpose === 'team_read') {
|
|
548
|
+
io.printErr('orizu git-credential: team_read is only for an Artifacts team copy')
|
|
549
|
+
return 1
|
|
550
|
+
}
|
|
544
551
|
const primaryPurpose = io.purpose ?? 'write'
|
|
545
552
|
let minted = await mintToken(
|
|
546
553
|
fetcher,
|
package/src/help.ts
CHANGED
|
@@ -84,8 +84,8 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
|
|
|
84
84
|
{ path: ['instructions', 'profiles', 'rollback'], aliases: [['instruction-sets', 'profiles', 'rollback']], usage: 'orizu instructions profiles rollback <set> --project <team/project> --model-config <identity> --to <n> [--json]', summary: 'Create a rollback version and move profile production to it.', group: 'Instruction sets', options: [{ name: '--model-config <identity>', help: 'Profile model config.', required: true }, { name: '--to <n>', help: 'Historical profile version to copy.', required: true }, { name: '--json', help: 'Print JSON.' }], examples: ['orizu instructions profiles rollback planner --project core/evals --model-config anthropic/claude-haiku --to 1'] },
|
|
85
85
|
{ path: ['instructions', 'default', 'show'], aliases: [['instruction-sets', 'default', 'show']], usage: 'orizu instructions default show <set> --project <team/project> [--json]', summary: 'Show the Default Profile and its Production state.', group: 'Instruction sets', options: [{ name: '--project <team/project>', help: 'Project slug.', required: true }, { name: '--json', help: 'Print JSON.' }], examples: ['orizu instructions default show planner --project core/evals'] },
|
|
86
86
|
{ path: ['instructions', 'default', 'move'], aliases: [['instruction-sets', 'default', 'move']], usage: 'orizu instructions default move <set> --project <team/project> --model-config <identity> [--json]', summary: 'Move the instruction-set Default to a Profile.', group: 'Instruction sets', options: [{ name: '--project <team/project>', help: 'Project slug.', required: true }, { name: '--model-config <identity>', help: 'Target Profile identity.', required: true }, { name: '--json', help: 'Print JSON.' }], examples: ['orizu instructions default move planner --project core/evals --model-config anthropic/claude-haiku'] },
|
|
87
|
-
{ path: ['instructions', 'shape', 'add'], aliases: [['instruction-sets', 'shape', 'add']], usage: 'orizu instructions shape add <set> --project <team/project> --key <key> --from <manifest> [--json]', summary: 'Add
|
|
88
|
-
{ path: ['instructions', 'shape', 'remove'], aliases: [['instruction-sets', 'shape', 'remove']], usage: 'orizu instructions shape remove <set> --project <team/project> --key <key> [--json]', summary: 'Remove
|
|
87
|
+
{ path: ['instructions', 'shape', 'add'], aliases: [['instruction-sets', 'shape', 'add']], usage: 'orizu instructions shape add <set> --project <team/project> --key <key> --from <manifest> [--json]', summary: 'Add a component before first promotion; a frozen shape requires a new instruction set.', group: 'Instruction sets', options: [{ name: '--project <team/project>', help: 'Project slug.', required: true }, { name: '--key <key>', help: 'Component key to add.', required: true }, { name: '--from <manifest>', help: 'Manifest containing the component body.', required: true }, { name: '--json', help: 'Print JSON.' }], examples: ['orizu instructions shape add planner --project core/evals --key skill --from planner.json'] },
|
|
88
|
+
{ path: ['instructions', 'shape', 'remove'], aliases: [['instruction-sets', 'shape', 'remove']], usage: 'orizu instructions shape remove <set> --project <team/project> --key <key> [--json]', summary: 'Remove a component before first promotion; a frozen shape requires a new instruction set.', group: 'Instruction sets', options: [{ name: '--project <team/project>', help: 'Project slug.', required: true }, { name: '--key <key>', help: 'Component key to remove.', required: true }, { name: '--json', help: 'Print JSON.' }], examples: ['orizu instructions shape remove planner --project core/evals --key skill'] },
|
|
89
89
|
{
|
|
90
90
|
path: ['instructions', 'sync'],
|
|
91
91
|
aliases: [['instruction-sets', 'sync']],
|
|
@@ -742,7 +742,7 @@ export const COMMAND_DOCS: CliCommandDoc[] = [
|
|
|
742
742
|
},
|
|
743
743
|
{
|
|
744
744
|
path: ['apps', 'create'],
|
|
745
|
-
usage: 'orizu apps create --project <team/project> --name <name> --dataset <datasetId> --file <path> --input-schema <json-path> --output-schema <json-path> [--component <name>]',
|
|
745
|
+
usage: 'orizu apps create --project <team/project> --name <name> --dataset <datasetId> --file <path> --input-schema <json-path> --output-schema <json-path> [--component <name>] [--id <app-id>]',
|
|
746
746
|
summary: 'Create a review app from a local component and schema files.',
|
|
747
747
|
group: 'Apps',
|
|
748
748
|
examples: [
|
package/src/hosted-boot.ts
CHANGED
|
@@ -34,6 +34,7 @@ import { spawnSync } from 'child_process'
|
|
|
34
34
|
import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeSync } from 'fs'
|
|
35
35
|
|
|
36
36
|
import { isValidCloudflareArtifactsGitRemote } from './cloudflare-artifacts-git-remote.js'
|
|
37
|
+
import { seedSessionCopy } from './hosted-session-seed.js'
|
|
37
38
|
import {
|
|
38
39
|
AGENT_GIT_IDENTITY,
|
|
39
40
|
BEARER_BASENAME,
|
|
@@ -681,6 +682,30 @@ export async function beginRunExecution(opts: {
|
|
|
681
682
|
throw new Error(`run_started transition failed: ${detail}`)
|
|
682
683
|
}
|
|
683
684
|
|
|
685
|
+
/** Mint a `team_read` key (ORI-2280 B1) to learn this session's team copy, so
|
|
686
|
+
* the boot can fill an empty session copy from it (ORI-2118). The credential
|
|
687
|
+
* helper mints its own keys for the git commands; this one only names the
|
|
688
|
+
* remote and lapses on its own (300 s). */
|
|
689
|
+
export async function resolveTeamRemote(opts: {
|
|
690
|
+
baseUrl: string
|
|
691
|
+
workspaceId: string
|
|
692
|
+
sessionId: string
|
|
693
|
+
bearer: string
|
|
694
|
+
fetchImpl: BootFetch
|
|
695
|
+
}): Promise<string> {
|
|
696
|
+
const url = `${opts.baseUrl}/api/cli/workspaces/${encodeURIComponent(opts.workspaceId)}/repo-token`
|
|
697
|
+
const minted = await bearerJson(opts.fetchImpl, url, opts.bearer, {
|
|
698
|
+
method: 'POST',
|
|
699
|
+
headers: { 'Content-Type': 'application/json' },
|
|
700
|
+
body: JSON.stringify({ purpose: 'team_read', sessionId: opts.sessionId }),
|
|
701
|
+
})
|
|
702
|
+
const remote = asString(minted.remote)
|
|
703
|
+
if (asString(minted.provider) !== 'cloudflare_artifacts' || !remote || !isValidCloudflareArtifactsGitRemote(remote)) {
|
|
704
|
+
throw new Error('team_read response carried no valid Artifacts remote')
|
|
705
|
+
}
|
|
706
|
+
return remote
|
|
707
|
+
}
|
|
708
|
+
|
|
684
709
|
/** Mint a session_read repo token to learn the repo full name, then build the
|
|
685
710
|
* GitHub clone URL (mirrors the operator path's `defaultResolveRepo`). */
|
|
686
711
|
export async function resolveRepo(opts: {
|
|
@@ -740,6 +765,10 @@ export interface BootExecResult {
|
|
|
740
765
|
}
|
|
741
766
|
export type BootExec = (cmd: string, args: string[], opts?: { cwd?: string }) => BootExecResult
|
|
742
767
|
|
|
768
|
+
/** The credential-helper context for the team copy, used only to fill an
|
|
769
|
+
* empty session copy (ORI-2118). */
|
|
770
|
+
const TEAM_BOOT_CONTEXT_BASENAME = 'boot-team.json'
|
|
771
|
+
|
|
743
772
|
const defaultExec: BootExec = (cmd, args, opts) => {
|
|
744
773
|
const res = spawnSync(cmd, args, { cwd: opts?.cwd, encoding: 'utf8' })
|
|
745
774
|
return { status: res.status ?? 1, stdout: res.stdout ?? '', stderr: res.stderr ?? '' }
|
|
@@ -1037,9 +1066,35 @@ export async function runHostedBoot(opts: RunHostedBootOptions): Promise<HostedB
|
|
|
1037
1066
|
}
|
|
1038
1067
|
writeFile(bootContextAbs, serializeBootContext(bootContext))
|
|
1039
1068
|
|
|
1069
|
+
const helperValue = `!node ${helperScriptAbs} ${bootContextAbs}`
|
|
1070
|
+
|
|
1071
|
+
// 4b — ORI-2118: a hosted session's Artifacts copy is created empty (forks
|
|
1072
|
+
// of a busy team copy cannot be cloned), so fill it from the team copy
|
|
1073
|
+
// before the first clone. A copy that already has `main` is left alone.
|
|
1074
|
+
if (isValidCloudflareArtifactsGitRemote(repo.cloneUrl)) {
|
|
1075
|
+
const credentialConfig = (helper: string) => [`credential.helper=${helper}`, 'credential.useHttpPath=true']
|
|
1076
|
+
await seedSessionCopy({
|
|
1077
|
+
exec,
|
|
1078
|
+
sessionRemote: repo.cloneUrl,
|
|
1079
|
+
sessionGitConfig: credentialConfig(helperValue),
|
|
1080
|
+
scratchDir: `${sessionDirAbs}/seed.git`,
|
|
1081
|
+
resolveTeam: async () => {
|
|
1082
|
+
const teamRemote = await resolveTeamRemote({ baseUrl: env.baseUrl, workspaceId, sessionId: env.sessionId, bearer: bearer.token, fetchImpl })
|
|
1083
|
+
assertSafeGitValue('teamRemote', teamRemote)
|
|
1084
|
+
const teamContextAbs = `${sessionDirAbs}/${TEAM_BOOT_CONTEXT_BASENAME}`
|
|
1085
|
+
writeFile(teamContextAbs, serializeBootContext({
|
|
1086
|
+
...bootContext,
|
|
1087
|
+
repoFullName: teamRemote,
|
|
1088
|
+
host: new URL(teamRemote).host.toLowerCase(),
|
|
1089
|
+
tokenPurposes: { primary: 'team_read', fallback: 'team_read' },
|
|
1090
|
+
}))
|
|
1091
|
+
return { remote: teamRemote, gitConfig: credentialConfig(`!node ${helperScriptAbs} ${teamContextAbs}`) }
|
|
1092
|
+
},
|
|
1093
|
+
})
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1040
1096
|
// 5 — Clone the session branch VIA the pull-mode credential helper (same
|
|
1041
1097
|
// invocation the operator path makes; no token in the URL/config).
|
|
1042
|
-
const helperValue = `!node ${helperScriptAbs} ${bootContextAbs}`
|
|
1043
1098
|
const clone = exec('git', [
|
|
1044
1099
|
'clone',
|
|
1045
1100
|
'--depth',
|
|
@@ -59,6 +59,7 @@ import { DEFAULT_HOSTED_MODEL, hostedLoopCommand, type HostedLoopContext } from
|
|
|
59
59
|
import { hostedBootCommand } from './hosted-boot.js'
|
|
60
60
|
import { mergeJobCommand } from './merge-job.js'
|
|
61
61
|
import { workspaceBootstrapJobCommand } from './workspace-bootstrap-job.js'
|
|
62
|
+
import { publishMergeJobCommand } from './publish-merge-job.js'
|
|
62
63
|
import { hostedOptimizationCommand } from './hosted-optimization.js'
|
|
63
64
|
import { writeChildStderr } from './child-output-tee.js'
|
|
64
65
|
import { skilledProposerBakeCommand } from './skilled-proposer-launch.js'
|
|
@@ -1557,12 +1558,16 @@ export async function hostedCommand(
|
|
|
1557
1558
|
if (positional[1] === 'workspace-bootstrap-job') {
|
|
1558
1559
|
return workspaceBootstrapJobCommand(args, io)
|
|
1559
1560
|
}
|
|
1561
|
+
// ORI-2280: the one-shot publish merge (coordinator publish-merge job).
|
|
1562
|
+
if (positional[1] === 'publish-merge-job') {
|
|
1563
|
+
return publishMergeJobCommand(args, io)
|
|
1564
|
+
}
|
|
1560
1565
|
if (positional[1] === 'hosted-optimization') {
|
|
1561
1566
|
return hostedOptimizationCommand({ ...io, printErr: writeChildStderr })
|
|
1562
1567
|
}
|
|
1563
1568
|
if (positional[1] === 'bake-skilled-proposer-venv') return skilledProposerBakeCommand(io)
|
|
1564
1569
|
if (positional[1] === 'verify-skilled-proposer-bake') return skilledProposerBakeCommand(io, true)
|
|
1565
|
-
io.printErr?.('Usage: orizu internal <hosted-loop --context <path> | hosted-boot | hosted-optimization | merge-job | workspace-bootstrap-job | bake-skilled-proposer-venv | verify-skilled-proposer-bake>')
|
|
1570
|
+
io.printErr?.('Usage: orizu internal <hosted-loop --context <path> | hosted-boot | hosted-optimization | merge-job | workspace-bootstrap-job | publish-merge-job | bake-skilled-proposer-venv | verify-skilled-proposer-bake>')
|
|
1566
1571
|
return 1
|
|
1567
1572
|
}
|
|
1568
1573
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fills an empty hosted session copy from its team copy (ORI-2118).
|
|
3
|
+
*
|
|
4
|
+
* Artifacts forks of a team copy whose history has a few file-adding commits
|
|
5
|
+
* cannot be cloned or pushed to (measured 2026-10-03, binding and REST). The
|
|
6
|
+
* broker therefore creates the session copy EMPTY, and the sandbox boot fills
|
|
7
|
+
* it here before its first clone: a bare clone of the team copy's `main`, then
|
|
8
|
+
* a push of that `main` into the session copy. A Worker cannot run git, so
|
|
9
|
+
* this is the first place in session start that can.
|
|
10
|
+
*
|
|
11
|
+
* It fills a copy only when the copy has no branches at all, which is what a
|
|
12
|
+
* freshly created copy looks like. A retried or resumed boot finds `main` and
|
|
13
|
+
* does nothing, so it never overwrites the agent's work, and a working
|
|
14
|
+
* repository whose content lives on another branch is never touched. The
|
|
15
|
+
* push is never forced; if another boot filled the copy first, that is fine.
|
|
16
|
+
* Credentials come only through the `-c` settings the caller passes (the
|
|
17
|
+
* boot's credential helper), never in a URL or argument.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const SESSION_COPY_SEED_FAILURE = 'session copy could not be filled from the team copy'
|
|
21
|
+
|
|
22
|
+
interface SeedExecResult {
|
|
23
|
+
status: number
|
|
24
|
+
stdout: string
|
|
25
|
+
stderr: string
|
|
26
|
+
}
|
|
27
|
+
type SeedExec = (cmd: string, args: string[], opts?: { cwd?: string }) => SeedExecResult
|
|
28
|
+
|
|
29
|
+
export interface SeedSessionCopyInput {
|
|
30
|
+
exec: SeedExec
|
|
31
|
+
sessionRemote: string
|
|
32
|
+
/** `git -c` settings that authenticate to the session copy. */
|
|
33
|
+
sessionGitConfig: string[]
|
|
34
|
+
/** Called only when the copy is empty: the team copy and its `-c` settings. */
|
|
35
|
+
resolveTeam: () => Promise<{ remote: string; gitConfig: string[] }>
|
|
36
|
+
/** A path owned only by this seed operation; cleared before and after use. */
|
|
37
|
+
scratchDir: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function withConfig(config: string[]): string[] {
|
|
41
|
+
return config.flatMap(setting => ['-c', setting])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function failure(step: string, result: SeedExecResult): Error {
|
|
45
|
+
const detail = (result.stderr || result.stdout || `exit ${result.status}`).trim()
|
|
46
|
+
return new Error(`${SESSION_COPY_SEED_FAILURE}: ${step}: ${detail}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Whether the session copy has any branch. Measured on Artifacts: an empty
|
|
50
|
+
* repository answers `ls-remote` with exit 0 and no output. Any non-zero
|
|
51
|
+
* exit is an error, never "empty". */
|
|
52
|
+
function sessionHasBranches(input: SeedSessionCopyInput): boolean {
|
|
53
|
+
const listed = input.exec('git', [...withConfig(input.sessionGitConfig), 'ls-remote', '--heads', input.sessionRemote])
|
|
54
|
+
if (listed.status !== 0) throw failure('reading the session copy', listed)
|
|
55
|
+
return listed.stdout.trim().length > 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function seedSessionCopy(input: SeedSessionCopyInput): Promise<{ seeded: boolean }> {
|
|
59
|
+
if (sessionHasBranches(input)) return { seeded: false }
|
|
60
|
+
const team = await input.resolveTeam()
|
|
61
|
+
try {
|
|
62
|
+
// A sandbox shutdown can interrupt clone before finally runs. This path
|
|
63
|
+
// belongs only to seeding; never clear the surrounding session directory.
|
|
64
|
+
const cleared = input.exec('rm', ['-rf', '--', input.scratchDir])
|
|
65
|
+
if (cleared.status !== 0) throw failure('clearing the seed scratch', cleared)
|
|
66
|
+
const fetched = input.exec('git', [...withConfig(team.gitConfig), 'clone', '--quiet', '--bare', '--single-branch', '--branch', 'main', team.remote, input.scratchDir])
|
|
67
|
+
if (fetched.status !== 0) throw failure('reading the team copy', fetched)
|
|
68
|
+
const pushed = input.exec('git', ['-C', input.scratchDir, ...withConfig(input.sessionGitConfig), 'push', '--quiet', input.sessionRemote, 'refs/heads/main:refs/heads/main'])
|
|
69
|
+
if (pushed.status !== 0) {
|
|
70
|
+
// Another boot of the same session may have filled it first.
|
|
71
|
+
if (sessionHasBranches(input)) return { seeded: false }
|
|
72
|
+
throw failure('pushing into the session copy', pushed)
|
|
73
|
+
}
|
|
74
|
+
return { seeded: true }
|
|
75
|
+
} finally {
|
|
76
|
+
input.exec('rm', ['-rf', '--', input.scratchDir])
|
|
77
|
+
}
|
|
78
|
+
}
|
package/src/jsonl-stream.ts
CHANGED
|
@@ -5,6 +5,7 @@ export const JSONL_UPLOAD_CHUNK_MAX_BYTES = 2 * 1024 * 1024
|
|
|
5
5
|
|
|
6
6
|
interface JsonlChunkOptions {
|
|
7
7
|
maxPayloadBytes?: number
|
|
8
|
+
signal?: AbortSignal
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
interface PendingRow {
|
|
@@ -66,11 +67,17 @@ export async function* streamJsonlRowChunks(
|
|
|
66
67
|
let parsedRowCount = 0
|
|
67
68
|
let lineNumber = 0
|
|
68
69
|
|
|
70
|
+
if (options.signal?.aborted) throw new Error('Dataset input interrupted')
|
|
71
|
+
|
|
69
72
|
const stream = createReadStream(filePath, { encoding: 'utf8' })
|
|
70
73
|
const lines = createInterface({
|
|
71
74
|
input: stream,
|
|
72
75
|
crlfDelay: Infinity,
|
|
73
76
|
})
|
|
77
|
+
// Closing the owned reader unblocks an outstanding next() before a scoped
|
|
78
|
+
// caller awaits return(); return alone would queue behind that pending read.
|
|
79
|
+
const interrupt = () => { lines.close(); stream.destroy() }
|
|
80
|
+
options.signal?.addEventListener('abort', interrupt, { once: true })
|
|
74
81
|
|
|
75
82
|
try {
|
|
76
83
|
for await (const line of lines) {
|
|
@@ -117,6 +124,7 @@ export async function* streamJsonlRowChunks(
|
|
|
117
124
|
} catch (error) {
|
|
118
125
|
throw mapReadError(filePath, error)
|
|
119
126
|
} finally {
|
|
127
|
+
options.signal?.removeEventListener('abort', interrupt)
|
|
120
128
|
lines.close()
|
|
121
129
|
stream.destroy()
|
|
122
130
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Context, Effect, Result, Schema } from 'effect'
|
|
2
|
+
|
|
3
|
+
import { authedFetch } from './http.js'
|
|
4
|
+
import { providerWireRecord as record } from './provider-wire-record.js'
|
|
5
|
+
import { isProviderRefusalCode } from './provider-refusals.js'
|
|
6
|
+
|
|
7
|
+
import type { ModelConfigsCommandIo } from './model-configs-cli.js'
|
|
8
|
+
|
|
9
|
+
type ProviderListFailure =
|
|
10
|
+
| { readonly _tag: 'Http'; readonly message: string }
|
|
11
|
+
| { readonly _tag: 'Transport' }
|
|
12
|
+
| { readonly _tag: 'Resolution'; readonly message: string }
|
|
13
|
+
| { readonly _tag: 'Adapter'; readonly message: string }
|
|
14
|
+
|
|
15
|
+
function providerListFailureMessage(failure: ProviderListFailure): string {
|
|
16
|
+
switch (failure._tag) {
|
|
17
|
+
case 'Http': return failure.message
|
|
18
|
+
case 'Transport': return 'Provider request failed'
|
|
19
|
+
// Project selection remains the existing, separately owned adapter.
|
|
20
|
+
case 'Resolution': case 'Adapter': return failure.message
|
|
21
|
+
default: { const exhaustive: never = failure; return exhaustive }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ProviderListTransport = Context.Service<{
|
|
26
|
+
readonly read: (project: string) => Effect.Effect<Response, ProviderListFailure>
|
|
27
|
+
}>('orizu/cli/ProviderListTransport')
|
|
28
|
+
|
|
29
|
+
const WireRecord = Schema.Record(Schema.String, Schema.Unknown)
|
|
30
|
+
const emptyWireRecord: Record<string, unknown> = {}
|
|
31
|
+
|
|
32
|
+
function providerListProgram(project: string, io: ModelConfigsCommandIo) {
|
|
33
|
+
return Effect.gen(function*() {
|
|
34
|
+
const transport = yield* ProviderListTransport
|
|
35
|
+
const response = yield* transport.read(project)
|
|
36
|
+
// Decode raw wire data; malformed JSON and non-record bodies retain the
|
|
37
|
+
// existing empty record fallback without narrowing accepted record fields.
|
|
38
|
+
const value = yield* Effect.promise(() => response.json().catch(() => null))
|
|
39
|
+
const body = yield* Schema.decodeUnknownEffect(WireRecord)(value).pipe(
|
|
40
|
+
Effect.catch(() => Effect.succeed(emptyWireRecord)),
|
|
41
|
+
)
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const message = typeof body.error === 'string' && isProviderRefusalCode(body.error) ? body.error : 'Provider request failed'
|
|
44
|
+
return yield* Effect.fail<ProviderListFailure>({ _tag: 'Http', message })
|
|
45
|
+
}
|
|
46
|
+
yield* Effect.sync(() => {
|
|
47
|
+
if (io.json) io.print(JSON.stringify(body))
|
|
48
|
+
else {
|
|
49
|
+
io.print('ID PROTOCOL HOST CREDENTIAL ENV BUILT-IN')
|
|
50
|
+
for (const value of Array.isArray(body.providers) ? body.providers : []) {
|
|
51
|
+
const row = record(value)
|
|
52
|
+
io.print(`${row.id} ${row.protocol} ${row.host} ${row.credentialEnv} ${row.builtIn ? 'yes' : 'no'}`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function listProviders(projectArg: string | null, io: ModelConfigsCommandIo): Promise<number> {
|
|
60
|
+
const controller = new AbortController()
|
|
61
|
+
let interrupted: 130 | 143 | undefined
|
|
62
|
+
const interrupt = () => { interrupted = 130; controller.abort() }
|
|
63
|
+
const terminate = () => { interrupted = 143; controller.abort() }
|
|
64
|
+
const transport = {
|
|
65
|
+
read: (project: string) => Effect.tryPromise({
|
|
66
|
+
try: signal => (io.fetcher ?? authedFetch)(`/api/cli/providers?project=${encodeURIComponent(project)}`, {
|
|
67
|
+
method: 'GET', signal: AbortSignal.any([signal, controller.signal]),
|
|
68
|
+
}),
|
|
69
|
+
// The retained HTTP adapter exposes login/config/refresh diagnostics as
|
|
70
|
+
// untyped Error values. Preserve its public messages until ORI-2420
|
|
71
|
+
// supplies typed adapter failures; never expose nested transport causes.
|
|
72
|
+
catch: (cause): ProviderListFailure => cause instanceof Error
|
|
73
|
+
? { _tag: 'Adapter', message: cause.message }
|
|
74
|
+
: { _tag: 'Transport' },
|
|
75
|
+
}),
|
|
76
|
+
}
|
|
77
|
+
const program = Effect.gen(function*() {
|
|
78
|
+
const project = yield* Effect.tryPromise({
|
|
79
|
+
try: async () => {
|
|
80
|
+
const project = await io.resolveProjectSlug?.(projectArg)
|
|
81
|
+
if (!project) throw new Error('Project resolver unavailable')
|
|
82
|
+
return project
|
|
83
|
+
},
|
|
84
|
+
catch: (cause): ProviderListFailure => ({ _tag: 'Resolution', message: cause instanceof Error ? cause.message : 'Provider request failed' }),
|
|
85
|
+
})
|
|
86
|
+
// The retained resolver is unsignalled. Do not suppress Node's natural
|
|
87
|
+
// termination until the cancellable catalog transport has been admitted.
|
|
88
|
+
return yield* Effect.acquireUseRelease(
|
|
89
|
+
Effect.sync(() => { process.once('SIGINT', interrupt); process.once('SIGTERM', terminate) }),
|
|
90
|
+
() => providerListProgram(project, io).pipe(Effect.provideService(ProviderListTransport, transport)),
|
|
91
|
+
() => Effect.sync(() => {
|
|
92
|
+
process.removeListener('SIGINT', interrupt)
|
|
93
|
+
process.removeListener('SIGTERM', terminate)
|
|
94
|
+
}),
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
try {
|
|
98
|
+
const result = await Effect.runPromise(Effect.result(program), { signal: controller.signal })
|
|
99
|
+
if (Result.isSuccess(result)) return 0
|
|
100
|
+
const message = providerListFailureMessage(result.failure)
|
|
101
|
+
io.print(io.json ? JSON.stringify({ error: message }) : message)
|
|
102
|
+
return 1
|
|
103
|
+
} catch {
|
|
104
|
+
if (interrupted !== undefined) return interrupted
|
|
105
|
+
io.print(io.json ? JSON.stringify({ error: 'Provider request failed' }) : 'Provider request failed')
|
|
106
|
+
return 1
|
|
107
|
+
}
|
|
108
|
+
}
|