venice-video-harness 2.5.3 → 2.6.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/CHANGELOG.md +35 -0
- package/README.md +52 -0
- package/dist/cli.js +0 -0
- package/dist/mini-drama/cli.d.ts +3 -0
- package/dist/mini-drama/cli.d.ts.map +1 -1
- package/dist/mini-drama/cli.js +200 -3
- package/dist/mini-drama/cli.js.map +1 -1
- package/dist/mini-drama/video-generator.d.ts.map +1 -1
- package/dist/mini-drama/video-generator.js +101 -3
- package/dist/mini-drama/video-generator.js.map +1 -1
- package/dist/session/context.d.ts +25 -0
- package/dist/session/context.d.ts.map +1 -0
- package/dist/session/context.js +86 -0
- package/dist/session/context.js.map +1 -0
- package/dist/session/jobs.d.ts +43 -0
- package/dist/session/jobs.d.ts.map +1 -0
- package/dist/session/jobs.js +110 -0
- package/dist/session/jobs.js.map +1 -0
- package/dist/session/output-router.d.ts +13 -0
- package/dist/session/output-router.d.ts.map +1 -0
- package/dist/session/output-router.js +67 -0
- package/dist/session/output-router.js.map +1 -0
- package/dist/session/program-context.d.ts +7 -0
- package/dist/session/program-context.d.ts.map +1 -0
- package/dist/session/program-context.js +98 -0
- package/dist/session/program-context.js.map +1 -0
- package/dist/session/program-runtime.d.ts +39 -0
- package/dist/session/program-runtime.d.ts.map +1 -0
- package/dist/session/program-runtime.js +149 -0
- package/dist/session/program-runtime.js.map +1 -0
- package/dist/session/shell.d.ts +18 -0
- package/dist/session/shell.d.ts.map +1 -0
- package/dist/session/shell.js +465 -0
- package/dist/session/shell.js.map +1 -0
- package/dist/session/status.d.ts +33 -0
- package/dist/session/status.d.ts.map +1 -0
- package/dist/session/status.js +160 -0
- package/dist/session/status.js.map +1 -0
- package/dist/user-config.d.ts +10 -0
- package/dist/user-config.d.ts.map +1 -1
- package/dist/user-config.js.map +1 -1
- package/dist/venice/audio.d.ts.map +1 -1
- package/dist/venice/audio.js +30 -6
- package/dist/venice/audio.js.map +1 -1
- package/dist/venice/client.d.ts +0 -2
- package/dist/venice/client.d.ts.map +1 -1
- package/dist/venice/client.js +16 -8
- package/dist/venice/client.js.map +1 -1
- package/dist/venice/job-store.d.ts +39 -0
- package/dist/venice/job-store.d.ts.map +1 -0
- package/dist/venice/job-store.js +119 -0
- package/dist/venice/job-store.js.map +1 -0
- package/dist/venice/operation-context.d.ts +47 -0
- package/dist/venice/operation-context.d.ts.map +1 -0
- package/dist/venice/operation-context.js +83 -0
- package/dist/venice/operation-context.js.map +1 -0
- package/dist/venice/video.d.ts +15 -0
- package/dist/venice/video.d.ts.map +1 -1
- package/dist/venice/video.js +73 -17
- package/dist/venice/video.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.6.0 — 2026-07-31
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `venice-video shell` — a persistent interactive session. Commands run without
|
|
8
|
+
a process restart, so the Venice client's rate-limit pacing and caches stay
|
|
9
|
+
warm across a whole production instead of resetting on every invocation.
|
|
10
|
+
- A selected project and part. `use <project> [part]` makes `-p` and `-e`
|
|
11
|
+
optional on every command; `unuse` clears it. The selection is stored in user
|
|
12
|
+
config, so it applies to one-shot commands and survives restarts.
|
|
13
|
+
- Background commands: suffix any command with `&`, then `/jobs`,
|
|
14
|
+
`/jobs log <id>`, and `/jobs cancel <id>`. Output from a backgrounded render is
|
|
15
|
+
captured to its own buffer rather than sprayed over the prompt.
|
|
16
|
+
- `venice-video status` — reports where a project sits in the pipeline and which
|
|
17
|
+
command to run next.
|
|
18
|
+
- `venice-video queue` — lists Venice renders left in flight, with `prune` and
|
|
19
|
+
`clear` subcommands.
|
|
20
|
+
- Shell conveniences: tab completion for commands, flags, and project slugs; a
|
|
21
|
+
persistent history file; a context-aware prompt; `!<cmd>` passthrough to the
|
|
22
|
+
system shell.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **Interrupted renders no longer orphan paid work.** Each Venice `queue_id` is
|
|
27
|
+
now recorded to disk *before* polling starts. Re-running a command re-attaches
|
|
28
|
+
to the pending job instead of submitting and billing a second render. Previously
|
|
29
|
+
a Ctrl-C, crash, or dropped connection lost the id while Venice kept charging.
|
|
30
|
+
- `Ctrl-C` now cancels the in-flight operation through an `AbortSignal` threaded
|
|
31
|
+
into every Venice request, poll loop, and retry backoff, rather than only
|
|
32
|
+
killing the process between requests.
|
|
33
|
+
- Video polling gained an overall timeout and a consecutive-error ceiling, so a
|
|
34
|
+
wedged render fails instead of hanging indefinitely.
|
|
35
|
+
- Tests no longer inherit an ambient `VENICE_VIDEO_WORKSPACE`, which could
|
|
36
|
+
silently redirect the projects they create.
|
|
37
|
+
|
|
3
38
|
## 2.5.3 — 2026-07-31
|
|
4
39
|
|
|
5
40
|
- Workshop generation now writes a formatted, self-contained `WORKSHOP.html` alongside JSON and Markdown.
|
package/README.md
CHANGED
|
@@ -291,6 +291,58 @@ venice-video list-series
|
|
|
291
291
|
venice-video --help
|
|
292
292
|
```
|
|
293
293
|
|
|
294
|
+
### Interactive shell
|
|
295
|
+
|
|
296
|
+
Every command above also runs inside a persistent session:
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
venice-video shell
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
The shell keeps one warm process for the whole production, which changes three
|
|
303
|
+
things that matter over a long session:
|
|
304
|
+
|
|
305
|
+
- **A selected project and part.** `use <project> [part]` sets them once; after
|
|
306
|
+
that `-p` and `-e` are optional on every command and the prompt shows what you
|
|
307
|
+
are pointed at. `unuse` clears the selection. The selection persists across
|
|
308
|
+
shell restarts and applies to one-shot commands too.
|
|
309
|
+
- **Warm rate limiting and caches.** The Venice client's pacing state survives
|
|
310
|
+
between commands instead of resetting on every invocation, so back-to-back
|
|
311
|
+
generation stops tripping 429s.
|
|
312
|
+
- **Background commands.** Suffix any command with `&` to detach it, then keep
|
|
313
|
+
working. `/jobs` lists them with elapsed time and current progress detail,
|
|
314
|
+
`/jobs log <id>` replays captured output, `/jobs cancel <id>` aborts one.
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
venice-video my-film · ep 01 › storyboard-episode
|
|
318
|
+
venice-video my-film · ep 01 › generate-videos &
|
|
319
|
+
[1] started in the background. Check with /jobs.
|
|
320
|
+
venice-video my-film · ep 01 › /jobs
|
|
321
|
+
[1] running 4m12s generate-videos — shot 3/12 polling
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Session extras: `Tab` completes commands, flags, and project slugs; `↑`/`↓` walk
|
|
325
|
+
a persistent history file; `Ctrl-C` cancels the running command without killing
|
|
326
|
+
the session (`Ctrl-D` or `/exit` leaves); `/help`, `/status`, `/jobs`, `/cd`, and
|
|
327
|
+
`/pwd` are shell meta-commands; `!<cmd>` runs something in your system shell.
|
|
328
|
+
|
|
329
|
+
### Interrupted renders are resumable
|
|
330
|
+
|
|
331
|
+
The harness records each Venice `queue_id` to disk *before* it starts polling, so
|
|
332
|
+
a cancelled command, a crash, or a closed laptop no longer orphans a render you
|
|
333
|
+
have already paid for. Re-running the same command re-attaches to the pending job
|
|
334
|
+
and keeps polling it instead of submitting and billing a second one.
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
venice-video queue # renders Venice still has in flight
|
|
338
|
+
venice-video queue prune # forget records too old for Venice to still hold
|
|
339
|
+
venice-video queue clear <id> # forget one record (does not refund it)
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The shell reports stranded renders in its banner on startup. Note the split:
|
|
343
|
+
`queue` is Venice's side of the work (real money, survives restarts), while
|
|
344
|
+
`/jobs` is only the background commands of the current session.
|
|
345
|
+
|
|
294
346
|
For server environments, prefer `VENICE_API_KEY` instead of writing a user
|
|
295
347
|
configuration file. Credential precedence is environment variable, then stored
|
|
296
348
|
user configuration, then the repository `.env` compatibility path.
|
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/mini-drama/cli.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/mini-drama/cli.ts"],"names":[],"mappings":";AAEA,OAAO,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/mini-drama/cli.ts"],"names":[],"mappings":";AAEA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2HpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAu5G9B,OAAO,EAAE,OAAO,EAAE,CAAC"}
|
package/dist/mini-drama/cli.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
import 'dotenv/config';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { resolve, join, basename } from 'node:path';
|
|
5
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
5
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
6
6
|
import { mkdir, readFile, writeFile, copyFile } from 'node:fs/promises';
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
8
|
import { stdin } from 'node:process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
9
10
|
import { createSeries, saveSeries, loadSeries, listSeries, addCharacter, getCharacter, addEpisode, getEpisodeDir, getCharacterDir, getLocationDir, getLocation, addLocation, locationSlugify, saveEpisodeScript, loadEpisodeScript, } from '../series/manager.js';
|
|
10
11
|
import { FEMALE_BASE_TRAITS, MALE_BASE_TRAITS, DEFAULT_CHARACTER_CONSISTENCY_MODEL, DEFAULT_IMAGE_GENERATION_MODEL, DEFAULT_IMAGE_EDIT_MODEL, } from '../series/types.js';
|
|
11
12
|
import { VeniceClient } from '../venice/client.js';
|
|
@@ -17,6 +18,11 @@ import { getVeniceApiKey } from '../config.js';
|
|
|
17
18
|
import { promptChoice, promptText } from '../interactive.js';
|
|
18
19
|
import { getConfigPath, getDefaultSetupWorkspace, getWorkspaceDir, maskApiKey, readUserConfig, updateUserConfig, validateVeniceApiKey, } from '../user-config.js';
|
|
19
20
|
import { listVoices, filterVoices, auditionVoices } from '../venice/voices.js';
|
|
21
|
+
import { clearContext, MissingContextError, readContext, resolveProjectRef, setContext, } from '../session/context.js';
|
|
22
|
+
import { OperationAbortedError } from '../venice/operation-context.js';
|
|
23
|
+
import { applyContextDefaults } from '../session/program-context.js';
|
|
24
|
+
import { collectProjectStatus, formatProjectStatus } from '../session/status.js';
|
|
25
|
+
import { clearPendingJob, isStale, listPendingJobs, prunePendingJobs, } from '../venice/job-store.js';
|
|
20
26
|
import { generateDialogueForShots, generateSoundEffect, generateMusic, generateSeedAudio, DEFAULT_VENICE_MUSIC_MODEL, DEFAULT_VENICE_SEED_AUDIO_MODEL, } from '../venice/audio.js';
|
|
21
27
|
import { getMusicModel } from '../venice/models.js';
|
|
22
28
|
import { buildImagePrompt, buildCharacterReferencePromptParts } from './prompt-builder.js';
|
|
@@ -32,11 +38,21 @@ import { buildGenerationPlan, saveGenerationPlan } from './generation-planner.js
|
|
|
32
38
|
import { AUDIO_STRATEGY_CHOICES, VIDEO_FAMILY_CHOICES } from './choices.js';
|
|
33
39
|
import { getProjectLanguage } from '../series/project-language.js';
|
|
34
40
|
import { approveWorkshop, generateWorkshop, getWorkshopPath, inventoryReferencePath, loadWorkshop, saveWorkshop, } from './workshop.js';
|
|
41
|
+
// Read from package.json rather than a literal, which drifts on every release.
|
|
42
|
+
const packageVersion = (() => {
|
|
43
|
+
try {
|
|
44
|
+
const pkgPath = fileURLToPath(new URL('../../package.json', import.meta.url));
|
|
45
|
+
return JSON.parse(readFileSync(pkgPath, 'utf-8')).version ?? '0.0.0';
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return '0.0.0';
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
35
51
|
const program = new Command();
|
|
36
52
|
program
|
|
37
53
|
.name('venice-video')
|
|
38
54
|
.description('Standalone consistency-first video production with Venice AI')
|
|
39
|
-
.version(
|
|
55
|
+
.version(packageVersion)
|
|
40
56
|
.option('--workspace <dir>', 'Workspace containing Venice Video projects');
|
|
41
57
|
const VIDEO_FAMILIES = new Set(VIDEO_FAMILY_CHOICES.map(c => c.value));
|
|
42
58
|
function openInDefaultBrowser(path) {
|
|
@@ -50,6 +66,33 @@ function openInDefaultBrowser(path) {
|
|
|
50
66
|
const result = spawnSync(command.name, command.args, { stdio: 'ignore' });
|
|
51
67
|
return result.status === 0;
|
|
52
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* True when this file is the process entry point. The program is only parsed in
|
|
71
|
+
* that case, so `shell` (and tests) can import the same command tree without
|
|
72
|
+
* argv being consumed at import time.
|
|
73
|
+
*
|
|
74
|
+
* `bin` points straight at this file, so the comparison is against argv[1] with
|
|
75
|
+
* symlinks resolved. The basename fallback covers the tsx dev path (.ts entry
|
|
76
|
+
* for a .js module specifier) -- getting this wrong would leave the CLI silently
|
|
77
|
+
* doing nothing, so it errs toward "yes, run".
|
|
78
|
+
*/
|
|
79
|
+
function isMainModule(moduleUrl) {
|
|
80
|
+
const entry = process.argv[1];
|
|
81
|
+
if (!entry)
|
|
82
|
+
return true;
|
|
83
|
+
const canonical = (path) => {
|
|
84
|
+
try {
|
|
85
|
+
return realpathSync(path);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return resolve(path);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const stripExtension = (path) => basename(path).replace(/\.[cm]?[jt]s$/, '');
|
|
92
|
+
const modulePath = canonical(fileURLToPath(moduleUrl));
|
|
93
|
+
const entryPath = canonical(entry);
|
|
94
|
+
return modulePath === entryPath || stripExtension(modulePath) === stripExtension(entryPath);
|
|
95
|
+
}
|
|
53
96
|
function runCommand(command, args) {
|
|
54
97
|
const result = spawnSync(command, args, {
|
|
55
98
|
encoding: 'utf-8',
|
|
@@ -3062,7 +3105,161 @@ program
|
|
|
3062
3105
|
await program.parseAsync(['', '', 'assemble-episode', '-p', opts.project, '-e', String(opts.episode)]);
|
|
3063
3106
|
console.log('\n=== Production Complete ===');
|
|
3064
3107
|
});
|
|
3065
|
-
|
|
3108
|
+
// ── session: use / status / queue / shell ─────────────────────────────
|
|
3109
|
+
program
|
|
3110
|
+
.command('use')
|
|
3111
|
+
.description('Select the project (and episode) that -p / -e default to')
|
|
3112
|
+
.argument('[project]', 'Project slug, name, or directory')
|
|
3113
|
+
.option('-e, --episode <number>', 'Episode number to select', parseInt)
|
|
3114
|
+
.option('--clear', 'Clear the current selection', false)
|
|
3115
|
+
.action(async (projectRef, opts) => {
|
|
3116
|
+
if (opts.clear) {
|
|
3117
|
+
await clearContext();
|
|
3118
|
+
console.log('Selection cleared. Commands now require -p / -e again.');
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
if (!projectRef && opts.episode === undefined) {
|
|
3122
|
+
const context = await readContext();
|
|
3123
|
+
if (!context.project) {
|
|
3124
|
+
console.log('Nothing selected. Usage: venice-video use <project> [-e <episode>]');
|
|
3125
|
+
const workspace = await getWorkspaceDir(program.opts().workspace);
|
|
3126
|
+
const available = await listSeries(workspace);
|
|
3127
|
+
if (available.length > 0) {
|
|
3128
|
+
console.log('\nAvailable projects:');
|
|
3129
|
+
for (const entry of available)
|
|
3130
|
+
console.log(` ${entry.slug.padEnd(28)} ${entry.name}`);
|
|
3131
|
+
}
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
console.log(`Project: ${context.project}`);
|
|
3135
|
+
if (context.episode !== undefined)
|
|
3136
|
+
console.log(`Episode: ${context.episode}`);
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
3139
|
+
const patch = {};
|
|
3140
|
+
if (projectRef) {
|
|
3141
|
+
const projectDir = await resolveProjectRef(projectRef, program.opts().workspace);
|
|
3142
|
+
const series = await loadSeries(projectDir);
|
|
3143
|
+
if (!series) {
|
|
3144
|
+
console.error(`No series.json found at ${projectDir}.`);
|
|
3145
|
+
console.error('Run `venice-video list-series` to see what is available.');
|
|
3146
|
+
process.exit(1);
|
|
3147
|
+
}
|
|
3148
|
+
patch.project = projectDir;
|
|
3149
|
+
}
|
|
3150
|
+
if (opts.episode !== undefined)
|
|
3151
|
+
patch.episode = opts.episode;
|
|
3152
|
+
const next = await setContext(patch);
|
|
3153
|
+
console.log(`Selected ${next.project}${next.episode !== undefined ? ` · episode ${next.episode}` : ''}`);
|
|
3154
|
+
});
|
|
3155
|
+
program
|
|
3156
|
+
.command('unuse')
|
|
3157
|
+
.description('Clear the selected project and episode')
|
|
3158
|
+
.action(async () => {
|
|
3159
|
+
await clearContext();
|
|
3160
|
+
console.log('Selection cleared.');
|
|
3161
|
+
});
|
|
3162
|
+
program
|
|
3163
|
+
.command('status')
|
|
3164
|
+
.description('Show pipeline state for a project and the next command to run')
|
|
3165
|
+
.option('-p, --project <dir>', 'Project directory (defaults to the selection)')
|
|
3166
|
+
.action(async (opts) => {
|
|
3167
|
+
const context = await readContext();
|
|
3168
|
+
const projectRef = opts.project ?? context.project;
|
|
3169
|
+
if (!projectRef) {
|
|
3170
|
+
console.log('No project selected. Run `venice-video use <project>` or pass -p <dir>.');
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
const projectDir = await resolveProjectRef(projectRef, program.opts().workspace);
|
|
3174
|
+
const status = await collectProjectStatus(projectDir);
|
|
3175
|
+
if (!status) {
|
|
3176
|
+
console.error(`No series.json found at ${projectDir}.`);
|
|
3177
|
+
process.exit(1);
|
|
3178
|
+
return;
|
|
3179
|
+
}
|
|
3180
|
+
console.log(formatProjectStatus(status, context.episode));
|
|
3181
|
+
});
|
|
3182
|
+
program
|
|
3183
|
+
.command('queue')
|
|
3184
|
+
.description('List Venice renders left in flight by an interrupted run')
|
|
3185
|
+
.argument('[action]', 'clear to drop a recorded job, prune to drop stale ones')
|
|
3186
|
+
.argument('[target]', 'Output path or queue id to clear')
|
|
3187
|
+
.action(async (action, target) => {
|
|
3188
|
+
if (action === 'prune') {
|
|
3189
|
+
const removed = await prunePendingJobs();
|
|
3190
|
+
console.log(`Pruned ${removed} stale job record(s).`);
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
if (action === 'clear') {
|
|
3194
|
+
if (!target) {
|
|
3195
|
+
console.error('Usage: venice-video queue clear <output-path|queue-id>');
|
|
3196
|
+
process.exit(1);
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3199
|
+
const pending = await listPendingJobs();
|
|
3200
|
+
const match = pending.find(job => job.queueId === target)
|
|
3201
|
+
?? pending.find(job => job.outputPath === resolve(target))
|
|
3202
|
+
?? pending.find(job => job.outputPath.endsWith(target));
|
|
3203
|
+
if (!match) {
|
|
3204
|
+
console.error(`No recorded job matching "${target}".`);
|
|
3205
|
+
process.exit(1);
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
await clearPendingJob(match.outputPath);
|
|
3209
|
+
console.log(`Dropped ${match.queueId} (${match.outputPath}).`);
|
|
3210
|
+
console.log('Note: this only forgets the id locally. Venice already charged for the render.');
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
const pending = await listPendingJobs();
|
|
3214
|
+
if (pending.length === 0) {
|
|
3215
|
+
console.log('No Venice jobs are recorded as in flight.');
|
|
3216
|
+
return;
|
|
3217
|
+
}
|
|
3218
|
+
console.log(`${pending.length} job(s) recorded as in flight:\n`);
|
|
3219
|
+
for (const job of pending) {
|
|
3220
|
+
const ageMin = Math.round((Date.now() - Date.parse(job.updatedAt)) / 60_000);
|
|
3221
|
+
console.log(` ${job.kind} ${job.model}`);
|
|
3222
|
+
console.log(` queue id ${job.queueId}`);
|
|
3223
|
+
console.log(` output ${job.outputPath}`);
|
|
3224
|
+
console.log(` last seen ${ageMin} min ago${isStale(job) ? ' (stale — Venice has likely dropped it)' : ''}`);
|
|
3225
|
+
if (job.prompt)
|
|
3226
|
+
console.log(` prompt ${job.prompt.slice(0, 70)}…`);
|
|
3227
|
+
console.log('');
|
|
3228
|
+
}
|
|
3229
|
+
console.log('Re-run the command that produced these to re-attach instead of re-billing.');
|
|
3230
|
+
});
|
|
3231
|
+
program
|
|
3232
|
+
.command('shell')
|
|
3233
|
+
.description('Start an interactive session that keeps the selected project and runs jobs in the background')
|
|
3234
|
+
.action(async () => {
|
|
3235
|
+
// Imported lazily so the shell (and its readline/job machinery) costs
|
|
3236
|
+
// nothing for one-shot invocations, and to avoid an import cycle back
|
|
3237
|
+
// into this module.
|
|
3238
|
+
const { startShell } = await import('../session/shell.js');
|
|
3239
|
+
await startShell(program);
|
|
3240
|
+
});
|
|
3241
|
+
applyContextDefaults(program);
|
|
3242
|
+
// Only parse when executed directly. The shell imports this module to reuse the
|
|
3243
|
+
// same command tree, and must not have argv consumed at import time.
|
|
3244
|
+
if (isMainModule(import.meta.url)) {
|
|
3245
|
+
try {
|
|
3246
|
+
await program.parseAsync();
|
|
3247
|
+
}
|
|
3248
|
+
catch (error) {
|
|
3249
|
+
// A missing project/episode selection is ordinary user error, not a crash —
|
|
3250
|
+
// report it the way Commander reports a missing required option.
|
|
3251
|
+
if (error instanceof MissingContextError) {
|
|
3252
|
+
console.error(`error: ${error.message}`);
|
|
3253
|
+
process.exit(1);
|
|
3254
|
+
}
|
|
3255
|
+
if (error instanceof OperationAbortedError) {
|
|
3256
|
+
console.error('Cancelled. Any in-flight Venice job is recorded — re-run to re-attach.');
|
|
3257
|
+
process.exit(130);
|
|
3258
|
+
}
|
|
3259
|
+
throw error;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
export { program };
|
|
3066
3263
|
// ── Helpers ───────────────────────────────────────────────────────────
|
|
3067
3264
|
function generateCompareHtml(styles, seriesName) {
|
|
3068
3265
|
const cards = styles.map(s => `
|