vigthoria-cli 1.13.18 → 1.13.22

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 (185) hide show
  1. package/completions/_vigthoria +49 -0
  2. package/completions/vigthoria.bash +6 -0
  3. package/completions/vigthoria.fish +41 -0
  4. package/dist/commands/auth.d.ts +1 -0
  5. package/dist/commands/auth.js +84 -135
  6. package/dist/commands/background.d.ts +0 -2
  7. package/dist/commands/background.js +26 -34
  8. package/dist/commands/bridge.js +45 -41
  9. package/dist/commands/cancel.d.ts +1 -0
  10. package/dist/commands/cancel.js +22 -16
  11. package/dist/commands/chat.d.ts +5 -42
  12. package/dist/commands/chat.js +462 -1122
  13. package/dist/commands/coding-registration.d.ts +4 -0
  14. package/dist/commands/coding-registration.js +24 -0
  15. package/dist/commands/config.js +32 -42
  16. package/dist/commands/conversation-registration.d.ts +4 -0
  17. package/dist/commands/conversation-registration.js +122 -0
  18. package/dist/commands/deploy.d.ts +8 -2
  19. package/dist/commands/deploy.js +92 -90
  20. package/dist/commands/device.d.ts +1 -2
  21. package/dist/commands/device.js +37 -21
  22. package/dist/commands/doctor-registration.d.ts +4 -0
  23. package/dist/commands/doctor-registration.js +200 -0
  24. package/dist/commands/edit.js +22 -33
  25. package/dist/commands/explain.js +7 -10
  26. package/dist/commands/fork.d.ts +1 -2
  27. package/dist/commands/fork.js +72 -22
  28. package/dist/commands/game.d.ts +1 -1
  29. package/dist/commands/game.js +17 -6
  30. package/dist/commands/generate.js +7 -9
  31. package/dist/commands/history.js +11 -7
  32. package/dist/commands/hub.d.ts +2 -2
  33. package/dist/commands/hub.js +34 -35
  34. package/dist/commands/legion.d.ts +0 -1
  35. package/dist/commands/legion.js +62 -79
  36. package/dist/commands/music.d.ts +1 -2
  37. package/dist/commands/music.js +62 -27
  38. package/dist/commands/platform-registration.d.ts +4 -0
  39. package/dist/commands/platform-registration.js +45 -0
  40. package/dist/commands/preview.d.ts +0 -1
  41. package/dist/commands/preview.js +82 -35
  42. package/dist/commands/product-run-registration.d.ts +4 -0
  43. package/dist/commands/product-run-registration.js +490 -0
  44. package/dist/commands/replay.d.ts +1 -2
  45. package/dist/commands/replay.js +15 -11
  46. package/dist/commands/repo.d.ts +14 -7
  47. package/dist/commands/repo.js +245 -196
  48. package/dist/commands/review.js +9 -12
  49. package/dist/commands/security.d.ts +1 -2
  50. package/dist/commands/security.js +12 -6
  51. package/dist/commands/update-registration.d.ts +3 -0
  52. package/dist/commands/update-registration.js +338 -0
  53. package/dist/commands/v4-registration.d.ts +4 -0
  54. package/dist/commands/v4-registration.js +24 -0
  55. package/dist/commands/v4.d.ts +1 -1
  56. package/dist/commands/v4.js +46 -8
  57. package/dist/commands/wallet.d.ts +1 -3
  58. package/dist/commands/wallet.js +18 -45
  59. package/dist/commands/workflow.d.ts +2 -0
  60. package/dist/commands/workflow.js +52 -39
  61. package/dist/index.js +122 -1498
  62. package/dist/utils/agent-stream-state.d.ts +27 -0
  63. package/dist/utils/agent-stream-state.js +93 -0
  64. package/dist/utils/agentRoute.d.ts +2 -1
  65. package/dist/utils/agentRoute.js +22 -9
  66. package/dist/utils/agentRunOutcome.d.ts +16 -0
  67. package/dist/utils/agentRunOutcome.js +37 -2
  68. package/dist/utils/api-client-factory.d.ts +8 -0
  69. package/dist/utils/api-client-factory.js +8 -0
  70. package/dist/utils/api.d.ts +45 -269
  71. package/dist/utils/api.js +664 -3210
  72. package/dist/utils/auth-session.d.ts +41 -0
  73. package/dist/utils/auth-session.js +224 -0
  74. package/dist/utils/brain-hub-client.d.ts +0 -4
  75. package/dist/utils/brain-hub-client.js +6 -3
  76. package/dist/utils/bridge-client.js +2 -0
  77. package/dist/utils/capability-health-service.d.ts +48 -0
  78. package/dist/utils/capability-health-service.js +64 -0
  79. package/dist/utils/chat-prompt-policy.d.ts +18 -0
  80. package/dist/utils/chat-prompt-policy.js +61 -0
  81. package/dist/utils/chat-tool-call-parser.d.ts +11 -0
  82. package/dist/utils/chat-tool-call-parser.js +71 -0
  83. package/dist/utils/cli-state.d.ts +22 -4
  84. package/dist/utils/cli-state.js +105 -47
  85. package/dist/utils/code-operations-service.d.ts +104 -0
  86. package/dist/utils/code-operations-service.js +837 -0
  87. package/dist/utils/codebase-indexer.d.ts +1 -0
  88. package/dist/utils/codebase-indexer.js +28 -8
  89. package/dist/utils/command-contract.d.ts +61 -0
  90. package/dist/utils/command-contract.js +149 -0
  91. package/dist/utils/command-policy.d.ts +10 -0
  92. package/dist/utils/command-policy.js +54 -0
  93. package/dist/utils/config.d.ts +13 -5
  94. package/dist/utils/config.js +227 -99
  95. package/dist/utils/context-ranker.js +11 -2
  96. package/dist/utils/context-state.d.ts +39 -0
  97. package/dist/utils/context-state.js +52 -0
  98. package/dist/utils/deckEvents.d.ts +0 -5
  99. package/dist/utils/deckEvents.js +3 -2
  100. package/dist/utils/desktop-bridge-client.js +2 -0
  101. package/dist/utils/direct-output-policy.d.ts +3 -0
  102. package/dist/utils/direct-output-policy.js +76 -0
  103. package/dist/utils/durable-json.d.ts +57 -0
  104. package/dist/utils/durable-json.js +319 -0
  105. package/dist/utils/fastAgentRouter.d.ts +6 -7
  106. package/dist/utils/fastAgentRouter.js +21 -30
  107. package/dist/utils/files.js +21 -13
  108. package/dist/utils/frontend-preview-service.d.ts +76 -0
  109. package/dist/utils/frontend-preview-service.js +482 -0
  110. package/dist/utils/goaEvents.d.ts +0 -4
  111. package/dist/utils/goaEvents.js +5 -2
  112. package/dist/utils/logger.d.ts +2 -1
  113. package/dist/utils/logger.js +27 -20
  114. package/dist/utils/mcp-context-client.d.ts +24 -0
  115. package/dist/utils/mcp-context-client.js +58 -0
  116. package/dist/utils/model-governance.d.ts +18 -0
  117. package/dist/utils/model-governance.js +75 -0
  118. package/dist/utils/model-transport-service.d.ts +44 -0
  119. package/dist/utils/model-transport-service.js +111 -0
  120. package/dist/utils/mutation-journal.d.ts +42 -0
  121. package/dist/utils/mutation-journal.js +387 -0
  122. package/dist/utils/network-policy.d.ts +24 -0
  123. package/dist/utils/network-policy.js +202 -0
  124. package/dist/utils/operator-client.d.ts +45 -0
  125. package/dist/utils/operator-client.js +159 -0
  126. package/dist/utils/post-write-validator.js +7 -9
  127. package/dist/utils/preview-screenshot-adapter.d.ts +13 -0
  128. package/dist/utils/preview-screenshot-adapter.js +36 -0
  129. package/dist/utils/process-policy.d.ts +23 -0
  130. package/dist/utils/process-policy.js +284 -0
  131. package/dist/utils/project-memory.d.ts +13 -3
  132. package/dist/utils/project-memory.js +70 -17
  133. package/dist/utils/prompt-workspace-resolver.d.ts +14 -0
  134. package/dist/utils/prompt-workspace-resolver.js +64 -0
  135. package/dist/utils/release-install.d.ts +11 -0
  136. package/dist/utils/release-install.js +155 -0
  137. package/dist/utils/release-policy.d.ts +64 -0
  138. package/dist/utils/release-policy.js +293 -0
  139. package/dist/utils/requestIntent.js +22 -3
  140. package/dist/utils/runtime-capability.d.ts +8 -0
  141. package/dist/utils/runtime-capability.js +16 -0
  142. package/dist/utils/secret-policy.d.ts +20 -0
  143. package/dist/utils/secret-policy.js +160 -0
  144. package/dist/utils/session.d.ts +34 -2
  145. package/dist/utils/session.js +354 -110
  146. package/dist/utils/tool-approval-service.d.ts +17 -0
  147. package/dist/utils/tool-approval-service.js +78 -0
  148. package/dist/utils/tool-capability-providers.d.ts +57 -0
  149. package/dist/utils/tool-capability-providers.js +57 -0
  150. package/dist/utils/tools.d.ts +2 -23
  151. package/dist/utils/tools.js +216 -508
  152. package/dist/utils/update-policy.d.ts +18 -0
  153. package/dist/utils/update-policy.js +137 -0
  154. package/dist/utils/v3-agent-client.d.ts +40 -0
  155. package/dist/utils/v3-agent-client.js +165 -0
  156. package/dist/utils/v3-workspace-service.d.ts +28 -0
  157. package/dist/utils/v3-workspace-service.js +279 -0
  158. package/dist/utils/vigflow-client.d.ts +95 -0
  159. package/dist/utils/vigflow-client.js +105 -0
  160. package/dist/utils/workspace-boundary.d.ts +45 -0
  161. package/dist/utils/workspace-boundary.js +377 -0
  162. package/dist/utils/workspace-brain-service.d.ts +2 -0
  163. package/dist/utils/workspace-brain-service.js +11 -1
  164. package/dist/utils/workspace-cache.js +37 -15
  165. package/dist/utils/workspace-stream.js +35 -10
  166. package/install.ps1 +86 -143
  167. package/install.sh +76 -81
  168. package/package.json +44 -9
  169. package/release-policy.json +43 -0
  170. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
  171. package/scripts/release/generate-completions.mjs +24 -0
  172. package/scripts/release/install-release.mjs +10 -0
  173. package/scripts/release/publish-cli-release.mjs +116 -0
  174. package/scripts/release/publish-cli-release.sh +2 -70
  175. package/scripts/release/resolve-release-manifest.mjs +26 -0
  176. package/scripts/release/test-balanced-model-live.sh +42 -0
  177. package/scripts/release/validate-live-service-gates.sh +90 -0
  178. package/scripts/release/validate-no-go-gates.sh +135 -172
  179. package/scripts/release/verify-runtime-consistency.mjs +4 -1
  180. package/dist/commands/index.d.ts +0 -12
  181. package/dist/commands/index.js +0 -194
  182. package/dist/commands/update.d.ts +0 -9
  183. package/dist/commands/update.js +0 -301
  184. package/dist/utils/templateInstantPath.d.ts +0 -18
  185. package/dist/utils/templateInstantPath.js +0 -228
@@ -1,29 +1,30 @@
1
1
  import chalk from 'chalk';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
+ import { guardedFetch } from '../utils/network-policy.js';
5
+ import { CliCommandError, commandFailure, formatSuccessJson } from '../utils/command-contract.js';
6
+ import { setTimeout as delay } from 'node:timers/promises';
4
7
  export class MusicCommand {
5
8
  config;
6
- logger;
7
- constructor(config, logger) {
9
+ constructor(config, _logger) {
8
10
  this.config = config;
9
- this.logger = logger;
10
11
  }
11
12
  apiBase() {
12
13
  const configured = String(this.config.getAll().musicApiUrl || '').trim();
13
14
  return (process.env.VIGTHORIA_MUSIC_API_URL || configured || 'https://music.vigthoria.io').replace(/\/+$/, '');
14
15
  }
15
16
  authHeaders() {
16
- const token = this.config.get('authToken');
17
+ const token = process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN || this.config.get('authToken');
17
18
  return token ? { Authorization: `Bearer ${token}` } : {};
18
19
  }
19
20
  async request(endpoint, init = {}) {
20
- const response = await fetch(`${this.apiBase()}${endpoint}`, {
21
+ const response = await guardedFetch(`${this.apiBase()}${endpoint}`, {
21
22
  ...init,
22
23
  headers: {
23
24
  ...this.authHeaders(),
24
25
  ...init.headers,
25
26
  },
26
- });
27
+ }, { audience: 'music' });
27
28
  const text = await response.text();
28
29
  let data = {};
29
30
  try {
@@ -58,7 +59,7 @@ export class MusicCommand {
58
59
  async generate(promptArg, options) {
59
60
  const prompt = String(promptArg || options.prompt || '').trim();
60
61
  if (!prompt) {
61
- throw new Error('Music generation requires a prompt.');
62
+ throw new CliCommandError('Music generation requires a prompt.', { code: 'MUSIC_PROMPT_REQUIRED', category: 'usage' });
62
63
  }
63
64
  let referenceAudioPath = '';
64
65
  if (options.voice) {
@@ -88,10 +89,7 @@ export class MusicCommand {
88
89
  body: JSON.stringify(payload),
89
90
  });
90
91
  const taskId = String(data.task_id || data.session_id || '').trim();
91
- if (options.json) {
92
- console.log(JSON.stringify({ ...data, task_id: taskId || data.task_id }, null, 2));
93
- }
94
- else {
92
+ if (!options.json) {
95
93
  console.log(chalk.green('Music generation queued.'));
96
94
  console.log(chalk.cyan('Task ID: ') + (taskId || '(pending)'));
97
95
  if (data.queue_position)
@@ -100,13 +98,18 @@ export class MusicCommand {
100
98
  console.log(chalk.gray(`Estimated wait: ${data.estimated_wait_seconds}s`));
101
99
  }
102
100
  if (options.wait && taskId) {
103
- await this.waitForStatus(taskId, Number(options.pollInterval || 5), Boolean(options.json));
101
+ const terminal = await this.waitForStatus(taskId, Number(options.pollInterval || 5), options.json === true);
102
+ if (options.json)
103
+ console.log(formatSuccessJson('music generate', terminal, { stage: 'terminal' }));
104
+ return;
104
105
  }
106
+ if (options.json)
107
+ console.log(formatSuccessJson('music generate', { ...data, task_id: taskId || data.task_id }, { stage: 'queued' }));
105
108
  }
106
109
  async status(taskId, options = {}) {
107
110
  const data = await this.request(`/api/status/${encodeURIComponent(taskId)}`);
108
111
  if (options.json) {
109
- console.log(JSON.stringify(data, null, 2));
112
+ console.log(formatSuccessJson('music status', data));
110
113
  return;
111
114
  }
112
115
  console.log(chalk.cyan(`Music task ${taskId}`));
@@ -117,21 +120,53 @@ export class MusicCommand {
117
120
  if (downloadUrl)
118
121
  console.log(`Download: ${downloadUrl}`);
119
122
  }
120
- async waitForStatus(taskId, intervalSeconds, json) {
121
- const delay = Math.max(1, intervalSeconds) * 1000;
122
- for (;;) {
123
- await new Promise((resolve) => setTimeout(resolve, delay));
124
- const data = await this.request(`/api/status/${encodeURIComponent(taskId)}`);
125
- const status = String(data.status || '').toLowerCase();
126
- if (!json) {
127
- const progress = data.progress !== undefined ? ` (${data.progress}%)` : '';
128
- console.log(chalk.gray(`Status: ${status || 'unknown'}${progress}`));
129
- }
130
- if (['completed', 'complete', 'finished', 'failed', 'error', 'cancelled'].includes(status)) {
131
- if (json)
132
- console.log(JSON.stringify(data, null, 2));
133
- return;
123
+ async waitForStatus(taskId, intervalSeconds, quiet) {
124
+ const pollDelayMs = Math.max(1, intervalSeconds) * 1000;
125
+ const configuredTimeout = Number.parseInt(process.env.VIGTHORIA_MUSIC_WAIT_TIMEOUT_MS || '', 10);
126
+ const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : 30 * 60_000;
127
+ const deadline = Date.now() + timeoutMs;
128
+ const cancellation = new AbortController();
129
+ let interrupted = false;
130
+ const onInterrupt = () => {
131
+ interrupted = true;
132
+ cancellation.abort();
133
+ };
134
+ process.once('SIGINT', onInterrupt);
135
+ try {
136
+ for (;;) {
137
+ if (Date.now() >= deadline) {
138
+ throw new CliCommandError(`Music generation did not finish within ${timeoutMs}ms.`, {
139
+ code: 'MUSIC_WAIT_TIMEOUT', category: 'network', details: { taskId, timeoutMs },
140
+ });
141
+ }
142
+ await delay(Math.min(pollDelayMs, Math.max(1, deadline - Date.now())), undefined, { signal: cancellation.signal });
143
+ const data = await this.request(`/api/status/${encodeURIComponent(taskId)}`, { signal: cancellation.signal });
144
+ const status = String(data.status || '').toLowerCase();
145
+ if (!quiet) {
146
+ const progress = data.progress !== undefined ? ` (${data.progress}%)` : '';
147
+ console.log(chalk.gray(`Status: ${status || 'unknown'}${progress}`));
148
+ }
149
+ if (['failed', 'error'].includes(status)) {
150
+ throw new CliCommandError(String(data.error || data.message || 'Music generation failed.'), {
151
+ code: 'MUSIC_GENERATION_FAILED', details: data,
152
+ });
153
+ }
154
+ if (status === 'cancelled') {
155
+ throw new CliCommandError('Music generation was cancelled.', {
156
+ code: 'MUSIC_GENERATION_CANCELLED', category: 'cancelled', details: data,
157
+ });
158
+ }
159
+ if (['completed', 'complete', 'finished'].includes(status))
160
+ return data;
134
161
  }
135
162
  }
163
+ catch (error) {
164
+ if (interrupted)
165
+ throw new CliCommandError('Music wait cancelled by user.', { code: 'MUSIC_WAIT_CANCELLED', category: 'cancelled', cause: error });
166
+ throw commandFailure(error, { code: 'MUSIC_WAIT_FAILED' });
167
+ }
168
+ finally {
169
+ process.removeListener('SIGINT', onInterrupt);
170
+ }
136
171
  }
137
172
  }
@@ -0,0 +1,4 @@
1
+ import type { Command } from 'commander';
2
+ import type { Config } from '../utils/config.js';
3
+ import type { Logger } from '../utils/logger.js';
4
+ export declare function registerPlatformCommands(program: Command, config: Config, logger: Logger): void;
@@ -0,0 +1,45 @@
1
+ import { BridgeCommand } from './bridge.js';
2
+ import { DeviceCommand } from './device.js';
3
+ import { SecurityCommand } from './security.js';
4
+ export function registerPlatformCommands(program, config, logger) {
5
+ program.command('bridge').description('Inspect local DevTools Bridge availability for browser debugging tasks')
6
+ .command('status').description('Show live DevTools Bridge status')
7
+ .action(async () => new BridgeCommand(config, logger).status());
8
+ const device = program.command('device').alias('adb').description('Android Developer Bridge commands via local ADB');
9
+ device.command('status').description('Show ADB availability and connected Android devices').option('--json', 'Emit JSON output', false)
10
+ .action(async (options) => new DeviceCommand(config, logger).status(options));
11
+ device.command('list').description('List connected Android devices').option('--json', 'Emit JSON output', false)
12
+ .action(async (options) => new DeviceCommand(config, logger).list(options));
13
+ device.command('screenshot').description('Capture a screenshot from an Android device')
14
+ .option('-d, --device <id>', 'ADB device id').option('-o, --output <path>', 'Output PNG path')
15
+ .action(async (options) => new DeviceCommand(config, logger).screenshot(options));
16
+ device.command('install <apk>').description('Install or update an APK on an Android device')
17
+ .option('-d, --device <id>', 'ADB device id').option('--json', 'Emit JSON output', false)
18
+ .action(async (apk, options) => new DeviceCommand(config, logger).install(apk, options));
19
+ device.command('launch <package>').description('Launch an Android app by package name')
20
+ .option('-d, --device <id>', 'ADB device id').option('-a, --activity <activity>', 'Activity class, for example .MainActivity')
21
+ .option('--json', 'Emit JSON output', false)
22
+ .action(async (packageName, options) => new DeviceCommand(config, logger).launch(packageName, options));
23
+ device.command('logs').description('Read Android logcat output').option('-d, --device <id>', 'ADB device id')
24
+ .option('-n, --lines <count>', 'Number of log lines for snapshot mode', '250').option('-f, --follow', 'Follow logcat until interrupted', false)
25
+ .option('--json', 'Emit JSON output for snapshot mode', false)
26
+ .action(async (options) => new DeviceCommand(config, logger).logs(options));
27
+ device.command('tcpip').description('Enable wireless ADB tcpip mode on the selected USB-connected device')
28
+ .option('-d, --device <id>', 'ADB device id').option('-p, --port <port>', 'Wireless ADB port', '5555')
29
+ .option('--json', 'Emit JSON output', false).action(async (options) => new DeviceCommand(config, logger).tcpip(options));
30
+ device.command('connect <address>').description('Connect to a wireless ADB device, for example 192.168.1.30:5555')
31
+ .option('--json', 'Emit JSON output', false).action(async (address, options) => new DeviceCommand(config, logger).connect(address, options));
32
+ device.command('disconnect [address]').description('Disconnect a wireless ADB device')
33
+ .option('--json', 'Emit JSON output', false).action(async (address, options) => new DeviceCommand(config, logger).disconnect(address, options));
34
+ device.action(async () => new DeviceCommand(config, logger).status({}));
35
+ const security = program.command('security').alias('vsec')
36
+ .description('Run security scans, scores, and fix plans via Vigthoria MCP security tools');
37
+ security.command('scan').description('Scan project for security issues').option('-d, --dir <path>', 'Directory to scan (default: current directory)')
38
+ .option('--json', 'Emit JSON output', false).action(async (options) => new SecurityCommand(config, logger).scan({ dir: options.dir, json: options.json }));
39
+ security.command('score').description('Calculate project security score').option('-d, --dir <path>', 'Directory to score (default: current directory)')
40
+ .option('--json', 'Emit JSON output', false).action(async (options) => new SecurityCommand(config, logger).score({ dir: options.dir, json: options.json }));
41
+ security.command('fix').description('Generate security fix plan').option('-d, --dir <path>', 'Directory to inspect (default: current directory)')
42
+ .option('-i, --issue-id <id>', 'Single issue id to target').option('--apply', 'Request auto-apply mode (safe mode still requires explicit patching)', false)
43
+ .option('--json', 'Emit JSON output', false)
44
+ .action(async (options) => new SecurityCommand(config, logger).fix({ dir: options.dir, issueId: options.issueId, apply: options.apply, json: options.json }));
45
+ }
@@ -21,7 +21,6 @@ interface PreviewOptions {
21
21
  screenshot?: boolean;
22
22
  }
23
23
  export declare class PreviewCommand {
24
- private config;
25
24
  private logger;
26
25
  private api;
27
26
  private server;
@@ -13,10 +13,15 @@ import chalk from 'chalk';
13
13
  import * as fs from 'fs';
14
14
  import * as path from 'path';
15
15
  import * as http from 'http';
16
+ import { execFileSync } from 'node:child_process';
16
17
  import { createRequire } from 'node:module';
17
18
  import { structuredPatch } from 'diff';
18
19
  import { createSpinner, CH } from '../utils/logger.js';
19
- import { APIClient } from '../utils/api.js';
20
+ import { createAPIClient } from '../utils/api-client-factory.js';
21
+ import { resolveWorkspacePath } from '../utils/workspace-boundary.js';
22
+ import { hardenGitArguments, safeGitEnvironment } from '../utils/process-policy.js';
23
+ import { safeChildProcessEnv } from '../utils/secret-policy.js';
24
+ import { CliCommandError, commandFailure } from '../utils/command-contract.js';
20
25
  const require = createRequire(import.meta.url);
21
26
  // Common MIME types for static file serving
22
27
  const MIME_TYPES = {
@@ -47,21 +52,20 @@ const MIME_TYPES = {
47
52
  '.map': 'application/json',
48
53
  };
49
54
  export class PreviewCommand {
50
- config;
51
55
  logger;
52
56
  api;
53
57
  server = null;
54
58
  constructor(config, logger) {
55
- this.config = config;
56
59
  this.logger = logger;
57
- this.api = new APIClient(config, logger);
60
+ this.api = createAPIClient(config, logger);
58
61
  }
59
62
  async run(options) {
60
63
  const projectPath = path.resolve(options.project || process.cwd());
61
64
  if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) {
62
- this.logger.error(`Project directory not found: ${projectPath}`);
63
65
  this.api.destroy();
64
- return;
66
+ throw new CliCommandError(`Project directory not found: ${projectPath}`, {
67
+ code: 'PROJECT_DIRECTORY_NOT_FOUND', category: 'usage',
68
+ });
65
69
  }
66
70
  console.log();
67
71
  console.log(chalk.bold.white(` ${CH.hLine.repeat(3)} Vigthoria Preview ${CH.hLine.repeat(40)}`));
@@ -88,7 +92,9 @@ export class PreviewCommand {
88
92
  this.logger.warn('No HTML entry file found. Use --entry <file> to specify one.');
89
93
  if (!options.diff && !options.proof) {
90
94
  this.api.destroy();
91
- return;
95
+ throw new CliCommandError('No HTML entry file found. Use --entry <file> to specify one.', {
96
+ code: 'PREVIEW_ENTRY_NOT_FOUND', category: 'configuration',
97
+ });
92
98
  }
93
99
  this.api.destroy();
94
100
  return;
@@ -102,9 +108,15 @@ export class PreviewCommand {
102
108
  */
103
109
  detectEntryFile(projectPath, specified) {
104
110
  if (specified) {
105
- const fullPath = path.join(projectPath, specified);
106
- if (fs.existsSync(fullPath))
107
- return specified;
111
+ try {
112
+ const fullPath = resolveWorkspacePath(projectPath, specified);
113
+ if (fs.statSync(fullPath).isFile())
114
+ return specified.replace(/\\/g, '/');
115
+ }
116
+ catch {
117
+ this.logger.warn(`Refused entry file outside project boundary: ${specified}`);
118
+ return null;
119
+ }
108
120
  this.logger.warn(`Specified entry file not found: ${specified}`);
109
121
  return null;
110
122
  }
@@ -152,28 +164,35 @@ export class PreviewCommand {
152
164
  async startServer(projectPath, entryFile, port, autoOpen) {
153
165
  return new Promise((resolve) => {
154
166
  this.server = http.createServer((req, res) => {
155
- // Sanitize URL to prevent path traversal
156
- const urlPath = decodeURIComponent(new URL(req.url || '/', `http://localhost:${port}`).pathname);
157
- const safePath = path.normalize(urlPath).replace(/^(\.\.[/\\])+/, '');
158
- let filePath = path.join(projectPath, safePath);
159
- // Ensure the resolved path is within the project directory
160
- const resolvedPath = path.resolve(filePath);
161
- if (!resolvedPath.startsWith(path.resolve(projectPath))) {
167
+ let filePath;
168
+ try {
169
+ const urlPath = decodeURIComponent(new URL(req.url || '/', `http://localhost:${port}`).pathname);
170
+ const relativeRequest = urlPath.replace(/^\/+/, '') || entryFile;
171
+ filePath = resolveWorkspacePath(projectPath, relativeRequest, { allowMissing: true });
172
+ }
173
+ catch {
162
174
  res.writeHead(403);
163
175
  res.end('Forbidden');
164
176
  return;
165
177
  }
166
178
  // Directory → serve index.html
167
179
  if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
168
- filePath = path.join(filePath, 'index.html');
180
+ try {
181
+ filePath = resolveWorkspacePath(projectPath, `${path.relative(projectPath, filePath).replace(/\\/g, '/')}/index.html`, { allowMissing: true });
182
+ }
183
+ catch {
184
+ res.writeHead(403);
185
+ res.end('Forbidden');
186
+ return;
187
+ }
169
188
  }
170
189
  // SPA fallback
171
190
  if (!fs.existsSync(filePath)) {
172
- const spaFallback = path.join(projectPath, entryFile);
173
- if (fs.existsSync(spaFallback)) {
191
+ try {
192
+ const spaFallback = resolveWorkspacePath(projectPath, entryFile);
174
193
  filePath = spaFallback;
175
194
  }
176
- else {
195
+ catch {
177
196
  res.writeHead(404);
178
197
  res.end('Not found');
179
198
  return;
@@ -225,20 +244,21 @@ export class PreviewCommand {
225
244
  if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
226
245
  return;
227
246
  const safeUrl = parsed.href;
247
+ const options = { env: safeChildProcessEnv(), windowsHide: true, timeout: 10_000 };
228
248
  if (platform === 'darwin') {
229
- execFile('open', [safeUrl], (err) => {
249
+ execFile('open', [safeUrl], options, (err) => {
230
250
  if (err)
231
251
  this.logger.debug(`Could not auto-open browser: ${err.message}`);
232
252
  });
233
253
  }
234
254
  else if (platform === 'win32') {
235
- execFile('cmd', ['/c', 'start', '', safeUrl], (err) => {
255
+ execFile('rundll32.exe', ['url.dll,FileProtocolHandler', safeUrl], options, (err) => {
236
256
  if (err)
237
257
  this.logger.debug(`Could not auto-open browser: ${err.message}`);
238
258
  });
239
259
  }
240
260
  else {
241
- execFile('xdg-open', [safeUrl], (err) => {
261
+ execFile('xdg-open', [safeUrl], options, (err) => {
242
262
  if (err)
243
263
  this.logger.debug(`Could not auto-open browser: ${err.message}`);
244
264
  });
@@ -254,15 +274,20 @@ export class PreviewCommand {
254
274
  async showConsolidatedDiff(projectPath) {
255
275
  console.log(chalk.bold.white(` ${CH.hLine.repeat(3)} Change Summary ${CH.hLine.repeat(43)}`));
256
276
  console.log();
257
- const proofDir = path.join(projectPath, '.vigthoria', 'proof', 'preview');
277
+ const proofDir = resolveWorkspacePath(projectPath, '.vigthoria/proof/preview', { allowMissing: true });
258
278
  const hasProof = fs.existsSync(proofDir);
259
279
  // Try git diff first (most reliable)
260
280
  try {
261
- const { execSync } = require('child_process');
262
281
  const isGit = fs.existsSync(path.join(projectPath, '.git'));
263
282
  if (isGit) {
264
283
  // Get list of changed files (unstaged + staged)
265
- const statusOutput = execSync('git status --porcelain', { cwd: projectPath, encoding: 'utf-8' }).trim();
284
+ const statusOutput = execFileSync('git', hardenGitArguments(['status', '--porcelain']), {
285
+ cwd: projectPath,
286
+ encoding: 'utf8',
287
+ windowsHide: true,
288
+ timeout: 30_000,
289
+ env: safeChildProcessEnv(safeGitEnvironment()),
290
+ }).trim();
266
291
  if (!statusOutput) {
267
292
  console.log(chalk.gray(' No changes detected (working tree clean).'));
268
293
  console.log();
@@ -295,9 +320,13 @@ export class PreviewCommand {
295
320
  // Show unified diffs for modified and created files (limit to keep terminal manageable)
296
321
  const diffTargets = [...modified, ...created].slice(0, 20);
297
322
  for (const relPath of diffTargets) {
298
- const absPath = path.join(projectPath, relPath);
299
- if (!fs.existsSync(absPath))
323
+ let absPath;
324
+ try {
325
+ absPath = resolveWorkspacePath(projectPath, relPath);
326
+ }
327
+ catch {
300
328
  continue;
329
+ }
301
330
  const ext = path.extname(relPath).toLowerCase();
302
331
  const textExts = ['.html', '.css', '.js', '.ts', '.jsx', '.tsx', '.json', '.md', '.yml', '.yaml', '.txt', '.xml', '.svg', '.py', '.sh', '.env'];
303
332
  if (!textExts.includes(ext))
@@ -309,7 +338,13 @@ export class PreviewCommand {
309
338
  try {
310
339
  // Validate relPath is a safe relative path before interpolating — prevents injection
311
340
  if (/^[a-zA-Z0-9_\-./]+$/.test(relPath) && !relPath.includes('..')) {
312
- oldContent = execSync(`git show HEAD:${relPath}`, { cwd: projectPath, encoding: 'utf-8' });
341
+ oldContent = execFileSync('git', hardenGitArguments(['show', `HEAD:${relPath}`]), {
342
+ cwd: projectPath,
343
+ encoding: 'utf8',
344
+ windowsHide: true,
345
+ timeout: 30_000,
346
+ env: safeChildProcessEnv(safeGitEnvironment()),
347
+ });
313
348
  }
314
349
  }
315
350
  catch {
@@ -363,7 +398,8 @@ export class PreviewCommand {
363
398
  console.log(chalk.white(' Recent proof bundles:'));
364
399
  for (const m of manifests) {
365
400
  try {
366
- const manifest = JSON.parse(fs.readFileSync(path.join(proofDir, m), 'utf-8'));
401
+ const manifestPath = resolveWorkspacePath(projectPath, `.vigthoria/proof/preview/${m}`);
402
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
367
403
  const passed = manifest.previewGate?.passed ? chalk.green('passed') : chalk.red('failed');
368
404
  console.log(chalk.gray(` ${manifest.createdAt || m} `) + passed + chalk.gray(` ${manifest.entryPath || '-'}`));
369
405
  }
@@ -390,14 +426,17 @@ export class PreviewCommand {
390
426
  workspacePath: projectPath,
391
427
  projectPath: projectPath,
392
428
  targetPath: projectPath,
429
+ forceFrontendPreview: true,
430
+ requireScreenshot: captureScreenshot === true,
393
431
  });
394
432
  spinner.stop();
395
433
  console.log(chalk.bold.white(` ${CH.hLine.repeat(3)} Preview Proof Gate ${CH.hLine.repeat(39)}`));
396
434
  console.log();
397
435
  if (!result.required) {
398
- console.log(chalk.gray(' Preview gate: not required (no frontend artifacts detected)'));
399
- console.log();
400
- return;
436
+ throw new CliCommandError('Preview proof was explicitly requested, but the proof service did not require or validate it.', {
437
+ code: 'PREVIEW_PROOF_SKIPPED',
438
+ details: result,
439
+ });
401
440
  }
402
441
  const statusIcon = result.passed ? chalk.green(CH.success) : chalk.red(CH.error);
403
442
  console.log(` ${statusIcon} Preview gate: ${result.passed ? chalk.green('PASSED') : chalk.red('FAILED')}`);
@@ -458,10 +497,18 @@ export class PreviewCommand {
458
497
  }
459
498
  }
460
499
  console.log();
500
+ if (!result.passed || result.skipped === true) {
501
+ throw new CliCommandError(result.error || 'Preview proof did not pass.', {
502
+ code: 'PREVIEW_PROOF_FAILED',
503
+ category: result.partialMutation === false ? 'execution' : 'partial_mutation',
504
+ partialMutation: result.partialMutation ?? false,
505
+ details: result,
506
+ });
507
+ }
461
508
  }
462
509
  catch (error) {
463
510
  spinner.stop();
464
- this.logger.error(`Preview proof gate failed: ${error?.message || String(error)}`);
511
+ throw commandFailure(error, { code: 'PREVIEW_PROOF_FAILED' });
465
512
  }
466
513
  }
467
514
  }
@@ -0,0 +1,4 @@
1
+ import type { Command } from 'commander';
2
+ import type { Config } from '../utils/config.js';
3
+ import type { Logger } from '../utils/logger.js';
4
+ export declare function registerProductAndRunCommands(program: Command, config: Config, logger: Logger): void;