@adityaaria/spark 6.0.4 → 6.0.6
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/bin/spark.js +2 -1
- package/package.json +1 -1
- package/src/cli/install.js +122 -12
- package/src/cli/output.js +108 -4
- package/src/cli/prompt.js +2 -1
- package/src/installer/adapters/claude-staging.js +68 -0
- package/src/installer/adapters/codex-staging.js +72 -0
- package/src/installer/adapters/common.js +63 -17
- package/src/installer/adapters/cursor-staging.js +42 -0
- package/src/installer/adapters/extension-style.js +25 -1
- package/src/installer/adapters/opencode-staging.js +61 -0
- package/src/installer/adapters/shell-hook.js +65 -5
- package/src/installer/detect.js +26 -6
package/bin/spark.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { run } from '../src/cli/index.js';
|
|
3
|
+
import { formatError } from '../src/cli/output.js';
|
|
3
4
|
|
|
4
5
|
run(process.argv.slice(2), process.env).catch((error) => {
|
|
5
|
-
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
6
|
+
process.stderr.write(`${formatError(error instanceof Error ? error.message : String(error))}\n`);
|
|
6
7
|
process.exitCode = 1;
|
|
7
8
|
});
|
package/package.json
CHANGED
package/src/cli/install.js
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
bullet,
|
|
3
|
+
commandText,
|
|
4
|
+
labelValue,
|
|
5
|
+
pathText,
|
|
6
|
+
printCommandHeader,
|
|
7
|
+
printLine,
|
|
8
|
+
printSection,
|
|
9
|
+
printSummary,
|
|
10
|
+
statusText,
|
|
11
|
+
step,
|
|
12
|
+
} from './output.js';
|
|
2
13
|
import { askQuestion } from './prompt.js';
|
|
3
14
|
import { chooseHarness } from '../installer/detect.js';
|
|
4
15
|
import { InstallerError } from '../installer/errors.js';
|
|
5
16
|
import fs from 'node:fs';
|
|
6
17
|
|
|
7
18
|
export async function runInstall(options, env) {
|
|
19
|
+
printCommandHeader('Install');
|
|
8
20
|
const selection = await chooseHarness({
|
|
9
21
|
forcedHarness: normalizeHarness(options.harness),
|
|
10
22
|
env,
|
|
@@ -15,11 +27,14 @@ export async function runInstall(options, env) {
|
|
|
15
27
|
const plan = adapter.planInstall();
|
|
16
28
|
|
|
17
29
|
if (options.dryRun) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
printLine(
|
|
21
|
-
printLine(
|
|
22
|
-
printLine('
|
|
30
|
+
const installPreview = await adapter.install({ dryRun: true, cwd: process.cwd(), env });
|
|
31
|
+
printSection('Preview');
|
|
32
|
+
printLine(labelValue('Mode', 'dry-run'));
|
|
33
|
+
printLine(labelValue('Harness', `${adapter.label} (${adapter.id})`));
|
|
34
|
+
printLine(labelValue('Selection', source));
|
|
35
|
+
printLine(labelValue('Bootstrap', plan.bootstrap));
|
|
36
|
+
printPlanDetails(installPreview.plan, installPreview.metadata ?? {});
|
|
37
|
+
printSummary('Nothing changed', [bullet('No filesystem changes were made.')], 'info');
|
|
23
38
|
return;
|
|
24
39
|
}
|
|
25
40
|
|
|
@@ -27,21 +42,116 @@ export async function runInstall(options, env) {
|
|
|
27
42
|
throw new InstallerError(`Install flow for ${adapter.label} is not implemented yet.`);
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
if (!plan.
|
|
31
|
-
|
|
32
|
-
printLine(
|
|
33
|
-
printLine(`
|
|
45
|
+
if (!plan.automated) {
|
|
46
|
+
printSection('Install');
|
|
47
|
+
printLine(labelValue('Harness', adapter.label));
|
|
48
|
+
printLine(statusText(`Interactive install is required for ${adapter.label}.`, 'warning'));
|
|
49
|
+
printPlanDetails(plan, {});
|
|
50
|
+
printSummary(
|
|
51
|
+
'Next step',
|
|
52
|
+
[bullet(`Complete the steps above inside ${adapter.label} and then start a fresh session.`)],
|
|
53
|
+
'warning'
|
|
54
|
+
);
|
|
34
55
|
return;
|
|
35
56
|
}
|
|
36
57
|
|
|
37
|
-
await adapter.install({ options, env });
|
|
58
|
+
const installResult = await adapter.install({ options, env });
|
|
38
59
|
if (typeof adapter.verify === 'function') {
|
|
39
60
|
await adapter.verify({ options, env });
|
|
40
61
|
}
|
|
41
|
-
|
|
62
|
+
const resolvedPlan = installResult?.plan ?? plan;
|
|
63
|
+
const metadata = installResult?.metadata ?? {};
|
|
64
|
+
|
|
65
|
+
printSection('Install');
|
|
66
|
+
printLine(labelValue('Harness', adapter.label));
|
|
67
|
+
printLine(statusText(resolvedPlan.successMessage ?? plan.successMessage, 'success'));
|
|
68
|
+
printPlanDetails(resolvedPlan, metadata);
|
|
69
|
+
printSummary(
|
|
70
|
+
'Ready',
|
|
71
|
+
buildReadyLines(adapter, resolvedPlan, metadata),
|
|
72
|
+
'success'
|
|
73
|
+
);
|
|
42
74
|
}
|
|
43
75
|
|
|
44
76
|
function normalizeHarness(harness) {
|
|
45
77
|
if (!harness) return null;
|
|
46
78
|
return String(harness).trim().toLowerCase();
|
|
47
79
|
}
|
|
80
|
+
|
|
81
|
+
function printPlanDetails(plan, metadata) {
|
|
82
|
+
if (plan.automated && plan.commands?.length) {
|
|
83
|
+
printLine('');
|
|
84
|
+
printSection('Commands');
|
|
85
|
+
for (const [index, command] of plan.commands.entries()) {
|
|
86
|
+
printLine(
|
|
87
|
+
step(
|
|
88
|
+
index + 1,
|
|
89
|
+
plan.commands.length,
|
|
90
|
+
commandText(formatCommand(command, metadata))
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (plan.automated && plan.automatedSteps?.length) {
|
|
97
|
+
printLine('');
|
|
98
|
+
printSection('Steps');
|
|
99
|
+
for (const [index, automatedStep] of plan.automatedSteps.entries()) {
|
|
100
|
+
printLine(step(index + 1, plan.automatedSteps.length, interpolatePlanText(automatedStep, metadata)));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!plan.automated && plan.manualSteps?.length) {
|
|
105
|
+
printLine('');
|
|
106
|
+
printSection('Steps');
|
|
107
|
+
for (const [index, manualStep] of plan.manualSteps.entries()) {
|
|
108
|
+
printLine(step(index + 1, plan.manualSteps.length, manualStep));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
printLine('');
|
|
113
|
+
printSection('Notes');
|
|
114
|
+
printLine(labelValue('Install hint', plan.installHint));
|
|
115
|
+
printLine(labelValue('Verify hint', plan.verifyHint));
|
|
116
|
+
if (metadata.relativeTargetRoot) {
|
|
117
|
+
printLine(labelValue('Bundle path', pathText(metadata.relativeTargetRoot)));
|
|
118
|
+
}
|
|
119
|
+
if (metadata.relativeMarketplaceRoot) {
|
|
120
|
+
printLine(labelValue('Marketplace path', pathText(metadata.relativeMarketplaceRoot)));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function formatCommand(command, metadata = {}) {
|
|
125
|
+
return [command.file, ...(command.args ?? [])]
|
|
126
|
+
.map((part) => interpolatePlanText(part, metadata))
|
|
127
|
+
.join(' ');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function interpolatePlanText(text, metadata) {
|
|
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 ?? '');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildReadyLines(adapter, plan, metadata) {
|
|
140
|
+
const lines = [];
|
|
141
|
+
|
|
142
|
+
if (metadata.relativeTargetRoot) {
|
|
143
|
+
lines.push(bullet(`Plugin bundle staged at ${pathText(metadata.relativeTargetRoot)}.`));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (metadata.relativeMarketplaceRoot) {
|
|
147
|
+
lines.push(bullet(`Local Codex marketplace staged at ${pathText(metadata.relativeMarketplaceRoot)}.`));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (adapter.id === 'codex') {
|
|
151
|
+
lines.push(bullet('Start a fresh Codex session to confirm using-spark loads before coding.'));
|
|
152
|
+
return lines;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
lines.push(bullet('Start a fresh session in the selected harness to confirm using-spark loads before coding.'));
|
|
156
|
+
return lines;
|
|
157
|
+
}
|
package/src/cli/output.js
CHANGED
|
@@ -1,13 +1,117 @@
|
|
|
1
|
+
const COLORS = {
|
|
2
|
+
reset: '\x1b[0m',
|
|
3
|
+
bold: '\x1b[1m',
|
|
4
|
+
dim: '\x1b[2m',
|
|
5
|
+
white: '\x1b[38;2;245;245;245m',
|
|
6
|
+
spark: '\x1b[38;2;255;107;53m',
|
|
7
|
+
accent: '\x1b[38;2;0;245;255m',
|
|
8
|
+
success: '\x1b[38;2;46;204;113m',
|
|
9
|
+
warning: '\x1b[38;2;255;215;0m',
|
|
10
|
+
error: '\x1b[38;2;255;71;87m',
|
|
11
|
+
pink: '\x1b[38;2;255;27;107m',
|
|
12
|
+
dimGray: '\x1b[38;2;107;114;128m',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const useColor = process.stdout.isTTY && process.env.NO_COLOR !== '1';
|
|
16
|
+
|
|
1
17
|
export function printLine(text = '') {
|
|
2
18
|
process.stdout.write(`${text}\n`);
|
|
3
19
|
}
|
|
4
20
|
|
|
5
21
|
export function printHelp() {
|
|
6
|
-
|
|
22
|
+
printCommandHeader('Install');
|
|
23
|
+
printLine(labelValue('Usage', 'npx @adityaaria/spark install [--harness <name>] [--dry-run] [--yes] [--verbose]'));
|
|
24
|
+
printLine('');
|
|
25
|
+
printMuted('Without --harness, the installer will ask which AI assistance to target.');
|
|
7
26
|
printLine('');
|
|
8
|
-
printLine('
|
|
27
|
+
printLine(labelValue('Supported', 'Codex, Cursor, Antigravity, Copilot, OpenCode, Gemini, Pi'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function printCommandHeader(title) {
|
|
31
|
+
printLine(styled('⚡', 'warning') + ' ' + styled('SPARK', 'spark', true) + ' ' + styled('›', 'dimGray') + ' ' + styled(title, 'white', true));
|
|
32
|
+
printLine(styled('Skills installer for coding agents', 'dimGray'));
|
|
9
33
|
printLine('');
|
|
10
|
-
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function printSection(title) {
|
|
37
|
+
printLine(styled(title, 'accent', true));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function printSummary(title, lines = [], tone = 'info') {
|
|
41
|
+
const toneLabel = {
|
|
42
|
+
info: styled('ℹ', 'accent'),
|
|
43
|
+
success: styled('✓', 'success'),
|
|
44
|
+
warning: styled('•', 'warning'),
|
|
45
|
+
error: styled('✕', 'error'),
|
|
46
|
+
}[tone];
|
|
47
|
+
|
|
11
48
|
printLine('');
|
|
12
|
-
printLine('
|
|
49
|
+
printLine(`${toneLabel} ${styled(title, tone === 'error' ? 'error' : tone === 'success' ? 'success' : tone === 'warning' ? 'warning' : 'accent', true)}`);
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
printLine(` ${line}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function labelValue(label, value) {
|
|
56
|
+
return `${styled(`${label}:`, 'dimGray')}${value ? ` ${styled(value, 'white')}` : ''}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function bullet(text, tone = 'dimGray') {
|
|
60
|
+
return `${styled('•', tone)} ${styled(text, 'white')}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function step(index, total, text) {
|
|
64
|
+
return `${styled(`[${index}/${total}]`, 'spark', true)} ${styled(text, 'white')}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function commandText(text) {
|
|
68
|
+
return styled(text, 'warning');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function pathText(text) {
|
|
72
|
+
return styled(text, 'accent');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function statusText(text, tone = 'white') {
|
|
76
|
+
return styled(text, tone);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function printMuted(text) {
|
|
80
|
+
printLine(styled(text, 'dimGray'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function formatError(message) {
|
|
84
|
+
return `${styled('✕', 'error')} ${styled(message, 'error', true)}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatPromptBlock(title, lines = [], footer = null) {
|
|
88
|
+
const output = [];
|
|
89
|
+
output.push(styled(title, 'accent', true));
|
|
90
|
+
output.push(styled('Choose the AI assistance you want SPARK to prepare.', 'dimGray'));
|
|
91
|
+
output.push('');
|
|
92
|
+
output.push(...lines);
|
|
93
|
+
if (footer) {
|
|
94
|
+
output.push('');
|
|
95
|
+
output.push(styled(footer, 'dimGray'));
|
|
96
|
+
}
|
|
97
|
+
return output.join('\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function formatPromptPrefix() {
|
|
101
|
+
return `${styled('◆', 'spark')} ${styled('Select', 'pink', true)} ${styled('›', 'dimGray')} `;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function promptOption(index, label, id, hint = null) {
|
|
105
|
+
const base = `${styled(`${index}.`, 'spark', true)} ${styled(label, 'white', true)} ${styled(`(${id})`, 'dimGray')}`;
|
|
106
|
+
return hint ? `${base} ${styled(hint, 'warning')}` : base;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function styled(text, tone, bold = false) {
|
|
110
|
+
if (!useColor) {
|
|
111
|
+
return text;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const color = COLORS[tone] ?? '';
|
|
115
|
+
const weight = bold ? COLORS.bold : '';
|
|
116
|
+
return `${weight}${color}${text}${COLORS.reset}`;
|
|
13
117
|
}
|
package/src/cli/prompt.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import readline from 'node:readline/promises';
|
|
2
|
+
import { formatPromptPrefix } from './output.js';
|
|
2
3
|
|
|
3
4
|
export async function askQuestion(question, { input = process.stdin, output = process.stdout } = {}) {
|
|
4
5
|
const rl = readline.createInterface({ input, output });
|
|
5
6
|
try {
|
|
6
|
-
return await rl.question(`${question}\n
|
|
7
|
+
return await rl.question(`${question}\n${formatPromptPrefix()}`);
|
|
7
8
|
} finally {
|
|
8
9
|
rl.close();
|
|
9
10
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const CODEX_MARKETPLACE_DIR = path.join('.spark', 'codex-marketplace');
|
|
5
|
+
const CODEX_PLUGIN_DIR = path.join(CODEX_MARKETPLACE_DIR, 'plugins', 'spark');
|
|
6
|
+
const COPY_PATHS = [
|
|
7
|
+
'.codex-plugin',
|
|
8
|
+
'assets',
|
|
9
|
+
path.join('hooks', 'hooks-codex.json'),
|
|
10
|
+
path.join('hooks', 'run-hook.cmd'),
|
|
11
|
+
path.join('hooks', 'session-start-codex'),
|
|
12
|
+
'skills',
|
|
13
|
+
];
|
|
14
|
+
|
|
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);
|
|
18
|
+
|
|
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);
|
|
28
|
+
|
|
29
|
+
if (stat.isDirectory()) {
|
|
30
|
+
fs.cpSync(sourcePath, targetPath, { recursive: true });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
35
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
36
|
+
fs.chmodSync(targetPath, stat.mode);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
writeMarketplaceManifest(marketplaceRoot);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
targetRoot,
|
|
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
|
+
],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
fs.mkdirSync(marketplaceRoot, { recursive: true });
|
|
71
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
72
|
+
}
|
|
@@ -8,52 +8,98 @@ export function createAdapter({
|
|
|
8
8
|
bootstrap,
|
|
9
9
|
installHint,
|
|
10
10
|
verifyHint,
|
|
11
|
+
successMessage = `Installed SPARK for ${label}.`,
|
|
11
12
|
command = null,
|
|
13
|
+
commands = [],
|
|
14
|
+
customInstall = null,
|
|
15
|
+
manualSteps = [],
|
|
16
|
+
automatedSteps = [],
|
|
12
17
|
envKeys = [],
|
|
13
18
|
binaryNames = [],
|
|
14
19
|
configPaths = [],
|
|
15
20
|
}) {
|
|
21
|
+
const commandList = normalizeCommandList(commands, command);
|
|
22
|
+
const automated = commandList.length > 0 || typeof customInstall === 'function';
|
|
23
|
+
|
|
16
24
|
return {
|
|
17
25
|
id,
|
|
18
26
|
label,
|
|
19
27
|
envKeys,
|
|
20
28
|
binaryNames,
|
|
21
29
|
configPaths,
|
|
22
|
-
command,
|
|
30
|
+
command: commandList[0] ?? null,
|
|
31
|
+
commands: commandList,
|
|
32
|
+
manualSteps,
|
|
23
33
|
planInstall() {
|
|
24
34
|
return {
|
|
25
35
|
kind,
|
|
26
36
|
bootstrap,
|
|
27
37
|
installHint,
|
|
28
38
|
verifyHint,
|
|
29
|
-
|
|
39
|
+
successMessage,
|
|
40
|
+
command: commandList[0] ?? null,
|
|
41
|
+
commands: commandList,
|
|
42
|
+
manualSteps,
|
|
43
|
+
automatedSteps,
|
|
44
|
+
automated,
|
|
30
45
|
};
|
|
31
46
|
},
|
|
32
|
-
async install({ runner = spawnSync, dryRun = false } = {}) {
|
|
33
|
-
if (
|
|
34
|
-
|
|
47
|
+
async install({ runner = spawnSync, dryRun = false, cwd = process.cwd(), env = process.env, fs = null } = {}) {
|
|
48
|
+
if (!automated) {
|
|
49
|
+
throw new InstallerError(`${label} install is not fully automatable yet.`);
|
|
35
50
|
}
|
|
36
51
|
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
let metadata = {};
|
|
53
|
+
|
|
54
|
+
if (typeof customInstall === 'function') {
|
|
55
|
+
metadata = (await customInstall({ cwd, env, fs, dryRun })) ?? {};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (dryRun) {
|
|
59
|
+
return { plan: this.planInstall(), metadata };
|
|
39
60
|
}
|
|
40
61
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
for (const entry of commandList) {
|
|
63
|
+
const result = runner(entry.file, interpolateArgs(entry.args ?? [], metadata), {
|
|
64
|
+
cwd: entry.cwd ?? cwd,
|
|
65
|
+
env: entry.env ?? env,
|
|
66
|
+
encoding: 'utf8',
|
|
67
|
+
});
|
|
46
68
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
if (result.status !== 0) {
|
|
70
|
+
throw new InstallerError(
|
|
71
|
+
`${label} install failed: ${result.stderr || result.stdout || 'unknown error'}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
51
74
|
}
|
|
52
75
|
|
|
53
|
-
return this.planInstall();
|
|
76
|
+
return { plan: this.planInstall(), metadata };
|
|
54
77
|
},
|
|
55
78
|
async verify() {
|
|
56
79
|
return verifyHint;
|
|
57
80
|
},
|
|
58
81
|
};
|
|
59
82
|
}
|
|
83
|
+
|
|
84
|
+
function normalizeCommandList(commands, command) {
|
|
85
|
+
if (Array.isArray(commands) && commands.length > 0) {
|
|
86
|
+
return commands;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (command) {
|
|
90
|
+
return [command];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return [];
|
|
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,7 +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: '
|
|
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
|
+
},
|
|
14
28
|
});
|
|
15
29
|
}
|
|
16
30
|
|
|
@@ -42,6 +56,11 @@ export function createGeminiAdapter() {
|
|
|
42
56
|
bootstrap: 'using-spark -> gemini-extension.json -> GEMINI.md context file',
|
|
43
57
|
installHint: 'gemini-extension.json + GEMINI.md + references/gemini-tools.md',
|
|
44
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
|
+
],
|
|
45
64
|
command: {
|
|
46
65
|
file: 'gemini',
|
|
47
66
|
args: ['extensions', 'install', 'https://github.com/adityaaria/SPARK'],
|
|
@@ -59,6 +78,11 @@ export function createAntigravityAdapter() {
|
|
|
59
78
|
bootstrap: 'using-spark -> agy plugin install -> contextFileName bootstrap',
|
|
60
79
|
installHint: 'skills/using-spark/references/antigravity-tools.md + plugin install flow',
|
|
61
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
|
+
],
|
|
62
86
|
command: {
|
|
63
87
|
file: 'agy',
|
|
64
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,4 +1,11 @@
|
|
|
1
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 { dirname, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
|
|
2
9
|
|
|
3
10
|
export function createClaudeCodeAdapter() {
|
|
4
11
|
return createAdapter({
|
|
@@ -10,6 +17,25 @@ export function createClaudeCodeAdapter() {
|
|
|
10
17
|
bootstrap: 'shell hook -> hooks/session-start -> using-spark',
|
|
11
18
|
installHint: '.claude-plugin/plugin.json + hooks/hooks.json + hooks/session-start',
|
|
12
19
|
verifyHint: 'Run a fresh Claude Code session and confirm using-spark loads before coding.',
|
|
20
|
+
successMessage: 'Installed SPARK for Claude Code.',
|
|
21
|
+
commands: [
|
|
22
|
+
{
|
|
23
|
+
file: 'claude',
|
|
24
|
+
args: ['plugin', 'marketplace', 'add', '{relativeMarketplaceRoot}'],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
file: 'claude',
|
|
28
|
+
args: ['plugin', 'install', 'spark@{marketplaceName}'],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
automatedSteps: [
|
|
32
|
+
'Stage a local Claude Code marketplace at .spark/claude-marketplace.',
|
|
33
|
+
'Register the local marketplace with Claude Code.',
|
|
34
|
+
'Install the spark plugin from that marketplace.',
|
|
35
|
+
],
|
|
36
|
+
customInstall({ cwd, dryRun }) {
|
|
37
|
+
return stageClaudePlugin({ cwd, packageRoot, dryRun });
|
|
38
|
+
},
|
|
13
39
|
});
|
|
14
40
|
}
|
|
15
41
|
|
|
@@ -23,6 +49,25 @@ export function createCodexAdapter() {
|
|
|
23
49
|
bootstrap: 'shell hook -> hooks/session-start-codex -> using-spark',
|
|
24
50
|
installHint: '.codex-plugin/plugin.json + hooks/hooks-codex.json + hooks/session-start-codex',
|
|
25
51
|
verifyHint: 'Run a fresh Codex session and confirm using-spark loads before coding.',
|
|
52
|
+
successMessage: 'Installed SPARK for Codex.',
|
|
53
|
+
commands: [
|
|
54
|
+
{
|
|
55
|
+
file: 'codex',
|
|
56
|
+
args: ['plugin', 'marketplace', 'add', '{relativeMarketplaceRoot}'],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
file: 'codex',
|
|
60
|
+
args: ['plugin', 'add', 'spark'],
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
automatedSteps: [
|
|
64
|
+
'Stage a local Codex marketplace at .spark/codex-marketplace.',
|
|
65
|
+
'Register the local marketplace with Codex.',
|
|
66
|
+
'Install the spark plugin from that marketplace.',
|
|
67
|
+
],
|
|
68
|
+
customInstall({ cwd, dryRun }) {
|
|
69
|
+
return stageCodexPlugin({ cwd, packageRoot, dryRun });
|
|
70
|
+
},
|
|
26
71
|
});
|
|
27
72
|
}
|
|
28
73
|
|
|
@@ -35,7 +80,16 @@ export function createCursorAdapter() {
|
|
|
35
80
|
binaryNames: ['cursor'],
|
|
36
81
|
bootstrap: 'shell hook -> hooks/session-start -> using-spark',
|
|
37
82
|
installHint: '.cursor-plugin/plugin.json + hooks/hooks-cursor.json + hooks/session-start',
|
|
38
|
-
verifyHint: '
|
|
83
|
+
verifyHint: 'Restart Cursor fully, then confirm using-spark loads in a fresh Agent session.',
|
|
84
|
+
successMessage: 'Installed SPARK for Cursor.',
|
|
85
|
+
automatedSteps: [
|
|
86
|
+
'Copy the SPARK plugin into ~/.cursor/plugins/spark.',
|
|
87
|
+
'Restart Cursor fully to load the new plugin.',
|
|
88
|
+
'Start a fresh Cursor Agent session and confirm using-spark loads before coding.',
|
|
89
|
+
],
|
|
90
|
+
customInstall({ env, dryRun }) {
|
|
91
|
+
return installCursorPlugin({ packageRoot, env, dryRun });
|
|
92
|
+
},
|
|
39
93
|
});
|
|
40
94
|
}
|
|
41
95
|
|
|
@@ -49,9 +103,15 @@ export function createCopilotAdapter() {
|
|
|
49
103
|
bootstrap: 'shell hook -> hooks/session-start -> using-spark',
|
|
50
104
|
installHint: 'skills/using-spark/references/copilot-tools.md + hooks/session-start',
|
|
51
105
|
verifyHint: 'Run a fresh Copilot CLI session and confirm using-spark loads before coding.',
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
106
|
+
commands: [
|
|
107
|
+
{
|
|
108
|
+
file: 'copilot',
|
|
109
|
+
args: ['plugin', 'marketplace', 'add', 'adityaaria/SPARK-marketplace'],
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
file: 'copilot',
|
|
113
|
+
args: ['plugin', 'install', 'spark@spark-marketplace'],
|
|
114
|
+
},
|
|
115
|
+
],
|
|
56
116
|
});
|
|
57
117
|
}
|
package/src/installer/detect.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
import { formatPromptBlock, promptOption } from '../cli/output.js';
|
|
4
5
|
import { getAdapterById, listAdapters } from './registry.js';
|
|
5
6
|
import { InstallerError } from './errors.js';
|
|
6
7
|
|
|
@@ -146,12 +147,31 @@ function buildPromptCandidates(detectedCandidates) {
|
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
function renderPrompt(candidates, validationMessage = null) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
'
|
|
154
|
-
|
|
150
|
+
const lines = [];
|
|
151
|
+
|
|
152
|
+
if (validationMessage) {
|
|
153
|
+
lines.push(validationMessage);
|
|
154
|
+
lines.push('');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const detectedCount = candidates.filter((candidate) => candidate.score > 0).length;
|
|
158
|
+
const recommendedIds = new Set(candidates.filter((candidate) => candidate.score > 0).map((candidate) => candidate.id));
|
|
159
|
+
|
|
160
|
+
if (detectedCount > 0) {
|
|
161
|
+
lines.push('Recommended from your current environment:');
|
|
162
|
+
lines.push('');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const [index, candidate] of candidates.entries()) {
|
|
166
|
+
const hint = recommendedIds.has(candidate.id) ? 'recommended' : null;
|
|
167
|
+
lines.push(promptOption(index + 1, candidate.label, candidate.id, hint));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return formatPromptBlock(
|
|
171
|
+
'Install SPARK',
|
|
172
|
+
lines,
|
|
173
|
+
'Enter a number or harness name.'
|
|
174
|
+
);
|
|
155
175
|
}
|
|
156
176
|
|
|
157
177
|
function resolvePromptAnswer(answer, candidates) {
|