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.
- package/dist/dataset-clone-workflow.d.ts +11 -0
- package/dist/dataset-clone-workflow.d.ts.map +1 -0
- package/dist/dataset-clone-workflow.js +100 -0
- package/dist/dataset-commands.d.ts.map +1 -1
- package/dist/dataset-commands.js +6 -29
- package/dist/dataset-transfer-upload.d.ts +2 -6
- package/dist/dataset-transfer-upload.d.ts.map +1 -1
- package/dist/dataset-transfer-upload.js +9 -2
- package/dist/dataset-upload-snapshot.d.ts.map +1 -1
- package/dist/dataset-upload-snapshot.js +7 -1
- package/dist/dataset-version-workflow.d.ts +24 -0
- package/dist/dataset-version-workflow.d.ts.map +1 -0
- package/dist/dataset-version-workflow.js +99 -0
- package/dist/feedback-cli.js +3 -3
- package/dist/hosted-harness-opencode.d.ts +1 -1
- package/dist/hosted-harness-opencode.js +7 -7
- package/dist/hosted-loop-lifecycle.d.ts +3 -2
- package/dist/hosted-loop-lifecycle.d.ts.map +1 -1
- package/dist/hosted-loop-lifecycle.js +3 -2
- package/dist/hosted-loop.d.ts +7 -0
- package/dist/hosted-loop.d.ts.map +1 -1
- package/dist/hosted-loop.js +14 -6
- package/dist/index.js +2 -2
- package/dist/last-error-record.d.ts +4 -2
- package/dist/last-error-record.d.ts.map +1 -1
- package/dist/last-error-record.js +10 -68
- package/dist/last-error-workflow.d.ts +4 -0
- package/dist/last-error-workflow.d.ts.map +1 -0
- package/dist/last-error-workflow.js +78 -0
- package/dist/task-commands.d.ts.map +1 -1
- package/dist/task-commands.js +9 -16
- package/dist/task-report-workflow.d.ts +47 -0
- package/dist/task-report-workflow.d.ts.map +1 -0
- package/dist/task-report-workflow.js +160 -0
- package/package.json +1 -1
- package/src/dataset-clone-workflow.ts +100 -0
- package/src/dataset-commands.ts +6 -53
- package/src/dataset-transfer-upload.ts +12 -5
- package/src/dataset-upload-snapshot.ts +8 -1
- package/src/dataset-version-workflow.ts +105 -0
- package/src/feedback-cli.ts +4 -4
- package/src/hosted-harness-opencode.ts +7 -7
- package/src/hosted-loop-lifecycle.ts +3 -2
- package/src/hosted-loop.ts +14 -6
- package/src/index.ts +2 -2
- package/src/last-error-record.ts +11 -85
- package/src/last-error-workflow.ts +121 -0
- package/src/task-commands.ts +9 -43
- 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":"
|
|
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
|
|
276
|
-
|
|
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
|
-
|
|
302
|
-
|
|
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":"
|
|
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"}
|
package/dist/task-commands.js
CHANGED
|
@@ -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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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 =
|
|
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
|
|
490
|
-
|
|
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
|
@@ -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
|
+
}
|