orizu 0.5.26 → 0.5.28
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 +19 -0
- package/dist/artifacts-git-runtime.d.ts +34 -0
- package/dist/artifacts-git-runtime.d.ts.map +1 -0
- package/dist/artifacts-git-runtime.js +312 -0
- package/dist/cloudflare-artifacts-git-remote.d.ts +5 -0
- package/dist/cloudflare-artifacts-git-remote.d.ts.map +1 -0
- package/dist/cloudflare-artifacts-git-remote.js +21 -0
- package/dist/dataset-transfer-download.d.ts +2 -0
- package/dist/dataset-transfer-download.d.ts.map +1 -0
- package/dist/dataset-transfer-download.js +66 -0
- package/dist/dataset-transfer-policy.d.ts +2 -0
- package/dist/dataset-transfer-policy.d.ts.map +1 -0
- package/dist/dataset-transfer-policy.js +8 -0
- package/dist/dataset-transfer-upload.d.ts +42 -0
- package/dist/dataset-transfer-upload.d.ts.map +1 -0
- package/dist/dataset-transfer-upload.js +110 -0
- package/dist/git-credential.d.ts.map +1 -1
- package/dist/git-credential.js +1 -1
- package/dist/git-recovery-environment.d.ts +1 -0
- package/dist/git-recovery-environment.d.ts.map +1 -1
- package/dist/git-recovery-environment.js +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +44 -11
- package/dist/hosted-boot.d.ts +75 -2
- package/dist/hosted-boot.d.ts.map +1 -1
- package/dist/hosted-boot.js +585 -102
- package/dist/hosted-bootstrap.d.ts +2 -0
- package/dist/hosted-bootstrap.d.ts.map +1 -1
- package/dist/hosted-bootstrap.js +12 -3
- package/dist/hosted-harness-opencode.d.ts +8 -9
- package/dist/hosted-harness-opencode.d.ts.map +1 -1
- package/dist/hosted-harness-opencode.js +126 -24
- package/dist/hosted-harness.d.ts +3 -0
- package/dist/hosted-harness.d.ts.map +1 -1
- package/dist/hosted-harness.js +4 -1
- package/dist/hosted-harvest.d.ts +30 -9
- package/dist/hosted-harvest.d.ts.map +1 -1
- package/dist/hosted-harvest.js +222 -28
- package/dist/hosted-loop-lifecycle.d.ts +15 -3
- package/dist/hosted-loop-lifecycle.d.ts.map +1 -1
- package/dist/hosted-loop-lifecycle.js +6 -4
- package/dist/hosted-loop.d.ts +47 -7
- package/dist/hosted-loop.d.ts.map +1 -1
- package/dist/hosted-loop.js +310 -64
- package/dist/hosted-question-limits.d.ts +7 -0
- package/dist/hosted-question-limits.d.ts.map +1 -0
- package/dist/hosted-question-limits.js +6 -0
- package/dist/hosted-question.d.ts +27 -0
- package/dist/hosted-question.d.ts.map +1 -0
- package/dist/hosted-question.js +118 -0
- package/dist/hosted-run-event-sink.d.ts +10 -5
- package/dist/hosted-run-event-sink.d.ts.map +1 -1
- package/dist/hosted-run-event-sink.js +12 -8
- package/dist/hosted-runtime-assets.d.ts +16 -7
- package/dist/hosted-runtime-assets.d.ts.map +1 -1
- package/dist/hosted-runtime-assets.js +40 -12
- package/dist/hosted-session-cli.d.ts.map +1 -1
- package/dist/hosted-session-cli.js +10 -19
- package/dist/http.d.ts +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +113 -92
- package/dist/instruction-set-gepa-launch.d.ts +1 -1
- package/dist/instruction-set-gepa-launch.d.ts.map +1 -1
- package/dist/instruction-set-gepa-launch.js +5 -3
- package/dist/instruction-set-loader.d.ts.map +1 -1
- package/dist/instruction-set-loader.js +63 -2
- package/dist/instruction-set-lock/index.d.ts +51 -0
- package/dist/instruction-set-lock/index.d.ts.map +1 -0
- package/dist/instruction-set-lock/index.js +459 -0
- package/dist/instruction-set-lock/schema.v1.d.ts +136 -0
- package/dist/instruction-set-lock/schema.v1.d.ts.map +1 -0
- package/dist/instruction-set-lock/schema.v1.js +155 -0
- package/dist/instruction-set-provenance.d.ts +26 -0
- package/dist/instruction-set-provenance.d.ts.map +1 -0
- package/dist/instruction-set-provenance.js +68 -0
- package/dist/instruction-set-sync/helpers.d.ts +22 -0
- package/dist/instruction-set-sync/helpers.d.ts.map +1 -0
- package/dist/instruction-set-sync/helpers.js +677 -0
- package/dist/instruction-set-sync/index.d.ts +60 -0
- package/dist/instruction-set-sync/index.d.ts.map +1 -0
- package/dist/instruction-set-sync/index.js +689 -0
- package/dist/instruction-set-update/index.d.ts +42 -0
- package/dist/instruction-set-update/index.d.ts.map +1 -0
- package/dist/instruction-set-update/index.js +445 -0
- package/dist/instruction-set-verify/index.d.ts +27 -0
- package/dist/instruction-set-verify/index.d.ts.map +1 -0
- package/dist/instruction-set-verify/index.js +865 -0
- package/dist/instruction-sets-cli.d.ts +1 -4
- package/dist/instruction-sets-cli.d.ts.map +1 -1
- package/dist/instruction-sets-cli.js +220 -109
- package/dist/model-config-fields.d.ts +35 -0
- package/dist/model-config-fields.d.ts.map +1 -0
- package/dist/model-config-fields.js +28 -0
- package/dist/optimizations-list-cli.d.ts.map +1 -1
- package/dist/optimizations-list-cli.js +7 -4
- package/dist/score-format.d.ts +2 -0
- package/dist/score-format.d.ts.map +1 -0
- package/dist/score-format.js +18 -0
- package/dist/workbench-repo-contract.d.ts +65 -0
- package/dist/workbench-repo-contract.d.ts.map +1 -0
- package/dist/workbench-repo-contract.js +250 -0
- package/dist/workspace-bootstrap-job.d.ts +45 -0
- package/dist/workspace-bootstrap-job.d.ts.map +1 -0
- package/dist/workspace-bootstrap-job.js +431 -0
- package/dist/workspace-bootstrap-seed.d.ts +74 -0
- package/dist/workspace-bootstrap-seed.d.ts.map +1 -0
- package/dist/workspace-bootstrap-seed.js +209 -0
- package/package.json +4 -3
- package/src/artifacts-git-runtime.ts +360 -0
- package/src/cloudflare-artifacts-git-remote.ts +24 -0
- package/src/dataset-transfer-download.ts +61 -0
- package/src/dataset-transfer-policy.ts +8 -0
- package/src/dataset-transfer-upload.ts +145 -0
- package/src/git-credential.ts +1 -2
- package/src/git-recovery-environment.ts +1 -1
- package/src/help.ts +44 -11
- package/src/hosted-boot.ts +644 -109
- package/src/hosted-bootstrap.ts +14 -3
- package/src/hosted-harness-opencode.ts +130 -28
- package/src/hosted-harness.ts +4 -1
- package/src/hosted-harvest.ts +258 -33
- package/src/hosted-loop-lifecycle.ts +19 -5
- package/src/hosted-loop.ts +340 -68
- package/src/hosted-question-limits.ts +6 -0
- package/src/hosted-question.ts +145 -0
- package/src/hosted-run-event-sink.ts +24 -12
- package/src/hosted-runtime-assets.ts +51 -11
- package/src/hosted-session-cli.ts +10 -24
- package/src/http.ts +10 -4
- package/src/index.ts +108 -134
- package/src/instruction-set-gepa-launch.ts +4 -4
- package/src/instruction-set-loader.ts +53 -2
- package/src/instruction-set-lock/index.ts +561 -0
- package/src/instruction-set-lock/schema.v1.json +141 -0
- package/src/instruction-set-lock/schema.v1.ts +155 -0
- package/src/instruction-set-provenance.ts +89 -0
- package/src/instruction-set-sync/helpers.ts +707 -0
- package/src/instruction-set-sync/index.ts +791 -0
- package/src/instruction-set-update/index.ts +488 -0
- package/src/instruction-set-verify/index.ts +1067 -0
- package/src/instruction-sets-cli.ts +226 -114
- package/src/model-config-fields.ts +31 -0
- package/src/optimizations-list-cli.ts +7 -4
- package/src/score-format.ts +16 -0
- package/src/workbench-repo-contract.ts +314 -0
- package/src/workspace-bootstrap-job.ts +611 -0
- package/src/workspace-bootstrap-seed.ts +350 -0
- package/vendor/orizu-gepa/src/orizu_gepa_connector/instruction_set_loader.py +46 -2
- package/vendor/skills/orizu/SKILL.md +4 -2
- package/vendor/skills/orizu/flows/promote.md +4 -0
- package/vendor/skills/orizu/references/assess-and-plan.md +1 -1
- package/vendor/skills/orizu/references/authority-map.md +1 -1
- package/vendor/skills/orizu/references/cli-reference.md +151 -35
- package/vendor/skills/orizu/references/instructions-after-prompts.md +3 -3
- package/vendor/skills/orizu/references/migrate-existing-gepa-setup.md +5 -6
- package/vendor/skills/orizu/references/migrate-hand-rolled-instruction-layout.md +252 -0
- package/vendor/skills/orizu/references/optimization-reports.md +5 -4
- package/vendor/skills/orizu/references/optimization-with-gepa.md +1 -1
- package/vendor/skills/orizu/references/orizu-in-your-codebase.md +195 -0
- package/vendor/skills/orizu/references/primer.md +4 -2
- package/vendor/skills/orizu/references/prompt-control-plane.md +33 -29
- package/vendor/skills/orizu/references/vocabulary.md +1 -1
package/README.md
CHANGED
|
@@ -427,6 +427,25 @@ Task and dataset export support:
|
|
|
427
427
|
- `json`
|
|
428
428
|
- `jsonl`
|
|
429
429
|
|
|
430
|
+
Dataset `upload` and `push` create an immutable version after the rows are
|
|
431
|
+
acknowledged. With `--json`, upload prints one result document. A failed snapshot
|
|
432
|
+
reports the uploaded dataset identity so recovery can target the snapshot step.
|
|
433
|
+
If a write's response is lost or times out, inspect the dataset before retrying:
|
|
434
|
+
the last chunk may already have committed.
|
|
435
|
+
|
|
436
|
+
Dataset downloads retry transient read failures up to three attempts. Each
|
|
437
|
+
transfer request, including its response body, has a two-minute deadline;
|
|
438
|
+
`ORIZU_DATASET_TRANSFER_TIMEOUT_MS` accepts 1–300000 milliseconds. Downloads
|
|
439
|
+
receive the complete response before writing to the destination. Existing files
|
|
440
|
+
keep their inode, ownership, permissions and ACLs; writes follow existing
|
|
441
|
+
symlinks and update hardlinked readers. New files request mode `0600`, subject
|
|
442
|
+
to the filesystem's access rules. A failed network transfer leaves an existing output
|
|
443
|
+
untouched. The final write retains ordinary filesystem behavior: it is not
|
|
444
|
+
atomic, and a disk failure during that write can leave partial output.
|
|
445
|
+
SIGINT/SIGTERM cancels a download. Upload writes
|
|
446
|
+
have no additional transient retries; the existing session refresh behavior is
|
|
447
|
+
unchanged.
|
|
448
|
+
|
|
430
449
|
## Interactive And Automated Usage
|
|
431
450
|
|
|
432
451
|
Many commands can prompt for missing team, project, app, dataset, or task selections in an interactive terminal. In scripts and CI, pass explicit flags instead:
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface GitCommandResult {
|
|
2
|
+
exitCode: number;
|
|
3
|
+
stdout: string;
|
|
4
|
+
stderr: string;
|
|
5
|
+
timedOut?: boolean;
|
|
6
|
+
cancelled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface GitAuth {
|
|
9
|
+
askPassPath: string;
|
|
10
|
+
token: string;
|
|
11
|
+
}
|
|
12
|
+
export interface GitRunOptions {
|
|
13
|
+
cwd: string;
|
|
14
|
+
auth?: GitAuth;
|
|
15
|
+
env?: NodeJS.ProcessEnv;
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
commitIdentityEnv?: Readonly<Record<string, string>>;
|
|
19
|
+
}
|
|
20
|
+
export type GitRunner = (args: string[], options: GitRunOptions) => Promise<GitCommandResult>;
|
|
21
|
+
export declare function assertCommitIdentityEnv(overlay: Readonly<Record<string, string>>): void;
|
|
22
|
+
export declare function parseArtifactTokenExpiry(token: string): number | null;
|
|
23
|
+
export declare function credentialSecretVariants(secrets: readonly (string | undefined)[]): string[];
|
|
24
|
+
export declare function redactSecrets(value: string, secrets: readonly (string | undefined)[]): string;
|
|
25
|
+
export declare function createEphemeralAskPass(root: string): Promise<{
|
|
26
|
+
path: string;
|
|
27
|
+
dispose: () => Promise<void>;
|
|
28
|
+
}>;
|
|
29
|
+
export declare function buildGitAuthEnvironment(auth: GitAuth, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
30
|
+
export declare function assertSafeGitInvocation(args: readonly string[], secrets?: readonly string[]): void;
|
|
31
|
+
export declare const defaultGitRunner: GitRunner;
|
|
32
|
+
export declare function gitOk(runGit: GitRunner, args: string[], options: GitRunOptions, operation: string): Promise<string>;
|
|
33
|
+
export declare function revParse(runGit: GitRunner, cwd: string, ref: string): Promise<string>;
|
|
34
|
+
//# sourceMappingURL=artifacts-git-runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifacts-git-runtime.d.ts","sourceRoot":"","sources":["../src/artifacts-git-runtime.ts"],"names":[],"mappings":"AAoCA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,MAAM,WAAW,OAAO;IACtB,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAA;IACvB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,iBAAiB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACrD;AAED,MAAM,MAAM,SAAS,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAA;AAE7F,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,IAAI,CASvF;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBrE;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,GAAG,MAAM,EAAE,CAS3F;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,GAAG,MAAM,CAK7F;AAED,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC,CAsBzD;AAUD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,OAAO,EACb,IAAI,GAAE,MAAM,CAAC,UAAwB,GACpC,MAAM,CAAC,UAAU,CAkBnB;AAgBD,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,GAAE,SAAS,MAAM,EAAO,GAC9B,IAAI,CAYN;AAWD,eAAO,MAAM,gBAAgB,EAAE,SAoI3B,CAAA;AAEJ,wBAAsB,KAAK,CACzB,MAAM,EAAE,SAAS,EACjB,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,aAAa,EACtB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CAUjB;AAED,wBAAsB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAE3F"}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { chmod, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const ARTIFACT_TOKEN_BODY = /^[^\s\u0000-\u001f\u007f]{16,1024}$/;
|
|
5
|
+
const ARTIFACT_TOKEN_ADVISORY_SUFFIX = '?expires=';
|
|
6
|
+
const ARTIFACT_TOKEN_BARE_BODY = /art_v1_[0-9a-fA-F]{40}/;
|
|
7
|
+
const DEFAULT_GIT_TIMEOUT_MS = 30_000;
|
|
8
|
+
const GIT_TERMINATION_GRACE_MS = 1_000;
|
|
9
|
+
const GIT_CONTEXT_ENV_KEYS = [
|
|
10
|
+
'GIT_DIR',
|
|
11
|
+
'GIT_WORK_TREE',
|
|
12
|
+
'GIT_INDEX_FILE',
|
|
13
|
+
'GIT_OBJECT_DIRECTORY',
|
|
14
|
+
'GIT_COMMON_DIR',
|
|
15
|
+
'GIT_NAMESPACE',
|
|
16
|
+
];
|
|
17
|
+
const GIT_CHILD_ENV_ALLOWLIST = [
|
|
18
|
+
'PATH',
|
|
19
|
+
'TMPDIR',
|
|
20
|
+
'TMP',
|
|
21
|
+
'TEMP',
|
|
22
|
+
'SYSTEMROOT',
|
|
23
|
+
'WINDIR',
|
|
24
|
+
'COMSPEC',
|
|
25
|
+
'PATHEXT',
|
|
26
|
+
'LANG',
|
|
27
|
+
'TZ',
|
|
28
|
+
'SSL_CERT_FILE',
|
|
29
|
+
'SSL_CERT_DIR',
|
|
30
|
+
'CURL_CA_BUNDLE',
|
|
31
|
+
'GIT_SSL_CAINFO',
|
|
32
|
+
];
|
|
33
|
+
const COMMIT_IDENTITY_ENV_PATTERN = /^GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL|DATE)$/;
|
|
34
|
+
export function assertCommitIdentityEnv(overlay) {
|
|
35
|
+
for (const key of Object.keys(overlay)) {
|
|
36
|
+
if (!COMMIT_IDENTITY_ENV_PATTERN.test(key)) {
|
|
37
|
+
throw new Error(`Refusing Git environment overlay ${JSON.stringify(key)}: only ` +
|
|
38
|
+
'GIT_AUTHOR_/GIT_COMMITTER_ NAME, EMAIL and DATE may be set');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function parseArtifactTokenExpiry(token) {
|
|
43
|
+
const separator = token.lastIndexOf(ARTIFACT_TOKEN_ADVISORY_SUFFIX);
|
|
44
|
+
if (separator !== -1) {
|
|
45
|
+
const body = token.slice(0, separator);
|
|
46
|
+
const suffix = token.slice(separator + ARTIFACT_TOKEN_ADVISORY_SUFFIX.length);
|
|
47
|
+
if (!ARTIFACT_TOKEN_BODY.test(body) || !/^\d{1,15}$/.test(suffix)) {
|
|
48
|
+
throw new Error('Artifacts returned an unexpected token format');
|
|
49
|
+
}
|
|
50
|
+
const expiry = Number(suffix);
|
|
51
|
+
if (!Number.isSafeInteger(expiry)) {
|
|
52
|
+
throw new Error('Artifacts returned an invalid token expiry');
|
|
53
|
+
}
|
|
54
|
+
return expiry;
|
|
55
|
+
}
|
|
56
|
+
if (!ARTIFACT_TOKEN_BODY.test(token)) {
|
|
57
|
+
throw new Error('Artifacts returned an unexpected token format');
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
export function credentialSecretVariants(secrets) {
|
|
62
|
+
const variants = new Set();
|
|
63
|
+
for (const secret of secrets) {
|
|
64
|
+
if (!secret)
|
|
65
|
+
continue;
|
|
66
|
+
variants.add(secret);
|
|
67
|
+
const bareBody = ARTIFACT_TOKEN_BARE_BODY.exec(secret)?.[0];
|
|
68
|
+
if (bareBody)
|
|
69
|
+
variants.add(bareBody);
|
|
70
|
+
}
|
|
71
|
+
return [...variants];
|
|
72
|
+
}
|
|
73
|
+
export function redactSecrets(value, secrets) {
|
|
74
|
+
return credentialSecretVariants(secrets)
|
|
75
|
+
.filter((secret) => secret.length >= 4)
|
|
76
|
+
.sort((left, right) => right.length - left.length)
|
|
77
|
+
.reduce((redacted, secret) => redacted.split(secret).join('[REDACTED]'), value);
|
|
78
|
+
}
|
|
79
|
+
export async function createEphemeralAskPass(root) {
|
|
80
|
+
const authDir = join(root, 'git-auth');
|
|
81
|
+
await mkdir(authDir, { recursive: true, mode: 0o700 });
|
|
82
|
+
await chmod(authDir, 0o700);
|
|
83
|
+
const path = join(authDir, 'askpass.sh');
|
|
84
|
+
const script = `#!/bin/sh
|
|
85
|
+
case "$1" in
|
|
86
|
+
*sername*) printf '%s\\n' x ;;
|
|
87
|
+
*assword*)
|
|
88
|
+
IFS= read -r password <&3 || exit 1
|
|
89
|
+
[ -n "$password" ] || exit 1
|
|
90
|
+
printf '%s\\n' "$password"
|
|
91
|
+
;;
|
|
92
|
+
*) exit 1 ;;
|
|
93
|
+
esac
|
|
94
|
+
`;
|
|
95
|
+
await writeFile(path, script, { mode: 0o700 });
|
|
96
|
+
await chmod(path, 0o700);
|
|
97
|
+
return {
|
|
98
|
+
path,
|
|
99
|
+
dispose: () => rm(authDir, { recursive: true, force: true }),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function minimalGitChildEnvironment(base) {
|
|
103
|
+
const selected = { NODE_ENV: base.NODE_ENV };
|
|
104
|
+
for (const key of GIT_CHILD_ENV_ALLOWLIST) {
|
|
105
|
+
if (base[key] !== undefined)
|
|
106
|
+
selected[key] = base[key];
|
|
107
|
+
}
|
|
108
|
+
return selected;
|
|
109
|
+
}
|
|
110
|
+
export function buildGitAuthEnvironment(auth, base = process.env) {
|
|
111
|
+
parseArtifactTokenExpiry(auth.token);
|
|
112
|
+
const env = {
|
|
113
|
+
...minimalGitChildEnvironment(base),
|
|
114
|
+
LC_ALL: 'C',
|
|
115
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
116
|
+
GIT_ASKPASS: auth.askPassPath,
|
|
117
|
+
GIT_ASKPASS_REQUIRE: 'force',
|
|
118
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
119
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
120
|
+
GIT_CONFIG_COUNT: '2',
|
|
121
|
+
GIT_CONFIG_KEY_0: 'credential.helper',
|
|
122
|
+
GIT_CONFIG_VALUE_0: '',
|
|
123
|
+
GIT_CONFIG_KEY_1: 'credential.useHttpPath',
|
|
124
|
+
GIT_CONFIG_VALUE_1: 'true',
|
|
125
|
+
};
|
|
126
|
+
for (const key of GIT_CONTEXT_ENV_KEYS)
|
|
127
|
+
delete env[key];
|
|
128
|
+
return env;
|
|
129
|
+
}
|
|
130
|
+
function buildUnauthenticatedGitEnvironment(base = process.env) {
|
|
131
|
+
const env = {
|
|
132
|
+
...minimalGitChildEnvironment(base),
|
|
133
|
+
LC_ALL: 'C',
|
|
134
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
135
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
136
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
137
|
+
};
|
|
138
|
+
for (const key of GIT_CONTEXT_ENV_KEYS)
|
|
139
|
+
delete env[key];
|
|
140
|
+
return env;
|
|
141
|
+
}
|
|
142
|
+
export function assertSafeGitInvocation(args, secrets = []) {
|
|
143
|
+
const joined = args.join('\u0000');
|
|
144
|
+
for (const secret of credentialSecretVariants(secrets)) {
|
|
145
|
+
if (joined.includes(secret))
|
|
146
|
+
throw new Error('Refusing to place a credential in Git argv');
|
|
147
|
+
}
|
|
148
|
+
for (const arg of args) {
|
|
149
|
+
if (!/^https?:\/\//i.test(arg))
|
|
150
|
+
continue;
|
|
151
|
+
const url = new URL(arg);
|
|
152
|
+
if (url.username || url.password) {
|
|
153
|
+
throw new Error('Refusing a Git remote URL containing userinfo');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function signalGitProcess(child, signal) {
|
|
158
|
+
try {
|
|
159
|
+
if (process.platform !== 'win32' && child.pid)
|
|
160
|
+
process.kill(-child.pid, signal);
|
|
161
|
+
else
|
|
162
|
+
child.kill(signal);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// The process may have exited between the state check and the signal.
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export const defaultGitRunner = (args, { cwd, auth, env: suppliedEnv, commitIdentityEnv, timeoutMs = DEFAULT_GIT_TIMEOUT_MS, signal }) => new Promise((resolve, reject) => {
|
|
169
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0) {
|
|
170
|
+
reject(new Error('Git timeout must be a positive integer'));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const secrets = auth ? [auth.token] : [];
|
|
174
|
+
assertSafeGitInvocation(args, secrets);
|
|
175
|
+
if (signal?.aborted) {
|
|
176
|
+
resolve({
|
|
177
|
+
exitCode: 130,
|
|
178
|
+
stdout: '',
|
|
179
|
+
stderr: 'Git operation cancelled',
|
|
180
|
+
cancelled: true,
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const baseEnv = auth
|
|
185
|
+
? buildGitAuthEnvironment(auth, suppliedEnv)
|
|
186
|
+
: buildUnauthenticatedGitEnvironment(suppliedEnv);
|
|
187
|
+
let env = baseEnv;
|
|
188
|
+
if (commitIdentityEnv) {
|
|
189
|
+
assertCommitIdentityEnv(commitIdentityEnv);
|
|
190
|
+
env = { ...baseEnv, ...commitIdentityEnv };
|
|
191
|
+
}
|
|
192
|
+
const spawnArgs = auth ? ['-c', 'core.hooksPath=/dev/null', ...args] : args;
|
|
193
|
+
const child = spawn('git', spawnArgs, {
|
|
194
|
+
cwd,
|
|
195
|
+
env,
|
|
196
|
+
detached: process.platform !== 'win32',
|
|
197
|
+
stdio: auth ? ['ignore', 'pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'],
|
|
198
|
+
});
|
|
199
|
+
let stdout = '';
|
|
200
|
+
let stderr = '';
|
|
201
|
+
let termination = null;
|
|
202
|
+
let settled = false;
|
|
203
|
+
let killTimer = null;
|
|
204
|
+
let hardStopTimer = null;
|
|
205
|
+
let timeoutTimer = null;
|
|
206
|
+
const cleanup = () => {
|
|
207
|
+
if (timeoutTimer)
|
|
208
|
+
clearTimeout(timeoutTimer);
|
|
209
|
+
if (killTimer)
|
|
210
|
+
clearTimeout(killTimer);
|
|
211
|
+
if (hardStopTimer)
|
|
212
|
+
clearTimeout(hardStopTimer);
|
|
213
|
+
signal?.removeEventListener('abort', cancel);
|
|
214
|
+
};
|
|
215
|
+
const finish = (exitCode) => {
|
|
216
|
+
if (settled)
|
|
217
|
+
return;
|
|
218
|
+
settled = true;
|
|
219
|
+
cleanup();
|
|
220
|
+
resolve({
|
|
221
|
+
exitCode,
|
|
222
|
+
stdout: redactSecrets(stdout, secrets),
|
|
223
|
+
stderr: redactSecrets(stderr, secrets),
|
|
224
|
+
timedOut: termination === 'timeout' || undefined,
|
|
225
|
+
cancelled: termination === 'cancelled' || undefined,
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
const terminate = (reason) => {
|
|
229
|
+
if (termination || settled)
|
|
230
|
+
return;
|
|
231
|
+
termination = reason;
|
|
232
|
+
signalGitProcess(child, 'SIGTERM');
|
|
233
|
+
killTimer = setTimeout(() => signalGitProcess(child, 'SIGKILL'), GIT_TERMINATION_GRACE_MS);
|
|
234
|
+
killTimer.unref?.();
|
|
235
|
+
hardStopTimer = setTimeout(() => finish(reason === 'timeout' ? 124 : 130), GIT_TERMINATION_GRACE_MS * 2);
|
|
236
|
+
hardStopTimer.unref?.();
|
|
237
|
+
};
|
|
238
|
+
const fail = (error) => {
|
|
239
|
+
if (settled)
|
|
240
|
+
return;
|
|
241
|
+
settled = true;
|
|
242
|
+
cleanup();
|
|
243
|
+
signalGitProcess(child, 'SIGKILL');
|
|
244
|
+
reject(error);
|
|
245
|
+
};
|
|
246
|
+
const cancel = () => terminate('cancelled');
|
|
247
|
+
timeoutTimer = setTimeout(() => terminate('timeout'), timeoutMs);
|
|
248
|
+
timeoutTimer.unref?.();
|
|
249
|
+
signal?.addEventListener('abort', cancel, { once: true });
|
|
250
|
+
if (signal?.aborted)
|
|
251
|
+
cancel();
|
|
252
|
+
const childStdout = child.stdout;
|
|
253
|
+
const childStderr = child.stderr;
|
|
254
|
+
if (!childStdout || !childStderr) {
|
|
255
|
+
fail(new Error('Could not capture Git process output'));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
childStdout.setEncoding('utf8');
|
|
259
|
+
childStderr.setEncoding('utf8');
|
|
260
|
+
childStdout.on('data', (chunk) => {
|
|
261
|
+
stdout += chunk;
|
|
262
|
+
});
|
|
263
|
+
childStderr.on('data', (chunk) => {
|
|
264
|
+
stderr += chunk;
|
|
265
|
+
});
|
|
266
|
+
child.on('error', (error) => {
|
|
267
|
+
if (termination) {
|
|
268
|
+
finish(termination === 'timeout' ? 124 : 130);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
fail(error);
|
|
272
|
+
});
|
|
273
|
+
child.on('close', (code) => {
|
|
274
|
+
finish(termination === 'timeout' ? 124 : termination === 'cancelled' ? 130 : (code ?? 1));
|
|
275
|
+
});
|
|
276
|
+
if (auth) {
|
|
277
|
+
const credentialPipe = child.stdio[3];
|
|
278
|
+
if (!credentialPipe) {
|
|
279
|
+
fail(new Error('Could not establish one-use Git credential pipe'));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
credentialPipe.on('error', () => {
|
|
283
|
+
/* EPIPE if Git fails before auth. */
|
|
284
|
+
});
|
|
285
|
+
if (termination) {
|
|
286
|
+
credentialPipe.destroy();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
credentialPipe.end(`${auth.token}\n`);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
fail(error instanceof Error ? error : new Error('Could not write the one-use Git credential'));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
export async function gitOk(runGit, args, options, operation) {
|
|
298
|
+
const result = await runGit(args, options);
|
|
299
|
+
if (result.timedOut)
|
|
300
|
+
throw new Error(`${operation} timed out`);
|
|
301
|
+
if (result.cancelled)
|
|
302
|
+
throw new Error(`${operation} cancelled`);
|
|
303
|
+
if (result.exitCode !== 0) {
|
|
304
|
+
const secrets = options.auth ? [options.auth.token] : [];
|
|
305
|
+
const detail = redactSecrets((result.stderr || result.stdout).trim(), secrets);
|
|
306
|
+
throw new Error(detail ? `${operation} failed: ${detail}` : `${operation} failed`);
|
|
307
|
+
}
|
|
308
|
+
return result.stdout.trim();
|
|
309
|
+
}
|
|
310
|
+
export async function revParse(runGit, cwd, ref) {
|
|
311
|
+
return gitOk(runGit, ['rev-parse', '--verify', ref], { cwd }, `resolve ${ref}`);
|
|
312
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const CLOUDFLARE_ARTIFACTS_HOST_PATTERN: RegExp;
|
|
2
|
+
export declare const CLOUDFLARE_ARTIFACTS_GIT_PATH_PATTERN: RegExp;
|
|
3
|
+
export declare function parseCanonicalCredentialFreeHttpsUrl(remote: unknown): URL | null;
|
|
4
|
+
export declare function isValidCloudflareArtifactsGitRemote(remote: unknown): remote is string;
|
|
5
|
+
//# sourceMappingURL=cloudflare-artifacts-git-remote.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudflare-artifacts-git-remote.d.ts","sourceRoot":"","sources":["../src/cloudflare-artifacts-git-remote.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iCAAiC,QACC,CAAA;AAE/C,eAAO,MAAM,qCAAqC,QACmC,CAAA;AAErF,wBAAgB,oCAAoC,CAAC,MAAM,EAAE,OAAO,GAAG,GAAG,GAAG,IAAI,CAWhF;AAED,wBAAgB,mCAAmC,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,IAAI,MAAM,CAIrF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const CLOUDFLARE_ARTIFACTS_HOST_PATTERN = /^[0-9a-f]{32}\.artifacts\.cloudflare\.net$/u;
|
|
2
|
+
export const CLOUDFLARE_ARTIFACTS_GIT_PATH_PATTERN = /^\/git\/[A-Za-z0-9][A-Za-z0-9._-]{0,255}\/[A-Za-z0-9][A-Za-z0-9._-]{0,255}\.git$/u;
|
|
3
|
+
export function parseCanonicalCredentialFreeHttpsUrl(remote) {
|
|
4
|
+
if (typeof remote !== 'string')
|
|
5
|
+
return null;
|
|
6
|
+
try {
|
|
7
|
+
const parsed = new URL(remote);
|
|
8
|
+
return parsed.protocol === 'https:' && !parsed.username && !parsed.password &&
|
|
9
|
+
!parsed.port && !parsed.search && !parsed.hash && parsed.toString() === remote
|
|
10
|
+
? parsed
|
|
11
|
+
: null;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function isValidCloudflareArtifactsGitRemote(remote) {
|
|
18
|
+
const parsed = parseCanonicalCredentialFreeHttpsUrl(remote);
|
|
19
|
+
return parsed !== null && CLOUDFLARE_ARTIFACTS_HOST_PATTERN.test(parsed.hostname) &&
|
|
20
|
+
CLOUDFLARE_ARTIFACTS_GIT_PATH_PATTERN.test(parsed.pathname);
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-transfer-download.d.ts","sourceRoot":"","sources":["../src/dataset-transfer-download.ts"],"names":[],"mappings":"AAmBA,wBAAsB,uBAAuB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyChH"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Effect, Either, Schedule } from 'effect';
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
|
+
import { datasetTransferTimeoutMs } from './dataset-transfer-policy.js';
|
|
4
|
+
import { authedFetch } from './http.js';
|
|
5
|
+
class DownloadHttpError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
_tag = 'DownloadHttpError';
|
|
8
|
+
constructor(status, detail) {
|
|
9
|
+
super(`Download failed (HTTP ${status}): ${detail}`);
|
|
10
|
+
this.status = status;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
class DownloadTransportError extends Error {
|
|
14
|
+
_tag = 'DownloadTransportError';
|
|
15
|
+
constructor(cause) {
|
|
16
|
+
super('Dataset download transport failed', { cause });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function transferDatasetDownload(datasetId, format, filename) {
|
|
20
|
+
const timeoutMs = datasetTransferTimeoutMs();
|
|
21
|
+
const download = Effect.tryPromise({
|
|
22
|
+
try: async (signal) => {
|
|
23
|
+
const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/download?format=${encodeURIComponent(format)}`, { signal });
|
|
24
|
+
if (!response.ok)
|
|
25
|
+
throw new DownloadHttpError(response.status, await response.text());
|
|
26
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
27
|
+
},
|
|
28
|
+
catch: cause => {
|
|
29
|
+
if (cause instanceof TypeError)
|
|
30
|
+
return new DownloadTransportError(cause);
|
|
31
|
+
return cause instanceof Error ? cause : new Error('Dataset download request failed', { cause });
|
|
32
|
+
},
|
|
33
|
+
}).pipe(Effect.timeoutFail({
|
|
34
|
+
duration: timeoutMs,
|
|
35
|
+
onTimeout: () => new Error('DATASET_TRANSFER_TIMEOUT: Dataset download exceeded its deadline'),
|
|
36
|
+
}), Effect.retry({
|
|
37
|
+
times: 2,
|
|
38
|
+
schedule: Schedule.spaced(100),
|
|
39
|
+
while: error => error instanceof DownloadTransportError || (error instanceof DownloadHttpError && [429, 502, 503, 504].includes(error.status)),
|
|
40
|
+
}));
|
|
41
|
+
const fileError = (cause) => new Error(`DATASET_DOWNLOAD_FILE: Could not save dataset to ${filename}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
42
|
+
const write = (bytes) => Effect.try({
|
|
43
|
+
// Wait for the complete response, then retain the CLI's existing in-place
|
|
44
|
+
// write semantics, including inode metadata, hardlinks and symlink targets.
|
|
45
|
+
try: () => writeFileSync(filename, bytes, { mode: 0o600 }),
|
|
46
|
+
catch: fileError,
|
|
47
|
+
});
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const handleInterrupt = () => controller.abort();
|
|
50
|
+
process.once('SIGINT', handleInterrupt);
|
|
51
|
+
process.once('SIGTERM', handleInterrupt);
|
|
52
|
+
try {
|
|
53
|
+
const result = await Effect.runPromise(Effect.either(Effect.flatMap(download, write)), { signal: controller.signal });
|
|
54
|
+
if (Either.isLeft(result))
|
|
55
|
+
throw result.left;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (controller.signal.aborted)
|
|
59
|
+
throw new Error('DATASET_TRANSFER_INTERRUPTED: Dataset download cancelled');
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
process.removeListener('SIGINT', handleInterrupt);
|
|
64
|
+
process.removeListener('SIGTERM', handleInterrupt);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-transfer-policy.d.ts","sourceRoot":"","sources":["../src/dataset-transfer-policy.ts"],"names":[],"mappings":"AAAA,wBAAgB,wBAAwB,IAAI,MAAM,CAOjD"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function datasetTransferTimeoutMs() {
|
|
2
|
+
const configured = process.env.ORIZU_DATASET_TRANSFER_TIMEOUT_MS;
|
|
3
|
+
const timeoutMs = configured === undefined ? 120_000 : Number(configured);
|
|
4
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300_000) {
|
|
5
|
+
throw new Error('DATASET_TRANSFER_CONFIG: ORIZU_DATASET_TRANSFER_TIMEOUT_MS must be an integer from 1 to 300000');
|
|
6
|
+
}
|
|
7
|
+
return timeoutMs;
|
|
8
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface DatasetUploadResponse {
|
|
2
|
+
dataset: {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
rowCount: number;
|
|
6
|
+
sourceType: string;
|
|
7
|
+
url?: string;
|
|
8
|
+
};
|
|
9
|
+
readmeVersion?: {
|
|
10
|
+
id: string;
|
|
11
|
+
version_num: number;
|
|
12
|
+
created_at: string;
|
|
13
|
+
} | null;
|
|
14
|
+
}
|
|
15
|
+
interface DatasetSnapshot {
|
|
16
|
+
datasetVersion: {
|
|
17
|
+
id: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
type UploadRows = Array<Record<string, unknown>>;
|
|
21
|
+
type UploadSource = {
|
|
22
|
+
kind: 'jsonl';
|
|
23
|
+
file: string;
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'rows';
|
|
26
|
+
rows: UploadRows;
|
|
27
|
+
sourceType: 'csv' | 'json' | 'jsonl';
|
|
28
|
+
};
|
|
29
|
+
export interface DatasetUploadOptions {
|
|
30
|
+
project: string;
|
|
31
|
+
name: string;
|
|
32
|
+
source: UploadSource;
|
|
33
|
+
readmeMarkdown: string | null;
|
|
34
|
+
json: boolean;
|
|
35
|
+
onChunk: (index: number, rowCount: number) => void;
|
|
36
|
+
}
|
|
37
|
+
export declare function transferDatasetUpload(options: DatasetUploadOptions): Promise<{
|
|
38
|
+
data: DatasetUploadResponse;
|
|
39
|
+
version: DatasetSnapshot;
|
|
40
|
+
}>;
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=dataset-transfer-upload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dataset-transfer-upload.d.ts","sourceRoot":"","sources":["../src/dataset-transfer-upload.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACzF,aAAa,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CAC/E;AAED,UAAU,eAAe;IAAG,cAAc,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE;AAE5D,KAAK,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAChD,KAAK,YAAY,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,UAAU,CAAC;IAAC,UAAU,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAA;CAAE,CAAA;AAE9H,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,YAAY,CAAA;IACpB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CACnD;AAoHD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,qBAAqB,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,CAAC,CAEvI"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Context, Effect, Either, Layer } from 'effect';
|
|
2
|
+
import { ensureDatasetUploadSnapshot } from './dataset-upload-snapshot.js';
|
|
3
|
+
import { datasetTransferTimeoutMs } from './dataset-transfer-policy.js';
|
|
4
|
+
import { extractErrorMessage } from './error-response.js';
|
|
5
|
+
import { authedFetch } from './http.js';
|
|
6
|
+
import { parseJsonResponse, sanitizeTerminalText } from './json-response.js';
|
|
7
|
+
import { streamJsonlRowChunks } from './jsonl-stream.js';
|
|
8
|
+
class DatasetWriteFailure extends Error {
|
|
9
|
+
outcome;
|
|
10
|
+
_tag = 'DatasetWriteFailure';
|
|
11
|
+
constructor(message, outcome, options) {
|
|
12
|
+
super(message, options);
|
|
13
|
+
this.outcome = outcome;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
class DatasetInputFailure extends Error {
|
|
17
|
+
_tag = 'DatasetInputFailure';
|
|
18
|
+
constructor(cause) { super(cause instanceof Error ? cause.message : String(cause), { cause }); }
|
|
19
|
+
}
|
|
20
|
+
const DatasetUploadApi = Context.GenericTag('orizu/DatasetUploadApi');
|
|
21
|
+
// Only the existing authentication client's rejected-token refresh may replay a
|
|
22
|
+
// request. No transient/transport retries belong around these non-idempotent writes.
|
|
23
|
+
function post(path, body, context, prefix, timeoutMs) {
|
|
24
|
+
return Effect.suspend(() => {
|
|
25
|
+
let requestStarted = false;
|
|
26
|
+
return Effect.tryPromise({
|
|
27
|
+
try: async (signal) => {
|
|
28
|
+
const response = await authedFetch(path, {
|
|
29
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
body: JSON.stringify(body), signal,
|
|
31
|
+
}, () => { requestStarted = true; });
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new DatasetWriteFailure(`${prefix}: ${await extractErrorMessage(response)}`, response.status >= 500 ? 'unknown' : 'rejected');
|
|
34
|
+
}
|
|
35
|
+
return await parseJsonResponse(response, context);
|
|
36
|
+
},
|
|
37
|
+
catch: cause => cause instanceof DatasetWriteFailure ? cause : new DatasetWriteFailure(cause instanceof Error ? cause.message : String(cause), requestStarted ? 'unknown' : 'rejected', { cause }),
|
|
38
|
+
}).pipe(Effect.timeoutFail({
|
|
39
|
+
duration: timeoutMs,
|
|
40
|
+
onTimeout: () => new DatasetWriteFailure(`DATASET_TRANSFER_TIMEOUT: ${context} exceeded its deadline`, requestStarted ? 'unknown' : 'rejected'),
|
|
41
|
+
}));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function liveUploadApi(options) {
|
|
45
|
+
const timeoutMs = datasetTransferTimeoutMs();
|
|
46
|
+
return Layer.succeed(DatasetUploadApi, {
|
|
47
|
+
create: (rows, sourceType) => post('/api/cli/datasets/upload', {
|
|
48
|
+
projectSlug: options.project, name: options.name, rows, sourceType,
|
|
49
|
+
...(options.readmeMarkdown !== null ? { readmeMarkdown: options.readmeMarkdown } : {}),
|
|
50
|
+
}, 'Dataset upload', 'Upload failed', timeoutMs),
|
|
51
|
+
append: (id, rows) => post(`/api/cli/datasets/${encodeURIComponent(id)}/rows`, { rows }, 'Dataset append', 'Append failed', timeoutMs),
|
|
52
|
+
snapshot: id => post(`/api/cli/datasets/${encodeURIComponent(id)}/versions`, {}, 'Dataset version create', 'Failed to create dataset version', timeoutMs),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async function expectedPromise(program) {
|
|
56
|
+
const result = await Effect.runPromise(Effect.either(program));
|
|
57
|
+
if (Either.isLeft(result))
|
|
58
|
+
throw result.left;
|
|
59
|
+
return result.right;
|
|
60
|
+
}
|
|
61
|
+
function uploadProgram(options) {
|
|
62
|
+
return Effect.gen(function* () {
|
|
63
|
+
const api = yield* DatasetUploadApi;
|
|
64
|
+
const chunks = yield* Effect.acquireRelease(Effect.sync(() => options.source.kind === 'jsonl'
|
|
65
|
+
? streamJsonlRowChunks(options.source.file)
|
|
66
|
+
: (async function* () { if (options.source.kind === 'rows')
|
|
67
|
+
yield options.source.rows; })()), iterator => Effect.promise(async () => { await iterator.return(undefined); }));
|
|
68
|
+
let data;
|
|
69
|
+
let confirmedRows = 0;
|
|
70
|
+
let chunkIndex = 0;
|
|
71
|
+
while (true) {
|
|
72
|
+
const next = yield* Effect.tryPromise({ try: () => chunks.next(), catch: cause => new DatasetInputFailure(cause) }).pipe(Effect.mapError(error => new Error(data
|
|
73
|
+
? `Upload stopped while reading the next JSONL chunk: ${error.message}\nDataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) was created and ${confirmedRows} rows were uploaded. Fix the file, remove the first ${confirmedRows} rows, and run orizu datasets append --dataset ${data.dataset.id} --file <remaining-file>.`
|
|
74
|
+
: error.message)));
|
|
75
|
+
if (next.done)
|
|
76
|
+
break;
|
|
77
|
+
chunkIndex++;
|
|
78
|
+
if (options.source.kind === 'jsonl')
|
|
79
|
+
yield* Effect.sync(() => options.onChunk(chunkIndex, next.value.length));
|
|
80
|
+
const write = data
|
|
81
|
+
? api.append(data.dataset.id, next.value).pipe(Effect.map(result => ({
|
|
82
|
+
data: { ...data, dataset: { ...data.dataset, rowCount: result.dataset.rowCount } },
|
|
83
|
+
added: result.appendedCount,
|
|
84
|
+
})))
|
|
85
|
+
: api.create(next.value, options.source.kind === 'jsonl' ? 'jsonl' : options.source.sourceType).pipe(Effect.map(result => ({ data: result, added: result.dataset.rowCount })));
|
|
86
|
+
const result = yield* write.pipe(Effect.mapError(error => {
|
|
87
|
+
if (error.outcome === 'unknown') {
|
|
88
|
+
return new Error(`DATASET_UPLOAD_UNCERTAIN: Chunk ${chunkIndex} may have committed, but its outcome could not be confirmed: ${error.message}\n` +
|
|
89
|
+
(data ? `Dataset ${sanitizeTerminalText(data.dataset.id)} has ${confirmedRows} confirmed uploaded rows. ` : 'Dataset creation could not be confirmed. ') +
|
|
90
|
+
'Inspect the dataset and reconcile row IDs before retrying; do not replay this chunk blindly.');
|
|
91
|
+
}
|
|
92
|
+
return new Error(data
|
|
93
|
+
? `Chunk ${chunkIndex} failed: ${error.message}\nDataset ${sanitizeTerminalText(data.dataset.name)} (${sanitizeTerminalText(data.dataset.id)}) was created and ${confirmedRows} rows were uploaded. To retry, remove the first ${confirmedRows} rows from your file and run orizu datasets append --dataset ${data.dataset.id} --file <remaining-file>.`
|
|
94
|
+
: error.message);
|
|
95
|
+
}));
|
|
96
|
+
data = result.data;
|
|
97
|
+
confirmedRows += result.added;
|
|
98
|
+
}
|
|
99
|
+
if (!data)
|
|
100
|
+
return yield* Effect.fail(new Error('Dataset file contains no rows'));
|
|
101
|
+
const version = yield* Effect.tryPromise({
|
|
102
|
+
try: () => ensureDatasetUploadSnapshot(data.dataset, options.json, id => expectedPromise(api.snapshot(id))),
|
|
103
|
+
catch: cause => cause instanceof Error ? cause : new Error(String(cause)),
|
|
104
|
+
});
|
|
105
|
+
return { data, version };
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
export function transferDatasetUpload(options) {
|
|
109
|
+
return expectedPromise(uploadProgram(options).pipe(Effect.scoped, Effect.provide(liveUploadApi(options))));
|
|
110
|
+
}
|