vigthoria-cli 1.13.34 → 1.13.42
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/README.md +11 -56
- package/SECURITY_HARDENING.md +2 -4
- package/dist/commands/auth.js +16 -14
- package/dist/commands/cancel.d.ts +1 -0
- package/dist/commands/cancel.js +21 -4
- package/dist/commands/chat.d.ts +13 -0
- package/dist/commands/chat.js +170 -14
- package/dist/commands/fork.js +8 -3
- package/dist/commands/history.js +8 -3
- package/dist/commands/legion.js +1 -1
- package/dist/commands/replay.js +8 -3
- package/dist/commands/update-registration.js +63 -206
- package/dist/commands/v4-menu.d.ts +0 -1
- package/dist/commands/v4-menu.js +8 -6
- package/dist/commands/v4-registration.js +0 -2
- package/dist/commands/v4.js +9 -4
- package/dist/commands/wallet.d.ts +1 -1
- package/dist/commands/wallet.js +3 -3
- package/dist/index.js +2 -3
- package/dist/utils/agent-stream-state.d.ts +7 -0
- package/dist/utils/agent-stream-state.js +31 -0
- package/dist/utils/agentRunOutcome.js +5 -1
- package/dist/utils/api.d.ts +19 -4
- package/dist/utils/api.js +322 -550
- package/dist/utils/frontend-preview-service.d.ts +10 -1
- package/dist/utils/frontend-preview-service.js +162 -17
- package/dist/utils/javascript-syntax.d.ts +19 -0
- package/dist/utils/javascript-syntax.js +35 -0
- package/dist/utils/localTestMode.js +1 -1
- package/dist/utils/mutation-journal.d.ts +10 -0
- package/dist/utils/mutation-journal.js +104 -76
- package/dist/utils/network-policy.js +7 -10
- package/dist/utils/preview-screenshot-adapter.d.ts +81 -0
- package/dist/utils/preview-screenshot-adapter.js +615 -38
- package/dist/utils/release-install.d.ts +0 -1
- package/dist/utils/release-install.js +44 -7
- package/dist/utils/requestIntent.d.ts +1 -1
- package/dist/utils/requestIntent.js +17 -3
- package/dist/utils/tools.js +21 -15
- package/dist/utils/update-policy.d.ts +0 -4
- package/dist/utils/update-policy.js +5 -9
- package/dist/utils/v3-agent-client.js +25 -11
- package/install.ps1 +30 -50
- package/install.sh +13 -24
- package/package.json +4 -2
- package/release-policy.json +1 -5
- package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +6 -7
- package/scripts/release/install-release.mjs +5 -4
- package/scripts/release/publish-cli-release.mjs +21 -8
- package/scripts/release/test-balanced-model-live.sh +4 -1
- package/scripts/release/validate-live-service-gates.sh +27 -5
|
@@ -44,6 +44,7 @@ export interface FrontendPreviewGateResult {
|
|
|
44
44
|
export interface FrontendPreviewServiceDependencies {
|
|
45
45
|
getBaseUrls(): string[];
|
|
46
46
|
getAccessToken(): string | null;
|
|
47
|
+
getServiceKey?(): string | null;
|
|
47
48
|
fetch(input: string, init: RequestInit): Promise<Response>;
|
|
48
49
|
resolveTargetPath(context: Record<string, any>): string;
|
|
49
50
|
extractExpectedFiles(message: string, context: Record<string, any>): string[];
|
|
@@ -52,10 +53,17 @@ export interface FrontendPreviewServiceDependencies {
|
|
|
52
53
|
screenshotAdapter: PreviewScreenshotPort;
|
|
53
54
|
}
|
|
54
55
|
export declare function isConstrainedStaticFrontendRequest(message?: string): boolean;
|
|
56
|
+
/**
|
|
57
|
+
* A code artifact can belong to a browser product without itself being a
|
|
58
|
+
* previewable application. Requiring an HTML entry for an explicitly scoped
|
|
59
|
+
* module/library request turns a successful native verification into a false
|
|
60
|
+
* failure and rolls the requested file back.
|
|
61
|
+
*/
|
|
62
|
+
export declare function isFocusedNonPreviewableCodeArtifactRequest(message?: string, expectedFiles?: string[]): boolean;
|
|
55
63
|
export declare class FrontendPreviewService {
|
|
56
64
|
private readonly dependencies;
|
|
57
65
|
constructor(dependencies: FrontendPreviewServiceDependencies);
|
|
58
|
-
|
|
66
|
+
isFrontendTask(message?: string, context?: Record<string, any>): boolean;
|
|
59
67
|
/**
|
|
60
68
|
* Returns true when the prompt describes a read-only / analysis task.
|
|
61
69
|
* Used to suppress preview gate and other write-oriented side effects.
|
|
@@ -70,6 +78,7 @@ export declare class FrontendPreviewService {
|
|
|
70
78
|
js: string[];
|
|
71
79
|
assets: string[];
|
|
72
80
|
};
|
|
81
|
+
private expandLinkedFrontendDependencyGraph;
|
|
73
82
|
private buildFrontendForPreview;
|
|
74
83
|
private gatherFrontendPreviewArtifacts;
|
|
75
84
|
private evaluateFrontendVisualProof;
|
|
@@ -11,6 +11,47 @@ export function isConstrainedStaticFrontendRequest(message = '') {
|
|
|
11
11
|
&& /\b(?:single[- ]file|one\s+paragraph|exactly\s+one|current\s+directory\s+only|write\s+the\s+file\b[^.\n]{0,80}\bonly)\b/.test(prompt)
|
|
12
12
|
&& !/\b(?:premium|signature|showcase|redesign|overhaul|dashboard|portal|saas|interactive|animation|motion|form|checkout|booking|commerce)\b/.test(prompt);
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* A code artifact can belong to a browser product without itself being a
|
|
16
|
+
* previewable application. Requiring an HTML entry for an explicitly scoped
|
|
17
|
+
* module/library request turns a successful native verification into a false
|
|
18
|
+
* failure and rolls the requested file back.
|
|
19
|
+
*/
|
|
20
|
+
export function isFocusedNonPreviewableCodeArtifactRequest(message = '', expectedFiles = []) {
|
|
21
|
+
const prompt = String(message || '').toLowerCase();
|
|
22
|
+
const normalizedFiles = Array.from(new Set(expectedFiles
|
|
23
|
+
.map((filePath) => String(filePath || '').trim().replace(/\\/g, '/').replace(/^\.\//, ''))
|
|
24
|
+
.filter(Boolean)));
|
|
25
|
+
if (normalizedFiles.length === 0 || normalizedFiles.length > 4)
|
|
26
|
+
return false;
|
|
27
|
+
if (normalizedFiles.some((filePath) => /\.(?:html?|css|scss|sass|less)$/i.test(filePath)))
|
|
28
|
+
return false;
|
|
29
|
+
if (!normalizedFiles.every((filePath) => /\.(?:[cm]?[jt]sx?|py|go|rs|java|kt|swift|rb|php|cs|cpp|cc|c|h|hpp)$/i.test(filePath))) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const explicitArtifactScope = /\b(?:es\s+module|module|library|utility|helper|class|function|service|adapter|middleware|route|schema)\b/i.test(prompt)
|
|
33
|
+
|| /\bexport\s+(?:exactly|only)\b/i.test(prompt)
|
|
34
|
+
|| /\b(?:create|write|implement|update|fix|repair)\b[^.\n]{0,120}\b(?:file|files)\b/i.test(prompt);
|
|
35
|
+
if (!explicitArtifactScope)
|
|
36
|
+
return false;
|
|
37
|
+
const wholeProductScope = /\b(?:build|create|implement|deliver|make)\b[^.\n]{0,100}\b(?:complete|full|entire|end-to-end|from scratch|production-ready)\b[^.\n]{0,80}\b(?:website|site|page|dashboard|portal|app|application|game)\b/i.test(prompt)
|
|
38
|
+
|| /\b(?:complete|full|entire|end-to-end|playable)\s+(?:website|site|page|dashboard|portal|app|application|game)\b/i.test(prompt);
|
|
39
|
+
return !wholeProductScope;
|
|
40
|
+
}
|
|
41
|
+
function htmlAttributeValue(openingTag, name) {
|
|
42
|
+
const match = String(openingTag || '').match(new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s'"\\x60=<>]+))`, 'i'));
|
|
43
|
+
return match ? String(match[1] ?? match[2] ?? match[3] ?? '') : '';
|
|
44
|
+
}
|
|
45
|
+
function hasViewportMeta(html) {
|
|
46
|
+
return Array.from(String(html || '').matchAll(/<meta\b[^>]*>/gi)).some((match) => htmlAttributeValue(match[0], 'name').toLowerCase() === 'viewport');
|
|
47
|
+
}
|
|
48
|
+
function canonicalizePreviewHtmlAttributes(html) {
|
|
49
|
+
// The browser accepts valid unquoted attributes. Some older preview-service
|
|
50
|
+
// deployments still parse only quoted name/src/href/type tokens. Send a
|
|
51
|
+
// semantically equivalent canonical proof document without rewriting the
|
|
52
|
+
// user's source file, so standards-compliant markup is not falsely rejected.
|
|
53
|
+
return String(html || '').replace(/\b(name|href|src|rel|type)\s*=\s*([^\s'"\x60=<>]+)/gi, (_match, name, value) => `${name}="${value}"`);
|
|
54
|
+
}
|
|
14
55
|
export class FrontendPreviewService {
|
|
15
56
|
dependencies;
|
|
16
57
|
constructor(dependencies) {
|
|
@@ -28,6 +69,9 @@ export class FrontendPreviewService {
|
|
|
28
69
|
const prompt = String(message || '');
|
|
29
70
|
const expectedFiles = this.dependencies.extractExpectedFiles(message, context);
|
|
30
71
|
const hasHtmlEntry = expectedFiles.some((filePath) => /\.html?$/i.test(filePath));
|
|
72
|
+
if (isFocusedNonPreviewableCodeArtifactRequest(prompt, expectedFiles)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
31
75
|
return /(premium|polished|landing|site|page|dashboard|saas|frontend|ui|pricing|showcase|hero|responsive|interactive|game|canvas)/i.test(prompt)
|
|
32
76
|
|| hasHtmlEntry;
|
|
33
77
|
}
|
|
@@ -156,24 +200,30 @@ export class FrontendPreviewService {
|
|
|
156
200
|
}
|
|
157
201
|
return this.normalizeWorkspaceRelativePath(path.posix.normalize(path.posix.join(entryDir, withoutQuery)));
|
|
158
202
|
};
|
|
159
|
-
const cssPattern = /<link[^>]+href=["']([^"']+\.css(?:[?#][^"']*)?)["'][^>]*>/gi;
|
|
160
|
-
const jsPattern = /<script[^>]+src=["']([^"']+\.(?:js|mjs|cjs)(?:[?#][^"']*)?)["'][^>]*>/gi;
|
|
161
203
|
let match;
|
|
162
|
-
|
|
163
|
-
|
|
204
|
+
const linkPattern = /<link\b[^>]*>/gi;
|
|
205
|
+
while ((match = linkPattern.exec(String(html || ''))) !== null) {
|
|
206
|
+
const href = htmlAttributeValue(match[0], 'href');
|
|
207
|
+
if (!/\.css(?:[?#]|$)/i.test(href))
|
|
208
|
+
continue;
|
|
209
|
+
const resolved = normalizeAsset(href);
|
|
164
210
|
if (resolved) {
|
|
165
211
|
css.add(resolved);
|
|
166
212
|
}
|
|
167
213
|
}
|
|
168
|
-
|
|
169
|
-
|
|
214
|
+
const scriptPattern = /<script\b[^>]*>/gi;
|
|
215
|
+
while ((match = scriptPattern.exec(String(html || ''))) !== null) {
|
|
216
|
+
const src = htmlAttributeValue(match[0], 'src');
|
|
217
|
+
if (!/\.(?:js|mjs|cjs)(?:[?#]|$)/i.test(src))
|
|
218
|
+
continue;
|
|
219
|
+
const resolved = normalizeAsset(src);
|
|
170
220
|
if (resolved) {
|
|
171
221
|
js.add(resolved);
|
|
172
222
|
}
|
|
173
223
|
}
|
|
174
|
-
const assetPattern = /<(?:link|img|source|video|audio)\b[^>]
|
|
224
|
+
const assetPattern = /<(?:link|img|source|video|audio)\b[^>]*>/gi;
|
|
175
225
|
while ((match = assetPattern.exec(String(html || ''))) !== null) {
|
|
176
|
-
const resolved = normalizeAsset(match[
|
|
226
|
+
const resolved = normalizeAsset(htmlAttributeValue(match[0], 'href') || htmlAttributeValue(match[0], 'src'));
|
|
177
227
|
if (resolved)
|
|
178
228
|
assets.add(resolved);
|
|
179
229
|
}
|
|
@@ -183,6 +233,93 @@ export class FrontendPreviewService {
|
|
|
183
233
|
assets: Array.from(assets),
|
|
184
234
|
};
|
|
185
235
|
}
|
|
236
|
+
expandLinkedFrontendDependencyGraph(rootPath, entryFiles, kind) {
|
|
237
|
+
const selected = [];
|
|
238
|
+
const seen = new Set();
|
|
239
|
+
const queued = new Set();
|
|
240
|
+
const queue = entryFiles
|
|
241
|
+
.map((filePath) => this.normalizeWorkspaceRelativePath(filePath))
|
|
242
|
+
.filter(Boolean);
|
|
243
|
+
for (const filePath of queue)
|
|
244
|
+
queued.add(filePath);
|
|
245
|
+
const extensions = kind === 'css' ? ['.css'] : ['.js', '.mjs', '.cjs'];
|
|
246
|
+
const resolveImport = (ownerPath, specifier) => {
|
|
247
|
+
const clean = String(specifier || '').trim().split('?')[0].split('#')[0];
|
|
248
|
+
if (!clean
|
|
249
|
+
|| clean.startsWith('#')
|
|
250
|
+
|| clean.startsWith('//')
|
|
251
|
+
|| /^(?:https?:|data:|node:)/i.test(clean)
|
|
252
|
+
|| (!clean.startsWith('.') && !clean.startsWith('/'))) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const ownerDir = path.posix.dirname(this.normalizeWorkspaceRelativePath(ownerPath) || '.');
|
|
256
|
+
const base = this.normalizeWorkspaceRelativePath(path.posix.normalize(clean.startsWith('/')
|
|
257
|
+
? clean.slice(1)
|
|
258
|
+
: path.posix.join(ownerDir, clean)));
|
|
259
|
+
const candidates = [base];
|
|
260
|
+
if (!path.posix.extname(base)) {
|
|
261
|
+
for (const extension of extensions)
|
|
262
|
+
candidates.push(`${base}${extension}`);
|
|
263
|
+
for (const extension of extensions)
|
|
264
|
+
candidates.push(path.posix.join(base, `index${extension}`));
|
|
265
|
+
}
|
|
266
|
+
for (const candidate of candidates) {
|
|
267
|
+
if (!extensions.includes(path.posix.extname(candidate).toLowerCase()))
|
|
268
|
+
continue;
|
|
269
|
+
try {
|
|
270
|
+
const absolute = resolveWorkspacePath(rootPath, candidate);
|
|
271
|
+
if (fs.statSync(absolute).isFile())
|
|
272
|
+
return candidate;
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// The candidate is absent or outside the workspace boundary.
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
};
|
|
280
|
+
// A dependency graph is authoritative because every admitted file is
|
|
281
|
+
// reachable from the HTML entry. The cap prevents cyclic or adversarial
|
|
282
|
+
// graphs from inflating a preview payload; the final payload has its own
|
|
283
|
+
// byte limit as a second boundary.
|
|
284
|
+
while (queue.length > 0 && selected.length < 64) {
|
|
285
|
+
const ownerPath = queue.shift();
|
|
286
|
+
if (!ownerPath || seen.has(ownerPath))
|
|
287
|
+
continue;
|
|
288
|
+
seen.add(ownerPath);
|
|
289
|
+
let source = '';
|
|
290
|
+
try {
|
|
291
|
+
source = fs.readFileSync(resolveWorkspacePath(rootPath, ownerPath), 'utf8');
|
|
292
|
+
}
|
|
293
|
+
catch {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
selected.push(ownerPath);
|
|
297
|
+
const specifiers = [];
|
|
298
|
+
if (kind === 'css') {
|
|
299
|
+
const importPattern = /@import\s+(?:url\(\s*)?(?:["']([^"']+)["']|([^\s)'";]+))\s*\)?/gi;
|
|
300
|
+
let match;
|
|
301
|
+
while ((match = importPattern.exec(source)) !== null)
|
|
302
|
+
specifiers.push(match[1] || match[2]);
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
const staticPattern = /\b(?:import|export)\s+(?:[^'";]*?\sfrom\s*)?["']([^"']+)["']/g;
|
|
306
|
+
const dynamicPattern = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g;
|
|
307
|
+
let match;
|
|
308
|
+
while ((match = staticPattern.exec(source)) !== null)
|
|
309
|
+
specifiers.push(match[1]);
|
|
310
|
+
while ((match = dynamicPattern.exec(source)) !== null)
|
|
311
|
+
specifiers.push(match[1]);
|
|
312
|
+
}
|
|
313
|
+
for (const specifier of specifiers) {
|
|
314
|
+
const dependency = resolveImport(ownerPath, specifier);
|
|
315
|
+
if (dependency && !seen.has(dependency) && !queued.has(dependency)) {
|
|
316
|
+
queue.push(dependency);
|
|
317
|
+
queued.add(dependency);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return selected;
|
|
322
|
+
}
|
|
186
323
|
async buildFrontendForPreview(rootPath) {
|
|
187
324
|
const packagePath = path.join(rootPath, 'package.json');
|
|
188
325
|
if (!fs.existsSync(packagePath)) {
|
|
@@ -256,12 +393,14 @@ export class FrontendPreviewService {
|
|
|
256
393
|
// Pulling every CSS/JS file from the workspace also ingested audit copies,
|
|
257
394
|
// rejected model drafts, and test harnesses into the production proof body.
|
|
258
395
|
// Fall back to expected/workspace files only for documents without links.
|
|
259
|
-
const
|
|
396
|
+
const directCssFiles = (linkedAssets.css.length > 0
|
|
260
397
|
? chooseExisting(linkedAssets.css, [])
|
|
261
398
|
: chooseExisting(expectedCss, cssFiles)).slice(0, 12);
|
|
262
|
-
const
|
|
399
|
+
const directJsFiles = (linkedAssets.js.length > 0
|
|
263
400
|
? chooseExisting(linkedAssets.js, [])
|
|
264
401
|
: chooseExisting(expectedJs, jsFiles)).slice(0, 12);
|
|
402
|
+
const selectedCssFiles = this.expandLinkedFrontendDependencyGraph(rootPath, directCssFiles, 'css');
|
|
403
|
+
const selectedJsFiles = this.expandLinkedFrontendDependencyGraph(rootPath, directJsFiles, 'js');
|
|
265
404
|
const css = selectedCssFiles.map((filePath) => fs.readFileSync(resolveWorkspacePath(rootPath, filePath), 'utf8')).join('\n\n');
|
|
266
405
|
const js = selectedJsFiles.map((filePath) => fs.readFileSync(resolveWorkspacePath(rootPath, filePath), 'utf8')).join('\n\n');
|
|
267
406
|
return { htmlPath, html, css, js, cssPaths: selectedCssFiles, jsPaths: selectedJsFiles };
|
|
@@ -318,11 +457,11 @@ export class FrontendPreviewService {
|
|
|
318
457
|
: null;
|
|
319
458
|
const runtimeInteraction = runtimeOptions
|
|
320
459
|
? (typeof this.dependencies.screenshotAdapter.captureRuntime === 'function'
|
|
321
|
-
? await this.dependencies.screenshotAdapter.captureRuntime(entryAbsolutePath, screenshotPath, runtimeOptions)
|
|
460
|
+
? await this.dependencies.screenshotAdapter.captureRuntime(entryAbsolutePath, screenshotPath, { ...runtimeOptions, workspaceRoot: rootPath })
|
|
322
461
|
: {
|
|
323
462
|
captured: false,
|
|
324
463
|
passed: false,
|
|
325
|
-
error: 'required
|
|
464
|
+
error: 'required loopback browser interaction adapter is unavailable',
|
|
326
465
|
})
|
|
327
466
|
: null;
|
|
328
467
|
const screenshot = runtimeInteraction
|
|
@@ -335,7 +474,7 @@ export class FrontendPreviewService {
|
|
|
335
474
|
...previewGate,
|
|
336
475
|
passed: false,
|
|
337
476
|
error: `${previewGate.error ? `${previewGate.error} ` : ''}${runtimeFailure
|
|
338
|
-
? (runtimeInteraction.error || 'Required
|
|
477
|
+
? (runtimeInteraction.error || 'Required loopback browser interaction proof failed.')
|
|
339
478
|
: `Required screenshot proof is unavailable: ${screenshot.error || 'capture failed'}.`}`,
|
|
340
479
|
}
|
|
341
480
|
: previewGate;
|
|
@@ -444,7 +583,7 @@ export class FrontendPreviewService {
|
|
|
444
583
|
const errors = [];
|
|
445
584
|
if (context.localScreenshotProof === true) {
|
|
446
585
|
const summary = {
|
|
447
|
-
hasViewportMeta:
|
|
586
|
+
hasViewportMeta: hasViewportMeta(html),
|
|
448
587
|
hasResponsiveSignals: /@media\b/i.test(`${html}\n${css}`),
|
|
449
588
|
hasInteractiveSignals: /<script[\s>]/i.test(html) || js.trim().length > 0,
|
|
450
589
|
sectionCount: (html.match(/<section\b/gi) || []).length,
|
|
@@ -486,7 +625,7 @@ export class FrontendPreviewService {
|
|
|
486
625
|
qualityProfile: isConstrainedStaticFrontendRequest(String(context.rawPrompt || message || ''))
|
|
487
626
|
? 'constrained-static'
|
|
488
627
|
: 'auto',
|
|
489
|
-
html,
|
|
628
|
+
html: canonicalizePreviewHtmlAttributes(html),
|
|
490
629
|
css,
|
|
491
630
|
js,
|
|
492
631
|
entryPath: artifacts.htmlPath,
|
|
@@ -505,8 +644,14 @@ export class FrontendPreviewService {
|
|
|
505
644
|
error: 'Preview proof skipped: payload exceeds size limit.',
|
|
506
645
|
};
|
|
507
646
|
}
|
|
508
|
-
const
|
|
509
|
-
|| process.env.
|
|
647
|
+
const internalServiceKeyCandidate = process.env.VIGTHORIA_TEMPLATE_SERVICE_KEY
|
|
648
|
+
|| process.env.VIGTHORIA_V3_SERVICE_KEY
|
|
649
|
+
|| process.env.V3_SERVICE_KEY
|
|
650
|
+
|| this.dependencies.getServiceKey?.();
|
|
651
|
+
const internalServiceKey = String(internalServiceKeyCandidate || '').trim();
|
|
652
|
+
if (internalServiceKey && /[\r\n\0]/.test(internalServiceKey)) {
|
|
653
|
+
throw new Error('Stored Template Service capability contains invalid header characters.');
|
|
654
|
+
}
|
|
510
655
|
const authToken = this.dependencies.getAccessToken();
|
|
511
656
|
const response = await this.dependencies.fetch(`${baseUrl}/preview-proof`, {
|
|
512
657
|
method: 'POST',
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect static ESM syntax even when a compact generator places exports after
|
|
3
|
+
* declarations on the same line. Dynamic import() alone remains valid in
|
|
4
|
+
* CommonJS syntax mode and does not require ESM parsing.
|
|
5
|
+
*/
|
|
6
|
+
export declare function usesJavaScriptModuleSyntax(source: string, filePath?: string): boolean;
|
|
7
|
+
export type JavaScriptSyntaxMode = 'script' | 'module';
|
|
8
|
+
/**
|
|
9
|
+
* Return the grammars that may legitimately own a JavaScript source file.
|
|
10
|
+
*
|
|
11
|
+
* A plain `.js` file is ambiguous until its package or HTML loader is known.
|
|
12
|
+
* In particular, top-level await can make otherwise valid browser-module code
|
|
13
|
+
* fail a CommonJS-only preflight without leaving a static import/export token
|
|
14
|
+
* for the lightweight detector above. Trying both grammars keeps the gate
|
|
15
|
+
* strict (one parser must accept the complete source) without rejecting valid
|
|
16
|
+
* browser modules or classic scripts. Explicit `.mjs` and `.cjs` identities
|
|
17
|
+
* remain single-grammar and therefore fail closed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveJavaScriptSyntaxModes(source: string, filePath?: string): JavaScriptSyntaxMode[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* Detect static ESM syntax even when a compact generator places exports after
|
|
4
|
+
* declarations on the same line. Dynamic import() alone remains valid in
|
|
5
|
+
* CommonJS syntax mode and does not require ESM parsing.
|
|
6
|
+
*/
|
|
7
|
+
export function usesJavaScriptModuleSyntax(source, filePath = '.js') {
|
|
8
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
9
|
+
if (extension === '.mjs')
|
|
10
|
+
return true;
|
|
11
|
+
if (extension === '.cjs')
|
|
12
|
+
return false;
|
|
13
|
+
return /(?:^|[;}\n]|\*\/)[ \t]*(?:import(?!\s*\()|export\b)/m.test(source);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Return the grammars that may legitimately own a JavaScript source file.
|
|
17
|
+
*
|
|
18
|
+
* A plain `.js` file is ambiguous until its package or HTML loader is known.
|
|
19
|
+
* In particular, top-level await can make otherwise valid browser-module code
|
|
20
|
+
* fail a CommonJS-only preflight without leaving a static import/export token
|
|
21
|
+
* for the lightweight detector above. Trying both grammars keeps the gate
|
|
22
|
+
* strict (one parser must accept the complete source) without rejecting valid
|
|
23
|
+
* browser modules or classic scripts. Explicit `.mjs` and `.cjs` identities
|
|
24
|
+
* remain single-grammar and therefore fail closed.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveJavaScriptSyntaxModes(source, filePath = '.js') {
|
|
27
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
28
|
+
if (extension === '.mjs')
|
|
29
|
+
return ['module'];
|
|
30
|
+
if (extension === '.cjs')
|
|
31
|
+
return ['script'];
|
|
32
|
+
if (usesJavaScriptModuleSyntax(source, filePath))
|
|
33
|
+
return ['module'];
|
|
34
|
+
return ['script', 'module'];
|
|
35
|
+
}
|
|
@@ -20,7 +20,7 @@ export function applyLocalTestfarmDefaults() {
|
|
|
20
20
|
process.env.VIGTHORIA_HYPERLOOP_URL ??= 'http://127.0.0.1:8020/api/hyperloop/health';
|
|
21
21
|
process.env.VIGTHORIA_HYPERLOOP_EXECUTE_URL ??= 'http://127.0.0.1:8020/api/hyperloop/execute';
|
|
22
22
|
process.env.VIGTHORIA_HYPERLOOP_MODULES_URL ??= 'http://127.0.0.1:8020/api/hyperloop/modules';
|
|
23
|
-
process.env.VIGTHORIA_V3_AGENT_URL ??= 'http://127.0.0.1:
|
|
23
|
+
process.env.VIGTHORIA_V3_AGENT_URL ??= 'http://127.0.0.1:8032';
|
|
24
24
|
process.env.VIGTHORIA_TEMPLATE_SERVICE_URL ??= 'http://127.0.0.1:4011';
|
|
25
25
|
process.env.VIGTHORIA_HYPERLOOP_TIMEOUT_MS ??= '180000';
|
|
26
26
|
const testToken = String(process.env.VIGTHORIA_LOCAL_TEST_TOKEN || '').trim();
|
|
@@ -39,4 +39,14 @@ export declare class MutationJournal {
|
|
|
39
39
|
commit(): MutationJournalReport;
|
|
40
40
|
rollback(): MutationJournalReport;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Last-resort synchronous rollback for process signals.
|
|
44
|
+
*
|
|
45
|
+
* APIClient normally owns the foreground journal, but command teardown or a
|
|
46
|
+
* second lifecycle wrapper must never be able to detach an open transaction
|
|
47
|
+
* from SIGINT/SIGTERM protection. Journals register at construction and
|
|
48
|
+
* unregister only after commit/rollback, so this fallback closes every still
|
|
49
|
+
* open workspace transaction before the original signal is re-raised.
|
|
50
|
+
*/
|
|
51
|
+
export declare function rollbackActiveMutationJournals(): MutationJournalReport[];
|
|
42
52
|
export declare function finalizeMutationTransaction(journal: MutationJournal, proofSucceeded: boolean): MutationJournalReport;
|
|
@@ -6,6 +6,7 @@ const IGNORE_NAMES = new Set(['.git', 'node_modules', '.vigthoria']);
|
|
|
6
6
|
const DEFAULT_MAX_BACKUP_BYTES = 128 * 1024 * 1024;
|
|
7
7
|
const DEFAULT_MAX_FILE_BYTES = 16 * 1024 * 1024;
|
|
8
8
|
const DEFAULT_MAX_FILES = 20_000;
|
|
9
|
+
const ACTIVE_MUTATION_JOURNALS = new Set();
|
|
9
10
|
function hashFile(filePath) {
|
|
10
11
|
const hash = createHash('sha256');
|
|
11
12
|
const descriptor = fs.openSync(filePath, 'r');
|
|
@@ -52,6 +53,7 @@ export class MutationJournal {
|
|
|
52
53
|
this.manifestPath = path.join(this.journalDirectory, 'journal.json');
|
|
53
54
|
this.captureBaseline();
|
|
54
55
|
this.persist('open', [], [], false);
|
|
56
|
+
ACTIVE_MUTATION_JOURNALS.add(this);
|
|
55
57
|
}
|
|
56
58
|
listWorkspaceDirectories() {
|
|
57
59
|
const directories = [];
|
|
@@ -284,104 +286,130 @@ export class MutationJournal {
|
|
|
284
286
|
};
|
|
285
287
|
}
|
|
286
288
|
commit() {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
289
|
+
try {
|
|
290
|
+
const affectedPaths = this.changedPaths();
|
|
291
|
+
this.persist('committed', affectedPaths, [], false);
|
|
292
|
+
fs.rmSync(this.backupDirectory, { recursive: true, force: true });
|
|
293
|
+
return this.report(affectedPaths, [], false);
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
ACTIVE_MUTATION_JOURNALS.delete(this);
|
|
297
|
+
}
|
|
291
298
|
}
|
|
292
299
|
rollback() {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const safe = assertSafeRelativePath(relativePath);
|
|
298
|
-
const absolute = path.resolve(this.rootPath, ...safe.split('/'));
|
|
299
|
-
const relative = path.relative(this.rootPath, absolute);
|
|
300
|
-
if (relative.startsWith('..') || path.isAbsolute(relative))
|
|
301
|
-
throw new Error('path escaped workspace');
|
|
300
|
+
try {
|
|
301
|
+
const affectedPaths = this.changedPaths();
|
|
302
|
+
const failures = [];
|
|
303
|
+
for (const relativePath of affectedPaths.filter((entry) => !this.baseline.has(entry))) {
|
|
302
304
|
try {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
+
const safe = assertSafeRelativePath(relativePath);
|
|
306
|
+
const absolute = path.resolve(this.rootPath, ...safe.split('/'));
|
|
307
|
+
const relative = path.relative(this.rootPath, absolute);
|
|
308
|
+
if (relative.startsWith('..') || path.isAbsolute(relative))
|
|
309
|
+
throw new Error('path escaped workspace');
|
|
310
|
+
try {
|
|
311
|
+
fs.lstatSync(absolute);
|
|
312
|
+
fs.unlinkSync(absolute);
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
if (error?.code !== 'ENOENT')
|
|
316
|
+
throw error;
|
|
317
|
+
}
|
|
305
318
|
}
|
|
306
319
|
catch (error) {
|
|
307
|
-
|
|
308
|
-
throw error;
|
|
320
|
+
failures.push(`${relativePath}: could not remove created file: ${error.message}`);
|
|
309
321
|
}
|
|
310
322
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const baseline = this.baseline.get(relativePath);
|
|
317
|
-
if (baseline.kind === 'file' && (!baseline.backupPath || !fs.existsSync(baseline.backupPath))) {
|
|
318
|
-
failures.push(`${relativePath}: rollback backup unavailable`);
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
try {
|
|
322
|
-
const destination = this.resolveRollbackDestination(relativePath);
|
|
323
|
-
this.removeRollbackDestination(destination);
|
|
324
|
-
if (baseline.kind === 'symlink') {
|
|
325
|
-
if (baseline.linkTarget === undefined)
|
|
326
|
-
throw new Error('rollback link target unavailable');
|
|
327
|
-
fs.symlinkSync(baseline.linkTarget, destination, baseline.linkType);
|
|
323
|
+
for (const relativePath of affectedPaths.filter((entry) => this.baseline.has(entry))) {
|
|
324
|
+
const baseline = this.baseline.get(relativePath);
|
|
325
|
+
if (baseline.kind === 'file' && (!baseline.backupPath || !fs.existsSync(baseline.backupPath))) {
|
|
326
|
+
failures.push(`${relativePath}: rollback backup unavailable`);
|
|
327
|
+
continue;
|
|
328
328
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
fs.
|
|
336
|
-
fs.renameSync(temporary, destination);
|
|
329
|
+
try {
|
|
330
|
+
const destination = this.resolveRollbackDestination(relativePath);
|
|
331
|
+
this.removeRollbackDestination(destination);
|
|
332
|
+
if (baseline.kind === 'symlink') {
|
|
333
|
+
if (baseline.linkTarget === undefined)
|
|
334
|
+
throw new Error('rollback link target unavailable');
|
|
335
|
+
fs.symlinkSync(baseline.linkTarget, destination, baseline.linkType);
|
|
337
336
|
}
|
|
338
|
-
|
|
337
|
+
else {
|
|
338
|
+
if (!baseline.backupPath || !fs.existsSync(baseline.backupPath))
|
|
339
|
+
throw new Error('rollback backup unavailable');
|
|
340
|
+
const temporary = `${destination}.rollback-${randomUUID()}`;
|
|
339
341
|
try {
|
|
340
|
-
fs.
|
|
342
|
+
fs.copyFileSync(baseline.backupPath, temporary, fs.constants.COPYFILE_EXCL);
|
|
343
|
+
fs.chmodSync(temporary, baseline.mode ?? 0o600);
|
|
344
|
+
fs.renameSync(temporary, destination);
|
|
341
345
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
346
|
+
finally {
|
|
347
|
+
try {
|
|
348
|
+
fs.unlinkSync(temporary);
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (error?.code !== 'ENOENT')
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
345
354
|
}
|
|
346
355
|
}
|
|
347
356
|
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
failures.push(`${relativePath}: restore failed: ${error.message}`);
|
|
359
|
+
}
|
|
348
360
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
catch (error) {
|
|
358
|
-
failures.push(`${relativePath}: directory restore failed: ${error.message}`);
|
|
361
|
+
for (const relativePath of [...this.baselineDirectories].sort((a, b) => a.length - b.length)) {
|
|
362
|
+
try {
|
|
363
|
+
fs.mkdirSync(resolveWorkspacePath(this.rootPath, relativePath, { allowMissing: true }), { recursive: true });
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
failures.push(`${relativePath}: directory restore failed: ${error.message}`);
|
|
367
|
+
}
|
|
359
368
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
369
|
+
for (const relativePath of this.listWorkspaceDirectories().sort((a, b) => b.length - a.length)) {
|
|
370
|
+
if (this.baselineDirectories.has(relativePath))
|
|
371
|
+
continue;
|
|
372
|
+
try {
|
|
373
|
+
const directory = resolveWorkspacePath(this.rootPath, relativePath);
|
|
374
|
+
if (fs.readdirSync(directory).length === 0)
|
|
375
|
+
fs.rmdirSync(directory);
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
failures.push(`${relativePath}: created directory remains: ${error.message}`);
|
|
379
|
+
}
|
|
368
380
|
}
|
|
369
|
-
|
|
370
|
-
|
|
381
|
+
const residual = this.changedPaths();
|
|
382
|
+
for (const relativePath of residual) {
|
|
383
|
+
if (!failures.some((failure) => failure.startsWith(`${relativePath}:`))) {
|
|
384
|
+
failures.push(`${relativePath}: mutation remains after rollback`);
|
|
385
|
+
}
|
|
371
386
|
}
|
|
387
|
+
const partial = failures.length > 0;
|
|
388
|
+
this.persist(partial ? 'partial_mutation' : 'rolled_back', affectedPaths, failures, !partial);
|
|
389
|
+
fs.rmSync(this.backupDirectory, { recursive: true, force: true });
|
|
390
|
+
return this.report(affectedPaths, failures, !partial);
|
|
372
391
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (!failures.some((failure) => failure.startsWith(`${relativePath}:`))) {
|
|
376
|
-
failures.push(`${relativePath}: mutation remains after rollback`);
|
|
377
|
-
}
|
|
392
|
+
finally {
|
|
393
|
+
ACTIVE_MUTATION_JOURNALS.delete(this);
|
|
378
394
|
}
|
|
379
|
-
const partial = failures.length > 0;
|
|
380
|
-
this.persist(partial ? 'partial_mutation' : 'rolled_back', affectedPaths, failures, !partial);
|
|
381
|
-
fs.rmSync(this.backupDirectory, { recursive: true, force: true });
|
|
382
|
-
return this.report(affectedPaths, failures, !partial);
|
|
383
395
|
}
|
|
384
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Last-resort synchronous rollback for process signals.
|
|
399
|
+
*
|
|
400
|
+
* APIClient normally owns the foreground journal, but command teardown or a
|
|
401
|
+
* second lifecycle wrapper must never be able to detach an open transaction
|
|
402
|
+
* from SIGINT/SIGTERM protection. Journals register at construction and
|
|
403
|
+
* unregister only after commit/rollback, so this fallback closes every still
|
|
404
|
+
* open workspace transaction before the original signal is re-raised.
|
|
405
|
+
*/
|
|
406
|
+
export function rollbackActiveMutationJournals() {
|
|
407
|
+
const reports = [];
|
|
408
|
+
for (const journal of [...ACTIVE_MUTATION_JOURNALS]) {
|
|
409
|
+
reports.push(journal.rollback());
|
|
410
|
+
}
|
|
411
|
+
return reports;
|
|
412
|
+
}
|
|
385
413
|
export function finalizeMutationTransaction(journal, proofSucceeded) {
|
|
386
414
|
return proofSucceeded ? journal.commit() : journal.rollback();
|
|
387
415
|
}
|
|
@@ -14,14 +14,7 @@ const AUDIENCE_HOSTS = {
|
|
|
14
14
|
hyperloop: new Set(['coder.vigthoria.io', 'hyperloop.vigthoria.io']),
|
|
15
15
|
vigflow: new Set(['coder.vigthoria.io', 'workflow.vigthoria.io']),
|
|
16
16
|
bridge: new Set(['bridge.vigthoria.io', 'devtools.vigthoria.io']),
|
|
17
|
-
release: new Set([
|
|
18
|
-
'extension.vigthoria.io',
|
|
19
|
-
'landing.vigthoria.io',
|
|
20
|
-
'cli.vigthoria.io',
|
|
21
|
-
'coder.vigthoria.io',
|
|
22
|
-
'market.vigthoria.io',
|
|
23
|
-
'registry.npmjs.org',
|
|
24
|
-
]),
|
|
17
|
+
release: new Set(['extension.vigthoria.io']),
|
|
25
18
|
};
|
|
26
19
|
// `auto` exists only as a compatibility guard for legacy calls to the primary
|
|
27
20
|
// Coder origin. It must never mean "any Vigthoria host": doing so allows a
|
|
@@ -30,9 +23,13 @@ const AUDIENCE_HOSTS = {
|
|
|
30
23
|
// its audience explicitly.
|
|
31
24
|
const AUTO_CREDENTIAL_HOSTS = AUDIENCE_HOSTS.coder;
|
|
32
25
|
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
|
|
33
|
-
|
|
26
|
+
// 18032/18041 are the isolated CLI-lane candidate ports used by OS TestFarm.
|
|
27
|
+
// They remain unreachable unless the caller also opts into loopback services.
|
|
28
|
+
const LOOPBACK_PORTS = new Set(['4008', '4009', '4011', '4016', '49160', '8032', '18032', '18041', '8020', '5060', '5050', '8016', '9000']);
|
|
34
29
|
const TESTFARM_BRIDGE_HOSTS = new Set(['host.lan']);
|
|
35
|
-
|
|
30
|
+
// Remote TestFarm CLI guests reach the CLI-exclusive V3 runtime through
|
|
31
|
+
// 18032. Port 18030 is deliberately absent because it belongs to Desktop/IDE.
|
|
32
|
+
const TESTFARM_BRIDGE_PORTS = new Set(['14009', '18020', '18032', '4011']);
|
|
36
33
|
const SENSITIVE_HEADER_NAMES = new Set(['authorization', 'cookie', 'proxy-authorization', 'x-api-key', 'x-service-key']);
|
|
37
34
|
export const DEFAULT_NETWORK_TIMEOUT_MS = 30_000;
|
|
38
35
|
export class NetworkPolicyError extends Error {
|