@adityaaria/spark 6.0.3 → 6.0.5
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 +1 -1
- package/bin/spark.js +2 -1
- package/docs/porting-to-a-new-harness.md +1 -2
- package/package.json +1 -1
- package/src/cli/install.js +105 -12
- package/src/cli/output.js +109 -3
- package/src/cli/prompt.js +2 -1
- package/src/installer/adapters/codex-staging.js +38 -0
- package/src/installer/adapters/common.js +49 -15
- package/src/installer/adapters/extension-style.js +4 -0
- package/src/installer/adapters/shell-hook.js +30 -5
- package/src/installer/detect.js +64 -38
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Installation differs by harness. If you use more than one, install SPARK separat
|
|
|
25
25
|
|
|
26
26
|
### NPM Meta-Installer
|
|
27
27
|
|
|
28
|
-
If you want one command that
|
|
28
|
+
If you want one command that asks which harness you are using, run:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
npx @adityaaria/spark install
|
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
|
});
|
|
@@ -5,8 +5,7 @@ agent runner that isn't Claude Code — so that SPARK skills auto-trigger
|
|
|
5
5
|
there the same way they do natively.
|
|
6
6
|
|
|
7
7
|
For end users, the preferred front door is now `npx @adityaaria/spark install`; the
|
|
8
|
-
CLI
|
|
9
|
-
appropriate adapter.
|
|
8
|
+
CLI asks which harness to target, then delegates to the appropriate adapter.
|
|
10
9
|
|
|
11
10
|
It is written in two layers. **Part 1–3** explain how the system works and how
|
|
12
11
|
to tell whether a harness can be supported at all; read these before you touch
|
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,13 @@ export async function runInstall(options, env) {
|
|
|
15
27
|
const plan = adapter.planInstall();
|
|
16
28
|
|
|
17
29
|
if (options.dryRun) {
|
|
18
|
-
|
|
19
|
-
printLine(
|
|
20
|
-
printLine(
|
|
21
|
-
printLine(
|
|
22
|
-
printLine('
|
|
30
|
+
printSection('Preview');
|
|
31
|
+
printLine(labelValue('Mode', 'dry-run'));
|
|
32
|
+
printLine(labelValue('Harness', `${adapter.label} (${adapter.id})`));
|
|
33
|
+
printLine(labelValue('Selection', source));
|
|
34
|
+
printLine(labelValue('Bootstrap', plan.bootstrap));
|
|
35
|
+
printPlanDetails(plan, {});
|
|
36
|
+
printSummary('Nothing changed', [bullet('No filesystem changes were made.')], 'info');
|
|
23
37
|
return;
|
|
24
38
|
}
|
|
25
39
|
|
|
@@ -27,21 +41,100 @@ export async function runInstall(options, env) {
|
|
|
27
41
|
throw new InstallerError(`Install flow for ${adapter.label} is not implemented yet.`);
|
|
28
42
|
}
|
|
29
43
|
|
|
30
|
-
if (!plan.
|
|
31
|
-
|
|
32
|
-
printLine(
|
|
33
|
-
printLine(`
|
|
44
|
+
if (!plan.automated) {
|
|
45
|
+
printSection('Install');
|
|
46
|
+
printLine(labelValue('Harness', adapter.label));
|
|
47
|
+
printLine(statusText(`Interactive install is required for ${adapter.label}.`, 'warning'));
|
|
48
|
+
printPlanDetails(plan, {});
|
|
49
|
+
printSummary(
|
|
50
|
+
'Next step',
|
|
51
|
+
[bullet(`Complete the steps above inside ${adapter.label} and then start a fresh session.`)],
|
|
52
|
+
'warning'
|
|
53
|
+
);
|
|
34
54
|
return;
|
|
35
55
|
}
|
|
36
56
|
|
|
37
|
-
await adapter.install({ options, env });
|
|
57
|
+
const installResult = await adapter.install({ options, env });
|
|
38
58
|
if (typeof adapter.verify === 'function') {
|
|
39
59
|
await adapter.verify({ options, env });
|
|
40
60
|
}
|
|
41
|
-
|
|
61
|
+
const resolvedPlan = installResult?.plan ?? plan;
|
|
62
|
+
const metadata = installResult?.metadata ?? {};
|
|
63
|
+
|
|
64
|
+
printSection('Install');
|
|
65
|
+
printLine(labelValue('Harness', adapter.label));
|
|
66
|
+
printLine(statusText(resolvedPlan.successMessage ?? plan.successMessage, 'success'));
|
|
67
|
+
printPlanDetails(resolvedPlan, metadata);
|
|
68
|
+
printSummary(
|
|
69
|
+
'Ready',
|
|
70
|
+
buildReadyLines(adapter, resolvedPlan, metadata),
|
|
71
|
+
'success'
|
|
72
|
+
);
|
|
42
73
|
}
|
|
43
74
|
|
|
44
75
|
function normalizeHarness(harness) {
|
|
45
76
|
if (!harness) return null;
|
|
46
77
|
return String(harness).trim().toLowerCase();
|
|
47
78
|
}
|
|
79
|
+
|
|
80
|
+
function printPlanDetails(plan, metadata) {
|
|
81
|
+
if (plan.automated && plan.commands?.length) {
|
|
82
|
+
printLine('');
|
|
83
|
+
printSection('Commands');
|
|
84
|
+
for (const [index, command] of plan.commands.entries()) {
|
|
85
|
+
printLine(step(index + 1, plan.commands.length, commandText(formatCommand(command))));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (plan.automated && plan.automatedSteps?.length) {
|
|
90
|
+
printLine('');
|
|
91
|
+
printSection('Steps');
|
|
92
|
+
for (const [index, automatedStep] of plan.automatedSteps.entries()) {
|
|
93
|
+
printLine(step(index + 1, plan.automatedSteps.length, interpolatePlanText(automatedStep, metadata)));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!plan.automated && plan.manualSteps?.length) {
|
|
98
|
+
printLine('');
|
|
99
|
+
printSection('Steps');
|
|
100
|
+
for (const [index, manualStep] of plan.manualSteps.entries()) {
|
|
101
|
+
printLine(step(index + 1, plan.manualSteps.length, manualStep));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
printLine('');
|
|
106
|
+
printSection('Notes');
|
|
107
|
+
printLine(labelValue('Install hint', plan.installHint));
|
|
108
|
+
printLine(labelValue('Verify hint', plan.verifyHint));
|
|
109
|
+
if (metadata.relativeTargetRoot) {
|
|
110
|
+
printLine(labelValue('Bundle path', pathText(metadata.relativeTargetRoot)));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatCommand(command) {
|
|
115
|
+
return [command.file, ...(command.args ?? [])].join(' ');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function interpolatePlanText(text, metadata) {
|
|
119
|
+
return String(text).replaceAll('{targetRoot}', metadata.targetRoot ?? '').replaceAll(
|
|
120
|
+
'{relativeTargetRoot}',
|
|
121
|
+
metadata.relativeTargetRoot ?? ''
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildReadyLines(adapter, plan, metadata) {
|
|
126
|
+
const lines = [];
|
|
127
|
+
|
|
128
|
+
if (metadata.relativeTargetRoot) {
|
|
129
|
+
lines.push(bullet(`Plugin bundle staged at ${pathText(metadata.relativeTargetRoot)}.`));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (adapter.id === 'codex') {
|
|
133
|
+
lines.push(bullet(`In Codex, install or import the local plugin from ${pathText(metadata.relativeTargetRoot ?? '.spark/codex-plugin')}.`));
|
|
134
|
+
lines.push(bullet('Start a fresh Codex session to confirm using-spark loads before coding.'));
|
|
135
|
+
return lines;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
lines.push(bullet('Start a fresh session in the selected harness to confirm using-spark loads before coding.'));
|
|
139
|
+
return lines;
|
|
140
|
+
}
|
package/src/cli/output.js
CHANGED
|
@@ -1,11 +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'));
|
|
33
|
+
printLine('');
|
|
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
|
+
|
|
9
48
|
printLine('');
|
|
10
|
-
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}`;
|
|
11
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,38 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const CODEX_STAGE_DIR = path.join('.spark', 'codex-plugin');
|
|
5
|
+
const COPY_PATHS = [
|
|
6
|
+
'.codex-plugin',
|
|
7
|
+
'assets',
|
|
8
|
+
path.join('hooks', 'hooks-codex.json'),
|
|
9
|
+
path.join('hooks', 'run-hook.cmd'),
|
|
10
|
+
path.join('hooks', 'session-start-codex'),
|
|
11
|
+
'skills',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export function stageCodexPlugin({ cwd = process.cwd(), packageRoot }) {
|
|
15
|
+
const targetRoot = path.join(cwd, CODEX_STAGE_DIR);
|
|
16
|
+
|
|
17
|
+
fs.mkdirSync(targetRoot, { recursive: true });
|
|
18
|
+
|
|
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);
|
|
23
|
+
|
|
24
|
+
if (stat.isDirectory()) {
|
|
25
|
+
fs.cpSync(sourcePath, targetPath, { recursive: true });
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
30
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
31
|
+
fs.chmodSync(targetPath, stat.mode);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
targetRoot,
|
|
36
|
+
relativeTargetRoot: CODEX_STAGE_DIR,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -8,52 +8,86 @@ 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 } = {}) {
|
|
47
|
+
async install({ runner = spawnSync, dryRun = false, cwd = process.cwd(), env = process.env, fs = null } = {}) {
|
|
33
48
|
if (dryRun) {
|
|
34
|
-
return this.planInstall();
|
|
49
|
+
return { plan: this.planInstall(), metadata: {} };
|
|
35
50
|
}
|
|
36
51
|
|
|
37
|
-
if (!
|
|
52
|
+
if (!automated) {
|
|
38
53
|
throw new InstallerError(`${label} install is not fully automatable yet.`);
|
|
39
54
|
}
|
|
40
55
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
if (typeof customInstall === 'function') {
|
|
57
|
+
const metadata = await customInstall({ cwd, env, fs });
|
|
58
|
+
return { plan: this.planInstall(), metadata: metadata ?? {} };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const entry of commandList) {
|
|
62
|
+
const result = runner(entry.file, entry.args ?? [], {
|
|
63
|
+
cwd: entry.cwd ?? cwd,
|
|
64
|
+
env: entry.env ?? env,
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
});
|
|
46
67
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
if (result.status !== 0) {
|
|
69
|
+
throw new InstallerError(
|
|
70
|
+
`${label} install failed: ${result.stderr || result.stdout || 'unknown error'}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
51
73
|
}
|
|
52
74
|
|
|
53
|
-
return this.planInstall();
|
|
75
|
+
return { plan: this.planInstall(), metadata: {} };
|
|
54
76
|
},
|
|
55
77
|
async verify() {
|
|
56
78
|
return verifyHint;
|
|
57
79
|
},
|
|
58
80
|
};
|
|
59
81
|
}
|
|
82
|
+
|
|
83
|
+
function normalizeCommandList(commands, command) {
|
|
84
|
+
if (Array.isArray(commands) && commands.length > 0) {
|
|
85
|
+
return commands;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (command) {
|
|
89
|
+
return [command];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
@@ -11,6 +11,10 @@ export function createOpenCodeAdapter() {
|
|
|
11
11
|
bootstrap: 'using-spark -> .opencode/plugins/spark.js -> message transform bootstrap',
|
|
12
12
|
installHint: '.opencode/plugins/spark.js + .opencode/INSTALL.md',
|
|
13
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.',
|
|
17
|
+
],
|
|
14
18
|
});
|
|
15
19
|
}
|
|
16
20
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { createAdapter } from './common.js';
|
|
2
|
+
import { stageCodexPlugin } from './codex-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 createClaudeCodeAdapter() {
|
|
4
9
|
return createAdapter({
|
|
@@ -10,6 +15,9 @@ export function createClaudeCodeAdapter() {
|
|
|
10
15
|
bootstrap: 'shell hook -> hooks/session-start -> using-spark',
|
|
11
16
|
installHint: '.claude-plugin/plugin.json + hooks/hooks.json + hooks/session-start',
|
|
12
17
|
verifyHint: 'Run a fresh Claude Code session and confirm using-spark loads before coding.',
|
|
18
|
+
manualSteps: [
|
|
19
|
+
'/plugin install spark@claude-plugins-official',
|
|
20
|
+
],
|
|
13
21
|
});
|
|
14
22
|
}
|
|
15
23
|
|
|
@@ -22,7 +30,15 @@ export function createCodexAdapter() {
|
|
|
22
30
|
binaryNames: ['codex'],
|
|
23
31
|
bootstrap: 'shell hook -> hooks/session-start-codex -> using-spark',
|
|
24
32
|
installHint: '.codex-plugin/plugin.json + hooks/hooks-codex.json + hooks/session-start-codex',
|
|
25
|
-
verifyHint: '
|
|
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.',
|
|
35
|
+
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.',
|
|
38
|
+
],
|
|
39
|
+
customInstall({ cwd }) {
|
|
40
|
+
return stageCodexPlugin({ cwd, packageRoot });
|
|
41
|
+
},
|
|
26
42
|
});
|
|
27
43
|
}
|
|
28
44
|
|
|
@@ -36,6 +52,9 @@ export function createCursorAdapter() {
|
|
|
36
52
|
bootstrap: 'shell hook -> hooks/session-start -> using-spark',
|
|
37
53
|
installHint: '.cursor-plugin/plugin.json + hooks/hooks-cursor.json + hooks/session-start',
|
|
38
54
|
verifyHint: 'Run a fresh Cursor Agent session and confirm using-spark loads before coding.',
|
|
55
|
+
manualSteps: [
|
|
56
|
+
'/add-plugin spark',
|
|
57
|
+
],
|
|
39
58
|
});
|
|
40
59
|
}
|
|
41
60
|
|
|
@@ -49,9 +68,15 @@ export function createCopilotAdapter() {
|
|
|
49
68
|
bootstrap: 'shell hook -> hooks/session-start -> using-spark',
|
|
50
69
|
installHint: 'skills/using-spark/references/copilot-tools.md + hooks/session-start',
|
|
51
70
|
verifyHint: 'Run a fresh Copilot CLI session and confirm using-spark loads before coding.',
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
commands: [
|
|
72
|
+
{
|
|
73
|
+
file: 'copilot',
|
|
74
|
+
args: ['plugin', 'marketplace', 'add', 'adityaaria/SPARK-marketplace'],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
file: 'copilot',
|
|
78
|
+
args: ['plugin', 'install', 'spark@spark-marketplace'],
|
|
79
|
+
},
|
|
80
|
+
],
|
|
56
81
|
});
|
|
57
82
|
}
|
package/src/installer/detect.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
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
|
|
|
7
|
-
const HIGH_CONFIDENCE_SCORE = 80;
|
|
8
|
-
const AMBIGUOUS_MARGIN = 20;
|
|
9
|
-
|
|
10
8
|
export function detectHarnessCandidates({ env = process.env, fsImpl = fs } = {}) {
|
|
11
9
|
const homeDir = os.homedir();
|
|
12
10
|
const adapters = listAdapters();
|
|
@@ -31,25 +29,22 @@ export async function chooseHarness({
|
|
|
31
29
|
return { adapter, source: 'forced', candidates: [] };
|
|
32
30
|
}
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
throw new InstallerError(
|
|
37
|
-
`Could not detect a supported harness. Supported harnesses: ${listAdapters().map((adapter) => adapter.label).join(', ')}`
|
|
38
|
-
);
|
|
32
|
+
if (!prompt) {
|
|
33
|
+
throw new InstallerError('No harness selected. Re-run with --harness or use an interactive terminal.');
|
|
39
34
|
}
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return { adapter, source: 'auto', candidates };
|
|
44
|
-
}
|
|
36
|
+
const detectedCandidates = detectHarnessCandidates({ env, fsImpl: fs });
|
|
37
|
+
const candidates = buildPromptCandidates(detectedCandidates);
|
|
45
38
|
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
let validationMessage = null;
|
|
40
|
+
while (true) {
|
|
41
|
+
const answer = await prompt(renderPrompt(candidates, validationMessage));
|
|
42
|
+
const adapter = resolvePromptAnswer(answer, candidates);
|
|
43
|
+
if (adapter) {
|
|
44
|
+
return { adapter, source: 'prompt', candidates };
|
|
45
|
+
}
|
|
46
|
+
validationMessage = `Unsupported choice: ${answer || '(empty)'}. Please choose a number or harness name from the list.`;
|
|
48
47
|
}
|
|
49
|
-
|
|
50
|
-
const answer = await prompt(renderPrompt(candidates));
|
|
51
|
-
const adapter = resolvePromptAnswer(answer, candidates);
|
|
52
|
-
return { adapter, source: 'prompt', candidates };
|
|
53
48
|
}
|
|
54
49
|
|
|
55
50
|
function scoreAdapter(adapter, env, fsImpl, homeDir) {
|
|
@@ -94,12 +89,16 @@ function candidateConfigPaths(homeDir, env, configPath) {
|
|
|
94
89
|
paths.push(path.join(env.OPENCODE_CONFIG_DIR, 'opencode.json'));
|
|
95
90
|
}
|
|
96
91
|
if (configPath === 'GEMINI.md') {
|
|
97
|
-
paths.push(
|
|
98
|
-
|
|
92
|
+
paths.push(
|
|
93
|
+
path.join(homeDir, '.gemini', 'GEMINI.md'),
|
|
94
|
+
path.join(homeDir, 'GEMINI.md')
|
|
95
|
+
);
|
|
99
96
|
}
|
|
100
97
|
if (configPath === 'gemini-extension.json') {
|
|
101
|
-
paths.push(
|
|
102
|
-
|
|
98
|
+
paths.push(
|
|
99
|
+
path.join(homeDir, 'gemini-extension.json'),
|
|
100
|
+
path.join(homeDir, '.gemini', 'gemini-extension.json')
|
|
101
|
+
);
|
|
103
102
|
}
|
|
104
103
|
return paths;
|
|
105
104
|
}
|
|
@@ -130,25 +129,54 @@ function commandExists(commandName, env, fsImpl) {
|
|
|
130
129
|
return false;
|
|
131
130
|
}
|
|
132
131
|
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return candidates[0].score - candidates[1].score >= AMBIGUOUS_MARGIN;
|
|
137
|
-
}
|
|
132
|
+
function buildPromptCandidates(detectedCandidates) {
|
|
133
|
+
const allAdapters = listAdapters();
|
|
134
|
+
const detectedIds = new Set(detectedCandidates.map((candidate) => candidate.id));
|
|
138
135
|
|
|
139
|
-
function renderPrompt(candidates) {
|
|
140
136
|
return [
|
|
141
|
-
|
|
142
|
-
...
|
|
143
|
-
|
|
144
|
-
|
|
137
|
+
...detectedCandidates,
|
|
138
|
+
...allAdapters
|
|
139
|
+
.filter((adapter) => !detectedIds.has(adapter.id))
|
|
140
|
+
.map((adapter) => ({
|
|
141
|
+
id: adapter.id,
|
|
142
|
+
label: adapter.label,
|
|
143
|
+
score: 0,
|
|
144
|
+
reasons: [],
|
|
145
|
+
})),
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function renderPrompt(candidates, validationMessage = null) {
|
|
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
|
+
);
|
|
145
175
|
}
|
|
146
176
|
|
|
147
177
|
function resolvePromptAnswer(answer, candidates) {
|
|
148
178
|
const value = normalizeHarness(answer);
|
|
149
|
-
if (!value)
|
|
150
|
-
throw new InstallerError('No harness selected.');
|
|
151
|
-
}
|
|
179
|
+
if (!value) return null;
|
|
152
180
|
|
|
153
181
|
const numeric = Number(value);
|
|
154
182
|
if (Number.isInteger(numeric) && numeric >= 1 && numeric <= candidates.length) {
|
|
@@ -156,9 +184,7 @@ function resolvePromptAnswer(answer, candidates) {
|
|
|
156
184
|
}
|
|
157
185
|
|
|
158
186
|
const exact = candidates.find((candidate) => candidate.id === value || candidate.label.toLowerCase() === value);
|
|
159
|
-
if (!exact)
|
|
160
|
-
throw new InstallerError(`Unsupported harness choice: ${answer}`);
|
|
161
|
-
}
|
|
187
|
+
if (!exact) return null;
|
|
162
188
|
|
|
163
189
|
return getAdapterById(exact.id);
|
|
164
190
|
}
|