@colin4k1024/tsp 2.4.7 → 2.4.9

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 (37) hide show
  1. package/README.md +4 -4
  2. package/hooks/hooks.json +11 -0
  3. package/manifests/install-modules.json +5 -2
  4. package/package.json +1 -2
  5. package/schemas/install-modules.schema.json +52 -0
  6. package/scripts/codegraph-preflight.js +68 -75
  7. package/scripts/hooks/codegraph-auto-init.js +324 -0
  8. package/scripts/install-apply.js +2 -1
  9. package/scripts/install-codegraph.js +235 -25
  10. package/scripts/install-plan.js +4 -1
  11. package/scripts/lib/install/apply.js +5 -0
  12. package/scripts/lib/install-executor.js +6 -0
  13. package/skills/codegraph/SKILL.md +6 -5
  14. package/skills/goframe-v2/examples/practices/quick-demo/manifest/config/config.yaml +14 -14
  15. package/skills/repo-scan/SKILL.md +63 -63
  16. package/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
  17. package/scripts/__pycache__/build_platform_artifacts.cpython-311.pyc +0 -0
  18. package/scripts/__pycache__/install_platform.cpython-311.pyc +0 -0
  19. package/scripts/__pycache__/langfuse_trace.cpython-311.pyc +0 -0
  20. package/scripts/__pycache__/query_audit_logs.cpython-311.pyc +0 -0
  21. package/scripts/__pycache__/scan_leaked_keys.cpython-311.pyc +0 -0
  22. package/scripts/__pycache__/team_skills_platform.cpython-311.pyc +0 -0
  23. package/scripts/__pycache__/team_skills_platform.cpython-313.pyc +0 -0
  24. package/scripts/__pycache__/validate_library.cpython-311.pyc +0 -0
  25. package/scripts/__pycache__/validate_workflow_state.cpython-311.pyc +0 -0
  26. package/scripts/evolution/__pycache__/__init__.cpython-311.pyc +0 -0
  27. package/scripts/evolution/__pycache__/store.cpython-311.pyc +0 -0
  28. package/scripts/hooks/__pycache__/__init__.cpython-311.pyc +0 -0
  29. package/scripts/hooks/__pycache__/mcp_health_check.cpython-311.pyc +0 -0
  30. package/scripts/hooks/__pycache__/observe.cpython-311.pyc +0 -0
  31. package/scripts/hooks/__pycache__/session_end.cpython-311.pyc +0 -0
  32. package/scripts/hooks/__pycache__/session_start.cpython-311.pyc +0 -0
  33. package/scripts/lib/__pycache__/audit_logger.cpython-311.pyc +0 -0
  34. package/scripts/lib/__pycache__/audit_query.cpython-311.pyc +0 -0
  35. package/scripts/lib/__pycache__/hook_contract.cpython-311.pyc +0 -0
  36. package/scripts/lib/__pycache__/memory_store.cpython-311.pyc +0 -0
  37. package/scripts/lib/__pycache__/utils.cpython-311.pyc +0 -0
@@ -0,0 +1,324 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const { spawnSync } = require('child_process');
8
+ const { resolveCodeGraphBin } = require('../install-codegraph');
9
+
10
+ const STATE_FILE = path.join('.codegraph', 'tsp-auto-init-state.json');
11
+ const DEFAULT_RETRY_MS = 60 * 60 * 1000;
12
+ const DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
13
+ const PROJECT_MARKERS = Object.freeze([
14
+ 'package.json',
15
+ 'pyproject.toml',
16
+ 'Cargo.toml',
17
+ 'go.mod',
18
+ 'pom.xml',
19
+ 'build.gradle',
20
+ 'build.gradle.kts',
21
+ 'Gemfile',
22
+ 'composer.json',
23
+ 'mix.exs',
24
+ 'deno.json',
25
+ 'deno.jsonc',
26
+ 'README.md',
27
+ 'AGENTS.md',
28
+ 'CLAUDE.md',
29
+ ]);
30
+
31
+ function isDisabled() {
32
+ const value = String(process.env.TSP_CODEGRAPH_AUTO_INIT || '').trim().toLowerCase();
33
+ return value === '0' || value === 'false' || value === 'off';
34
+ }
35
+
36
+ function readHookInput(rawInput) {
37
+ if (!rawInput || !String(rawInput).trim()) {
38
+ return {};
39
+ }
40
+
41
+ try {
42
+ const parsed = JSON.parse(rawInput);
43
+ return parsed && typeof parsed === 'object' ? parsed : {};
44
+ } catch {
45
+ return {};
46
+ }
47
+ }
48
+
49
+ function normalizePath(candidate) {
50
+ try {
51
+ return fs.realpathSync(candidate);
52
+ } catch {
53
+ return path.resolve(candidate);
54
+ }
55
+ }
56
+
57
+ function getStartDir(rawInput) {
58
+ if (process.env.CODEGRAPH_AUTO_INIT_PROJECT_ROOT) {
59
+ return path.resolve(process.env.CODEGRAPH_AUTO_INIT_PROJECT_ROOT);
60
+ }
61
+
62
+ const input = readHookInput(rawInput);
63
+ const candidates = [
64
+ input.cwd,
65
+ input.projectDir,
66
+ input.project_dir,
67
+ input.projectPath,
68
+ input.project_path,
69
+ input.workspaceDir,
70
+ input.workspace_dir,
71
+ ];
72
+
73
+ for (const candidate of candidates) {
74
+ if (typeof candidate === 'string' && candidate.trim()) {
75
+ return path.resolve(candidate.trim());
76
+ }
77
+ }
78
+
79
+ return process.cwd();
80
+ }
81
+
82
+ function parseGitDirFile(filePath) {
83
+ try {
84
+ const content = fs.readFileSync(filePath, 'utf8').trim();
85
+ const match = content.match(/^gitdir:\s*(.+)$/i);
86
+ if (!match) {
87
+ return null;
88
+ }
89
+ const gitDir = match[1].trim();
90
+ return path.resolve(path.dirname(filePath), gitDir);
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ function findGitRoot(startDir) {
97
+ let current = normalizePath(startDir);
98
+ const root = path.parse(current).root;
99
+
100
+ while (current && current !== root) {
101
+ const gitPath = path.join(current, '.git');
102
+ if (fs.existsSync(gitPath)) {
103
+ return current;
104
+ }
105
+ current = path.dirname(current);
106
+ }
107
+
108
+ const rootGitPath = path.join(root, '.git');
109
+ return fs.existsSync(rootGitPath) ? root : null;
110
+ }
111
+
112
+ function findMarkerRoot(startDir) {
113
+ let current = normalizePath(startDir);
114
+ const root = path.parse(current).root;
115
+ const home = normalizePath(os.homedir());
116
+
117
+ while (current && current !== root) {
118
+ if (current === home) {
119
+ return null;
120
+ }
121
+ if (PROJECT_MARKERS.some(marker => fs.existsSync(path.join(current, marker)))) {
122
+ return current;
123
+ }
124
+ current = path.dirname(current);
125
+ }
126
+
127
+ return null;
128
+ }
129
+
130
+ function resolveProjectRoot(rawInput) {
131
+ const startDir = getStartDir(rawInput);
132
+ if (!fs.existsSync(startDir)) {
133
+ return null;
134
+ }
135
+
136
+ return findGitRoot(startDir) || findMarkerRoot(startDir);
137
+ }
138
+
139
+ function codegraphDbPath(projectRoot) {
140
+ return path.join(projectRoot, '.codegraph', 'codegraph.db');
141
+ }
142
+
143
+ function statePath(projectRoot) {
144
+ return path.join(projectRoot, STATE_FILE);
145
+ }
146
+
147
+ function readState(projectRoot) {
148
+ try {
149
+ return JSON.parse(fs.readFileSync(statePath(projectRoot), 'utf8'));
150
+ } catch {
151
+ return {};
152
+ }
153
+ }
154
+
155
+ function writeState(projectRoot, state) {
156
+ const filePath = statePath(projectRoot);
157
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
158
+ fs.writeFileSync(filePath, JSON.stringify(state, null, 2) + '\n', 'utf8');
159
+ }
160
+
161
+ function retryDelayMs() {
162
+ const parsed = Number(process.env.TSP_CODEGRAPH_AUTO_INIT_RETRY_MS || DEFAULT_RETRY_MS);
163
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_RETRY_MS;
164
+ }
165
+
166
+ function shouldThrottleFailure(projectRoot, now = Date.now()) {
167
+ const state = readState(projectRoot);
168
+ if (state.status !== 'failed' || !state.failedAt) {
169
+ return false;
170
+ }
171
+
172
+ const failedAt = Date.parse(state.failedAt);
173
+ if (!Number.isFinite(failedAt)) {
174
+ return false;
175
+ }
176
+
177
+ return now - failedAt < retryDelayMs();
178
+ }
179
+
180
+ function resolveGitDir(projectRoot) {
181
+ const gitPath = path.join(projectRoot, '.git');
182
+ try {
183
+ const stat = fs.statSync(gitPath);
184
+ if (stat.isDirectory()) {
185
+ return gitPath;
186
+ }
187
+ if (stat.isFile()) {
188
+ return parseGitDirFile(gitPath);
189
+ }
190
+ } catch {
191
+ return null;
192
+ }
193
+
194
+ return null;
195
+ }
196
+
197
+ function ensureGitExclude(projectRoot) {
198
+ const gitDir = resolveGitDir(projectRoot);
199
+ if (!gitDir) {
200
+ return false;
201
+ }
202
+
203
+ const excludePath = path.join(gitDir, 'info', 'exclude');
204
+ fs.mkdirSync(path.dirname(excludePath), { recursive: true });
205
+ const existing = fs.existsSync(excludePath) ? fs.readFileSync(excludePath, 'utf8') : '';
206
+ const lines = existing.split(/\r?\n/).map(line => line.trim());
207
+ if (lines.includes('.codegraph/')) {
208
+ return false;
209
+ }
210
+
211
+ const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
212
+ fs.appendFileSync(excludePath, `${prefix}.codegraph/\n`, 'utf8');
213
+ return true;
214
+ }
215
+
216
+ function timeoutMs() {
217
+ const parsed = Number(process.env.TSP_CODEGRAPH_AUTO_INIT_TIMEOUT_MS || DEFAULT_TIMEOUT_MS);
218
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS;
219
+ }
220
+
221
+ function truncate(text, maxLength = 2000) {
222
+ const value = String(text || '');
223
+ return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
224
+ }
225
+
226
+ function initializeProject(projectRoot) {
227
+ const resolved = resolveCodeGraphBin({ ignoreForceStandalone: true });
228
+ if (!resolved) {
229
+ throw new Error('CodeGraph binary not found. Install it with the official standalone installer or set CODEGRAPH_INSTALL_BIN.');
230
+ }
231
+
232
+ const result = spawnSync(resolved.command, [...resolved.argsPrefix, 'init', '-i', projectRoot], {
233
+ cwd: projectRoot,
234
+ env: process.env,
235
+ encoding: 'utf8',
236
+ timeout: timeoutMs(),
237
+ });
238
+
239
+ if (result.error) {
240
+ throw result.error;
241
+ }
242
+
243
+ if (result.status !== 0) {
244
+ throw new Error(truncate(`${result.stdout || ''}${result.stderr || ''}`.trim() || `codegraph exited with status ${result.status}`));
245
+ }
246
+
247
+ return {
248
+ binary: resolved.displayCommand,
249
+ };
250
+ }
251
+
252
+ function runAutoInit(rawInput = '') {
253
+ if (isDisabled()) {
254
+ return { status: 'skipped', reason: 'disabled' };
255
+ }
256
+
257
+ const projectRoot = resolveProjectRoot(rawInput);
258
+ if (!projectRoot) {
259
+ return { status: 'skipped', reason: 'no-project-root' };
260
+ }
261
+
262
+ if (fs.existsSync(codegraphDbPath(projectRoot))) {
263
+ return { status: 'skipped', reason: 'already-initialized', projectRoot };
264
+ }
265
+
266
+ if (shouldThrottleFailure(projectRoot)) {
267
+ return { status: 'skipped', reason: 'recent-failure', projectRoot };
268
+ }
269
+
270
+ try {
271
+ ensureGitExclude(projectRoot);
272
+ const initialized = initializeProject(projectRoot);
273
+ writeState(projectRoot, {
274
+ status: 'initialized',
275
+ projectRoot,
276
+ initializedAt: new Date().toISOString(),
277
+ binary: initialized.binary,
278
+ });
279
+ return { status: 'initialized', projectRoot };
280
+ } catch (error) {
281
+ writeState(projectRoot, {
282
+ status: 'failed',
283
+ projectRoot,
284
+ failedAt: new Date().toISOString(),
285
+ error: truncate(error && error.message ? error.message : error),
286
+ });
287
+ return { status: 'failed', projectRoot, error };
288
+ }
289
+ }
290
+
291
+ function run(rawInput = '') {
292
+ const result = runAutoInit(rawInput);
293
+ if (result.status === 'failed' && process.env.TSP_CODEGRAPH_AUTO_INIT_VERBOSE === '1') {
294
+ return {
295
+ stderr: `[CodeGraph] auto-init failed for ${result.projectRoot}: ${result.error && result.error.message ? result.error.message : result.error}\n`,
296
+ exitCode: 0,
297
+ };
298
+ }
299
+ return { exitCode: 0 };
300
+ }
301
+
302
+ if (require.main === module) {
303
+ let raw = '';
304
+ process.stdin.setEncoding('utf8');
305
+ process.stdin.on('data', chunk => {
306
+ raw += chunk;
307
+ });
308
+ process.stdin.on('end', () => {
309
+ run(raw);
310
+ });
311
+ }
312
+
313
+ module.exports = {
314
+ codegraphDbPath,
315
+ ensureGitExclude,
316
+ findGitRoot,
317
+ findMarkerRoot,
318
+ readState,
319
+ resolveProjectRoot,
320
+ run,
321
+ runAutoInit,
322
+ shouldThrottleFailure,
323
+ statePath,
324
+ };
@@ -218,7 +218,8 @@ function printHumanPlan(plan, dryRun, knownRisks = []) {
218
218
  if (Array.isArray(plan.externalInstalls) && plan.externalInstalls.length > 0) {
219
219
  console.log('\nPlanned external installs:');
220
220
  for (const externalInstall of plan.externalInstalls) {
221
- console.log(`- ${externalInstall.id}: ${externalInstall.description || externalInstall.script}`);
221
+ const mode = externalInstall.failureMode === 'warn' ? ' [warn-on-failure]' : '';
222
+ console.log(`- ${externalInstall.id}${mode}: ${externalInstall.description || externalInstall.script}`);
222
223
  }
223
224
  }
224
225
 
@@ -1,9 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict'
3
3
 
4
+ const fs = require('fs')
5
+ const os = require('os')
4
6
  const path = require('path')
5
7
  const { spawnSync } = require('child_process')
6
8
 
9
+ const INSTALL_SH_URL = 'https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh'
10
+ const INSTALL_PS1_URL = 'https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.ps1'
11
+
7
12
  const SUPPORTED_TARGETS = Object.freeze({
8
13
  claude: 'claude',
9
14
  codex: 'codex',
@@ -50,29 +55,144 @@ function parseArgs(argv) {
50
55
  return parsed
51
56
  }
52
57
 
53
- function resolveCodeGraphBin() {
58
+ function isWindows(platform = process.platform) {
59
+ return platform === 'win32'
60
+ }
61
+
62
+ function executableNames(name, platform = process.platform) {
63
+ if (!isWindows(platform)) {
64
+ return [name]
65
+ }
66
+
67
+ const extensions = String(process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM')
68
+ .split(';')
69
+ .map(value => value.trim().toLowerCase())
70
+ .filter(Boolean)
71
+ const lowerName = name.toLowerCase()
72
+ if (extensions.some(ext => lowerName.endsWith(ext))) {
73
+ return [name]
74
+ }
75
+ return [name, ...extensions.map(ext => `${name}${ext}`)]
76
+ }
77
+
78
+ function canExecute(filePath) {
79
+ try {
80
+ fs.accessSync(filePath, fs.constants.X_OK)
81
+ return true
82
+ } catch {
83
+ return false
84
+ }
85
+ }
86
+
87
+ function findOnPath(commandName, options = {}) {
88
+ const searchPath = options.pathValue !== undefined ? options.pathValue : process.env.PATH
89
+ const platform = options.platform || process.platform
90
+ const delimiter = isWindows(platform) ? ';' : path.delimiter
91
+ const names = executableNames(commandName, platform)
92
+
93
+ for (const entry of String(searchPath || '').split(delimiter)) {
94
+ if (!entry) {
95
+ continue
96
+ }
97
+ for (const name of names) {
98
+ const candidate = path.join(entry, name)
99
+ if (canExecute(candidate)) {
100
+ return candidate
101
+ }
102
+ }
103
+ }
104
+
105
+ return null
106
+ }
107
+
108
+ function defaultCodeGraphCandidates(options = {}) {
109
+ const homeDir = options.homeDir || os.homedir()
110
+ const platform = options.platform || process.platform
111
+
112
+ if (isWindows(platform)) {
113
+ const localAppData = process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local')
114
+ return [
115
+ path.join(localAppData, 'codegraph', 'current', 'bin', 'codegraph.exe'),
116
+ path.join(localAppData, 'codegraph', 'current', 'bin', 'codegraph.cmd'),
117
+ ]
118
+ }
119
+
120
+ return [
121
+ path.join(homeDir, '.local', 'bin', 'codegraph'),
122
+ path.join(homeDir, '.codegraph', 'current', 'bin', 'codegraph'),
123
+ ]
124
+ }
125
+
126
+ function resolveCodeGraphBin(options = {}) {
54
127
  if (process.env.CODEGRAPH_INSTALL_BIN) {
55
128
  return {
56
129
  command: process.env.CODEGRAPH_INSTALL_BIN,
57
130
  argsPrefix: [],
58
131
  displayCommand: process.env.CODEGRAPH_INSTALL_BIN,
132
+ source: 'env',
59
133
  }
60
134
  }
61
135
 
62
- let packageJsonPath
63
- try {
64
- packageJsonPath = require.resolve('@colbymchenry/codegraph/package.json')
65
- } catch (error) {
66
- throw new Error(
67
- 'Unable to resolve @colbymchenry/codegraph. Run npm install before applying the CodeGraph integration.'
68
- )
136
+ if (process.env.CODEGRAPH_INSTALL_FORCE_STANDALONE === '1' && !options.ignoreForceStandalone) {
137
+ return null
138
+ }
139
+
140
+ const pathCandidate = findOnPath('codegraph', options)
141
+ if (pathCandidate) {
142
+ return {
143
+ command: pathCandidate,
144
+ argsPrefix: [],
145
+ displayCommand: pathCandidate,
146
+ source: 'path',
147
+ }
148
+ }
149
+
150
+ for (const candidate of defaultCodeGraphCandidates(options)) {
151
+ if (canExecute(candidate)) {
152
+ return {
153
+ command: candidate,
154
+ argsPrefix: [],
155
+ displayCommand: candidate,
156
+ source: 'default-location',
157
+ }
158
+ }
159
+ }
160
+
161
+ return null
162
+ }
163
+
164
+ function resolvePowerShellCommand() {
165
+ return findOnPath('powershell.exe') || findOnPath('powershell') || findOnPath('pwsh') || 'powershell.exe'
166
+ }
167
+
168
+ function buildStandaloneInstallerInfo(platform = process.platform) {
169
+ if (platform === 'darwin' || platform === 'linux') {
170
+ return {
171
+ supported: true,
172
+ platform,
173
+ display: `curl -fsSL ${INSTALL_SH_URL} -o "$TMPDIR/codegraph-install.sh" && sh "$TMPDIR/codegraph-install.sh"`,
174
+ requiredCommand: 'curl',
175
+ url: INSTALL_SH_URL,
176
+ }
177
+ }
178
+
179
+ if (platform === 'win32') {
180
+ return {
181
+ supported: true,
182
+ platform,
183
+ display: `irm ${INSTALL_PS1_URL} | iex`,
184
+ requiredCommand: resolvePowerShellCommand(),
185
+ url: INSTALL_PS1_URL,
186
+ }
69
187
  }
70
188
 
71
- const binPath = path.join(path.dirname(packageJsonPath), 'dist', 'bin', 'codegraph.js')
72
189
  return {
73
- command: process.execPath,
74
- argsPrefix: [binPath],
75
- displayCommand: `${process.execPath} ${binPath}`,
190
+ supported: false,
191
+ platform,
192
+ display: '',
193
+ requiredCommand: '',
194
+ url: '',
195
+ reason: `CodeGraph standalone installer does not support platform: ${platform}`,
76
196
  }
77
197
  }
78
198
 
@@ -88,27 +208,101 @@ function buildInstallCommand(target) {
88
208
 
89
209
  const resolved = resolveCodeGraphBin()
90
210
  const args = [
91
- ...resolved.argsPrefix,
211
+ ...(resolved ? resolved.argsPrefix : []),
92
212
  'install',
93
213
  `--target=${mappedTarget}`,
94
214
  '--location=global',
95
215
  '--yes',
96
216
  ]
217
+ const displayCommand = resolved ? resolved.displayCommand : 'codegraph'
97
218
 
98
219
  return {
99
220
  supported: true,
100
221
  target: mappedTarget,
222
+ command: resolved ? resolved.command : null,
223
+ args,
224
+ display: [displayCommand, ...args.slice(resolved ? resolved.argsPrefix.length : 0)].join(' '),
225
+ needsBootstrap: !resolved,
226
+ bootstrap: buildStandaloneInstallerInfo(),
227
+ binarySource: resolved ? resolved.source : null,
228
+ }
229
+ }
230
+
231
+ function spawnChecked(command, args, options = {}) {
232
+ const result = spawnSync(command, args, {
233
+ cwd: options.cwd || process.cwd(),
234
+ env: options.env || process.env,
235
+ encoding: 'utf8',
236
+ stdio: options.stdio || ['inherit', process.stderr, process.stderr],
237
+ timeout: options.timeout,
238
+ })
239
+
240
+ return typeof result.status === 'number' ? result.status : 1
241
+ }
242
+
243
+ function runStandaloneInstaller() {
244
+ const installer = buildStandaloneInstallerInfo()
245
+ if (!installer.supported) {
246
+ throw new Error(installer.reason)
247
+ }
248
+
249
+ if (installer.platform === 'darwin' || installer.platform === 'linux') {
250
+ const curl = findOnPath('curl') || 'curl'
251
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-install-'))
252
+ const scriptPath = path.join(tempDir, 'install.sh')
253
+ try {
254
+ let status = spawnChecked(curl, ['-fsSL', INSTALL_SH_URL, '-o', scriptPath])
255
+ if (status !== 0) {
256
+ return status
257
+ }
258
+ status = spawnChecked('sh', [scriptPath])
259
+ return status
260
+ } finally {
261
+ fs.rmSync(tempDir, { recursive: true, force: true })
262
+ }
263
+ }
264
+
265
+ return spawnChecked(resolvePowerShellCommand(), [
266
+ '-NoProfile',
267
+ '-ExecutionPolicy',
268
+ 'Bypass',
269
+ '-Command',
270
+ `irm ${INSTALL_PS1_URL} | iex`,
271
+ ])
272
+ }
273
+
274
+ function buildTargetInstallInvocation(target) {
275
+ const mappedTarget = mapTarget(target)
276
+ const resolved = resolveCodeGraphBin({ ignoreForceStandalone: true })
277
+ if (!mappedTarget || !resolved) {
278
+ return null
279
+ }
280
+
281
+ const args = [
282
+ ...resolved.argsPrefix,
283
+ 'install',
284
+ `--target=${mappedTarget}`,
285
+ '--location=global',
286
+ '--yes',
287
+ ]
288
+
289
+ return {
101
290
  command: resolved.command,
102
291
  args,
103
- display: [resolved.displayCommand, ...args.slice(resolved.argsPrefix.length)].join(' '),
104
292
  }
105
293
  }
106
294
 
107
295
  function printHelp() {
108
296
  console.log(`Usage: node scripts/install-codegraph.js [--target <claude|codex|cursor|opencode>] [--dry-run]
109
297
 
110
- Runs the upstream CodeGraph installer for the current TSP install target only.
111
- The project index is not initialized here; run codegraph init -i inside a target project when needed.
298
+ Installs the standalone CodeGraph CLI with the official upstream curl/PowerShell installer when needed,
299
+ then configures CodeGraph for the current TSP install target only.
300
+
301
+ Environment:
302
+ CODEGRAPH_INSTALL_BIN=<path> Use an existing CodeGraph binary, including offline installs.
303
+ CODEGRAPH_INSTALL_FORCE_STANDALONE=1 Re-run the official standalone installer before target config.
304
+
305
+ Project indexes are created by the Claude SessionStart auto-init hook or by running codegraph init -i.
112
306
  `)
113
307
  }
114
308
 
@@ -126,19 +320,31 @@ function run(argv = process.argv.slice(2)) {
126
320
  }
127
321
 
128
322
  if (options.dryRun) {
129
- console.log(`CodeGraph install command: ${install.display}`)
323
+ if (install.needsBootstrap) {
324
+ console.log(`CodeGraph standalone install command: ${install.bootstrap.display}`)
325
+ } else {
326
+ console.log(`CodeGraph binary: ${install.command} (${install.binarySource})`)
327
+ }
328
+ console.log(`CodeGraph target install command: ${install.display}`)
130
329
  return 0
131
330
  }
132
331
 
133
- console.error(`Running CodeGraph installer for ${install.target}`)
134
- const result = spawnSync(install.command, install.args, {
135
- cwd: process.cwd(),
136
- env: process.env,
137
- encoding: 'utf8',
138
- stdio: ['inherit', process.stderr, process.stderr],
139
- })
332
+ if (install.needsBootstrap) {
333
+ console.error('Installing CodeGraph standalone CLI with the official upstream installer')
334
+ const bootstrapStatus = runStandaloneInstaller()
335
+ if (bootstrapStatus !== 0) {
336
+ return bootstrapStatus
337
+ }
338
+ }
140
339
 
141
- return typeof result.status === 'number' ? result.status : 1
340
+ const invocation = buildTargetInstallInvocation(install.target)
341
+ if (!invocation) {
342
+ console.error('CodeGraph binary is still unavailable after standalone install. Add it to PATH or set CODEGRAPH_INSTALL_BIN.')
343
+ return 1
344
+ }
345
+
346
+ console.error(`Running CodeGraph installer for ${install.target}`)
347
+ return spawnChecked(invocation.command, invocation.args)
142
348
  }
143
349
 
144
350
  if (require.main === module) {
@@ -152,7 +358,11 @@ if (require.main === module) {
152
358
 
153
359
  module.exports = {
154
360
  buildInstallCommand,
361
+ buildStandaloneInstallerInfo,
362
+ defaultCodeGraphCandidates,
363
+ findOnPath,
155
364
  mapTarget,
156
365
  parseArgs,
366
+ resolveCodeGraphBin,
157
367
  run,
158
368
  }
@@ -156,6 +156,8 @@ function resolvePlanExternalInstalls(plan) {
156
156
  args: Array.isArray(externalInstall.args)
157
157
  ? externalInstall.args.map(value => String(value))
158
158
  : [],
159
+ failureMode: externalInstall.failureMode === 'warn' ? 'warn' : 'error',
160
+ failureHint: typeof externalInstall.failureHint === 'string' ? externalInstall.failureHint : '',
159
161
  target: plan.target || null,
160
162
  profileId: plan.profileId || null,
161
163
  }));
@@ -213,7 +215,8 @@ function printPlan(plan) {
213
215
  console.log('');
214
216
  console.log(`External install plan (${externalInstalls.length}):`);
215
217
  for (const externalInstall of externalInstalls) {
216
- console.log(`- ${externalInstall.id}: ${externalInstall.description || externalInstall.script}`);
218
+ const mode = externalInstall.failureMode === 'warn' ? ' [warn-on-failure]' : '';
219
+ console.log(`- ${externalInstall.id}${mode}: ${externalInstall.description || externalInstall.script}`);
217
220
  }
218
221
  }
219
222
  }
@@ -219,6 +219,11 @@ function runExternalInstall(externalInstall) {
219
219
  });
220
220
 
221
221
  if (result.status !== 0) {
222
+ if (externalInstall.failureMode === 'warn') {
223
+ console.error(`Warning: optional external install failed: ${label}`);
224
+ console.error(externalInstall.failureHint || 'TSP core install will continue; rerun when the external dependency is reachable.');
225
+ return;
226
+ }
222
227
  throw new Error(`External install failed: ${label}`);
223
228
  }
224
229
  }