vigthoria-cli 1.13.31 → 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.
Files changed (51) hide show
  1. package/README.md +11 -56
  2. package/SECURITY_HARDENING.md +2 -4
  3. package/dist/commands/auth.js +16 -14
  4. package/dist/commands/cancel.d.ts +1 -0
  5. package/dist/commands/cancel.js +21 -4
  6. package/dist/commands/chat.d.ts +13 -0
  7. package/dist/commands/chat.js +170 -14
  8. package/dist/commands/fork.js +8 -3
  9. package/dist/commands/history.js +8 -3
  10. package/dist/commands/legion.js +1 -1
  11. package/dist/commands/replay.js +8 -3
  12. package/dist/commands/update-registration.js +63 -206
  13. package/dist/commands/v4-menu.d.ts +0 -1
  14. package/dist/commands/v4-menu.js +8 -6
  15. package/dist/commands/v4-registration.js +0 -2
  16. package/dist/commands/v4.js +9 -4
  17. package/dist/commands/wallet.d.ts +1 -1
  18. package/dist/commands/wallet.js +3 -3
  19. package/dist/index.js +2 -3
  20. package/dist/utils/agent-stream-state.d.ts +7 -0
  21. package/dist/utils/agent-stream-state.js +31 -0
  22. package/dist/utils/agentRunOutcome.js +5 -1
  23. package/dist/utils/api.d.ts +21 -3
  24. package/dist/utils/api.js +379 -550
  25. package/dist/utils/frontend-preview-service.d.ts +12 -2
  26. package/dist/utils/frontend-preview-service.js +214 -24
  27. package/dist/utils/javascript-syntax.d.ts +19 -0
  28. package/dist/utils/javascript-syntax.js +35 -0
  29. package/dist/utils/localTestMode.js +1 -1
  30. package/dist/utils/mutation-journal.d.ts +10 -0
  31. package/dist/utils/mutation-journal.js +104 -76
  32. package/dist/utils/network-policy.js +7 -10
  33. package/dist/utils/preview-screenshot-adapter.d.ts +111 -1
  34. package/dist/utils/preview-screenshot-adapter.js +990 -7
  35. package/dist/utils/release-install.d.ts +0 -1
  36. package/dist/utils/release-install.js +44 -7
  37. package/dist/utils/requestIntent.d.ts +1 -1
  38. package/dist/utils/requestIntent.js +17 -3
  39. package/dist/utils/tools.js +21 -15
  40. package/dist/utils/update-policy.d.ts +0 -4
  41. package/dist/utils/update-policy.js +5 -9
  42. package/dist/utils/v3-agent-client.js +25 -11
  43. package/install.ps1 +30 -50
  44. package/install.sh +12 -23
  45. package/package.json +4 -2
  46. package/release-policy.json +1 -5
  47. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +6 -7
  48. package/scripts/release/install-release.mjs +5 -4
  49. package/scripts/release/publish-cli-release.mjs +21 -8
  50. package/scripts/release/test-balanced-model-live.sh +4 -1
  51. package/scripts/release/validate-live-service-gates.sh +27 -5
@@ -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
- const affectedPaths = this.changedPaths();
288
- this.persist('committed', affectedPaths, [], false);
289
- fs.rmSync(this.backupDirectory, { recursive: true, force: true });
290
- return this.report(affectedPaths, [], false);
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
- const affectedPaths = this.changedPaths();
294
- const failures = [];
295
- for (const relativePath of affectedPaths.filter((entry) => !this.baseline.has(entry))) {
296
- try {
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
- fs.lstatSync(absolute);
304
- fs.unlinkSync(absolute);
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
- if (error?.code !== 'ENOENT')
308
- throw error;
320
+ failures.push(`${relativePath}: could not remove created file: ${error.message}`);
309
321
  }
310
322
  }
311
- catch (error) {
312
- failures.push(`${relativePath}: could not remove created file: ${error.message}`);
313
- }
314
- }
315
- for (const relativePath of affectedPaths.filter((entry) => this.baseline.has(entry))) {
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
- else {
330
- if (!baseline.backupPath || !fs.existsSync(baseline.backupPath))
331
- throw new Error('rollback backup unavailable');
332
- const temporary = `${destination}.rollback-${randomUUID()}`;
333
- try {
334
- fs.copyFileSync(baseline.backupPath, temporary, fs.constants.COPYFILE_EXCL);
335
- fs.chmodSync(temporary, baseline.mode ?? 0o600);
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
- finally {
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.unlinkSync(temporary);
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
- catch (error) {
343
- if (error?.code !== 'ENOENT')
344
- throw error;
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
- catch (error) {
350
- failures.push(`${relativePath}: restore failed: ${error.message}`);
351
- }
352
- }
353
- for (const relativePath of [...this.baselineDirectories].sort((a, b) => a.length - b.length)) {
354
- try {
355
- fs.mkdirSync(resolveWorkspacePath(this.rootPath, relativePath, { allowMissing: true }), { recursive: true });
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
- for (const relativePath of this.listWorkspaceDirectories().sort((a, b) => b.length - a.length)) {
362
- if (this.baselineDirectories.has(relativePath))
363
- continue;
364
- try {
365
- const directory = resolveWorkspacePath(this.rootPath, relativePath);
366
- if (fs.readdirSync(directory).length === 0)
367
- fs.rmdirSync(directory);
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
- catch (error) {
370
- failures.push(`${relativePath}: created directory remains: ${error.message}`);
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
- const residual = this.changedPaths();
374
- for (const relativePath of residual) {
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
- const LOOPBACK_PORTS = new Set(['4008', '4009', '4011', '4016', '49160', '8030', '8020', '5060', '5050', '8016', '9000']);
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
- const TESTFARM_BRIDGE_PORTS = new Set(['14009', '18020', '18030', '4011']);
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 {
@@ -1,17 +1,124 @@
1
+ import WebSocket from 'ws';
2
+ export type PreviewRuntimeInteractionOptions = {
3
+ key?: string;
4
+ expectText?: string;
5
+ requireStateChange?: boolean;
6
+ waitMs?: number;
7
+ timeoutMs?: number;
8
+ steps?: PreviewRuntimeStep[];
9
+ workspaceRoot?: string;
10
+ };
11
+ export type PreviewRuntimeStep = {
12
+ action: 'assert' | 'click' | 'fill' | 'select' | 'press' | 'focus' | 'wait' | 'reload';
13
+ selector?: string;
14
+ value?: string;
15
+ wait_ms?: number;
16
+ offset_x_ratio?: number;
17
+ offset_y_ratio?: number;
18
+ expect_selector?: string;
19
+ expect_count?: number;
20
+ expect_text?: string;
21
+ expect_text_not?: string;
22
+ expect_canvas_nonblank?: boolean;
23
+ expect_value?: string;
24
+ expect_visible?: boolean;
25
+ expect_hidden?: boolean;
26
+ expect_attribute?: string;
27
+ expect_attribute_value?: string;
28
+ expect_attribute_absent?: string;
29
+ expect_attribute_token?: string;
30
+ expect_attribute_token_absent?: string;
31
+ expect_focused?: boolean;
32
+ expect_focus_within?: string;
33
+ };
1
34
  export type PreviewScreenshotResult = {
2
35
  captured: boolean;
3
36
  error?: string;
4
37
  };
38
+ export type PreviewRuntimeInteractionResult = PreviewScreenshotResult & {
39
+ passed: boolean;
40
+ action?: string;
41
+ initialBodyText?: string;
42
+ finalBodyText?: string;
43
+ bodyChanged?: boolean;
44
+ visualChanged?: boolean;
45
+ runtimeErrors?: string[];
46
+ interactionStepsRequested?: number;
47
+ interactionStepsPassed?: number;
48
+ interactionFailures?: string[];
49
+ };
5
50
  export interface PreviewScreenshotPort {
6
51
  capture(entryAbsolutePath: string, screenshotPath: string): Promise<PreviewScreenshotResult>;
52
+ captureRuntime?(entryAbsolutePath: string, screenshotPath: string, options: PreviewRuntimeInteractionOptions): Promise<PreviewRuntimeInteractionResult>;
7
53
  }
8
54
  export type PreviewBrowserResolution = {
9
55
  executablePath: string;
10
56
  source: 'system';
11
57
  };
12
58
  export type PreviewBrowserRunner = (executablePath: string, args: readonly string[], platform: NodeJS.Platform, environment?: NodeJS.ProcessEnv) => Promise<void>;
59
+ type PreviewRuntimeBrowserObservation = {
60
+ action?: string;
61
+ initialBodyText: string;
62
+ finalBodyText: string;
63
+ bodyChanged: boolean;
64
+ visualChanged: boolean;
65
+ runtimeErrors: string[];
66
+ interactionStepsRequested?: number;
67
+ interactionStepsPassed?: number;
68
+ interactionFailures?: string[];
69
+ };
70
+ export type PreviewRuntimeBrowserRunner = (executablePath: string, args: readonly string[], platform: NodeJS.Platform, environment: NodeJS.ProcessEnv, screenshotPath: string, options: PreviewRuntimeInteractionOptions) => Promise<PreviewRuntimeBrowserObservation>;
71
+ export declare function assertPreviewBrowserSandboxUser(platform?: NodeJS.Platform, effectiveUserId?: number | undefined): void;
13
72
  export declare function resolvePreviewBrowserExecutable(environment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, exists?: (candidate: string) => boolean): PreviewBrowserResolution | null;
14
73
  export declare const runPreviewBrowser: PreviewBrowserRunner;
74
+ type CdpResponse = {
75
+ id?: number;
76
+ method?: string;
77
+ params?: Record<string, any>;
78
+ result?: Record<string, any>;
79
+ error?: {
80
+ message?: string;
81
+ };
82
+ sessionId?: string;
83
+ };
84
+ declare class PreviewCdpClient {
85
+ private readonly socket;
86
+ private nextId;
87
+ private readonly pending;
88
+ private readonly eventListeners;
89
+ constructor(socket: WebSocket);
90
+ onEvent(listener: (event: CdpResponse) => void): () => void;
91
+ send(method: string, params?: Record<string, any>, sessionId?: string): Promise<Record<string, any>>;
92
+ close(): void;
93
+ private rejectPending;
94
+ }
95
+ type RuntimeKeyDefinition = {
96
+ key: string;
97
+ code: string;
98
+ virtualKeyCode: number;
99
+ text?: string;
100
+ };
101
+ type RuntimeModifierDefinition = RuntimeKeyDefinition & {
102
+ bit: number;
103
+ };
104
+ export declare function runtimeKeyChordDefinition(rawKey?: string): {
105
+ key: RuntimeKeyDefinition;
106
+ modifiers: RuntimeModifierDefinition[];
107
+ modifierMask: number;
108
+ } | null;
109
+ export declare function applyRuntimeStepAction(client: PreviewCdpClient, sessionId: string, step: PreviewRuntimeStep): Promise<void>;
110
+ export declare function waitForRuntimeActionTarget(client: PreviewCdpClient, sessionId: string, selector: string, scenarioDeadline: number): Promise<void>;
111
+ export declare function runtimeAttributeExpectationFailures(step: Pick<PreviewRuntimeStep, 'expect_attribute' | 'expect_attribute_value' | 'expect_attribute_absent' | 'expect_attribute_token' | 'expect_attribute_token_absent'>, snapshot: {
112
+ attributeValue: string | null;
113
+ absentAttributeValue: string | null;
114
+ }): string[];
115
+ export declare function runtimeVisibilityExpectationFailures(step: Pick<PreviewRuntimeStep, 'expect_visible' | 'expect_hidden'>, snapshot: {
116
+ visible: boolean;
117
+ hiddenAttribute: boolean;
118
+ hiddenProperty: boolean;
119
+ computedDisplay: string;
120
+ }): string[];
121
+ export declare const runPreviewRuntimeBrowser: PreviewRuntimeBrowserRunner;
15
122
  /** Dependency-free system-browser adapter. No browser download or archive
16
123
  * extractor is shipped, and only fixed operating-system installation paths
17
124
  * are eligible.
@@ -23,8 +130,11 @@ export declare class SystemBrowserScreenshotAdapter implements PreviewScreenshot
23
130
  private readonly platform;
24
131
  private readonly exists;
25
132
  private readonly runner;
26
- constructor(environment?: NodeJS.ProcessEnv, allocateTemp?: (prefix: string, requestedMaxBytes?: number) => string, releaseTemp?: (directory: string) => void, platform?: NodeJS.Platform, exists?: (candidate: string) => boolean, runner?: PreviewBrowserRunner);
133
+ private readonly runtimeRunner;
134
+ constructor(environment?: NodeJS.ProcessEnv, allocateTemp?: (prefix: string, requestedMaxBytes?: number) => string, releaseTemp?: (directory: string) => void, platform?: NodeJS.Platform, exists?: (candidate: string) => boolean, runner?: PreviewBrowserRunner, runtimeRunner?: PreviewRuntimeBrowserRunner);
27
135
  capture(entryAbsolutePath: string, screenshotPath: string): Promise<PreviewScreenshotResult>;
136
+ captureRuntime(entryAbsolutePath: string, screenshotPath: string, options: PreviewRuntimeInteractionOptions): Promise<PreviewRuntimeInteractionResult>;
137
+ private captureRuntimeOnce;
28
138
  private captureOnce;
29
139
  }
30
140
  /** @deprecated Internal compatibility alias retained for older deep imports. */