@lingjingai/scriptctl 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +12 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +433 -0
- package/dist/cli.js.map +1 -0
- package/dist/common.d.ts +124 -0
- package/dist/common.js +337 -0
- package/dist/common.js.map +1 -0
- package/dist/domain/direct-core.d.ts +114 -0
- package/dist/domain/direct-core.js +3040 -0
- package/dist/domain/direct-core.js.map +1 -0
- package/dist/domain/script-core.d.ts +18 -0
- package/dist/domain/script-core.js +1886 -0
- package/dist/domain/script-core.js.map +1 -0
- package/dist/help-text.d.ts +2 -0
- package/dist/help-text.js +399 -0
- package/dist/help-text.js.map +1 -0
- package/dist/infra/converters.d.ts +10 -0
- package/dist/infra/converters.js +560 -0
- package/dist/infra/converters.js.map +1 -0
- package/dist/infra/env.d.ts +2 -0
- package/dist/infra/env.js +81 -0
- package/dist/infra/env.js.map +1 -0
- package/dist/infra/providers.d.ts +25 -0
- package/dist/infra/providers.js +330 -0
- package/dist/infra/providers.js.map +1 -0
- package/dist/infra/script-output-api.d.ts +44 -0
- package/dist/infra/script-output-api.js +160 -0
- package/dist/infra/script-output-api.js.map +1 -0
- package/dist/infra/storage.d.ts +35 -0
- package/dist/infra/storage.js +91 -0
- package/dist/infra/storage.js.map +1 -0
- package/dist/output.d.ts +4 -0
- package/dist/output.js +55 -0
- package/dist/output.js.map +1 -0
- package/dist/usecases/direct.d.ts +13 -0
- package/dist/usecases/direct.js +1679 -0
- package/dist/usecases/direct.js.map +1 -0
- package/dist/usecases/doctor.d.ts +2 -0
- package/dist/usecases/doctor.js +75 -0
- package/dist/usecases/doctor.js.map +1 -0
- package/dist/usecases/script.d.ts +32 -0
- package/dist/usecases/script.js +949 -0
- package/dist/usecases/script.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { CliError, EXIT_INPUT, EXIT_NEEDS_AGENT, EXIT_OK, EXIT_RUNTIME, EXIT_USAGE, directDir, exists, readJson, readText, scriptJsonPath, sha256Text, writeJson, } from "../common.js";
|
|
6
|
+
import { applyPatchOperations, collectStateRefs, parseStateTarget, validateScript, } from "../domain/script-core.js";
|
|
7
|
+
import { ScriptOutputApiError, ScriptOutputClient } from "../infra/script-output-api.js";
|
|
8
|
+
import { markMetadataConfidenceReviewed, readRunState, reviewBlockers, summarizeIssues, updateRunState, markPatched, } from "./direct.js";
|
|
9
|
+
function strOf(v) {
|
|
10
|
+
if (v === null || v === undefined)
|
|
11
|
+
return "";
|
|
12
|
+
return String(v);
|
|
13
|
+
}
|
|
14
|
+
function isDict(v) {
|
|
15
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
16
|
+
}
|
|
17
|
+
function isList(v) {
|
|
18
|
+
return Array.isArray(v);
|
|
19
|
+
}
|
|
20
|
+
function asList(v) {
|
|
21
|
+
return Array.isArray(v) ? v : [];
|
|
22
|
+
}
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// ScriptEditSession
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
export class ScriptEditSession {
|
|
27
|
+
workspace;
|
|
28
|
+
script;
|
|
29
|
+
scriptPath;
|
|
30
|
+
client;
|
|
31
|
+
projectGroupNo;
|
|
32
|
+
revision;
|
|
33
|
+
constructor(opts) {
|
|
34
|
+
this.workspace = opts.workspace;
|
|
35
|
+
this.script = opts.script;
|
|
36
|
+
this.scriptPath = opts.scriptPath ?? null;
|
|
37
|
+
this.client = opts.client ?? null;
|
|
38
|
+
this.projectGroupNo = opts.projectGroupNo ?? null;
|
|
39
|
+
this.revision = opts.revision ?? null;
|
|
40
|
+
}
|
|
41
|
+
get remote() {
|
|
42
|
+
return this.client !== null;
|
|
43
|
+
}
|
|
44
|
+
get artifactLabel() {
|
|
45
|
+
if (this.remote) {
|
|
46
|
+
return `db:/script-output/project-groups/${this.projectGroupNo}@revision/${this.revision ?? 0}`;
|
|
47
|
+
}
|
|
48
|
+
return this.scriptPath ?? "";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function scriptOutputClient(opts) {
|
|
52
|
+
try {
|
|
53
|
+
return ScriptOutputClient.fromEnv(strOf(opts["project_group_no"]).trim() || null);
|
|
54
|
+
}
|
|
55
|
+
catch (exc) {
|
|
56
|
+
const msg = exc instanceof Error ? exc.message : String(exc);
|
|
57
|
+
throw new CliError("SCRIPT API BLOCKED: Gateway configuration missing", msg, {
|
|
58
|
+
exitCode: EXIT_INPUT,
|
|
59
|
+
required: ["SCRIPTCTL_GATEWAY_URL/AWB_BASE_URL, SANDBOX_PROJECT_GROUP_NO/--project-group-no, and X-Access-Key credentials"],
|
|
60
|
+
received: [msg],
|
|
61
|
+
nextSteps: ["Run inside a project sandbox or pass the missing configuration explicitly."],
|
|
62
|
+
errorCode: "SCRIPT_API_CONFIG_MISSING",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function apiErrorToCli(title, exc) {
|
|
67
|
+
const message = exc.message || "script-output API failed";
|
|
68
|
+
const isConflict = message.includes("版本冲突") || message.toLowerCase().includes("revision");
|
|
69
|
+
return new CliError(title, message, {
|
|
70
|
+
exitCode: exc.status !== null && exc.status >= 500 ? EXIT_RUNTIME : EXIT_INPUT,
|
|
71
|
+
required: ["successful script-output gateway request"],
|
|
72
|
+
received: [message],
|
|
73
|
+
nextSteps: [isConflict ? "Reload the latest script revision and retry." : "Check gateway URL, access key, project id, and workbench service status."],
|
|
74
|
+
errorCode: isConflict ? "SCRIPT_REVISION_CONFLICT" : "SCRIPT_API_FAILED",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async function currentRevisionOrZero(client) {
|
|
78
|
+
try {
|
|
79
|
+
const revision = await client.getRevision();
|
|
80
|
+
if (!revision)
|
|
81
|
+
return 0;
|
|
82
|
+
return Number(revision["revision"] ?? 0);
|
|
83
|
+
}
|
|
84
|
+
catch (exc) {
|
|
85
|
+
if (exc instanceof ScriptOutputApiError) {
|
|
86
|
+
throw apiErrorToCli("SCRIPT API BLOCKED: Revision query failed", exc);
|
|
87
|
+
}
|
|
88
|
+
throw exc;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function loadRemoteScript(opts, workspace) {
|
|
92
|
+
const client = scriptOutputClient(opts);
|
|
93
|
+
let script;
|
|
94
|
+
let revision;
|
|
95
|
+
try {
|
|
96
|
+
script = await client.getScript();
|
|
97
|
+
revision = await client.getRevision();
|
|
98
|
+
}
|
|
99
|
+
catch (exc) {
|
|
100
|
+
if (exc instanceof ScriptOutputApiError) {
|
|
101
|
+
throw apiErrorToCli("SCRIPT API BLOCKED: Script query failed", exc);
|
|
102
|
+
}
|
|
103
|
+
throw exc;
|
|
104
|
+
}
|
|
105
|
+
if (script === null) {
|
|
106
|
+
throw new CliError("SCRIPT BLOCKED: Final script not found", "Final script data not found.", {
|
|
107
|
+
exitCode: EXIT_INPUT,
|
|
108
|
+
required: ["existing script-output project document"],
|
|
109
|
+
received: [`projectGroupNo=${client.projectGroupNo}`],
|
|
110
|
+
nextSteps: ["Run scriptctl direct export first, or pass --script-path to edit a local intermediate script JSON."],
|
|
111
|
+
errorCode: "SCRIPT_NOT_FOUND",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (!isDict(script)) {
|
|
115
|
+
throw new CliError("SCRIPT BLOCKED: script root invalid", "script root invalid.", {
|
|
116
|
+
exitCode: EXIT_USAGE,
|
|
117
|
+
required: ["script root object"],
|
|
118
|
+
received: [typeof script],
|
|
119
|
+
nextSteps: ["Replace the final script with a valid script document object."],
|
|
120
|
+
errorCode: "SCRIPT_ROOT_INVALID",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return new ScriptEditSession({
|
|
124
|
+
workspace,
|
|
125
|
+
script,
|
|
126
|
+
client,
|
|
127
|
+
projectGroupNo: client.projectGroupNo,
|
|
128
|
+
revision: Number((revision ?? {})["revision"] ?? 0),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async function loadScriptForEdit(opts) {
|
|
132
|
+
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
133
|
+
if (!opts["script_path"]) {
|
|
134
|
+
return loadRemoteScript(opts, workspace);
|
|
135
|
+
}
|
|
136
|
+
const p = scriptJsonPath(opts);
|
|
137
|
+
if (!exists(p)) {
|
|
138
|
+
throw new CliError("SCRIPT BLOCKED: Local script file not found", "Local script file not found.", {
|
|
139
|
+
exitCode: EXIT_INPUT,
|
|
140
|
+
required: ["--script-path existing local script JSON"],
|
|
141
|
+
received: [p],
|
|
142
|
+
nextSteps: ["Pass --script-path to an existing local intermediate script JSON, or omit --script-path to use DB-backed script-output."],
|
|
143
|
+
errorCode: "SCRIPT_NOT_FOUND",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
let data;
|
|
147
|
+
try {
|
|
148
|
+
data = readJson(p);
|
|
149
|
+
}
|
|
150
|
+
catch (exc) {
|
|
151
|
+
throw new CliError("SCRIPT BLOCKED: script JSON invalid", "script JSON invalid.", {
|
|
152
|
+
exitCode: EXIT_INPUT,
|
|
153
|
+
required: ["valid JSON"],
|
|
154
|
+
received: [`${p}: ${exc.message}`],
|
|
155
|
+
nextSteps: ["Fix the local script JSON syntax before editing."],
|
|
156
|
+
errorCode: "SCRIPT_JSON_INVALID",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (!isDict(data)) {
|
|
160
|
+
throw new CliError("SCRIPT BLOCKED: script root invalid", "script root invalid.", {
|
|
161
|
+
exitCode: EXIT_USAGE,
|
|
162
|
+
required: ["script root object"],
|
|
163
|
+
received: [Array.isArray(data) ? "array" : typeof data],
|
|
164
|
+
nextSteps: ["Use a valid script document object."],
|
|
165
|
+
errorCode: "SCRIPT_ROOT_INVALID",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return new ScriptEditSession({ workspace, scriptPath: p, script: data });
|
|
169
|
+
}
|
|
170
|
+
function validateSession(session, opts = {}) {
|
|
171
|
+
const requireSource = opts.requireSource ?? false;
|
|
172
|
+
if (!session.remote) {
|
|
173
|
+
return validateScript(session.workspace, session.scriptPath, { requireSource });
|
|
174
|
+
}
|
|
175
|
+
const tmpPath = path.join(os.tmpdir(), `scriptctl-db-script-${randomUUID()}.json`);
|
|
176
|
+
try {
|
|
177
|
+
fs.writeFileSync(tmpPath, JSON.stringify(session.script, null, 2) + "\n", "utf-8");
|
|
178
|
+
const validation = validateScript(session.workspace, tmpPath, { requireSource });
|
|
179
|
+
validation["script_path"] = session.artifactLabel;
|
|
180
|
+
writeJson(path.join(directDir(session.workspace), "validation.json"), validation);
|
|
181
|
+
return validation;
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
try {
|
|
185
|
+
fs.unlinkSync(tmpPath);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// ignore
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function validationIssuePath(issue) {
|
|
193
|
+
if (issue["path"])
|
|
194
|
+
return strOf(issue["path"]);
|
|
195
|
+
const parts = [];
|
|
196
|
+
if (issue["episode"] !== null && issue["episode"] !== undefined)
|
|
197
|
+
parts.push(strOf(issue["episode"]));
|
|
198
|
+
if (issue["scene"] !== null && issue["scene"] !== undefined)
|
|
199
|
+
parts.push(strOf(issue["scene"]));
|
|
200
|
+
if (issue["action_index"] !== null && issue["action_index"] !== undefined)
|
|
201
|
+
parts.push(`action[${issue["action_index"]}]`);
|
|
202
|
+
return parts.length > 0 ? parts.join("/") : null;
|
|
203
|
+
}
|
|
204
|
+
function validationIssueSeverity(raw) {
|
|
205
|
+
const severity = strOf(raw || "info");
|
|
206
|
+
if (severity === "blocking" || severity === "error")
|
|
207
|
+
return "error";
|
|
208
|
+
if (severity === "warning")
|
|
209
|
+
return "warning";
|
|
210
|
+
return "info";
|
|
211
|
+
}
|
|
212
|
+
function validationToApiPayload(validation) {
|
|
213
|
+
const issues = asList(validation["issues"]);
|
|
214
|
+
const status = validation["passed"] ? "valid" : "invalid";
|
|
215
|
+
const summary = {
|
|
216
|
+
passed: Boolean(validation["passed"]),
|
|
217
|
+
hasBlocking: Boolean(validation["has_blocking"]),
|
|
218
|
+
stats: validation["stats"] ?? {},
|
|
219
|
+
issueCount: issues.length,
|
|
220
|
+
};
|
|
221
|
+
const apiIssues = [];
|
|
222
|
+
for (const issue of issues) {
|
|
223
|
+
if (!isDict(issue))
|
|
224
|
+
continue;
|
|
225
|
+
const payload = { ...issue };
|
|
226
|
+
for (const key of ["severity", "code", "path", "summary", "repair_hint"])
|
|
227
|
+
delete payload[key];
|
|
228
|
+
apiIssues.push({
|
|
229
|
+
severity: validationIssueSeverity(issue["severity"]),
|
|
230
|
+
code: strOf(issue["code"] || "SCRIPT_VALIDATION_ISSUE"),
|
|
231
|
+
path: validationIssuePath(issue),
|
|
232
|
+
summary: strOf(issue["summary"]),
|
|
233
|
+
repairHint: issue["repair_hint"],
|
|
234
|
+
payload,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return [status, summary, apiIssues];
|
|
238
|
+
}
|
|
239
|
+
async function syncValidationResult(session, validation, revision) {
|
|
240
|
+
if (!session.remote || session.client === null)
|
|
241
|
+
return;
|
|
242
|
+
const targetRevision = Number(revision ?? session.revision ?? 0);
|
|
243
|
+
if (targetRevision <= 0)
|
|
244
|
+
return;
|
|
245
|
+
const [status, summary, issues] = validationToApiPayload(validation);
|
|
246
|
+
try {
|
|
247
|
+
await session.client.saveValidationResult({
|
|
248
|
+
revision: targetRevision,
|
|
249
|
+
validationStatus: status,
|
|
250
|
+
validationSummary: summary,
|
|
251
|
+
issues,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch (exc) {
|
|
255
|
+
if (exc instanceof ScriptOutputApiError) {
|
|
256
|
+
throw apiErrorToCli("SCRIPT API BLOCKED: Validation sync failed", exc);
|
|
257
|
+
}
|
|
258
|
+
throw exc;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function requestIdForScriptWrite(opts, op) {
|
|
262
|
+
const explicit = strOf(opts["request_id"]).trim();
|
|
263
|
+
if (explicit)
|
|
264
|
+
return explicit;
|
|
265
|
+
return `scriptctl:${op}:${randomUUID()}`;
|
|
266
|
+
}
|
|
267
|
+
async function saveScriptSession(session, opts, op) {
|
|
268
|
+
if (!session.remote) {
|
|
269
|
+
writeJson(session.scriptPath, session.script);
|
|
270
|
+
return [null, false];
|
|
271
|
+
}
|
|
272
|
+
if (session.client === null)
|
|
273
|
+
throw new Error("remote script session missing client");
|
|
274
|
+
const requestId = requestIdForScriptWrite(opts, op);
|
|
275
|
+
let res;
|
|
276
|
+
try {
|
|
277
|
+
res = await session.client.replaceScript({
|
|
278
|
+
requestId,
|
|
279
|
+
baseRevision: Number(session.revision ?? 0),
|
|
280
|
+
script: session.script,
|
|
281
|
+
source: "ctl",
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
catch (exc) {
|
|
285
|
+
if (exc instanceof ScriptOutputApiError) {
|
|
286
|
+
throw apiErrorToCli("SCRIPT API BLOCKED: Script write failed", exc);
|
|
287
|
+
}
|
|
288
|
+
throw exc;
|
|
289
|
+
}
|
|
290
|
+
session.revision = Number(res["revision"] ?? session.revision ?? 0);
|
|
291
|
+
return [session.revision, Boolean(res["idempotent"])];
|
|
292
|
+
}
|
|
293
|
+
function scriptSourceText(workspace) {
|
|
294
|
+
const sourcePath = path.join(workspace, "source.txt");
|
|
295
|
+
return exists(sourcePath) ? readText(sourcePath) : "";
|
|
296
|
+
}
|
|
297
|
+
function patchOperationsFromPayload(payload) {
|
|
298
|
+
let operations;
|
|
299
|
+
if (isList(payload)) {
|
|
300
|
+
operations = payload;
|
|
301
|
+
}
|
|
302
|
+
else if (isDict(payload) && isList(payload["ops"])) {
|
|
303
|
+
operations = payload["ops"];
|
|
304
|
+
}
|
|
305
|
+
else if (isDict(payload) && isList(payload["operations"])) {
|
|
306
|
+
operations = payload["operations"];
|
|
307
|
+
}
|
|
308
|
+
else if (isDict(payload)) {
|
|
309
|
+
operations = [payload];
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
throw new CliError("PATCH BLOCKED: Patch schema invalid", "Patch schema invalid.", {
|
|
313
|
+
exitCode: EXIT_USAGE,
|
|
314
|
+
required: ["object, array, object with ops[], or object with operations[]"],
|
|
315
|
+
received: [Array.isArray(payload) ? "array" : typeof payload],
|
|
316
|
+
nextSteps: ["Fix patch JSON and rerun patch."],
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (operations.some((op) => !isDict(op))) {
|
|
320
|
+
throw new CliError("PATCH BLOCKED: Patch operation invalid", "Patch operation invalid.", {
|
|
321
|
+
exitCode: EXIT_USAGE,
|
|
322
|
+
required: ["each operation must be an object"],
|
|
323
|
+
received: [JSON.stringify(operations).slice(0, 500)],
|
|
324
|
+
nextSteps: ["Fix patch JSON and rerun patch."],
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
return operations;
|
|
328
|
+
}
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
// command_patch (legacy: applies to script.initial.json directly with source)
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
export function commandPatch(opts) {
|
|
333
|
+
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
334
|
+
const patchPath = strOf(opts["patch"]);
|
|
335
|
+
const scriptPath = path.join(directDir(workspace), "script.initial.json");
|
|
336
|
+
const sourcePath = path.join(workspace, "source.txt");
|
|
337
|
+
if (!exists(patchPath)) {
|
|
338
|
+
throw new CliError("PATCH BLOCKED: Patch file not found", "Patch file not found.", {
|
|
339
|
+
exitCode: EXIT_INPUT,
|
|
340
|
+
required: ["--patch: existing JSON file"],
|
|
341
|
+
received: [patchPath],
|
|
342
|
+
nextSteps: ["Write a patch JSON file and rerun patch."],
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
if (!exists(scriptPath) || !exists(sourcePath)) {
|
|
346
|
+
throw new CliError("PATCH BLOCKED: Required artifact missing", "Required artifact missing.", {
|
|
347
|
+
exitCode: EXIT_INPUT,
|
|
348
|
+
required: ["source.txt and script.initial.json"],
|
|
349
|
+
received: [scriptPath, sourcePath],
|
|
350
|
+
nextSteps: ["Run scriptctl direct init first."],
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
let payload;
|
|
354
|
+
try {
|
|
355
|
+
payload = readJson(patchPath);
|
|
356
|
+
}
|
|
357
|
+
catch (exc) {
|
|
358
|
+
throw new CliError("PATCH BLOCKED: Patch JSON invalid", "Patch JSON invalid.", {
|
|
359
|
+
exitCode: EXIT_USAGE,
|
|
360
|
+
required: ["valid JSON patch file"],
|
|
361
|
+
received: [`${patchPath}: ${exc.message}`],
|
|
362
|
+
nextSteps: ["Fix patch JSON and rerun patch."],
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
const operations = patchOperationsFromPayload(payload);
|
|
366
|
+
const script = readJson(scriptPath);
|
|
367
|
+
const applied = applyPatchOperations(script, readText(sourcePath), operations);
|
|
368
|
+
writeJson(scriptPath, script);
|
|
369
|
+
markMetadataConfidenceReviewed(workspace, operations);
|
|
370
|
+
const validation = validateScript(workspace, scriptPath);
|
|
371
|
+
markPatched(workspace, applied.length);
|
|
372
|
+
const passed = Boolean(validation["passed"]);
|
|
373
|
+
const report = {
|
|
374
|
+
title: passed ? "PATCH APPLIED: Validation passed" : "PATCH APPLIED: Repair issues remain",
|
|
375
|
+
result: [
|
|
376
|
+
`operations: ${applied.length}`,
|
|
377
|
+
`validation: ${passed ? "passed" : "needs repair"}`,
|
|
378
|
+
],
|
|
379
|
+
artifacts: [scriptPath, path.join(directDir(workspace), "validation.json")],
|
|
380
|
+
issues: summarizeIssues(asList(validation["issues"])),
|
|
381
|
+
next: [passed ? "Export the final script." : "Inspect remaining issues and apply another patch."],
|
|
382
|
+
};
|
|
383
|
+
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
384
|
+
}
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// commandScriptValidate / commandScriptInspect
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
export async function commandScriptValidate(opts) {
|
|
389
|
+
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
390
|
+
const session = await loadScriptForEdit(opts);
|
|
391
|
+
const validation = validateSession(session);
|
|
392
|
+
await syncValidationResult(session, validation);
|
|
393
|
+
const stats = validation["stats"] ?? {};
|
|
394
|
+
const passed = Boolean(validation["passed"]);
|
|
395
|
+
const report = {
|
|
396
|
+
title: passed ? "SCRIPT VALIDATE PASSED" : "SCRIPT VALIDATE NEEDS REPAIR",
|
|
397
|
+
op: "script.validate",
|
|
398
|
+
changed: false,
|
|
399
|
+
summary: `episodes=${stats["episodes"] ?? 0}, scenes=${stats["scenes"] ?? 0}, actions=${stats["actions"] ?? 0}`,
|
|
400
|
+
result: [
|
|
401
|
+
`episodes: ${stats["episodes"] ?? 0}`,
|
|
402
|
+
`scenes: ${stats["scenes"] ?? 0}`,
|
|
403
|
+
`actions: ${stats["actions"] ?? 0}`,
|
|
404
|
+
`speakers: ${stats["speakers"] ?? 0}`,
|
|
405
|
+
],
|
|
406
|
+
issues: summarizeIssues(asList(validation["issues"])),
|
|
407
|
+
artifacts: [session.artifactLabel, path.join(directDir(workspace), "validation.json")],
|
|
408
|
+
next: [passed ? "Continue editing the final script." : "Inspect issues and apply a script patch."],
|
|
409
|
+
};
|
|
410
|
+
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
411
|
+
}
|
|
412
|
+
export async function commandScriptInspect(opts) {
|
|
413
|
+
const session = await loadScriptForEdit(opts);
|
|
414
|
+
const script = session.script;
|
|
415
|
+
const target = strOf(opts["target"] || "summary").trim();
|
|
416
|
+
const itemId = strOf(opts["id"]).trim();
|
|
417
|
+
const lines = [];
|
|
418
|
+
if (target === "summary") {
|
|
419
|
+
const episodes = asList(script["episodes"]);
|
|
420
|
+
const scenes = [];
|
|
421
|
+
for (const ep of episodes)
|
|
422
|
+
scenes.push(...asList(ep["scenes"]));
|
|
423
|
+
const actions = [];
|
|
424
|
+
for (const scene of scenes)
|
|
425
|
+
actions.push(...asList(scene["actions"]));
|
|
426
|
+
lines.push(`title: ${script["title"] || "-"}`, `episodes: ${episodes.length}`, `scenes: ${scenes.length}`, `actions: ${actions.length}`, `actors: ${asList(script["actors"]).length}`, `locations: ${asList(script["locations"]).length}`, `props: ${asList(script["props"]).length}`, `speakers: ${asList(script["speakers"]).length}`);
|
|
427
|
+
}
|
|
428
|
+
else if (target === "episode") {
|
|
429
|
+
for (const ep of asList(script["episodes"])) {
|
|
430
|
+
if (itemId && itemId !== strOf(ep["episode_id"]))
|
|
431
|
+
continue;
|
|
432
|
+
const scenes = asList(ep["scenes"]);
|
|
433
|
+
let actionCount = 0;
|
|
434
|
+
for (const scene of scenes)
|
|
435
|
+
actionCount += asList(scene["actions"]).length;
|
|
436
|
+
lines.push(`${ep["episode_id"]}: scenes=${scenes.length}, actions=${actionCount}, title=${ep["title"] || "-"}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
else if (target === "asset") {
|
|
440
|
+
for (const [key, idKey, nameKey] of [
|
|
441
|
+
["actors", "actor_id", "actor_name"],
|
|
442
|
+
["locations", "location_id", "location_name"],
|
|
443
|
+
["props", "prop_id", "prop_name"],
|
|
444
|
+
]) {
|
|
445
|
+
for (const asset of asList(script[key])) {
|
|
446
|
+
if (itemId && itemId !== strOf(asset[idKey]) && itemId !== strOf(asset[nameKey]))
|
|
447
|
+
continue;
|
|
448
|
+
const singular = key.slice(0, -1);
|
|
449
|
+
lines.push(`${singular} ${asset[idKey]}: ${asset[nameKey]} states=${asList(asset["states"]).length}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
else if (target === "speaker") {
|
|
454
|
+
for (const speaker of asList(script["speakers"])) {
|
|
455
|
+
if (itemId && itemId !== strOf(speaker["speaker_id"]))
|
|
456
|
+
continue;
|
|
457
|
+
lines.push(`${speaker["speaker_id"]}: ${speaker["display_name"]} [${speaker["source_kind"]}] source=${speaker["source_id"]}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
else if (target === "issue") {
|
|
461
|
+
const validation = validateSession(session);
|
|
462
|
+
for (const issue of asList(validation["issues"])) {
|
|
463
|
+
if (itemId && itemId !== strOf(issue["code"]) && itemId !== strOf(issue["severity"]))
|
|
464
|
+
continue;
|
|
465
|
+
const whereParts = [];
|
|
466
|
+
for (const k of ["episode", "scene", "action_index"]) {
|
|
467
|
+
if (issue[k] !== null && issue[k] !== undefined)
|
|
468
|
+
whereParts.push(strOf(issue[k]));
|
|
469
|
+
}
|
|
470
|
+
const where = whereParts.join(" ");
|
|
471
|
+
lines.push(`${issue["severity"]} ${issue["code"]}: ${issue["summary"]}${where ? ` [${where}]` : ""}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
throw new CliError("SCRIPT INSPECT BLOCKED: Target invalid", "Target invalid.", {
|
|
476
|
+
exitCode: EXIT_USAGE,
|
|
477
|
+
required: ["target: summary, episode, asset, speaker, or issue"],
|
|
478
|
+
received: [target],
|
|
479
|
+
nextSteps: ["Use a supported target."],
|
|
480
|
+
errorCode: "INSPECT_TARGET_INVALID",
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
const report = {
|
|
484
|
+
title: `SCRIPT INSPECT: ${target}`,
|
|
485
|
+
op: "script.inspect",
|
|
486
|
+
changed: false,
|
|
487
|
+
summary: `${target}: ${lines.length} item(s)`,
|
|
488
|
+
result: lines.length > 0 ? lines : ["No matching items."],
|
|
489
|
+
artifacts: [session.artifactLabel],
|
|
490
|
+
next: ["Use script subcommands or script patch for edits."],
|
|
491
|
+
};
|
|
492
|
+
return [report, EXIT_OK];
|
|
493
|
+
}
|
|
494
|
+
// ---------------------------------------------------------------------------
|
|
495
|
+
// command_script_patch
|
|
496
|
+
// ---------------------------------------------------------------------------
|
|
497
|
+
export async function commandScriptPatch(opts) {
|
|
498
|
+
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
499
|
+
const session = await loadScriptForEdit(opts);
|
|
500
|
+
const script = session.script;
|
|
501
|
+
const patchPath = strOf(opts["patch"]);
|
|
502
|
+
if (!exists(patchPath)) {
|
|
503
|
+
throw new CliError("SCRIPT PATCH BLOCKED: Patch file not found", "Patch file not found.", {
|
|
504
|
+
exitCode: EXIT_INPUT,
|
|
505
|
+
required: ["--patch existing JSON file"],
|
|
506
|
+
received: [patchPath],
|
|
507
|
+
nextSteps: ["Write patch JSON and rerun."],
|
|
508
|
+
errorCode: "PATCH_NOT_FOUND",
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
let payload;
|
|
512
|
+
try {
|
|
513
|
+
payload = readJson(patchPath);
|
|
514
|
+
}
|
|
515
|
+
catch (exc) {
|
|
516
|
+
throw new CliError("SCRIPT PATCH BLOCKED: Patch JSON invalid", "Patch JSON invalid.", {
|
|
517
|
+
exitCode: EXIT_USAGE,
|
|
518
|
+
required: ["valid JSON patch"],
|
|
519
|
+
received: [`${patchPath}: ${exc.message}`],
|
|
520
|
+
nextSteps: ["Fix patch JSON and rerun."],
|
|
521
|
+
errorCode: "PATCH_JSON_INVALID",
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
const operations = patchOperationsFromPayload(payload);
|
|
525
|
+
const applied = applyPatchOperations(script, scriptSourceText(workspace), operations);
|
|
526
|
+
const [newRevision, idempotent] = await saveScriptSession(session, opts, applied.length === 1 ? applied[0] : "script.patch");
|
|
527
|
+
const validation = validateSession(session);
|
|
528
|
+
await syncValidationResult(session, validation, newRevision);
|
|
529
|
+
const passed = Boolean(validation["passed"]);
|
|
530
|
+
const resultLines = [
|
|
531
|
+
`operations: ${applied.length}`,
|
|
532
|
+
`validation: ${passed ? "passed" : "needs repair"}`,
|
|
533
|
+
];
|
|
534
|
+
if (session.remote) {
|
|
535
|
+
resultLines.push(`revision: ${newRevision}`);
|
|
536
|
+
resultLines.push(`idempotent: ${String(idempotent).toLowerCase()}`);
|
|
537
|
+
}
|
|
538
|
+
const report = {
|
|
539
|
+
title: passed ? "SCRIPT PATCH APPLIED" : "SCRIPT PATCH APPLIED: Validation needs repair",
|
|
540
|
+
op: applied.length === 1 ? applied[0] : "script.patch",
|
|
541
|
+
changed: applied.length > 0,
|
|
542
|
+
summary: `applied ${applied.length} operation(s)`,
|
|
543
|
+
warnings: passed ? [] : summarizeIssues(asList(validation["issues"])),
|
|
544
|
+
result: resultLines,
|
|
545
|
+
issues: summarizeIssues(asList(validation["issues"])),
|
|
546
|
+
artifacts: [session.artifactLabel, path.join(directDir(workspace), "validation.json")],
|
|
547
|
+
next: [passed ? "Continue editing the final script." : "Inspect issues and apply another patch."],
|
|
548
|
+
};
|
|
549
|
+
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
550
|
+
}
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
// Single-op helpers
|
|
553
|
+
// ---------------------------------------------------------------------------
|
|
554
|
+
async function applySingleScriptOp(opts, op) {
|
|
555
|
+
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
556
|
+
const session = await loadScriptForEdit(opts);
|
|
557
|
+
const script = session.script;
|
|
558
|
+
const applied = applyPatchOperations(script, scriptSourceText(workspace), [op]);
|
|
559
|
+
const opName = applied[0] ?? strOf(op["op"]);
|
|
560
|
+
const [newRevision, idempotent] = await saveScriptSession(session, opts, opName);
|
|
561
|
+
const validation = validateSession(session);
|
|
562
|
+
await syncValidationResult(session, validation, newRevision);
|
|
563
|
+
const passed = Boolean(validation["passed"]);
|
|
564
|
+
const resultLines = [
|
|
565
|
+
`operation: ${opName}`,
|
|
566
|
+
`validation: ${passed ? "passed" : "needs repair"}`,
|
|
567
|
+
];
|
|
568
|
+
if (session.remote) {
|
|
569
|
+
resultLines.push(`revision: ${newRevision}`);
|
|
570
|
+
resultLines.push(`idempotent: ${String(idempotent).toLowerCase()}`);
|
|
571
|
+
}
|
|
572
|
+
const report = {
|
|
573
|
+
title: passed ? "SCRIPT OP APPLIED" : "SCRIPT OP APPLIED: Validation needs repair",
|
|
574
|
+
op: opName,
|
|
575
|
+
changed: applied.length > 0,
|
|
576
|
+
summary: summarizeScriptOp(script, op),
|
|
577
|
+
warnings: passed ? [] : summarizeIssues(asList(validation["issues"])),
|
|
578
|
+
result: resultLines,
|
|
579
|
+
issues: summarizeIssues(asList(validation["issues"])),
|
|
580
|
+
artifacts: [session.artifactLabel, path.join(directDir(workspace), "validation.json")],
|
|
581
|
+
next: [passed ? "Continue editing the final script." : "Inspect issues and apply another patch."],
|
|
582
|
+
};
|
|
583
|
+
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
584
|
+
}
|
|
585
|
+
function summarizeScriptOp(_script, op) {
|
|
586
|
+
const kind = strOf(op["op"]);
|
|
587
|
+
if (kind === "state.add")
|
|
588
|
+
return `已为 ${op["target"]} 新增状态 ${op["state_id"]}: ${op["name"] || op["state_name"]}`;
|
|
589
|
+
if (kind === "action.state.change")
|
|
590
|
+
return `已在 ${op["at"]} 为 ${op["target"]} 设置状态切换到 ${op["to"] || op["to_state_id"]}`;
|
|
591
|
+
if (kind.startsWith("state."))
|
|
592
|
+
return `已执行 ${kind}: ${op["target"]}`;
|
|
593
|
+
if (kind.startsWith("dialogue."))
|
|
594
|
+
return `已执行 ${kind}: ${op["at"]}`;
|
|
595
|
+
return `已执行 ${kind}`;
|
|
596
|
+
}
|
|
597
|
+
async function commandStateRefs(opts, plan = false) {
|
|
598
|
+
const session = await loadScriptForEdit(opts);
|
|
599
|
+
const script = session.script;
|
|
600
|
+
const args = asList(opts["_args"]);
|
|
601
|
+
const [targetKind, targetId, stateId] = parseStateTarget(args[0] ?? "");
|
|
602
|
+
// Verify state exists (will throw if not)
|
|
603
|
+
const _asset = (() => {
|
|
604
|
+
for (const asset of asList(script[`${targetKind}s`])) {
|
|
605
|
+
if (strOf(asset[`${targetKind}_id`]) === targetId) {
|
|
606
|
+
for (const st of asList(asset["states"])) {
|
|
607
|
+
if (strOf(st["state_id"]) === stateId)
|
|
608
|
+
return st;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
throw new CliError("SCRIPT OP BLOCKED: State not found", "State not found.", {
|
|
613
|
+
exitCode: EXIT_USAGE,
|
|
614
|
+
required: ["existing state_id on the target asset"],
|
|
615
|
+
received: [`${targetKind}:${targetId}/${stateId}`],
|
|
616
|
+
nextSteps: ["Inspect states and fix the target."],
|
|
617
|
+
errorCode: "STATE_NOT_FOUND",
|
|
618
|
+
});
|
|
619
|
+
})();
|
|
620
|
+
void _asset;
|
|
621
|
+
const refs = collectStateRefs(script, targetKind, targetId, stateId);
|
|
622
|
+
const report = {
|
|
623
|
+
title: plan ? "SCRIPT STATE DELETE PLAN" : "SCRIPT STATE REFS",
|
|
624
|
+
op: plan ? "state.delete-plan" : "state.refs",
|
|
625
|
+
changed: false,
|
|
626
|
+
summary: `${targetKind}:${targetId}/${stateId} refs=${refs.length}`,
|
|
627
|
+
result: refs.length > 0 ? refs.map((r) => JSON.stringify(r)) : ["No references."],
|
|
628
|
+
refs: refs,
|
|
629
|
+
artifacts: [session.artifactLabel],
|
|
630
|
+
next: plan && refs.length > 0
|
|
631
|
+
? ["Choose replace/remove strategy. Do not delete silently when references affect story continuity."]
|
|
632
|
+
: [plan ? "State can be deleted directly." : "Use delete-plan before deleting a state."],
|
|
633
|
+
};
|
|
634
|
+
return [report, EXIT_OK];
|
|
635
|
+
}
|
|
636
|
+
export async function commandScriptState(opts, action) {
|
|
637
|
+
const args = asList(opts["_args"]);
|
|
638
|
+
if (action === "refs" || action === "delete-plan") {
|
|
639
|
+
return commandStateRefs(opts, action === "delete-plan");
|
|
640
|
+
}
|
|
641
|
+
if (args.length === 0) {
|
|
642
|
+
throw new CliError("SCRIPT STATE BLOCKED: Target missing", "Target missing.", {
|
|
643
|
+
exitCode: EXIT_USAGE,
|
|
644
|
+
required: ["state or asset target"],
|
|
645
|
+
received: ["<empty>"],
|
|
646
|
+
nextSteps: ["Run scriptctl script state --help."],
|
|
647
|
+
errorCode: "TARGET_MISSING",
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
let op;
|
|
651
|
+
if (action === "add") {
|
|
652
|
+
op = {
|
|
653
|
+
op: "state.add",
|
|
654
|
+
target: args[0],
|
|
655
|
+
name: opts["name"],
|
|
656
|
+
description: opts["description"],
|
|
657
|
+
state_id: opts["state_id"],
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
else if (action === "rename") {
|
|
661
|
+
op = { op: "state.rename", target: args[0], name: opts["name"] };
|
|
662
|
+
}
|
|
663
|
+
else if (action === "describe") {
|
|
664
|
+
op = { op: "state.describe", target: args[0], description: opts["description"] };
|
|
665
|
+
}
|
|
666
|
+
else if (action === "delete-apply") {
|
|
667
|
+
op = {
|
|
668
|
+
op: "state.delete",
|
|
669
|
+
target: args[0],
|
|
670
|
+
strategy: opts["strategy"],
|
|
671
|
+
replacement: opts["replacement"],
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
throw new CliError("SCRIPT STATE BLOCKED: Command invalid", "Command invalid.", {
|
|
676
|
+
exitCode: EXIT_USAGE,
|
|
677
|
+
required: ["add, rename, describe, refs, delete-plan, delete-apply"],
|
|
678
|
+
received: [action],
|
|
679
|
+
nextSteps: ["Run scriptctl script state --help."],
|
|
680
|
+
errorCode: "STATE_COMMAND_INVALID",
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
return applySingleScriptOp(opts, op);
|
|
684
|
+
}
|
|
685
|
+
export async function commandScriptContext(opts, action) {
|
|
686
|
+
const args = asList(opts["_args"]);
|
|
687
|
+
if (args.length < 2) {
|
|
688
|
+
throw new CliError("SCRIPT CONTEXT BLOCKED: Arguments missing", "Arguments missing.", {
|
|
689
|
+
exitCode: EXIT_USAGE,
|
|
690
|
+
required: ["scene ref and target"],
|
|
691
|
+
received: args,
|
|
692
|
+
nextSteps: ["Run scriptctl script context --help."],
|
|
693
|
+
errorCode: "ARGS_MISSING",
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
if (action !== "set" && action !== "clear") {
|
|
697
|
+
throw new CliError("SCRIPT CONTEXT BLOCKED: Command invalid", "Command invalid.", {
|
|
698
|
+
exitCode: EXIT_USAGE,
|
|
699
|
+
required: ["set or clear"],
|
|
700
|
+
received: [action],
|
|
701
|
+
nextSteps: ["Run scriptctl script context --help."],
|
|
702
|
+
errorCode: "CONTEXT_COMMAND_INVALID",
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
const op = {
|
|
706
|
+
op: action === "set" ? "context.set" : "context.clear",
|
|
707
|
+
at: args[0],
|
|
708
|
+
target: args[1],
|
|
709
|
+
state: opts["state"],
|
|
710
|
+
};
|
|
711
|
+
return applySingleScriptOp(opts, op);
|
|
712
|
+
}
|
|
713
|
+
export async function commandScriptAction(opts, action) {
|
|
714
|
+
const args = asList(opts["_args"]);
|
|
715
|
+
if (args.length < 2) {
|
|
716
|
+
throw new CliError("SCRIPT ACTION BLOCKED: Arguments missing", "Arguments missing.", {
|
|
717
|
+
exitCode: EXIT_USAGE,
|
|
718
|
+
required: ["action ref and target"],
|
|
719
|
+
received: args,
|
|
720
|
+
nextSteps: ["Run scriptctl script action --help."],
|
|
721
|
+
errorCode: "ARGS_MISSING",
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
let op;
|
|
725
|
+
if (action === "state-change") {
|
|
726
|
+
op = {
|
|
727
|
+
op: "action.state.change",
|
|
728
|
+
at: args[0],
|
|
729
|
+
target: args[1],
|
|
730
|
+
to: opts["to"],
|
|
731
|
+
from: opts["from"],
|
|
732
|
+
effective: opts["effective"] || "after",
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
else if (action === "state-remove") {
|
|
736
|
+
op = { op: "action.state.remove", at: args[0], target: args[1] };
|
|
737
|
+
}
|
|
738
|
+
else if (action === "transition-set") {
|
|
739
|
+
op = {
|
|
740
|
+
op: "action.transition.set",
|
|
741
|
+
at: args[0],
|
|
742
|
+
target: args[1],
|
|
743
|
+
process: opts["process"],
|
|
744
|
+
contrast: opts["contrast"],
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
else if (action === "transition-clear") {
|
|
748
|
+
op = { op: "action.transition.clear", at: args[0], target: args[1] };
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
throw new CliError("SCRIPT ACTION BLOCKED: Command invalid", "Command invalid.", {
|
|
752
|
+
exitCode: EXIT_USAGE,
|
|
753
|
+
required: ["state-change, state-remove, transition-set, transition-clear"],
|
|
754
|
+
received: [action],
|
|
755
|
+
nextSteps: ["Run scriptctl script action --help."],
|
|
756
|
+
errorCode: "ACTION_COMMAND_INVALID",
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
return applySingleScriptOp(opts, op);
|
|
760
|
+
}
|
|
761
|
+
export async function commandScriptSpeaker(opts, action) {
|
|
762
|
+
if (action !== "add") {
|
|
763
|
+
throw new CliError("SCRIPT SPEAKER BLOCKED: Command invalid", "Command invalid.", {
|
|
764
|
+
exitCode: EXIT_USAGE,
|
|
765
|
+
required: ["add"],
|
|
766
|
+
received: [action],
|
|
767
|
+
nextSteps: ["Run scriptctl script speaker --help."],
|
|
768
|
+
errorCode: "SPEAKER_COMMAND_INVALID",
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
const op = {
|
|
772
|
+
op: "speaker.add",
|
|
773
|
+
kind: opts["kind"],
|
|
774
|
+
name: opts["name"],
|
|
775
|
+
source_id: opts["source_id"],
|
|
776
|
+
voice_desc: opts["voice_desc"],
|
|
777
|
+
speaker_id: opts["speaker_id"],
|
|
778
|
+
};
|
|
779
|
+
return applySingleScriptOp(opts, op);
|
|
780
|
+
}
|
|
781
|
+
export async function commandScriptDialogue(opts, action) {
|
|
782
|
+
const args = asList(opts["_args"]);
|
|
783
|
+
if (args.length === 0) {
|
|
784
|
+
throw new CliError("SCRIPT DIALOGUE BLOCKED: Action ref missing", "Action ref missing.", {
|
|
785
|
+
exitCode: EXIT_USAGE,
|
|
786
|
+
required: ["action ref"],
|
|
787
|
+
received: ["<empty>"],
|
|
788
|
+
nextSteps: ["Run scriptctl script dialogue --help."],
|
|
789
|
+
errorCode: "ACTION_REF_MISSING",
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
let op;
|
|
793
|
+
if (action === "speakers") {
|
|
794
|
+
op = {
|
|
795
|
+
op: "dialogue.speakers",
|
|
796
|
+
at: args[0],
|
|
797
|
+
speakers: opts["speaker"] || [],
|
|
798
|
+
delivery: opts["delivery"] || "simultaneous",
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
else if (action === "overlap") {
|
|
802
|
+
op = {
|
|
803
|
+
op: "dialogue.overlap",
|
|
804
|
+
at: args[0],
|
|
805
|
+
lines: opts["line"] || [],
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
throw new CliError("SCRIPT DIALOGUE BLOCKED: Command invalid", "Command invalid.", {
|
|
810
|
+
exitCode: EXIT_USAGE,
|
|
811
|
+
required: ["speakers or overlap"],
|
|
812
|
+
received: [action],
|
|
813
|
+
nextSteps: ["Run scriptctl script dialogue --help."],
|
|
814
|
+
errorCode: "DIALOGUE_COMMAND_INVALID",
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
return applySingleScriptOp(opts, op);
|
|
818
|
+
}
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
// command_export (direct export)
|
|
821
|
+
// ---------------------------------------------------------------------------
|
|
822
|
+
export async function commandExport(opts) {
|
|
823
|
+
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
824
|
+
const force = Boolean(opts["force"]);
|
|
825
|
+
const scriptPath = path.join(directDir(workspace), "script.initial.json");
|
|
826
|
+
if (!exists(scriptPath)) {
|
|
827
|
+
throw new CliError("EXPORT BLOCKED: script.initial.json not found", "script.initial.json not found.", {
|
|
828
|
+
exitCode: EXIT_INPUT,
|
|
829
|
+
required: ["workspace/draft/scriptctl/direct/script.initial.json"],
|
|
830
|
+
received: [scriptPath],
|
|
831
|
+
nextSteps: ["Run scriptctl direct init first."],
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
const state = readRunState(workspace);
|
|
835
|
+
const provider = strOf(state["provider"]);
|
|
836
|
+
if (provider === "mock" && process.env.SCRIPTCTL_ALLOW_MOCK_EXPORT !== "1") {
|
|
837
|
+
const report = {
|
|
838
|
+
title: "EXPORT BLOCKED: Mock provider result",
|
|
839
|
+
result: ["script.initial.json was produced by --provider mock and was not exported."],
|
|
840
|
+
artifacts: [path.join(directDir(workspace), "run_state.json")],
|
|
841
|
+
next: ["Rerun init with --provider anthropic for deliverable conversion."],
|
|
842
|
+
};
|
|
843
|
+
return [report, EXIT_NEEDS_AGENT];
|
|
844
|
+
}
|
|
845
|
+
const missingReview = reviewBlockers(state);
|
|
846
|
+
if (missingReview.length > 0) {
|
|
847
|
+
const report = {
|
|
848
|
+
title: "EXPORT BLOCKED: Agent review incomplete",
|
|
849
|
+
result: ["script.initial.json was not exported.", `missing review: ${missingReview.join(", ")}`],
|
|
850
|
+
artifacts: [path.join(directDir(workspace), "run_state.json")],
|
|
851
|
+
next: ["Run inspect for each missing target, or apply a structured patch, then validate/export."],
|
|
852
|
+
};
|
|
853
|
+
return [report, EXIT_NEEDS_AGENT];
|
|
854
|
+
}
|
|
855
|
+
const validation = validateScript(workspace, scriptPath);
|
|
856
|
+
const blockingOrError = Boolean(validation["has_blocking"]) ||
|
|
857
|
+
asList(validation["issues"]).some((it) => isDict(it) && (it["severity"] === "blocking" || it["severity"] === "error"));
|
|
858
|
+
if (!validation["passed"] && (!force || blockingOrError)) {
|
|
859
|
+
const title = force
|
|
860
|
+
? "EXPORT BLOCKED: Validation errors require repair"
|
|
861
|
+
: "EXPORT BLOCKED: Validation needs agent repair";
|
|
862
|
+
const report = {
|
|
863
|
+
title,
|
|
864
|
+
result: ["script.initial.json was not exported."],
|
|
865
|
+
artifacts: [path.join(directDir(workspace), "validation.json")],
|
|
866
|
+
issues: summarizeIssues(asList(validation["issues"])),
|
|
867
|
+
next: ["Inspect issues and apply structured patches, then validate/export."],
|
|
868
|
+
};
|
|
869
|
+
return [report, EXIT_NEEDS_AGENT];
|
|
870
|
+
}
|
|
871
|
+
let script;
|
|
872
|
+
try {
|
|
873
|
+
script = readJson(scriptPath);
|
|
874
|
+
}
|
|
875
|
+
catch (exc) {
|
|
876
|
+
throw new CliError("EXPORT BLOCKED: script.initial.json invalid", "script.initial.json invalid.", {
|
|
877
|
+
exitCode: EXIT_INPUT,
|
|
878
|
+
required: ["valid script.initial.json"],
|
|
879
|
+
received: [`${scriptPath}: ${exc.message}`],
|
|
880
|
+
nextSteps: ["Fix script.initial.json or rerun direct init."],
|
|
881
|
+
errorCode: "SCRIPT_JSON_INVALID",
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
if (!isDict(script)) {
|
|
885
|
+
throw new CliError("EXPORT BLOCKED: script root invalid", "script root invalid.", {
|
|
886
|
+
exitCode: EXIT_USAGE,
|
|
887
|
+
required: ["script root object"],
|
|
888
|
+
received: [Array.isArray(script) ? "array" : typeof script],
|
|
889
|
+
nextSteps: ["Use a valid script document object."],
|
|
890
|
+
errorCode: "SCRIPT_ROOT_INVALID",
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
const client = scriptOutputClient(opts);
|
|
894
|
+
const baseRevision = await currentRevisionOrZero(client);
|
|
895
|
+
// Sorted-keys serialization mirrors Python json.dumps(sort_keys=True, separators=(",",":"))
|
|
896
|
+
const sortedScript = sortDeep(script);
|
|
897
|
+
const scriptHash = sha256Text(JSON.stringify(sortedScript));
|
|
898
|
+
const requestId = strOf(opts["request_id"]).trim() || `scriptctl-direct-export:${scriptHash}`;
|
|
899
|
+
let replaceRes;
|
|
900
|
+
try {
|
|
901
|
+
replaceRes = await client.replaceScript({
|
|
902
|
+
requestId,
|
|
903
|
+
baseRevision,
|
|
904
|
+
script,
|
|
905
|
+
source: "ctl",
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
catch (exc) {
|
|
909
|
+
if (exc instanceof ScriptOutputApiError) {
|
|
910
|
+
throw apiErrorToCli("SCRIPT API BLOCKED: Export write failed", exc);
|
|
911
|
+
}
|
|
912
|
+
throw exc;
|
|
913
|
+
}
|
|
914
|
+
const revision = Number(replaceRes["revision"] ?? 0);
|
|
915
|
+
const remoteSession = new ScriptEditSession({
|
|
916
|
+
workspace,
|
|
917
|
+
script,
|
|
918
|
+
client,
|
|
919
|
+
projectGroupNo: client.projectGroupNo,
|
|
920
|
+
revision,
|
|
921
|
+
});
|
|
922
|
+
await syncValidationResult(remoteSession, validation, revision);
|
|
923
|
+
const outputLabel = remoteSession.artifactLabel;
|
|
924
|
+
updateRunState(workspace, { status: "exported", output_path: outputLabel });
|
|
925
|
+
const report = {
|
|
926
|
+
title: "EXPORT COMPLETE: Final script stored in DB",
|
|
927
|
+
result: [
|
|
928
|
+
`validation: ${validation["passed"] ? "passed" : "forced"}`,
|
|
929
|
+
`base_revision: ${baseRevision}`,
|
|
930
|
+
`revision: ${revision}`,
|
|
931
|
+
`idempotent: ${String(Boolean(replaceRes["idempotent"])).toLowerCase()}`,
|
|
932
|
+
],
|
|
933
|
+
artifacts: [outputLabel, path.join(directDir(workspace), "validation.json")],
|
|
934
|
+
next: ["Proceed to downstream asset or footage stages."],
|
|
935
|
+
};
|
|
936
|
+
return [report, EXIT_OK];
|
|
937
|
+
}
|
|
938
|
+
function sortDeep(value) {
|
|
939
|
+
if (Array.isArray(value))
|
|
940
|
+
return value.map((v) => sortDeep(v));
|
|
941
|
+
if (isDict(value)) {
|
|
942
|
+
const sorted = {};
|
|
943
|
+
for (const k of Object.keys(value).sort())
|
|
944
|
+
sorted[k] = sortDeep(value[k]);
|
|
945
|
+
return sorted;
|
|
946
|
+
}
|
|
947
|
+
return value;
|
|
948
|
+
}
|
|
949
|
+
//# sourceMappingURL=script.js.map
|