@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 +19 -0
- package/README.md +24 -11
- package/bin/cli.mjs +7 -2
- package/lib/commands.mjs +45 -2
- package/lib/guide.mjs +18 -9
- package/lib/hosts.mjs +4 -3
- package/lib/prompt.mjs +72 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/sw-setup/reference/rows.md +10 -0
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
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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`
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
6
|
-
//
|
|
7
|
-
// `--host
|
|
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.
|
|
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
|
+
"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,
|