@securityreviewai/securityreview-kit 0.1.50 → 0.1.52
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.
- package/README.md +105 -0
- package/bin/securityreview-kit.js +5 -0
- package/package.json +30 -24
- package/src/cli.js +109 -0
- package/src/commands/init.js +851 -0
- package/src/commands/status.js +99 -0
- package/src/commands/switch-project.js +207 -0
- package/src/generators/mcp/claude.js +85 -0
- package/src/generators/mcp/claude.test.js +64 -0
- package/src/generators/mcp/codex.js +70 -0
- package/src/generators/mcp/codex.test.js +43 -0
- package/src/generators/mcp/cursor.js +29 -0
- package/src/generators/mcp/cursor.test.js +50 -0
- package/src/generators/mcp/gemini.js +28 -0
- package/src/generators/mcp/vscode.js +29 -0
- package/src/generators/mcp/windsurf.js +27 -0
- package/src/generators/rules/antigravity.js +22 -0
- package/src/generators/rules/claude.js +87 -0
- package/src/generators/rules/claude.test.js +60 -0
- package/src/generators/rules/codex.js +141 -0
- package/src/generators/rules/codex.test.js +59 -0
- package/src/generators/rules/content.js +110 -0
- package/src/generators/rules/cursor.js +128 -0
- package/src/generators/rules/gemini.js +13 -0
- package/src/generators/rules/guardrails-init-profile.md +56 -0
- package/src/generators/rules/guardrails-profiler/SKILL.md +130 -0
- package/src/generators/rules/guardrails-profiler/references/signal-registry.json +514 -0
- package/src/generators/rules/guardrails-selection/references/category-threat-map.md +232 -0
- package/src/generators/rules/guardrails_rule.md +94 -0
- package/src/generators/rules/hooks.json +11 -0
- package/src/generators/rules/srai-profile.md +32 -0
- package/src/generators/rules/vscode.js +101 -0
- package/src/generators/rules/vscode.test.js +54 -0
- package/src/generators/rules/windsurf.js +13 -0
- package/src/utils/constants.js +95 -0
- package/src/utils/cursor-agent-path.js +67 -0
- package/src/utils/cursor-cli-permissions.js +28 -0
- package/src/utils/detect.js +27 -0
- package/src/utils/fs-helpers.js +82 -0
- package/src/utils/guardrails-profiler-bundle.js +84 -0
- package/src/utils/ide-cli-install.js +138 -0
- package/src/utils/profiler-agent.js +446 -0
- package/src/utils/profiler-agent.test.js +81 -0
- package/src/utils/srai.js +252 -0
- package/dist/api.js +0 -44
- package/dist/commands/guardrails.js +0 -13
- package/dist/commands/init.js +0 -88
- package/dist/commands/profile.js +0 -14
- package/dist/commands/status.js +0 -27
- package/dist/commands/sync.js +0 -6
- package/dist/config.js +0 -18
- package/dist/fs.js +0 -43
- package/dist/index.js +0 -44
- package/dist/profile.js +0 -113
- package/dist/scaffold/claude-code.js +0 -43
- package/dist/scaffold/codex.js +0 -41
- package/dist/scaffold/cursor.js +0 -45
- package/dist/scaffold/gemini.js +0 -10
- package/dist/scaffold/index.js +0 -22
- package/dist/scaffold/mcp.js +0 -15
- package/dist/scaffold/rules.js +0 -191
- package/dist/scaffold/vibreview.js +0 -30
- package/dist/scaffold/vscode.js +0 -28
- package/dist/scaffold/windsurf.js +0 -10
- package/dist/sync/index.js +0 -34
- package/dist/sync/payload.js +0 -23
- package/dist/sync/state.js +0 -12
- package/dist/types.js +0 -1
- package/templates/claude/CLAUDE.md +0 -13
- package/templates/claude/agents/guardrail_profiler.md +0 -12
- package/templates/claude/agents/threat_modeler.md +0 -5
- package/templates/claude/skills/vibreview/SKILL.md +0 -21
- package/templates/claude/skills/vibreview/guardrail_patterns.md +0 -12
- package/templates/cursor/rules/vibreview-security.mdc +0 -8
- /package/{templates/shared → src/generators/rules}/content.md +0 -0
- /package/{templates/shared/guardrails-selection.md → src/generators/rules/guardrails-selection/SKILL.md} +0 -0
- /package/{templates/shared/threat-modelling.md → src/generators/rules/skill.md} +0 -0
- /package/{templates/shared → src/generators/rules}/vibereview-sync/SKILL.md +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { delimiter, isAbsolute, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extra PATH segments where Cursor / other CLIs are often installed.
|
|
8
|
+
* GUI-launched terminals load shell rc files; Node subprocesses often do not.
|
|
9
|
+
*/
|
|
10
|
+
export function augmentPathEnv(baseEnv = process.env) {
|
|
11
|
+
const home = homedir();
|
|
12
|
+
const extra = [
|
|
13
|
+
join(home, '.local', 'bin'),
|
|
14
|
+
join(home, '.cursor', 'bin'),
|
|
15
|
+
'/opt/homebrew/bin',
|
|
16
|
+
'/usr/local/bin',
|
|
17
|
+
];
|
|
18
|
+
if (process.platform === 'win32') {
|
|
19
|
+
extra.push(join(home, 'AppData', 'Local', 'Programs', 'cursor'));
|
|
20
|
+
}
|
|
21
|
+
const pathKey = process.platform === 'win32' ? 'Path' : 'PATH';
|
|
22
|
+
const current = baseEnv[pathKey] || baseEnv.PATH || '';
|
|
23
|
+
const merged = [...extra.filter((p) => p), current].filter(Boolean).join(delimiter);
|
|
24
|
+
return { ...baseEnv, [pathKey]: merged, PATH: merged };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Cursor Agent CLI binary (`agent` or legacy `cursor-agent`) that responds to `--version`, or null.
|
|
29
|
+
* New installs often only ship `agent` in ~/.local/bin (see https://cursor.com/docs/cli/installation).
|
|
30
|
+
*/
|
|
31
|
+
export function resolveCursorAgentExecutable() {
|
|
32
|
+
const env = augmentPathEnv();
|
|
33
|
+
const home = homedir();
|
|
34
|
+
const candidates = [
|
|
35
|
+
join(home, '.local', 'bin', 'agent'),
|
|
36
|
+
join(home, '.local', 'bin', 'cursor-agent'),
|
|
37
|
+
...(process.platform === 'win32'
|
|
38
|
+
? [
|
|
39
|
+
join(home, '.local', 'bin', 'agent.cmd'),
|
|
40
|
+
join(home, '.local', 'bin', 'cursor-agent.cmd'),
|
|
41
|
+
]
|
|
42
|
+
: []),
|
|
43
|
+
join(home, '.cursor', 'bin', 'agent'),
|
|
44
|
+
join(home, '.cursor', 'bin', 'cursor-agent'),
|
|
45
|
+
'agent',
|
|
46
|
+
'cursor-agent',
|
|
47
|
+
];
|
|
48
|
+
if (process.platform === 'win32') {
|
|
49
|
+
const base = join(home, 'AppData', 'Local', 'Programs', 'cursor');
|
|
50
|
+
candidates.splice(
|
|
51
|
+
candidates.length - 2,
|
|
52
|
+
0,
|
|
53
|
+
join(base, 'agent.exe'),
|
|
54
|
+
join(base, 'cursor-agent.exe'),
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
for (const cmd of candidates) {
|
|
58
|
+
if (isAbsolute(cmd) && !existsSync(cmd)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const r = spawnSync(cmd, ['--version'], { stdio: 'ignore', env });
|
|
62
|
+
if (r.status === 0) {
|
|
63
|
+
return cmd;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { MCP_SERVER_NAME } from './constants.js';
|
|
3
|
+
import { readJson, writeJson } from './fs-helpers.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Merge project-level Cursor CLI permissions so cursor-agent can use the SRAI MCP tools.
|
|
7
|
+
* @see https://docs.cursor.com/cli/reference/configuration
|
|
8
|
+
* @see https://docs.cursor.com/cli/reference/permissions
|
|
9
|
+
*/
|
|
10
|
+
export function mergeCursorCliMcpAllowlist(cwd) {
|
|
11
|
+
const cliPath = join(cwd, '.cursor', 'cli.json');
|
|
12
|
+
const existing = readJson(cliPath) || {};
|
|
13
|
+
if (!existing.permissions || typeof existing.permissions !== 'object' || Array.isArray(existing.permissions)) {
|
|
14
|
+
existing.permissions = {};
|
|
15
|
+
}
|
|
16
|
+
if (!Array.isArray(existing.permissions.allow)) {
|
|
17
|
+
existing.permissions.allow = [];
|
|
18
|
+
}
|
|
19
|
+
const token = `Mcp(${MCP_SERVER_NAME}:*)`;
|
|
20
|
+
if (!existing.permissions.allow.includes(token)) {
|
|
21
|
+
existing.permissions.allow.push(token);
|
|
22
|
+
}
|
|
23
|
+
if (!Array.isArray(existing.permissions.deny)) {
|
|
24
|
+
existing.permissions.deny = [];
|
|
25
|
+
}
|
|
26
|
+
writeJson(cliPath, existing);
|
|
27
|
+
return cliPath;
|
|
28
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { TARGETS, TARGET_NAMES } from './constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Auto-detect which IDE/CLI targets are configured in the given directory
|
|
7
|
+
* by checking for known directories.
|
|
8
|
+
* @param {string} cwd - The directory to scan
|
|
9
|
+
* @returns {string[]} - List of detected target keys
|
|
10
|
+
*/
|
|
11
|
+
export function detectTargets(cwd) {
|
|
12
|
+
const detected = [];
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
|
|
15
|
+
for (const key of TARGET_NAMES) {
|
|
16
|
+
const target = TARGETS[key];
|
|
17
|
+
for (const dir of target.detectDirs) {
|
|
18
|
+
const fullPath = join(cwd, dir);
|
|
19
|
+
if (existsSync(fullPath) && !seen.has(key)) {
|
|
20
|
+
detected.push(key);
|
|
21
|
+
seen.add(key);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return detected;
|
|
27
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { SENTINEL_START, SENTINEL_END } from './constants.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Ensure a directory exists, creating parent dirs as needed.
|
|
7
|
+
*/
|
|
8
|
+
export function ensureDir(dirPath) {
|
|
9
|
+
if (!existsSync(dirPath)) {
|
|
10
|
+
mkdirSync(dirPath, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read a JSON file, returning null if it doesn't exist or is invalid.
|
|
16
|
+
*/
|
|
17
|
+
export function readJson(filePath) {
|
|
18
|
+
try {
|
|
19
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Write an object as pretty-printed JSON.
|
|
28
|
+
*/
|
|
29
|
+
export function writeJson(filePath, data) {
|
|
30
|
+
ensureDir(dirname(filePath));
|
|
31
|
+
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read a text file, returning empty string if it doesn't exist.
|
|
36
|
+
*/
|
|
37
|
+
export function readText(filePath) {
|
|
38
|
+
try {
|
|
39
|
+
return readFileSync(filePath, 'utf-8');
|
|
40
|
+
} catch {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Write raw text to a file, ensuring parent dirs exist.
|
|
47
|
+
*/
|
|
48
|
+
export function writeText(filePath, content) {
|
|
49
|
+
ensureDir(dirname(filePath));
|
|
50
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Append or replace a sentinel-guarded block in a file.
|
|
55
|
+
* If the file exists and already has the block, it replaces it.
|
|
56
|
+
* If the file exists but doesn't have the block, it appends.
|
|
57
|
+
* If the file doesn't exist, it creates it with just the block.
|
|
58
|
+
*/
|
|
59
|
+
export function upsertSentinelBlock(filePath, content) {
|
|
60
|
+
const block = `${SENTINEL_START}\n${content}\n${SENTINEL_END}`;
|
|
61
|
+
const existing = readText(filePath);
|
|
62
|
+
|
|
63
|
+
if (!existing) {
|
|
64
|
+
writeText(filePath, block + '\n');
|
|
65
|
+
return 'created';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const startIdx = existing.indexOf(SENTINEL_START);
|
|
69
|
+
const endIdx = existing.indexOf(SENTINEL_END);
|
|
70
|
+
|
|
71
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
72
|
+
const before = existing.substring(0, startIdx);
|
|
73
|
+
const after = existing.substring(endIdx + SENTINEL_END.length);
|
|
74
|
+
writeText(filePath, before + block + after);
|
|
75
|
+
return 'updated';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Append with a blank line separator
|
|
79
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
80
|
+
writeText(filePath, existing + separator + block + '\n');
|
|
81
|
+
return 'appended';
|
|
82
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { copyFileSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { GUARDRAILS_PROFILER_SKILL_REL_DIR, GUARDRAILS_SELECTION_SKILL_REL_DIR } from './constants.js';
|
|
5
|
+
import { ensureDir, writeText } from './fs-helpers.js';
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
function sanitizeProjectName(value) {
|
|
10
|
+
return String(value || '').replace(/[\r\n`]/g, ' ').trim();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function injectProjectName(content, projectName) {
|
|
14
|
+
const resolved = sanitizeProjectName(projectName) || '<SRAI_PROJECT_NAME>';
|
|
15
|
+
return content.replaceAll('{{SRAI_PROJECT_NAME}}', resolved).replaceAll('<SRAI_PROJECT_NAME>', resolved);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function injectSkillDir(content, skillDirRel) {
|
|
19
|
+
return content
|
|
20
|
+
.replaceAll('<GUARDRAILS_SKILL_DIR>', skillDirRel)
|
|
21
|
+
.replaceAll('{{GUARDRAILS_SELECTION_SKILL_DIR}}', skillDirRel);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function injectSkillTemplate(content, projectName, skillDirRel) {
|
|
25
|
+
return injectSkillDir(injectProjectName(content, projectName), skillDirRel);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const PROFILER_BUNDLE_ROOT = join(__dirname, '..', 'generators', 'rules', 'guardrails-profiler');
|
|
29
|
+
const SELECTION_BUNDLE_ROOT = join(__dirname, '..', 'generators', 'rules', 'guardrails-selection');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Writes bundled guardrails skills under each selected IDE skills directory.
|
|
33
|
+
*
|
|
34
|
+
* @returns {string[]} Absolute paths to each skill root written */
|
|
35
|
+
export function writeGuardrailsSkillBundles(cwd, options = {}) {
|
|
36
|
+
const targets = Array.isArray(options.targets) ? options.targets : [];
|
|
37
|
+
const written = [];
|
|
38
|
+
|
|
39
|
+
for (const target of targets) {
|
|
40
|
+
const profilerRel = GUARDRAILS_PROFILER_SKILL_REL_DIR[target];
|
|
41
|
+
if (profilerRel) {
|
|
42
|
+
const profilerDestBase = join(cwd, profilerRel);
|
|
43
|
+
const profilerDestRefs = join(profilerDestBase, 'references');
|
|
44
|
+
ensureDir(profilerDestRefs);
|
|
45
|
+
|
|
46
|
+
const profilerSkillTemplate = readFileSync(join(PROFILER_BUNDLE_ROOT, 'SKILL.md'), 'utf-8');
|
|
47
|
+
writeText(
|
|
48
|
+
join(profilerDestBase, 'SKILL.md'),
|
|
49
|
+
injectSkillTemplate(profilerSkillTemplate, options.projectName, profilerRel),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
copyFileSync(
|
|
53
|
+
join(PROFILER_BUNDLE_ROOT, 'references', 'signal-registry.json'),
|
|
54
|
+
join(profilerDestRefs, 'signal-registry.json'),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
written.push(profilerDestBase);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const selectionRel = GUARDRAILS_SELECTION_SKILL_REL_DIR[target];
|
|
61
|
+
if (selectionRel) {
|
|
62
|
+
const selectionDestBase = join(cwd, selectionRel);
|
|
63
|
+
const selectionDestRefs = join(selectionDestBase, 'references');
|
|
64
|
+
ensureDir(selectionDestRefs);
|
|
65
|
+
|
|
66
|
+
const selectionSkillTemplate = readFileSync(join(SELECTION_BUNDLE_ROOT, 'SKILL.md'), 'utf-8');
|
|
67
|
+
writeText(
|
|
68
|
+
join(selectionDestBase, 'SKILL.md'),
|
|
69
|
+
injectSkillTemplate(selectionSkillTemplate, options.projectName, selectionRel),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
copyFileSync(
|
|
73
|
+
join(SELECTION_BUNDLE_ROOT, 'references', 'category-threat-map.md'),
|
|
74
|
+
join(selectionDestRefs, 'category-threat-map.md'),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
written.push(selectionDestBase);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return written;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const writeGuardrailsProfilerBundles = writeGuardrailsSkillBundles;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { augmentPathEnv, resolveCursorAgentExecutable } from './cursor-agent-path.js';
|
|
3
|
+
|
|
4
|
+
const AGENT_CLI_TARGETS = new Set(['cursor', 'claude', 'vscode', 'codex']);
|
|
5
|
+
|
|
6
|
+
function commandOk(cmd, args = ['--version'], env = process.env) {
|
|
7
|
+
const r = spawnSync(cmd, args, { stdio: 'ignore', env });
|
|
8
|
+
return r.status === 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function runShell(script) {
|
|
12
|
+
return spawnSync(script, { shell: true, stdio: 'inherit' });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Ensure Cursor / Claude Code / Copilot / Codex CLIs are present when those targets are selected.
|
|
17
|
+
* Installation uses vendor scripts or npm where appropriate.
|
|
18
|
+
*/
|
|
19
|
+
export function ensureIdeCliForTarget(target, options = {}) {
|
|
20
|
+
const { skipInstall = false } = options;
|
|
21
|
+
|
|
22
|
+
if (!AGENT_CLI_TARGETS.has(target)) {
|
|
23
|
+
return { target, ok: true, skipped: true };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (target === 'cursor') {
|
|
27
|
+
if (resolveCursorAgentExecutable()) {
|
|
28
|
+
return { target, ok: true, already: true };
|
|
29
|
+
}
|
|
30
|
+
if (skipInstall) {
|
|
31
|
+
return {
|
|
32
|
+
target,
|
|
33
|
+
ok: false,
|
|
34
|
+
message:
|
|
35
|
+
'Cursor Agent CLI not found (`agent` or `cursor-agent`). Install from https://cursor.com/docs/cli/installation.',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (process.platform === 'win32') {
|
|
39
|
+
return {
|
|
40
|
+
target,
|
|
41
|
+
ok: false,
|
|
42
|
+
message: 'Install Cursor CLI manually: https://cursor.com/cli',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const r = runShell('curl -fsSL https://cursor.com/install | bash');
|
|
46
|
+
if (r.status !== 0) {
|
|
47
|
+
return { target, ok: false, message: 'Cursor CLI install script failed' };
|
|
48
|
+
}
|
|
49
|
+
if (!resolveCursorAgentExecutable()) {
|
|
50
|
+
return {
|
|
51
|
+
target,
|
|
52
|
+
ok: false,
|
|
53
|
+
message:
|
|
54
|
+
'Cursor Agent CLI (`agent` / `cursor-agent`) not found after install; open a new shell or ensure ~/.local/bin exists and re-run init',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { target, ok: true };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (target === 'claude') {
|
|
61
|
+
if (commandOk('claude', ['--version'], augmentPathEnv())) {
|
|
62
|
+
return { target, ok: true, already: true };
|
|
63
|
+
}
|
|
64
|
+
if (skipInstall) {
|
|
65
|
+
return { target, ok: false, message: 'claude not found on PATH' };
|
|
66
|
+
}
|
|
67
|
+
if (process.platform === 'win32') {
|
|
68
|
+
const r = runShell('powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://claude.ai/install.ps1 | iex"');
|
|
69
|
+
return r.status === 0
|
|
70
|
+
? { target, ok: true }
|
|
71
|
+
: { target, ok: false, message: 'Claude Code install failed' };
|
|
72
|
+
}
|
|
73
|
+
const r = runShell('curl -fsSL https://claude.ai/install.sh | bash');
|
|
74
|
+
return r.status === 0
|
|
75
|
+
? { target, ok: true }
|
|
76
|
+
: { target, ok: false, message: 'Claude Code install failed' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (target === 'codex') {
|
|
80
|
+
if (commandOk('codex', ['--version'], augmentPathEnv())) {
|
|
81
|
+
return { target, ok: true, already: true };
|
|
82
|
+
}
|
|
83
|
+
if (skipInstall) {
|
|
84
|
+
return { target, ok: false, message: 'codex not found on PATH' };
|
|
85
|
+
}
|
|
86
|
+
const r = runShell('npm install -g @openai/codex');
|
|
87
|
+
return r.status === 0
|
|
88
|
+
? { target, ok: true }
|
|
89
|
+
: { target, ok: false, message: 'Codex CLI npm install failed' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (target === 'vscode') {
|
|
93
|
+
if (commandOk('copilot', ['--version'], augmentPathEnv())) {
|
|
94
|
+
return { target, ok: true, already: true };
|
|
95
|
+
}
|
|
96
|
+
if (skipInstall) {
|
|
97
|
+
return {
|
|
98
|
+
target,
|
|
99
|
+
ok: false,
|
|
100
|
+
message:
|
|
101
|
+
'GitHub Copilot CLI not found (`copilot`). Install from https://docs.github.com/copilot/how-tos/copilot-cli/set-up-copilot-cli/install-copilot-cli.',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (process.platform === 'win32') {
|
|
105
|
+
const r = runShell('winget install GitHub.Copilot');
|
|
106
|
+
if (r.status !== 0) {
|
|
107
|
+
return { target, ok: false, message: 'GitHub Copilot CLI winget install failed' };
|
|
108
|
+
}
|
|
109
|
+
return commandOk('copilot', ['--version'], augmentPathEnv())
|
|
110
|
+
? { target, ok: true }
|
|
111
|
+
: {
|
|
112
|
+
target,
|
|
113
|
+
ok: false,
|
|
114
|
+
message:
|
|
115
|
+
'GitHub Copilot CLI (`copilot`) not found after install; open a new shell or ensure it is on PATH and re-run init',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const r = runShell('curl -fsSL https://gh.io/copilot-install | bash');
|
|
119
|
+
if (r.status !== 0) {
|
|
120
|
+
return { target, ok: false, message: 'GitHub Copilot CLI install script failed' };
|
|
121
|
+
}
|
|
122
|
+
if (!commandOk('copilot', ['--version'], augmentPathEnv())) {
|
|
123
|
+
return {
|
|
124
|
+
target,
|
|
125
|
+
ok: false,
|
|
126
|
+
message:
|
|
127
|
+
'GitHub Copilot CLI (`copilot`) not found after install; open a new shell or ensure the install directory is on PATH and re-run init',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
return { target, ok: true };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { target, ok: true, skipped: true };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function ensureIdeClisForTargets(targets, options = {}) {
|
|
137
|
+
return targets.map((t) => ensureIdeCliForTarget(t, options));
|
|
138
|
+
}
|