orizu 0.5.40 → 0.5.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/dataset-clone-workflow.d.ts +11 -0
  2. package/dist/dataset-clone-workflow.d.ts.map +1 -0
  3. package/dist/dataset-clone-workflow.js +100 -0
  4. package/dist/dataset-commands.d.ts.map +1 -1
  5. package/dist/dataset-commands.js +6 -29
  6. package/dist/dataset-transfer-upload.d.ts +2 -6
  7. package/dist/dataset-transfer-upload.d.ts.map +1 -1
  8. package/dist/dataset-transfer-upload.js +9 -2
  9. package/dist/dataset-upload-snapshot.d.ts.map +1 -1
  10. package/dist/dataset-upload-snapshot.js +7 -1
  11. package/dist/dataset-version-workflow.d.ts +24 -0
  12. package/dist/dataset-version-workflow.d.ts.map +1 -0
  13. package/dist/dataset-version-workflow.js +99 -0
  14. package/dist/feedback-cli.js +3 -3
  15. package/dist/hosted-harness-opencode.d.ts +1 -1
  16. package/dist/hosted-harness-opencode.js +7 -7
  17. package/dist/hosted-loop-lifecycle.d.ts +3 -2
  18. package/dist/hosted-loop-lifecycle.d.ts.map +1 -1
  19. package/dist/hosted-loop-lifecycle.js +3 -2
  20. package/dist/hosted-loop.d.ts +7 -0
  21. package/dist/hosted-loop.d.ts.map +1 -1
  22. package/dist/hosted-loop.js +14 -6
  23. package/dist/index.js +2 -2
  24. package/dist/last-error-record.d.ts +4 -2
  25. package/dist/last-error-record.d.ts.map +1 -1
  26. package/dist/last-error-record.js +10 -68
  27. package/dist/last-error-workflow.d.ts +4 -0
  28. package/dist/last-error-workflow.d.ts.map +1 -0
  29. package/dist/last-error-workflow.js +78 -0
  30. package/dist/task-commands.d.ts.map +1 -1
  31. package/dist/task-commands.js +9 -16
  32. package/dist/task-report-workflow.d.ts +47 -0
  33. package/dist/task-report-workflow.d.ts.map +1 -0
  34. package/dist/task-report-workflow.js +160 -0
  35. package/package.json +1 -1
  36. package/src/dataset-clone-workflow.ts +100 -0
  37. package/src/dataset-commands.ts +6 -53
  38. package/src/dataset-transfer-upload.ts +12 -5
  39. package/src/dataset-upload-snapshot.ts +8 -1
  40. package/src/dataset-version-workflow.ts +105 -0
  41. package/src/feedback-cli.ts +4 -4
  42. package/src/hosted-harness-opencode.ts +7 -7
  43. package/src/hosted-loop-lifecycle.ts +3 -2
  44. package/src/hosted-loop.ts +14 -6
  45. package/src/index.ts +2 -2
  46. package/src/last-error-record.ts +11 -85
  47. package/src/last-error-workflow.ts +121 -0
  48. package/src/task-commands.ts +9 -43
  49. package/src/task-report-workflow.ts +179 -0
@@ -1 +1 @@
1
- {"version":3,"file":"last-error-record.d.ts","sourceRoot":"","sources":["../src/last-error-record.ts"],"names":[],"mappings":"AAkCA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,CAAC,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,aAAa,EAAE,OAAO,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED,eAAO,MAAM,yBAAyB,IAAI,CAAA;AAC1C,eAAO,MAAM,mBAAmB,oBAAoB,CAAA;AACpD,eAAO,MAAM,4BAA4B,WAAa,CAAA;AACtD,eAAO,MAAM,4BAA4B,gBAAgB,CAAA;AAezD,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,GAAG,CAAC,EAAE,IAAI,CAAA;CACX;AAED,MAAM,WAAW,2BAA2B;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA8ID,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAKlE;AA6DD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,oBAAoB,GAAG,eAAe,CA0DjF;AAED,wBAAgB,sBAAsB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,oBAAoB,EAC3B,OAAO,GAAE,2BAAgC,GACxC,IAAI,CAyCN;AAED,wBAAgB,mBAAmB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAgB9E"}
1
+ {"version":3,"file":"last-error-record.d.ts","sourceRoot":"","sources":["../src/last-error-record.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,CAAC,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,aAAa,EAAE,OAAO,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED,eAAO,MAAM,yBAAyB,IAAI,CAAA;AAC1C,eAAO,MAAM,mBAAmB,oBAAoB,CAAA;AACpD,eAAO,MAAM,4BAA4B,WAAa,CAAA;AACtD,eAAO,MAAM,4BAA4B,gBAAgB,CAAA;AAezD,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC3B,GAAG,CAAC,EAAE,IAAI,CAAA;CACX;AAED,MAAM,WAAW,2BAA2B;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AA8ID,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAKlE;AAUD,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EACd,iBAAiB,EAAE,MAAM,EACzB,YAAY,EAAE,OAAO,GACpB,KAAK,IAAI,MAAM,CAMjB;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAIpF;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,oBAAoB,GAAG,eAAe,CA0DjF;AAED,wBAAgB,sBAAsB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,oBAAoB,EAC3B,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED,wBAAsB,mBAAmB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAQ7F"}
@@ -1,5 +1,3 @@
1
- import { randomBytes } from 'crypto';
2
- import { chmodSync, closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from 'fs';
3
1
  import { homedir } from 'os';
4
2
  import { join } from 'path';
5
3
  import { getConfigDir, getSecretsSeenThisProcess, resolveAuthTokenForBaseUrl, resolveEnvBearerToken, } from './credentials.js';
@@ -154,7 +152,7 @@ function resolveHomeDir() {
154
152
  return null;
155
153
  }
156
154
  }
157
- function isStringWithinCap(value, maximumCharacters, wasTruncated) {
155
+ export function isStringWithinCap(value, maximumCharacters, wasTruncated) {
158
156
  if (typeof value !== 'string')
159
157
  return false;
160
158
  if (value.length <= maximumCharacters)
@@ -163,41 +161,11 @@ function isStringWithinCap(value, maximumCharacters, wasTruncated) {
163
161
  && value.length <= maximumCharacters + LAST_ERROR_TRUNCATION_MARKER.length
164
162
  && value.endsWith(LAST_ERROR_TRUNCATION_MARKER);
165
163
  }
166
- function boundedServerBaseUrl(value) {
164
+ export function boundedServerBaseUrl(value) {
167
165
  return value && Buffer.byteLength(value, 'utf8') <= LAST_ERROR_SERVER_BASE_URL_MAX_BYTES
168
166
  ? value
169
167
  : null;
170
168
  }
171
- function isLastErrorRecord(value) {
172
- if (!value || typeof value !== 'object' || Array.isArray(value))
173
- return false;
174
- const record = value;
175
- if (record.version !== LAST_ERROR_RECORD_VERSION)
176
- return false;
177
- if (typeof record.recordedAt !== 'string' || !Number.isFinite(Date.parse(record.recordedAt)))
178
- return false;
179
- if (typeof record.cliVersion !== 'string' && record.cliVersion !== null)
180
- return false;
181
- if (record.code !== undefined && record.code !== null && (typeof record.code !== 'string'
182
- || Array.from(record.code).length > LAST_ERROR_CODE_MAX_CHARS))
183
- return false;
184
- if (record.serverBaseUrl !== undefined && typeof record.serverBaseUrl !== 'string' && record.serverBaseUrl !== null)
185
- return false;
186
- if (record.teamSlug !== undefined && typeof record.teamSlug !== 'string' && record.teamSlug !== null)
187
- return false;
188
- if (typeof record.argvTruncated !== 'boolean')
189
- return false;
190
- const argvTruncated = record.argvTruncated;
191
- if (record.command !== null && !isStringWithinCap(record.command, LAST_ERROR_ARG_MAX_CHARS, argvTruncated))
192
- return false;
193
- if (!Array.isArray(record.argv) || record.argv.length > LAST_ERROR_MAX_ARGS)
194
- return false;
195
- if (!record.argv.every(argument => isStringWithinCap(argument, LAST_ERROR_ARG_MAX_CHARS, argvTruncated)))
196
- return false;
197
- if (typeof record.messageTruncated !== 'boolean')
198
- return false;
199
- return isStringWithinCap(record.message, LAST_ERROR_MESSAGE_MAX_CHARS, record.messageTruncated);
200
- }
201
169
  export function buildLastErrorRecord(input) {
202
170
  const context = {
203
171
  homeDir: input.homeDir ?? null,
@@ -255,11 +223,9 @@ export function buildLastErrorRecord(input) {
255
223
  export function getLastErrorRecordPath(configDir) {
256
224
  return join(configDir ?? getConfigDir(), LAST_ERROR_FILENAME);
257
225
  }
258
- export function writeLastErrorRecord(input, options = {}) {
226
+ export async function writeLastErrorRecord(input, options = {}) {
259
227
  try {
260
228
  const configDirectory = options.configDir ?? getConfigDir();
261
- mkdirSync(configDirectory, { recursive: true, mode: 0o700 });
262
- chmodSync(configDirectory, 0o700);
263
229
  const record = buildLastErrorRecord({
264
230
  ...input,
265
231
  homeDir: input.homeDir === undefined ? resolveHomeDir() : input.homeDir,
@@ -272,43 +238,19 @@ export function writeLastErrorRecord(input, options = {}) {
272
238
  const serialized = serializeRecord(record);
273
239
  if (Buffer.byteLength(serialized, 'utf8') > LAST_ERROR_FILE_MAX_BYTES)
274
240
  return;
275
- const path = getLastErrorRecordPath(configDirectory);
276
- const temporaryPath = join(configDirectory, `.${LAST_ERROR_FILENAME}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`);
277
- try {
278
- const fileDescriptor = openSync(temporaryPath, 'wx', 0o600);
279
- try {
280
- writeFileSync(fileDescriptor, serialized, 'utf8');
281
- fsyncSync(fileDescriptor);
282
- }
283
- finally {
284
- closeSync(fileDescriptor);
285
- }
286
- chmodSync(temporaryPath, 0o600);
287
- renameSync(temporaryPath, path);
288
- chmodSync(path, 0o600);
289
- }
290
- finally {
291
- rmSync(temporaryPath, { force: true });
292
- }
241
+ const { publishLastErrorRecord } = await import('./last-error-workflow.js');
242
+ await publishLastErrorRecord(configDirectory, serialized);
293
243
  }
294
244
  catch {
295
- // A diagnostic record is best effort and must never replace the original CLI failure.
245
+ // A diagnostic record is best effort and must never replace the original CLI failure,
246
+ // including when Effect cannot load (a packed CLI without its dependencies).
296
247
  }
297
248
  }
298
- export function readLastErrorRecord(configDir) {
249
+ export async function readLastErrorRecord(configDir) {
299
250
  try {
300
251
  const path = getLastErrorRecordPath(configDir);
301
- if (statSync(path).size > LAST_ERROR_FILE_MAX_BYTES)
302
- return null;
303
- const record = JSON.parse(readFileSync(path, 'utf8'));
304
- return isLastErrorRecord(record)
305
- ? {
306
- ...record,
307
- code: record.code ?? null,
308
- serverBaseUrl: boundedServerBaseUrl(record.serverBaseUrl),
309
- teamSlug: record.teamSlug ?? null,
310
- }
311
- : null;
252
+ const { loadLastErrorRecord } = await import('./last-error-workflow.js');
253
+ return await loadLastErrorRecord(path);
312
254
  }
313
255
  catch {
314
256
  return null;
@@ -0,0 +1,4 @@
1
+ import { type LastErrorRecord } from './last-error-record.js';
2
+ export declare function publishLastErrorRecord(configDirectory: string, serialized: string): Promise<void>;
3
+ export declare function loadLastErrorRecord(path: string): Promise<LastErrorRecord | null>;
4
+ //# sourceMappingURL=last-error-workflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"last-error-workflow.d.ts","sourceRoot":"","sources":["../src/last-error-workflow.ts"],"names":[],"mappings":"AAuBA,OAAO,EAKL,KAAK,eAAe,EACrB,MAAM,wBAAwB,CAAA;AAmE/B,wBAAgB,sBAAsB,CAAC,eAAe,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEjG;AAoBD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAEjF"}
@@ -0,0 +1,78 @@
1
+ import { randomBytes } from 'crypto';
2
+ import { chmodSync, closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from 'fs';
3
+ import { join } from 'path';
4
+ import { Effect, Schema } from 'effect';
5
+ import { LAST_ERROR_ARG_MAX_CHARS, LAST_ERROR_CODE_MAX_CHARS, LAST_ERROR_FILE_MAX_BYTES, LAST_ERROR_MAX_ARGS, LAST_ERROR_MESSAGE_MAX_CHARS, } from './last-error-caps.js';
6
+ import { boundedServerBaseUrl, isStringWithinCap, LAST_ERROR_FILENAME, LAST_ERROR_RECORD_VERSION, } from './last-error-record.js';
7
+ // ORI-2450: Effect owns the last-error file's publication and reading.
8
+ // last-error-record.ts loads this module with a literal dynamic import, so the
9
+ // npm CLI only loads Effect on a failure or a feedback read, and the source
10
+ // bundle still includes it.
11
+ class LastErrorFileFailure extends Error {
12
+ operation;
13
+ constructor(operation) {
14
+ super(`Could not ${operation}`);
15
+ this.operation = operation;
16
+ }
17
+ }
18
+ const file = (operation, action) => Effect.try({
19
+ try: action,
20
+ catch: () => new LastErrorFileFailure(operation),
21
+ });
22
+ const NullableString = Schema.NullOr(Schema.String);
23
+ const LastErrorRecordSchema = Schema.Struct({
24
+ version: Schema.Literal(LAST_ERROR_RECORD_VERSION),
25
+ recordedAt: Schema.String.check(Schema.makeFilter(value => Number.isFinite(Date.parse(value)))),
26
+ cliVersion: NullableString,
27
+ code: Schema.optionalKey(NullableString.check(Schema.makeFilter(value => value === null || Array.from(value).length <= LAST_ERROR_CODE_MAX_CHARS))),
28
+ serverBaseUrl: Schema.optionalKey(NullableString),
29
+ teamSlug: Schema.optionalKey(NullableString),
30
+ command: NullableString,
31
+ argv: Schema.Array(Schema.String),
32
+ argvTruncated: Schema.Boolean,
33
+ message: Schema.String,
34
+ messageTruncated: Schema.Boolean,
35
+ }).check(Schema.makeFilter(record => (record.command === null || isStringWithinCap(record.command, LAST_ERROR_ARG_MAX_CHARS, record.argvTruncated))
36
+ && record.argv.length <= LAST_ERROR_MAX_ARGS
37
+ && record.argv.every(argument => isStringWithinCap(argument, LAST_ERROR_ARG_MAX_CHARS, record.argvTruncated))
38
+ && isStringWithinCap(record.message, LAST_ERROR_MESSAGE_MAX_CHARS, record.messageTruncated)));
39
+ const publication = (configDirectory, serialized) => Effect.gen(function* () {
40
+ yield* file('create directory', () => mkdirSync(configDirectory, { recursive: true, mode: 0o700 }));
41
+ yield* file('protect directory', () => chmodSync(configDirectory, 0o700));
42
+ const path = join(configDirectory, LAST_ERROR_FILENAME);
43
+ // Write a private temporary file, then rename it over the record, so a
44
+ // reader never sees a half-written record and a failed write keeps the old one.
45
+ yield* Effect.acquireUseRelease(Effect.sync(() => join(configDirectory, `.${LAST_ERROR_FILENAME}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`)), temporaryPath => Effect.gen(function* () {
46
+ yield* Effect.acquireUseRelease(file('open temporary record', () => openSync(temporaryPath, 'wx', 0o600)), descriptor => Effect.gen(function* () {
47
+ yield* file('write temporary record', () => writeFileSync(descriptor, serialized, 'utf8'));
48
+ yield* file('flush temporary record', () => fsyncSync(descriptor));
49
+ }), descriptor => Effect.sync(() => closeSync(descriptor)));
50
+ yield* file('protect temporary record', () => chmodSync(temporaryPath, 0o600));
51
+ yield* file('publish record', () => renameSync(temporaryPath, path));
52
+ yield* file('protect published record', () => chmodSync(path, 0o600));
53
+ }), temporaryPath => Effect.sync(() => rmSync(temporaryPath, { force: true })));
54
+ });
55
+ export function publishLastErrorRecord(configDirectory, serialized) {
56
+ return Effect.runPromise(publication(configDirectory, serialized));
57
+ }
58
+ const loading = (path) => Effect.gen(function* () {
59
+ const stat = yield* file('inspect record', () => statSync(path));
60
+ if (stat.size > LAST_ERROR_FILE_MAX_BYTES)
61
+ return null;
62
+ const text = yield* file('read record', () => readFileSync(path, 'utf8'));
63
+ const raw = yield* file('parse record', () => JSON.parse(text));
64
+ const record = yield* Schema.decodeUnknownEffect(LastErrorRecordSchema)(raw);
65
+ // The Schema checks the known fields; the reader has always kept any others.
66
+ const admitted = {
67
+ ...raw,
68
+ ...record,
69
+ argv: [...record.argv],
70
+ code: record.code ?? null,
71
+ serverBaseUrl: boundedServerBaseUrl(record.serverBaseUrl),
72
+ teamSlug: record.teamSlug ?? null,
73
+ };
74
+ return admitted;
75
+ });
76
+ export function loadLastErrorRecord(path) {
77
+ return Effect.runPromise(loading(path).pipe(Effect.orElseSucceed(() => null)));
78
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"task-commands.d.ts","sourceRoot":"","sources":["../src/task-commands.ts"],"names":[],"mappings":"AA8CA,iBAAe,SAAS,kBAWvB;AAED,iBAAS,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,EAAE,CAQ3D;AA6CD,iBAAe,UAAU,kBA0IxB;AAED,iBAAe,UAAU,kBA+GxB;AAED,iBAAe,WAAW,kBAkCzB;AAED,iBAAe,WAAW,kBAoEzB;AAED,iBAAe,UAAU,kBA8DxB;AAED,iBAAe,UAAU,kBA0CxB;AAED,iBAAe,gBAAgB,CAAC,YAAY,EAAE,QAAQ,GAAG,QAAQ,GAAG,WAAW,iBAwB9E;AAED,iBAAe,aAAa,kBAkD3B;AAED,iBAAe,aAAa,kBA+B3B;AAED,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,UAAU,EACV,UAAU,EACV,WAAW,EACX,WAAW,EACX,UAAU,EACV,UAAU,EACV,aAAa,EACb,aAAa,EACb,gBAAgB,GACjB,CAAA"}
1
+ {"version":3,"file":"task-commands.d.ts","sourceRoot":"","sources":["../src/task-commands.ts"],"names":[],"mappings":"AAgCA,iBAAe,SAAS,kBAWvB;AAED,iBAAS,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,EAAE,CAQ3D;AA6CD,iBAAe,UAAU,kBA0IxB;AAED,iBAAe,UAAU,kBA+GxB;AAED,iBAAe,WAAW,kBAkCzB;AAED,iBAAe,WAAW,kBAoEzB;AAED,iBAAe,UAAU,kBA8DxB;AAED,iBAAe,UAAU,kBA0CxB;AAED,iBAAe,gBAAgB,CAAC,YAAY,EAAE,QAAQ,GAAG,QAAQ,GAAG,WAAW,iBAwB9E;AAED,iBAAe,aAAa,kBAkC3B;AAED,iBAAe,aAAa,kBA2B3B;AAED,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,UAAU,EACV,UAAU,EACV,WAAW,EACX,WAAW,EACX,UAAU,EACV,UAAU,EACV,aAAa,EACb,aAAa,EACb,gBAAgB,GACjB,CAAA"}
@@ -461,18 +461,14 @@ async function setTaskReport() {
461
461
  if (!report) {
462
462
  throw new Error('Usage: orizu tasks report set --task <taskId> (--report <markdown|@file> | --report-file <path>) [--json]');
463
463
  }
464
- const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/report`, {
465
- method: 'PATCH',
466
- headers: { 'Content-Type': 'application/json' },
467
- body: JSON.stringify({
468
- reportMarkdown: report.markdown,
469
- reportSourceName: report.sourceName,
470
- }),
471
- });
472
- if (!response.ok) {
473
- throw new Error(`Failed to update task report: ${await response.text()}`);
464
+ // Loaded lazily: the workflow pulls in Effect (ORI-2408).
465
+ const workflow = await import('./task-report-workflow.js');
466
+ const outcome = await workflow.setTaskReport(taskId, report);
467
+ if (outcome.kind === 'interrupted') {
468
+ process.exitCode = outcome.exitCode;
469
+ return;
474
470
  }
475
- const data = await parseJsonResponse(response, 'Task report update');
471
+ const data = outcome.reply;
476
472
  if (hasJsonFlag()) {
477
473
  printJson(data);
478
474
  return;
@@ -486,11 +482,8 @@ async function getTaskReport() {
486
482
  if (!taskId) {
487
483
  throw new Error('Usage: orizu tasks report get --task <taskId> [--json]');
488
484
  }
489
- const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(taskId)}/report`);
490
- if (!response.ok) {
491
- throw new Error(`Failed to read task report: ${await response.text()}`);
492
- }
493
- const data = await parseJsonResponse(response, 'Task report get');
485
+ const workflow = await import('./task-report-workflow.js');
486
+ const data = await workflow.getTaskReport(taskId);
494
487
  if (hasJsonFlag()) {
495
488
  printJson(data);
496
489
  return;
@@ -0,0 +1,47 @@
1
+ import { Schema } from 'effect';
2
+ declare const SetReply: Schema.Struct<{
3
+ readonly task: Schema.Struct<{
4
+ readonly id: Schema.String;
5
+ readonly status: Schema.String;
6
+ readonly report: Schema.Struct<{
7
+ readonly markdown: Schema.String;
8
+ readonly sourceName: Schema.NullOr<Schema.String>;
9
+ readonly createdAt: Schema.NullOr<Schema.String>;
10
+ }>;
11
+ }>;
12
+ }>;
13
+ declare const GetReply: Schema.Struct<{
14
+ readonly task: Schema.Struct<{
15
+ readonly id: Schema.String;
16
+ readonly title: Schema.optionalKey<Schema.NullOr<Schema.String>>;
17
+ readonly status: Schema.optionalKey<Schema.NullOr<Schema.String>>;
18
+ readonly report: Schema.Struct<{
19
+ readonly markdown: Schema.String;
20
+ readonly sourceName: Schema.NullOr<Schema.String>;
21
+ readonly createdAt: Schema.NullOr<Schema.String>;
22
+ readonly updatedAt: Schema.optionalKey<Schema.NullOr<Schema.String>>;
23
+ }>;
24
+ }>;
25
+ }>;
26
+ type TaskReportSetReply = typeof SetReply.Type;
27
+ export type TaskReportGetReply = typeof GetReply.Type;
28
+ export type TaskReportSetOutcome = {
29
+ kind: 'saved';
30
+ reply: TaskReportSetReply;
31
+ } | {
32
+ kind: 'interrupted';
33
+ exitCode: 130 | 143;
34
+ };
35
+ /**
36
+ * `orizu tasks report set`. Ctrl-C or SIGTERM aborts the request and, once it
37
+ * was sent, says the report may already be saved: the server finishes on its
38
+ * own. Other failures throw, adding the same line when the outcome is unknown.
39
+ */
40
+ export declare function setTaskReport(taskId: string, report: {
41
+ markdown: string;
42
+ sourceName: string | null;
43
+ }): Promise<TaskReportSetOutcome>;
44
+ /** `orizu tasks report get`: one read with a deadline. Signals keep Node's default handling. */
45
+ export declare function getTaskReport(taskId: string): Promise<TaskReportGetReply>;
46
+ export {};
47
+ //# sourceMappingURL=task-report-workflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-report-workflow.d.ts","sourceRoot":"","sources":["../src/task-report-workflow.ts"],"names":[],"mappings":"AAAA,OAAO,EAA+B,MAAM,EAAE,MAAM,QAAQ,CAAA;AAa5D,QAAA,MAAM,QAAQ;;;;;;;;;;EAUZ,CAAA;AAGF,QAAA,MAAM,QAAQ;;;;;;;;;;;;EAYZ,CAAA;AAEF,KAAK,kBAAkB,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAC9C,MAAM,MAAM,kBAAkB,GAAG,OAAO,QAAQ,CAAC,IAAI,CAAA;AAwErD,MAAM,MAAM,oBAAoB,GAC5B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,kBAAkB,CAAA;CAAE,GAC5C;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,EAAE,GAAG,GAAG,GAAG,CAAA;CAAE,CAAA;AAEhD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAkC1I;AAQD,gGAAgG;AAChG,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAa/E"}
@@ -0,0 +1,160 @@
1
+ import { Cause, Effect, Exit, Result, Schema } from 'effect';
2
+ import { datasetTransferTimeoutMs } from './dataset-transfer-policy.js';
3
+ import { isJsonErrorResponsePayload } from './error-response.js';
4
+ import { authedFetch } from './http.js';
5
+ import { parseJsonResponse } from './json-response.js';
6
+ // Loaded only through `await import(...)` from task-commands. One request per
7
+ // command, never retried, with the same deadline as the dataset workflows.
8
+ const mayBeSaved = (taskId) => `The task report may already be saved: check with \`orizu tasks report get --task ${taskId}\` before setting it again.`;
9
+ const SetReply = Schema.Struct({
10
+ task: Schema.Struct({
11
+ id: Schema.String,
12
+ status: Schema.String,
13
+ report: Schema.Struct({
14
+ markdown: Schema.String,
15
+ sourceName: Schema.NullOr(Schema.String),
16
+ createdAt: Schema.NullOr(Schema.String),
17
+ }),
18
+ }),
19
+ });
20
+ // title, status and updatedAt stay optional, as in the payload type this replaced.
21
+ const GetReply = Schema.Struct({
22
+ task: Schema.Struct({
23
+ id: Schema.String,
24
+ title: Schema.optionalKey(Schema.NullOr(Schema.String)),
25
+ status: Schema.optionalKey(Schema.NullOr(Schema.String)),
26
+ report: Schema.Struct({
27
+ markdown: Schema.String,
28
+ sourceName: Schema.NullOr(Schema.String),
29
+ createdAt: Schema.NullOr(Schema.String),
30
+ updatedAt: Schema.optionalKey(Schema.NullOr(Schema.String)),
31
+ }),
32
+ }),
33
+ });
34
+ // `mayBeSaved` (the #2164 rule): an error reply in the app's own `{ error }`
35
+ // shape is reported plainly, whatever its status. A lost, stalled or unreadable
36
+ // reply, a 5xx that did not come from the app (a gateway giving up while the
37
+ // app kept working), or a reply of the wrong shape means it may have been saved.
38
+ class ReportRequestFailure extends Error {
39
+ mayBeSaved;
40
+ constructor(message, mayBeSaved, cause) {
41
+ super(message, { cause });
42
+ this.mayBeSaved = mayBeSaved;
43
+ }
44
+ }
45
+ function isAppError(response, text) {
46
+ try {
47
+ return isJsonErrorResponsePayload(response, JSON.parse(text));
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ /** One request with a deadline. Returns the reply as parsed once `accepts` agrees with its shape. */
54
+ function reportRequest(input) {
55
+ return Effect.suspend(() => {
56
+ let sent = false;
57
+ let timedOut = false;
58
+ return Effect.tryPromise({
59
+ try: async (signal) => {
60
+ const request = new AbortController();
61
+ const abort = () => request.abort();
62
+ const timer = setTimeout(() => { timedOut = true; request.abort(); }, input.timeoutMs);
63
+ if (signal.aborted)
64
+ abort();
65
+ else
66
+ signal.addEventListener('abort', abort, { once: true });
67
+ try {
68
+ const response = await authedFetch(`/api/cli/tasks/${encodeURIComponent(input.taskId)}/report`, { ...input.init, signal: request.signal }, () => { sent = true; input.onSent?.(); });
69
+ if (!response.ok) {
70
+ const text = await response.text();
71
+ const fromApp = isAppError(response, text);
72
+ throw new ReportRequestFailure(`${input.failurePrefix}: ${text}`, response.status < 400 || (response.status >= 500 && !fromApp));
73
+ }
74
+ const data = await parseJsonResponse(response, input.label);
75
+ if (!input.accepts(data)) {
76
+ throw new ReportRequestFailure(`${input.label} returned an unexpected response (status ${response.status}).`, true);
77
+ }
78
+ return data;
79
+ }
80
+ finally {
81
+ clearTimeout(timer);
82
+ signal.removeEventListener('abort', abort);
83
+ request.abort();
84
+ }
85
+ },
86
+ catch: cause => {
87
+ if (timedOut)
88
+ return new ReportRequestFailure(input.deadlineMessage, sent, cause);
89
+ if (cause instanceof ReportRequestFailure)
90
+ return cause;
91
+ return new ReportRequestFailure(cause instanceof Error ? cause.message : String(cause), sent, cause);
92
+ },
93
+ });
94
+ });
95
+ }
96
+ const accepts = (schema) => (data) => Result.isSuccess(Schema.decodeUnknownResult(schema)(data));
97
+ /**
98
+ * `orizu tasks report set`. Ctrl-C or SIGTERM aborts the request and, once it
99
+ * was sent, says the report may already be saved: the server finishes on its
100
+ * own. Other failures throw, adding the same line when the outcome is unknown.
101
+ */
102
+ export async function setTaskReport(taskId, report) {
103
+ const timeoutMs = datasetTransferTimeoutMs();
104
+ const controller = new AbortController();
105
+ let interrupted;
106
+ let sent = false;
107
+ const interrupt = () => { interrupted ??= 130; controller.abort(); };
108
+ const terminate = () => { interrupted ??= 143; controller.abort(); };
109
+ process.on('SIGINT', interrupt);
110
+ process.on('SIGTERM', terminate);
111
+ try {
112
+ const exit = await Effect.runPromiseExit(reportRequest({
113
+ taskId,
114
+ init: {
115
+ method: 'PATCH',
116
+ headers: { 'Content-Type': 'application/json' },
117
+ body: JSON.stringify({ reportMarkdown: report.markdown, reportSourceName: report.sourceName }),
118
+ },
119
+ failurePrefix: 'Failed to update task report',
120
+ label: 'Task report update',
121
+ accepts: accepts(SetReply),
122
+ deadlineMessage: 'Task report save exceeded its deadline',
123
+ timeoutMs,
124
+ onSent: () => { sent = true; },
125
+ }), { signal: controller.signal });
126
+ if (interrupted !== undefined) {
127
+ process.stderr.write(sent ? `Task report save interrupted. ${mayBeSaved(taskId)}\n` : 'Task report save interrupted.\n');
128
+ return { kind: 'interrupted', exitCode: interrupted };
129
+ }
130
+ if (Exit.isSuccess(exit))
131
+ return { kind: 'saved', reply: exit.value };
132
+ throw withMayBeSaved(Cause.squash(exit.cause), taskId);
133
+ }
134
+ finally {
135
+ process.removeListener('SIGINT', interrupt);
136
+ process.removeListener('SIGTERM', terminate);
137
+ }
138
+ }
139
+ function withMayBeSaved(failure, taskId) {
140
+ if (!(failure instanceof ReportRequestFailure))
141
+ return failure;
142
+ const message = failure.mayBeSaved ? `${failure.message}\n${mayBeSaved(taskId)}` : failure.message;
143
+ return new Error(message, { cause: failure.cause });
144
+ }
145
+ /** `orizu tasks report get`: one read with a deadline. Signals keep Node's default handling. */
146
+ export async function getTaskReport(taskId) {
147
+ const exit = await Effect.runPromiseExit(reportRequest({
148
+ taskId,
149
+ init: {},
150
+ failurePrefix: 'Failed to read task report',
151
+ label: 'Task report get',
152
+ accepts: accepts(GetReply),
153
+ deadlineMessage: 'Task report read exceeded its deadline',
154
+ timeoutMs: datasetTransferTimeoutMs(),
155
+ }));
156
+ if (Exit.isSuccess(exit))
157
+ return exit.value;
158
+ const failure = Cause.squash(exit.cause);
159
+ throw failure instanceof ReportRequestFailure ? new Error(failure.message, { cause: failure.cause }) : failure;
160
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orizu",
3
- "version": "0.5.40",
3
+ "version": "0.5.41",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,100 @@
1
+ import { Cause, Effect, Exit, Result, Schema } from 'effect'
2
+
3
+ import { datasetTransferTimeoutMs } from './dataset-transfer-policy.js'
4
+ import { authedFetch } from './http.js'
5
+ import { parseJsonResponse } from './json-response.js'
6
+
7
+ // Loaded only through `await import(...)` from dataset-commands, like lock.
8
+
9
+ const SERVER_MAY_BE_CREATED = 'Dataset clone may already be created'
10
+
11
+ const ClonedDataset = Schema.Struct({
12
+ id: Schema.String,
13
+ name: Schema.String,
14
+ rowCount: Schema.Number,
15
+ parentDatasetId: Schema.String,
16
+ })
17
+ const CloneReply = Schema.Struct({ dataset: ClonedDataset })
18
+ const ErrorReply = Schema.Struct({ error: Schema.String })
19
+ export type ClonedDataset = typeof ClonedDataset.Type
20
+
21
+ // A refusal means nothing was cloned. After any other failure once the request
22
+ // was sent, the clone may exist. Unlike lock, cloning again is not harmless: it
23
+ // makes another copy, so the clone is never retried and the user is told to look.
24
+ class CloneFailure extends Error {
25
+ constructor(message: string, readonly mayBeCreated: boolean) { super(message) }
26
+ }
27
+
28
+ function mayBeCreatedGuidance(name: string | null): string {
29
+ const target = name ? `a dataset named ${JSON.stringify(name)}` : 'a new dataset named after the source with " Copy" at the end'
30
+ return `The clone may already have been created. Run \`orizu datasets list\` and look for ${target} before cloning again: running clone again makes another copy.`
31
+ }
32
+
33
+ // authedFetch raises this code only when it sent nothing, or when the server
34
+ // answered 401 and the changed credentials stopped a replay. Nothing was cloned.
35
+ function isAuthContextChange(cause: unknown): boolean {
36
+ return cause instanceof Error && cause.message.startsWith('ORIZU_AUTH_CONTEXT_CHANGED:')
37
+ }
38
+
39
+ function serverError(text: string): string | null {
40
+ try {
41
+ const decoded = Schema.decodeUnknownResult(ErrorReply)(JSON.parse(text))
42
+ return Result.isSuccess(decoded) ? decoded.success.error : null
43
+ } catch {
44
+ return null
45
+ }
46
+ }
47
+
48
+ function cloneRequest(datasetId: string, name: string | null, timeoutMs: number) {
49
+ return Effect.suspend(() => {
50
+ let sent = false
51
+ let timedOut = false
52
+ return Effect.tryPromise({
53
+ try: async signal => {
54
+ const request = new AbortController()
55
+ const abort = () => request.abort()
56
+ const timer = setTimeout(() => { timedOut = true; request.abort() }, timeoutMs)
57
+ if (signal.aborted) abort()
58
+ else signal.addEventListener('abort', abort, { once: true })
59
+ try {
60
+ const response = await authedFetch(`/api/cli/datasets/${encodeURIComponent(datasetId)}/clone`, {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify(name ? { name } : {}),
64
+ signal: request.signal,
65
+ }, () => { sent = true })
66
+ if (!response.ok) {
67
+ const text = await response.text()
68
+ const error = serverError(text)
69
+ // The app's own 5xx bodies name what happened; anything else at 5xx
70
+ // (a gateway page, an empty body) says nothing about the clone.
71
+ const mayBeCreated = response.status < 400 || error === SERVER_MAY_BE_CREATED || (response.status >= 500 && error === null)
72
+ throw new CloneFailure(`Clone failed: ${text}`, mayBeCreated)
73
+ }
74
+ const data = await parseJsonResponse<unknown>(response, 'Dataset clone')
75
+ const decoded = Schema.decodeUnknownResult(CloneReply)(data)
76
+ if (Result.isFailure(decoded)) {
77
+ throw new CloneFailure(`Dataset clone returned an unexpected response (status ${response.status}).`, true)
78
+ }
79
+ return decoded.success.dataset
80
+ } finally {
81
+ clearTimeout(timer)
82
+ signal.removeEventListener('abort', abort)
83
+ request.abort()
84
+ }
85
+ },
86
+ catch: cause => {
87
+ if (timedOut) return new CloneFailure('DATASET_TRANSFER_TIMEOUT: Dataset clone exceeded its deadline', true)
88
+ if (cause instanceof CloneFailure) return cause
89
+ return new CloneFailure(cause instanceof Error ? cause.message : String(cause), sent && !isAuthContextChange(cause))
90
+ },
91
+ })
92
+ }).pipe(Effect.mapError(failure => new Error(failure.mayBeCreated ? `${failure.message}\n${mayBeCreatedGuidance(name)}` : failure.message)))
93
+ }
94
+
95
+ // Nothing to clean up, so signals keep Node's default handling.
96
+ export async function cloneDataset(datasetId: string, name: string | null): Promise<ClonedDataset> {
97
+ const exit = await Effect.runPromiseExit(cloneRequest(datasetId, name, datasetTransferTimeoutMs()))
98
+ if (Exit.isSuccess(exit)) return exit.value
99
+ throw Cause.squash(exit.cause)
100
+ }