@execuro-sw-ecosystem/sw-ecosystem-agentic-harness 0.1.4 → 0.1.6

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/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ release.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.6] - 2026-09-18
11
+
12
+ ### Changed
13
+
14
+ - `README.md` only (commit subject: "Test"). No code change.
15
+
16
+ ## [0.1.5] - 2026-09-17
17
+
18
+ ### Fixed
19
+
20
+ - **`install`/`apply` no longer write into every detected host when
21
+ `--host` is omitted.** They now resolve which host(s) to use in order:
22
+ `--host` if given; else the hosts a lock file already records; else an
23
+ interactive picker when a terminal is attached; else exit 2 naming the
24
+ four `install --host <h>` commands as `help`, instead of silently
25
+ defaulting to Claude Code, Codex, Copilot *and* Cursor. `status` and
26
+ `uninstall` keep covering every host by default, and `plan` previews all
27
+ four and says so when nothing was selected.
28
+
10
29
  ## [0.1.4] - 2026-09-17
11
30
 
12
31
  ### Added
package/README.md CHANGED
@@ -4,6 +4,8 @@ The `sw-*` skills and sub-agents for Shopware 6 projects: they turn a briefing
4
4
  into a PRD, a PRD into a technical spec, a spec into tested code, and tested
5
5
  code into a project wiki page.
6
6
 
7
+ New line
8
+
7
9
  It ships as **one npm package carrying an installer**. The installer copies the
8
10
  skills and agents into whichever coding agents your repository uses and
9
11
  registers the MCP servers and permission rules they depend on. Nothing is
@@ -49,18 +51,26 @@ declines is fully set up without them, and `--editor` on `sw-design-requirements
49
51
 
50
52
  | Command | What it does |
51
53
  | --- | --- |
52
- | `install` | installs into every detected host; the same as `apply --yes` |
54
+ | `install` | installs into the resolved host(s) (see Flags below); the same as `apply --yes` |
53
55
  | `status` | read-only; what is installed, what drifted, which optional extra components are present |
54
56
  | `plan` | the exact changes, written as a diff; writes nothing |
55
57
  | `apply --yes` | performs them |
56
58
  | `uninstall --yes` | removes what the lock file records |
57
59
  | `guide` | the install protocol, the single source of truth |
58
60
 
59
- Flags: `--host claude-code|codex|copilot|cursor` (repeatable; omitting it
60
- selects every host that is present), `--scope project|user` (default
61
- `project`), `--root <path>` (default the current directory), `--yes` (required
62
- by `apply` and `uninstall`, never prompts — `install` implies it), and
63
- `--no-extra-components` (skip detection of the optional editor packages).
61
+ Flags: `--host claude-code|codex|copilot|cursor` (repeatable), `--scope
62
+ project|user` (default `project`), `--root <path>` (default the current
63
+ directory), `--yes` (required by `apply` and `uninstall` — `install` implies
64
+ it), and `--no-extra-components` (skip detection of the optional editor
65
+ packages).
66
+
67
+ `install`/`apply` resolve which host(s) to write into, in order: `--host` if
68
+ given; else the hosts a lock file already records (a re-run needs no
69
+ question); else, with a terminal attached, an interactive picker; else exit 2
70
+ with the four `install --host <h>` commands as `help` — it never guesses and
71
+ never hangs. `status` and `uninstall` are unaffected by this and keep
72
+ covering every host by default; `plan` writes nothing, so with no `--host` and
73
+ no lock it previews all four and says so.
64
74
 
65
75
  ## Install
66
76
 
@@ -74,11 +84,14 @@ writes anything.
74
84
  Then restart your coding agent — every host reads its configuration at startup.
75
85
  The command's output tells you which ones and how.
76
86
 
77
- `apply` refuses without `--yes` and never prompts: Codex's question tool does
78
- not block outside Plan mode, and CI has no terminal. Add `--host claude-code`
79
- (repeatable) to install into one agent, and `--scope user` to install into your
80
- home directory instead of this repository. The default scope is the project —
81
- nothing is written under `~` unless you ask.
87
+ `apply` refuses without `--yes`. Neither `apply` nor `install` prompts for
88
+ consent — typing the verb, or passing `--yes`, is the consent — but with no
89
+ `--host`, no existing lock and no terminal (Codex's question tool does not
90
+ block outside Plan mode, and CI has no terminal either), it exits 2 rather
91
+ than guess. Add `--host claude-code` (repeatable) to install into one agent,
92
+ and `--scope user` to install into your home directory instead of this
93
+ repository. The default scope is the project — nothing is written under `~`
94
+ unless you ask.
82
95
 
83
96
  ### What it writes
84
97
 
package/bin/cli.mjs CHANGED
@@ -36,10 +36,15 @@ const USAGE = `usage: sw-ecosystem-agentic-harness <command> [options]
36
36
  guide print the install protocol
37
37
 
38
38
  install is apply --yes under a friendlier name: the whole install in one
39
- command, no --yes needed — typing the verb is the consent.
39
+ command, no --yes needed — typing the verb is the consent for that write;
40
+ it can still ask which host(s), see below.
40
41
 
41
42
  <h> is one of: claude-code, codex, copilot, cursor. Repeat --host to select
42
- several; omitting it selects every host that is present.
43
+ several. install/apply resolve which host(s) to use, in order: --host if
44
+ given; else the hosts already recorded in the lock file, if one exists;
45
+ else, with a terminal attached, an interactive picker; else exit 2 with
46
+ the four "... install --host <h>" commands as help. status and uninstall
47
+ keep covering every host by default.
43
48
 
44
49
  --scope defaults to project: nothing is written under your home directory
45
50
  unless you ask for it. --root defaults to the current directory.
package/lib/commands.mjs CHANGED
@@ -15,6 +15,7 @@ import { HOSTS, buildPlan, detectAll, installedCounts } from './hosts.mjs';
15
15
  import * as lockfile from './lock.mjs';
16
16
  import { dedupe, execute, isWritableState, preview, pruneDirs, revert } from './actions.mjs';
17
17
  import { probeAll } from './extra-components.mjs';
18
+ import { askHosts } from './prompt.mjs';
18
19
 
19
20
  const RUN = 'sw-ecosystem-agentic-harness';
20
21
 
@@ -65,13 +66,49 @@ export function buildContext(argv, command) {
65
66
 
66
67
  return {
67
68
  root, scope, yes, projectWiki, extraComponents,
68
- hosts: hosts.length ? [...new Set(hosts)] : [...HOSTS],
69
+ // Empty means "nothing chosen yet" — resolveHosts() applies the
70
+ // per-command fallback once the lock (and, for apply/install, a TTY)
71
+ // is available to consult.
72
+ hosts: [...new Set(hosts)],
69
73
  platform: process.platform,
70
74
  version: PKG_VERSION,
71
75
  lock: null,
72
76
  };
73
77
  }
74
78
 
79
+ /**
80
+ * The host-resolution chain every write command shares:
81
+ * 1. `--host` given → use exactly those. No question.
82
+ * 2. else a lock already records hosts → reuse them. This is an update;
83
+ * the choice was already made. No question.
84
+ * 3. else a TTY is attached (apply/install only) → ask, via lib/prompt.mjs.
85
+ * 4. else → UsageError, exit 2, `help` lists the four runnable commands.
86
+ *
87
+ * `status` and `uninstall` keep their old, ungated default of every host —
88
+ * reporting and removal are not consent points. `plan` writes nothing, so it
89
+ * never reaches step 3: steps 1-2, then it previews all four and says so.
90
+ *
91
+ * Call after `loadLock(ctx)` so `ctx.lock` is populated for step 2.
92
+ */
93
+ async function resolveHosts(ctx, command) {
94
+ if (ctx.hosts.length) return { hosts: [...new Set(ctx.hosts)], chosen: true };
95
+ if (command === 'status' || command === 'uninstall') return { hosts: [...HOSTS], chosen: true };
96
+
97
+ const lockHosts = ctx.lock?.hosts ? Object.keys(ctx.lock.hosts) : [];
98
+ if (lockHosts.length) return { hosts: lockHosts, chosen: true };
99
+
100
+ if (command === 'plan') return { hosts: [...HOSTS], chosen: false };
101
+
102
+ if (process.stdin.isTTY) {
103
+ const chosen = await askHosts(HOSTS);
104
+ if (chosen) return { hosts: chosen, chosen: true };
105
+ }
106
+ throw new UsageError(
107
+ 'no --host given, this repository has no recorded install, and there is no terminal to ask which host(s) to use',
108
+ HOSTS.map((h) => `${RUN} ${command} --host ${h}`),
109
+ );
110
+ }
111
+
75
112
  function head(ctx, extra = {}) {
76
113
  return {
77
114
  version: PKG_VERSION, content_version: CONTENT_VERSION,
@@ -138,6 +175,7 @@ export async function status(argv) {
138
175
  }
139
176
  throw e;
140
177
  }
178
+ ({ hosts: ctx.hosts } = await resolveHosts(ctx, 'status'));
141
179
 
142
180
  const detections = await detectAll(ctx);
143
181
  const { actions, errors } = previewAll(ctx);
@@ -213,11 +251,14 @@ export async function plan(argv) {
213
251
  }
214
252
  throw e;
215
253
  }
254
+ const { hosts, chosen } = await resolveHosts(ctx, 'plan');
255
+ ctx.hosts = hosts;
216
256
  const { actions, errors } = previewAll(ctx);
217
257
  const summary = summarise(actions);
218
258
  const changes = summary.create + summary.update;
219
259
 
220
- let next = changes
260
+ let next = chosen ? '' : 'No --host given and no existing install found, so this previews all four hosts. ';
261
+ next += changes
221
262
  ? `Nothing was written. Run \`${RUN} apply --yes\` to perform the ${changes} change(s).`
222
263
  + (summary.conflict || summary.manual
223
264
  ? ` The ${summary.conflict} conflict(s) and ${summary.manual} manual step(s) will be reported again and left to you.`
@@ -262,6 +303,7 @@ export async function apply(argv, label = 'apply') {
262
303
  }
263
304
  throw e;
264
305
  }
306
+ ({ hosts: ctx.hosts } = await resolveHosts(ctx, label));
265
307
 
266
308
  // An apply we cannot record is an apply we cannot undo. Refuse up front
267
309
  // rather than leave the user with files and no way back.
@@ -370,6 +412,7 @@ export async function uninstall(argv) {
370
412
  }
371
413
  throw e;
372
414
  }
415
+ ({ hosts: ctx.hosts } = await resolveHosts(ctx, 'uninstall'));
373
416
 
374
417
  if (!lock) {
375
418
  // No record means no evidence of what we own. Guessing here would delete
package/lib/guide.mjs CHANGED
@@ -20,9 +20,14 @@ registers the MCP servers and permission rules they depend on.
20
20
  ## The loop
21
21
 
22
22
  1. \`${RUN} install\`
23
- The whole install in one command. It refuses to prompt — Codex's question
24
- tool does not block outside Plan mode, and CI has no terminal — so the verb
25
- itself is the consent. Same as \`apply --yes\`.
23
+ The whole install in one command. It never prompts for consent — typing
24
+ the verb is the consent — but it does still need to know **which** host(s)
25
+ to write into, resolved in order: \`--host\`, if given; else the hosts a
26
+ lock file already records, if this is a re-run; else, with a real
27
+ terminal attached, an interactive picker; else it exits 2 with the four
28
+ \`${RUN} install --host <h>\` commands as \`help\` rather than guess or
29
+ hang — Codex's question tool does not block outside Plan mode, and CI has
30
+ no terminal, so a skill invoking this must always pass \`--host\`.
26
31
 
27
32
  2. \`${RUN} status\`
28
33
  Read-only, optional. Reports which hosts are present, what is already
@@ -34,16 +39,19 @@ registers the MCP servers and permission rules they depend on.
34
39
 
35
40
  4. \`${RUN} apply --yes\`
36
41
  The scriptable name for the same write \`install\` performs — use it for
37
- re-runs and repairs. It refuses without \`--yes\`; there is no prompt, because
38
- Codex's question tool does not block outside Plan mode and CI has no
39
- terminal.
42
+ re-runs and repairs. It refuses without \`--yes\`, and resolves which
43
+ host(s) to touch the same way \`install\` does (see step 1).
40
44
 
41
45
  5. \`${RUN} uninstall --yes\`
42
46
  Removes what the lock file records and reverts the managed config blocks.
43
47
 
44
48
  ## Flags
45
49
 
46
- --host claude-code|codex|copilot|cursor restrict to one host, repeatable
50
+ --host claude-code|codex|copilot|cursor select one host, repeatable —
51
+ see the host-resolution order in
52
+ step 1 above; a skill invoking
53
+ \`install\`/\`apply\` must always
54
+ pass it, since it has no terminal
47
55
  --scope project|user default: project
48
56
  --root <path> default: the current directory
49
57
  --no-extra-components skip the optional extra-component skills
@@ -68,8 +76,9 @@ The exact command is also in a \`skipped\` action's \`remedy\`, and in
68
76
  - **stdout is exactly one JSON object.** Every successful result carries a
69
77
  \`next_step\` string: the literal command to run next. Errors carry a
70
78
  \`help\` array of runnable commands.
71
- - **Exit codes:** 0 success, 2 usage error (your invocation was wrong),
72
- 1 the work could not be done.
79
+ - **Exit codes:** 0 success, 2 usage error (your invocation was wrong,
80
+ including \`install\`/\`apply\` run with no \`--host\`, no existing lock and
81
+ no terminal to ask on), 1 the work could not be done.
73
82
  - **Nothing is silently overwritten.** A file we did not install, or one you
74
83
  edited after we installed it, is reported as \`conflict\` or \`drift\` and
75
84
  left exactly as it is. Its \`remedy\` field tells the user what to do.
package/lib/hosts.mjs CHANGED
@@ -2,9 +2,10 @@
2
2
  // so adding a host or following a schema change is one table to edit.
3
3
  //
4
4
  // Detection is marker-directory existence only — no network, no process scan,
5
- // no `which`. It is allowed to be wrong in the harmless direction: a host that
6
- // is installed but has no marker directory simply is not written to, and
7
- // `--host` forces it.
5
+ // no `which`. It feeds `status` reporting only; it never selects which hosts
6
+ // a write command touches. Which hosts get written to is decided by
7
+ // commands.mjs's `resolveHosts()` chain (`--host`, then a recorded lock,
8
+ // then a TTY prompt, then a usage error) — detection has no vote there.
8
9
 
9
10
  import { homedir } from 'node:os';
10
11
  import { join } from 'node:path';
package/lib/prompt.mjs ADDED
@@ -0,0 +1,72 @@
1
+ // The interactive host picker. Used only when `apply`/`install` reach step 3
2
+ // of the host-resolution chain: no `--host`, no lock to reuse, but a real
3
+ // terminal to ask on.
4
+ //
5
+ // node:readline only — the package has zero dependencies and that must not
6
+ // change. Every prompt and echo goes to STDERR, never stdout: stdout carries
7
+ // exactly one JSON object per run and every skill parses it, so a menu on
8
+ // stdout would corrupt that contract. This module never inspects
9
+ // `process.stdin.isTTY` itself — the caller decides whether to invoke it.
10
+
11
+ import { createInterface } from 'node:readline';
12
+
13
+ const LABELS = {
14
+ 'claude-code': 'Claude Code',
15
+ codex: 'Codex',
16
+ copilot: 'GitHub Copilot',
17
+ cursor: 'Cursor',
18
+ };
19
+
20
+ /**
21
+ * Pure parsing, no I/O — so it can be unit-tested without a TTY.
22
+ *
23
+ * Accepts comma-separated 1-based indexes into `hosts` (e.g. "1,3"), the
24
+ * word "all", or an empty string — both of the latter two mean every host,
25
+ * made explicit rather than left as a silent default. Throws a short,
26
+ * human-readable `Error` on anything else (out of range, not a number,
27
+ * nothing at all).
28
+ */
29
+ export function parseSelection(input, hosts) {
30
+ const trimmed = (input ?? '').trim();
31
+ if (trimmed === '' || trimmed.toLowerCase() === 'all') return [...hosts];
32
+ const parts = trimmed.split(',').map((p) => p.trim()).filter(Boolean);
33
+ if (!parts.length) throw new Error('no selection given');
34
+ const chosen = [];
35
+ for (const part of parts) {
36
+ if (!/^\d+$/.test(part)) throw new Error(`not a number: "${part}"`);
37
+ const n = Number(part);
38
+ if (n < 1 || n > hosts.length) throw new Error(`out of range 1-${hosts.length}: ${part}`);
39
+ const id = hosts[n - 1];
40
+ if (!chosen.includes(id)) chosen.push(id);
41
+ }
42
+ return chosen;
43
+ }
44
+
45
+ function question(rl, text) {
46
+ return new Promise((resolve) => rl.question(text, resolve));
47
+ }
48
+
49
+ /**
50
+ * Ask which host(s) to install into. Re-asks once on unparseable input, then
51
+ * gives up and returns `null` — the caller turns that into the same exit-2
52
+ * usage error the no-TTY path uses, so the output contract stays one shape.
53
+ */
54
+ export async function askHosts(hosts) {
55
+ const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
56
+ const menu = hosts.map((h, i) => ` ${i + 1}. ${LABELS[h] ?? h}`).join('\n');
57
+ const prompt = `Which coding agent(s) should this install into?\n${menu}\n`
58
+ + 'Enter comma-separated numbers (e.g. "1,3"), "all", or press Enter for all: ';
59
+ try {
60
+ for (let attempt = 0; attempt < 2; attempt += 1) {
61
+ const answer = await question(rl, attempt === 0 ? prompt : 'Not understood — try again: ');
62
+ try {
63
+ return parseSelection(answer, hosts);
64
+ } catch {
65
+ // Re-ask once; the loop's second pass falls through to null below.
66
+ }
67
+ }
68
+ return null;
69
+ } finally {
70
+ rl.close();
71
+ }
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@execuro-sw-ecosystem/sw-ecosystem-agentic-harness",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Shopware 6 agentic harness: sw-* skills and sub-agents, plus an installer that writes them into Claude Code, Codex, GitHub Copilot and Cursor.",
5
5
  "license": "MIT",
6
6
  "author": "Execuro UG (haftungsbeschränkt)",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "sw-ecosystem-agentic-harness",
4
- "version": "0.1.4",
4
+ "version": "0.1.6",
5
5
  "description": "Shopware 6 agentic harness: sw-* skills and sub-agents for requirements, specs, implementation, verification and documentation, with an installer CLI that writes them into Claude Code, Codex, GitHub Copilot and Cursor. The knowledge-base MCP and the editor tools are separate npm packages.",
6
6
  "author": {
7
7
  "name": "Execuro UG (haftungsbeschränkt)",
@@ -6,6 +6,16 @@ unticked. Host configuration files (`.mcp.json` entries, `.gitignore` lines,
6
6
  permission grants) are not rows here — the installer CLI owns them and
7
7
  reports their state in its own `status`/`plan` output; see `SKILL.md`.
8
8
 
9
+ When that `status` call reports no install at all (no lock file, so no
10
+ recorded hosts), the installer CLI's `apply --yes`/`install` has no `--host`
11
+ to reuse and, run from this skill, no terminal to ask on — it would exit 2.
12
+ Do not pass a guessed `--host`. Report this to the user instead: the first
13
+ install must be run by them directly in a terminal, e.g.
14
+ `npx -y @execuro-sw-ecosystem/sw-ecosystem-agentic-harness@latest install`,
15
+ which then either takes their `--host` or prompts them interactively. Re-run
16
+ this skill's `status` check afterwards — every later `apply --yes` reuses the
17
+ hosts that first run recorded, and this skill can call it directly again.
18
+
9
19
  ## 1. vendor/
10
20
 
11
21
  - **Check:** `vendor/shopware/core` exists, `composer.lock` exists,