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,12 +5,11 @@ import * as path from 'path';
5
5
  import { execFile, spawn } from 'child_process';
6
6
  import { promisify } from 'util';
7
7
  import { CH } from '../utils/logger.js';
8
+ import { safeChildProcessEnv } from '../utils/secret-policy.js';
9
+ import { CliCommandError, formatSuccessJson } from '../utils/command-contract.js';
8
10
  const execFileAsync = promisify(execFile);
9
11
  export class DeviceCommand {
10
- logger;
11
- constructor(_config, logger) {
12
- this.logger = logger;
13
- }
12
+ constructor(_config, _logger) { }
14
13
  async status(options = {}) {
15
14
  const adbPath = this.findAdbPath();
16
15
  const devices = adbPath ? await this.getDevices(adbPath) : [];
@@ -21,7 +20,7 @@ export class DeviceCommand {
21
20
  devices,
22
21
  };
23
22
  if (options.json) {
24
- console.log(JSON.stringify(payload, null, 2));
23
+ console.log(formatSuccessJson('device status', payload));
25
24
  return;
26
25
  }
27
26
  console.log();
@@ -49,7 +48,7 @@ export class DeviceCommand {
49
48
  const adbPath = this.requireAdbPath();
50
49
  const devices = await this.getDevices(adbPath);
51
50
  if (options.json) {
52
- console.log(JSON.stringify({ devices }, null, 2));
51
+ console.log(formatSuccessJson('device list', { devices }));
53
52
  return;
54
53
  }
55
54
  if (devices.length === 0) {
@@ -65,7 +64,7 @@ export class DeviceCommand {
65
64
  const deviceId = await this.resolveDeviceId(adbPath, options.device);
66
65
  const output = path.resolve(options.output || `vigthoria-android-screenshot-${this.timestamp()}.png`);
67
66
  const args = this.withDevice(deviceId, ['exec-out', 'screencap', '-p']);
68
- const result = await execFileAsync(adbPath, args, { encoding: 'buffer', maxBuffer: 20 * 1024 * 1024 });
67
+ const result = await execFileAsync(adbPath, args, { encoding: 'buffer', maxBuffer: 20 * 1024 * 1024, env: safeChildProcessEnv() });
69
68
  fs.writeFileSync(output, result.stdout);
70
69
  console.log(chalk.green(`Saved screenshot: ${output}`));
71
70
  }
@@ -78,7 +77,7 @@ export class DeviceCommand {
78
77
  const adbPath = this.requireAdbPath();
79
78
  const deviceId = await this.resolveDeviceId(adbPath, options.device);
80
79
  const result = await this.runAdb(adbPath, this.withDevice(deviceId, ['install', '-r', resolvedApk]), 120000);
81
- this.printResult(result, options.json);
80
+ this.printResult('device install', result, options.json);
82
81
  }
83
82
  async launch(packageName, options = {}) {
84
83
  if (!packageName)
@@ -89,7 +88,7 @@ export class DeviceCommand {
89
88
  ? ['shell', 'am', 'start', '-n', `${packageName}/${options.activity}`]
90
89
  : ['shell', 'monkey', '-p', packageName, '-c', 'android.intent.category.LAUNCHER', '1'];
91
90
  const result = await this.runAdb(adbPath, this.withDevice(deviceId, args));
92
- this.printResult(result, options.json);
91
+ this.printResult('device launch', result, options.json);
93
92
  }
94
93
  async logs(options = {}) {
95
94
  const adbPath = this.requireAdbPath();
@@ -98,10 +97,23 @@ export class DeviceCommand {
98
97
  const args = this.withDevice(deviceId, options.follow ? ['logcat'] : ['logcat', '-d', '-t', lines]);
99
98
  if (options.follow) {
100
99
  await new Promise((resolve, reject) => {
101
- const child = spawn(adbPath, args, { stdio: 'inherit' });
102
- child.on('error', reject);
100
+ const child = spawn(adbPath, args, { stdio: 'inherit', env: safeChildProcessEnv() });
101
+ let interrupted = false;
102
+ const onInterrupt = () => {
103
+ interrupted = true;
104
+ child.kill('SIGINT');
105
+ };
106
+ process.once('SIGINT', onInterrupt);
107
+ const finish = () => process.removeListener('SIGINT', onInterrupt);
108
+ child.on('error', (error) => {
109
+ finish();
110
+ reject(error);
111
+ });
103
112
  child.on('exit', (code) => {
104
- if (code && code !== 0)
113
+ finish();
114
+ if (interrupted)
115
+ reject(new CliCommandError('Device log stream cancelled by user.', { code: 'DEVICE_LOGS_CANCELLED', category: 'cancelled' }));
116
+ else if (code && code !== 0)
105
117
  reject(new Error(`adb logcat exited with code ${code}`));
106
118
  else
107
119
  resolve();
@@ -110,26 +122,26 @@ export class DeviceCommand {
110
122
  return;
111
123
  }
112
124
  const result = await this.runAdb(adbPath, args, 30000, 8 * 1024 * 1024);
113
- this.printResult(result, options.json);
125
+ this.printResult('device logs', result, options.json);
114
126
  }
115
127
  async tcpip(options = {}) {
116
128
  const adbPath = this.requireAdbPath();
117
129
  const deviceId = await this.resolveDeviceId(adbPath, options.device);
118
130
  const port = String(options.port || '5555');
119
131
  const result = await this.runAdb(adbPath, this.withDevice(deviceId, ['tcpip', port]));
120
- this.printResult(result, options.json);
132
+ this.printResult('device tcpip', result, options.json);
121
133
  }
122
134
  async connect(address, options = {}) {
123
135
  if (!address)
124
136
  throw new Error('Device address is required, for example 192.168.1.30:5555.');
125
137
  const adbPath = this.requireAdbPath();
126
138
  const result = await this.runAdb(adbPath, ['connect', address]);
127
- this.printResult(result, options.json);
139
+ this.printResult('device connect', result, options.json);
128
140
  }
129
141
  async disconnect(address, options = {}) {
130
142
  const adbPath = this.requireAdbPath();
131
143
  const result = await this.runAdb(adbPath, address ? ['disconnect', address] : ['disconnect']);
132
- this.printResult(result, options.json);
144
+ this.printResult('device disconnect', result, options.json);
133
145
  }
134
146
  async getDevices(adbPath) {
135
147
  const result = await this.runAdb(adbPath, ['devices', '-l']);
@@ -176,7 +188,7 @@ export class DeviceCommand {
176
188
  }
177
189
  async runAdb(adbPath, args, timeout = 30000, maxBuffer = 1024 * 1024) {
178
190
  try {
179
- const result = await execFileAsync(adbPath, args, { timeout, maxBuffer });
191
+ const result = await execFileAsync(adbPath, args, { timeout, maxBuffer, env: safeChildProcessEnv() });
180
192
  return { success: true, stdout: String(result.stdout || '').trim(), stderr: String(result.stderr || '').trim() };
181
193
  }
182
194
  catch (error) {
@@ -188,17 +200,21 @@ export class DeviceCommand {
188
200
  };
189
201
  }
190
202
  }
191
- printResult(result, json = false) {
203
+ printResult(command, result, json = false) {
204
+ if (!result.success) {
205
+ throw new CliCommandError(result.stderr || `${command} failed`, {
206
+ code: 'DEVICE_COMMAND_FAILED',
207
+ details: { command, stdout: result.stdout },
208
+ });
209
+ }
192
210
  if (json) {
193
- console.log(JSON.stringify(result, null, 2));
211
+ console.log(formatSuccessJson(command, result));
194
212
  return;
195
213
  }
196
214
  if (result.stdout)
197
215
  console.log(result.stdout);
198
216
  if (result.stderr)
199
217
  console.error(result.success ? chalk.gray(result.stderr) : chalk.red(result.stderr));
200
- if (!result.success)
201
- process.exitCode = 1;
202
218
  }
203
219
  requireAdbPath() {
204
220
  const adbPath = this.findAdbPath();
@@ -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 registerDoctorCommand(program: Command, config: Config, logger: Logger, version: string): void;
@@ -0,0 +1,200 @@
1
+ import { createRequire } from 'node:module';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import axios from 'axios';
6
+ import chalk from 'chalk';
7
+ import { isIOSEnvironment, resolveClientOsSlug } from '../utils/clientManifest.js';
8
+ import { createAPIClient } from '../utils/api-client-factory.js';
9
+ import { sanitizeUserFacingErrorText } from '../utils/api.js';
10
+ import { getCliStateFile, isOfflineMode, isUpdateCheckSuppressed } from '../utils/cli-state.js';
11
+ import { CliCommandError, formatSuccessJson } from '../utils/command-contract.js';
12
+ export function registerDoctorCommand(program, config, logger, version) {
13
+ program
14
+ .command('doctor')
15
+ .description('Run local Vigthoria CLI diagnostics (no network calls unless --check-api)')
16
+ .option('--check-api', 'Also probe Vigthoria API reachability', false)
17
+ .option('--json', 'Emit machine-readable JSON output', false)
18
+ .action(async (options) => {
19
+ const apiUrl = String(config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
20
+ const modelsApiUrl = String(config.get('modelsApiUrl') || 'https://api.vigthoria.io').replace(/\/$/, '');
21
+ const offline = isOfflineMode();
22
+ const updateSuppressed = isUpdateCheckSuppressed();
23
+ const subscription = config.get('subscription') || { plan: null, status: null, expiresAt: null };
24
+ const iosSandbox = isIOSEnvironment();
25
+ const platformSupport = iosSandbox
26
+ ? 'experimental-mobile-terminal-sandbox'
27
+ : process.platform === 'darwin'
28
+ ? 'supported-macos-desktop'
29
+ : process.platform === 'win32' || process.platform === 'linux'
30
+ ? 'supported-desktop'
31
+ : 'unsupported';
32
+ const nodeMatch = process.versions.node.match(/^(\d+)\.(\d+)/);
33
+ const nodeCompatible = Boolean(nodeMatch && (Number(nodeMatch[1]) > 20
34
+ || (Number(nodeMatch[1]) === 20 && Number(nodeMatch[2]) >= 19)));
35
+ const runtimeRequire = createRequire(import.meta.url);
36
+ const requiredRuntimePackages = ['axios', 'commander', 'inquirer', 'ws'];
37
+ const missingRuntimePackages = requiredRuntimePackages.filter((name) => {
38
+ try {
39
+ runtimeRequire.resolve(name);
40
+ return false;
41
+ }
42
+ catch {
43
+ return true;
44
+ }
45
+ });
46
+ const sessionsDir = path.join(os.homedir(), '.vigthoria', 'sessions');
47
+ let sessionStorageWritable = false;
48
+ let latestCheckpoint = null;
49
+ try {
50
+ fs.mkdirSync(sessionsDir, { recursive: true, mode: 0o700 });
51
+ const probePath = path.join(sessionsDir, `.doctor-${process.pid}-${Date.now()}.tmp`);
52
+ fs.writeFileSync(probePath, '{}', { encoding: 'utf8', mode: 0o600 });
53
+ fs.unlinkSync(probePath);
54
+ sessionStorageWritable = true;
55
+ const latestSessionFile = fs.readdirSync(sessionsDir)
56
+ .filter((name) => name.endsWith('.json') && name !== 'index.json')
57
+ .map((name) => ({ name, mtime: fs.statSync(path.join(sessionsDir, name)).mtimeMs }))
58
+ .sort((left, right) => right.mtime - left.mtime)[0]?.name;
59
+ if (latestSessionFile) {
60
+ const document = JSON.parse(fs.readFileSync(path.join(sessionsDir, latestSessionFile), 'utf8'));
61
+ const persisted = document?.data && typeof document.data === 'object' ? document.data : document;
62
+ const checkpoint = persisted?.activeAgentRun;
63
+ if (checkpoint && typeof checkpoint === 'object') {
64
+ latestCheckpoint = {
65
+ executionId: checkpoint.executionId || null,
66
+ status: checkpoint.status || null,
67
+ updatedAt: checkpoint.updatedAt || null,
68
+ resumable: ['submitted', 'running', 'failed'].includes(String(checkpoint.status || '')),
69
+ };
70
+ }
71
+ }
72
+ }
73
+ catch {
74
+ sessionStorageWritable = false;
75
+ }
76
+ const envToken = String(process.env.VIGTHORIA_AUTH_TOKEN || process.env.VIGTHORIA_TOKEN || '').trim();
77
+ const configuredToken = envToken || String(config.get('authToken') || '').trim();
78
+ const tokenSegments = configuredToken ? configuredToken.split('.').length : 0;
79
+ const report = {
80
+ cliVersion: version,
81
+ nodeVersion: process.version,
82
+ nodeCompatible,
83
+ runtimeDependencies: missingRuntimePackages.length === 0 ? 'complete' : `missing: ${missingRuntimePackages.join(', ')}`,
84
+ platform: `${process.platform} ${process.arch}`,
85
+ clientOs: resolveClientOsSlug(),
86
+ platformSupport,
87
+ platformLimitations: iosSandbox
88
+ ? 'Not a native iOS app. Requires an iSH-like Linux terminal with Node.js >=20.19, npm and filesystem access; Workbench and device tooling are unavailable.'
89
+ : null,
90
+ cwd: process.cwd(),
91
+ homeDir: os.homedir(),
92
+ configPath: config.getConfigPath(),
93
+ stateFile: getCliStateFile(),
94
+ apiUrl,
95
+ modelsApiUrl,
96
+ loggedIn: config.isAuthenticated(),
97
+ authToken: configuredToken
98
+ ? { present: true, source: envToken ? 'environment' : 'secure-config', structure: tokenSegments === 3 ? 'jwt' : 'opaque' }
99
+ : { present: false, source: null, structure: null },
100
+ sessionPersistence: {
101
+ directory: sessionsDir,
102
+ writable: sessionStorageWritable,
103
+ latestCheckpoint,
104
+ },
105
+ subscriptionPlan: subscription.plan || null,
106
+ subscriptionStatus: subscription.status || null,
107
+ offlineMode: offline,
108
+ updateCheckSuppressed: updateSuppressed,
109
+ envOverrides: {
110
+ VIGTHORIA_API_URL: process.env.VIGTHORIA_API_URL || null,
111
+ VIGTHORIA_V3_AGENT_URL: process.env.VIGTHORIA_V3_AGENT_URL || null,
112
+ VIGTHORIA_OFFLINE: process.env.VIGTHORIA_OFFLINE || null,
113
+ VIGTHORIA_TOKEN: process.env.VIGTHORIA_TOKEN ? '<set>' : null,
114
+ HYPERLOOP_SERVICE_KEY: process.env.HYPERLOOP_SERVICE_KEY ? '<set>' : null,
115
+ V3_SERVICE_KEY: process.env.V3_SERVICE_KEY ? '<set>' : null,
116
+ },
117
+ };
118
+ if (options.checkApi && !offline) {
119
+ const api = createAPIClient(config, logger);
120
+ if (configuredToken) {
121
+ try {
122
+ const tokenCheck = await api.validateToken({ allowNetworkFailOpen: false, enforceTokenShape: true });
123
+ report.authValidation = tokenCheck.valid ? 'authenticated' : `failed (${tokenCheck.error || 'invalid token'})`;
124
+ }
125
+ catch (error) {
126
+ report.authValidation = `unreachable (${sanitizeUserFacingErrorText(error.message)})`;
127
+ }
128
+ }
129
+ else {
130
+ report.authValidation = 'not checked (login required)';
131
+ }
132
+ try {
133
+ const probe = await axios.get(`${apiUrl}/api/health`, { timeout: 12000, validateStatus: () => true });
134
+ report.apiHealth = probe.status >= 200 && probe.status < 400 ? 'online' : `status ${probe.status}`;
135
+ }
136
+ catch (error) {
137
+ report.apiHealth = `unreachable (${error.message})`;
138
+ }
139
+ try {
140
+ const probe = await axios.get(`${modelsApiUrl}/health`, { timeout: 12000, validateStatus: () => true });
141
+ const body = typeof probe.data === 'string' ? probe.data : JSON.stringify(probe.data || {});
142
+ report.modelsApiHealth = probe.status >= 200 && probe.status < 400
143
+ ? 'online'
144
+ : probe.status === 503 && /pre-release validation|closed/i.test(body)
145
+ ? 'pre-release-closed (Agent mode uses the Coder gateway)'
146
+ : `status ${probe.status}`;
147
+ }
148
+ catch (error) {
149
+ report.modelsApiHealth = `unreachable (${error.message})`;
150
+ }
151
+ if (configuredToken) {
152
+ try {
153
+ const capabilities = await api.getCapabilityTruthStatus({ workspacePath: process.cwd() });
154
+ report.agentInfrastructure = {
155
+ overall: capabilities.overallOk ? 'online' : 'degraded',
156
+ v3Agent: capabilities.v3Agent.ok ? 'online' : capabilities.v3Agent.error || 'offline',
157
+ hyperLoop: capabilities.hyperLoop.ok ? 'online' : capabilities.hyperLoop.error || 'offline',
158
+ repoMemory: capabilities.repoMemory.ok ? 'online' : capabilities.repoMemory.error || 'offline',
159
+ };
160
+ }
161
+ catch (error) {
162
+ report.agentInfrastructure = `unreachable (${sanitizeUserFacingErrorText(error.message)})`;
163
+ }
164
+ }
165
+ }
166
+ const printHumanReport = () => {
167
+ console.log(chalk.bold('Vigthoria CLI diagnostics'));
168
+ for (const [key, value] of Object.entries(report)) {
169
+ if (value && typeof value === 'object') {
170
+ console.log(chalk.gray(`- ${key}:`));
171
+ for (const [subKey, subVal] of Object.entries(value)) {
172
+ console.log(chalk.gray(` - ${subKey}: ${subVal ?? '(none)'}`));
173
+ }
174
+ }
175
+ else {
176
+ console.log(chalk.gray(`- ${key}: ${value ?? '(none)'}`));
177
+ }
178
+ }
179
+ if (!options.checkApi) {
180
+ console.log(chalk.gray('\nTip: pass --check-api to verify API reachability.'));
181
+ }
182
+ };
183
+ const requiredLocalChecksPassed = nodeCompatible && missingRuntimePackages.length === 0 && sessionStorageWritable;
184
+ const coderReachable = !options.checkApi || offline || report.apiHealth === 'online';
185
+ if (!requiredLocalChecksPassed || !coderReachable) {
186
+ if (!options.json)
187
+ printHumanReport();
188
+ throw new CliCommandError('One or more required doctor checks failed.', {
189
+ code: 'DOCTOR_CHECK_FAILED',
190
+ details: report,
191
+ });
192
+ }
193
+ if (options.json) {
194
+ console.log(formatSuccessJson('doctor', report));
195
+ }
196
+ else {
197
+ printHumanReport();
198
+ }
199
+ });
200
+ }
@@ -4,8 +4,9 @@
4
4
  import chalk from 'chalk';
5
5
  import inquirer from 'inquirer';
6
6
  import { createSpinner } from '../utils/logger.js';
7
- import { APIClient, CLIError, classifyError, formatCLIError } from '../utils/api.js';
7
+ import { createAPIClient } from '../utils/api-client-factory.js';
8
8
  import { FileUtils } from '../utils/files.js';
9
+ import { CliCommandError, commandFailure } from '../utils/command-contract.js';
9
10
  export class EditCommand {
10
11
  config;
11
12
  logger;
@@ -14,26 +15,23 @@ export class EditCommand {
14
15
  constructor(config, logger) {
15
16
  this.config = config;
16
17
  this.logger = logger;
17
- this.api = new APIClient(config, logger);
18
+ this.api = createAPIClient(config, logger);
18
19
  this.fileUtils = new FileUtils(process.cwd(), config.get('project').ignorePatterns);
19
20
  }
20
21
  async run(filePath, options) {
21
22
  // Check auth
22
23
  if (!this.config.isAuthenticated()) {
23
- this.logger.error('Not authenticated. Run: vigthoria login');
24
- return;
24
+ throw new CliCommandError('Not authenticated. Run: vigthoria login', { code: 'AUTH_REQUIRED', category: 'authentication' });
25
25
  }
26
26
  // Server-side token validation — fail fast instead of waiting for 401
27
27
  const tokenCheck = await this.api.validateToken();
28
28
  if (!tokenCheck.valid) {
29
- this.logger.error(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login');
30
- return;
29
+ throw new CliCommandError(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login', { code: 'AUTH_SESSION_INVALID', category: 'authentication' });
31
30
  }
32
31
  // Read file
33
32
  const file = this.fileUtils.readFile(filePath);
34
33
  if (!file) {
35
- this.logger.error(`File not found: ${filePath}`);
36
- return;
34
+ throw new CliCommandError(`File not found: ${filePath}`, { code: 'FILE_NOT_FOUND', category: 'configuration' });
37
35
  }
38
36
  this.logger.section(`Editing: ${file.relativePath}`);
39
37
  console.log(chalk.gray(`Language: ${file.language} | Lines: ${file.lines}`));
@@ -43,13 +41,11 @@ export class EditCommand {
43
41
  if (!instruction) {
44
42
  // --apply requires --instruction to avoid hanging on non-interactive terminals
45
43
  if (options.apply) {
46
- this.logger.error('The --apply flag requires --instruction. Example: vigthoria edit file.ts --apply --instruction "fix the bug"');
47
- return;
44
+ throw new CliCommandError('The --apply flag requires --instruction. Example: vigthoria edit file.ts --apply --instruction "fix the bug"', { code: 'INVALID_OPTION_COMBINATION', category: 'usage' });
48
45
  }
49
46
  // Non-TTY stdin cannot prompt interactively
50
47
  if (!process.stdin.isTTY) {
51
- this.logger.error('No --instruction provided and stdin is not interactive. Use: vigthoria edit file.ts --instruction "..."');
52
- return;
48
+ throw new CliCommandError('No --instruction provided and stdin is not interactive. Use: vigthoria edit file.ts --instruction "..."', { code: 'INSTRUCTION_REQUIRED', category: 'usage' });
53
49
  }
54
50
  const answer = await inquirer.prompt([
55
51
  {
@@ -100,15 +96,14 @@ Return the complete modified file content:`,
100
96
  // Extract code from response
101
97
  let modifiedCode = this.extractCode(response.message, file.language);
102
98
  if (!modifiedCode) {
103
- this.logger.error('Failed to generate valid code changes');
104
- return;
99
+ throw new CliCommandError('Failed to generate valid code changes', { code: 'EDIT_RESPONSE_INVALID' });
105
100
  }
106
101
  // Always deduplicate — extractCode only dedupes on the no-fence
107
102
  // fallback path, so fenced responses (the common case) need this.
108
103
  modifiedCode = this.deduplicateCode(modifiedCode);
109
104
  // Show diff and apply
110
105
  if (options.apply) {
111
- await this.applyFix(file.path, file.content, modifiedCode);
106
+ await this.applyFix(file.path, modifiedCode);
112
107
  }
113
108
  else {
114
109
  await this.showDiffAndConfirm(file.path, file.content, modifiedCode);
@@ -116,27 +111,23 @@ Return the complete modified file content:`,
116
111
  }
117
112
  catch (error) {
118
113
  spinner.stop();
119
- const cliErr = error instanceof CLIError ? error : classifyError(error);
120
- this.logger.error(formatCLIError(cliErr));
114
+ throw commandFailure(error, { code: 'EDIT_FAILED' });
121
115
  }
122
116
  }
123
117
  async fix(filePath, options) {
124
118
  // Check auth
125
119
  if (!this.config.isAuthenticated()) {
126
- this.logger.error('Not authenticated. Run: vigthoria login');
127
- return;
120
+ throw new CliCommandError('Not authenticated. Run: vigthoria login', { code: 'AUTH_REQUIRED', category: 'authentication' });
128
121
  }
129
122
  // Server-side token validation — fail fast instead of waiting for 401
130
123
  const tokenCheck = await this.api.validateToken();
131
124
  if (!tokenCheck.valid) {
132
- this.logger.error(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login');
133
- return;
125
+ throw new CliCommandError(tokenCheck.error || 'Auth token is invalid. Run: vigthoria login', { code: 'AUTH_SESSION_INVALID', category: 'authentication' });
134
126
  }
135
127
  // Read file
136
128
  const file = this.fileUtils.readFile(filePath);
137
129
  if (!file) {
138
- this.logger.error(`File not found: ${filePath}`);
139
- return;
130
+ throw new CliCommandError(`File not found: ${filePath}`, { code: 'FILE_NOT_FOUND', category: 'configuration' });
140
131
  }
141
132
  this.logger.section(`Fixing: ${file.relativePath}`);
142
133
  console.log(chalk.gray(`Fix type: ${options.type} | Language: ${file.language}`));
@@ -159,7 +150,7 @@ Return the complete modified file content:`,
159
150
  console.log(chalk.yellow('1. Operator/character-level fix detected'));
160
151
  console.log();
161
152
  if (options.apply) {
162
- await this.applyFix(file.path, file.content, result.fixed);
153
+ await this.applyFix(file.path, result.fixed);
163
154
  }
164
155
  else {
165
156
  await this.showDiffAndConfirm(file.path, file.content, result.fixed);
@@ -180,7 +171,7 @@ Return the complete modified file content:`,
180
171
  });
181
172
  // Apply or confirm
182
173
  if (options.apply) {
183
- await this.applyFix(file.path, file.content, result.fixed);
174
+ await this.applyFix(file.path, result.fixed);
184
175
  }
185
176
  else {
186
177
  await this.showDiffAndConfirm(file.path, file.content, result.fixed);
@@ -188,8 +179,7 @@ Return the complete modified file content:`,
188
179
  }
189
180
  catch (error) {
190
181
  spinner.stop();
191
- const cliErr = error instanceof CLIError ? error : classifyError(error);
192
- this.logger.error(formatCLIError(cliErr));
182
+ throw commandFailure(error, { code: 'FIX_FAILED' });
193
183
  }
194
184
  }
195
185
  extractCode(response, language) {
@@ -359,10 +349,9 @@ Return the complete modified file content:`,
359
349
  });
360
350
  console.log();
361
351
  // Confirm
362
- const showDiffs = this.config.get('preferences').showDiffs;
363
352
  const autoApply = this.config.get('preferences').autoApplyFixes;
364
353
  if (autoApply) {
365
- await this.applyFix(filePath, original, modified);
354
+ await this.applyFix(filePath, modified);
366
355
  return;
367
356
  }
368
357
  // Non-TTY: show the diff but don't try to prompt — re-run with --apply
@@ -384,7 +373,7 @@ Return the complete modified file content:`,
384
373
  ]);
385
374
  switch (action) {
386
375
  case 'apply':
387
- await this.applyFix(filePath, original, modified);
376
+ await this.applyFix(filePath, modified);
388
377
  break;
389
378
  case 'view':
390
379
  this.showFullDiff(original, modified);
@@ -397,7 +386,7 @@ Return the complete modified file content:`,
397
386
  },
398
387
  ]);
399
388
  if (confirm) {
400
- await this.applyFix(filePath, original, modified);
389
+ await this.applyFix(filePath, modified);
401
390
  }
402
391
  break;
403
392
  case 'discard':
@@ -463,7 +452,7 @@ Return the complete modified file content:`,
463
452
  console.log(chalk.gray('─'.repeat(60)));
464
453
  console.log();
465
454
  }
466
- async applyFix(filePath, original, modified) {
455
+ async applyFix(filePath, modified) {
467
456
  // Create backup
468
457
  const backup = this.fileUtils.backupFile(filePath);
469
458
  if (backup) {
@@ -474,7 +463,7 @@ Return the complete modified file content:`,
474
463
  this.logger.success(`Changes applied to ${filePath}`);
475
464
  }
476
465
  else {
477
- this.logger.error('Failed to write changes');
466
+ throw new CliCommandError(`Failed to write changes to ${filePath}`, { code: 'FILE_WRITE_FAILED' });
478
467
  }
479
468
  }
480
469
  }
@@ -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 } 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 } from '../utils/command-contract.js';
10
11
  export class ExplainCommand {
11
12
  config;
12
13
  logger;
@@ -16,7 +17,7 @@ export class ExplainCommand {
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 ExplainCommand {
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
  // Get code to explain
48
46
  let codeToExplain = file.content;
@@ -80,8 +78,7 @@ export class ExplainCommand {
80
78
  }
81
79
  catch (error) {
82
80
  spinner.stop();
83
- const cliErr = error instanceof CLIError ? error : classifyError(error);
84
- this.logger.error(formatCLIError(cliErr));
81
+ throw commandFailure(error, { code: 'EXPLAIN_FAILED' });
85
82
  }
86
83
  }
87
84
  /**
@@ -7,8 +7,7 @@ interface ForkOptions {
7
7
  }
8
8
  export declare class ForkCommand {
9
9
  private config;
10
- private logger;
11
- constructor(config: Config, logger: Logger);
10
+ constructor(config: Config, _logger: Logger);
12
11
  private getHeaders;
13
12
  private getBaseUrl;
14
13
  private resolveWorkspaceRoot;