@adityaaria/spark 6.0.15 → 6.0.17

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.
@@ -1,42 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
-
5
- const CURSOR_PLUGIN_DIR = path.join('.cursor', 'plugins', 'spark');
6
- const COPY_PATHS = [
7
- '.cursor-plugin',
8
- 'assets',
9
- path.join('hooks', 'hooks-cursor.json'),
10
- path.join('hooks', 'run-hook.cmd'),
11
- path.join('hooks', 'session-start'),
12
- 'skills',
13
- ];
14
-
15
- export function installCursorPlugin({ packageRoot, env = process.env, dryRun = false }) {
16
- const homeDir = env.HOME || env.USERPROFILE || os.homedir();
17
- const targetRoot = path.join(homeDir, CURSOR_PLUGIN_DIR);
18
-
19
- if (!dryRun) {
20
- fs.mkdirSync(targetRoot, { recursive: true });
21
-
22
- for (const relativePath of COPY_PATHS) {
23
- const sourcePath = path.join(packageRoot, relativePath);
24
- const targetPath = path.join(targetRoot, relativePath);
25
- const stat = fs.statSync(sourcePath);
26
-
27
- if (stat.isDirectory()) {
28
- fs.cpSync(sourcePath, targetPath, { recursive: true });
29
- continue;
30
- }
31
-
32
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
33
- fs.copyFileSync(sourcePath, targetPath);
34
- fs.chmodSync(targetPath, stat.mode);
35
- }
36
- }
37
-
38
- return {
39
- targetRoot,
40
- relativeTargetRoot: `~/${CURSOR_PLUGIN_DIR}`,
41
- };
42
- }
@@ -1,93 +0,0 @@
1
- import { createAdapter } from './common.js';
2
- import { installOpenCodePlugin } from './opencode-staging.js';
3
- import { dirname, resolve } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
-
6
- const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
7
-
8
- export function createOpenCodeAdapter() {
9
- return createAdapter({
10
- id: 'opencode',
11
- label: 'OpenCode',
12
- kind: 'extension',
13
- envKeys: ['OPENCODE_CONFIG_DIR'],
14
- binaryNames: ['opencode'],
15
- configPaths: ['opencode.json'],
16
- bootstrap: 'using-spark -> .opencode/plugins/spark.js -> message transform bootstrap',
17
- installHint: '.opencode/plugins/spark.js + .opencode/INSTALL.md',
18
- verifyHint: 'Restart OpenCode, then confirm spark loads in a fresh session.',
19
- successMessage: 'Installed SPARK for OpenCode.',
20
- automatedSteps: [
21
- 'Install the SPARK package into the OpenCode config directory.',
22
- 'Register the spark plugin entry inside the OpenCode plugins directory.',
23
- 'Restart OpenCode to load the plugin and skills.',
24
- ],
25
- customInstall({ env, dryRun }) {
26
- return installOpenCodePlugin({ packageRoot, env, dryRun });
27
- },
28
- });
29
- }
30
-
31
- export function createPiAdapter() {
32
- return createAdapter({
33
- id: 'pi',
34
- label: 'Pi',
35
- kind: 'extension',
36
- envKeys: ['PI_HOME'],
37
- binaryNames: ['pi'],
38
- bootstrap: 'using-spark -> .pi/extensions/spark.ts -> session_start/session_compact bootstrap',
39
- installHint: '.pi/extensions/spark.ts + package.json pi fields',
40
- verifyHint: 'Run a fresh Pi session and confirm using-spark loads at session start.',
41
- command: {
42
- file: 'pi',
43
- args: ['install', 'git:github.com/adityaaria/SPARK'],
44
- },
45
- });
46
- }
47
-
48
- export function createGeminiAdapter() {
49
- return createAdapter({
50
- id: 'gemini',
51
- label: 'Gemini CLI',
52
- kind: 'context-file',
53
- packageRoot,
54
- envKeys: ['GEMINI_HOME', 'GOOGLE_GEMINI_CLI'],
55
- binaryNames: ['gemini'],
56
- configPaths: ['GEMINI.md', 'gemini-extension.json'],
57
- bootstrap: 'using-spark -> gemini-extension.json -> GEMINI.md context file',
58
- installHint: 'gemini-extension.json + GEMINI.md + references/gemini-tools.md',
59
- verifyHint: 'Run a fresh Gemini CLI session and confirm using-spark loads at session start.',
60
- successMessage: 'Installed SPARK for Gemini CLI.',
61
- automatedSteps: [
62
- 'Install the SPARK extension into Gemini CLI.',
63
- 'Start a fresh Gemini CLI session to confirm using-spark loads at startup.',
64
- ],
65
- command: {
66
- file: 'gemini',
67
- args: ['extensions', 'install', 'https://github.com/adityaaria/SPARK'],
68
- },
69
- });
70
- }
71
-
72
- export function createAntigravityAdapter() {
73
- return createAdapter({
74
- id: 'antigravity',
75
- label: 'Antigravity',
76
- kind: 'context-file',
77
- packageRoot,
78
- envKeys: ['ANTIGRAVITY_PLUGIN_ROOT'],
79
- binaryNames: ['agy'],
80
- bootstrap: 'using-spark -> agy plugin install -> contextFileName bootstrap',
81
- installHint: 'skills/using-spark/references/antigravity-tools.md + plugin install flow',
82
- verifyHint: 'Run a fresh Antigravity session and confirm using-spark loads from the plugin install.',
83
- successMessage: 'Installed SPARK for Antigravity.',
84
- automatedSteps: [
85
- 'Install the SPARK plugin into Antigravity.',
86
- 'Start a fresh Antigravity session to confirm using-spark loads before coding.',
87
- ],
88
- command: {
89
- file: 'agy',
90
- args: ['plugin', 'install', 'https://github.com/adityaaria/SPARK'],
91
- },
92
- });
93
- }
@@ -1,61 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
-
5
- const PACKAGE_DIR = 'spark';
6
- const SPARK_PLUGIN_FILE = path.join(PACKAGE_DIR, '.opencode', 'plugins', 'spark.js');
7
-
8
- export function installOpenCodePlugin({ packageRoot, env = process.env, dryRun = false }) {
9
- const homeDir = env.HOME || env.USERPROFILE || os.homedir();
10
- const configDir = env.OPENCODE_CONFIG_DIR
11
- ? resolveHomeAwarePath(env.OPENCODE_CONFIG_DIR, homeDir)
12
- : path.join(homeDir, '.config', 'opencode');
13
-
14
- const sparkRoot = path.join(configDir, PACKAGE_DIR);
15
- const pluginFile = path.join(configDir, SPARK_PLUGIN_FILE);
16
- const registeredPlugin = path.join(configDir, 'plugins', 'spark.js');
17
-
18
- if (!dryRun) {
19
- fs.mkdirSync(sparkRoot, { recursive: true });
20
- fs.cpSync(path.join(packageRoot, 'skills'), path.join(sparkRoot, 'skills'), { recursive: true });
21
-
22
- fs.mkdirSync(path.dirname(pluginFile), { recursive: true });
23
- fs.copyFileSync(path.join(packageRoot, '.opencode', 'plugins', 'spark.js'), pluginFile);
24
-
25
- fs.mkdirSync(path.dirname(registeredPlugin), { recursive: true });
26
- safeReplace(registeredPlugin);
27
- fs.symlinkSync(pluginFile, registeredPlugin);
28
- }
29
-
30
- return {
31
- targetRoot: sparkRoot,
32
- relativeTargetRoot: simplifyHomePath(sparkRoot, homeDir),
33
- pluginFile: simplifyHomePath(pluginFile, homeDir),
34
- registeredPlugin: simplifyHomePath(registeredPlugin, homeDir),
35
- };
36
- }
37
-
38
- function resolveHomeAwarePath(target, homeDir) {
39
- if (target === '~') {
40
- return homeDir;
41
- }
42
- if (target.startsWith('~/')) {
43
- return path.join(homeDir, target.slice(2));
44
- }
45
- return path.resolve(target);
46
- }
47
-
48
- function simplifyHomePath(target, homeDir) {
49
- if (target.startsWith(`${homeDir}${path.sep}`)) {
50
- return `~/${path.relative(homeDir, target).split(path.sep).join('/')}`;
51
- }
52
- return target;
53
- }
54
-
55
- function safeReplace(target) {
56
- try {
57
- fs.rmSync(target, { force: true });
58
- } catch {
59
- // Ignore missing targets.
60
- }
61
- }
@@ -1,138 +0,0 @@
1
- import { createAdapter } from './common.js';
2
- import { stageClaudePlugin } from './claude-staging.js';
3
- import { stageCodexPlugin } from './codex-staging.js';
4
- import { installCursorPlugin } from './cursor-staging.js';
5
- import { installVsCodePlugin } from './vscode-staging.js';
6
- import { dirname, resolve } from 'node:path';
7
- import { fileURLToPath } from 'node:url';
8
-
9
- const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
10
-
11
- export function createClaudeCodeAdapter() {
12
- return createAdapter({
13
- id: 'claude',
14
- label: 'Claude Code',
15
- kind: 'shell-hook',
16
- envKeys: ['CLAUDE_PLUGIN_ROOT'],
17
- binaryNames: ['claude'],
18
- bootstrap: 'shell hook -> hooks/session-start -> using-spark',
19
- installHint: '.claude-plugin/plugin.json + hooks/hooks.json + hooks/session-start',
20
- verifyHint: 'Run a fresh Claude Code session and confirm using-spark loads before coding.',
21
- successMessage: 'Installed SPARK for Claude Code.',
22
- commands: [
23
- {
24
- file: 'claude',
25
- args: ['plugin', 'marketplace', 'add', '{relativeMarketplaceRoot}'],
26
- },
27
- {
28
- file: 'claude',
29
- args: ['plugin', 'install', 'spark@{marketplaceName}'],
30
- },
31
- ],
32
- automatedSteps: [
33
- 'Stage a local Claude Code marketplace at .spark/claude-marketplace.',
34
- 'Register the local marketplace with Claude Code.',
35
- 'Install the spark plugin from that marketplace.',
36
- ],
37
- customInstall({ cwd, dryRun }) {
38
- return stageClaudePlugin({ cwd, packageRoot, dryRun });
39
- },
40
- });
41
- }
42
-
43
- export function createCodexAdapter() {
44
- return createAdapter({
45
- id: 'codex',
46
- label: 'Codex CLI',
47
- kind: 'shell-hook',
48
- envKeys: ['CLAUDE_PLUGIN_ROOT'],
49
- binaryNames: ['codex'],
50
- bootstrap: 'shell hook -> hooks/session-start-codex -> using-spark',
51
- installHint: '.codex-plugin/plugin.json + hooks/hooks-codex.json + hooks/session-start-codex',
52
- verifyHint: 'Run a fresh Codex session and confirm using-spark loads before coding.',
53
- successMessage: 'Installed SPARK for Codex CLI.',
54
- commands: [
55
- {
56
- file: 'codex',
57
- args: ['plugin', 'marketplace', 'add', '{relativeMarketplaceRoot}'],
58
- },
59
- {
60
- file: 'codex',
61
- args: ['plugin', 'add', 'spark@{marketplaceName}'],
62
- },
63
- ],
64
- automatedSteps: [
65
- 'Stage a local Codex marketplace at .spark/codex-marketplace.',
66
- 'Register the local marketplace with Codex.',
67
- 'Install the spark plugin from that marketplace.',
68
- ],
69
- customInstall({ cwd, dryRun }) {
70
- return stageCodexPlugin({ cwd, packageRoot, dryRun });
71
- },
72
- });
73
- }
74
-
75
- export function createVsCodeAdapter() {
76
- return createAdapter({
77
- id: 'vscode',
78
- label: 'VS Code',
79
- kind: 'shell-hook',
80
- bootstrap: 'workspace plugin bundle -> VS Code chat.pluginLocations -> hooks/session-start -> using-spark',
81
- installHint: 'A standard plugin bundle is staged locally and registered through .vscode/settings.json chat.pluginLocations.',
82
- verifyHint: 'Open a fresh VS Code agent session and confirm using-spark loads before coding.',
83
- successMessage: 'SPARK is ready in VS Code.',
84
- automatedSteps: [
85
- 'Prepare a local VS Code plugin bundle at .spark/vscode-plugin.',
86
- 'Register that bundle in .vscode/settings.json via chat.pluginLocations.',
87
- 'Start a fresh VS Code agent session so using-spark loads before coding.',
88
- ],
89
- customInstall({ cwd, dryRun }) {
90
- return installVsCodePlugin({ cwd, packageRoot, dryRun });
91
- },
92
- });
93
- }
94
-
95
- export function createCursorAdapter() {
96
- return createAdapter({
97
- id: 'cursor',
98
- label: 'Cursor',
99
- kind: 'shell-hook',
100
- envKeys: ['CURSOR_PLUGIN_ROOT'],
101
- binaryNames: ['cursor'],
102
- bootstrap: 'shell hook -> hooks/session-start -> using-spark',
103
- installHint: '.cursor-plugin/plugin.json + hooks/hooks-cursor.json + hooks/session-start',
104
- verifyHint: 'Restart Cursor fully, then confirm using-spark loads in a fresh Agent session.',
105
- successMessage: 'Installed SPARK for Cursor.',
106
- automatedSteps: [
107
- 'Copy the SPARK plugin into ~/.cursor/plugins/spark.',
108
- 'Restart Cursor fully to load the new plugin.',
109
- 'Start a fresh Cursor Agent session and confirm using-spark loads before coding.',
110
- ],
111
- customInstall({ env, dryRun }) {
112
- return installCursorPlugin({ packageRoot, env, dryRun });
113
- },
114
- });
115
- }
116
-
117
- export function createCopilotAdapter() {
118
- return createAdapter({
119
- id: 'copilot',
120
- label: 'Copilot CLI',
121
- kind: 'shell-hook',
122
- envKeys: ['COPILOT_CLI'],
123
- binaryNames: ['copilot'],
124
- bootstrap: 'shell hook -> hooks/session-start -> using-spark',
125
- installHint: 'skills/using-spark/references/copilot-tools.md + hooks/session-start',
126
- verifyHint: 'Run a fresh Copilot CLI session and confirm using-spark loads before coding.',
127
- commands: [
128
- {
129
- file: 'copilot',
130
- args: ['plugin', 'marketplace', 'add', 'adityaaria/SPARK-marketplace'],
131
- },
132
- {
133
- file: 'copilot',
134
- args: ['plugin', 'install', 'spark@spark-marketplace'],
135
- },
136
- ],
137
- });
138
- }
@@ -1,75 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
-
4
- const VSCODE_PLUGIN_DIR = path.join('.spark', 'vscode-plugin');
5
- const SETTINGS_PATH = path.join('.vscode', 'settings.json');
6
- const COPY_PATHS = [
7
- { src: '.claude-plugin', dest: '.vscode-plugin' },
8
- { src: 'assets', dest: 'assets' },
9
- { src: path.join('hooks', 'hooks.json'), dest: path.join('hooks', 'hooks.json') },
10
- { src: path.join('hooks', 'run-hook.cmd'), dest: path.join('hooks', 'run-hook.cmd') },
11
- { src: path.join('hooks', 'session-start'), dest: path.join('hooks', 'session-start') },
12
- { src: 'skills', dest: 'skills' },
13
- ];
14
-
15
- export function installVsCodePlugin({ cwd = process.cwd(), packageRoot, dryRun = false }) {
16
- const targetRoot = path.join(cwd, VSCODE_PLUGIN_DIR);
17
- const settingsPath = path.join(cwd, SETTINGS_PATH);
18
-
19
- if (!dryRun) {
20
- fs.mkdirSync(targetRoot, { recursive: true });
21
-
22
- for (const mapping of COPY_PATHS) {
23
- const sourcePath = path.join(packageRoot, mapping.src);
24
- const targetPath = path.join(targetRoot, mapping.dest);
25
- const stat = fs.statSync(sourcePath);
26
-
27
- if (stat.isDirectory()) {
28
- fs.cpSync(sourcePath, targetPath, { recursive: true });
29
- continue;
30
- }
31
-
32
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
33
- fs.copyFileSync(sourcePath, targetPath);
34
- fs.chmodSync(targetPath, stat.mode);
35
- }
36
-
37
- const targetPluginJsonPath = path.join(targetRoot, '.vscode-plugin', 'plugin.json');
38
- if (fs.existsSync(targetPluginJsonPath)) {
39
- const pluginData = JSON.parse(fs.readFileSync(targetPluginJsonPath, 'utf8'));
40
- pluginData.description = "Core skills library for VS Code: TDD, debugging, collaboration patterns, and proven techniques";
41
- fs.writeFileSync(targetPluginJsonPath, JSON.stringify(pluginData, null, 2) + '\n', 'utf8');
42
- }
43
-
44
- writeVsCodeSettings(settingsPath, targetRoot);
45
- }
46
-
47
- return {
48
- targetRoot,
49
- relativeTargetRoot: VSCODE_PLUGIN_DIR,
50
- settingsPath,
51
- relativeSettingsPath: SETTINGS_PATH,
52
- };
53
- }
54
-
55
- function writeVsCodeSettings(settingsPath, targetRoot) {
56
- fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
57
-
58
- let settings = {};
59
- if (fs.existsSync(settingsPath)) {
60
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
61
- }
62
-
63
- const pluginLocations = isPlainObject(settings['chat.pluginLocations'])
64
- ? { ...settings['chat.pluginLocations'] }
65
- : {};
66
-
67
- pluginLocations[targetRoot] = true;
68
- settings['chat.pluginLocations'] = pluginLocations;
69
-
70
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
71
- }
72
-
73
- function isPlainObject(value) {
74
- return value !== null && typeof value === 'object' && !Array.isArray(value);
75
- }
@@ -1,257 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { formatPromptBlock, promptOption } from '../cli/output.js';
5
- import { getAdapterById, listAdapters } from './registry.js';
6
- import { InstallerError } from './errors.js';
7
-
8
- export function detectHarnessCandidates({ env = process.env, fsImpl = fs } = {}) {
9
- const homeDir = os.homedir();
10
- const adapters = listAdapters();
11
-
12
- return adapters
13
- .map((adapter) => scoreAdapter(adapter, env, fsImpl, homeDir))
14
- .filter((candidate) => candidate.score > 0)
15
- .sort((a, b) => b.score - a.score || a.label.localeCompare(b.label));
16
- }
17
-
18
- export async function chooseHarness({
19
- forcedHarness = null,
20
- env = process.env,
21
- fs = fs,
22
- prompt = null,
23
- } = {}) {
24
- if (forcedHarness) {
25
- const adapter = getAdapterById(normalizeHarness(forcedHarness));
26
- if (!adapter) {
27
- throw new InstallerError(`Unsupported harness: ${forcedHarness}`);
28
- }
29
- return { adapter, source: 'forced', candidates: [] };
30
- }
31
-
32
- if (!prompt) {
33
- throw new InstallerError('No harness selected. Re-run with --harness or use an interactive terminal.');
34
- }
35
-
36
- const homeDir = os.homedir();
37
- const detectedCandidates = detectHarnessCandidates({ env, fsImpl: fs });
38
- const candidates = buildPromptCandidates(detectedCandidates, env, fs, homeDir);
39
-
40
- let validationMessage = null;
41
- while (true) {
42
- const answer = await prompt(renderPrompt(candidates, validationMessage));
43
- const adapter = resolvePromptAnswer(answer, candidates);
44
- if (adapter) {
45
- return { adapter, source: 'prompt', candidates };
46
- }
47
- validationMessage = `Unsupported choice: ${answer || '(empty)'}. Please choose a number or harness name from the list.`;
48
- }
49
- }
50
-
51
- function scoreAdapter(adapter, env, fsImpl, homeDir) {
52
- let score = 0;
53
- const reasons = [];
54
-
55
- let missingBinaries = false;
56
-
57
- for (const key of adapter.envKeys) {
58
- if (env[key]) {
59
- score += 100;
60
- reasons.push(`env:${key}`);
61
- }
62
- }
63
-
64
- if (adapter.binaryNames && adapter.binaryNames.length > 0) {
65
- missingBinaries = true;
66
- for (const binaryName of adapter.binaryNames) {
67
- if (commandExists(binaryName, env, fsImpl)) {
68
- score += 70;
69
- reasons.push(`path:${binaryName}`);
70
- missingBinaries = false;
71
- break;
72
- }
73
- }
74
- }
75
-
76
- for (const configPath of adapter.configPaths) {
77
- for (const candidatePath of candidateConfigPaths(homeDir, env, configPath)) {
78
- if (fsImpl.existsSync(candidatePath)) {
79
- score += 90;
80
- reasons.push(`config:${candidatePath}`);
81
- break;
82
- }
83
- }
84
- }
85
-
86
- return {
87
- id: adapter.id,
88
- label: adapter.label,
89
- score,
90
- reasons,
91
- missingBinaries,
92
- };
93
- }
94
-
95
- function candidateConfigPaths(homeDir, env, configPath) {
96
- const paths = [];
97
- if (env.OPENCODE_CONFIG_DIR && configPath === 'opencode.json') {
98
- paths.push(path.join(env.OPENCODE_CONFIG_DIR, 'opencode.json'));
99
- }
100
- if (configPath === 'GEMINI.md') {
101
- paths.push(
102
- path.join(homeDir, '.gemini', 'GEMINI.md'),
103
- path.join(homeDir, 'GEMINI.md')
104
- );
105
- }
106
- if (configPath === 'gemini-extension.json') {
107
- paths.push(
108
- path.join(homeDir, 'gemini-extension.json'),
109
- path.join(homeDir, '.gemini', 'gemini-extension.json')
110
- );
111
- }
112
- return paths;
113
- }
114
-
115
- function commandExists(commandName, env, fsImpl) {
116
- const paths = String(env.PATH ?? '').split(path.delimiter).filter(Boolean);
117
- const exts = process.platform === 'win32'
118
- ? String(env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
119
- : [''];
120
-
121
- for (const dir of paths) {
122
- for (const ext of exts) {
123
- const candidate = path.join(dir, `${commandName}${ext}`);
124
- try {
125
- fsImpl.accessSync(candidate, fs.constants.X_OK);
126
- return true;
127
- } catch {
128
- try {
129
- fsImpl.accessSync(candidate, fs.constants.F_OK);
130
- return true;
131
- } catch {
132
- continue;
133
- }
134
- }
135
- }
136
- }
137
-
138
- return false;
139
- }
140
-
141
- function buildPromptCandidates(detectedCandidates, env, fsImpl, homeDir) {
142
- const allAdapters = listAdapters();
143
- const detectedIds = new Set(detectedCandidates.map((candidate) => candidate.id));
144
-
145
- return [
146
- ...detectedCandidates,
147
- ...allAdapters
148
- .filter((adapter) => !detectedIds.has(adapter.id))
149
- .map((adapter) => {
150
- const result = scoreAdapter(adapter, env, fsImpl, homeDir);
151
- result.score = 0;
152
- result.reasons = [];
153
- return result;
154
- }),
155
- ];
156
- }
157
-
158
- function renderPrompt(candidates, validationMessage = null) {
159
- const lines = [];
160
-
161
- if (validationMessage) {
162
- lines.push(validationMessage);
163
- lines.push('');
164
- }
165
-
166
- const detectedCount = candidates.filter((candidate) => candidate.score > 0).length;
167
- const recommendedIds = new Set(candidates.filter((candidate) => candidate.score > 0).map((candidate) => candidate.id));
168
-
169
- if (detectedCount > 0) {
170
- lines.push('Recommended from your current environment:');
171
- lines.push('');
172
- }
173
-
174
- for (const [index, candidate] of candidates.entries()) {
175
- let label = candidate.label;
176
- if (candidate.missingBinaries) {
177
- label += ' [Missing CLI]';
178
- }
179
-
180
- const hint = recommendedIds.has(candidate.id) ? 'recommended' : null;
181
- lines.push(
182
- promptOption(
183
- index + 1,
184
- label,
185
- candidate.id,
186
- hint,
187
- describeHarness(candidate.id, candidate.missingBinaries)
188
- )
189
- );
190
- }
191
-
192
- return formatPromptBlock(
193
- 'Install SPARK',
194
- lines,
195
- 'Enter a number or harness name.'
196
- );
197
- }
198
-
199
- function resolvePromptAnswer(answer, candidates) {
200
- const value = normalizeHarness(answer);
201
- if (!value) return null;
202
-
203
- const numeric = Number(value);
204
- if (Number.isInteger(numeric) && numeric >= 1 && numeric <= candidates.length) {
205
- return getAdapterById(candidates[numeric - 1].id);
206
- }
207
-
208
- const exact = candidates.find((candidate) => candidate.id === value || candidate.label.toLowerCase() === value);
209
- if (!exact) return null;
210
-
211
- return getAdapterById(exact.id);
212
- }
213
-
214
- function describeHarness(id, missingBinaries = false) {
215
- const descriptions = {
216
- claude: 'Best for Claude Code sessions and project-local plugin workflows.',
217
- codex: 'For the Codex CLI plugin marketplace flow in terminal sessions.',
218
- vscode: 'One install path for VS Code agent sessions across Claude, Copilot, GPT, and other supported models.',
219
- cursor: 'Native Cursor plugin install for fresh Agent sessions.',
220
- copilot: 'For GitHub Copilot CLI in terminal-based agent sessions.',
221
- opencode: 'Registers the OpenCode plugin in your local config directory.',
222
- gemini: 'Installs the Gemini CLI extension and loads SPARK at startup.',
223
- pi: 'Installs the Pi extension from the SPARK repository.',
224
- antigravity: 'Installs the Antigravity plugin and loads SPARK on new sessions.',
225
- };
226
-
227
- let description = descriptions[id] ?? null;
228
- if (description && missingBinaries) {
229
- description += '\n ⚠️ Requires its official CLI to be installed first.';
230
- }
231
-
232
- return description;
233
- }
234
-
235
- function normalizeHarness(value) {
236
- const normalized = String(value ?? '').trim().toLowerCase();
237
-
238
- if (!normalized) {
239
- return '';
240
- }
241
-
242
- const aliases = new Map([
243
- ['codex cli', 'codex'],
244
- ['codex-cli', 'codex'],
245
- ['codex vscode', 'vscode'],
246
- ['codex vs code', 'vscode'],
247
- ['codex-vs-code', 'vscode'],
248
- ['codex app', 'vscode'],
249
- ['copilot vscode', 'vscode'],
250
- ['copilot vs code', 'vscode'],
251
- ['copilot-vs-code', 'vscode'],
252
- ['vscode agent', 'vscode'],
253
- ['vs code', 'vscode'],
254
- ]);
255
-
256
- return aliases.get(normalized) ?? normalized;
257
- }
@@ -1,7 +0,0 @@
1
- export class InstallerError extends Error {
2
- constructor(message, code = 'INSTALLER_ERROR') {
3
- super(message);
4
- this.name = 'InstallerError';
5
- this.code = code;
6
- }
7
- }