@adityaaria/spark 6.0.8 → 6.0.10
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 +1 -1
- package/src/cli/install.js +27 -0
- package/src/installer/adapters/common.js +50 -0
- package/src/installer/adapters/extension-style.js +2 -0
- package/src/installer/adapters/global-skills-copy.js +77 -0
- package/src/installer/adapters/shell-hook.js +3 -0
- package/src/installer/detect.js +35 -16
package/package.json
CHANGED
package/src/cli/install.js
CHANGED
|
@@ -56,6 +56,33 @@ export async function runInstall(options, env) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
const installResult = await adapter.install({ options, env });
|
|
59
|
+
|
|
60
|
+
// Handle global copy result
|
|
61
|
+
if (installResult?.globalCopy) {
|
|
62
|
+
printSection('Install');
|
|
63
|
+
printLine(labelValue('Harness', adapter.label));
|
|
64
|
+
|
|
65
|
+
if (installResult.cliSuccess) {
|
|
66
|
+
// Both global copy AND CLI succeeded — full install
|
|
67
|
+
printLine(statusText(`Installed SPARK for ${adapter.label}.`, 'success'));
|
|
68
|
+
} else {
|
|
69
|
+
// Global copy succeeded, CLI was skipped — still fully functional
|
|
70
|
+
printLine(statusText(`SPARK skills installed to ${installResult.globalSkillsPath}`, 'success'));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const readyLines = [
|
|
74
|
+
bullet(`SPARK skills and hooks copied to ${installResult.globalSkillsPath}.`),
|
|
75
|
+
bullet(`Open a fresh ${adapter.label} session to confirm using-spark loads before coding.`),
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
if (installResult.cliSkipped) {
|
|
79
|
+
readyLines.push(bullet(`CLI integration skipped (optional — SPARK already works without it).`));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
printSummary('Ready', readyLines, 'success');
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
59
86
|
if (typeof adapter.verify === 'function') {
|
|
60
87
|
await adapter.verify({ options, env });
|
|
61
88
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { InstallerError } from '../errors.js';
|
|
3
|
+
import { copyToGlobal } from './global-skills-copy.js';
|
|
3
4
|
|
|
4
5
|
export function createAdapter({
|
|
5
6
|
id,
|
|
@@ -17,6 +18,7 @@ export function createAdapter({
|
|
|
17
18
|
envKeys = [],
|
|
18
19
|
binaryNames = [],
|
|
19
20
|
configPaths = [],
|
|
21
|
+
packageRoot = null,
|
|
20
22
|
}) {
|
|
21
23
|
const commandList = normalizeCommandList(commands, command);
|
|
22
24
|
const automated = commandList.length > 0 || typeof customInstall === 'function';
|
|
@@ -59,6 +61,17 @@ export function createAdapter({
|
|
|
59
61
|
return { plan: this.planInstall(), metadata };
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
// Step 1: ALWAYS copy skills + hooks to global directory (primary mechanism)
|
|
65
|
+
let globalCopyResult = { copied: false };
|
|
66
|
+
if (packageRoot) {
|
|
67
|
+
try {
|
|
68
|
+
globalCopyResult = copyToGlobal(id, packageRoot, env);
|
|
69
|
+
} catch { /* global copy failed — will try CLI next */ }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Step 2: Try CLI commands (optional bonus — for marketplace integration)
|
|
73
|
+
let cliSuccess = false;
|
|
74
|
+
let cliSkipped = false;
|
|
62
75
|
for (const entry of commandList) {
|
|
63
76
|
const result = runner(entry.file, interpolateArgs(entry.args ?? [], metadata), {
|
|
64
77
|
cwd: entry.cwd ?? cwd,
|
|
@@ -66,11 +79,48 @@ export function createAdapter({
|
|
|
66
79
|
encoding: 'utf8',
|
|
67
80
|
});
|
|
68
81
|
|
|
82
|
+
if (result.error) {
|
|
83
|
+
if (result.error.code === 'ENOENT') {
|
|
84
|
+
// CLI not found — not a problem if global copy succeeded
|
|
85
|
+
cliSkipped = true;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
// Other errors — also non-fatal if global copy succeeded
|
|
89
|
+
if (globalCopyResult.copied) {
|
|
90
|
+
cliSkipped = true;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
throw new InstallerError(`${label} install failed: ${result.error.message}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
69
96
|
if (result.status !== 0) {
|
|
97
|
+
if (globalCopyResult.copied) {
|
|
98
|
+
cliSkipped = true;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
70
101
|
throw new InstallerError(
|
|
71
102
|
`${label} install failed: ${result.stderr || result.stdout || 'unknown error'}`
|
|
72
103
|
);
|
|
73
104
|
}
|
|
105
|
+
|
|
106
|
+
cliSuccess = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Determine result type
|
|
110
|
+
if (globalCopyResult.copied) {
|
|
111
|
+
return {
|
|
112
|
+
plan: this.planInstall(),
|
|
113
|
+
metadata,
|
|
114
|
+
globalCopy: true,
|
|
115
|
+
globalSkillsPath: globalCopyResult.globalSkillsPath,
|
|
116
|
+
cliSuccess,
|
|
117
|
+
cliSkipped,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Neither global copy nor CLI worked — nothing we can do
|
|
122
|
+
if (!cliSuccess && commandList.length > 0) {
|
|
123
|
+
throw new InstallerError(`${label} install failed: unable to copy skills or run CLI.`);
|
|
74
124
|
}
|
|
75
125
|
|
|
76
126
|
return { plan: this.planInstall(), metadata };
|
|
@@ -50,6 +50,7 @@ export function createGeminiAdapter() {
|
|
|
50
50
|
id: 'gemini',
|
|
51
51
|
label: 'Gemini CLI',
|
|
52
52
|
kind: 'context-file',
|
|
53
|
+
packageRoot,
|
|
53
54
|
envKeys: ['GEMINI_HOME', 'GOOGLE_GEMINI_CLI'],
|
|
54
55
|
binaryNames: ['gemini'],
|
|
55
56
|
configPaths: ['GEMINI.md', 'gemini-extension.json'],
|
|
@@ -73,6 +74,7 @@ export function createAntigravityAdapter() {
|
|
|
73
74
|
id: 'antigravity',
|
|
74
75
|
label: 'Antigravity',
|
|
75
76
|
kind: 'context-file',
|
|
77
|
+
packageRoot,
|
|
76
78
|
envKeys: ['ANTIGRAVITY_PLUGIN_ROOT'],
|
|
77
79
|
binaryNames: ['agy'],
|
|
78
80
|
bootstrap: 'using-spark -> agy plugin install -> contextFileName bootstrap',
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Mapping of harness ID to its global directory structure.
|
|
7
|
+
* Each entry defines where skills and hooks should be copied
|
|
8
|
+
* so the harness discovers SPARK at session start.
|
|
9
|
+
*/
|
|
10
|
+
const GLOBAL_TARGETS = {
|
|
11
|
+
codex: {
|
|
12
|
+
skillsDir: path.join('.codex', 'skills'),
|
|
13
|
+
hooksDir: path.join('.codex', 'hooks'),
|
|
14
|
+
hookFiles: [
|
|
15
|
+
{ src: 'hooks/hooks-codex.json', dest: 'hooks.json' },
|
|
16
|
+
{ src: 'hooks/session-start-codex', dest: 'session-start-codex' },
|
|
17
|
+
{ src: 'hooks/run-hook.cmd', dest: 'run-hook.cmd' },
|
|
18
|
+
],
|
|
19
|
+
pluginDir: path.join('.codex', '.codex-plugin'),
|
|
20
|
+
pluginSrc: '.codex-plugin',
|
|
21
|
+
},
|
|
22
|
+
claude: {
|
|
23
|
+
skillsDir: path.join('.claude', 'skills'),
|
|
24
|
+
hooksDir: path.join('.claude', 'hooks'),
|
|
25
|
+
hookFiles: [
|
|
26
|
+
{ src: 'hooks/hooks.json', dest: 'hooks.json' },
|
|
27
|
+
{ src: 'hooks/session-start', dest: 'session-start' },
|
|
28
|
+
{ src: 'hooks/run-hook.cmd', dest: 'run-hook.cmd' },
|
|
29
|
+
],
|
|
30
|
+
pluginDir: path.join('.claude', '.claude-plugin'),
|
|
31
|
+
pluginSrc: '.claude-plugin',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Copy SPARK skills and hooks directly to a harness's global directory.
|
|
37
|
+
* This is a fallback mechanism used when the harness CLI is not available.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} id - The harness identifier (e.g. 'codex', 'claude')
|
|
40
|
+
* @param {string} packageRoot - Absolute path to the SPARK package root
|
|
41
|
+
* @param {object} env - Process environment (to resolve HOME)
|
|
42
|
+
* @returns {{ globalSkillsPath: string, globalHooksPath: string, copied: boolean }}
|
|
43
|
+
*/
|
|
44
|
+
export function copyToGlobal(id, packageRoot, env = process.env) {
|
|
45
|
+
const target = GLOBAL_TARGETS[id];
|
|
46
|
+
if (!target) {
|
|
47
|
+
return { globalSkillsPath: null, globalHooksPath: null, copied: false };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const homeDir = env.HOME || env.USERPROFILE || os.homedir();
|
|
51
|
+
const globalSkillsPath = path.join(homeDir, target.skillsDir);
|
|
52
|
+
const globalHooksPath = path.join(homeDir, target.hooksDir);
|
|
53
|
+
|
|
54
|
+
// 1. Copy skills
|
|
55
|
+
const sourceSkills = path.join(packageRoot, 'skills');
|
|
56
|
+
fs.cpSync(sourceSkills, globalSkillsPath, { recursive: true });
|
|
57
|
+
|
|
58
|
+
// 2. Copy hooks
|
|
59
|
+
fs.mkdirSync(globalHooksPath, { recursive: true });
|
|
60
|
+
for (const hookFile of target.hookFiles) {
|
|
61
|
+
const src = path.join(packageRoot, hookFile.src);
|
|
62
|
+
const dest = path.join(globalHooksPath, hookFile.dest);
|
|
63
|
+
fs.copyFileSync(src, dest);
|
|
64
|
+
// Preserve executable permission for shell scripts
|
|
65
|
+
const stat = fs.statSync(src);
|
|
66
|
+
fs.chmodSync(dest, stat.mode);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 3. Copy plugin descriptor (e.g. .codex-plugin/)
|
|
70
|
+
if (target.pluginDir && target.pluginSrc) {
|
|
71
|
+
const srcPlugin = path.join(packageRoot, target.pluginSrc);
|
|
72
|
+
const destPlugin = path.join(homeDir, target.pluginDir);
|
|
73
|
+
fs.cpSync(srcPlugin, destPlugin, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { globalSkillsPath, globalHooksPath, copied: true };
|
|
77
|
+
}
|
|
@@ -13,6 +13,7 @@ export function createClaudeCodeAdapter() {
|
|
|
13
13
|
id: 'claude',
|
|
14
14
|
label: 'Claude Code',
|
|
15
15
|
kind: 'shell-hook',
|
|
16
|
+
packageRoot,
|
|
16
17
|
envKeys: ['CLAUDE_PLUGIN_ROOT'],
|
|
17
18
|
binaryNames: ['claude'],
|
|
18
19
|
bootstrap: 'shell hook -> hooks/session-start -> using-spark',
|
|
@@ -45,6 +46,7 @@ export function createCodexAdapter() {
|
|
|
45
46
|
id: 'codex',
|
|
46
47
|
label: 'Codex CLI',
|
|
47
48
|
kind: 'shell-hook',
|
|
49
|
+
packageRoot,
|
|
48
50
|
envKeys: ['CLAUDE_PLUGIN_ROOT'],
|
|
49
51
|
binaryNames: ['codex'],
|
|
50
52
|
bootstrap: 'shell hook -> hooks/session-start-codex -> using-spark',
|
|
@@ -119,6 +121,7 @@ export function createCopilotAdapter() {
|
|
|
119
121
|
id: 'copilot',
|
|
120
122
|
label: 'Copilot CLI',
|
|
121
123
|
kind: 'shell-hook',
|
|
124
|
+
packageRoot,
|
|
122
125
|
envKeys: ['COPILOT_CLI'],
|
|
123
126
|
binaryNames: ['copilot'],
|
|
124
127
|
bootstrap: 'shell hook -> hooks/session-start -> using-spark',
|
package/src/installer/detect.js
CHANGED
|
@@ -33,8 +33,9 @@ export async function chooseHarness({
|
|
|
33
33
|
throw new InstallerError('No harness selected. Re-run with --harness or use an interactive terminal.');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
const homeDir = os.homedir();
|
|
36
37
|
const detectedCandidates = detectHarnessCandidates({ env, fsImpl: fs });
|
|
37
|
-
const candidates = buildPromptCandidates(detectedCandidates);
|
|
38
|
+
const candidates = buildPromptCandidates(detectedCandidates, env, fs, homeDir);
|
|
38
39
|
|
|
39
40
|
let validationMessage = null;
|
|
40
41
|
while (true) {
|
|
@@ -51,6 +52,8 @@ function scoreAdapter(adapter, env, fsImpl, homeDir) {
|
|
|
51
52
|
let score = 0;
|
|
52
53
|
const reasons = [];
|
|
53
54
|
|
|
55
|
+
let missingBinaries = false;
|
|
56
|
+
|
|
54
57
|
for (const key of adapter.envKeys) {
|
|
55
58
|
if (env[key]) {
|
|
56
59
|
score += 100;
|
|
@@ -58,10 +61,15 @@ function scoreAdapter(adapter, env, fsImpl, homeDir) {
|
|
|
58
61
|
}
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
}
|
|
65
73
|
}
|
|
66
74
|
}
|
|
67
75
|
|
|
@@ -80,6 +88,7 @@ function scoreAdapter(adapter, env, fsImpl, homeDir) {
|
|
|
80
88
|
label: adapter.label,
|
|
81
89
|
score,
|
|
82
90
|
reasons,
|
|
91
|
+
missingBinaries,
|
|
83
92
|
};
|
|
84
93
|
}
|
|
85
94
|
|
|
@@ -129,7 +138,7 @@ function commandExists(commandName, env, fsImpl) {
|
|
|
129
138
|
return false;
|
|
130
139
|
}
|
|
131
140
|
|
|
132
|
-
function buildPromptCandidates(detectedCandidates) {
|
|
141
|
+
function buildPromptCandidates(detectedCandidates, env, fsImpl, homeDir) {
|
|
133
142
|
const allAdapters = listAdapters();
|
|
134
143
|
const detectedIds = new Set(detectedCandidates.map((candidate) => candidate.id));
|
|
135
144
|
|
|
@@ -137,12 +146,12 @@ function buildPromptCandidates(detectedCandidates) {
|
|
|
137
146
|
...detectedCandidates,
|
|
138
147
|
...allAdapters
|
|
139
148
|
.filter((adapter) => !detectedIds.has(adapter.id))
|
|
140
|
-
.map((adapter) =>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
})
|
|
149
|
+
.map((adapter) => {
|
|
150
|
+
const result = scoreAdapter(adapter, env, fsImpl, homeDir);
|
|
151
|
+
result.score = 0;
|
|
152
|
+
result.reasons = [];
|
|
153
|
+
return result;
|
|
154
|
+
}),
|
|
146
155
|
];
|
|
147
156
|
}
|
|
148
157
|
|
|
@@ -163,14 +172,19 @@ function renderPrompt(candidates, validationMessage = null) {
|
|
|
163
172
|
}
|
|
164
173
|
|
|
165
174
|
for (const [index, candidate] of candidates.entries()) {
|
|
175
|
+
let label = candidate.label;
|
|
176
|
+
if (candidate.missingBinaries) {
|
|
177
|
+
label += ' [Missing CLI]';
|
|
178
|
+
}
|
|
179
|
+
|
|
166
180
|
const hint = recommendedIds.has(candidate.id) ? 'recommended' : null;
|
|
167
181
|
lines.push(
|
|
168
182
|
promptOption(
|
|
169
183
|
index + 1,
|
|
170
|
-
|
|
184
|
+
label,
|
|
171
185
|
candidate.id,
|
|
172
186
|
hint,
|
|
173
|
-
describeHarness(candidate.id)
|
|
187
|
+
describeHarness(candidate.id, candidate.missingBinaries)
|
|
174
188
|
)
|
|
175
189
|
);
|
|
176
190
|
}
|
|
@@ -197,7 +211,7 @@ function resolvePromptAnswer(answer, candidates) {
|
|
|
197
211
|
return getAdapterById(exact.id);
|
|
198
212
|
}
|
|
199
213
|
|
|
200
|
-
function describeHarness(id) {
|
|
214
|
+
function describeHarness(id, missingBinaries = false) {
|
|
201
215
|
const descriptions = {
|
|
202
216
|
claude: 'Best for Claude Code sessions and project-local plugin workflows.',
|
|
203
217
|
codex: 'For the Codex CLI plugin marketplace flow in terminal sessions.',
|
|
@@ -210,7 +224,12 @@ function describeHarness(id) {
|
|
|
210
224
|
antigravity: 'Installs the Antigravity plugin and loads SPARK on new sessions.',
|
|
211
225
|
};
|
|
212
226
|
|
|
213
|
-
|
|
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;
|
|
214
233
|
}
|
|
215
234
|
|
|
216
235
|
function normalizeHarness(value) {
|