@execuro-sw-ecosystem/sw-ecosystem-agentic-harness 0.1.7 → 0.1.8
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 +52 -0
- package/README.md +33 -17
- package/SECURITY.md +1 -1
- package/bin/cli.mjs +7 -5
- package/lib/actions.mjs +1 -45
- package/lib/commands.mjs +43 -45
- package/lib/content.mjs +31 -1
- package/lib/guide.mjs +9 -16
- package/lib/hosts.mjs +36 -34
- package/lib/lock.mjs +41 -9
- package/lib/render.mjs +10 -9
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/sw-design-requirements/SKILL.md +1 -1
- package/skills/sw-design-requirements/reference/editor-gate.md +1 -1
- package/skills/sw-design-requirements/reference/editor-mode.md +2 -2
- package/skills/sw-design-solution/SKILL.md +1 -1
- package/skills/sw-design-solution/reference/editor-mode.md +2 -2
- package/skills/sw-discover-tender/SKILL.md +2 -2
- package/skills/sw-discover-tender/reference/editor-session.md +3 -3
- package/skills/sw-discover-tender/reference/procedure.md +2 -2
- package/skills/sw-setup/SKILL.md +5 -5
- package/skills/sw-setup/reference/rows.md +105 -68
- package/lib/extra-components.mjs +0 -117
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,58 @@ release.
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.8] - 2026-09-19
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **The lock file moved to `var/sw-ai-sdk/harness.lock.json`, and the
|
|
15
|
+
`.gitignore` line is gone with it.** A Shopware project ignores the whole of
|
|
16
|
+
`var/` through the `shopware/core` Flex recipe (`/var/*`, with a single
|
|
17
|
+
`!/var/.htaccess` negation) and already keeps its machine-local state there
|
|
18
|
+
— `var/cache`, `var/log` — so the installer no longer creates a directory in
|
|
19
|
+
the repository root and no longer writes to `.gitignore` at all. A root with
|
|
20
|
+
no `var/` (`--scope user` under $HOME, or a checkout that is not a Shopware
|
|
21
|
+
project) keeps `.sw-ai-sdk/` and its managed line, unchanged. Nothing here
|
|
22
|
+
reads a path outside the install root, on any platform.
|
|
23
|
+
**Migration is automatic and lossless:** a lock still at the old path is
|
|
24
|
+
read, rewritten to the new one, and the old file, its directory and the
|
|
25
|
+
managed `.gitignore` block are removed. Declined rules, recorded agents and
|
|
26
|
+
file hashes all survive, so the first run after upgrading reinstalls nothing
|
|
27
|
+
— it reports `migrated[]` in `--json` and one line in the human summary.
|
|
28
|
+
`lock_file` in every result body now carries the resolved path; anything
|
|
29
|
+
that hard-coded `.sw-ai-sdk/` should read that key instead.
|
|
30
|
+
|
|
31
|
+
### Removed
|
|
32
|
+
|
|
33
|
+
- **BREAKING: the "extra component" mechanism is gone.** The CLI no longer
|
|
34
|
+
detects, installs, updates or removes the two optional visual editors
|
|
35
|
+
(Specs Editor, Tender Discovery Tool); each package installs its own skill,
|
|
36
|
+
orchestrated by the `sw-setup` skill. Removed with it: the
|
|
37
|
+
`extra_components[]` key from `status --json`, the `--no-extra-components`
|
|
38
|
+
flag on `install`/`plan`/`apply`, the `skipped` field from `plan`'s and
|
|
39
|
+
`apply`'s `summary` (nothing else could produce that state), the
|
|
40
|
+
`skill-copy` action kind, and the "Optional extra components" section of
|
|
41
|
+
`guide`. Anything reading `extra_components[]` or counting `summary.skipped`
|
|
42
|
+
must stop.
|
|
43
|
+
**Why:** the mechanism had never once worked. It spawned `npx --no-install
|
|
44
|
+
<pkg>@<pin> install-skill --print` and required stdout to begin with `---`,
|
|
45
|
+
but both editors document a `source:`/`next_step:` preamble before the skill
|
|
46
|
+
document, so the check could never pass and neither skill was ever installed.
|
|
47
|
+
`--no-install` also suppresses installing but not resolving, so every
|
|
48
|
+
`status`, `plan` and `apply` issued a real request to the npm registry —
|
|
49
|
+
`status` was never offline-safe, contrary to what the module claimed.
|
|
50
|
+
- The two editors are now invoked as `@latest` everywhere, including in the
|
|
51
|
+
`allowed-tools` permission grants of `sw-design-requirements`,
|
|
52
|
+
`sw-design-solution` and `sw-discover-tender`, which still named an exact
|
|
53
|
+
version and would therefore have denied the calls. The `@latest` check now
|
|
54
|
+
guards `allowed-tools` lines as well as prose.
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- `status --json` reports `skills_dir` per agent — the directory that agent
|
|
59
|
+
actually reads skills from, and the `--target` `sw-setup` hands to an
|
|
60
|
+
editor package's own `install-skill`/`uninstall-skill`.
|
|
61
|
+
|
|
10
62
|
## [0.1.7] - 2026-09-19
|
|
11
63
|
|
|
12
64
|
### Changed
|
package/README.md
CHANGED
|
@@ -43,16 +43,20 @@ identity, not an npm package name — and can be run standalone in any stdio MCP
|
|
|
43
43
|
client without this harness.
|
|
44
44
|
|
|
45
45
|
The two editors are **optional add-ons**, and each ships its own skill inside
|
|
46
|
-
its package rather than here.
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
its package rather than here. They are not this CLI's business at all: it
|
|
47
|
+
neither detects nor installs them. `sw-setup` owns their whole lifecycle —
|
|
48
|
+
it offers them, and installs, updates and removes each one with that
|
|
49
|
+
package's own `install-skill`/`uninstall-skill --target <skills dir>`. A
|
|
50
|
+
project that declines is fully set up without them, and `--editor` on
|
|
51
|
+
`sw-design-requirements`, `sw-design-solution` or `sw-discover-tender` then
|
|
52
|
+
stops with one line saying so.
|
|
49
53
|
|
|
50
54
|
## Commands
|
|
51
55
|
|
|
52
56
|
| Command | What it does |
|
|
53
57
|
| --- | --- |
|
|
54
58
|
| `install` | installs into the resolved coding agent(s) (see Flags below); the same as `apply --yes` |
|
|
55
|
-
| `status` | read-only; what is installed, what drifted,
|
|
59
|
+
| `status` | read-only; what is installed, what drifted, and each agent's `skills_dir` |
|
|
56
60
|
| `plan` | the exact changes, written as a diff; writes nothing |
|
|
57
61
|
| `apply --yes` | performs them |
|
|
58
62
|
| `uninstall --yes` | removes what the lock file records |
|
|
@@ -61,8 +65,7 @@ declines is fully set up without them, and `--editor` on `sw-design-requirements
|
|
|
61
65
|
Flags: `--agent claude-code|codex|copilot|cursor` (repeatable), `--scope
|
|
62
66
|
project|user` (default `project`), `--root <path>` (default the current
|
|
63
67
|
directory), `--yes` (required by `apply` and `uninstall` — `install` implies
|
|
64
|
-
it), `--
|
|
65
|
-
packages), `--json` and `--verbose` (see Output below).
|
|
68
|
+
it), `--json` and `--verbose` (see Output below).
|
|
66
69
|
|
|
67
70
|
`install`/`apply` resolve which coding agent(s) to write into, in order:
|
|
68
71
|
`--agent` if given; else the agents a lock file already records (a re-run
|
|
@@ -118,6 +121,17 @@ unless you ask.
|
|
|
118
121
|
`AGENTS.md` gets a managed block too — Codex, Copilot and Cursor read it, and
|
|
119
122
|
Codex never reads `CLAUDE.md`.
|
|
120
123
|
|
|
124
|
+
The installer's own state — the lock file recording what it wrote — goes to
|
|
125
|
+
`var/sw-ai-sdk/harness.lock.json`. A Shopware project already ignores the whole
|
|
126
|
+
of `var/` through the `shopware/core` Flex recipe (`/var/*`) and already keeps
|
|
127
|
+
its machine-local state there, so the repository root stays clean and **no
|
|
128
|
+
`.gitignore` line is written at all**. A root with no `var/` — `--scope user`
|
|
129
|
+
under your home directory, or a checkout that is not a Shopware project —
|
|
130
|
+
falls back to `.sw-ai-sdk/` and gets the managed `.gitignore` line as before.
|
|
131
|
+
An install recorded at the old path is moved on the next `install`, which also
|
|
132
|
+
takes back the `.gitignore` line it no longer needs; nothing is reinstalled and
|
|
133
|
+
no declined rule is forgotten.
|
|
134
|
+
|
|
121
135
|
### What it will not do
|
|
122
136
|
|
|
123
137
|
- **It never overwrites a file it did not install.** A file already at one of
|
|
@@ -137,7 +151,7 @@ Undo everything with:
|
|
|
137
151
|
npx -y @execuro-sw-ecosystem/sw-ecosystem-agentic-harness@latest uninstall --yes
|
|
138
152
|
```
|
|
139
153
|
|
|
140
|
-
It removes exactly what the lock file at
|
|
154
|
+
It removes exactly what the lock file at `var/sw-ai-sdk/harness.lock.json` records,
|
|
141
155
|
keeps anything you hand-edited, and tells you what it kept.
|
|
142
156
|
|
|
143
157
|
### From a skill
|
|
@@ -160,16 +174,16 @@ Three things update on different schedules.
|
|
|
160
174
|
| --- | --- |
|
|
161
175
|
| This package — skills, sub-agents, permission rules, MCP registrations | `npx -y @execuro-sw-ecosystem/sw-ecosystem-agentic-harness@latest install` |
|
|
162
176
|
| The MCP servers (`ShopwareDevKnowledgeBase`, `playwright`) | By themselves. They are registered as `@latest`, so your agent fetches the newest build the next time it starts one. Nothing to run. |
|
|
163
|
-
| Specs Editor, Tender Discovery Tool |
|
|
177
|
+
| Specs Editor, Tender Discovery Tool | `sw-setup` does, by re-running each package's own `install-skill` at `@latest` — see below. |
|
|
164
178
|
|
|
165
179
|
### The optional editors
|
|
166
180
|
|
|
167
|
-
|
|
168
|
-
|
|
181
|
+
They are installed, updated and removed by the `sw-setup` skill, not by this
|
|
182
|
+
CLI, and there is no version to match against this package:
|
|
169
183
|
|
|
170
184
|
```bash
|
|
171
|
-
|
|
172
|
-
|
|
185
|
+
npx -y @execuro-sw-ecosystem/sw-specs-editor@latest install-skill --target <skills dir>
|
|
186
|
+
npx -y @execuro-sw-ecosystem/sw-tender-discovery-tool@latest install-skill --target <skills dir>
|
|
173
187
|
```
|
|
174
188
|
|
|
175
189
|
- `sw-specs-editor` backs the `--editor` flag on `sw-design-requirements` and
|
|
@@ -177,9 +191,11 @@ npm i -D @execuro-sw-ecosystem/sw-tender-discovery-tool@0.1.0
|
|
|
177
191
|
- `sw-tender-discovery-tool` backs the `--editor` flag and the `.xlsx` import
|
|
178
192
|
on `sw-discover-tender`.
|
|
179
193
|
|
|
180
|
-
|
|
181
|
-
`
|
|
182
|
-
|
|
194
|
+
`<skills dir>` is the `skills_dir` this CLI reports for that agent in
|
|
195
|
+
`status --json`. Re-running the same line is the update — `@latest` plus the
|
|
196
|
+
editor's own "already current" path makes it idempotent — and
|
|
197
|
+
`uninstall-skill` with the same `--target` removes it. Nothing needs to be
|
|
198
|
+
re-run here afterwards.
|
|
183
199
|
|
|
184
200
|
Re-running `install` is safe at any time: it rewrites only what it owns,
|
|
185
201
|
leaves anything you hand-edited alone (reported as drift), and records the new
|
|
@@ -192,8 +208,8 @@ in this project's `node_modules`, so you would silently keep running the
|
|
|
192
208
|
version you first installed.
|
|
193
209
|
|
|
194
210
|
To look before you change anything: `... @latest status` reports
|
|
195
|
-
`installed_version`, any drift, and
|
|
196
|
-
|
|
211
|
+
`installed_version`, any drift, and each agent's `skills_dir`. It writes
|
|
212
|
+
nothing.
|
|
197
213
|
|
|
198
214
|
## Prerequisites
|
|
199
215
|
|
package/SECURITY.md
CHANGED
|
@@ -41,7 +41,7 @@ scripts** on install, which removes a large part of the usual npm attack
|
|
|
41
41
|
surface.
|
|
42
42
|
|
|
43
43
|
Out of scope: vulnerabilities in the coding agents themselves, in the
|
|
44
|
-
separately published
|
|
44
|
+
separately published visual editors (they have their own repositories
|
|
45
45
|
and their own reporting), or in a third-party MCP server this package merely
|
|
46
46
|
registers, such as Playwright's (report those upstream).
|
|
47
47
|
|
package/bin/cli.mjs
CHANGED
|
@@ -28,10 +28,10 @@ const COMMANDS = {
|
|
|
28
28
|
|
|
29
29
|
const USAGE = `usage: sw-ecosystem-agentic-harness <command> [options]
|
|
30
30
|
|
|
31
|
-
install [--agent <a>] [--scope project|user] [--root <path>]
|
|
31
|
+
install [--agent <a>] [--scope project|user] [--root <path>]
|
|
32
32
|
status [--agent <a>] [--scope project|user] [--root <path>]
|
|
33
|
-
plan [--agent <a>] [--scope project|user] [--root <path>]
|
|
34
|
-
apply --yes [--agent <a>] [--scope project|user] [--root <path>]
|
|
33
|
+
plan [--agent <a>] [--scope project|user] [--root <path>]
|
|
34
|
+
apply --yes [--agent <a>] [--scope project|user] [--root <path>]
|
|
35
35
|
uninstall --yes [--agent <a>] [--scope project|user] [--root <path>]
|
|
36
36
|
guide print the install protocol
|
|
37
37
|
|
|
@@ -49,8 +49,10 @@ const USAGE = `usage: sw-ecosystem-agentic-harness <command> [options]
|
|
|
49
49
|
--scope defaults to project: nothing is written under your home directory
|
|
50
50
|
unless you ask for it. --root defaults to the current directory.
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
The two optional visual editors (Specs Editor, Tender Discovery Tool) are
|
|
53
|
+
not this CLI's business: each ships its own skill inside its own npm
|
|
54
|
+
package, and the sw-setup skill offers, installs, updates and removes
|
|
55
|
+
them.
|
|
54
56
|
|
|
55
57
|
--json print the result object instead of the human summary
|
|
56
58
|
--verbose include the per-file detail --json carries
|
package/lib/actions.mjs
CHANGED
|
@@ -20,13 +20,11 @@ import {
|
|
|
20
20
|
import { PKG_ROOT } from './content.mjs';
|
|
21
21
|
import { isDeclined } from './lock.mjs';
|
|
22
22
|
import { readFileSync } from 'node:fs';
|
|
23
|
-
import { EXTRA_COMPONENTS, probe } from './extra-components.mjs';
|
|
24
23
|
|
|
25
24
|
/** Preview one action against the filesystem. Reads only. */
|
|
26
25
|
export function preview(action, ctx) {
|
|
27
26
|
switch (action.kind) {
|
|
28
27
|
case 'file-copy': return previewFile(action, ctx);
|
|
29
|
-
case 'skill-copy': return previewSkill(action, ctx);
|
|
30
28
|
case 'json-merge': return previewJson(action, ctx);
|
|
31
29
|
case 'toml-fence':
|
|
32
30
|
case 'text-fence': return previewFence(action, ctx);
|
|
@@ -42,8 +40,7 @@ function previewFile(action, ctx) {
|
|
|
42
40
|
return previewBytes(action, ctx, shipped);
|
|
43
41
|
}
|
|
44
42
|
|
|
45
|
-
/** The adopt/conflict/drift/update ladder
|
|
46
|
-
* probed body instead of a shipped one, `skill-copy`. */
|
|
43
|
+
/** The adopt/conflict/drift/update ladder every byte-for-byte target walks. */
|
|
47
44
|
function previewBytes(action, ctx, bytes) {
|
|
48
45
|
const hash = sha256(bytes);
|
|
49
46
|
const target = abs(ctx.root, action.target);
|
|
@@ -74,35 +71,6 @@ function previewBytes(action, ctx, bytes) {
|
|
|
74
71
|
detail: 'shipped content changed' };
|
|
75
72
|
}
|
|
76
73
|
|
|
77
|
-
// ------------------------------------------------------------ skill-copy
|
|
78
|
-
|
|
79
|
-
/** Strip the probed body (a Buffer) before it lands anywhere near the
|
|
80
|
-
* action, which `commands.mjs` serialises straight to stdout. */
|
|
81
|
-
function stripProbeBody({ body, ...rest }) {
|
|
82
|
-
return rest;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function previewSkill(action, ctx) {
|
|
86
|
-
const component = EXTRA_COMPONENTS.find((c) => c.id === action.component);
|
|
87
|
-
const probed = probe(component, { platform: ctx.platform });
|
|
88
|
-
const probeSummary = stripProbeBody(probed);
|
|
89
|
-
|
|
90
|
-
if (!probed.available) {
|
|
91
|
-
const remedy = probed.state === 'absent'
|
|
92
|
-
? `npm i -D ${probed.spec}`
|
|
93
|
-
: 'check that npx works — probing needs network-free npx --no-install';
|
|
94
|
-
return {
|
|
95
|
-
...action, state: 'skipped', available: false,
|
|
96
|
-
probe: probeSummary, reason: probed.reason, remedy,
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
...previewBytes(action, ctx, probed.body),
|
|
102
|
-
available: true, probe: probeSummary, body: probed.body,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
74
|
// ------------------------------------------------------------- json-merge
|
|
107
75
|
|
|
108
76
|
function previewJson(action, ctx) {
|
|
@@ -275,18 +243,6 @@ export function execute(action, ctx) {
|
|
|
275
243
|
return { kind: 'file-copy', host: action.host, target: action.target, source: action.source,
|
|
276
244
|
hash: action.hash, bytes: action.bytes, created_file: action.created_file !== false, dirs };
|
|
277
245
|
}
|
|
278
|
-
case 'skill-copy': {
|
|
279
|
-
// Collapses to a `file-copy` lock entry on purpose: `lock.record`,
|
|
280
|
-
// `lock.entries`, `revert` and `rebuild` need no extra-component-specific
|
|
281
|
-
// case, and `uninstall` needs no probe. `source` is provenance only —
|
|
282
|
-
// `revert` never reads it.
|
|
283
|
-
ensureDir(dirname(target));
|
|
284
|
-
writeAtomic(target, action.body);
|
|
285
|
-
const dirs = ancestorDirs(ctx.root, action.target);
|
|
286
|
-
return { kind: 'file-copy', host: action.host, target: action.target,
|
|
287
|
-
source: `npm:${action.package}#install-skill --print`,
|
|
288
|
-
hash: action.hash, bytes: action.bytes, created_file: action.created_file !== false, dirs };
|
|
289
|
-
}
|
|
290
246
|
case 'json-merge': {
|
|
291
247
|
// Recompute from what is on disk right now rather than writing the
|
|
292
248
|
// snapshot `preview` took. Several actions can target one file — the two
|
package/lib/commands.mjs
CHANGED
|
@@ -8,13 +8,14 @@ import { homedir } from 'node:os';
|
|
|
8
8
|
import { resolve } from 'node:path';
|
|
9
9
|
import * as out from './out.mjs';
|
|
10
10
|
import {
|
|
11
|
-
CONTENT_VERSION,
|
|
11
|
+
CONTENT_VERSION, LOCK_DIR as LOCK_DIR_LEGACY, PKG_NAME, PKG_VERSION, agents,
|
|
12
|
+
lockDir, lockFile, lockNeedsGitignore, skills,
|
|
12
13
|
} from './content.mjs';
|
|
13
14
|
import { exists, isWritable, abs } from './fsx.mjs';
|
|
14
|
-
import { AGENTS, buildPlan, detectAll, installedCounts } from './hosts.mjs';
|
|
15
|
+
import { AGENTS, buildPlan, detectAll, installedCounts, skillsDir } from './hosts.mjs';
|
|
15
16
|
import * as lockfile from './lock.mjs';
|
|
16
17
|
import { dedupe, execute, isWritableState, preview, pruneDirs, revert } from './actions.mjs';
|
|
17
|
-
import {
|
|
18
|
+
import { removeDirIfEmpty, removeFile } from './fsx.mjs';
|
|
18
19
|
import { askAgents } from './prompt.mjs';
|
|
19
20
|
import { RESTART } from './render.mjs';
|
|
20
21
|
|
|
@@ -31,14 +32,12 @@ export function buildContext(argv, command) {
|
|
|
31
32
|
let root = process.cwd();
|
|
32
33
|
let yes = false;
|
|
33
34
|
let projectWiki;
|
|
34
|
-
let extraComponents = true;
|
|
35
35
|
let asJson = false;
|
|
36
36
|
let verbose = false;
|
|
37
37
|
|
|
38
38
|
for (let i = 0; i < argv.length; i += 1) {
|
|
39
39
|
const arg = argv[i];
|
|
40
40
|
if (arg === '--yes' || arg === '-y') { yes = true; continue; }
|
|
41
|
-
if (arg === '--no-extra-components') { extraComponents = false; continue; }
|
|
42
41
|
if (arg === '--json') { asJson = true; continue; }
|
|
43
42
|
if (arg === '--verbose') { verbose = true; continue; }
|
|
44
43
|
if (arg === '--agent') {
|
|
@@ -73,7 +72,7 @@ export function buildContext(argv, command) {
|
|
|
73
72
|
if (!exists(root)) throw new UsageError(`--root does not exist: ${root}`, [`${RUN} ${command} --root .`]);
|
|
74
73
|
|
|
75
74
|
return {
|
|
76
|
-
root, scope, yes, projectWiki,
|
|
75
|
+
root, scope, yes, projectWiki, json: asJson, verbose,
|
|
77
76
|
// Empty means "nothing chosen yet" — resolveAgents() applies the
|
|
78
77
|
// per-command fallback once the lock (and, for apply/install, a TTY)
|
|
79
78
|
// is available to consult.
|
|
@@ -152,18 +151,11 @@ function previewAll(ctx) {
|
|
|
152
151
|
}
|
|
153
152
|
|
|
154
153
|
function summarise(actions) {
|
|
155
|
-
const s = { create: 0, update: 0, unchanged: 0, drift: 0, conflict: 0, manual: 0
|
|
154
|
+
const s = { create: 0, update: 0, unchanged: 0, drift: 0, conflict: 0, manual: 0 };
|
|
156
155
|
for (const a of actions) if (a.state in s) s[a.state] += 1;
|
|
157
156
|
return s;
|
|
158
157
|
}
|
|
159
158
|
|
|
160
|
-
/** The `npm i -D <spec>` line(s) for every skipped skill-copy action. */
|
|
161
|
-
function skippedInstalls(actions) {
|
|
162
|
-
return [...new Set(
|
|
163
|
-
actions.filter((a) => a.state === 'skipped' && a.remedy?.startsWith('npm i -D')).map((a) => a.remedy),
|
|
164
|
-
)];
|
|
165
|
-
}
|
|
166
|
-
|
|
167
159
|
/** Strip the fields that are machinery, not result. */
|
|
168
160
|
function publicAction(a) {
|
|
169
161
|
const { doc, fmt, body, value, conflict, newly_declined: nd, absolute, ...rest } = a;
|
|
@@ -202,6 +194,7 @@ export async function status(argv) {
|
|
|
202
194
|
}
|
|
203
195
|
return {
|
|
204
196
|
id: d.id, detected: d.detected, marker: d.marker, writable: d.writable,
|
|
197
|
+
skills_dir: skillsDir(d.id, ctx),
|
|
205
198
|
installed: installedCounts(d.id, ctx), state, notes: d.notes,
|
|
206
199
|
};
|
|
207
200
|
});
|
|
@@ -217,24 +210,9 @@ export async function status(argv) {
|
|
|
217
210
|
const worst = ['conflict', 'drift', 'not-installed', 'ok', 'absent']
|
|
218
211
|
.find((s) => agentStates.some((a) => a.state === s)) ?? 'absent';
|
|
219
212
|
|
|
220
|
-
const extraComponents = ctx.extraComponents === false ? [] : probeAll({ platform: ctx.platform }).map((p) => ({
|
|
221
|
-
id: p.id, package: p.pkg, version: p.version, label: p.label,
|
|
222
|
-
available: p.available, probe: p.state, reason: p.reason ?? null,
|
|
223
|
-
installed_for: Object.entries(lock?.files ?? {})
|
|
224
|
-
.filter(([target]) => target.endsWith(`/skills/${p.id}/SKILL.md`))
|
|
225
|
-
.map(([, v]) => v.host),
|
|
226
|
-
install: p.available ? null : `npm i -D ${p.spec}`,
|
|
227
|
-
}));
|
|
228
|
-
|
|
229
213
|
let next = pending
|
|
230
214
|
? `Run \`${RUN} plan\` to see the ${pending} change(s) needed, then \`${RUN} apply --yes\`.`
|
|
231
215
|
: `Nothing to do — the harness is installed and up to date. Run \`${RUN} plan\` any time to re-check.`;
|
|
232
|
-
if (ctx.extraComponents !== false) {
|
|
233
|
-
const unavailable = extraComponents.filter((c) => !c.available);
|
|
234
|
-
if (unavailable.length) {
|
|
235
|
-
next += ` ${unavailable.length} optional extra component(s) are not on this machine — install with ${unavailable.map((c) => `\`${c.install}\``).join(', ')} to enable their skills.`;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
216
|
|
|
239
217
|
return out.ok('status', head(ctx, {
|
|
240
218
|
installed_version: lock?.version ?? null,
|
|
@@ -242,7 +220,7 @@ export async function status(argv) {
|
|
|
242
220
|
next_step: next,
|
|
243
221
|
help: [`${RUN} plan`, `${RUN} apply --yes`, `${RUN} guide`],
|
|
244
222
|
inventory: { skills: skills().length, agents: agents().length },
|
|
245
|
-
agents: agentStates,
|
|
223
|
+
agents: agentStates, drift,
|
|
246
224
|
declined: lock?.declined ?? [],
|
|
247
225
|
manual: lock?.manual ?? [],
|
|
248
226
|
errors,
|
|
@@ -275,10 +253,6 @@ export async function plan(argv) {
|
|
|
275
253
|
? ` The ${summary.conflict} conflict(s) and ${summary.manual} manual step(s) will be reported again and left to you.`
|
|
276
254
|
: '')
|
|
277
255
|
: `Nothing to do — every target is already up to date.`;
|
|
278
|
-
const installs = skippedInstalls(actions);
|
|
279
|
-
if (installs.length) {
|
|
280
|
-
next += ` ${summary.skipped} action(s) skipped — run ${installs.map((i) => `\`${i}\``).join(', ')} to enable them.`;
|
|
281
|
-
}
|
|
282
256
|
|
|
283
257
|
return out.ok('plan', head(ctx, {
|
|
284
258
|
summary, next_step: next,
|
|
@@ -311,13 +285,35 @@ export async function apply(argv, label = 'apply') {
|
|
|
311
285
|
|
|
312
286
|
// An apply we cannot record is an apply we cannot undo. Refuse up front
|
|
313
287
|
// rather than leave the user with files and no way back.
|
|
314
|
-
if (!isWritable(abs(ctx.root,
|
|
315
|
-
return out.failure(label, `cannot write the lock file at ${
|
|
288
|
+
if (!isWritable(abs(ctx.root, lockFile(ctx.root)))) {
|
|
289
|
+
return out.failure(label, `cannot write the lock file at ${lockFile(ctx.root)}`, {
|
|
316
290
|
next_step: `Make ${ctx.root} writable, then re-run \`${RUN} apply --yes\`.`,
|
|
317
291
|
help: [`${RUN} status`],
|
|
318
292
|
});
|
|
319
293
|
}
|
|
320
294
|
|
|
295
|
+
// Migration: a lock that used to live in the repository root carried a
|
|
296
|
+
// managed `.gitignore` line. Under `var/` the recipe ignores it already, so
|
|
297
|
+
// that line is ours to take back rather than leave behind as litter.
|
|
298
|
+
const staleIgnore = !lockNeedsGitignore(ctx.root)
|
|
299
|
+
&& (lock.fences ?? []).find((f) => f.file === '.gitignore' && f.block === 'lock');
|
|
300
|
+
const migrated = [];
|
|
301
|
+
if (staleIgnore) {
|
|
302
|
+
try {
|
|
303
|
+
const result = revert({ kind: 'text-fence', target: '.gitignore', ...staleIgnore }, ctx);
|
|
304
|
+
if (result.state !== 'kept') {
|
|
305
|
+
lock = { ...lock, fences: lock.fences.filter((f) => f !== staleIgnore) };
|
|
306
|
+
migrated.push({
|
|
307
|
+
target: '.gitignore',
|
|
308
|
+
detail: `removed the managed \`${LOCK_DIR_LEGACY}/\` line — the lock now lives in ${lockDir(ctx.root)}/, which this project already ignores`,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
// Cosmetic. An apply that installed everything must not fail over a
|
|
313
|
+
// line we were trying to tidy away.
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
321
317
|
const { actions, errors } = previewAll(ctx);
|
|
322
318
|
const results = [];
|
|
323
319
|
const perAgent = new Map(ctx.agents.map((a) => [a, { id: a, created: 0, updated: 0, unchanged: 0, failed: 0 }]));
|
|
@@ -371,7 +367,6 @@ export async function apply(argv, label = 'apply') {
|
|
|
371
367
|
conflict: results.filter((r) => r.state === 'conflict').length,
|
|
372
368
|
drift: results.filter((r) => r.state === 'drift').length,
|
|
373
369
|
manual: manual.length,
|
|
374
|
-
skipped: results.filter((r) => r.state === 'skipped').length,
|
|
375
370
|
failed: results.filter((r) => r.state === 'failed').length,
|
|
376
371
|
};
|
|
377
372
|
|
|
@@ -382,14 +377,11 @@ export async function apply(argv, label = 'apply') {
|
|
|
382
377
|
? `${summary.failed} change(s) failed — see \`errors\`. Fix those, then re-run \`${RUN} apply --yes\`.`
|
|
383
378
|
: `Restart the coding agents listed in \`help\` so they pick up the new skills, sub-agents and MCP servers.`
|
|
384
379
|
+ (manual.length ? ` Then finish the ${manual.length} manual step(s) in \`manual\`.` : '');
|
|
385
|
-
const installs = skippedInstalls(results);
|
|
386
|
-
if (installs.length) {
|
|
387
|
-
next += ` ${summary.skipped} action(s) skipped — run ${installs.map((i) => `\`${i}\``).join(', ')} to enable them.`;
|
|
388
|
-
}
|
|
389
380
|
|
|
390
381
|
const body = head(ctx, {
|
|
391
|
-
summary, lock_file:
|
|
382
|
+
summary, lock_file: lockFile(ctx.root), next_step: next, help,
|
|
392
383
|
agents: [...perAgent.values()], actions: results, manual, errors,
|
|
384
|
+
...(migrated.length ? { migrated } : {}),
|
|
393
385
|
});
|
|
394
386
|
return summary.failed ? out.failure(label, `${summary.failed} change(s) failed`, body) : out.ok(label, body);
|
|
395
387
|
}
|
|
@@ -423,7 +415,7 @@ export async function uninstall(argv) {
|
|
|
423
415
|
// someone else's files, so it does nothing and says so.
|
|
424
416
|
return out.ok('uninstall', head(ctx, {
|
|
425
417
|
summary: { removed: 0, reverted: 0, kept: 0, missing: 0, failed: 0 },
|
|
426
|
-
next_step: `Nothing to remove — no ${
|
|
418
|
+
next_step: `Nothing to remove — no ${lockFile(ctx.root)} was found, so this repository has no recorded install.`,
|
|
427
419
|
help: [`${RUN} status`],
|
|
428
420
|
kept: [], missing: [], errors: [],
|
|
429
421
|
}));
|
|
@@ -458,9 +450,15 @@ export async function uninstall(argv) {
|
|
|
458
450
|
|
|
459
451
|
if (failed === 0 && survivors.length === 0 && selected.size === AGENTS.length) {
|
|
460
452
|
// Everything we recorded is gone; the lock has nothing left to describe.
|
|
461
|
-
const { removeFile } = await import('./fsx.mjs');
|
|
462
453
|
removeFile(lockfile.lockPath(ctx.root));
|
|
463
|
-
|
|
454
|
+
removeFile(lockfile.legacyLockPath(ctx.root));
|
|
455
|
+
// `pruneDirs` deliberately never touches a top-level directory — that
|
|
456
|
+
// rule protects a host's own `.claude`/`.codex`, but `.sw-ai-sdk` and
|
|
457
|
+
// `var/sw-ai-sdk` are ours, and an empty one left behind is the clutter
|
|
458
|
+
// this move set out to remove.
|
|
459
|
+
pruneDirs([lockDir(ctx.root)], ctx);
|
|
460
|
+
removeDirIfEmpty(abs(ctx.root, LOCK_DIR_LEGACY));
|
|
461
|
+
removeDirIfEmpty(abs(ctx.root, lockDir(ctx.root)));
|
|
464
462
|
} else {
|
|
465
463
|
// Keep a lock describing exactly what survived, so a re-run resumes.
|
|
466
464
|
lockfile.write(ctx.root, rebuild(lock, survivors, selected));
|
package/lib/content.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
10
10
|
import { dirname, join, resolve } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import { walk } from './fsx.mjs';
|
|
12
|
+
import { isDir, walk } from './fsx.mjs';
|
|
13
13
|
|
|
14
14
|
export const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
15
|
|
|
@@ -25,8 +25,38 @@ export const PKG_VERSION = pkg.version;
|
|
|
25
25
|
export const CONTENT_VERSION = 1;
|
|
26
26
|
export const SUPPORTED_CONTENT = [1];
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Where the lock lives, relative to the install root.
|
|
30
|
+
*
|
|
31
|
+
* A Shopware project ignores the whole of `var/` through the `shopware/core`
|
|
32
|
+
* Flex recipe (`/var/*` with a single `!/var/.htaccess` negation), and `var/`
|
|
33
|
+
* is already where that project keeps machine-local state — `var/cache`,
|
|
34
|
+
* `var/log`. Writing the lock there keeps the repository root clean and means
|
|
35
|
+
* the installer needs no `.gitignore` line at all.
|
|
36
|
+
*
|
|
37
|
+
* A root with no `var/` — a user-scope install under $HOME, or a checkout that
|
|
38
|
+
* is not a Shopware project — keeps the original dot-directory, which is
|
|
39
|
+
* gitignored through the managed block as before. Nothing here looks at the
|
|
40
|
+
* platform or at any path outside the install root.
|
|
41
|
+
*/
|
|
28
42
|
export const LOCK_DIR = '.sw-ai-sdk';
|
|
29
43
|
export const LOCK_FILE = `${LOCK_DIR}/harness.lock.json`;
|
|
44
|
+
export const VAR_LOCK_DIR = 'var/sw-ai-sdk';
|
|
45
|
+
export const VAR_LOCK_FILE = `${VAR_LOCK_DIR}/harness.lock.json`;
|
|
46
|
+
|
|
47
|
+
/** `var/sw-ai-sdk` when the root has a `var/`, else `.sw-ai-sdk`. */
|
|
48
|
+
export function lockDir(root) {
|
|
49
|
+
return isDir(join(root, 'var')) ? VAR_LOCK_DIR : LOCK_DIR;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function lockFile(root) {
|
|
53
|
+
return `${lockDir(root)}/harness.lock.json`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether this root still needs the managed `.gitignore` line for the lock. */
|
|
57
|
+
export function lockNeedsGitignore(root) {
|
|
58
|
+
return lockDir(root) === LOCK_DIR;
|
|
59
|
+
}
|
|
30
60
|
|
|
31
61
|
function dirsIn(abs) {
|
|
32
62
|
return readdirSync(abs)
|
package/lib/guide.mjs
CHANGED
|
@@ -6,10 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import * as out from './out.mjs';
|
|
8
8
|
import { PKG_NAME, PKG_VERSION } from './content.mjs';
|
|
9
|
-
import { EXTRA_COMPONENTS } from './extra-components.mjs';
|
|
10
9
|
|
|
11
10
|
const RUN = `npx -y ${PKG_NAME}@${PKG_VERSION}`;
|
|
12
|
-
const EXTRA_COMPONENT_INSTALLS = EXTRA_COMPONENTS.map((c) => `npm i -D ${c.pkg}@${c.version}`).join('\n');
|
|
13
11
|
|
|
14
12
|
export const GUIDE = `# ${PKG_NAME} — install protocol
|
|
15
13
|
|
|
@@ -55,26 +53,21 @@ registers the MCP servers and permission rules they depend on.
|
|
|
55
53
|
terminal
|
|
56
54
|
--scope project|user default: project
|
|
57
55
|
--root <path> default: the current directory
|
|
58
|
-
--no-extra-components skip the optional extra-component skills
|
|
59
56
|
--json print the result object instead of
|
|
60
57
|
the human summary — a skill or
|
|
61
58
|
agent always passes this
|
|
62
59
|
--verbose include the per-file detail
|
|
63
60
|
|
|
64
|
-
##
|
|
61
|
+
## The two visual editors
|
|
65
62
|
|
|
66
|
-
|
|
67
|
-
npm
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
\`\`\`
|
|
75
|
-
|
|
76
|
-
The exact command is also in a \`skipped\` action's \`remedy\`, and in
|
|
77
|
-
\`status\`'s \`extra_components\` array. Install it, then re-run \`${RUN} apply --yes\`.
|
|
63
|
+
The Specs Editor and the Tender Discovery Tool are optional, ship their own
|
|
64
|
+
skill inside their own npm package, and are **not installed by this CLI**.
|
|
65
|
+
The \`sw-setup\` skill owns their whole lifecycle — it offers them, installs
|
|
66
|
+
each one with that package's own \`install-skill --target <skills dir>\`,
|
|
67
|
+
updates them by re-running it, and removes them with \`uninstall-skill\`.
|
|
68
|
+
This CLI never probes for them, never reports them, and never reaches the
|
|
69
|
+
network for them; \`status --json\` reports each agent's \`skills_dir\`,
|
|
70
|
+
which is the target \`sw-setup\` hands them.
|
|
78
71
|
|
|
79
72
|
## What you can rely on
|
|
80
73
|
|