@adityaaria/spark 6.0.9 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adityaaria/spark",
3
- "version": "6.0.9",
3
+ "version": "6.0.10",
4
4
  "description": "SPARK skills and runtime bootstrap for coding agents",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -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,
@@ -68,21 +81,46 @@ export function createAdapter({
68
81
 
69
82
  if (result.error) {
70
83
  if (result.error.code === 'ENOENT') {
71
- throw new InstallerError(
72
- `\n\u2715 ${label} install failed: Command '${entry.file}' not found.\n` +
73
- `SPARK requires ${label} to be installed first.\n` +
74
- `Please install ${label} from its official source (e.g. npm install -g <package-name>)\n` +
75
- `and ensure '${entry.file}' is in your PATH before retrying.\n`
76
- );
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;
77
92
  }
78
93
  throw new InstallerError(`${label} install failed: ${result.error.message}`);
79
94
  }
80
95
 
81
96
  if (result.status !== 0) {
97
+ if (globalCopyResult.copied) {
98
+ cliSkipped = true;
99
+ break;
100
+ }
82
101
  throw new InstallerError(
83
102
  `${label} install failed: ${result.stderr || result.stdout || 'unknown error'}`
84
103
  );
85
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.`);
86
124
  }
87
125
 
88
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',