@lingjingai/scriptctl 0.13.1 → 0.19.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.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +33 -49
  3. package/dist/cli.js.map +1 -1
  4. package/dist/common.d.ts +11 -4
  5. package/dist/common.js +83 -61
  6. package/dist/common.js.map +1 -1
  7. package/dist/domain/direct-core.d.ts +27 -4
  8. package/dist/domain/direct-core.js +281 -55
  9. package/dist/domain/direct-core.js.map +1 -1
  10. package/dist/domain/script-core.d.ts +15 -0
  11. package/dist/domain/script-core.js +207 -40
  12. package/dist/domain/script-core.js.map +1 -1
  13. package/dist/help-text.js +45 -315
  14. package/dist/help-text.js.map +1 -1
  15. package/dist/infra/providers.d.ts +10 -36
  16. package/dist/infra/providers.js +144 -300
  17. package/dist/infra/providers.js.map +1 -1
  18. package/dist/usecases/direct.js +342 -60
  19. package/dist/usecases/direct.js.map +1 -1
  20. package/dist/usecases/doctor.js +2 -5
  21. package/dist/usecases/doctor.js.map +1 -1
  22. package/dist/usecases/parse.js +104 -14
  23. package/dist/usecases/parse.js.map +1 -1
  24. package/dist/usecases/script.d.ts +7 -2
  25. package/dist/usecases/script.js +180 -12
  26. package/dist/usecases/script.js.map +1 -1
  27. package/package.json +2 -3
  28. package/dist/domain/asset-registry.d.ts +0 -141
  29. package/dist/domain/asset-registry.js +0 -318
  30. package/dist/domain/asset-registry.js.map +0 -1
  31. package/dist/domain/collision-detector.d.ts +0 -83
  32. package/dist/domain/collision-detector.js +0 -248
  33. package/dist/domain/collision-detector.js.map +0 -1
  34. package/dist/infra/default-writing-prompt.d.ts +0 -31
  35. package/dist/infra/default-writing-prompt.js +0 -50
  36. package/dist/infra/default-writing-prompt.js.map +0 -1
  37. package/dist/infra/default-writing-prompt.md +0 -115
  38. package/dist/infra/gemini-writer.d.ts +0 -107
  39. package/dist/infra/gemini-writer.js +0 -207
  40. package/dist/infra/gemini-writer.js.map +0 -1
  41. package/dist/usecases/episode.d.ts +0 -48
  42. package/dist/usecases/episode.js +0 -1242
  43. package/dist/usecases/episode.js.map +0 -1
@@ -3,11 +3,12 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { randomUUID } from "node:crypto";
5
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, collectAssetRefs, collectStateRefs, parseAnyAddress, parseStateTarget, validateScript, PATCH_OP_SCHEMA, } from "../domain/script-core.js";
6
+ import { applyPatchOperations, blankScript, collectAssetRefs, collectStateRefs, parseAnyAddress, parseStateTarget, validateScript, PATCH_OP_SCHEMA, } from "../domain/script-core.js";
7
7
  import { RemoteScriptOutputStore } from "../infra/script-output-api.js";
8
8
  import { LocalScriptOutputStore } from "../infra/local-script-output-store.js";
9
9
  import { ScriptOutputApiError, resolveOutputMode, } from "../infra/script-output-store.js";
10
10
  import { readRunState, summarizeIssues, updateRunState, } from "./direct.js";
11
+ import { sortDeep } from "../domain/direct-core.js";
11
12
  function strOf(v) {
12
13
  if (v === null || v === undefined)
13
14
  return "";
@@ -747,6 +748,14 @@ async function applySingleScriptOp(opts, op) {
747
748
  }
748
749
  function summarizeScriptOp(_script, op) {
749
750
  const kind = strOf(op["op"]);
751
+ if (kind === "actor.add")
752
+ return `已新增人物 ${op["name"] || op["actor_name"]}${op["role_type"] ? `(${op["role_type"]})` : ""}`;
753
+ if (kind === "location.add")
754
+ return `已新增场景地点 ${op["name"] || op["location_name"]}`;
755
+ if (kind === "prop.add")
756
+ return `已新增道具 ${op["name"] || op["prop_name"]}`;
757
+ if (kind === "episode.add")
758
+ return `已新增分集${op["episode_id"] ? ` ${op["episode_id"]}` : "(自动编号)"}${op["title"] ? `:${op["title"]}` : ""}`;
750
759
  if (kind === "state.add")
751
760
  return `已为 ${op["target"]} 新增状态 ${op["state_id"]}: ${op["name"] || op["state_name"]}`;
752
761
  if (kind === "action.state.change")
@@ -905,17 +914,9 @@ export async function commandExport(opts) {
905
914
  };
906
915
  return [report, EXIT_OK];
907
916
  }
908
- export function sortDeep(value) {
909
- if (Array.isArray(value))
910
- return value.map((v) => sortDeep(v));
911
- if (isDict(value)) {
912
- const sorted = {};
913
- for (const k of Object.keys(value).sort())
914
- sorted[k] = sortDeep(value[k]);
915
- return sorted;
916
- }
917
- return value;
918
- }
917
+ // sortDeep is the canonical helper in domain/direct-core; re-exported here for
918
+ // back-compat with existing importers of `usecases/script`.
919
+ export { sortDeep };
919
920
  // ===========================================================================
920
921
  // 0.6.0 plural-noun query commands (replace `commandScriptInspect --target X`)
921
922
  // ===========================================================================
@@ -1071,9 +1072,20 @@ export async function commandSummary(opts) {
1071
1072
  `props: ${asList(script["props"]).length}`,
1072
1073
  `speakers: ${asList(script["speakers"]).length}`,
1073
1074
  ];
1075
+ const logline = strOf(script["logline"]).trim();
1076
+ if (logline)
1077
+ lines.push(`logline: ${logline}`);
1078
+ const theme = strOf(script["theme"]).trim();
1079
+ if (theme)
1080
+ lines.push(`theme: ${theme}`);
1074
1081
  const synopsis = strOf(script["synopsis"]).trim();
1075
1082
  if (synopsis)
1076
1083
  lines.push(`synopsis: ${synopsisPreview(synopsis, 120)}`);
1084
+ const mainCharacters = asList(script["main_characters"])
1085
+ .map((c) => strOf(c["name"]).trim())
1086
+ .filter((n) => n);
1087
+ if (mainCharacters.length > 0)
1088
+ lines.push(`main characters: ${mainCharacters.slice(0, 6).join(", ")}`);
1077
1089
  return [buildReport("query.summary", "SCRIPT SUMMARY", lines, session.artifactLabel, ["Use plural-noun queries (episodes / scenes / actions / actors / ...) or edit verbs to act on the script."]), EXIT_OK];
1078
1090
  }
1079
1091
  // ----- episodes -------------------------------------------------------------
@@ -2177,4 +2189,160 @@ export async function commandAddSpeaker(opts) {
2177
2189
  speaker_id: opts["speaker_id"],
2178
2190
  });
2179
2191
  }
2192
+ // ----- create (blank scaffold) ----------------------------------------------
2193
+ // Create an empty schema-v2 script as the starting point for building a script
2194
+ // bottom-up with the atomic add/insert verbs (instead of direct/parse/write
2195
+ // assembly). Targets the same three destinations as the edit verbs:
2196
+ // --script-path <file> local JSON file
2197
+ // --draft the direct intermediate (script.initial.json)
2198
+ // --project-group-no DB-backed script-output (first write at revision 0)
2199
+ // Refuses to clobber an existing target (local: pass --force to overwrite; DB:
2200
+ // edit the existing script instead). Validates in incomplete-tolerant mode, so
2201
+ // the blank scaffold reports passed.
2202
+ export async function commandCreate(opts) {
2203
+ const workspace = strOf(opts["workspace_path"] || "workspace");
2204
+ const force = Boolean(opts["force"]);
2205
+ const title = strOf(opts["title"]).trim();
2206
+ const script = blankScript(title);
2207
+ const draft = Boolean(opts["draft"]);
2208
+ const scriptPathOpt = strOf(opts["script_path"]).trim();
2209
+ if (draft && scriptPathOpt) {
2210
+ throw new CliError("CREATE BLOCKED: --draft conflicts with --script-path", "--draft conflicts with --script-path.", {
2211
+ exitCode: EXIT_USAGE,
2212
+ required: ["either --draft or --script-path, not both"],
2213
+ received: ["--draft and --script-path"],
2214
+ nextSteps: ["Drop one of the two flags and retry."],
2215
+ errorCode: "DRAFT_FLAG_CONFLICT",
2216
+ });
2217
+ }
2218
+ if (draft && strOf(opts["project_group_no"]).trim()) {
2219
+ throw new CliError("CREATE BLOCKED: --draft conflicts with --project-group-no", "--draft conflicts with --project-group-no.", {
2220
+ exitCode: EXIT_USAGE,
2221
+ required: ["either --draft (local draft) or --project-group-no (DB), not both"],
2222
+ received: ["--draft and --project-group-no"],
2223
+ nextSteps: ["Drop one of the two flags and retry."],
2224
+ errorCode: "DRAFT_FLAG_CONFLICT",
2225
+ });
2226
+ }
2227
+ const localPath = draft ? path.join(directDir(workspace), "script.initial.json") : (scriptPathOpt || null);
2228
+ if (localPath) {
2229
+ if (exists(localPath) && !force) {
2230
+ throw new CliError("CREATE BLOCKED: target already exists", "A script already exists at the target.", {
2231
+ exitCode: EXIT_INPUT,
2232
+ required: ["a non-existent target, or --force to overwrite"],
2233
+ received: [localPath],
2234
+ nextSteps: ["Edit the existing script with the atomic verbs, or pass --force to overwrite with a blank scaffold."],
2235
+ errorCode: "SCRIPT_EXISTS",
2236
+ });
2237
+ }
2238
+ writeJson(localPath, script);
2239
+ const session = new ScriptEditSession({ workspace, scriptPath: localPath, script });
2240
+ const validation = validateSession(session);
2241
+ const passed = Boolean(validation["passed"]);
2242
+ return [blankCreateReport(session, validation, null), passed ? EXIT_OK : EXIT_NEEDS_AGENT];
2243
+ }
2244
+ // DB mode: first write must be at revision 0. We never silently overwrite a
2245
+ // populated DB script with a blank one — that would destroy work.
2246
+ const client = scriptOutputClient(opts);
2247
+ const baseRevision = await currentRevisionOrZero(client);
2248
+ if (baseRevision > 0) {
2249
+ throw new CliError("CREATE BLOCKED: DB script already exists", "A script already exists for this project.", {
2250
+ exitCode: EXIT_INPUT,
2251
+ required: ["an empty project (no published script)"],
2252
+ received: [`revision=${baseRevision}`],
2253
+ nextSteps: ["Edit the existing script with the atomic verbs instead of creating a blank one."],
2254
+ errorCode: "SCRIPT_EXISTS",
2255
+ });
2256
+ }
2257
+ const requestId = requestIdForScriptWrite(opts, "script.create", { script });
2258
+ let res;
2259
+ try {
2260
+ res = await client.replaceScript({ requestId, baseRevision, script, source: "ctl" });
2261
+ }
2262
+ catch (exc) {
2263
+ if (exc instanceof ScriptOutputApiError) {
2264
+ throw apiErrorToCli("SCRIPT API BLOCKED: Create write failed", exc);
2265
+ }
2266
+ throw exc;
2267
+ }
2268
+ const revision = Number(res["revision"] ?? 0);
2269
+ const session = new ScriptEditSession({ workspace, script, client, projectGroupNo: client.projectGroupNo, revision });
2270
+ const validation = validateSession(session);
2271
+ await syncValidationResult(session, validation, revision);
2272
+ const passed = Boolean(validation["passed"]);
2273
+ return [blankCreateReport(session, validation, Boolean(res["idempotent"])), passed ? EXIT_OK : EXIT_NEEDS_AGENT];
2274
+ }
2275
+ function blankCreateReport(session, validation, idempotent) {
2276
+ const passed = Boolean(validation["passed"]);
2277
+ const resultLines = ["created: blank script (schema v2)", `validation: ${passed ? "passed" : "needs repair"}`];
2278
+ if (session.remote) {
2279
+ resultLines.push(`revision: ${session.revision}`);
2280
+ if (idempotent !== null)
2281
+ resultLines.push(`idempotent: ${String(idempotent).toLowerCase()}`);
2282
+ }
2283
+ return {
2284
+ title: passed ? "SCRIPT CREATED: Blank scaffold" : "SCRIPT CREATED: Validation needs repair",
2285
+ op: "script.create",
2286
+ changed: true,
2287
+ summary: "已创建空白剧本(schema v2,待填充人物/地点/道具/分集)",
2288
+ result: resultLines,
2289
+ warnings: summarizeIssues(asList(validation["issues"])),
2290
+ artifacts: [session.artifactLabel],
2291
+ next: [
2292
+ "用 worldview 设定世界观,用 add-actor / add-location / add-prop 注册资产,用 add-episode + insert 搭出分集与场景。",
2293
+ ],
2294
+ };
2295
+ }
2296
+ // ----- add-* (register new entities) ----------------------------------------
2297
+ // Read the single positional <name> arg shared by add-actor/location/prop, or
2298
+ // throw the common ARGS_MISSING usage error with a verb-specific example.
2299
+ function requireNameArg(opts, verb, example) {
2300
+ const args = asList(opts["_args"]);
2301
+ if (args.length < 1) {
2302
+ throw new CliError(`VERB BLOCKED: ${verb} name missing`, "Name missing.", {
2303
+ exitCode: EXIT_USAGE,
2304
+ required: ["<name>"],
2305
+ received: ["<empty>"],
2306
+ nextSteps: [example],
2307
+ errorCode: "ARGS_MISSING",
2308
+ });
2309
+ }
2310
+ return args[0];
2311
+ }
2312
+ export async function commandAddActor(opts) {
2313
+ const name = requireNameArg(opts, "add-actor", "Example: scriptctl add-actor \"林夏\" --role 主角");
2314
+ return applySingleScriptOp(opts, {
2315
+ op: "actor.add",
2316
+ name,
2317
+ actor_id: opts["id"],
2318
+ role_type: opts["role"],
2319
+ description: opts["description"],
2320
+ aliases: opts["alias"],
2321
+ });
2322
+ }
2323
+ export async function commandAddLocation(opts) {
2324
+ const name = requireNameArg(opts, "add-location", "Example: scriptctl add-location \"教室\"");
2325
+ return applySingleScriptOp(opts, {
2326
+ op: "location.add",
2327
+ name,
2328
+ location_id: opts["id"],
2329
+ description: opts["description"],
2330
+ });
2331
+ }
2332
+ export async function commandAddProp(opts) {
2333
+ const name = requireNameArg(opts, "add-prop", "Example: scriptctl add-prop \"怀表\"");
2334
+ return applySingleScriptOp(opts, {
2335
+ op: "prop.add",
2336
+ name,
2337
+ prop_id: opts["id"],
2338
+ description: opts["description"],
2339
+ });
2340
+ }
2341
+ export async function commandAddEpisode(opts) {
2342
+ return applySingleScriptOp(opts, {
2343
+ op: "episode.add",
2344
+ episode_id: opts["id"],
2345
+ title: opts["title"],
2346
+ });
2347
+ }
2180
2348
  //# sourceMappingURL=script.js.map