@adityaaria/spark 6.0.15 → 6.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -3
- package/bin/spark-install.sh +861 -0
- package/package.json +1 -1
- package/skills/using-spark/SKILL.md +7 -3
- package/src/cli/index.js +6 -12
- package/src/cli/install.js +38 -166
- package/src/cli/output.js +6 -3
- package/src/cli/parse-args.js +0 -46
- package/src/cli/prompt.js +0 -11
- package/src/installer/adapters/claude-staging.js +0 -68
- package/src/installer/adapters/codex-staging.js +0 -77
- package/src/installer/adapters/common.js +0 -158
- package/src/installer/adapters/cursor-staging.js +0 -42
- package/src/installer/adapters/extension-style.js +0 -93
- package/src/installer/adapters/opencode-staging.js +0 -61
- package/src/installer/adapters/shell-hook.js +0 -138
- package/src/installer/adapters/vscode-staging.js +0 -75
- package/src/installer/detect.js +0 -257
- package/src/installer/errors.js +0 -7
- package/src/installer/registry.js +0 -37
package/package.json
CHANGED
|
@@ -49,6 +49,10 @@ Skills speak in actions ("dispatch a subagent", "create a todo", "read a file")
|
|
|
49
49
|
|
|
50
50
|
**Invoke relevant or requested skills BEFORE any response or action.** Even a 1% chance a skill might apply means that you should invoke the skill to check. If an invoked skill turns out to be wrong for the situation, you don't need to use it.
|
|
51
51
|
|
|
52
|
+
## Announcement Format
|
|
53
|
+
When you invoke a skill, you MUST announce it to the developer using exactly this format as the very first thing you say:
|
|
54
|
+
`spark detection 💥 Using [skill] to [purpose]`
|
|
55
|
+
|
|
52
56
|
```dot
|
|
53
57
|
digraph skill_flow {
|
|
54
58
|
"User message received" [shape=doublecircle];
|
|
@@ -57,7 +61,7 @@ digraph skill_flow {
|
|
|
57
61
|
"Invoke brainstorming skill" [shape=box];
|
|
58
62
|
"Might any skill apply?" [shape=diamond];
|
|
59
63
|
"Invoke the skill" [shape=box];
|
|
60
|
-
"Announce: 'Using [skill]
|
|
64
|
+
"Announce: 'spark detection 💥 Using [skill]...'" [shape=box];
|
|
61
65
|
"Has checklist?" [shape=diamond];
|
|
62
66
|
"Create a todo per item" [shape=box];
|
|
63
67
|
"Follow skill exactly" [shape=box];
|
|
@@ -71,8 +75,8 @@ digraph skill_flow {
|
|
|
71
75
|
"User message received" -> "Might any skill apply?";
|
|
72
76
|
"Might any skill apply?" -> "Invoke the skill" [label="yes, even 1%"];
|
|
73
77
|
"Might any skill apply?" -> "Respond (including clarifications)" [label="definitely not"];
|
|
74
|
-
"Invoke the skill" -> "Announce: 'Using [skill]
|
|
75
|
-
"Announce: 'Using [skill]
|
|
78
|
+
"Invoke the skill" -> "Announce: 'spark detection 💥 Using [skill]...'";
|
|
79
|
+
"Announce: 'spark detection 💥 Using [skill]...'" -> "Has checklist?";
|
|
76
80
|
"Has checklist?" -> "Create a todo per item" [label="yes"];
|
|
77
81
|
"Has checklist?" -> "Follow skill exactly" [label="no"];
|
|
78
82
|
"Create a todo per item" -> "Follow skill exactly";
|
package/src/cli/index.js
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
import { parseArgs } from './parse-args.js';
|
|
2
1
|
import { printHelp, printLine } from './output.js';
|
|
3
2
|
import { runInstall } from './install.js';
|
|
4
3
|
|
|
5
|
-
export async function run(argv, env) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (options.help || options.command === 'help') {
|
|
4
|
+
export async function run(argv = [], env = process.env) {
|
|
5
|
+
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h' || argv[0] === 'help') {
|
|
9
6
|
printHelp();
|
|
10
7
|
return;
|
|
11
8
|
}
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
printHelp();
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
10
|
+
const [command, ...args] = argv;
|
|
17
11
|
|
|
18
|
-
if (
|
|
19
|
-
await runInstall(
|
|
12
|
+
if (command === 'install') {
|
|
13
|
+
await runInstall(args, env);
|
|
20
14
|
return;
|
|
21
15
|
}
|
|
22
16
|
|
|
23
|
-
throw new Error(`Unknown command: ${
|
|
17
|
+
throw new Error(`Unknown command: ${command}`);
|
|
24
18
|
}
|
|
25
19
|
|
|
26
20
|
export { printLine };
|
package/src/cli/install.js
CHANGED
|
@@ -1,168 +1,40 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
export function runInstall(args = [], env = process.env) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const scriptPath = path.resolve(__dirname, '../../bin/spark-install.sh');
|
|
10
|
+
const child = spawn('bash', [scriptPath, ...args], {
|
|
11
|
+
stdio: 'inherit',
|
|
12
|
+
env,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
let settled = false;
|
|
16
|
+
|
|
17
|
+
child.on('error', (err) => {
|
|
18
|
+
if (settled) return;
|
|
19
|
+
settled = true;
|
|
20
|
+
if (err.code === 'ENOENT') {
|
|
21
|
+
process.stderr.write(
|
|
22
|
+
'Error: "bash" command not found.\n\n' +
|
|
23
|
+
'SPARK native installer requires bash to run.\n' +
|
|
24
|
+
'If you are on Windows, please run this command inside WSL, Git Bash, or check README.md for manual installation instructions.\n'
|
|
25
|
+
);
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
resolve();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
reject(err);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
child.on('close', (code) => {
|
|
34
|
+
if (settled) return;
|
|
35
|
+
settled = true;
|
|
36
|
+
process.exitCode = code ?? 1;
|
|
37
|
+
resolve();
|
|
38
|
+
});
|
|
25
39
|
});
|
|
26
|
-
const { adapter, source } = selection;
|
|
27
|
-
const plan = adapter.planInstall();
|
|
28
|
-
|
|
29
|
-
if (options.dryRun) {
|
|
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');
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (typeof adapter.install !== 'function') {
|
|
42
|
-
throw new InstallerError(`Install flow for ${adapter.label} is not implemented yet.`);
|
|
43
|
-
}
|
|
44
|
-
|
|
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
|
-
);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const installResult = await adapter.install({ options, env });
|
|
59
|
-
|
|
60
|
-
if (typeof adapter.verify === 'function') {
|
|
61
|
-
await adapter.verify({ options, env });
|
|
62
|
-
}
|
|
63
|
-
const resolvedPlan = installResult?.plan ?? plan;
|
|
64
|
-
const metadata = installResult?.metadata ?? {};
|
|
65
|
-
|
|
66
|
-
printSection('Install');
|
|
67
|
-
printLine(labelValue('Harness', adapter.label));
|
|
68
|
-
printLine(statusText(resolvedPlan.successMessage ?? plan.successMessage, 'success'));
|
|
69
|
-
printPlanDetails(resolvedPlan, metadata);
|
|
70
|
-
printSummary(
|
|
71
|
-
'Ready',
|
|
72
|
-
buildReadyLines(adapter, resolvedPlan, metadata),
|
|
73
|
-
'success'
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function normalizeHarness(harness) {
|
|
78
|
-
if (!harness) return null;
|
|
79
|
-
return String(harness).trim().toLowerCase();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function printPlanDetails(plan, metadata) {
|
|
83
|
-
if (plan.automated && plan.commands?.length) {
|
|
84
|
-
printLine('');
|
|
85
|
-
printSection('Commands');
|
|
86
|
-
for (const [index, command] of plan.commands.entries()) {
|
|
87
|
-
printLine(
|
|
88
|
-
step(
|
|
89
|
-
index + 1,
|
|
90
|
-
plan.commands.length,
|
|
91
|
-
commandText(formatCommand(command, metadata))
|
|
92
|
-
)
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (plan.automated && plan.automatedSteps?.length) {
|
|
98
|
-
printLine('');
|
|
99
|
-
printSection('Steps');
|
|
100
|
-
for (const [index, automatedStep] of plan.automatedSteps.entries()) {
|
|
101
|
-
printLine(step(index + 1, plan.automatedSteps.length, interpolatePlanText(automatedStep, metadata)));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (!plan.automated && plan.manualSteps?.length) {
|
|
106
|
-
printLine('');
|
|
107
|
-
printSection('Steps');
|
|
108
|
-
for (const [index, manualStep] of plan.manualSteps.entries()) {
|
|
109
|
-
printLine(step(index + 1, plan.manualSteps.length, manualStep));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
printLine('');
|
|
114
|
-
printSection('Notes');
|
|
115
|
-
printLine(labelValue('Install hint', plan.installHint));
|
|
116
|
-
printLine(labelValue('Verify hint', plan.verifyHint));
|
|
117
|
-
if (metadata.relativeTargetRoot) {
|
|
118
|
-
printLine(labelValue('Bundle path', pathText(metadata.relativeTargetRoot)));
|
|
119
|
-
}
|
|
120
|
-
if (metadata.relativeMarketplaceRoot) {
|
|
121
|
-
printLine(labelValue('Marketplace path', pathText(metadata.relativeMarketplaceRoot)));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function formatCommand(command, metadata = {}) {
|
|
126
|
-
return [command.file, ...(command.args ?? [])]
|
|
127
|
-
.map((part) => interpolatePlanText(part, metadata))
|
|
128
|
-
.join(' ');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function interpolatePlanText(text, metadata) {
|
|
132
|
-
return String(text)
|
|
133
|
-
.replaceAll('{targetRoot}', metadata.targetRoot ?? '')
|
|
134
|
-
.replaceAll('{relativeTargetRoot}', metadata.relativeTargetRoot ?? '')
|
|
135
|
-
.replaceAll('{marketplaceRoot}', metadata.marketplaceRoot ?? '')
|
|
136
|
-
.replaceAll('{relativeMarketplaceRoot}', metadata.relativeMarketplaceRoot ?? '')
|
|
137
|
-
.replaceAll('{marketplaceName}', metadata.marketplaceName ?? '');
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function buildReadyLines(adapter, plan, metadata) {
|
|
141
|
-
const lines = [];
|
|
142
|
-
|
|
143
|
-
if (metadata.relativeTargetRoot && adapter.id === 'vscode') {
|
|
144
|
-
lines.push(bullet(`Local VS Code bundle prepared at ${pathText(metadata.relativeTargetRoot)}.`));
|
|
145
|
-
} else if (metadata.relativeTargetRoot) {
|
|
146
|
-
lines.push(bullet(`Plugin bundle staged at ${pathText(metadata.relativeTargetRoot)}.`));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (metadata.relativeMarketplaceRoot) {
|
|
150
|
-
lines.push(bullet(`Local marketplace staged at ${pathText(metadata.relativeMarketplaceRoot)}.`));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (adapter.id === 'codex') {
|
|
154
|
-
lines.push(bullet('Start a fresh Codex session to confirm using-spark loads before coding.'));
|
|
155
|
-
return lines;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (adapter.id === 'vscode') {
|
|
159
|
-
if (metadata.relativeSettingsPath) {
|
|
160
|
-
lines.push(bullet(`VS Code plugin registration was written to ${pathText(metadata.relativeSettingsPath)}.`));
|
|
161
|
-
}
|
|
162
|
-
lines.push(bullet('Open a fresh VS Code agent session to confirm using-spark loads before coding.'));
|
|
163
|
-
return lines;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
lines.push(bullet('Start a fresh session in the selected harness to confirm using-spark loads before coding.'));
|
|
167
|
-
return lines;
|
|
168
40
|
}
|
package/src/cli/output.js
CHANGED
|
@@ -20,11 +20,14 @@ export function printLine(text = '') {
|
|
|
20
20
|
|
|
21
21
|
export function printHelp() {
|
|
22
22
|
printCommandHeader('Install');
|
|
23
|
-
printLine(labelValue('Usage', 'npx @adityaaria/spark install [
|
|
23
|
+
printLine(labelValue('Usage', 'npx @adityaaria/spark install [options]'));
|
|
24
24
|
printLine('');
|
|
25
|
-
printMuted('
|
|
25
|
+
printMuted('Wraps the native SPARK installer (bin/spark-install.sh).');
|
|
26
26
|
printLine('');
|
|
27
|
-
printLine(labelValue('
|
|
27
|
+
printLine(labelValue('Options', '-g, --global Install to global agent config (~/.agent/skills/)'));
|
|
28
|
+
printLine(labelValue(' ', '--force Re-install even if already installed'));
|
|
29
|
+
printLine(labelValue(' ', '--dry-run Show what would be done without making changes'));
|
|
30
|
+
printLine(labelValue(' ', '-h, --help Show this help message'));
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export function printCommandHeader(title) {
|
package/src/cli/parse-args.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
export function parseArgs(argv) {
|
|
2
|
-
const args = [...argv];
|
|
3
|
-
const options = {
|
|
4
|
-
command: null,
|
|
5
|
-
help: false,
|
|
6
|
-
dryRun: false,
|
|
7
|
-
yes: false,
|
|
8
|
-
verbose: false,
|
|
9
|
-
harness: null,
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
while (args.length) {
|
|
13
|
-
const arg = args.shift();
|
|
14
|
-
if (arg === '--help' || arg === '-h') {
|
|
15
|
-
options.help = true;
|
|
16
|
-
continue;
|
|
17
|
-
}
|
|
18
|
-
if (arg === '--dry-run') {
|
|
19
|
-
options.dryRun = true;
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
if (arg === '--yes' || arg === '-y') {
|
|
23
|
-
options.yes = true;
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
if (arg === '--verbose' || arg === '-v') {
|
|
27
|
-
options.verbose = true;
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (arg === '--harness') {
|
|
31
|
-
options.harness = args.shift() ?? null;
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
if (arg.startsWith('--harness=')) {
|
|
35
|
-
options.harness = arg.slice('--harness='.length) || null;
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
if (!options.command) {
|
|
39
|
-
options.command = arg;
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
throw new Error(`Unknown argument: ${arg}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return options;
|
|
46
|
-
}
|
package/src/cli/prompt.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import readline from 'node:readline/promises';
|
|
2
|
-
import { formatPromptPrefix } from './output.js';
|
|
3
|
-
|
|
4
|
-
export async function askQuestion(question, { input = process.stdin, output = process.stdout } = {}) {
|
|
5
|
-
const rl = readline.createInterface({ input, output });
|
|
6
|
-
try {
|
|
7
|
-
return await rl.question(`${question}\n${formatPromptPrefix()}`);
|
|
8
|
-
} finally {
|
|
9
|
-
rl.close();
|
|
10
|
-
}
|
|
11
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
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,77 +0,0 @@
|
|
|
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
|
-
marketplaceName: 'spark-local',
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function writeMarketplaceManifest(marketplaceRoot) {
|
|
52
|
-
const manifestPath = path.join(marketplaceRoot, '.agents', 'plugins', 'marketplace.json');
|
|
53
|
-
const manifest = {
|
|
54
|
-
name: 'spark-local',
|
|
55
|
-
interface: {
|
|
56
|
-
displayName: 'SPARK',
|
|
57
|
-
shortDescription: 'Local SPARK marketplace for Codex CLI',
|
|
58
|
-
},
|
|
59
|
-
plugins: [
|
|
60
|
-
{
|
|
61
|
-
name: 'spark',
|
|
62
|
-
source: {
|
|
63
|
-
source: 'local',
|
|
64
|
-
path: './plugins/spark',
|
|
65
|
-
},
|
|
66
|
-
policy: {
|
|
67
|
-
installation: 'AVAILABLE',
|
|
68
|
-
authentication: 'ON_INSTALL',
|
|
69
|
-
},
|
|
70
|
-
category: 'Productivity',
|
|
71
|
-
},
|
|
72
|
-
],
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
76
|
-
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
|
|
77
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { InstallerError } from '../errors.js';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
|
|
6
|
-
export function createAdapter({
|
|
7
|
-
id,
|
|
8
|
-
label,
|
|
9
|
-
kind,
|
|
10
|
-
bootstrap,
|
|
11
|
-
installHint,
|
|
12
|
-
verifyHint,
|
|
13
|
-
successMessage = `Installed SPARK for ${label}.`,
|
|
14
|
-
command = null,
|
|
15
|
-
commands = [],
|
|
16
|
-
customInstall = null,
|
|
17
|
-
manualSteps = [],
|
|
18
|
-
automatedSteps = [],
|
|
19
|
-
envKeys = [],
|
|
20
|
-
binaryNames = [],
|
|
21
|
-
configPaths = [],
|
|
22
|
-
}) {
|
|
23
|
-
const commandList = normalizeCommandList(commands, command);
|
|
24
|
-
const automated = commandList.length > 0 || typeof customInstall === 'function';
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
id,
|
|
28
|
-
label,
|
|
29
|
-
envKeys,
|
|
30
|
-
binaryNames,
|
|
31
|
-
configPaths,
|
|
32
|
-
command: commandList[0] ?? null,
|
|
33
|
-
commands: commandList,
|
|
34
|
-
manualSteps,
|
|
35
|
-
planInstall() {
|
|
36
|
-
return {
|
|
37
|
-
kind,
|
|
38
|
-
bootstrap,
|
|
39
|
-
installHint,
|
|
40
|
-
verifyHint,
|
|
41
|
-
successMessage,
|
|
42
|
-
command: commandList[0] ?? null,
|
|
43
|
-
commands: commandList,
|
|
44
|
-
manualSteps,
|
|
45
|
-
automatedSteps,
|
|
46
|
-
automated,
|
|
47
|
-
};
|
|
48
|
-
},
|
|
49
|
-
async install({ runner = spawnSync, dryRun = false, cwd = process.cwd(), env = process.env, fs = null } = {}) {
|
|
50
|
-
if (!automated) {
|
|
51
|
-
throw new InstallerError(`${label} install is not fully automatable yet.`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const fsImpl = fs ?? nativeFs;
|
|
55
|
-
if (!dryRun) {
|
|
56
|
-
assertBinaryAvailability({ label, binaryNames, env, fsImpl });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
let metadata = {};
|
|
60
|
-
|
|
61
|
-
if (typeof customInstall === 'function') {
|
|
62
|
-
metadata = (await customInstall({ cwd, env, fs, dryRun })) ?? {};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (dryRun) {
|
|
66
|
-
return { plan: this.planInstall(), metadata };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
for (const entry of commandList) {
|
|
70
|
-
const result = runner(entry.file, interpolateArgs(entry.args ?? [], metadata), {
|
|
71
|
-
cwd: entry.cwd ?? cwd,
|
|
72
|
-
env: entry.env ?? env,
|
|
73
|
-
encoding: 'utf8',
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
if (result.error) {
|
|
77
|
-
throw new InstallerError(`${label} install failed: ${result.error.message}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (result.status !== 0) {
|
|
81
|
-
throw new InstallerError(
|
|
82
|
-
`${label} install failed: ${result.stderr || result.stdout || 'unknown error'}`
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return { plan: this.planInstall(), metadata };
|
|
88
|
-
},
|
|
89
|
-
async verify() {
|
|
90
|
-
return verifyHint;
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const nativeFs = fs;
|
|
96
|
-
|
|
97
|
-
function normalizeCommandList(commands, command) {
|
|
98
|
-
if (Array.isArray(commands) && commands.length > 0) {
|
|
99
|
-
return commands;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (command) {
|
|
103
|
-
return [command];
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return [];
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function assertBinaryAvailability({ label, binaryNames, env, fsImpl }) {
|
|
110
|
-
if (!binaryNames || binaryNames.length === 0) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
for (const binaryName of binaryNames) {
|
|
115
|
-
if (commandExists(binaryName, env, fsImpl)) {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
throw new InstallerError(`${label} is not installed or not on PATH. Install ${label} first, then rerun SPARK install.`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function commandExists(commandName, env, fsImpl) {
|
|
124
|
-
const paths = String(env.PATH ?? '').split(path.delimiter).filter(Boolean);
|
|
125
|
-
const exts = process.platform === 'win32'
|
|
126
|
-
? String(env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
|
|
127
|
-
: [''];
|
|
128
|
-
|
|
129
|
-
for (const dir of paths) {
|
|
130
|
-
for (const ext of exts) {
|
|
131
|
-
const candidate = path.join(dir, `${commandName}${ext}`);
|
|
132
|
-
try {
|
|
133
|
-
fsImpl.accessSync(candidate, fs.constants.X_OK);
|
|
134
|
-
return true;
|
|
135
|
-
} catch {
|
|
136
|
-
try {
|
|
137
|
-
fsImpl.accessSync(candidate, fs.constants.F_OK);
|
|
138
|
-
return true;
|
|
139
|
-
} catch {
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function interpolateArgs(args, metadata) {
|
|
150
|
-
return args.map((arg) =>
|
|
151
|
-
String(arg)
|
|
152
|
-
.replaceAll('{targetRoot}', metadata.targetRoot ?? '')
|
|
153
|
-
.replaceAll('{relativeTargetRoot}', metadata.relativeTargetRoot ?? '')
|
|
154
|
-
.replaceAll('{marketplaceRoot}', metadata.marketplaceRoot ?? '')
|
|
155
|
-
.replaceAll('{relativeMarketplaceRoot}', metadata.relativeMarketplaceRoot ?? '')
|
|
156
|
-
.replaceAll('{marketplaceName}', metadata.marketplaceName ?? '')
|
|
157
|
-
);
|
|
158
|
-
}
|