@adityaaria/spark 6.0.3 → 6.0.4

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 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 detects the harness or asks you which one you are using, run:
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
@@ -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 detects the harness or asks which one to target, then delegates to the
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adityaaria/spark",
3
- "version": "6.0.3",
3
+ "version": "6.0.4",
4
4
  "description": "SPARK skills and runtime bootstrap for coding agents",
5
5
  "type": "module",
6
6
  "publishConfig": {
package/src/cli/output.js CHANGED
@@ -7,5 +7,7 @@ export function printHelp() {
7
7
  printLine('');
8
8
  printLine('Usage: npx @adityaaria/spark install [--harness <name>] [--dry-run] [--yes] [--verbose]');
9
9
  printLine('');
10
+ printLine('Without --harness, the installer will ask which AI assistance to target.');
11
+ printLine('');
10
12
  printLine('Supported harnesses: Codex, Cursor, Antigravity, Copilot, OpenCode, Gemini, Pi');
11
13
  }
@@ -4,9 +4,6 @@ import path from 'node:path';
4
4
  import { getAdapterById, listAdapters } from './registry.js';
5
5
  import { InstallerError } from './errors.js';
6
6
 
7
- const HIGH_CONFIDENCE_SCORE = 80;
8
- const AMBIGUOUS_MARGIN = 20;
9
-
10
7
  export function detectHarnessCandidates({ env = process.env, fsImpl = fs } = {}) {
11
8
  const homeDir = os.homedir();
12
9
  const adapters = listAdapters();
@@ -31,25 +28,22 @@ export async function chooseHarness({
31
28
  return { adapter, source: 'forced', candidates: [] };
32
29
  }
33
30
 
34
- const candidates = detectHarnessCandidates({ env, fsImpl: fs });
35
- if (candidates.length === 0) {
36
- throw new InstallerError(
37
- `Could not detect a supported harness. Supported harnesses: ${listAdapters().map((adapter) => adapter.label).join(', ')}`
38
- );
31
+ if (!prompt) {
32
+ throw new InstallerError('No harness selected. Re-run with --harness or use an interactive terminal.');
39
33
  }
40
34
 
41
- if (isConfident(candidates)) {
42
- const adapter = getAdapterById(candidates[0].id);
43
- return { adapter, source: 'auto', candidates };
44
- }
35
+ const detectedCandidates = detectHarnessCandidates({ env, fsImpl: fs });
36
+ const candidates = buildPromptCandidates(detectedCandidates);
45
37
 
46
- if (!prompt) {
47
- throw new InstallerError(`Multiple harnesses look plausible: ${candidates.map((candidate) => candidate.label).join(', ')}`);
38
+ let validationMessage = null;
39
+ while (true) {
40
+ const answer = await prompt(renderPrompt(candidates, validationMessage));
41
+ const adapter = resolvePromptAnswer(answer, candidates);
42
+ if (adapter) {
43
+ return { adapter, source: 'prompt', candidates };
44
+ }
45
+ validationMessage = `Unsupported choice: ${answer || '(empty)'}. Please choose a number or harness name from the list.`;
48
46
  }
49
-
50
- const answer = await prompt(renderPrompt(candidates));
51
- const adapter = resolvePromptAnswer(answer, candidates);
52
- return { adapter, source: 'prompt', candidates };
53
47
  }
54
48
 
55
49
  function scoreAdapter(adapter, env, fsImpl, homeDir) {
@@ -94,12 +88,16 @@ function candidateConfigPaths(homeDir, env, configPath) {
94
88
  paths.push(path.join(env.OPENCODE_CONFIG_DIR, 'opencode.json'));
95
89
  }
96
90
  if (configPath === 'GEMINI.md') {
97
- paths.push(path.join(homeDir, '.gemini', 'GEMINI.md'));
98
- paths.push(path.join(homeDir, 'GEMINI.md'));
91
+ paths.push(
92
+ path.join(homeDir, '.gemini', 'GEMINI.md'),
93
+ path.join(homeDir, 'GEMINI.md')
94
+ );
99
95
  }
100
96
  if (configPath === 'gemini-extension.json') {
101
- paths.push(path.join(homeDir, 'gemini-extension.json'));
102
- paths.push(path.join(homeDir, '.gemini', 'gemini-extension.json'));
97
+ paths.push(
98
+ path.join(homeDir, 'gemini-extension.json'),
99
+ path.join(homeDir, '.gemini', 'gemini-extension.json')
100
+ );
103
101
  }
104
102
  return paths;
105
103
  }
@@ -130,15 +128,27 @@ function commandExists(commandName, env, fsImpl) {
130
128
  return false;
131
129
  }
132
130
 
133
- function isConfident(candidates) {
134
- if (candidates[0].score < HIGH_CONFIDENCE_SCORE) return false;
135
- if (candidates.length === 1) return true;
136
- return candidates[0].score - candidates[1].score >= AMBIGUOUS_MARGIN;
131
+ function buildPromptCandidates(detectedCandidates) {
132
+ const allAdapters = listAdapters();
133
+ const detectedIds = new Set(detectedCandidates.map((candidate) => candidate.id));
134
+
135
+ return [
136
+ ...detectedCandidates,
137
+ ...allAdapters
138
+ .filter((adapter) => !detectedIds.has(adapter.id))
139
+ .map((adapter) => ({
140
+ id: adapter.id,
141
+ label: adapter.label,
142
+ score: 0,
143
+ reasons: [],
144
+ })),
145
+ ];
137
146
  }
138
147
 
139
- function renderPrompt(candidates) {
148
+ function renderPrompt(candidates, validationMessage = null) {
140
149
  return [
141
- 'Choose the harness to install SPARK for:',
150
+ ...(validationMessage ? [validationMessage] : []),
151
+ 'Install SPARK for which AI assistance?',
142
152
  ...candidates.map((candidate, index) => `${index + 1}. ${candidate.label} (${candidate.id})`),
143
153
  'Enter a number or harness name:',
144
154
  ].join('\n');
@@ -146,9 +156,7 @@ function renderPrompt(candidates) {
146
156
 
147
157
  function resolvePromptAnswer(answer, candidates) {
148
158
  const value = normalizeHarness(answer);
149
- if (!value) {
150
- throw new InstallerError('No harness selected.');
151
- }
159
+ if (!value) return null;
152
160
 
153
161
  const numeric = Number(value);
154
162
  if (Number.isInteger(numeric) && numeric >= 1 && numeric <= candidates.length) {
@@ -156,9 +164,7 @@ function resolvePromptAnswer(answer, candidates) {
156
164
  }
157
165
 
158
166
  const exact = candidates.find((candidate) => candidate.id === value || candidate.label.toLowerCase() === value);
159
- if (!exact) {
160
- throw new InstallerError(`Unsupported harness choice: ${answer}`);
161
- }
167
+ if (!exact) return null;
162
168
 
163
169
  return getAdapterById(exact.id);
164
170
  }