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.
- package/completions/_vigthoria +49 -0
- package/completions/vigthoria.bash +6 -0
- package/completions/vigthoria.fish +41 -0
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +84 -135
- package/dist/commands/background.d.ts +0 -2
- package/dist/commands/background.js +26 -34
- package/dist/commands/bridge.js +45 -41
- package/dist/commands/cancel.d.ts +1 -0
- package/dist/commands/cancel.js +22 -16
- package/dist/commands/chat.d.ts +5 -42
- package/dist/commands/chat.js +462 -1122
- package/dist/commands/coding-registration.d.ts +4 -0
- package/dist/commands/coding-registration.js +24 -0
- package/dist/commands/config.js +32 -42
- package/dist/commands/conversation-registration.d.ts +4 -0
- package/dist/commands/conversation-registration.js +122 -0
- package/dist/commands/deploy.d.ts +8 -2
- package/dist/commands/deploy.js +92 -90
- package/dist/commands/device.d.ts +1 -2
- package/dist/commands/device.js +37 -21
- package/dist/commands/doctor-registration.d.ts +4 -0
- package/dist/commands/doctor-registration.js +200 -0
- package/dist/commands/edit.js +22 -33
- package/dist/commands/explain.js +7 -10
- package/dist/commands/fork.d.ts +1 -2
- package/dist/commands/fork.js +72 -22
- package/dist/commands/game.d.ts +1 -1
- package/dist/commands/game.js +17 -6
- package/dist/commands/generate.js +7 -9
- package/dist/commands/history.js +11 -7
- package/dist/commands/hub.d.ts +2 -2
- package/dist/commands/hub.js +34 -35
- package/dist/commands/legion.d.ts +0 -1
- package/dist/commands/legion.js +62 -79
- package/dist/commands/music.d.ts +1 -2
- package/dist/commands/music.js +62 -27
- package/dist/commands/platform-registration.d.ts +4 -0
- package/dist/commands/platform-registration.js +45 -0
- package/dist/commands/preview.d.ts +0 -1
- package/dist/commands/preview.js +82 -35
- package/dist/commands/product-run-registration.d.ts +4 -0
- package/dist/commands/product-run-registration.js +490 -0
- package/dist/commands/replay.d.ts +1 -2
- package/dist/commands/replay.js +15 -11
- package/dist/commands/repo.d.ts +14 -7
- package/dist/commands/repo.js +245 -196
- package/dist/commands/review.js +9 -12
- package/dist/commands/security.d.ts +1 -2
- package/dist/commands/security.js +12 -6
- package/dist/commands/update-registration.d.ts +3 -0
- package/dist/commands/update-registration.js +338 -0
- package/dist/commands/v4-registration.d.ts +4 -0
- package/dist/commands/v4-registration.js +24 -0
- package/dist/commands/v4.d.ts +1 -1
- package/dist/commands/v4.js +46 -8
- package/dist/commands/wallet.d.ts +1 -3
- package/dist/commands/wallet.js +18 -45
- package/dist/commands/workflow.d.ts +2 -0
- package/dist/commands/workflow.js +52 -39
- package/dist/index.js +122 -1498
- package/dist/utils/agent-stream-state.d.ts +27 -0
- package/dist/utils/agent-stream-state.js +93 -0
- package/dist/utils/agentRoute.d.ts +2 -1
- package/dist/utils/agentRoute.js +22 -9
- package/dist/utils/agentRunOutcome.d.ts +16 -0
- package/dist/utils/agentRunOutcome.js +37 -2
- package/dist/utils/api-client-factory.d.ts +8 -0
- package/dist/utils/api-client-factory.js +8 -0
- package/dist/utils/api.d.ts +45 -269
- package/dist/utils/api.js +664 -3210
- package/dist/utils/auth-session.d.ts +41 -0
- package/dist/utils/auth-session.js +224 -0
- package/dist/utils/brain-hub-client.d.ts +0 -4
- package/dist/utils/brain-hub-client.js +6 -3
- package/dist/utils/bridge-client.js +2 -0
- package/dist/utils/capability-health-service.d.ts +48 -0
- package/dist/utils/capability-health-service.js +64 -0
- package/dist/utils/chat-prompt-policy.d.ts +18 -0
- package/dist/utils/chat-prompt-policy.js +61 -0
- package/dist/utils/chat-tool-call-parser.d.ts +11 -0
- package/dist/utils/chat-tool-call-parser.js +71 -0
- package/dist/utils/cli-state.d.ts +22 -4
- package/dist/utils/cli-state.js +105 -47
- package/dist/utils/code-operations-service.d.ts +104 -0
- package/dist/utils/code-operations-service.js +837 -0
- package/dist/utils/codebase-indexer.d.ts +1 -0
- package/dist/utils/codebase-indexer.js +28 -8
- package/dist/utils/command-contract.d.ts +61 -0
- package/dist/utils/command-contract.js +149 -0
- package/dist/utils/command-policy.d.ts +10 -0
- package/dist/utils/command-policy.js +54 -0
- package/dist/utils/config.d.ts +13 -5
- package/dist/utils/config.js +227 -99
- package/dist/utils/context-ranker.js +11 -2
- package/dist/utils/context-state.d.ts +39 -0
- package/dist/utils/context-state.js +52 -0
- package/dist/utils/deckEvents.d.ts +0 -5
- package/dist/utils/deckEvents.js +3 -2
- package/dist/utils/desktop-bridge-client.js +2 -0
- package/dist/utils/direct-output-policy.d.ts +3 -0
- package/dist/utils/direct-output-policy.js +76 -0
- package/dist/utils/durable-json.d.ts +57 -0
- package/dist/utils/durable-json.js +319 -0
- package/dist/utils/fastAgentRouter.d.ts +6 -7
- package/dist/utils/fastAgentRouter.js +21 -30
- package/dist/utils/files.js +21 -13
- package/dist/utils/frontend-preview-service.d.ts +76 -0
- package/dist/utils/frontend-preview-service.js +482 -0
- package/dist/utils/goaEvents.d.ts +0 -4
- package/dist/utils/goaEvents.js +5 -2
- package/dist/utils/logger.d.ts +2 -1
- package/dist/utils/logger.js +27 -20
- package/dist/utils/mcp-context-client.d.ts +24 -0
- package/dist/utils/mcp-context-client.js +58 -0
- package/dist/utils/model-governance.d.ts +18 -0
- package/dist/utils/model-governance.js +75 -0
- package/dist/utils/model-transport-service.d.ts +44 -0
- package/dist/utils/model-transport-service.js +111 -0
- package/dist/utils/mutation-journal.d.ts +42 -0
- package/dist/utils/mutation-journal.js +387 -0
- package/dist/utils/network-policy.d.ts +24 -0
- package/dist/utils/network-policy.js +202 -0
- package/dist/utils/operator-client.d.ts +45 -0
- package/dist/utils/operator-client.js +159 -0
- package/dist/utils/post-write-validator.js +7 -9
- package/dist/utils/preview-screenshot-adapter.d.ts +13 -0
- package/dist/utils/preview-screenshot-adapter.js +36 -0
- package/dist/utils/process-policy.d.ts +23 -0
- package/dist/utils/process-policy.js +284 -0
- package/dist/utils/project-memory.d.ts +13 -3
- package/dist/utils/project-memory.js +70 -17
- package/dist/utils/prompt-workspace-resolver.d.ts +14 -0
- package/dist/utils/prompt-workspace-resolver.js +64 -0
- package/dist/utils/release-install.d.ts +11 -0
- package/dist/utils/release-install.js +155 -0
- package/dist/utils/release-policy.d.ts +64 -0
- package/dist/utils/release-policy.js +293 -0
- package/dist/utils/requestIntent.js +22 -3
- package/dist/utils/runtime-capability.d.ts +8 -0
- package/dist/utils/runtime-capability.js +16 -0
- package/dist/utils/secret-policy.d.ts +20 -0
- package/dist/utils/secret-policy.js +160 -0
- package/dist/utils/session.d.ts +34 -2
- package/dist/utils/session.js +354 -110
- package/dist/utils/tool-approval-service.d.ts +17 -0
- package/dist/utils/tool-approval-service.js +78 -0
- package/dist/utils/tool-capability-providers.d.ts +57 -0
- package/dist/utils/tool-capability-providers.js +57 -0
- package/dist/utils/tools.d.ts +2 -23
- package/dist/utils/tools.js +216 -508
- package/dist/utils/update-policy.d.ts +18 -0
- package/dist/utils/update-policy.js +137 -0
- package/dist/utils/v3-agent-client.d.ts +40 -0
- package/dist/utils/v3-agent-client.js +165 -0
- package/dist/utils/v3-workspace-service.d.ts +28 -0
- package/dist/utils/v3-workspace-service.js +279 -0
- package/dist/utils/vigflow-client.d.ts +95 -0
- package/dist/utils/vigflow-client.js +105 -0
- package/dist/utils/workspace-boundary.d.ts +45 -0
- package/dist/utils/workspace-boundary.js +377 -0
- package/dist/utils/workspace-brain-service.d.ts +2 -0
- package/dist/utils/workspace-brain-service.js +11 -1
- package/dist/utils/workspace-cache.js +37 -15
- package/dist/utils/workspace-stream.js +35 -10
- package/install.ps1 +86 -143
- package/install.sh +76 -81
- package/package.json +44 -9
- package/release-policy.json +43 -0
- package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
- package/scripts/release/generate-completions.mjs +24 -0
- package/scripts/release/install-release.mjs +10 -0
- package/scripts/release/publish-cli-release.mjs +116 -0
- package/scripts/release/publish-cli-release.sh +2 -70
- package/scripts/release/resolve-release-manifest.mjs +26 -0
- package/scripts/release/test-balanced-model-live.sh +42 -0
- package/scripts/release/validate-live-service-gates.sh +90 -0
- package/scripts/release/validate-no-go-gates.sh +135 -172
- package/scripts/release/verify-runtime-consistency.mjs +4 -1
- package/dist/commands/index.d.ts +0 -12
- package/dist/commands/index.js +0 -194
- package/dist/commands/update.d.ts +0 -9
- package/dist/commands/update.js +0 -301
- package/dist/utils/templateInstantPath.d.ts +0 -18
- package/dist/utils/templateInstantPath.js +0 -228
package/dist/commands/music.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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),
|
|
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(
|
|
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,
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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,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
|
+
}
|
package/dist/commands/preview.js
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
191
|
+
try {
|
|
192
|
+
const spaFallback = resolveWorkspacePath(projectPath, entryFile);
|
|
174
193
|
filePath = spaFallback;
|
|
175
194
|
}
|
|
176
|
-
|
|
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('
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
299
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
511
|
+
throw commandFailure(error, { code: 'PREVIEW_PROOF_FAILED' });
|
|
465
512
|
}
|
|
466
513
|
}
|
|
467
514
|
}
|