@lingjingai/scriptctl 0.1.0 → 0.3.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 (40) hide show
  1. package/README.md +72 -0
  2. package/dist/cli.js +309 -396
  3. package/dist/cli.js.map +1 -1
  4. package/dist/common.d.ts +9 -0
  5. package/dist/common.js.map +1 -1
  6. package/dist/domain/asset-registry.d.ts +141 -0
  7. package/dist/domain/asset-registry.js +318 -0
  8. package/dist/domain/asset-registry.js.map +1 -0
  9. package/dist/domain/collision-detector.d.ts +83 -0
  10. package/dist/domain/collision-detector.js +248 -0
  11. package/dist/domain/collision-detector.js.map +1 -0
  12. package/dist/domain/direct-core.d.ts +13 -1
  13. package/dist/domain/direct-core.js +19 -6
  14. package/dist/domain/direct-core.js.map +1 -1
  15. package/dist/domain/script-core.d.ts +11 -0
  16. package/dist/domain/script-core.js +34 -19
  17. package/dist/domain/script-core.js.map +1 -1
  18. package/dist/help-text.js +336 -4
  19. package/dist/help-text.js.map +1 -1
  20. package/dist/infra/converters.js +21 -7
  21. package/dist/infra/converters.js.map +1 -1
  22. package/dist/infra/default-writing-prompt.d.ts +31 -0
  23. package/dist/infra/default-writing-prompt.js +50 -0
  24. package/dist/infra/default-writing-prompt.js.map +1 -0
  25. package/dist/infra/default-writing-prompt.md +115 -0
  26. package/dist/infra/gemini-writer.d.ts +107 -0
  27. package/dist/infra/gemini-writer.js +207 -0
  28. package/dist/infra/gemini-writer.js.map +1 -0
  29. package/dist/infra/providers.d.ts +36 -0
  30. package/dist/infra/providers.js +186 -2
  31. package/dist/infra/providers.js.map +1 -1
  32. package/dist/output.js +26 -9
  33. package/dist/output.js.map +1 -1
  34. package/dist/usecases/episode.d.ts +48 -0
  35. package/dist/usecases/episode.js +1209 -0
  36. package/dist/usecases/episode.js.map +1 -0
  37. package/dist/usecases/script.d.ts +6 -2
  38. package/dist/usecases/script.js +49 -5
  39. package/dist/usecases/script.js.map +1 -1
  40. package/package.json +9 -5
package/dist/cli.js CHANGED
@@ -1,433 +1,346 @@
1
+ import { Command, CommanderError } from "commander";
1
2
  import { CliError, EXIT_OK, EXIT_USAGE } from "./common.js";
2
3
  import { getHelp } from "./help-text.js";
3
4
  import { loadLocalEnv } from "./infra/env.js";
4
5
  import { emit, emitError } from "./output.js";
5
6
  import { commandInit, commandInspect, commandValidate, } from "./usecases/direct.js";
6
7
  import { commandDoctor } from "./usecases/doctor.js";
8
+ import { commandEpisodeBatch, commandEpisodeDraft, commandEpisodeInit, commandEpisodeMerge, commandEpisodePush, commandEpisodeSpec, commandEpisodeWorkspace, } from "./usecases/episode.js";
7
9
  import { commandExport, commandScriptAction, commandScriptContext, commandScriptDialogue, commandScriptInspect, commandScriptPatch, commandScriptSpeaker, commandScriptState, commandScriptValidate, } from "./usecases/script.js";
8
- const SCRIPT_NESTED_GROUPS = new Set(["state", "context", "action", "speaker", "dialogue"]);
9
- const NESTED_COMMANDS = new Set([
10
- "script.state.add",
11
- "script.state.rename",
12
- "script.state.describe",
13
- "script.state.refs",
14
- "script.state.delete-plan",
15
- "script.state.delete-apply",
16
- "script.context.set",
17
- "script.context.clear",
18
- "script.action.state-change",
19
- "script.action.state-remove",
20
- "script.action.transition-set",
21
- "script.action.transition-clear",
22
- "script.speaker.add",
23
- "script.dialogue.speakers",
24
- "script.dialogue.overlap",
10
+ const KNOWN_COMMANDS = new Set([
11
+ "doctor",
12
+ "direct", "direct.init", "direct.validate", "direct.inspect", "direct.export",
13
+ "script", "script.inspect", "script.validate", "script.patch",
14
+ "script.state", "script.state.add", "script.state.rename", "script.state.describe",
15
+ "script.state.refs", "script.state.delete-plan", "script.state.delete-apply",
16
+ "script.context", "script.context.set", "script.context.clear",
17
+ "script.action", "script.action.state-change", "script.action.state-remove",
18
+ "script.action.transition-set", "script.action.transition-clear",
19
+ "script.speaker", "script.speaker.add",
20
+ "script.dialogue", "script.dialogue.speakers", "script.dialogue.overlap",
21
+ "episode", "episode.init", "episode.draft", "episode.batch", "episode.workspace", "episode.merge", "episode.push", "episode.spec",
25
22
  ]);
26
- function key(command) {
27
- return command.join(".");
28
- }
29
- function parseOptions(command, argv) {
30
- const opts = {};
31
- const positionals = [];
32
- let jsonMode = false;
33
- const specs = optionSpecs(command);
34
- const flagSet = new Set(specs.flags);
35
- const arraySet = new Set(specs.arrays);
36
- const valueSet = new Set(specs.values);
37
- let i = 0;
38
- while (i < argv.length) {
39
- const token = argv[i];
40
- if (token === "--json") {
41
- jsonMode = true;
42
- i += 1;
43
- continue;
44
- }
45
- if (token.startsWith("--")) {
46
- const eqIdx = token.indexOf("=");
47
- const rawName = eqIdx >= 0 ? token.slice(2, eqIdx) : token.slice(2);
48
- const camel = rawName.replace(/-/g, "_");
49
- const name = aliasRename(camel);
50
- if (flagSet.has(name)) {
51
- opts[name] = true;
52
- i += 1;
53
- continue;
54
- }
55
- let value;
56
- if (eqIdx >= 0) {
57
- value = token.slice(eqIdx + 1);
58
- i += 1;
59
- }
60
- else {
61
- if (i + 1 >= argv.length) {
62
- throw new CliError("USAGE ERROR: Invalid arguments", "Invalid arguments.", {
63
- exitCode: EXIT_USAGE,
64
- required: [`Run: scriptctl ${command.join(" ")} --help`],
65
- received: [`option --${rawName} requires a value`],
66
- nextSteps: [`Run: scriptctl ${command.join(" ")} --help`],
67
- });
68
- }
69
- value = argv[i + 1];
70
- i += 2;
71
- }
72
- if (arraySet.has(name)) {
73
- if (!Array.isArray(opts[name]))
74
- opts[name] = [];
75
- opts[name].push(value);
76
- }
77
- else if (valueSet.has(name)) {
78
- opts[name] = value;
79
- }
80
- else {
81
- // Allow unknown options for forward-compat but record verbatim
82
- opts[name] = value;
83
- }
84
- continue;
85
- }
86
- positionals.push(token);
87
- i += 1;
23
+ // Groups are command nodes that exist only to host subcommands. Extra non-flag tokens
24
+ // after a group path indicate an unknown subcommand (e.g. `direct nope` → error).
25
+ // Leaves are real commands that can accept positional arguments (e.g. `episode draft <n>`).
26
+ const GROUP_COMMANDS = new Set([
27
+ "direct",
28
+ "script",
29
+ "script.state",
30
+ "script.context",
31
+ "script.action",
32
+ "script.speaker",
33
+ "script.dialogue",
34
+ "episode",
35
+ ]);
36
+ function extractLeadingPath(args) {
37
+ // Collect leading non-flag tokens, then take only as many as form the longest known
38
+ // command prefix. Trailing tokens are treated as either:
39
+ // - positional arguments (when the matched node is a leaf, e.g. `episode draft 1`)
40
+ // - unknown subcommands (when the matched node is a group, e.g. `direct nope`)
41
+ const allNonFlag = [];
42
+ for (const tok of args) {
43
+ if (tok.startsWith("-"))
44
+ break;
45
+ allNonFlag.push(tok);
46
+ if (allNonFlag.length >= 5)
47
+ break;
88
48
  }
89
- if (specs.acceptsPositionals)
90
- opts["_args"] = positionals;
91
- // Validate required
92
- for (const required of specs.required) {
93
- if (!(required in opts)) {
94
- throw new CliError("USAGE ERROR: Invalid arguments", "Invalid arguments.", {
95
- exitCode: EXIT_USAGE,
96
- required: [`Run: scriptctl ${command.join(" ")} --help`],
97
- received: [`missing --${required.replace(/_/g, "-")}`],
98
- nextSteps: [`Run: scriptctl ${command.join(" ")} --help`],
99
- });
100
- }
49
+ const matchLen = longestKnownPrefix(allNonFlag);
50
+ if (matchLen === 0) {
51
+ // No known command at all — return up to 3 tokens for a readable error.
52
+ return allNonFlag.slice(0, 3);
101
53
  }
102
- // Apply defaults
103
- for (const [k, v] of Object.entries(specs.defaults)) {
104
- if (!(k in opts))
105
- opts[k] = v;
54
+ const matchedKey = allNonFlag.slice(0, matchLen).join(".");
55
+ const isGroup = GROUP_COMMANDS.has(matchedKey);
56
+ if (isGroup && allNonFlag.length > matchLen) {
57
+ // Group + extra tokens = unknown subcommand. Return the truncated path so the
58
+ // validator (knownLen < cmdPath.length) reports an error.
59
+ return allNonFlag.slice(0, Math.min(allNonFlag.length, 3));
106
60
  }
107
- return { opts, jsonMode };
108
- }
109
- function aliasRename(camel) {
110
- // argparse used dest="from"; we keep TS field also "from"
111
- return camel;
61
+ return allNonFlag.slice(0, matchLen);
112
62
  }
113
- function optionSpecs(command) {
114
- const k = key(command);
115
- if (k === "direct.init") {
116
- return {
117
- flags: [],
118
- values: ["source_path", "workspace_path", "provider", "model", "concurrency", "batch_mode", "batch_target_lines", "batch_max_chars", "batch_min_lines"],
119
- arrays: [],
120
- required: ["source_path"],
121
- defaults: { workspace_path: "workspace", provider: "anthropic", concurrency: "30", batch_mode: "episode", batch_target_lines: "36", batch_max_chars: "1800", batch_min_lines: "12" },
122
- acceptsPositionals: false,
123
- };
124
- }
125
- if (k === "direct.validate") {
126
- return {
127
- flags: [],
128
- values: ["workspace_path", "script_path"],
129
- arrays: [],
130
- required: [],
131
- defaults: { workspace_path: "workspace" },
132
- acceptsPositionals: false,
133
- };
134
- }
135
- if (k === "direct.inspect") {
136
- return {
137
- flags: [],
138
- values: ["workspace_path", "target", "id", "episode"],
139
- arrays: [],
140
- required: ["target"],
141
- defaults: { workspace_path: "workspace" },
142
- acceptsPositionals: false,
143
- };
144
- }
145
- if (k === "direct.export") {
146
- return {
147
- flags: ["force"],
148
- values: ["workspace_path", "output_path", "project_group_no", "request_id"],
149
- arrays: [],
150
- required: [],
151
- defaults: { workspace_path: "workspace", output_path: "output" },
152
- acceptsPositionals: false,
153
- };
154
- }
155
- if (k === "script.inspect") {
156
- return {
157
- flags: [],
158
- values: ["script_path", "workspace_path", "project_group_no", "target", "id"],
159
- arrays: [],
160
- required: [],
161
- defaults: { workspace_path: "workspace", target: "summary" },
162
- acceptsPositionals: false,
163
- };
164
- }
165
- if (k === "script.validate") {
166
- return {
167
- flags: [],
168
- values: ["script_path", "workspace_path", "project_group_no"],
169
- arrays: [],
170
- required: [],
171
- defaults: { workspace_path: "workspace" },
172
- acceptsPositionals: false,
173
- };
174
- }
175
- if (k === "script.patch") {
176
- return {
177
- flags: [],
178
- values: ["script_path", "workspace_path", "project_group_no", "request_id", "patch"],
179
- arrays: [],
180
- required: ["patch"],
181
- defaults: { workspace_path: "workspace" },
182
- acceptsPositionals: false,
183
- };
184
- }
185
- if (k.startsWith("script.state.")) {
186
- const sub = command[2];
187
- const values = ["script_path", "workspace_path", "project_group_no", "request_id"];
188
- const required = [];
189
- const defaults = { workspace_path: "workspace" };
190
- if (sub === "add" || sub === "rename") {
191
- values.push("name");
192
- required.push("name");
193
- }
194
- if (sub === "describe") {
195
- values.push("description");
196
- required.push("description");
197
- }
198
- if (sub === "add") {
199
- values.push("description", "state_id");
200
- defaults["description"] = "";
201
- }
202
- if (sub === "delete-apply") {
203
- values.push("strategy", "replacement");
204
- required.push("strategy");
205
- }
206
- return { flags: [], values, arrays: [], required, defaults, acceptsPositionals: true };
207
- }
208
- if (k.startsWith("script.context.")) {
209
- const sub = command[2];
210
- const values = ["script_path", "workspace_path", "project_group_no", "request_id"];
211
- const required = [];
212
- if (sub === "set") {
213
- values.push("state");
214
- required.push("state");
215
- }
216
- return {
217
- flags: [],
218
- values,
219
- arrays: [],
220
- required,
221
- defaults: { workspace_path: "workspace" },
222
- acceptsPositionals: true,
223
- };
224
- }
225
- if (k.startsWith("script.action.")) {
226
- const sub = command[2];
227
- const values = ["script_path", "workspace_path", "project_group_no", "request_id"];
228
- const required = [];
229
- const defaults = { workspace_path: "workspace" };
230
- if (sub === "state-change") {
231
- values.push("to", "from", "effective");
232
- required.push("to");
233
- defaults["effective"] = "after";
234
- }
235
- if (sub === "transition-set") {
236
- values.push("process", "contrast");
237
- required.push("process", "contrast");
238
- }
239
- return { flags: [], values, arrays: [], required, defaults, acceptsPositionals: true };
240
- }
241
- if (k === "script.speaker.add") {
242
- return {
243
- flags: [],
244
- values: ["script_path", "workspace_path", "project_group_no", "request_id", "kind", "name", "source_id", "voice_desc", "speaker_id"],
245
- arrays: [],
246
- required: ["kind", "name"],
247
- defaults: { workspace_path: "workspace", voice_desc: "" },
248
- acceptsPositionals: true,
249
- };
250
- }
251
- if (k === "script.dialogue.speakers") {
252
- return {
253
- flags: [],
254
- values: ["script_path", "workspace_path", "project_group_no", "request_id", "delivery"],
255
- arrays: ["speaker"],
256
- required: [],
257
- defaults: { workspace_path: "workspace", delivery: "simultaneous" },
258
- acceptsPositionals: true,
259
- };
63
+ function longestKnownPrefix(path) {
64
+ for (let len = path.length; len > 0; len--) {
65
+ if (KNOWN_COMMANDS.has(path.slice(0, len).join(".")))
66
+ return len;
260
67
  }
261
- if (k === "script.dialogue.overlap") {
262
- return {
263
- flags: [],
264
- values: ["script_path", "workspace_path", "project_group_no", "request_id"],
265
- arrays: ["line"],
266
- required: [],
267
- defaults: { workspace_path: "workspace" },
268
- acceptsPositionals: true,
269
- };
68
+ return 0;
69
+ }
70
+ function commandPath(cmd) {
71
+ const parts = [];
72
+ let c = cmd;
73
+ while (c) {
74
+ const name = c.name();
75
+ if (!name || name === "scriptctl")
76
+ break;
77
+ parts.unshift(name);
78
+ c = c.parent;
270
79
  }
271
- return {
272
- flags: [],
273
- values: ["script_path", "workspace_path", "project_group_no", "request_id"],
274
- arrays: [],
275
- required: [],
276
- defaults: { workspace_path: "workspace" },
277
- acceptsPositionals: true,
80
+ return parts;
81
+ }
82
+ function camelToSnake(s) {
83
+ return s.replace(/([A-Z])/g, "_$1").toLowerCase();
84
+ }
85
+ function normalizeOpts(opts) {
86
+ const out = {};
87
+ for (const [k, v] of Object.entries(opts))
88
+ out[camelToSnake(k)] = v;
89
+ return out;
90
+ }
91
+ function dispatch(handler, state) {
92
+ return async (...args) => {
93
+ const cmd = args[args.length - 1];
94
+ const positionals = args.slice(0, -2).filter((p) => p !== undefined);
95
+ const normalized = normalizeOpts(cmd.opts());
96
+ if (positionals.length > 0)
97
+ normalized._args = positionals;
98
+ const [report, exitCode] = await handler(normalized);
99
+ emit(report, exitCode, state.jsonMode);
100
+ state.exitCode = exitCode;
278
101
  };
279
102
  }
280
- function resolveCommand(argv) {
281
- if (argv.length === 0)
282
- return null;
283
- if (argv[0] === "doctor")
284
- return { command: ["doctor"], rest: argv.slice(1) };
285
- if (argv[0] !== "direct" && argv[0] !== "script")
286
- return null;
287
- const group = argv[0];
288
- if (argv.length < 2)
289
- return { command: [group], rest: [] };
290
- if (argv[1] === "--help" || argv[1] === "-h")
291
- return { command: [group], rest: [] };
292
- if (group === "script" && SCRIPT_NESTED_GROUPS.has(argv[1])) {
293
- if (argv.length < 3 || argv[2] === "--help" || argv[2] === "-h") {
294
- return { command: ["script", argv[1]], rest: [] };
295
- }
296
- return { command: ["script", argv[1], argv[2]], rest: argv.slice(3) };
297
- }
298
- return { command: [group, argv[1]], rest: argv.slice(2) };
103
+ function showGroupHelp(commandPathParts, state) {
104
+ return () => {
105
+ process.stdout.write(getHelp(commandPathParts) ?? getHelp([]) ?? "");
106
+ state.exitCode = EXIT_OK;
107
+ };
299
108
  }
300
- function unknownCommandError(argv, jsonMode) {
301
- const received = argv.length > 0 ? argv[0] : "<empty>";
302
- return emitError(new CliError("USAGE ERROR: Unknown command group", "Unknown command group.", {
303
- exitCode: EXIT_USAGE,
304
- required: ["doctor, direct, or script"],
305
- received: [received],
306
- nextSteps: ["Run: scriptctl --help"],
307
- }), jsonMode);
109
+ const appendStr = (value, prev) => [...prev, value];
110
+ function withScriptOpts(cmd) {
111
+ return cmd
112
+ .option("--script-path <path>")
113
+ .option("--workspace-path <path>", "", "workspace")
114
+ .option("--project-group-no <no>")
115
+ .option("--request-id <id>");
116
+ }
117
+ function buildProgram(state) {
118
+ const program = new Command("scriptctl")
119
+ .exitOverride()
120
+ .allowExcessArguments(false)
121
+ .allowUnknownOption(false)
122
+ .configureOutput({
123
+ writeOut: (str) => process.stdout.write(str),
124
+ writeErr: () => {
125
+ /* swallowed; we emit our own envelope */
126
+ },
127
+ })
128
+ .configureHelp({
129
+ formatHelp: (cmd) => getHelp(commandPath(cmd)) ?? getHelp([]) ?? "",
130
+ });
131
+ program.command("doctor").action(dispatch((o) => commandDoctor(o), state));
132
+ // ============================== direct ==============================
133
+ const direct = program.command("direct").action(showGroupHelp(["direct"], state));
134
+ direct.command("init")
135
+ .requiredOption("--source-path <path>")
136
+ .option("--workspace-path <path>", "", "workspace")
137
+ .option("--provider <name>", "", "anthropic")
138
+ .option("--model <model>")
139
+ .option("--concurrency <n>", "", "30")
140
+ .option("--batch-mode <mode>", "", "episode")
141
+ .option("--batch-target-lines <n>", "", "36")
142
+ .option("--batch-max-chars <n>", "", "1800")
143
+ .option("--batch-min-lines <n>", "", "12")
144
+ .action(dispatch(commandInit, state));
145
+ direct.command("validate")
146
+ .option("--workspace-path <path>", "", "workspace")
147
+ .option("--script-path <path>")
148
+ .action(dispatch((o) => commandValidate(o), state));
149
+ direct.command("inspect")
150
+ .requiredOption("--target <target>")
151
+ .option("--workspace-path <path>", "", "workspace")
152
+ .option("--id <id>")
153
+ .option("--episode <e>")
154
+ .action(dispatch((o) => commandInspect(o), state));
155
+ direct.command("export")
156
+ .option("--workspace-path <path>", "", "workspace")
157
+ .option("--output-path <path>", "", "output")
158
+ .option("--project-group-no <no>")
159
+ .option("--request-id <id>")
160
+ .option("--force")
161
+ .action(dispatch(commandExport, state));
162
+ // ============================== script ==============================
163
+ const script = program.command("script").action(showGroupHelp(["script"], state));
164
+ withScriptOpts(script.command("inspect"))
165
+ .option("--target <target>", "", "summary")
166
+ .option("--id <id>")
167
+ .option("--min-chars <n>")
168
+ .option("--max-chars <n>")
169
+ .action(dispatch(commandScriptInspect, state));
170
+ withScriptOpts(script.command("validate")).action(dispatch(commandScriptValidate, state));
171
+ withScriptOpts(script.command("patch"))
172
+ .requiredOption("--patch <file>")
173
+ .action(dispatch(commandScriptPatch, state));
174
+ // ------- script state -------
175
+ const scriptState = script.command("state").action(showGroupHelp(["script", "state"], state));
176
+ withScriptOpts(scriptState.command("add <target>"))
177
+ .requiredOption("--name <name>")
178
+ .option("--description <text>", "", "")
179
+ .option("--state-id <id>")
180
+ .action(dispatch((o) => commandScriptState(o, "add"), state));
181
+ withScriptOpts(scriptState.command("rename <target>"))
182
+ .requiredOption("--name <name>")
183
+ .action(dispatch((o) => commandScriptState(o, "rename"), state));
184
+ withScriptOpts(scriptState.command("describe <target>"))
185
+ .requiredOption("--description <text>")
186
+ .action(dispatch((o) => commandScriptState(o, "describe"), state));
187
+ withScriptOpts(scriptState.command("refs <target>"))
188
+ .action(dispatch((o) => commandScriptState(o, "refs"), state));
189
+ withScriptOpts(scriptState.command("delete-plan <target>"))
190
+ .action(dispatch((o) => commandScriptState(o, "delete-plan"), state));
191
+ withScriptOpts(scriptState.command("delete-apply <target>"))
192
+ .requiredOption("--strategy <strategy>")
193
+ .option("--replacement <id>")
194
+ .action(dispatch((o) => commandScriptState(o, "delete-apply"), state));
195
+ // ------- script context -------
196
+ const scriptContext = script.command("context").action(showGroupHelp(["script", "context"], state));
197
+ withScriptOpts(scriptContext.command("set <at> <target>"))
198
+ .requiredOption("--state <id>")
199
+ .action(dispatch((o) => commandScriptContext(o, "set"), state));
200
+ withScriptOpts(scriptContext.command("clear <at> <target>"))
201
+ .action(dispatch((o) => commandScriptContext(o, "clear"), state));
202
+ // ------- script action -------
203
+ const scriptAction = script.command("action").action(showGroupHelp(["script", "action"], state));
204
+ withScriptOpts(scriptAction.command("state-change <at> <target>"))
205
+ .requiredOption("--to <id>")
206
+ .option("--from <id>")
207
+ .option("--effective <when>", "", "after")
208
+ .action(dispatch((o) => commandScriptAction(o, "state-change"), state));
209
+ withScriptOpts(scriptAction.command("state-remove <at> <target>"))
210
+ .action(dispatch((o) => commandScriptAction(o, "state-remove"), state));
211
+ withScriptOpts(scriptAction.command("transition-set <at> <target>"))
212
+ .requiredOption("--process <text>")
213
+ .requiredOption("--contrast <text>")
214
+ .action(dispatch((o) => commandScriptAction(o, "transition-set"), state));
215
+ withScriptOpts(scriptAction.command("transition-clear <at> <target>"))
216
+ .action(dispatch((o) => commandScriptAction(o, "transition-clear"), state));
217
+ // ------- script speaker -------
218
+ const scriptSpeaker = script.command("speaker").action(showGroupHelp(["script", "speaker"], state));
219
+ withScriptOpts(scriptSpeaker.command("add"))
220
+ .requiredOption("--kind <kind>")
221
+ .requiredOption("--name <name>")
222
+ .option("--source-id <id>")
223
+ .option("--voice-desc <text>", "", "")
224
+ .option("--speaker-id <id>")
225
+ .action(dispatch((o) => commandScriptSpeaker(o, "add"), state));
226
+ // ------- script dialogue -------
227
+ const scriptDialogue = script.command("dialogue").action(showGroupHelp(["script", "dialogue"], state));
228
+ withScriptOpts(scriptDialogue.command("speakers <at>"))
229
+ .option("--speaker <id>", "", appendStr, [])
230
+ .option("--delivery <mode>", "", "simultaneous")
231
+ .action(dispatch((o) => commandScriptDialogue(o, "speakers"), state));
232
+ withScriptOpts(scriptDialogue.command("overlap <at>"))
233
+ .option("--line <spec>", "", appendStr, [])
234
+ .action(dispatch((o) => commandScriptDialogue(o, "overlap"), state));
235
+ // ============================== episode ==============================
236
+ const episode = program.command("episode").action(showGroupHelp(["episode"], state));
237
+ episode.command("init")
238
+ .option("--workspace-path <path>", "", "workspace")
239
+ .option("--force")
240
+ .action(dispatch((o) => commandEpisodeInit(o), state));
241
+ episode.command("draft <n>")
242
+ .option("--workspace-path <path>", "", "workspace")
243
+ .option("--prompt-template <path>")
244
+ .option("--outline <path>")
245
+ .option("--ref <spec>", "", appendStr, [])
246
+ .option("--from-md <path>")
247
+ .option("--provider <name>")
248
+ .option("--model <model>")
249
+ .option("--max-tokens <n>")
250
+ .option("--regen")
251
+ .option("--resume")
252
+ .action(dispatch((o) => commandEpisodeDraft(o), state));
253
+ episode.command("batch")
254
+ .requiredOption("--range <a-b>")
255
+ .option("--workspace-path <path>", "", "workspace")
256
+ .option("--prompt-template <path>")
257
+ .option("--outline <path>")
258
+ .option("--ref <spec>", "", appendStr, [])
259
+ .option("--provider <name>")
260
+ .option("--model <model>")
261
+ .option("--max-tokens <n>")
262
+ .action(dispatch((o) => commandEpisodeBatch(o), state));
263
+ episode.command("workspace")
264
+ .option("--workspace-path <path>", "", "workspace")
265
+ .action(dispatch((o) => commandEpisodeWorkspace(o), state));
266
+ episode.command("merge")
267
+ .option("--workspace-path <path>", "", "workspace")
268
+ .option("--meta-path <path>")
269
+ .option("--from <n>")
270
+ .option("--to <n>")
271
+ .option("--force")
272
+ .action(dispatch((o) => commandEpisodeMerge(o), state));
273
+ episode.command("push")
274
+ .option("--workspace-path <path>", "", "workspace")
275
+ .option("--project-group-no <no>")
276
+ .option("--request-id <id>")
277
+ .action(dispatch((o) => commandEpisodePush(o), state));
278
+ episode.command("spec")
279
+ .action(dispatch((o) => commandEpisodeSpec(o), state));
280
+ return program;
308
281
  }
309
- const TOP_LEVEL_GROUPS_WITHOUT_SUB = new Set([
310
- "direct",
311
- "script",
312
- "script.state",
313
- "script.context",
314
- "script.action",
315
- "script.speaker",
316
- "script.dialogue",
317
- ]);
318
282
  export async function main(argv = null) {
319
283
  loadLocalEnv();
320
- const argsAll = argv ?? process.argv.slice(2);
321
- let jsonMode = argsAll.includes("--json");
322
- const args = argsAll.filter((a) => a !== "--json");
323
- if (args.length === 0 || (args.length === 1 && (args[0] === "--help" || args[0] === "-h"))) {
324
- process.stdout.write(getHelp([]) ?? "");
325
- return EXIT_OK;
326
- }
327
- const resolved = resolveCommand(args);
328
- if (resolved === null)
329
- return unknownCommandError(args, jsonMode);
330
- const { command, rest } = resolved;
331
- const cmdKey = key(command);
332
- if (TOP_LEVEL_GROUPS_WITHOUT_SUB.has(cmdKey)) {
333
- if (rest.length === 0 || rest[0] === "--help" || rest[0] === "-h") {
334
- process.stdout.write(getHelp(command) ?? "");
284
+ let args = argv ?? process.argv.slice(2);
285
+ const state = { jsonMode: args.includes("--json"), exitCode: EXIT_OK };
286
+ args = args.filter((a) => a !== "--json");
287
+ const wantsHelp = args.includes("--help") || args.includes("-h");
288
+ const cmdPath = extractLeadingPath(args);
289
+ if (cmdPath.length === 0) {
290
+ if (args.length === 0 || wantsHelp) {
291
+ process.stdout.write(getHelp([]) ?? "");
335
292
  return EXIT_OK;
336
293
  }
294
+ return emitError(new CliError("USAGE ERROR: Unknown command", "Unknown command.", {
295
+ exitCode: EXIT_USAGE,
296
+ required: ["valid scriptctl command"],
297
+ received: [args.join(" ") || "<empty>"],
298
+ nextSteps: ["Run: scriptctl --help"],
299
+ }), state.jsonMode);
337
300
  }
338
- if (cmdKey === "doctor") {
339
- if (rest.length > 0 && (rest[0] === "--help" || rest[0] === "-h")) {
340
- process.stdout.write(getHelp(["doctor"]) ?? "");
341
- return EXIT_OK;
342
- }
343
- const [report, exitCode] = commandDoctor({});
344
- emit(report, exitCode, jsonMode);
345
- return exitCode;
346
- }
347
- // Validate command is recognized
348
- const helpExists = getHelp(command) !== undefined;
349
- if (!helpExists && !NESTED_COMMANDS.has(cmdKey)) {
350
- const required = command[0] === "direct"
351
- ? ["init, validate, inspect, export"]
352
- : ["inspect, validate, patch, state, context, action, speaker, dialogue"];
353
- return emitError(new CliError(`USAGE ERROR: Unknown ${command[0]} command`, "Unknown command.", {
301
+ const knownLen = longestKnownPrefix(cmdPath);
302
+ if (knownLen < cmdPath.length) {
303
+ return emitError(new CliError("USAGE ERROR: Unknown command", "Unknown command.", {
354
304
  exitCode: EXIT_USAGE,
355
- required,
356
- received: [command.join(" ")],
357
- nextSteps: [`Run: scriptctl ${command[0]} --help`],
358
- }), jsonMode);
305
+ required: ["valid scriptctl command"],
306
+ received: [cmdPath.join(" ")],
307
+ nextSteps: ["Run: scriptctl --help"],
308
+ }), state.jsonMode);
359
309
  }
360
- if (rest.includes("--help") || rest.includes("-h")) {
361
- let helpText = getHelp(command);
362
- if (helpText === undefined && command.length >= 2) {
363
- helpText = getHelp(command.slice(0, 2));
364
- }
365
- if (helpText === undefined)
366
- helpText = getHelp([]) ?? "";
367
- process.stdout.write(helpText);
310
+ if (wantsHelp) {
311
+ process.stdout.write(getHelp(cmdPath) ?? getHelp([]) ?? "");
368
312
  return EXIT_OK;
369
313
  }
314
+ const program = buildProgram(state);
370
315
  try {
371
- const parsed = parseOptions(command, rest);
372
- if (parsed.jsonMode)
373
- jsonMode = true;
374
- const handler = pickHandler(command);
375
- if (!handler) {
376
- throw new CliError("USAGE ERROR: Handler missing", "Handler missing.", {
316
+ await program.parseAsync(args, { from: "user" });
317
+ return state.exitCode;
318
+ }
319
+ catch (err) {
320
+ if (err instanceof CommanderError) {
321
+ const code = err.code;
322
+ if (code === "commander.help" ||
323
+ code === "commander.helpDisplayed" ||
324
+ code === "commander.version") {
325
+ return EXIT_OK;
326
+ }
327
+ const message = err.message || "Invalid arguments.";
328
+ return emitError(new CliError("USAGE ERROR: Invalid arguments", "Invalid arguments.", {
377
329
  exitCode: EXIT_USAGE,
378
- required: ["supported command"],
379
- received: [command.join(" ")],
380
- nextSteps: ["Run scriptctl --help"],
381
- });
330
+ required: ["Run: scriptctl --help"],
331
+ received: [message],
332
+ nextSteps: ["Run: scriptctl --help"],
333
+ }), state.jsonMode);
382
334
  }
383
- const result = await handler(parsed.opts);
384
- const [report, exitCode] = result;
385
- emit(report, exitCode, jsonMode);
386
- return exitCode;
387
- }
388
- catch (exc) {
389
- if (exc instanceof CliError)
390
- return emitError(exc, jsonMode);
391
- const err = exc;
335
+ if (err instanceof CliError)
336
+ return emitError(err, state.jsonMode);
337
+ const e = err;
392
338
  return emitError(new CliError("RUNTIME FAILED: Unexpected error", "Unexpected error.", {
393
339
  exitCode: 70,
394
340
  required: ["successful command execution"],
395
- received: [err?.name ?? "Error"],
341
+ received: [e?.name ?? "Error"],
396
342
  nextSteps: ["Rerun with valid inputs or inspect workspace artifacts."],
397
- }), jsonMode);
398
- }
399
- }
400
- function pickHandler(command) {
401
- const k = key(command);
402
- if (k === "direct.init")
403
- return commandInit;
404
- if (k === "direct.validate")
405
- return (opts) => commandValidate(opts);
406
- if (k === "direct.inspect")
407
- return (opts) => commandInspect(opts);
408
- if (k === "direct.export")
409
- return commandExport;
410
- if (k === "script.inspect")
411
- return commandScriptInspect;
412
- if (k === "script.validate")
413
- return commandScriptValidate;
414
- if (k === "script.patch")
415
- return commandScriptPatch;
416
- if (command.length === 3 && command[0] === "script" && command[1] === "state") {
417
- return (opts) => commandScriptState(opts, command[2]);
418
- }
419
- if (command.length === 3 && command[0] === "script" && command[1] === "context") {
420
- return (opts) => commandScriptContext(opts, command[2]);
421
- }
422
- if (command.length === 3 && command[0] === "script" && command[1] === "action") {
423
- return (opts) => commandScriptAction(opts, command[2]);
424
- }
425
- if (command.length === 3 && command[0] === "script" && command[1] === "speaker") {
426
- return (opts) => commandScriptSpeaker(opts, command[2]);
427
- }
428
- if (command.length === 3 && command[0] === "script" && command[1] === "dialogue") {
429
- return (opts) => commandScriptDialogue(opts, command[2]);
343
+ }), state.jsonMode);
430
344
  }
431
- return null;
432
345
  }
433
346
  //# sourceMappingURL=cli.js.map