vigthoria-cli 1.13.20 → 1.13.23

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 (187) 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 +207 -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 +244 -198
  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 +368 -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 +124 -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 +15 -0
  128. package/dist/utils/preview-screenshot-adapter.js +51 -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/runtime-temp.d.ts +76 -0
  143. package/dist/utils/runtime-temp.js +492 -0
  144. package/dist/utils/secret-policy.d.ts +20 -0
  145. package/dist/utils/secret-policy.js +160 -0
  146. package/dist/utils/session.d.ts +17 -3
  147. package/dist/utils/session.js +354 -110
  148. package/dist/utils/tool-approval-service.d.ts +17 -0
  149. package/dist/utils/tool-approval-service.js +78 -0
  150. package/dist/utils/tool-capability-providers.d.ts +57 -0
  151. package/dist/utils/tool-capability-providers.js +57 -0
  152. package/dist/utils/tools.d.ts +2 -23
  153. package/dist/utils/tools.js +226 -512
  154. package/dist/utils/update-policy.d.ts +18 -0
  155. package/dist/utils/update-policy.js +137 -0
  156. package/dist/utils/v3-agent-client.d.ts +40 -0
  157. package/dist/utils/v3-agent-client.js +165 -0
  158. package/dist/utils/v3-workspace-service.d.ts +28 -0
  159. package/dist/utils/v3-workspace-service.js +279 -0
  160. package/dist/utils/vigflow-client.d.ts +95 -0
  161. package/dist/utils/vigflow-client.js +105 -0
  162. package/dist/utils/workspace-boundary.d.ts +45 -0
  163. package/dist/utils/workspace-boundary.js +377 -0
  164. package/dist/utils/workspace-brain-service.d.ts +2 -0
  165. package/dist/utils/workspace-brain-service.js +11 -1
  166. package/dist/utils/workspace-cache.js +37 -15
  167. package/dist/utils/workspace-stream.js +35 -10
  168. package/install.ps1 +115 -142
  169. package/install.sh +112 -81
  170. package/package.json +43 -9
  171. package/release-policy.json +43 -0
  172. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
  173. package/scripts/release/generate-completions.mjs +24 -0
  174. package/scripts/release/install-release.mjs +10 -0
  175. package/scripts/release/publish-cli-release.mjs +116 -0
  176. package/scripts/release/publish-cli-release.sh +2 -70
  177. package/scripts/release/resolve-release-manifest.mjs +26 -0
  178. package/scripts/release/test-balanced-model-live.sh +45 -0
  179. package/scripts/release/validate-live-service-gates.sh +93 -0
  180. package/scripts/release/validate-no-go-gates.sh +145 -174
  181. package/scripts/release/verify-runtime-consistency.mjs +4 -1
  182. package/dist/commands/index.d.ts +0 -12
  183. package/dist/commands/index.js +0 -194
  184. package/dist/commands/update.d.ts +0 -9
  185. package/dist/commands/update.js +0 -301
  186. package/dist/utils/templateInstantPath.d.ts +0 -18
  187. package/dist/utils/templateInstantPath.js +0 -228
@@ -15,12 +15,14 @@
15
15
  import chalk from 'chalk';
16
16
  import * as fs from 'fs';
17
17
  import * as path from 'path';
18
- import { createRequire } from 'node:module';
18
+ import { guardedFetch } from '../utils/network-policy.js';
19
19
  import { createSpinner, CH } from '../utils/logger.js';
20
- const require = createRequire(import.meta.url);
20
+ import { createOperationId } from '../utils/mutation-journal.js';
21
+ import { containsHighConfidenceSecret, isSensitivePath, safeChildProcessEnv } from '../utils/secret-policy.js';
22
+ import { CliCommandError, commandFailure, formatSuccessJson } from '../utils/command-contract.js';
23
+ import { createRuntimeTempDirectory, removeRuntimeTempDirectory } from '../utils/runtime-temp.js';
24
+ import { assertSafeDestinationPath, assertSafeRelativePath, inspectZipArchive, mergeValidatedWorkspace, resolveWorkspacePath, validateExtractedWorkspace, } from '../utils/workspace-boundary.js';
21
25
  import inquirer from 'inquirer';
22
- import * as archiverModule from 'archiver';
23
- import { createWriteStream } from 'fs';
24
26
  const MAX_REPOSITORY_CONTENT_BYTES = 100 * 1024 * 1024;
25
27
  const MAX_REPOSITORY_REQUEST_BYTES = 120 * 1024 * 1024;
26
28
  class RepositoryApiError extends Error {
@@ -33,6 +35,24 @@ class RepositoryApiError extends Error {
33
35
  this.payload = payload || {};
34
36
  }
35
37
  }
38
+ export class RepositoryMutationAmbiguousError extends Error {
39
+ operationId;
40
+ mutationOutcome = 'unknown';
41
+ partialMutation = 'unknown';
42
+ constructor(operationId, cause) {
43
+ super(`Repository mutation outcome is unknown for operation ${operationId}; no alternate backend retry was attempted.`);
44
+ this.name = 'RepositoryMutationAmbiguousError';
45
+ this.operationId = operationId;
46
+ if (cause instanceof Error)
47
+ this.cause = cause;
48
+ }
49
+ }
50
+ export function canFallbackRepositoryRequest(method, status) {
51
+ const normalized = method.toUpperCase();
52
+ if (normalized === 'GET' || normalized === 'HEAD')
53
+ return true;
54
+ return status !== undefined && [401, 403, 404, 413].includes(status);
55
+ }
36
56
  function formatBytes(bytes) {
37
57
  return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
38
58
  }
@@ -49,24 +69,35 @@ async function readResponsePayload(response) {
49
69
  }
50
70
  export class RepoCommand {
51
71
  config;
52
- logger;
53
72
  apiBase;
54
73
  communityBase;
55
74
  communityToken;
56
- constructor(config, logger) {
75
+ constructor(config, _logger) {
57
76
  this.config = config;
58
- this.logger = logger;
59
77
  this.apiBase = this.config.get('apiUrl') || 'https://coder.vigthoria.io';
60
78
  this.communityBase = process.env.VIGTHORIA_COMMUNITY_API_URL || 'https://community.vigthoria.io';
61
79
  this.communityToken = process.env.VIGTHORIA_COMMUNITY_TOKEN || null;
62
80
  }
63
- getAuthHeaders() {
64
- const token = this.communityToken || this.config.get('authToken');
81
+ getAuthHeaders(audience) {
82
+ const token = audience === 'coder'
83
+ ? process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN || this.config.get('authToken')
84
+ : this.communityToken;
65
85
  return {
66
- 'Authorization': `Bearer ${token}`,
67
- 'Content-Type': 'application/json'
86
+ ...(token ? { 'Authorization': `Bearer ${token}` } : {}),
87
+ 'Content-Type': 'application/json',
68
88
  };
69
89
  }
90
+ getRepositoryAudienceForUrl(rawUrl) {
91
+ const hostname = new URL(rawUrl).hostname.toLowerCase();
92
+ if (hostname === 'coder.vigthoria.io')
93
+ return 'coder';
94
+ if (hostname === 'community.vigthoria.io')
95
+ return 'community';
96
+ throw new Error('Repository download URL is outside the Coder and Community trust boundary');
97
+ }
98
+ getAuthHeadersForUrl(rawUrl) {
99
+ return this.getAuthHeaders(this.getRepositoryAudienceForUrl(rawUrl));
100
+ }
70
101
  async ensureCommunityAuth() {
71
102
  if (this.communityToken) {
72
103
  return;
@@ -76,58 +107,62 @@ export class RepoCommand {
76
107
  if (!email || !password) {
77
108
  return;
78
109
  }
79
- const response = await fetch(`${this.communityBase}/api/auth/login`, {
110
+ const response = await guardedFetch(`${this.communityBase}/api/auth/login`, {
80
111
  method: 'POST',
81
112
  headers: { 'Content-Type': 'application/json' },
82
113
  body: JSON.stringify({ email, password })
83
- });
114
+ }, { audience: 'community' });
84
115
  const data = await response.json();
85
116
  if (!response.ok || !data.token) {
86
117
  throw new Error(data.error || data.message || 'Community login failed');
87
118
  }
88
119
  this.communityToken = data.token;
89
120
  }
90
- async repoFetch(apiPath, init = {}) {
121
+ async repoFetch(apiPath, init = {}, options = {}) {
91
122
  const normalizedApiPath = apiPath.startsWith('/api/repo') ? apiPath : `/api/repo${apiPath.startsWith('/') ? apiPath : `/${apiPath}`}`;
92
123
  const proxyPath = `${this.apiBase}/api/community-repo${normalizedApiPath.replace('/api/repo', '')}`;
93
124
  const directPath = `${this.communityBase}${normalizedApiPath}`;
94
- const attempt = async (url, allowCommunityAuthRetry) => {
95
- const response = await fetch(url, {
125
+ const method = String(init.method || 'GET').toUpperCase();
126
+ const readOnly = options.readOnly === true || method === 'GET' || method === 'HEAD';
127
+ const mutationId = readOnly ? undefined : createOperationId(options.operationId);
128
+ const attempt = async (url, audience) => {
129
+ if (audience === 'community' && !this.communityToken)
130
+ await this.ensureCommunityAuth();
131
+ const response = await guardedFetch(url, {
96
132
  ...init,
97
133
  headers: {
98
- ...this.getAuthHeaders(),
134
+ ...this.getAuthHeaders(audience),
135
+ ...(mutationId ? {
136
+ 'Idempotency-Key': mutationId,
137
+ 'X-Vigthoria-Operation-Id': mutationId,
138
+ } : {}),
99
139
  ...(init.headers || {})
100
140
  }
101
- });
102
- // Both 401 (missing/expired credentials) and 403 (a valid Coder
103
- // token that Community cannot accept) must activate the explicit
104
- // Community credential fallback. Previously a stale/incompatible
105
- // Coder token made configured Community credentials unreachable.
106
- if ([401, 403].includes(response.status) && allowCommunityAuthRetry && !this.communityToken) {
107
- await this.ensureCommunityAuth();
108
- if (this.communityToken) {
109
- return attempt(url, false);
110
- }
111
- }
141
+ }, { audience });
112
142
  return response;
113
143
  };
114
144
  try {
115
- const proxyResponse = await attempt(proxyPath, true);
145
+ const proxyResponse = await attempt(proxyPath, 'coder');
116
146
  // A large repository payload can exceed the Coder API's general
117
147
  // 50 MiB parser while remaining valid under Community's 100 MiB
118
148
  // repository contract. Retry that one status directly.
119
- const shouldTryDirectCommunity = [401, 403, 404, 413].includes(proxyResponse.status);
149
+ const shouldTryDirectCommunity = readOnly || canFallbackRepositoryRequest(method, proxyResponse.status);
120
150
  if (proxyResponse.ok || (proxyResponse.status < 500 && !shouldTryDirectCommunity)) {
121
151
  return proxyResponse;
122
152
  }
123
153
  if (shouldTryDirectCommunity) {
124
154
  await proxyResponse.body?.cancel();
125
155
  }
156
+ else {
157
+ return proxyResponse;
158
+ }
126
159
  }
127
- catch {
128
- // fall through to direct community endpoint
160
+ catch (error) {
161
+ if (!readOnly) {
162
+ throw new RepositoryMutationAmbiguousError(mutationId, error);
163
+ }
129
164
  }
130
- return attempt(directPath, true);
165
+ return attempt(directPath, 'community');
131
166
  }
132
167
  collectProjectFiles(projectPath) {
133
168
  const files = [];
@@ -140,15 +175,26 @@ export class RepoCommand {
140
175
  }
141
176
  const fullPath = path.join(dir, entry.name);
142
177
  const relativePath = path.relative(projectPath, fullPath);
178
+ if (isSensitivePath(relativePath)) {
179
+ continue;
180
+ }
181
+ if (entry.isSymbolicLink()) {
182
+ throw new Error(`Repository push refused symbolic link or junction: ${relativePath}`);
183
+ }
143
184
  if (entry.isDirectory()) {
185
+ resolveWorkspacePath(projectPath, relativePath);
144
186
  walk(fullPath);
145
187
  continue;
146
188
  }
147
- if (!entry.isFile() || ignoreSuffixes.some((suffix) => entry.name.endsWith(suffix))) {
189
+ if (!entry.isFile()) {
190
+ throw new Error(`Repository push refused special or unreadable entry: ${relativePath}`);
191
+ }
192
+ if (ignoreSuffixes.some((suffix) => entry.name.endsWith(suffix))) {
148
193
  continue;
149
194
  }
150
195
  try {
151
- const raw = fs.readFileSync(fullPath);
196
+ const containedPath = resolveWorkspacePath(projectPath, relativePath);
197
+ const raw = fs.readFileSync(containedPath);
152
198
  let content;
153
199
  let encoding;
154
200
  try {
@@ -159,9 +205,14 @@ export class RepoCommand {
159
205
  if (looksBinary)
160
206
  throw new Error('binary content');
161
207
  content = new TextDecoder('utf-8', { fatal: true }).decode(raw);
208
+ if (containsHighConfidenceSecret(content)) {
209
+ throw new Error(`Repository push refused ${relativePath}: detected secret material`);
210
+ }
162
211
  encoding = 'utf8';
163
212
  }
164
- catch {
213
+ catch (error) {
214
+ if (error instanceof Error && error.message.startsWith('Repository push refused'))
215
+ throw error;
165
216
  content = raw.toString('base64');
166
217
  encoding = 'base64';
167
218
  }
@@ -172,8 +223,8 @@ export class RepoCommand {
172
223
  size: raw.length,
173
224
  });
174
225
  }
175
- catch {
176
- // Skip unreadable files. Binary files are transported as base64.
226
+ catch (error) {
227
+ throw new Error(`Repository push could not safely read ${relativePath}: ${error.message}`);
177
228
  }
178
229
  }
179
230
  };
@@ -250,13 +301,14 @@ export class RepoCommand {
250
301
  console.log(chalk.cyan(` Retry fixed content: ${review.retryCommand || `vigthoria repo push --retry ${review.reviewId}`}\n`));
251
302
  }
252
303
  isAuthenticated() {
253
- const token = this.config.get('authToken');
304
+ const token = process.env.VIGTHORIA_TOKEN || process.env.VIGTHORIA_AUTH_TOKEN || this.config.get('authToken');
254
305
  return !!token;
255
306
  }
256
307
  requireAuth() {
257
308
  if (!this.isAuthenticated()) {
258
- console.log(chalk.red('\n❌ Authentication required. Run: vigthoria login\n'));
259
- process.exit(1);
309
+ throw new CliCommandError('Authentication required. Run: vigthoria login', {
310
+ code: 'AUTH_REQUIRED', category: 'authentication',
311
+ });
260
312
  }
261
313
  }
262
314
  /**
@@ -321,44 +373,6 @@ export class RepoCommand {
321
373
  }
322
374
  return { name, description, techStack: [...new Set(techStack)] };
323
375
  }
324
- /**
325
- * Create a zip archive of a project
326
- */
327
- async createProjectArchive(projectPath) {
328
- const tempDir = path.join(require('os').tmpdir(), 'vigthoria-cli');
329
- if (!fs.existsSync(tempDir)) {
330
- fs.mkdirSync(tempDir, { recursive: true });
331
- }
332
- const archivePath = path.join(tempDir, `project-${Date.now()}.zip`);
333
- const output = createWriteStream(archivePath);
334
- // Archiver 8 is ESM-only and exposes format-specific constructors
335
- // instead of the legacy default factory.
336
- const archive = new archiverModule.ZipArchive({ zlib: { level: 9 } });
337
- return new Promise((resolve, reject) => {
338
- output.on('close', () => resolve(archivePath));
339
- archive.on('error', (err) => reject(err));
340
- archive.pipe(output);
341
- // Ignore patterns
342
- const ignorePatterns = [
343
- 'node_modules/**',
344
- '.git/**',
345
- 'dist/**',
346
- 'build/**',
347
- '__pycache__/**',
348
- '.venv/**',
349
- 'venv/**',
350
- '*.log',
351
- '.env',
352
- '.DS_Store'
353
- ];
354
- archive.glob('**/*', {
355
- cwd: projectPath,
356
- ignore: ignorePatterns,
357
- dot: true
358
- });
359
- archive.finalize();
360
- });
361
- }
362
376
  /**
363
377
  * Push a project to Vigthoria Repository
364
378
  */
@@ -366,8 +380,9 @@ export class RepoCommand {
366
380
  this.requireAuth();
367
381
  const projectPath = options.path ? path.resolve(options.path) : process.cwd();
368
382
  if (!fs.existsSync(projectPath)) {
369
- console.log(chalk.red(`\n❌ Path does not exist: ${projectPath}\n`));
370
- return;
383
+ throw new CliCommandError(`Path does not exist: ${projectPath}`, {
384
+ code: 'PROJECT_PATH_NOT_FOUND', category: 'configuration',
385
+ });
371
386
  }
372
387
  const spinner = createSpinner('Analyzing project...').start();
373
388
  try {
@@ -428,7 +443,9 @@ export class RepoCommand {
428
443
  if (contentBytes > MAX_REPOSITORY_CONTENT_BYTES) {
429
444
  throw new Error(`Repository content is ${formatBytes(contentBytes)} and exceeds the ${formatBytes(MAX_REPOSITORY_CONTENT_BYTES)} limit`);
430
445
  }
446
+ const operationId = createOperationId();
431
447
  const pushPayload = JSON.stringify({
448
+ operationId,
432
449
  projectName: answers.name,
433
450
  description: answers.description,
434
451
  techStack: projectInfo.techStack.join(', '),
@@ -447,7 +464,7 @@ export class RepoCommand {
447
464
  const registerResponse = await this.repoFetch('/api/repo/push', {
448
465
  method: 'POST',
449
466
  body: pushPayload
450
- });
467
+ }, { operationId });
451
468
  if (!registerResponse.ok) {
452
469
  const error = await readResponsePayload(registerResponse);
453
470
  throw new RepositoryApiError(registerResponse.status, error, 'Failed to push project');
@@ -467,7 +484,6 @@ export class RepoCommand {
467
484
  }
468
485
  catch (error) {
469
486
  spinner.stop();
470
- this.logger.error('Push failed');
471
487
  if (error instanceof RepositoryApiError && error.payload?.code === 'REPOSITORY_SECURITY_REVIEW') {
472
488
  this.printSecurityReview({
473
489
  reviewId: error.payload.reviewId,
@@ -478,8 +494,7 @@ export class RepoCommand {
478
494
  retryCommand: error.payload.retryCommand,
479
495
  });
480
496
  }
481
- const errMsg = this.formatRepoError(error);
482
- console.log(chalk.red(`\n❌ Error: ${errMsg}\n`));
497
+ throw commandFailure(error, { code: 'REPOSITORY_PUSH_FAILED', message: this.formatRepoError(error) });
483
498
  }
484
499
  }
485
500
  /**
@@ -499,7 +514,7 @@ export class RepoCommand {
499
514
  }
500
515
  spinner.stop();
501
516
  if (options.json) {
502
- console.log(JSON.stringify(reviewId ? payload.review : payload.reviews, null, 2));
517
+ console.log(formatSuccessJson('repo review', reviewId ? payload.review : payload.reviews));
503
518
  return;
504
519
  }
505
520
  if (reviewId) {
@@ -519,7 +534,101 @@ export class RepoCommand {
519
534
  }
520
535
  catch (error) {
521
536
  spinner.stop();
522
- console.log(chalk.red(`\n❌ Error: ${this.formatRepoError(error)}\n`));
537
+ throw commandFailure(error, { code: 'REPOSITORY_REVIEW_FAILED', message: this.formatRepoError(error) });
538
+ }
539
+ }
540
+ async materializeProject(data, repo, requestedName, options, publicDownload = false) {
541
+ const remoteProjectName = String(data.project?.project_name || data.projectName || repo.name || repo.project_name || requestedName);
542
+ const outputPath = options.output
543
+ ? assertSafeDestinationPath(path.resolve(options.output))
544
+ : resolveWorkspacePath(process.cwd(), assertSafeRelativePath(remoteProjectName), { allowMissing: true });
545
+ if (fs.existsSync(outputPath) && !options.force) {
546
+ const { overwrite } = await inquirer.prompt([{
547
+ type: 'confirm',
548
+ name: 'overwrite',
549
+ message: `Directory ${outputPath} exists. Overwrite?`,
550
+ default: false
551
+ }]);
552
+ if (!overwrite) {
553
+ throw new CliCommandError('Repository materialization cancelled by user', {
554
+ code: 'REPOSITORY_PULL_CANCELLED', category: 'cancelled',
555
+ });
556
+ }
557
+ }
558
+ const downloadSpinner = createSpinner('Downloading project files...').start();
559
+ try {
560
+ if (fs.existsSync(outputPath))
561
+ validateExtractedWorkspace(outputPath);
562
+ if (data.downloadUrl) {
563
+ const archiveResponse = await guardedFetch(data.downloadUrl, {
564
+ headers: publicDownload
565
+ ? { 'Content-Type': 'application/json' }
566
+ : this.getAuthHeadersForUrl(data.downloadUrl)
567
+ }, { audience: this.getRepositoryAudienceForUrl(data.downloadUrl) });
568
+ if (!archiveResponse.ok)
569
+ throw new Error('Failed to download project archive');
570
+ const archiveBuffer = Buffer.from(await archiveResponse.arrayBuffer());
571
+ inspectZipArchive(archiveBuffer);
572
+ const stagingRoot = createRuntimeTempDirectory('repo-pull-');
573
+ const tempArchive = path.join(stagingRoot, 'archive.zip');
574
+ const extractedPath = path.join(stagingRoot, 'extracted');
575
+ try {
576
+ fs.mkdirSync(extractedPath, { mode: 0o700 });
577
+ fs.writeFileSync(tempArchive, archiveBuffer, { mode: 0o600 });
578
+ if (process.platform === 'win32') {
579
+ const { execFileSync } = await import('child_process');
580
+ execFileSync('powershell', [
581
+ '-NoProfile', '-NonInteractive', '-Command',
582
+ 'Expand-Archive -LiteralPath $env:VIGTHORIA_ARCHIVE_PATH -DestinationPath $env:VIGTHORIA_ARCHIVE_DESTINATION -Force'
583
+ ], {
584
+ stdio: 'ignore',
585
+ timeout: 120_000,
586
+ windowsHide: true,
587
+ env: safeChildProcessEnv(process.env, {
588
+ VIGTHORIA_ARCHIVE_PATH: tempArchive,
589
+ VIGTHORIA_ARCHIVE_DESTINATION: extractedPath,
590
+ }),
591
+ });
592
+ }
593
+ else {
594
+ const { execFileSync } = await import('child_process');
595
+ execFileSync('unzip', ['-o', tempArchive, '-d', extractedPath], { stdio: 'ignore', timeout: 120_000, env: safeChildProcessEnv() });
596
+ }
597
+ validateExtractedWorkspace(extractedPath);
598
+ mergeValidatedWorkspace(extractedPath, outputPath);
599
+ }
600
+ finally {
601
+ removeRuntimeTempDirectory(stagingRoot);
602
+ }
603
+ }
604
+ else if (data.files) {
605
+ const stagingRoot = createRuntimeTempDirectory('repo-inline-');
606
+ try {
607
+ for (const file of data.files) {
608
+ const filePath = resolveWorkspacePath(stagingRoot, file.path, { allowMissing: true });
609
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
610
+ fs.writeFileSync(filePath, file.content, { mode: 0o600 });
611
+ }
612
+ validateExtractedWorkspace(stagingRoot);
613
+ mergeValidatedWorkspace(stagingRoot, outputPath);
614
+ }
615
+ finally {
616
+ removeRuntimeTempDirectory(stagingRoot);
617
+ }
618
+ }
619
+ else {
620
+ throw new Error('Repository response contained neither an archive nor inline files');
621
+ }
622
+ downloadSpinner.succeed(chalk.green('Project materialized successfully!'));
623
+ console.log(chalk.cyan('\n📁 Project extracted to:'));
624
+ console.log(chalk.white(` ${outputPath}`));
625
+ console.log(chalk.gray(`\n Tech Stack: ${data.project?.tech_stack || repo.tech_stack || 'Unknown'}`));
626
+ console.log(chalk.gray(` Description: ${data.project?.description || data.description || repo.description || '(none)'}`));
627
+ console.log();
628
+ }
629
+ catch (error) {
630
+ downloadSpinner.stop();
631
+ throw error;
523
632
  }
524
633
  }
525
634
  /**
@@ -533,81 +642,18 @@ export class RepoCommand {
533
642
  const response = await this.repoFetch('/api/repo/pull', {
534
643
  method: 'POST',
535
644
  body: JSON.stringify({ projectId: repo.id })
536
- });
645
+ }, { readOnly: true });
537
646
  if (!response.ok) {
538
647
  const error = await readResponsePayload(response);
539
648
  throw new Error(error.error || error.message || 'Project not found');
540
649
  }
541
650
  const data = await response.json();
542
651
  spinner.succeed(`Found project: ${chalk.cyan(data.project?.project_name || data.projectName || repo.name || repo.project_name || projectName)}`);
543
- const outputPath = options.output
544
- ? path.resolve(options.output)
545
- : path.join(process.cwd(), data.project?.project_name || data.projectName || repo.name || repo.project_name || projectName);
546
- if (fs.existsSync(outputPath) && !options.force) {
547
- const { overwrite } = await inquirer.prompt([{
548
- type: 'confirm',
549
- name: 'overwrite',
550
- message: `Directory ${outputPath} exists. Overwrite?`,
551
- default: false
552
- }]);
553
- if (!overwrite) {
554
- console.log(chalk.yellow(`\n${CH.warnEmoji} Pull cancelled.\n`));
555
- return;
556
- }
557
- }
558
- const downloadSpinner = createSpinner('Downloading project files...').start();
559
- // Create output directory
560
- fs.mkdirSync(outputPath, { recursive: true });
561
- // If we have a download URL, fetch and extract
562
- if (data.downloadUrl) {
563
- const archiveResponse = await fetch(data.downloadUrl, {
564
- headers: this.getAuthHeaders()
565
- });
566
- if (!archiveResponse.ok) {
567
- throw new Error('Failed to download project archive');
568
- }
569
- // Save and extract the archive
570
- const tempArchive = path.join(require('os').tmpdir(), `vigthoria-pull-${Date.now()}.zip`);
571
- const archiveBuffer = Buffer.from(await archiveResponse.arrayBuffer());
572
- fs.writeFileSync(tempArchive, archiveBuffer);
573
- // Extract archive - cross-platform
574
- const os = require('os');
575
- const platform = os.platform();
576
- if (platform === 'win32') {
577
- // Use PowerShell's Expand-Archive — args as array to prevent injection
578
- const { execFileSync } = await import('child_process');
579
- execFileSync('powershell', [
580
- '-NoProfile', '-NonInteractive', '-Command',
581
- `Expand-Archive -Path '${tempArchive.replace(/'/g, "''")}' -DestinationPath '${outputPath.replace(/'/g, "''")}' -Force`
582
- ], { stdio: 'ignore', windowsHide: true });
583
- }
584
- else {
585
- // Use unzip on Unix-like systems — args as array to prevent injection
586
- const { execFileSync } = await import('child_process');
587
- execFileSync('unzip', ['-o', tempArchive, '-d', outputPath], { stdio: 'ignore' });
588
- }
589
- fs.unlinkSync(tempArchive);
590
- }
591
- else if (data.files) {
592
- // If we have files inline (small projects)
593
- for (const file of data.files) {
594
- const filePath = path.join(outputPath, file.path);
595
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
596
- fs.writeFileSync(filePath, file.content);
597
- }
598
- }
599
- downloadSpinner.succeed(chalk.green('Project pulled successfully!'));
600
- console.log(chalk.cyan('\n📁 Project extracted to:'));
601
- console.log(chalk.white(` ${outputPath}`));
602
- console.log(chalk.gray(`\n Tech Stack: ${data.project?.tech_stack || repo.tech_stack || 'Unknown'}`));
603
- console.log(chalk.gray(` Description: ${data.project?.description || data.description || repo.description || '(none)'}`));
604
- console.log();
652
+ await this.materializeProject(data, repo, projectName, options);
605
653
  }
606
654
  catch (error) {
607
655
  spinner.stop();
608
- this.logger.error('Pull failed');
609
- const errMsg = this.formatRepoError(error);
610
- console.log(chalk.red(`\n❌ Error: ${errMsg}\n`));
656
+ throw commandFailure(error, { code: 'REPOSITORY_PULL_FAILED', message: this.formatRepoError(error) });
611
657
  }
612
658
  }
613
659
  /**
@@ -704,9 +750,7 @@ export class RepoCommand {
704
750
  }
705
751
  catch (error) {
706
752
  spinner.stop();
707
- this.logger.error('List failed');
708
- const errMsg = this.formatRepoError(error);
709
- console.log(chalk.red(`\n❌ Error: ${errMsg}\n`));
753
+ throw commandFailure(error, { code: 'REPOSITORY_LIST_FAILED', message: this.formatRepoError(error) });
710
754
  }
711
755
  }
712
756
  /**
@@ -718,10 +762,10 @@ export class RepoCommand {
718
762
  const projectInfo = this.detectProjectInfo(projectPath);
719
763
  const spinner = createSpinner('Checking sync status...').start();
720
764
  try {
721
- const response = await fetch(`${this.apiBase}/api/repo/status/${encodeURIComponent(projectInfo.name)}`, {
765
+ const response = await guardedFetch(`${this.apiBase}/api/repo/status/${encodeURIComponent(projectInfo.name)}`, {
722
766
  method: 'GET',
723
- headers: this.getAuthHeaders()
724
- });
767
+ headers: this.getAuthHeaders('coder')
768
+ }, { audience: 'coder' });
725
769
  if (!response.ok) {
726
770
  spinner.succeed('Project not tracked in Vigthoria Community Repository');
727
771
  console.log(chalk.yellow('\n📦 Current project is not synced with Vigthoria Community Repository.\n'));
@@ -755,9 +799,7 @@ export class RepoCommand {
755
799
  }
756
800
  catch (error) {
757
801
  spinner.stop();
758
- this.logger.error('Status check failed');
759
- const errMsg = error instanceof Error ? error.message : 'Unknown error';
760
- console.log(chalk.red(`\n❌ Error: ${errMsg}\n`));
802
+ throw commandFailure(error, { code: 'REPOSITORY_STATUS_FAILED' });
761
803
  }
762
804
  }
763
805
  /**
@@ -767,14 +809,15 @@ export class RepoCommand {
767
809
  this.requireAuth();
768
810
  const spinner = createSpinner('Generating share link...').start();
769
811
  try {
770
- const response = await fetch(`${this.apiBase}/api/repo/share`, {
812
+ const operationId = createOperationId();
813
+ const response = await this.repoFetch('/api/repo/share', {
771
814
  method: 'POST',
772
- headers: this.getAuthHeaders(),
773
815
  body: JSON.stringify({
816
+ operationId,
774
817
  projectName,
775
818
  expiresIn: options.expires || '7d'
776
819
  })
777
- });
820
+ }, { operationId });
778
821
  if (!response.ok) {
779
822
  const error = await response.json();
780
823
  throw new Error(error.error || 'Failed to generate share link');
@@ -788,32 +831,33 @@ export class RepoCommand {
788
831
  }
789
832
  catch (error) {
790
833
  spinner.stop();
791
- this.logger.error('Share failed');
792
- const errMsg = error instanceof Error ? error.message : 'Unknown error';
793
- console.log(chalk.red(`\n❌ Error: ${errMsg}\n`));
834
+ throw commandFailure(error, { code: 'REPOSITORY_SHARE_FAILED' });
794
835
  }
795
836
  }
796
837
  /**
797
838
  * Delete a project from Vigthoria Repository
798
839
  */
799
- async delete(projectName) {
840
+ async delete(projectName, options = {}) {
800
841
  this.requireAuth();
801
- const { confirm } = await inquirer.prompt([{
802
- type: 'confirm',
803
- name: 'confirm',
804
- message: chalk.red(`Are you sure you want to delete "${projectName}" from Vigthoria Community Repository?`),
805
- default: false
806
- }]);
842
+ const confirm = options.yes === true
843
+ ? true
844
+ : (await inquirer.prompt([{
845
+ type: 'confirm',
846
+ name: 'confirm',
847
+ message: chalk.red(`Are you sure you want to delete "${projectName}" from Vigthoria Community Repository?`),
848
+ default: false
849
+ }])).confirm;
807
850
  if (!confirm) {
808
851
  console.log(chalk.yellow(`\n${CH.warnEmoji} Delete cancelled.\n`));
809
852
  return;
810
853
  }
811
854
  const spinner = createSpinner('Deleting project...').start();
812
855
  try {
813
- const response = await fetch(`${this.apiBase}/api/repo/projects/${encodeURIComponent(projectName)}`, {
856
+ const operationId = createOperationId();
857
+ const response = await this.repoFetch(`/api/repo/projects/${encodeURIComponent(projectName)}`, {
814
858
  method: 'DELETE',
815
- headers: this.getAuthHeaders()
816
- });
859
+ headers: { 'X-Vigthoria-Operation-Id': operationId },
860
+ }, { operationId });
817
861
  if (!response.ok) {
818
862
  const error = await response.json();
819
863
  throw new Error(error.error || 'Failed to delete project');
@@ -823,9 +867,7 @@ export class RepoCommand {
823
867
  }
824
868
  catch (error) {
825
869
  spinner.stop();
826
- this.logger.error('Delete failed');
827
- const errMsg = error instanceof Error ? error.message : 'Unknown error';
828
- console.log(chalk.red(`\n❌ Error: ${errMsg}\n`));
870
+ throw commandFailure(error, { code: 'REPOSITORY_DELETE_FAILED' });
829
871
  }
830
872
  }
831
873
  /**
@@ -844,16 +886,22 @@ export class RepoCommand {
844
886
  const resolvedName = projectName || this.detectProjectInfo(process.cwd()).name;
845
887
  const spinner = createSpinner(`Opening "${resolvedName}" in ${label}...`).start();
846
888
  try {
847
- const response = await fetch(`${this.apiBase}/api/engine-import`, {
889
+ const operationId = createOperationId();
890
+ const response = await guardedFetch(`${this.apiBase}/api/engine-import`, {
848
891
  method: 'POST',
849
- headers: this.getAuthHeaders(),
892
+ headers: {
893
+ ...this.getAuthHeaders('coder'),
894
+ 'Idempotency-Key': operationId,
895
+ 'X-Vigthoria-Operation-Id': operationId,
896
+ },
850
897
  body: JSON.stringify({
898
+ operationId,
851
899
  engine,
852
900
  projectName: resolvedName,
853
901
  shopId: options.shopId || 'default',
854
902
  source: 'repo'
855
903
  })
856
- });
904
+ }, { audience: 'coder' });
857
905
  if (!response.ok) {
858
906
  const err = await readResponsePayload(response);
859
907
  throw new Error(err.error || `Engine import failed (${response.status})`);
@@ -873,11 +921,11 @@ export class RepoCommand {
873
921
  }
874
922
  const safeUrl = parsedUrl.href;
875
923
  if (platform === 'win32')
876
- execFileSync('cmd', ['/c', 'start', '', safeUrl], { stdio: 'ignore', windowsHide: true });
924
+ execFileSync('rundll32.exe', ['url.dll,FileProtocolHandler', safeUrl], { stdio: 'ignore', timeout: 10_000, windowsHide: true, env: safeChildProcessEnv() });
877
925
  else if (platform === 'darwin')
878
- execFileSync('open', [safeUrl], { stdio: 'ignore' });
926
+ execFileSync('open', [safeUrl], { stdio: 'ignore', timeout: 10_000, env: safeChildProcessEnv() });
879
927
  else
880
- execFileSync('xdg-open', [safeUrl], { stdio: 'ignore' });
928
+ execFileSync('xdg-open', [safeUrl], { stdio: 'ignore', timeout: 10_000, env: safeChildProcessEnv() });
881
929
  }
882
930
  catch { /* browser open is optional */ }
883
931
  }
@@ -887,9 +935,7 @@ export class RepoCommand {
887
935
  }
888
936
  catch (error) {
889
937
  spinner.stop();
890
- const errMsg = error instanceof Error ? error.message : 'Unknown error';
891
- console.log(chalk.red(`\n❌ ${label} import failed: ${errMsg}\n`));
892
- console.log(chalk.gray('Tip: Make sure your project is pushed first with `vigthoria repo push`\n'));
938
+ throw commandFailure(error, { code: 'REPOSITORY_ENGINE_IMPORT_FAILED' });
893
939
  }
894
940
  }
895
941
  /**
@@ -901,24 +947,24 @@ export class RepoCommand {
901
947
  const projectId = projectIdMatch ? projectIdMatch[1] : projectUrl;
902
948
  const spinner = createSpinner(`Cloning project...`).start();
903
949
  try {
904
- const response = await fetch(`${this.apiBase}/api/repo/clone/${projectId}`, {
950
+ const response = await guardedFetch(`${this.apiBase}/api/repo/clone/${projectId}`, {
905
951
  method: 'GET',
906
- headers: this.isAuthenticated() ? this.getAuthHeaders() : { 'Content-Type': 'application/json' }
907
- });
952
+ headers: this.isAuthenticated() ? this.getAuthHeaders('coder') : { 'Content-Type': 'application/json' }
953
+ }, { audience: 'coder' });
908
954
  if (!response.ok) {
909
955
  const error = await response.json();
910
956
  throw new Error(error.error || 'Project not found or not public');
911
957
  }
912
958
  const data = await response.json();
913
959
  spinner.succeed(`Found project: ${chalk.cyan(data.project.project_name)}`);
914
- // Use pull logic for the rest
915
- await this.pull(data.project.project_name, options);
960
+ // Public clones materialize the clone response directly. They must
961
+ // not route through authenticated `pull`, and public archive
962
+ // downloads must not inherit a user credential.
963
+ await this.materializeProject(data, data.project, data.project.project_name, options, true);
916
964
  }
917
965
  catch (error) {
918
966
  spinner.stop();
919
- this.logger.error('Clone failed');
920
- const errMsg = error instanceof Error ? error.message : 'Unknown error';
921
- console.log(chalk.red(`\n❌ Error: ${errMsg}\n`));
967
+ throw commandFailure(error, { code: 'REPOSITORY_CLONE_FAILED' });
922
968
  }
923
969
  }
924
970
  }