vigthoria-cli 1.13.20 → 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 +63 -128
  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 +3 -42
  12. package/dist/commands/chat.js +378 -1123
  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 +23 -29
  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 -1589
  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 +44 -266
  71. package/dist/utils/api.js +647 -3180
  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 +220 -94
  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 +5 -6
  106. package/dist/utils/fastAgentRouter.js +17 -25
  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 +17 -3
  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 +85 -142
  167. package/install.sh +75 -80
  168. package/package.json +43 -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 -174
  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
@@ -5,8 +5,9 @@ import chalk from 'chalk';
5
5
  import { Marked } from 'marked';
6
6
  import { markedTerminal } from 'marked-terminal';
7
7
  import { createSpinner, CH } from '../utils/logger.js';
8
- import { APIClient, CLIError, classifyError, formatCLIError } from '../utils/api.js';
8
+ import { createAPIClient } from '../utils/api-client-factory.js';
9
9
  import { FileUtils } from '../utils/files.js';
10
+ import { CliCommandError, commandFailure, formatSuccessJson } from '../utils/command-contract.js';
10
11
  export class ReviewCommand {
11
12
  config;
12
13
  logger;
@@ -16,7 +17,7 @@ export class ReviewCommand {
16
17
  constructor(config, logger) {
17
18
  this.config = config;
18
19
  this.logger = logger;
19
- this.api = new APIClient(config, logger);
20
+ this.api = createAPIClient(config, logger);
20
21
  this.fileUtils = new FileUtils(process.cwd(), config.get('project').ignorePatterns);
21
22
  this.marked = new Marked();
22
23
  this.marked.use(markedTerminal({
@@ -29,20 +30,17 @@ export class ReviewCommand {
29
30
  async run(filePath, options) {
30
31
  // Check auth
31
32
  if (!this.config.isAuthenticated()) {
32
- this.logger.error('Not authenticated. Run: vigthoria login');
33
- return;
33
+ throw new CliCommandError('Not authenticated. Run: vigthoria login', { code: 'AUTH_REQUIRED', category: 'authentication' });
34
34
  }
35
35
  // Server-side token validation — fail fast instead of waiting for 401
36
36
  const tokenCheck = await this.api.validateToken();
37
37
  if (!tokenCheck.valid) {
38
- this.logger.error(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login');
39
- return;
38
+ throw new CliCommandError(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login', { code: 'AUTH_SESSION_INVALID', category: 'authentication' });
40
39
  }
41
40
  // Read file
42
41
  const file = this.fileUtils.readFile(filePath);
43
42
  if (!file) {
44
- this.logger.error(`File not found: ${filePath}`);
45
- return;
43
+ throw new CliCommandError(`File not found: ${filePath}`, { code: 'FILE_NOT_FOUND', category: 'configuration' });
46
44
  }
47
45
  this.logger.section(`Reviewing: ${file.relativePath}`);
48
46
  console.log(chalk.gray(`Language: ${file.language} | Lines: ${file.lines}`));
@@ -57,7 +55,7 @@ export class ReviewCommand {
57
55
  // Format output
58
56
  switch (options.format) {
59
57
  case 'json':
60
- console.log(JSON.stringify(review, null, 2));
58
+ console.log(formatSuccessJson('review', review));
61
59
  break;
62
60
  case 'markdown':
63
61
  this.printMarkdownReview(review, file.relativePath);
@@ -70,8 +68,7 @@ export class ReviewCommand {
70
68
  }
71
69
  catch (error) {
72
70
  spinner.stop();
73
- const cliErr = error instanceof CLIError ? error : classifyError(error);
74
- this.logger.error(formatCLIError(cliErr));
71
+ throw commandFailure(error, { code: 'REVIEW_FAILED' });
75
72
  }
76
73
  }
77
74
  printTextReview(review) {
@@ -84,7 +81,7 @@ export class ReviewCommand {
84
81
  // Issues
85
82
  if (review.issues.length > 0) {
86
83
  this.logger.section(`Issues (${review.issues.length})`);
87
- review.issues.forEach((issue, i) => {
84
+ review.issues.forEach((issue) => {
88
85
  const severityIcon = this.getSeverityIcon(issue.severity);
89
86
  const severityColor = this.getSeverityColor(issue.severity);
90
87
  console.log(severityColor(`${severityIcon} [${issue.type}]`) +
@@ -7,9 +7,8 @@ interface SecurityOptions {
7
7
  apply?: boolean;
8
8
  }
9
9
  export declare class SecurityCommand {
10
- private config;
11
10
  private logger;
12
- constructor(config: Config, logger: Logger);
11
+ constructor(_config: Config, logger: Logger);
13
12
  private getMcpBaseUrl;
14
13
  private resolveDir;
15
14
  private execute;
@@ -1,11 +1,12 @@
1
1
  import axios from 'axios';
2
2
  import path from 'path';
3
+ import { assertTrustedEndpoint, installAxiosNetworkPolicy } from '../utils/network-policy.js';
4
+ import { formatSuccessJson } from '../utils/command-contract.js';
3
5
  export class SecurityCommand {
4
- config;
5
6
  logger;
6
- constructor(config, logger) {
7
- this.config = config;
7
+ constructor(_config, logger) {
8
8
  this.logger = logger;
9
+ installAxiosNetworkPolicy(axios, 'mcp');
9
10
  }
10
11
  getMcpBaseUrl() {
11
12
  const fromEnv = process.env.VIGTHORIA_MCP_URL || process.env.MCP_SERVER_URL;
@@ -19,6 +20,11 @@ export class SecurityCommand {
19
20
  }
20
21
  async execute(tool, parameters) {
21
22
  const baseUrl = this.getMcpBaseUrl();
23
+ const endpoint = assertTrustedEndpoint(baseUrl, { audience: 'mcp' });
24
+ const loopback = ['localhost', '127.0.0.1', '::1'].includes(endpoint.hostname.toLowerCase());
25
+ if (!loopback && typeof parameters.dir === 'string' && path.isAbsolute(parameters.dir)) {
26
+ throw new Error('Remote MCP security execution requires an uploaded workspace binding; refusing to send a local absolute path.');
27
+ }
22
28
  const response = await axios.post(`${baseUrl}/mcp/execute`, {
23
29
  tool,
24
30
  parameters,
@@ -41,7 +47,7 @@ export class SecurityCommand {
41
47
  const dir = this.resolveDir(options.dir);
42
48
  const result = await this.execute('security_scan', { dir });
43
49
  if (options.json) {
44
- console.log(JSON.stringify(result, null, 2));
50
+ console.log(formatSuccessJson('security scan', result));
45
51
  return;
46
52
  }
47
53
  this.logger.info(`Security scan complete for: ${dir}`);
@@ -60,7 +66,7 @@ export class SecurityCommand {
60
66
  const dir = this.resolveDir(options.dir);
61
67
  const result = await this.execute('security_score', { dir });
62
68
  if (options.json) {
63
- console.log(JSON.stringify(result, null, 2));
69
+ console.log(formatSuccessJson('security score', result));
64
70
  return;
65
71
  }
66
72
  this.logger.info(`Security score for ${dir}`);
@@ -75,7 +81,7 @@ export class SecurityCommand {
75
81
  confirm: Boolean(options.apply),
76
82
  });
77
83
  if (options.json) {
78
- console.log(JSON.stringify(result, null, 2));
84
+ console.log(formatSuccessJson('security fix', result));
79
85
  return;
80
86
  }
81
87
  this.logger.info(`Security fix plan for ${dir}`);
@@ -0,0 +1,3 @@
1
+ import type { Command } from 'commander';
2
+ export declare function checkForUpdatesQuietly(version: string): Promise<void>;
3
+ export declare function registerUpdateCommand(program: Command, version: string): void;
@@ -0,0 +1,338 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { execFileSync as execFileSyncProcess } from 'node:child_process';
3
+ import * as fs from 'node:fs';
4
+ import * as os from 'node:os';
5
+ import * as path from 'node:path';
6
+ import axios from 'axios';
7
+ import chalk from 'chalk';
8
+ import { CH } from '../utils/logger.js';
9
+ import { isOfflineMode, isUpdateCheckSuppressed, readCachedLatestVersion, writeCachedLatestVersion } from '../utils/cli-state.js';
10
+ import { guardedFetch } from '../utils/network-policy.js';
11
+ import { safeChildProcessEnv } from '../utils/secret-policy.js';
12
+ import { CliCommandError, commandFailure } from '../utils/command-contract.js';
13
+ import { compareSemanticVersions as compareVersions, resolveUpdateSource } from '../utils/update-policy.js';
14
+ import { assertReleaseTransition, assertReleaseUrl, getReleasePolicy, parseReleaseManifestText, verifyReleaseSignature } from '../utils/release-policy.js';
15
+ import { installReleaseTransaction } from '../utils/release-install.js';
16
+ function maxVersion(...versions) {
17
+ let best = null;
18
+ for (const version of versions) {
19
+ if (!version)
20
+ continue;
21
+ if (!best || compareVersions(version, best) > 0)
22
+ best = version;
23
+ }
24
+ return best;
25
+ }
26
+ async function fetchNpmLatestVersion() {
27
+ try {
28
+ // Hard 5s timeout: a slow/blocked path to the public npm registry must
29
+ // never hang the caller (e.g. the Workbench's blocking `vigthoria update`
30
+ // invocation) indefinitely.
31
+ const latest = execFileSyncProcess(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['view', 'vigthoria-cli', 'version'], {
32
+ encoding: 'utf8',
33
+ stdio: ['pipe', 'pipe', 'pipe'],
34
+ windowsHide: true,
35
+ timeout: 5000,
36
+ env: safeChildProcessEnv(),
37
+ }).trim();
38
+ return latest || null;
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ }
44
+ const VIGTHORIA_DEFAULT_MANIFEST_URL = getReleasePolicy().origins.manifest;
45
+ async function downloadFile(url, targetPath, explicitLocal = false) {
46
+ let bytes;
47
+ if (explicitLocal) {
48
+ const response = await axios.get(url, {
49
+ responseType: 'arraybuffer',
50
+ timeout: 20000,
51
+ maxRedirects: 0,
52
+ validateStatus: (status) => status >= 200 && status < 300,
53
+ });
54
+ bytes = Buffer.from(response.data);
55
+ }
56
+ else {
57
+ const response = await guardedFetch(url, {}, { audience: 'release' });
58
+ if (!response.ok)
59
+ throw new Error(`Release download returned HTTP ${response.status}`);
60
+ bytes = Buffer.from(await response.arrayBuffer());
61
+ }
62
+ fs.writeFileSync(targetPath, bytes, { mode: 0o600, flag: 'wx' });
63
+ }
64
+ function sha256File(filePath) {
65
+ const hash = createHash('sha256');
66
+ const data = fs.readFileSync(filePath);
67
+ hash.update(data);
68
+ return hash.digest('hex');
69
+ }
70
+ function inspectReleaseArchive(archivePath) {
71
+ const output = execFileSyncProcess('tar', ['-xOf', archivePath, 'package/package.json'], {
72
+ encoding: 'utf8',
73
+ stdio: ['ignore', 'pipe', 'pipe'],
74
+ windowsHide: true,
75
+ timeout: 15_000,
76
+ env: safeChildProcessEnv(),
77
+ maxBuffer: 1024 * 1024,
78
+ });
79
+ const manifest = JSON.parse(output);
80
+ return { name: String(manifest.name || ''), version: String(manifest.version || '') };
81
+ }
82
+ function renderUpdateBanner(latestVersion, version) {
83
+ if (compareVersions(latestVersion, '1.4.0') >= 0 && compareVersions(version, '1.4.0') < 0) {
84
+ console.log(chalk.red.bold(`\n${CH.warnEmoji} SECURITY UPDATE AVAILABLE`));
85
+ console.log(chalk.red(` Version ${version} has security vulnerabilities.`));
86
+ console.log(chalk.yellow(` Please update to ${latestVersion} immediately:`));
87
+ console.log(chalk.white.bold(' npm install -g vigthoria-cli@latest\n'));
88
+ }
89
+ else {
90
+ console.log(chalk.yellow(`\n${CH.warnEmoji} Update available: ${version} -> ${latestVersion}`));
91
+ console.log(chalk.gray(' Run `vigthoria update` to install\n'));
92
+ }
93
+ }
94
+ // Check for updates quietly on startup. Cached (24 h) so we never block
95
+ // startup on `npm view`. Honours VIGTHORIA_OFFLINE / VIGTHORIA_NO_UPDATE_CHECK.
96
+ export async function checkForUpdatesQuietly(version) {
97
+ if (isUpdateCheckSuppressed()) {
98
+ return;
99
+ }
100
+ const cached = readCachedLatestVersion();
101
+ if (cached) {
102
+ if (compareVersions(cached, version) > 0) {
103
+ renderUpdateBanner(cached, version);
104
+ }
105
+ return;
106
+ }
107
+ // Cache miss: probe npm registry in the background, with a hard 5 s
108
+ // timeout. Result is cached for 24 h so subsequent runs are instant.
109
+ try {
110
+ const npmVersion = execFileSyncProcess(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['view', 'vigthoria-cli', 'version'], {
111
+ encoding: 'utf8',
112
+ timeout: 5000,
113
+ stdio: ['pipe', 'pipe', 'pipe'],
114
+ windowsHide: true,
115
+ env: safeChildProcessEnv(),
116
+ }).trim();
117
+ if (!npmVersion) {
118
+ return;
119
+ }
120
+ writeCachedLatestVersion(npmVersion);
121
+ if (compareVersions(npmVersion, version) > 0) {
122
+ renderUpdateBanner(npmVersion, version);
123
+ }
124
+ }
125
+ catch {
126
+ // Network or npm failure should never block CLI; do not cache failure.
127
+ }
128
+ }
129
+ export function registerUpdateCommand(program, version) {
130
+ // Update command - Check for and install updates
131
+ program
132
+ .command('update')
133
+ .alias('upgrade')
134
+ .description('Check for updates and upgrade Vigthoria CLI. Default manifest: https://extension.vigthoria.io/downloads/manifest.json')
135
+ .option('-c, --check', 'Only check for updates, don\'t install')
136
+ .option('-f, --from <target>', 'Install from exact vigthoria-cli@version, local .tgz, or trusted HTTPS .tgz')
137
+ .option('--sha256 <hex>', 'Required SHA-256 for HTTPS archives; optional verification for local archives')
138
+ .option('-m, --manifest <url>', 'Update manifest URL for server-driven releases')
139
+ .option('--channel <name>', 'Release channel to use from manifest (default: stable)', 'stable')
140
+ .option('--allow-downgrade', 'Allow installing an older version from custom update source')
141
+ .action(async (options) => {
142
+ const updateTarget = typeof options.from === 'string' ? options.from.trim() : '';
143
+ const manifestUrl = typeof options.manifest === 'string' ? options.manifest.trim() : VIGTHORIA_DEFAULT_MANIFEST_URL.trim();
144
+ const channel = typeof options.channel === 'string' ? options.channel.trim() : 'stable';
145
+ const allowDowngrade = !!options.allowDowngrade;
146
+ if (updateTarget) {
147
+ const source = resolveUpdateSource(updateTarget, { sha256: options.sha256 });
148
+ try {
149
+ if (options.check) {
150
+ console.log(chalk.cyan(`Update source validated (${source.kind}): ${updateTarget}`));
151
+ console.log(chalk.gray('Run `vigthoria update --from <target>` to install from this source'));
152
+ return;
153
+ }
154
+ console.log(chalk.cyan(`Installing update from ${updateTarget}...`));
155
+ let installTarget = source.kind === 'remote' ? '' : source.installTarget;
156
+ let updateTempDirectory = null;
157
+ try {
158
+ if (source.kind === 'remote') {
159
+ updateTempDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'vigthoria-update-'));
160
+ installTarget = path.join(updateTempDirectory, 'candidate.tgz');
161
+ await downloadFile(source.downloadUrl, installTarget);
162
+ }
163
+ if (source.expectedSha256) {
164
+ const actual = sha256File(installTarget).toLowerCase();
165
+ if (actual !== source.expectedSha256) {
166
+ throw new CliCommandError('Update archive checksum verification failed.', {
167
+ code: 'UPDATE_CHECKSUM_MISMATCH',
168
+ details: { expected: source.expectedSha256, actual },
169
+ });
170
+ }
171
+ }
172
+ const identity = source.kind === 'registry'
173
+ ? { name: getReleasePolicy().packageName, version: installTarget.slice(installTarget.lastIndexOf('@') + 1) }
174
+ : inspectReleaseArchive(installTarget);
175
+ if (identity.name !== getReleasePolicy().packageName) {
176
+ throw new CliCommandError('Update archive package identity is not vigthoria-cli.', { code: 'UPDATE_PACKAGE_IDENTITY_INVALID' });
177
+ }
178
+ if (!allowDowngrade && compareVersions(identity.version, version) < 0) {
179
+ throw new CliCommandError(`Downgrade from ${version} to ${identity.version} requires --allow-downgrade.`, { code: 'UPDATE_DOWNGRADE_PROHIBITED' });
180
+ }
181
+ const installed = installReleaseTransaction(installTarget, identity.version, {
182
+ ...(source.kind === 'registry' ? { npmRegistry: 'https://registry.npmjs.org/' } : {}),
183
+ });
184
+ console.log(chalk.gray(`Versioned installation: ${installed.versionDirectory}`));
185
+ console.log(chalk.gray(`Active pointer: ${installed.currentLink}`));
186
+ }
187
+ finally {
188
+ if (updateTempDirectory)
189
+ fs.rmSync(updateTempDirectory, { recursive: true, force: true });
190
+ }
191
+ console.log(chalk.green('Update installed successfully'));
192
+ console.log(chalk.gray('Please restart the CLI to use the new version'));
193
+ return;
194
+ }
195
+ catch (error) {
196
+ throw commandFailure(error, {
197
+ code: 'UPDATE_INSTALL_FAILED',
198
+ message: `Failed to install the validated ${source.kind} update source.`,
199
+ });
200
+ }
201
+ }
202
+ if (isOfflineMode()) {
203
+ console.log(chalk.yellow('Offline mode (VIGTHORIA_OFFLINE=1): skipping update check.'));
204
+ return;
205
+ }
206
+ const currentVersion = version;
207
+ let manifestEntry = null;
208
+ if (manifestUrl) {
209
+ try {
210
+ assertReleaseUrl(manifestUrl, 'manifest');
211
+ console.log(chalk.cyan(`Checking manifest channel ${channel}...`));
212
+ const response = await guardedFetch(manifestUrl, {
213
+ headers: { Accept: 'application/json' },
214
+ }, { audience: 'release', maxRedirects: getReleasePolicy().origins.maximumRedirects });
215
+ if (!response.ok)
216
+ throw new Error(`Manifest returned HTTP ${response.status}`);
217
+ const manifestText = await response.text();
218
+ manifestEntry = parseReleaseManifestText(manifestText, channel);
219
+ const signatureResponse = await guardedFetch(manifestEntry.signature.url, {
220
+ headers: { Accept: 'application/octet-stream' },
221
+ }, { audience: 'release', maxRedirects: getReleasePolicy().origins.maximumRedirects });
222
+ if (!signatureResponse.ok)
223
+ throw new Error(`Detached signature returned HTTP ${signatureResponse.status}`);
224
+ verifyReleaseSignature(channel, manifestEntry, await signatureResponse.text());
225
+ assertReleaseTransition(currentVersion, manifestEntry, channel, allowDowngrade);
226
+ }
227
+ catch (error) {
228
+ if (error instanceof CliCommandError)
229
+ throw error;
230
+ console.log(chalk.yellow(`Manifest check failed: ${error.message}`));
231
+ console.log(chalk.gray('Continuing with the exact-version npm registry fallback...'));
232
+ }
233
+ }
234
+ // The release manifest (server-curated, sha256-verified) is the
235
+ // authoritative source of truth for this private CLI. Only fall back
236
+ // to probing the public npm registry when the manifest itself has no
237
+ // usable entry - never let an npm version number override a valid,
238
+ // verified manifest entry (npm install -g against the public registry
239
+ // is slower, unverified, and has previously caused the Workbench's
240
+ // update button to hang for minutes).
241
+ let npmVersion = null;
242
+ if (manifestEntry?.version && manifestEntry?.url) {
243
+ console.log(chalk.gray('Release manifest has a valid entry; skipping public npm registry lookup.'));
244
+ }
245
+ else {
246
+ console.log(chalk.cyan('Checking npm registry...'));
247
+ npmVersion = await fetchNpmLatestVersion();
248
+ if (!npmVersion) {
249
+ console.log(chalk.yellow('Could not read latest version from npm registry.'));
250
+ }
251
+ }
252
+ const effectiveLatest = maxVersion(manifestEntry?.version, npmVersion);
253
+ if (!effectiveLatest) {
254
+ throw new CliCommandError('Unable to determine the latest CLI version from manifest or npm.', {
255
+ code: 'UPDATE_VERSION_UNAVAILABLE',
256
+ category: 'network',
257
+ });
258
+ }
259
+ if (!allowDowngrade && compareVersions(effectiveLatest, currentVersion) <= 0) {
260
+ console.log(chalk.green(`You are running the latest version (${currentVersion})`));
261
+ return;
262
+ }
263
+ console.log(chalk.yellow(`Update available: ${currentVersion} -> ${effectiveLatest}`));
264
+ if (manifestEntry?.notes && compareVersions(manifestEntry.version, effectiveLatest) >= 0) {
265
+ console.log(chalk.gray(` ${manifestEntry.notes}`));
266
+ }
267
+ if (options.check) {
268
+ console.log(chalk.gray('Run `vigthoria update` to install the update'));
269
+ return;
270
+ }
271
+ const manifestIsAuthoritative = Boolean(manifestEntry
272
+ && manifestEntry.url
273
+ && compareVersions(manifestEntry.version, effectiveLatest) >= 0
274
+ && compareVersions(manifestEntry.version, currentVersion) > 0);
275
+ if (manifestIsAuthoritative && manifestEntry) {
276
+ const updateTempDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'vigthoria-update-'));
277
+ const tmpFile = path.join(updateTempDirectory, 'candidate.tgz');
278
+ try {
279
+ console.log(chalk.cyan(`Downloading release package (${manifestEntry.version})...`));
280
+ await downloadFile(manifestEntry.url, tmpFile);
281
+ const expected = String(manifestEntry.sha256).toLowerCase();
282
+ const actual = sha256File(tmpFile).toLowerCase();
283
+ if (actual !== expected) {
284
+ throw new CliCommandError('Release checksum verification failed.', {
285
+ code: 'UPDATE_CHECKSUM_MISMATCH',
286
+ details: { expected, actual },
287
+ });
288
+ }
289
+ if (fs.statSync(tmpFile).size !== manifestEntry.size) {
290
+ throw new CliCommandError('Release archive size does not match signed manifest metadata.', {
291
+ code: 'UPDATE_SIZE_MISMATCH',
292
+ });
293
+ }
294
+ console.log(chalk.green('Checksum verification passed'));
295
+ console.log(chalk.cyan('Installing update...'));
296
+ const identity = inspectReleaseArchive(tmpFile);
297
+ if (identity.name !== manifestEntry.packageName || identity.version !== manifestEntry.packageVersion) {
298
+ throw new CliCommandError('Release archive package metadata conflicts with signed manifest identity.', {
299
+ code: 'UPDATE_PACKAGE_IDENTITY_INVALID',
300
+ });
301
+ }
302
+ const installed = installReleaseTransaction(tmpFile, manifestEntry.version);
303
+ console.log(chalk.gray(`Versioned installation: ${installed.versionDirectory}`));
304
+ console.log(chalk.green(`Updated to version ${manifestEntry.version}`));
305
+ console.log(chalk.gray('Please restart the CLI to use the new version'));
306
+ return;
307
+ }
308
+ finally {
309
+ fs.rmSync(updateTempDirectory, { recursive: true, force: true });
310
+ }
311
+ }
312
+ const npmSpec = npmVersion && compareVersions(npmVersion, currentVersion) > 0
313
+ ? `vigthoria-cli@${npmVersion}`
314
+ : `vigthoria-cli@${effectiveLatest}`;
315
+ try {
316
+ const registrySource = resolveUpdateSource(npmSpec);
317
+ if (registrySource.kind !== 'registry') {
318
+ throw new CliCommandError('Registry fallback did not resolve to an exact package version.', {
319
+ code: 'UPDATE_REGISTRY_SOURCE_INVALID',
320
+ category: 'configuration',
321
+ });
322
+ }
323
+ console.log(chalk.cyan(`Installing update from npm registry (${npmSpec})...`));
324
+ const installed = installReleaseTransaction(registrySource.installTarget, effectiveLatest, {
325
+ npmRegistry: 'https://registry.npmjs.org/',
326
+ });
327
+ console.log(chalk.gray(`Versioned installation: ${installed.versionDirectory}`));
328
+ console.log(chalk.green(`Updated to version ${effectiveLatest}`));
329
+ console.log(chalk.gray('Please restart the CLI to use the new version'));
330
+ }
331
+ catch (error) {
332
+ throw commandFailure(error, {
333
+ code: 'UPDATE_REGISTRY_INSTALL_FAILED',
334
+ message: `Failed to install the exact registry package ${npmSpec}.`,
335
+ });
336
+ }
337
+ });
338
+ }
@@ -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 registerV4Commands(program: Command, config: Config, logger: Logger): void;
@@ -0,0 +1,24 @@
1
+ import { V4Command } from './v4.js';
2
+ import { runV4Menu } from './v4-menu.js';
3
+ export function registerV4Commands(program, config, logger) {
4
+ new V4Command(config, logger).register(program);
5
+ program
6
+ .command('v4-menu')
7
+ .description('Internal: interactive V3/V4 agent + provider picker used by the V4 Operating Agent launcher')
8
+ .option('--default-agent <agent>', 'v3 | v4', 'v3')
9
+ .option('--default-provider <provider>', 'cloud | v3_local | byok', 'cloud')
10
+ .option('--byok-api-key <key>', 'Existing BYOK api key', '')
11
+ .option('--byok-base-url <url>', 'Existing BYOK base url', '')
12
+ .option('--byok-model <name>', 'Existing BYOK model', '')
13
+ .requiredOption('--out-file <path>', 'Write JSON selection result to this file')
14
+ .action(async (options) => {
15
+ await runV4Menu({
16
+ defaultAgent: options.defaultAgent,
17
+ defaultProvider: options.defaultProvider,
18
+ byokApiKey: options.byokApiKey,
19
+ byokBaseUrl: options.byokBaseUrl,
20
+ byokModel: options.byokModel,
21
+ outFile: options.outFile,
22
+ });
23
+ });
24
+ }
@@ -18,9 +18,9 @@ type V4Options = {
18
18
  nonInteractive?: boolean;
19
19
  python?: string;
20
20
  v4Path?: string;
21
+ timeoutMs?: string | number;
21
22
  };
22
23
  export declare class V4Command {
23
- private readonly _config;
24
24
  private readonly logger;
25
25
  constructor(_config: Config, logger: Logger);
26
26
  register(program: Command): void;
@@ -2,6 +2,8 @@ import { spawn } from 'child_process';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import chalk from 'chalk';
5
+ import { safeChildProcessEnv } from '../utils/secret-policy.js';
6
+ import { CliCommandError } from '../utils/command-contract.js';
5
7
  const DEFAULT_V4_AGENT_DIR_UNIX = '/var/www/V4-Operating-Agent';
6
8
  export function defaultV4PythonBin() {
7
9
  return process.env.VIGTHORIA_V4_PYTHON || (process.platform === 'win32' ? 'python' : 'python3');
@@ -20,10 +22,8 @@ export function defaultV4AgentDir() {
20
22
  return undefined;
21
23
  }
22
24
  export class V4Command {
23
- _config;
24
25
  logger;
25
26
  constructor(_config, logger) {
26
- this._config = _config;
27
27
  this.logger = logger;
28
28
  }
29
29
  register(program) {
@@ -45,6 +45,7 @@ export class V4Command {
45
45
  .option('--non-interactive', 'Disable interactive menu and use CLI options', false)
46
46
  .option('--python <bin>', 'Python binary to use', defaultV4PythonBin())
47
47
  .option('--v4-path <path>', 'V4 agent root path (or set VIGTHORIA_V4_AGENT_DIR)', defaultV4AgentDir())
48
+ .option('--timeout-ms <ms>', 'Maximum V4 process runtime in milliseconds', process.env.VIGTHORIA_V4_TIMEOUT_MS || '1800000')
48
49
  .action(async (options) => {
49
50
  await this.run(options);
50
51
  });
@@ -98,17 +99,54 @@ export class V4Command {
98
99
  args.push('--session-id', String(options.sessionId));
99
100
  }
100
101
  this.logger.info(chalk.cyan(`Launching V4 Operating Agent from ${v4Path}`));
102
+ const requestedTimeout = Number(options.timeoutMs ?? process.env.VIGTHORIA_V4_TIMEOUT_MS ?? 1_800_000);
103
+ if (!Number.isFinite(requestedTimeout) || requestedTimeout < 1_000 || requestedTimeout > 86_400_000) {
104
+ throw new CliCommandError('V4 timeout must be between 1000 and 86400000 milliseconds.', {
105
+ code: 'V4_TIMEOUT_INVALID',
106
+ category: 'usage',
107
+ });
108
+ }
109
+ const timeoutMs = Math.floor(requestedTimeout);
101
110
  const child = spawn(pythonBin, args, {
102
111
  cwd: v4Path,
103
112
  stdio: 'inherit',
104
- env: {
105
- ...process.env,
106
- PYTHONUNBUFFERED: '1',
107
- },
113
+ env: safeChildProcessEnv(process.env, { PYTHONUNBUFFERED: '1' }),
108
114
  });
109
115
  const exitCode = await new Promise((resolve, reject) => {
110
- child.on('error', reject);
111
- child.on('close', (code) => resolve(code ?? 1));
116
+ let interrupted = false;
117
+ let timedOut = false;
118
+ let forceKillTimer;
119
+ const onInterrupt = () => {
120
+ interrupted = true;
121
+ child.kill('SIGINT');
122
+ };
123
+ process.once('SIGINT', onInterrupt);
124
+ const timeout = setTimeout(() => {
125
+ timedOut = true;
126
+ child.kill('SIGTERM');
127
+ forceKillTimer = setTimeout(() => child.kill('SIGKILL'), 5_000);
128
+ forceKillTimer.unref();
129
+ }, timeoutMs);
130
+ timeout.unref();
131
+ const finish = () => {
132
+ clearTimeout(timeout);
133
+ if (forceKillTimer)
134
+ clearTimeout(forceKillTimer);
135
+ process.removeListener('SIGINT', onInterrupt);
136
+ };
137
+ child.on('error', (error) => {
138
+ finish();
139
+ reject(error);
140
+ });
141
+ child.on('close', (code) => {
142
+ finish();
143
+ if (interrupted)
144
+ reject(new CliCommandError('V4 agent cancelled by user.', { code: 'V4_CANCELLED', category: 'cancelled' }));
145
+ else if (timedOut)
146
+ reject(new CliCommandError(`V4 agent timed out after ${timeoutMs}ms.`, { code: 'V4_TIMEOUT', category: 'execution' }));
147
+ else
148
+ resolve(code ?? 1);
149
+ });
112
150
  });
113
151
  if (exitCode !== 0) {
114
152
  throw new Error(`V4 agent exited with status ${exitCode}`);
@@ -12,13 +12,11 @@ export interface WalletOptions {
12
12
  }
13
13
  export declare class WalletCommand {
14
14
  private config;
15
- private logger;
15
+ private authSession;
16
16
  constructor();
17
17
  register(program: Command): void;
18
18
  private getBaseUrl;
19
19
  private getToken;
20
- private getRefreshToken;
21
- private refreshAccessToken;
22
20
  private parseResponseBody;
23
21
  private apiFetch;
24
22
  showBalance(opts?: WalletOptions): Promise<void>;