@adityaaria/spark 6.0.5 → 6.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adityaaria/spark",
3
- "version": "6.0.5",
3
+ "version": "6.0.7",
4
4
  "description": "SPARK skills and runtime bootstrap for coding agents",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -27,12 +27,13 @@ export async function runInstall(options, env) {
27
27
  const plan = adapter.planInstall();
28
28
 
29
29
  if (options.dryRun) {
30
+ const installPreview = await adapter.install({ dryRun: true, cwd: process.cwd(), env });
30
31
  printSection('Preview');
31
32
  printLine(labelValue('Mode', 'dry-run'));
32
33
  printLine(labelValue('Harness', `${adapter.label} (${adapter.id})`));
33
34
  printLine(labelValue('Selection', source));
34
35
  printLine(labelValue('Bootstrap', plan.bootstrap));
35
- printPlanDetails(plan, {});
36
+ printPlanDetails(installPreview.plan, installPreview.metadata ?? {});
36
37
  printSummary('Nothing changed', [bullet('No filesystem changes were made.')], 'info');
37
38
  return;
38
39
  }
@@ -82,7 +83,13 @@ function printPlanDetails(plan, metadata) {
82
83
  printLine('');
83
84
  printSection('Commands');
84
85
  for (const [index, command] of plan.commands.entries()) {
85
- printLine(step(index + 1, plan.commands.length, commandText(formatCommand(command))));
86
+ printLine(
87
+ step(
88
+ index + 1,
89
+ plan.commands.length,
90
+ commandText(formatCommand(command, metadata))
91
+ )
92
+ );
86
93
  }
87
94
  }
88
95
 
@@ -109,32 +116,52 @@ function printPlanDetails(plan, metadata) {
109
116
  if (metadata.relativeTargetRoot) {
110
117
  printLine(labelValue('Bundle path', pathText(metadata.relativeTargetRoot)));
111
118
  }
119
+ if (metadata.relativeMarketplaceRoot) {
120
+ printLine(labelValue('Marketplace path', pathText(metadata.relativeMarketplaceRoot)));
121
+ }
112
122
  }
113
123
 
114
- function formatCommand(command) {
115
- return [command.file, ...(command.args ?? [])].join(' ');
124
+ function formatCommand(command, metadata = {}) {
125
+ return [command.file, ...(command.args ?? [])]
126
+ .map((part) => interpolatePlanText(part, metadata))
127
+ .join(' ');
116
128
  }
117
129
 
118
130
  function interpolatePlanText(text, metadata) {
119
- return String(text).replaceAll('{targetRoot}', metadata.targetRoot ?? '').replaceAll(
120
- '{relativeTargetRoot}',
121
- metadata.relativeTargetRoot ?? ''
122
- );
131
+ return String(text)
132
+ .replaceAll('{targetRoot}', metadata.targetRoot ?? '')
133
+ .replaceAll('{relativeTargetRoot}', metadata.relativeTargetRoot ?? '')
134
+ .replaceAll('{marketplaceRoot}', metadata.marketplaceRoot ?? '')
135
+ .replaceAll('{relativeMarketplaceRoot}', metadata.relativeMarketplaceRoot ?? '')
136
+ .replaceAll('{marketplaceName}', metadata.marketplaceName ?? '');
123
137
  }
124
138
 
125
139
  function buildReadyLines(adapter, plan, metadata) {
126
140
  const lines = [];
127
141
 
128
- if (metadata.relativeTargetRoot) {
142
+ if (metadata.relativeTargetRoot && adapter.id === 'vscode') {
143
+ lines.push(bullet(`Local VS Code bundle prepared at ${pathText(metadata.relativeTargetRoot)}.`));
144
+ } else if (metadata.relativeTargetRoot) {
129
145
  lines.push(bullet(`Plugin bundle staged at ${pathText(metadata.relativeTargetRoot)}.`));
130
146
  }
131
147
 
148
+ if (metadata.relativeMarketplaceRoot) {
149
+ lines.push(bullet(`Local marketplace staged at ${pathText(metadata.relativeMarketplaceRoot)}.`));
150
+ }
151
+
132
152
  if (adapter.id === 'codex') {
133
- lines.push(bullet(`In Codex, install or import the local plugin from ${pathText(metadata.relativeTargetRoot ?? '.spark/codex-plugin')}.`));
134
153
  lines.push(bullet('Start a fresh Codex session to confirm using-spark loads before coding.'));
135
154
  return lines;
136
155
  }
137
156
 
157
+ if (adapter.id === 'vscode') {
158
+ if (metadata.relativeSettingsPath) {
159
+ lines.push(bullet(`VS Code plugin registration was written to ${pathText(metadata.relativeSettingsPath)}.`));
160
+ }
161
+ lines.push(bullet('Open a fresh VS Code agent session to confirm using-spark loads before coding.'));
162
+ return lines;
163
+ }
164
+
138
165
  lines.push(bullet('Start a fresh session in the selected harness to confirm using-spark loads before coding.'));
139
166
  return lines;
140
167
  }
package/src/cli/output.js CHANGED
@@ -24,7 +24,7 @@ export function printHelp() {
24
24
  printLine('');
25
25
  printMuted('Without --harness, the installer will ask which AI assistance to target.');
26
26
  printLine('');
27
- printLine(labelValue('Supported', 'Codex, Cursor, Antigravity, Copilot, OpenCode, Gemini, Pi'));
27
+ printLine(labelValue('Supported', 'Claude Code, Codex CLI, VS Code, Cursor, Copilot CLI, OpenCode, Gemini CLI, Pi, Antigravity'));
28
28
  }
29
29
 
30
30
  export function printCommandHeader(title) {
@@ -101,9 +101,15 @@ export function formatPromptPrefix() {
101
101
  return `${styled('◆', 'spark')} ${styled('Select', 'pink', true)} ${styled('›', 'dimGray')} `;
102
102
  }
103
103
 
104
- export function promptOption(index, label, id, hint = null) {
104
+ export function promptOption(index, label, id, hint = null, detail = null) {
105
105
  const base = `${styled(`${index}.`, 'spark', true)} ${styled(label, 'white', true)} ${styled(`(${id})`, 'dimGray')}`;
106
- return hint ? `${base} ${styled(hint, 'warning')}` : base;
106
+ const header = hint ? `${base} ${styled(hint, 'warning')}` : base;
107
+
108
+ if (!detail) {
109
+ return header;
110
+ }
111
+
112
+ return `${header}\n ${styled(detail, 'dimGray')}`;
107
113
  }
108
114
 
109
115
  function styled(text, tone, bold = false) {
@@ -0,0 +1,68 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const CLAUDE_MARKETPLACE_DIR = path.join('.spark', 'claude-marketplace');
5
+ const CLAUDE_PLUGIN_DIR = path.join(CLAUDE_MARKETPLACE_DIR, 'plugins', 'spark');
6
+ const COPY_PATHS = [
7
+ '.claude-plugin',
8
+ 'assets',
9
+ path.join('hooks', 'hooks.json'),
10
+ path.join('hooks', 'run-hook.cmd'),
11
+ path.join('hooks', 'session-start'),
12
+ 'skills',
13
+ ];
14
+
15
+ export function stageClaudePlugin({ cwd = process.cwd(), packageRoot, dryRun = false }) {
16
+ const marketplaceRoot = path.join(cwd, CLAUDE_MARKETPLACE_DIR);
17
+ const targetRoot = path.join(cwd, CLAUDE_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
+ writeMarketplaceManifest(marketplaceRoot);
38
+ }
39
+
40
+ return {
41
+ targetRoot,
42
+ relativeTargetRoot: CLAUDE_PLUGIN_DIR,
43
+ marketplaceRoot,
44
+ relativeMarketplaceRoot: CLAUDE_MARKETPLACE_DIR,
45
+ marketplaceName: 'spark-local',
46
+ };
47
+ }
48
+
49
+ function writeMarketplaceManifest(marketplaceRoot) {
50
+ const manifestPath = path.join(marketplaceRoot, 'marketplace.json');
51
+ const manifest = {
52
+ name: 'spark-local',
53
+ plugins: [
54
+ {
55
+ name: 'spark',
56
+ description: 'SPARK local marketplace for Claude Code',
57
+ version: 'local',
58
+ source: './plugins/spark',
59
+ author: {
60
+ name: 'SPARK',
61
+ },
62
+ },
63
+ ],
64
+ };
65
+
66
+ fs.mkdirSync(marketplaceRoot, { recursive: true });
67
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
68
+ }
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- const CODEX_STAGE_DIR = path.join('.spark', 'codex-plugin');
4
+ const CODEX_MARKETPLACE_DIR = path.join('.spark', 'codex-marketplace');
5
+ const CODEX_PLUGIN_DIR = path.join(CODEX_MARKETPLACE_DIR, 'plugins', 'spark');
5
6
  const COPY_PATHS = [
6
7
  '.codex-plugin',
7
8
  'assets',
@@ -11,28 +12,61 @@ const COPY_PATHS = [
11
12
  'skills',
12
13
  ];
13
14
 
14
- export function stageCodexPlugin({ cwd = process.cwd(), packageRoot }) {
15
- const targetRoot = path.join(cwd, CODEX_STAGE_DIR);
15
+ export function stageCodexPlugin({ cwd = process.cwd(), packageRoot, dryRun = false }) {
16
+ const marketplaceRoot = path.join(cwd, CODEX_MARKETPLACE_DIR);
17
+ const targetRoot = path.join(cwd, CODEX_PLUGIN_DIR);
16
18
 
17
- fs.mkdirSync(targetRoot, { recursive: true });
19
+ if (!dryRun) {
20
+ fs.mkdirSync(targetRoot, { recursive: true });
21
+ }
22
+
23
+ if (!dryRun) {
24
+ for (const relativePath of COPY_PATHS) {
25
+ const sourcePath = path.join(packageRoot, relativePath);
26
+ const targetPath = path.join(targetRoot, relativePath);
27
+ const stat = fs.statSync(sourcePath);
18
28
 
19
- for (const relativePath of COPY_PATHS) {
20
- const sourcePath = path.join(packageRoot, relativePath);
21
- const targetPath = path.join(targetRoot, relativePath);
22
- const stat = fs.statSync(sourcePath);
29
+ if (stat.isDirectory()) {
30
+ fs.cpSync(sourcePath, targetPath, { recursive: true });
31
+ continue;
32
+ }
23
33
 
24
- if (stat.isDirectory()) {
25
- fs.cpSync(sourcePath, targetPath, { recursive: true });
26
- continue;
34
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
35
+ fs.copyFileSync(sourcePath, targetPath);
36
+ fs.chmodSync(targetPath, stat.mode);
27
37
  }
28
38
 
29
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
30
- fs.copyFileSync(sourcePath, targetPath);
31
- fs.chmodSync(targetPath, stat.mode);
39
+ writeMarketplaceManifest(marketplaceRoot);
32
40
  }
33
41
 
34
42
  return {
35
43
  targetRoot,
36
- relativeTargetRoot: CODEX_STAGE_DIR,
44
+ relativeTargetRoot: CODEX_PLUGIN_DIR,
45
+ marketplaceRoot,
46
+ relativeMarketplaceRoot: CODEX_MARKETPLACE_DIR,
47
+ };
48
+ }
49
+
50
+ function writeMarketplaceManifest(marketplaceRoot) {
51
+ const manifestPath = path.join(marketplaceRoot, 'marketplace.json');
52
+ const manifest = {
53
+ name: 'spark-local',
54
+ plugins: [
55
+ {
56
+ name: 'spark',
57
+ source: {
58
+ source: 'local',
59
+ path: './plugins/spark',
60
+ },
61
+ policy: {
62
+ installation: 'AVAILABLE',
63
+ authentication: 'ON_INSTALL',
64
+ },
65
+ category: 'Productivity',
66
+ },
67
+ ],
37
68
  };
69
+
70
+ fs.mkdirSync(marketplaceRoot, { recursive: true });
71
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
38
72
  }
@@ -45,21 +45,22 @@ export function createAdapter({
45
45
  };
46
46
  },
47
47
  async install({ runner = spawnSync, dryRun = false, cwd = process.cwd(), env = process.env, fs = null } = {}) {
48
- if (dryRun) {
49
- return { plan: this.planInstall(), metadata: {} };
50
- }
51
-
52
48
  if (!automated) {
53
49
  throw new InstallerError(`${label} install is not fully automatable yet.`);
54
50
  }
55
51
 
52
+ let metadata = {};
53
+
56
54
  if (typeof customInstall === 'function') {
57
- const metadata = await customInstall({ cwd, env, fs });
58
- return { plan: this.planInstall(), metadata: metadata ?? {} };
55
+ metadata = (await customInstall({ cwd, env, fs, dryRun })) ?? {};
56
+ }
57
+
58
+ if (dryRun) {
59
+ return { plan: this.planInstall(), metadata };
59
60
  }
60
61
 
61
62
  for (const entry of commandList) {
62
- const result = runner(entry.file, entry.args ?? [], {
63
+ const result = runner(entry.file, interpolateArgs(entry.args ?? [], metadata), {
63
64
  cwd: entry.cwd ?? cwd,
64
65
  env: entry.env ?? env,
65
66
  encoding: 'utf8',
@@ -72,7 +73,7 @@ export function createAdapter({
72
73
  }
73
74
  }
74
75
 
75
- return { plan: this.planInstall(), metadata: {} };
76
+ return { plan: this.planInstall(), metadata };
76
77
  },
77
78
  async verify() {
78
79
  return verifyHint;
@@ -91,3 +92,14 @@ function normalizeCommandList(commands, command) {
91
92
 
92
93
  return [];
93
94
  }
95
+
96
+ function interpolateArgs(args, metadata) {
97
+ return args.map((arg) =>
98
+ String(arg)
99
+ .replaceAll('{targetRoot}', metadata.targetRoot ?? '')
100
+ .replaceAll('{relativeTargetRoot}', metadata.relativeTargetRoot ?? '')
101
+ .replaceAll('{marketplaceRoot}', metadata.marketplaceRoot ?? '')
102
+ .replaceAll('{relativeMarketplaceRoot}', metadata.relativeMarketplaceRoot ?? '')
103
+ .replaceAll('{marketplaceName}', metadata.marketplaceName ?? '')
104
+ );
105
+ }
@@ -0,0 +1,42 @@
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,4 +1,9 @@
1
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)), '../../..');
2
7
 
3
8
  export function createOpenCodeAdapter() {
4
9
  return createAdapter({
@@ -10,11 +15,16 @@ export function createOpenCodeAdapter() {
10
15
  configPaths: ['opencode.json'],
11
16
  bootstrap: 'using-spark -> .opencode/plugins/spark.js -> message transform bootstrap',
12
17
  installHint: '.opencode/plugins/spark.js + .opencode/INSTALL.md',
13
- verifyHint: 'Run opencode logs or a fresh OpenCode session and confirm bootstrap injection.',
14
- manualSteps: [
15
- 'Add `spark@git+https://github.com/adityaaria/SPARK.git` to the `plugin` array in `opencode.json`.',
16
- 'Restart OpenCode.',
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.',
17
24
  ],
25
+ customInstall({ env, dryRun }) {
26
+ return installOpenCodePlugin({ packageRoot, env, dryRun });
27
+ },
18
28
  });
19
29
  }
20
30
 
@@ -46,6 +56,11 @@ export function createGeminiAdapter() {
46
56
  bootstrap: 'using-spark -> gemini-extension.json -> GEMINI.md context file',
47
57
  installHint: 'gemini-extension.json + GEMINI.md + references/gemini-tools.md',
48
58
  verifyHint: 'Run a fresh Gemini CLI session and confirm using-spark loads at session start.',
59
+ successMessage: 'Installed SPARK for Gemini CLI.',
60
+ automatedSteps: [
61
+ 'Install the SPARK extension into Gemini CLI.',
62
+ 'Start a fresh Gemini CLI session to confirm using-spark loads at startup.',
63
+ ],
49
64
  command: {
50
65
  file: 'gemini',
51
66
  args: ['extensions', 'install', 'https://github.com/adityaaria/SPARK'],
@@ -63,6 +78,11 @@ export function createAntigravityAdapter() {
63
78
  bootstrap: 'using-spark -> agy plugin install -> contextFileName bootstrap',
64
79
  installHint: 'skills/using-spark/references/antigravity-tools.md + plugin install flow',
65
80
  verifyHint: 'Run a fresh Antigravity session and confirm using-spark loads from the plugin install.',
81
+ successMessage: 'Installed SPARK for Antigravity.',
82
+ automatedSteps: [
83
+ 'Install the SPARK plugin into Antigravity.',
84
+ 'Start a fresh Antigravity session to confirm using-spark loads before coding.',
85
+ ],
66
86
  command: {
67
87
  file: 'agy',
68
88
  args: ['plugin', 'install', 'https://github.com/adityaaria/SPARK'],
@@ -0,0 +1,61 @@
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,5 +1,8 @@
1
1
  import { createAdapter } from './common.js';
2
+ import { stageClaudePlugin } from './claude-staging.js';
2
3
  import { stageCodexPlugin } from './codex-staging.js';
4
+ import { installCursorPlugin } from './cursor-staging.js';
5
+ import { installVsCodePlugin } from './vscode-staging.js';
3
6
  import { dirname, resolve } from 'node:path';
4
7
  import { fileURLToPath } from 'node:url';
5
8
 
@@ -15,29 +18,76 @@ export function createClaudeCodeAdapter() {
15
18
  bootstrap: 'shell hook -> hooks/session-start -> using-spark',
16
19
  installHint: '.claude-plugin/plugin.json + hooks/hooks.json + hooks/session-start',
17
20
  verifyHint: 'Run a fresh Claude Code session and confirm using-spark loads before coding.',
18
- manualSteps: [
19
- '/plugin install spark@claude-plugins-official',
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.',
20
36
  ],
37
+ customInstall({ cwd, dryRun }) {
38
+ return stageClaudePlugin({ cwd, packageRoot, dryRun });
39
+ },
21
40
  });
22
41
  }
23
42
 
24
43
  export function createCodexAdapter() {
25
44
  return createAdapter({
26
45
  id: 'codex',
27
- label: 'Codex',
46
+ label: 'Codex CLI',
28
47
  kind: 'shell-hook',
29
48
  envKeys: ['CLAUDE_PLUGIN_ROOT'],
30
49
  binaryNames: ['codex'],
31
50
  bootstrap: 'shell hook -> hooks/session-start-codex -> using-spark',
32
51
  installHint: '.codex-plugin/plugin.json + hooks/hooks-codex.json + hooks/session-start-codex',
33
- verifyHint: 'Open Codex plugin install, point it at .spark/codex-plugin, then confirm using-spark loads in a fresh session.',
34
- successMessage: 'Staged SPARK for Codex in .spark/codex-plugin.',
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'],
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 Claude-compatible 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.',
35
84
  automatedSteps: [
36
- 'Stage a project-local Codex plugin bundle at .spark/codex-plugin.',
37
- 'Open Codex plugin install and point it at .spark/codex-plugin.',
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.',
38
88
  ],
39
- customInstall({ cwd }) {
40
- return stageCodexPlugin({ cwd, packageRoot });
89
+ customInstall({ cwd, dryRun }) {
90
+ return installVsCodePlugin({ cwd, packageRoot, dryRun });
41
91
  },
42
92
  });
43
93
  }
@@ -51,10 +101,16 @@ export function createCursorAdapter() {
51
101
  binaryNames: ['cursor'],
52
102
  bootstrap: 'shell hook -> hooks/session-start -> using-spark',
53
103
  installHint: '.cursor-plugin/plugin.json + hooks/hooks-cursor.json + hooks/session-start',
54
- verifyHint: 'Run a fresh Cursor Agent session and confirm using-spark loads before coding.',
55
- manualSteps: [
56
- '/add-plugin spark',
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.',
57
110
  ],
111
+ customInstall({ env, dryRun }) {
112
+ return installCursorPlugin({ packageRoot, env, dryRun });
113
+ },
58
114
  });
59
115
  }
60
116
 
@@ -0,0 +1,68 @@
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
+ '.claude-plugin',
8
+ 'assets',
9
+ path.join('hooks', 'hooks.json'),
10
+ path.join('hooks', 'run-hook.cmd'),
11
+ path.join('hooks', 'session-start'),
12
+ '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 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
+ writeVsCodeSettings(settingsPath, targetRoot);
38
+ }
39
+
40
+ return {
41
+ targetRoot,
42
+ relativeTargetRoot: VSCODE_PLUGIN_DIR,
43
+ settingsPath,
44
+ relativeSettingsPath: SETTINGS_PATH,
45
+ };
46
+ }
47
+
48
+ function writeVsCodeSettings(settingsPath, targetRoot) {
49
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
50
+
51
+ let settings = {};
52
+ if (fs.existsSync(settingsPath)) {
53
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
54
+ }
55
+
56
+ const pluginLocations = isPlainObject(settings['chat.pluginLocations'])
57
+ ? { ...settings['chat.pluginLocations'] }
58
+ : {};
59
+
60
+ pluginLocations[targetRoot] = true;
61
+ settings['chat.pluginLocations'] = pluginLocations;
62
+
63
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
64
+ }
65
+
66
+ function isPlainObject(value) {
67
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
68
+ }
@@ -164,7 +164,15 @@ function renderPrompt(candidates, validationMessage = null) {
164
164
 
165
165
  for (const [index, candidate] of candidates.entries()) {
166
166
  const hint = recommendedIds.has(candidate.id) ? 'recommended' : null;
167
- lines.push(promptOption(index + 1, candidate.label, candidate.id, hint));
167
+ lines.push(
168
+ promptOption(
169
+ index + 1,
170
+ candidate.label,
171
+ candidate.id,
172
+ hint,
173
+ describeHarness(candidate.id)
174
+ )
175
+ );
168
176
  }
169
177
 
170
178
  return formatPromptBlock(
@@ -189,6 +197,42 @@ function resolvePromptAnswer(answer, candidates) {
189
197
  return getAdapterById(exact.id);
190
198
  }
191
199
 
200
+ function describeHarness(id) {
201
+ const descriptions = {
202
+ claude: 'Best for Claude Code sessions and project-local plugin workflows.',
203
+ codex: 'For the Codex CLI plugin marketplace flow in terminal sessions.',
204
+ vscode: 'One install path for VS Code agent sessions across Claude, Copilot, GPT, and other supported models.',
205
+ cursor: 'Native Cursor plugin install for fresh Agent sessions.',
206
+ copilot: 'For GitHub Copilot CLI in terminal-based agent sessions.',
207
+ opencode: 'Registers the OpenCode plugin in your local config directory.',
208
+ gemini: 'Installs the Gemini CLI extension and loads SPARK at startup.',
209
+ pi: 'Installs the Pi extension from the SPARK repository.',
210
+ antigravity: 'Installs the Antigravity plugin and loads SPARK on new sessions.',
211
+ };
212
+
213
+ return descriptions[id] ?? null;
214
+ }
215
+
192
216
  function normalizeHarness(value) {
193
- return String(value ?? '').trim().toLowerCase();
217
+ const normalized = String(value ?? '').trim().toLowerCase();
218
+
219
+ if (!normalized) {
220
+ return '';
221
+ }
222
+
223
+ const aliases = new Map([
224
+ ['codex cli', 'codex'],
225
+ ['codex-cli', 'codex'],
226
+ ['codex vscode', 'vscode'],
227
+ ['codex vs code', 'vscode'],
228
+ ['codex-vs-code', 'vscode'],
229
+ ['codex app', 'vscode'],
230
+ ['copilot vscode', 'vscode'],
231
+ ['copilot vs code', 'vscode'],
232
+ ['copilot-vs-code', 'vscode'],
233
+ ['vscode agent', 'vscode'],
234
+ ['vs code', 'vscode'],
235
+ ]);
236
+
237
+ return aliases.get(normalized) ?? normalized;
194
238
  }
@@ -3,6 +3,7 @@ import {
3
3
  createCodexAdapter,
4
4
  createCopilotAdapter,
5
5
  createCursorAdapter,
6
+ createVsCodeAdapter,
6
7
  } from './adapters/shell-hook.js';
7
8
  import {
8
9
  createAntigravityAdapter,
@@ -14,6 +15,7 @@ import {
14
15
  const ADAPTERS = [
15
16
  createClaudeCodeAdapter(),
16
17
  createCodexAdapter(),
18
+ createVsCodeAdapter(),
17
19
  createCursorAdapter(),
18
20
  createCopilotAdapter(),
19
21
  createOpenCodeAdapter(),