vigthoria-cli 1.13.18 → 1.13.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +84 -135
  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 +5 -42
  12. package/dist/commands/chat.js +462 -1122
  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 +32 -42
  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 -1498
  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 +45 -269
  71. package/dist/utils/api.js +664 -3210
  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 +227 -99
  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 +6 -7
  106. package/dist/utils/fastAgentRouter.js +21 -30
  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 +34 -2
  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 +86 -143
  167. package/install.sh +76 -81
  168. package/package.json +44 -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 -172
  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
@@ -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;
@@ -1,17 +1,16 @@
1
- import { isServerRuntime } from '../utils/api.js';
1
+ import { hasLocalV3AgentCapability } from '../utils/runtime-capability.js';
2
2
  /**
3
3
  * fork.ts — Fork from an existing V3 agent run and stream the result.
4
4
  */
5
5
  import chalk from 'chalk';
6
6
  import { createRequire } from 'node:module';
7
7
  import { createSpinner, CH } from '../utils/logger.js';
8
+ import { CliCommandError, commandFailure, formatSuccessJson } from '../utils/command-contract.js';
8
9
  const require = createRequire(import.meta.url);
9
10
  export class ForkCommand {
10
11
  config;
11
- logger;
12
- constructor(config, logger) {
12
+ constructor(config, _logger) {
13
13
  this.config = config;
14
- this.logger = logger;
15
14
  }
16
15
  getHeaders() {
17
16
  const headers = { 'Content-Type': 'application/json' };
@@ -29,7 +28,7 @@ export class ForkCommand {
29
28
  }
30
29
  getBaseUrl() {
31
30
  const configuredApiUrl = String(this.config.get('apiUrl') || 'https://coder.vigthoria.io').replace(/\/$/, '');
32
- const allowLocal = isServerRuntime() && process.env.VIGTHORIA_ALLOW_LOCAL_V3_AGENT === '1';
31
+ const allowLocal = hasLocalV3AgentCapability();
33
32
  return (process.env.VIGTHORIA_V3_AGENT_URL ||
34
33
  process.env.V3_AGENT_URL ||
35
34
  (allowLocal ? 'http://127.0.0.1:8030' : null) ||
@@ -53,6 +52,13 @@ export class ForkCommand {
53
52
  const workspace = this.resolveWorkspaceRoot(project);
54
53
  const eventIndex = options.eventIndex || 0;
55
54
  const spinner = createSpinner(`Forking run ${runId}...`).start();
55
+ const streamController = new AbortController();
56
+ let interrupted = false;
57
+ const onInterrupt = () => {
58
+ interrupted = true;
59
+ streamController.abort();
60
+ };
61
+ process.once('SIGINT', onInterrupt);
56
62
  try {
57
63
  const baseUrl = this.getBaseUrl();
58
64
  const body = {
@@ -66,33 +72,57 @@ export class ForkCommand {
66
72
  method: 'POST',
67
73
  headers: this.getHeaders(),
68
74
  body: JSON.stringify(body),
75
+ signal: streamController.signal,
69
76
  });
70
77
  if (!resp.ok) {
71
78
  spinner.stop();
72
79
  if (resp.status === 404) {
73
- this.logger.error(`Run ${runId} not found or has no event log.`);
80
+ throw new CliCommandError(`Run ${runId} not found or has no event log.`, {
81
+ code: 'RUN_NOT_FOUND', status: resp.status,
82
+ });
74
83
  }
75
84
  else if (resp.status === 502 || resp.status === 503) {
76
- this.logger.error(`V3 run-history route is not available on ${baseUrl}. Backend may not expose /api/runs.`);
85
+ throw new CliCommandError(`V3 run-history route is not available on ${baseUrl}. Backend may not expose /api/runs.`, {
86
+ code: 'RUN_HISTORY_UNAVAILABLE', status: resp.status, category: 'network',
87
+ });
77
88
  }
78
89
  else {
79
- this.logger.error(`Fork failed: ${resp.status} ${resp.statusText}`);
90
+ throw new CliCommandError(`Fork failed: ${resp.status} ${resp.statusText}`, {
91
+ code: 'RUN_FORK_FAILED', status: resp.status,
92
+ });
80
93
  }
81
- return;
82
94
  }
83
95
  spinner.stop();
84
- console.log(chalk.bold(`\n${CH.success} Forked from ${chalk.cyan(runId)} at event ${eventIndex}\n`));
96
+ if (!options.json)
97
+ console.log(chalk.bold(`\n${CH.success} Forked from ${chalk.cyan(runId)} at event ${eventIndex}\n`));
85
98
  if (!resp.body) {
86
- this.logger.error('No response body');
87
- return;
99
+ throw new CliCommandError('Fork response did not contain an event stream', { code: 'RUN_FORK_STREAM_MISSING' });
88
100
  }
89
101
  // Stream SSE events
90
102
  const reader = resp.body.getReader();
91
103
  const decoder = new TextDecoder();
92
104
  let buffer = '';
93
105
  let toolCallNum = 0;
94
- while (true) {
95
- const { done, value } = await reader.read();
106
+ let completed = false;
107
+ const events = [];
108
+ const configuredIdle = Number.parseInt(process.env.VIGTHORIA_FORK_IDLE_TIMEOUT_MS || '', 10);
109
+ const idleTimeoutMs = Number.isFinite(configuredIdle) && configuredIdle > 0 ? configuredIdle : 60_000;
110
+ stream: while (true) {
111
+ let timer;
112
+ const { done, value } = await Promise.race([
113
+ reader.read(),
114
+ new Promise((_resolve, reject) => {
115
+ timer = setTimeout(() => {
116
+ streamController.abort();
117
+ reject(new CliCommandError(`Fork event stream was idle for ${idleTimeoutMs}ms.`, {
118
+ code: 'RUN_FORK_IDLE_TIMEOUT', category: 'network',
119
+ }));
120
+ }, idleTimeoutMs);
121
+ }),
122
+ ]).finally(() => {
123
+ if (timer)
124
+ clearTimeout(timer);
125
+ });
96
126
  if (done)
97
127
  break;
98
128
  buffer += decoder.decode(value, { stream: true });
@@ -103,14 +133,14 @@ export class ForkCommand {
103
133
  continue;
104
134
  const payload = line.slice(6).trim();
105
135
  if (payload === '[DONE]') {
106
- console.log(chalk.bold(`\n${CH.success} Fork run complete\n`));
107
- return;
136
+ completed = true;
137
+ break stream;
108
138
  }
109
139
  try {
110
140
  const evt = JSON.parse(payload);
111
141
  const type = evt.type || 'unknown';
142
+ events.push(evt);
112
143
  if (options.json) {
113
- console.log(JSON.stringify(evt));
114
144
  continue;
115
145
  }
116
146
  switch (type) {
@@ -144,21 +174,41 @@ export class ForkCommand {
144
174
  break;
145
175
  }
146
176
  case 'error':
147
- console.log(chalk.red(` ❌ ERROR: `) + (evt.message || '').substring(0, 200));
148
- break;
177
+ throw new CliCommandError(String(evt.message || 'Fork stream reported an error.'), {
178
+ code: 'RUN_FORK_STREAM_ERROR', details: evt,
179
+ });
149
180
  default:
150
181
  console.log(chalk.dim(` ${type}: ${JSON.stringify(evt).substring(0, 80)}`));
151
182
  }
152
183
  }
153
- catch {
154
- // skip unparseable lines
184
+ catch (error) {
185
+ if (error instanceof CliCommandError)
186
+ throw error;
187
+ throw new CliCommandError('Fork event stream contained malformed JSON.', {
188
+ code: 'RUN_FORK_STREAM_MALFORMED', details: { payload: payload.slice(0, 200) }, cause: error,
189
+ });
155
190
  }
156
191
  }
157
192
  }
193
+ if (!completed) {
194
+ throw new CliCommandError('Fork event stream ended without a completion marker.', { code: 'RUN_FORK_STREAM_INCOMPLETE' });
195
+ }
196
+ if (options.json) {
197
+ console.log(formatSuccessJson('fork', { runId, eventIndex, events }, { stream: true, eventCount: events.length }));
198
+ }
199
+ else {
200
+ console.log(chalk.bold(`\n${CH.success} Fork run complete\n`));
201
+ }
158
202
  }
159
203
  catch (err) {
160
204
  spinner.stop();
161
- this.logger.error(`Fork error: ${err.message}`);
205
+ if (interrupted) {
206
+ throw new CliCommandError('Fork cancelled by user.', { code: 'RUN_FORK_CANCELLED', category: 'cancelled', cause: err });
207
+ }
208
+ throw commandFailure(err, { code: 'RUN_FORK_FAILED' });
209
+ }
210
+ finally {
211
+ process.removeListener('SIGINT', onInterrupt);
162
212
  }
163
213
  }
164
214
  }
@@ -18,7 +18,7 @@ export declare class GameCommand {
18
18
  }): Promise<void>;
19
19
  validate(o: CommonOptions & {
20
20
  json?: boolean;
21
- }): Promise<boolean>;
21
+ }): Promise<true>;
22
22
  private project;
23
23
  private sources;
24
24
  private finish;