vigthoria-cli 1.13.25 → 1.13.29

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 (54) hide show
  1. package/dist/commands/chat.js +83 -27
  2. package/dist/commands/config.js +4 -4
  3. package/dist/commands/fork.d.ts +3 -2
  4. package/dist/commands/fork.js +124 -123
  5. package/dist/commands/game.d.ts +8 -0
  6. package/dist/commands/game.js +118 -10
  7. package/dist/commands/history.d.ts +0 -1
  8. package/dist/commands/history.js +8 -22
  9. package/dist/commands/hub.d.ts +20 -0
  10. package/dist/commands/hub.js +17 -3
  11. package/dist/commands/preview.d.ts +1 -0
  12. package/dist/commands/preview.js +36 -15
  13. package/dist/commands/product-run-registration.js +1 -1
  14. package/dist/commands/replay.d.ts +0 -1
  15. package/dist/commands/replay.js +10 -19
  16. package/dist/commands/repo.d.ts +35 -2
  17. package/dist/commands/repo.js +76 -34
  18. package/dist/commands/update-registration.js +4 -3
  19. package/dist/commands/workflow.d.ts +4 -0
  20. package/dist/commands/workflow.js +27 -0
  21. package/dist/index.js +6 -4
  22. package/dist/utils/agentRunOutcome.d.ts +7 -0
  23. package/dist/utils/agentRunOutcome.js +13 -0
  24. package/dist/utils/api.d.ts +20 -5
  25. package/dist/utils/api.js +440 -54
  26. package/dist/utils/command-policy.js +1 -1
  27. package/dist/utils/config.d.ts +2 -0
  28. package/dist/utils/config.js +8 -3
  29. package/dist/utils/frontend-preview-service.d.ts +1 -0
  30. package/dist/utils/frontend-preview-service.js +54 -5
  31. package/dist/utils/model-governance.js +23 -14
  32. package/dist/utils/model-transport-service.js +1 -1
  33. package/dist/utils/network-policy.js +16 -4
  34. package/dist/utils/operator-client.js +23 -4
  35. package/dist/utils/post-write-validator.js +7 -3
  36. package/dist/utils/preview-screenshot-adapter.d.ts +19 -3
  37. package/dist/utils/preview-screenshot-adapter.js +289 -28
  38. package/dist/utils/runtime-capability.d.ts +7 -0
  39. package/dist/utils/runtime-capability.js +11 -0
  40. package/dist/utils/runtime-temp.d.ts +6 -2
  41. package/dist/utils/runtime-temp.js +143 -27
  42. package/dist/utils/tools.js +1 -1
  43. package/dist/utils/v3-stream-events.d.ts +8 -0
  44. package/dist/utils/v3-stream-events.js +70 -0
  45. package/dist/utils/v3-workspace-service.d.ts +1 -0
  46. package/dist/utils/v3-workspace-service.js +38 -1
  47. package/dist/utils/vigflow-client.d.ts +9 -0
  48. package/dist/utils/vigflow-client.js +48 -2
  49. package/dist/utils/workspace-reference.d.ts +8 -0
  50. package/dist/utils/workspace-reference.js +21 -0
  51. package/package.json +4 -6
  52. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +2 -2
  53. package/scripts/release/validate-live-service-gates.sh +3 -3
  54. package/scripts/release/validate-no-go-gates.sh +2 -0
@@ -39,14 +39,121 @@ export function gameProcessInvocation(pm, args, platform = process.platform, com
39
39
  category: 'usage',
40
40
  });
41
41
  }
42
- const command = [executable, ...args].map((token) => `"${token}"`).join(' ');
42
+ // Every token is already restricted to an inert alphabet above. Passing the
43
+ // tokens without embedded quotes is important: on Windows, cmd.exe can hand
44
+ // the quote characters to the .cmd shim as part of argv[0], producing the
45
+ // observed literal `"npm.cmd"` launch failure.
46
+ const command = [executable, ...args].join(' ');
43
47
  return { executable: comSpec, args: ['/d', '/s', '/c', command] };
44
48
  }
45
- const run = (pm, args, cwd, timeoutMs = 10 * 60_000) => new Promise((resolve, reject) => { const invocation = gameProcessInvocation(pm, args); const child = spawn(invocation.executable, invocation.args, { cwd, stdio: 'inherit', windowsHide: true, env: safeChildProcessEnv() }); let interrupted = false; const onInterrupt = () => { interrupted = true; child.kill('SIGINT'); }; process.once('SIGINT', onInterrupt); const timer = timeoutMs > 0 ? setTimeout(() => { child.kill('SIGTERM'); reject(new CliCommandError(`${pm} command timed out after ${timeoutMs}ms`, { code: 'GAME_PROCESS_TIMEOUT' })); }, timeoutMs) : null; const finish = () => { if (timer)
46
- clearTimeout(timer); process.removeListener('SIGINT', onInterrupt); }; child.once('error', error => { finish(); reject(error); }); child.once('exit', (code, signal) => { finish(); if (interrupted)
47
- reject(new CliCommandError(`${pm} command cancelled by user`, { code: 'GAME_PROCESS_CANCELLED', category: 'cancelled' }));
48
- else
49
- code === 0 ? resolve() : reject(new Error(`${pm} exited with ${signal || code}`)); }); });
49
+ export function windowsProcessTreeTerminationInvocation(pid, systemRoot = process.env.SystemRoot || 'C:\\Windows') {
50
+ if (!Number.isSafeInteger(pid) || pid <= 0) {
51
+ throw new CliCommandError('Cannot terminate a game process without a valid owned PID.', {
52
+ code: 'GAME_PROCESS_ID_INVALID',
53
+ });
54
+ }
55
+ return {
56
+ executable: path.win32.join(systemRoot, 'System32', 'taskkill.exe'),
57
+ args: ['/PID', String(pid), '/T', '/F'],
58
+ };
59
+ }
60
+ async function terminateOwnedProcessTree(child, platform) {
61
+ const pid = child.pid;
62
+ if (!pid)
63
+ return;
64
+ if (platform === 'win32') {
65
+ const invocation = windowsProcessTreeTerminationInvocation(pid);
66
+ await new Promise((resolve) => {
67
+ const killer = spawn(invocation.executable, invocation.args, {
68
+ stdio: 'ignore',
69
+ windowsHide: true,
70
+ env: safeChildProcessEnv(),
71
+ });
72
+ const timer = setTimeout(() => { try {
73
+ killer.kill();
74
+ }
75
+ catch { /* exact helper only */ } resolve(); }, 5_000);
76
+ killer.once('error', () => { clearTimeout(timer); try {
77
+ child.kill();
78
+ }
79
+ catch { /* already gone */ } resolve(); });
80
+ killer.once('exit', () => { clearTimeout(timer); resolve(); });
81
+ });
82
+ return;
83
+ }
84
+ try {
85
+ process.kill(-pid, 'SIGTERM');
86
+ }
87
+ catch {
88
+ try {
89
+ child.kill('SIGTERM');
90
+ }
91
+ catch { /* already gone */ }
92
+ }
93
+ await new Promise((resolve) => setTimeout(resolve, 250));
94
+ try {
95
+ process.kill(-pid, 0);
96
+ process.kill(-pid, 'SIGKILL');
97
+ }
98
+ catch { /* process group exited */ }
99
+ }
100
+ export const runGameProcess = (pm, args, cwd, timeoutMs = 10 * 60_000, outputMode = 'inherit') => new Promise((resolve, reject) => {
101
+ const invocation = gameProcessInvocation(pm, args);
102
+ const platform = process.platform;
103
+ const child = spawn(invocation.executable, invocation.args, {
104
+ cwd,
105
+ stdio: outputMode === 'capture' ? ['ignore', 'pipe', 'pipe'] : 'inherit',
106
+ windowsHide: true,
107
+ env: safeChildProcessEnv(),
108
+ detached: platform !== 'win32',
109
+ });
110
+ let interrupted = false;
111
+ let timedOut = false;
112
+ let settled = false;
113
+ let stdout = '';
114
+ let stderr = '';
115
+ const appendBounded = (current, chunk) => `${current}${String(chunk)}`.slice(-64 * 1024);
116
+ if (outputMode === 'capture') {
117
+ child.stdout?.setEncoding('utf8');
118
+ child.stderr?.setEncoding('utf8');
119
+ child.stdout?.on('data', (chunk) => { stdout = appendBounded(stdout, chunk); });
120
+ child.stderr?.on('data', (chunk) => { stderr = appendBounded(stderr, chunk); });
121
+ }
122
+ const settle = (error) => {
123
+ if (settled)
124
+ return;
125
+ settled = true;
126
+ if (timer)
127
+ clearTimeout(timer);
128
+ process.removeListener('SIGINT', onInterrupt);
129
+ error ? reject(error) : resolve({ stdout, stderr });
130
+ };
131
+ const onInterrupt = () => {
132
+ interrupted = true;
133
+ void terminateOwnedProcessTree(child, platform);
134
+ };
135
+ process.once('SIGINT', onInterrupt);
136
+ const timer = timeoutMs > 0 ? setTimeout(() => {
137
+ timedOut = true;
138
+ void terminateOwnedProcessTree(child, platform).finally(() => settle(new CliCommandError(`${pm} command timed out after ${timeoutMs}ms`, { code: 'GAME_PROCESS_TIMEOUT' })));
139
+ }, timeoutMs) : null;
140
+ child.once('error', (error) => settle(error));
141
+ child.once('exit', (code, signal) => {
142
+ if (timedOut) {
143
+ settle(new CliCommandError(`${pm} command timed out after ${timeoutMs}ms`, { code: 'GAME_PROCESS_TIMEOUT' }));
144
+ }
145
+ else if (interrupted) {
146
+ settle(new CliCommandError(`${pm} command cancelled by user`, { code: 'GAME_PROCESS_CANCELLED', category: 'cancelled' }));
147
+ }
148
+ else if (code === 0) {
149
+ settle();
150
+ }
151
+ else {
152
+ const detail = outputMode === 'capture' ? `: ${(stderr || stdout).trim().slice(-4_000)}` : '';
153
+ settle(new Error(`${pm} exited with ${signal || code}${detail}`));
154
+ }
155
+ });
156
+ });
50
157
  const html = `<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>VGE Game</title></head><body><canvas id="game"></canvas><div id="hud"><strong>Vigthoria Gaming Engine</strong><span>Ready</span></div><script type="module" src="/src/main.ts"></script></body></html>\n`;
51
158
  const style = `*{box-sizing:border-box}html,body,#game{width:100%;height:100%;margin:0;overflow:hidden}body{background:#030510;color:#e8f7ff;font-family:Inter,system-ui,sans-serif}#game{display:block;touch-action:none}#hud{position:fixed;left:24px;top:24px;display:flex;gap:16px;padding:12px 16px;border:1px solid #00d4ff66;border-radius:12px;background:#070a18cc;backdrop-filter:blur(12px);box-shadow:0 0 32px #7c3aed33}#hud span{color:#5eead4}\n`;
52
159
  const main = `import { ArcRotateCamera, Color3, Color4, DirectionalLight, Engine, HemisphericLight, MeshBuilder, PBRMaterial, Scene, ShadowGenerator, Vector3 } from '@babylonjs/core';
@@ -76,21 +183,22 @@ export class GameCommand {
76
183
  if (fs.existsSync(root) && fs.readdirSync(root).length)
77
184
  throw new Error(`Target directory is not empty: ${root}`);
78
185
  fs.mkdirSync(path.join(root, 'src'), { recursive: true });
79
- const pkg = { name: slug, version: '0.1.0', private: true, type: 'module', vigthoria: { kind: 'game', engine: 'babylon', renderTransport: 'client-state', schemaVersion: 1 }, scripts: { dev: 'vite', build: 'tsc --noEmit && vite build', preview: 'vite preview' }, dependencies: { '@babylonjs/core': '7.54.3', '@babylonjs/loaders': '7.54.3' }, devDependencies: { typescript: '5.8.3', vite: '6.1.0' } };
186
+ const pkg = { name: slug, version: '0.1.0', private: true, type: 'module', engines: { node: '>=20.19.0' }, vigthoria: { kind: 'game', engine: 'babylon', renderTransport: 'client-state', schemaVersion: 1 }, scripts: { dev: 'node ./node_modules/vite/bin/vite.js', build: 'node ./node_modules/typescript/bin/tsc --noEmit && node ./node_modules/vite/bin/vite.js build', preview: 'node ./node_modules/vite/bin/vite.js preview' }, dependencies: { '@babylonjs/core': '9.20.0', '@babylonjs/loaders': '9.20.0' }, devDependencies: { typescript: '5.9.3', vite: '7.3.6' } };
80
187
  fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
81
188
  fs.writeFileSync(path.join(root, 'index.html'), html);
82
189
  fs.writeFileSync(path.join(root, 'src/main.ts'), main);
83
190
  fs.writeFileSync(path.join(root, 'src/style.css'), style);
191
+ fs.writeFileSync(path.join(root, 'src/vite-env.d.ts'), '/// <reference types="vite/client" />\n');
84
192
  fs.writeFileSync(path.join(root, '.gitignore'), 'node_modules\ndist\n.vigthoria\n');
85
193
  fs.writeFileSync(path.join(root, 'tsconfig.json'), JSON.stringify({ compilerOptions: { target: 'ES2022', module: 'ESNext', lib: ['ES2022', 'DOM'], skipLibCheck: true, moduleResolution: 'Bundler', isolatedModules: true, noEmit: true, strict: true }, include: ['src'] }, null, 2) + '\n');
86
194
  if (o.install !== false)
87
- await run(manager(root, o.packageManager), ['install'], root);
195
+ await runGameProcess(manager(root, o.packageManager), ['install'], root);
88
196
  this.logger.success(`Babylon/VGE game created: ${root}`);
89
197
  this.logger.info(`Next: cd ${slug} && vigthoria game run`);
90
198
  }
91
199
  async run(o) { const root = path.resolve(o.project || process.cwd()); this.project(root); const args = ['run', 'dev', '--', '--host', '127.0.0.1']; if (o.port)
92
200
  args.push('--port', String(o.port)); if (o.open !== false)
93
- args.push('--open'); await run(manager(root, o.packageManager), args, root, 0); }
201
+ args.push('--open'); await runGameProcess(manager(root, o.packageManager), args, root, 0); }
94
202
  async validate(o) {
95
203
  const root = path.resolve(o.project || process.cwd());
96
204
  const checks = [];
@@ -106,7 +214,7 @@ export class GameCommand {
106
214
  const source = this.sources(root);
107
215
  checks.push({ check: 'Babylon client-state metadata', passed: pkg.vigthoria?.engine === 'babylon' && pkg.vigthoria?.renderTransport === 'client-state' }, { check: 'Pinned local Babylon dependencies', passed: /^\d/.test(pkg.dependencies?.['@babylonjs/core'] || '') && /^\d/.test(pkg.dependencies?.['@babylonjs/loaders'] || '') }, { check: 'No CDN engine imports', passed: !/https?:\/\/(cdn|unpkg|jsdelivr)[^\s"']*(babylon|three)/i.test(source) }, { check: 'Runtime diagnostics', passed: source.includes('__vgeDiagnostics') }, { check: 'Client source entry', passed: fs.existsSync(path.join(root, 'src/main.ts')) || fs.existsSync(path.join(root, 'src/main.js')) });
108
216
  try {
109
- await run(manager(root, o.packageManager), ['run', 'build'], root);
217
+ await runGameProcess(manager(root, o.packageManager), ['run', 'build'], root, 10 * 60_000, o.json ? 'capture' : 'inherit');
110
218
  checks.push({ check: 'Production build', passed: true });
111
219
  }
112
220
  catch (e) {
@@ -11,7 +11,6 @@ export declare class HistoryCommand {
11
11
  constructor(config: Config, logger: Logger);
12
12
  private getHeaders;
13
13
  private getBaseUrl;
14
- private resolveWorkspaceRoot;
15
14
  run(options: HistoryOptions): Promise<void>;
16
15
  }
17
16
  export {};
@@ -3,10 +3,10 @@ import { hasLocalV3AgentCapability } from '../utils/runtime-capability.js';
3
3
  * history.ts — List recent V3 agent runs with summaries.
4
4
  */
5
5
  import chalk from 'chalk';
6
- import { createRequire } from 'node:module';
7
6
  import { createSpinner, CH } from '../utils/logger.js';
8
7
  import { CliCommandError, commandFailure, formatSuccessJson } from '../utils/command-contract.js';
9
- const require = createRequire(import.meta.url);
8
+ import { buildLocalWorkspaceReference } from '../utils/workspace-reference.js';
9
+ import { guardedFetch } from '../utils/network-policy.js';
10
10
  export class HistoryCommand {
11
11
  config;
12
12
  logger;
@@ -36,36 +36,22 @@ export class HistoryCommand {
36
36
  (allowLocal ? 'http://127.0.0.1:8030' : null) ||
37
37
  configuredApiUrl);
38
38
  }
39
- resolveWorkspaceRoot(project) {
40
- // On Windows or non-absolute paths, send empty so the server uses its fallback root
41
- if (/^[a-zA-Z]:[\\/]/.test(project) || /^\\\\/.test(project))
42
- return '';
43
- if (typeof require !== 'undefined') {
44
- try {
45
- const path = require('path');
46
- if (!path.isAbsolute(project))
47
- return '';
48
- }
49
- catch { }
50
- }
51
- return project;
52
- }
53
39
  async run(options) {
54
40
  const limit = options.limit || 20;
55
41
  const project = options.project || process.cwd();
56
- const workspace = this.resolveWorkspaceRoot(project);
42
+ const workspaceRef = buildLocalWorkspaceReference(project, String(this.config.get('userId') || this.config.get('email') || ''));
57
43
  const spinner = createSpinner('Loading run history...').start();
58
44
  try {
59
45
  const baseUrl = this.getBaseUrl();
60
46
  const params = new URLSearchParams({
61
47
  limit: String(limit),
62
- workspace_root: workspace,
63
- local_workspace_path: project,
64
- project_path: project,
48
+ workspace_root: '',
49
+ local_workspace_path: workspaceRef,
50
+ project_path: workspaceRef,
65
51
  });
66
- const resp = await fetch(`${baseUrl}/api/runs?${params}`, {
52
+ const resp = await guardedFetch(`${baseUrl}/api/runs?${params}`, {
67
53
  headers: this.getHeaders(),
68
- });
54
+ }, { audience: 'v3' });
69
55
  if (!resp.ok) {
70
56
  spinner.stop();
71
57
  if (resp.status === 404 || resp.status === 502 || resp.status === 503) {
@@ -6,6 +6,25 @@
6
6
  */
7
7
  import { Config } from '../utils/config.js';
8
8
  import { Logger } from '../utils/logger.js';
9
+ interface Module {
10
+ id: string;
11
+ name: string;
12
+ description: string;
13
+ endpoint: string;
14
+ documentation: string;
15
+ pricing: {
16
+ unit: string;
17
+ cost: number;
18
+ example: string;
19
+ };
20
+ status: string;
21
+ category: string;
22
+ tags: string[];
23
+ unit?: string;
24
+ creditsPerUnit?: number | string;
25
+ euroPerUnit?: number | string;
26
+ }
27
+ export declare function moduleMatchesIdentifier(module: Pick<Module, 'id' | 'name'>, requested: string): boolean;
9
28
  export declare class HubCommand {
10
29
  private config;
11
30
  constructor(config: Config, _logger: Logger);
@@ -39,3 +58,4 @@ export declare class HubCommand {
39
58
  */
40
59
  discover(): Promise<void>;
41
60
  }
61
+ export {};
@@ -8,6 +8,19 @@ import chalk from 'chalk';
8
8
  import { guardedFetch } from '../utils/network-policy.js';
9
9
  import { CliCommandError, commandFailure } from '../utils/command-contract.js';
10
10
  const API_BASE = 'https://hub.vigthoria.io';
11
+ function normalizeModuleIdentifier(value) {
12
+ return String(value || '').trim().toLowerCase().replace(/[\s_]+/g, '-');
13
+ }
14
+ export function moduleMatchesIdentifier(module, requested) {
15
+ const needle = normalizeModuleIdentifier(requested);
16
+ if (!needle)
17
+ return false;
18
+ const id = normalizeModuleIdentifier(module.id);
19
+ const name = normalizeModuleIdentifier(module.name);
20
+ return needle === id
21
+ || needle === name
22
+ || needle === id.replace(/-api$/, '');
23
+ }
11
24
  export class HubCommand {
12
25
  config;
13
26
  constructor(config, _logger) {
@@ -76,6 +89,7 @@ export class HubCommand {
76
89
  data.results.forEach((module, index) => {
77
90
  const statusColor = module.status === 'available' ? chalk.green : chalk.gray;
78
91
  console.log(chalk.bold.white(` ${index + 1}. ${module.name}`));
92
+ console.log(chalk.gray(` Module ID: ${module.id}`));
79
93
  console.log(chalk.gray(` ${module.description.substring(0, 80)}...`));
80
94
  console.log(chalk.cyan(` 💰 ${module.pricing.example}`));
81
95
  console.log(statusColor(` 📊 Status: ${module.status.toUpperCase()}`));
@@ -86,7 +100,7 @@ export class HubCommand {
86
100
  console.log(chalk.cyan(`💡 ${data.suggestion}`));
87
101
  }
88
102
  console.log(chalk.gray('\nTo activate a module: vigthoria hub activate <module-name>'));
89
- console.log(chalk.gray('Example: vigthoria hub activate music\n'));
103
+ console.log(chalk.gray(`Example: vigthoria hub activate ${data.results[0].id}\n`));
90
104
  }
91
105
  catch (error) {
92
106
  throw commandFailure(error, { code: 'HUB_SEARCH_FAILED' });
@@ -144,7 +158,7 @@ export class HubCommand {
144
158
  this.getAuthToken();
145
159
  console.log(chalk.cyan(`\n🔌 Activating module: ${moduleId}...\n`));
146
160
  try {
147
- const module = (await this.moduleCatalog()).find((entry) => entry.id === moduleId);
161
+ const module = (await this.moduleCatalog()).find((entry) => moduleMatchesIdentifier(entry, moduleId));
148
162
  if (!module)
149
163
  throw new CliCommandError(`Unknown Hub module: ${moduleId}`, { code: 'HUB_MODULE_NOT_FOUND', category: 'usage' });
150
164
  console.log(chalk.green(`✅ ${module.name} is published in the Vigthoria module catalog.`));
@@ -186,7 +200,7 @@ export class HubCommand {
186
200
  async info(moduleId) {
187
201
  console.log(chalk.cyan(`\n📖 Module Info: ${moduleId}\n`));
188
202
  try {
189
- const module = (await this.moduleCatalog()).find((entry) => entry.id === moduleId);
203
+ const module = (await this.moduleCatalog()).find((entry) => moduleMatchesIdentifier(entry, moduleId));
190
204
  if (!module)
191
205
  throw new CliCommandError(`Unknown Hub module: ${moduleId}`, { code: 'HUB_MODULE_NOT_FOUND', category: 'usage' });
192
206
  console.log(chalk.bold.white(`${module.name}`));
@@ -20,6 +20,7 @@ interface PreviewOptions {
20
20
  proof?: boolean;
21
21
  screenshot?: boolean;
22
22
  }
23
+ export declare function isPreviewProofRequested(options: Pick<PreviewOptions, 'proof' | 'screenshot'>): boolean;
23
24
  export declare class PreviewCommand {
24
25
  private logger;
25
26
  private api;
@@ -51,6 +51,9 @@ const MIME_TYPES = {
51
51
  '.txt': 'text/plain',
52
52
  '.map': 'application/json',
53
53
  };
54
+ export function isPreviewProofRequested(options) {
55
+ return options.proof === true || options.screenshot === true;
56
+ }
54
57
  export class PreviewCommand {
55
58
  logger;
56
59
  api;
@@ -76,11 +79,17 @@ export class PreviewCommand {
76
79
  await this.showConsolidatedDiff(projectPath);
77
80
  }
78
81
  // Run Template Service preview proof gate
79
- if (options.proof) {
80
- await this.runProofGate(projectPath, options.screenshot);
82
+ if (isPreviewProofRequested(options)) {
83
+ try {
84
+ await this.runProofGate(projectPath, options.screenshot, options.screenshot === true && options.proof !== true);
85
+ }
86
+ catch (error) {
87
+ this.api.destroy();
88
+ throw error;
89
+ }
81
90
  }
82
91
  // If only --diff or --proof was requested (no explicit entry/port), just exit
83
- if (options.diff || options.proof) {
92
+ if (options.diff || isPreviewProofRequested(options)) {
84
93
  if (!options.entry && !options.port) {
85
94
  this.api.destroy();
86
95
  return;
@@ -90,7 +99,7 @@ export class PreviewCommand {
90
99
  const entryFile = this.detectEntryFile(projectPath, options.entry);
91
100
  if (!entryFile) {
92
101
  this.logger.warn('No HTML entry file found. Use --entry <file> to specify one.');
93
- if (!options.diff && !options.proof) {
102
+ if (!options.diff && !options.proof && !options.screenshot) {
94
103
  this.api.destroy();
95
104
  throw new CliCommandError('No HTML entry file found. Use --entry <file> to specify one.', {
96
105
  code: 'PREVIEW_ENTRY_NOT_FOUND', category: 'configuration',
@@ -162,7 +171,7 @@ export class PreviewCommand {
162
171
  * Start a local HTTP server for preview
163
172
  */
164
173
  async startServer(projectPath, entryFile, port, autoOpen) {
165
- return new Promise((resolve) => {
174
+ return new Promise((resolve, reject) => {
166
175
  this.server = http.createServer((req, res) => {
167
176
  let filePath;
168
177
  try {
@@ -210,6 +219,20 @@ export class PreviewCommand {
210
219
  res.end('Internal server error');
211
220
  }
212
221
  });
222
+ const finish = (error) => {
223
+ process.removeListener('SIGINT', shutdown);
224
+ process.removeListener('SIGTERM', shutdown);
225
+ this.api.destroy();
226
+ this.server = null;
227
+ error ? reject(error) : resolve();
228
+ };
229
+ const shutdown = () => {
230
+ console.log(chalk.gray('\n Stopping preview server...'));
231
+ if (!this.server)
232
+ return finish();
233
+ this.server.close((error) => finish(error || undefined));
234
+ };
235
+ this.server.once('error', finish);
213
236
  this.server.listen(port, () => {
214
237
  const url = `http://localhost:${port}/${entryFile}`;
215
238
  console.log(chalk.green(` ${CH.success} Preview server running`));
@@ -221,15 +244,8 @@ export class PreviewCommand {
221
244
  this.openBrowser(url);
222
245
  }
223
246
  });
224
- // Handle graceful shutdown
225
- const shutdown = () => {
226
- console.log(chalk.gray('\n Stopping preview server...'));
227
- this.server?.close();
228
- this.api.destroy();
229
- resolve();
230
- };
231
- process.on('SIGINT', shutdown);
232
- process.on('SIGTERM', shutdown);
247
+ process.once('SIGINT', shutdown);
248
+ process.once('SIGTERM', shutdown);
233
249
  });
234
250
  }
235
251
  /**
@@ -419,7 +435,7 @@ export class PreviewCommand {
419
435
  /**
420
436
  * Run Template Service preview gate and persist proof bundle
421
437
  */
422
- async runProofGate(projectPath, captureScreenshot) {
438
+ async runProofGate(projectPath, captureScreenshot, localScreenshotProof = false) {
423
439
  const spinner = createSpinner('Running preview proof gate...').start();
424
440
  try {
425
441
  const result = await this.api.runTemplateServicePreviewGate('', {
@@ -428,6 +444,11 @@ export class PreviewCommand {
428
444
  targetPath: projectPath,
429
445
  forceFrontendPreview: true,
430
446
  requireScreenshot: captureScreenshot === true,
447
+ // An explicit screenshot-only command is a local operation. Do not
448
+ // upload the user's frontend or require an ecosystem login merely to
449
+ // capture local visual evidence. `--proof` retains the hosted proof
450
+ // contract, including when combined with `--screenshot`.
451
+ localScreenshotProof,
431
452
  });
432
453
  spinner.stop();
433
454
  console.log(chalk.bold.white(` ${CH.hLine.repeat(3)} Preview Proof Gate ${CH.hLine.repeat(39)}`));
@@ -386,7 +386,7 @@ Examples:
386
386
  .option('--no-open', 'Do not auto-open browser')
387
387
  .option('--diff', 'Show consolidated diff of recent agent changes')
388
388
  .option('--proof', 'Run Template Service preview gate and persist proof bundle')
389
- .option('--screenshot', 'Capture screenshot via Puppeteer')
389
+ .option('--screenshot', 'Capture screenshot via an installed system browser')
390
390
  .action(async (options) => {
391
391
  const preview = new PreviewCommand(config, logger);
392
392
  await preview.run({
@@ -10,7 +10,6 @@ export declare class ReplayCommand {
10
10
  constructor(config: Config, _logger: Logger);
11
11
  private getHeaders;
12
12
  private getBaseUrl;
13
- private resolveWorkspaceRoot;
14
13
  private sleep;
15
14
  run(runId: string, options: ReplayOptions): Promise<void>;
16
15
  }
@@ -3,10 +3,10 @@ import { hasLocalV3AgentCapability } from '../utils/runtime-capability.js';
3
3
  * replay.ts — Replay events from a V3 agent run step-by-step.
4
4
  */
5
5
  import chalk from 'chalk';
6
- import { createRequire } from 'node:module';
7
6
  import { createSpinner, CH } from '../utils/logger.js';
8
7
  import { CliCommandError, commandFailure, formatSuccessJson } from '../utils/command-contract.js';
9
- const require = createRequire(import.meta.url);
8
+ import { buildLocalWorkspaceReference } from '../utils/workspace-reference.js';
9
+ import { guardedFetch } from '../utils/network-policy.js';
10
10
  export class ReplayCommand {
11
11
  config;
12
12
  constructor(config, _logger) {
@@ -34,33 +34,24 @@ export class ReplayCommand {
34
34
  (allowLocal ? 'http://127.0.0.1:8030' : null) ||
35
35
  configuredApiUrl);
36
36
  }
37
- resolveWorkspaceRoot(project) {
38
- if (/^[a-zA-Z]:[\\/]/.test(project) || /^\\\\/.test(project))
39
- return '';
40
- if (typeof require !== 'undefined') {
41
- try {
42
- const path = require('path');
43
- if (!path.isAbsolute(project))
44
- return '';
45
- }
46
- catch { }
47
- }
48
- return project;
49
- }
50
37
  sleep(ms) {
51
38
  return new Promise((resolve) => setTimeout(resolve, ms));
52
39
  }
53
40
  async run(runId, options) {
54
41
  const speed = options.speed || 200;
55
42
  const project = options.project || process.cwd();
56
- const workspace = this.resolveWorkspaceRoot(project);
43
+ const workspaceRef = buildLocalWorkspaceReference(project, String(this.config.get('userId') || this.config.get('email') || ''));
57
44
  const spinner = createSpinner(`Loading events for run ${runId}...`).start();
58
45
  try {
59
46
  const baseUrl = this.getBaseUrl();
60
- const params = new URLSearchParams({ workspace_root: workspace });
61
- const resp = await fetch(`${baseUrl}/api/runs/${encodeURIComponent(runId)}/events?${params}`, {
62
- headers: this.getHeaders(),
47
+ const params = new URLSearchParams({
48
+ workspace_root: '',
49
+ local_workspace_path: workspaceRef,
50
+ project_path: workspaceRef,
63
51
  });
52
+ const resp = await guardedFetch(`${baseUrl}/api/runs/${encodeURIComponent(runId)}/events?${params}`, {
53
+ headers: this.getHeaders(),
54
+ }, { audience: 'v3' });
64
55
  if (!resp.ok) {
65
56
  spinner.stop();
66
57
  if (resp.status === 404) {
@@ -14,6 +14,22 @@
14
14
  */
15
15
  import { Config } from '../utils/config.js';
16
16
  import { Logger } from '../utils/logger.js';
17
+ interface Project {
18
+ id: number;
19
+ project_name: string;
20
+ project_path: string;
21
+ description: string;
22
+ tech_stack: string;
23
+ visibility: 'public' | 'private' | 'restricted';
24
+ is_public: boolean;
25
+ demo_url: string | null;
26
+ repo_url: string | null;
27
+ status: string;
28
+ storage_usage_mb: number;
29
+ created_at: string;
30
+ updated_at: string;
31
+ last_synced_at: string | null;
32
+ }
17
33
  interface PushOptions {
18
34
  path?: string;
19
35
  name?: string;
@@ -34,6 +50,25 @@ interface ListOptions {
34
50
  interface ShareOptions {
35
51
  expires?: string;
36
52
  }
53
+ interface RepositoryMaterialization {
54
+ project?: Project;
55
+ projectName?: string;
56
+ description?: string;
57
+ downloadUrl?: string;
58
+ inlineComplete?: boolean;
59
+ files?: Array<{
60
+ path: string;
61
+ content?: string;
62
+ encoding?: 'utf8' | 'base64';
63
+ size?: number;
64
+ binary?: boolean;
65
+ }>;
66
+ }
67
+ export declare function isCompleteRepositoryInlinePayload(data: RepositoryMaterialization): boolean;
68
+ export declare function resolveRepositoryDownloadRoute(rawUrl: string, coderApiBase: string, publicDownload?: boolean): {
69
+ url: string;
70
+ audience: 'coder' | 'community';
71
+ };
37
72
  export declare class RepositoryMutationAmbiguousError extends Error {
38
73
  readonly operationId: string;
39
74
  readonly mutationOutcome = "unknown";
@@ -48,8 +83,6 @@ export declare class RepoCommand {
48
83
  private communityToken;
49
84
  constructor(config: Config, _logger: Logger);
50
85
  private getAuthHeaders;
51
- private getRepositoryAudienceForUrl;
52
- private getAuthHeadersForUrl;
53
86
  private ensureCommunityAuth;
54
87
  private repoFetch;
55
88
  private collectProjectFiles;