skillshop 2.1.0 → 2.2.0
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 +64 -4
- package/package.json +1 -1
- package/skills-content/skillshop/SKILL.md +58 -5
- package/src/agents-instructions.js +41 -4
- package/src/legacy-cli.js +610 -22
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ npm install --global skillshop
|
|
|
13
13
|
skillshop init
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
`init` asks which agents you use and saves their destinations in `~/.skillshop/config.json`.
|
|
16
|
+
`skillshop init` asks which agents you use and saves their destinations in `~/.skillshop/config.json`. It is optional for project skill installs: without saved agent selection, Skillshop uses the nearest existing `.agents`, `.github`, or `.codex` directory and falls back to project-root `.agents/skills`. Run `skillshop init` to choose system-wide defaults; change that selection later with `skillshop config agents`. Codex and OpenCode use `.agents/skills` inside projects and `~/.agents/skills` for user-level installs. Copilot and Claude use their own skill directories. Shared destinations get one copy.
|
|
17
17
|
|
|
18
18
|
### Install a named skill
|
|
19
19
|
|
|
@@ -34,7 +34,7 @@ skillshop one-shot --global
|
|
|
34
34
|
# Equivalent: sk install one-shot --global
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
For Codex or OpenCode, this writes `~/.agents/skills/one-shot`. A global install replaces an existing skill directory with that name, including local edits and extra files. Global installs use `~/.skillshop/global-skills.json` for bookkeeping.
|
|
37
|
+
For Codex or OpenCode, this writes `~/.agents/skills/one-shot`. A global install replaces an existing skill directory with that name, including local edits and extra files. Global installs use `~/.skillshop/global-skills.json` for bookkeeping. Except for `skillshop upgrade`, ordinary non-agent-instruction CLI runs reconcile recorded installations. Upgrade skips reconciliation in the old process and refreshes changed copies from the newly installed package in a fresh process. Agent instruction commands also bypass reconciliation and do not install the bundled registry.
|
|
38
38
|
|
|
39
39
|
### Update or remove skills
|
|
40
40
|
|
|
@@ -48,16 +48,74 @@ skillshop uninstall one-shot --global
|
|
|
48
48
|
|
|
49
49
|
`update` replaces skills managed in the current project's lockfile. `update --push` updates registered projects. Neither command updates global copies; reinstall a global skill by name with `--global`. Project uninstall removes only a skill recorded in `.skillshop-lock`. Global uninstall removes the named skill from configured user directories.
|
|
50
50
|
|
|
51
|
+
### Upgrade Skillshop
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
skillshop upgrade
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The command upgrades the npm installation that supplied the invoked CLI. A project-local installation updates Skillshop in the owning npm project and preserves the dependency section where it is already declared. It refreshes only that project's recorded Skillshop-managed skill copies. A global installation updates its npm prefix, then refreshes recorded global copies and copies in registered projects; it does not update those projects' package manifests. Running from a source checkout keeps the historical global-install behavior and does not edit the checkout's package manifest.
|
|
58
|
+
|
|
59
|
+
After npm succeeds, the newly installed CLI runs in a fresh process and refreshes only existing copies recorded in `.skillshop-lock` or `~/.skillshop/global-skills.json` whose bundled skill version changed. Same-version copies retain their local bodies. A changed-version replacement overwrites local edits in that managed copy; make durable edits in the bundled source. Deleted paths are pruned from ownership records instead of recreated. Unmanaged directories and package-import snapshots are not adopted. An npm install failure stops before copy refresh; a later refresh failure reports that Skillshop was upgraded and gives a nonzero result with the partial-refresh summary.
|
|
60
|
+
|
|
61
|
+
`skillshop init` is optional. It configures agent destinations and is not required to upgrade Skillshop. The upgrade command never initializes, renders, or rewrites project `AGENTS.md` files or instruction templates.
|
|
62
|
+
|
|
51
63
|
An exact version such as `skillshop one-shot@2026.8.21` checks the currently bundled version. It does not fetch a historical release. Use `skillshop migrate` to discover older lockfiles and register their repositories.
|
|
52
64
|
|
|
53
65
|
### Manage agent instructions
|
|
54
66
|
|
|
55
67
|
```bash
|
|
56
68
|
skillshop agents
|
|
69
|
+
skillshop agents init
|
|
70
|
+
skillshop agents --check
|
|
57
71
|
skillshop agents --global
|
|
58
72
|
```
|
|
59
73
|
|
|
60
|
-
|
|
74
|
+
There are two local modes. Plain `skillshop agents` keeps the bounded legacy behavior for an uninitialized project: it updates the nearest existing `AGENTS.md` within the selected boundary, or creates one at the boundary root if none exists. After project sources are initialized, plain `skillshop agents` renders them instead. The global command remains separate and updates the supported user instruction files.
|
|
75
|
+
|
|
76
|
+
Initialize project-owned instructions with `skillshop agents init`. The neutral starter template is `.agents/instructions/AGENTS.template.md`; put source blocks beside it as `*.block.md`. Skillshop renders these sources into a distinct managed region in the project-root `AGENTS.md`. Existing user text and prior Skillshop managed guidance are preserved. Edit the template and source blocks, not the generated region. Repeated `skillshop agents init` leaves existing sources alone. If an initialized project's template is removed, `skillshop agents` reports an error rather than restoring bundled personal guidance. Symlinked source, template, block, or output path components are rejected, including dangling links.
|
|
77
|
+
|
|
78
|
+
The default boundary is the nearest Git repository or worktree root. Outside Git, the current working directory is the boundary. Use `--project <directory>` to choose another boundary; a relative path is resolved from the command's current directory. This is required when a nested project inside a larger repository should own its own output. Put all three explicit-boundary scripts in `packages/app/package.json` so init, rendering, and checking stay in that package:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"scripts": {
|
|
83
|
+
"agents": "skillshop agents --project .",
|
|
84
|
+
"agents:init": "skillshop agents init --project .",
|
|
85
|
+
"agents:check": "skillshop agents --check --project ."
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For a local project dependency, add Skillshop as a development dependency and opt into rendering through explicit scripts:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install --save-dev skillshop
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"scripts": {
|
|
99
|
+
"agents": "skillshop agents",
|
|
100
|
+
"agents:init": "skillshop agents init",
|
|
101
|
+
"agents:check": "skillshop agents --check"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Run `npm run agents:init` once, edit the project sources, then run `npm run agents`. `npm run agents:check` exits successfully when the generated region in the working tree matches the current sources; it does not inspect Git's index or commit. CI can run `npm ci` followed by `npm run agents:check`. Commit the npm manifest and lockfile, `.agents/instructions/AGENTS.template.md`, its source blocks, and the rendered root `AGENTS.md`. There is no install hook: initialization and rendering happen only when the scripts are run.
|
|
107
|
+
|
|
108
|
+
Project and bundled source templates support explicit includes:
|
|
109
|
+
|
|
110
|
+
```markdown
|
|
111
|
+
{{ include "020_testing.block.md" }}
|
|
112
|
+
|
|
113
|
+
{{ blocks }}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Use `{{ include "020_testing.block.md" }}` or `{{ include '020_testing.block.md' }}` for named blocks. The older `{{ fragments["020_testing.block.md"] }}` and single-quoted lookup forms remain supported. `{{ blocks }}` inserts the remaining fragments in filename order, excluding every explicitly included file even when its include appears later in the template. Repeated includes render at each position. Fragment contents are inserted literally; placeholder-looking text inside a block is not evaluated recursively. Invalid expressions and missing fragments fail before generated output is written.
|
|
117
|
+
|
|
118
|
+
These expressions apply to source templates only. Skillshop does not interpret them in destination instruction files or sibling fragments.
|
|
61
119
|
|
|
62
120
|
### Use skills from another package
|
|
63
121
|
|
|
@@ -235,7 +293,9 @@ skillshop install <name> [--global]
|
|
|
235
293
|
skillshop update [name]
|
|
236
294
|
skillshop update --push
|
|
237
295
|
skillshop uninstall <name> [--global]
|
|
238
|
-
skillshop agents [--
|
|
296
|
+
skillshop agents init [--project <directory>]
|
|
297
|
+
skillshop agents [--project <directory>] [--check]
|
|
298
|
+
skillshop agents --global
|
|
239
299
|
skillshop migrate [root ...]
|
|
240
300
|
skillshop library init
|
|
241
301
|
skillshop library list
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: skillshop
|
|
3
|
-
description: Manage repository-local agent skills
|
|
3
|
+
description: Manage repository-local agent skills, Skillshop upgrades, and project agent instructions. Use when a repository has a .skillshop-lock or the user asks to install, uninstall, pin, list, update, or upgrade Skillshop-managed copies, or initialize, render, or check project AGENTS.md instructions.
|
|
4
4
|
metadata:
|
|
5
5
|
author: Daniel Fosco
|
|
6
|
-
version: "2026.10.
|
|
6
|
+
version: "2026.10.23"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Skillshop — Local Skill Registry
|
|
@@ -12,7 +12,9 @@ Use the `skillshop` CLI to manage skills in this repository. The project-root `.
|
|
|
12
12
|
|
|
13
13
|
This registry is hand-maintained. `metadata.author` is original-author attribution, not ownership. The user can adapt skills locally. skillshop does not read other installers' state or update their skills; the user resolves name collisions.
|
|
14
14
|
|
|
15
|
-
Every invocation, including help, version, and failed commands, reconciles the current project's lockfile, registration, and Git exclusions.
|
|
15
|
+
Every non-`agents` invocation, including help, version, and failed commands, reconciles the current project's lockfile, registration, and Git exclusions, except `skillshop upgrade`. Agent instruction commands also bypass reconciliation. Upgrade skips the old process's pre/post reconciliation, installs the npm package first, then starts the installed CLI in a fresh process to refresh only changed-version copies recorded in Skillshop's project/global ownership files. A failed npm install must not touch managed copies; report any partial copy refresh and return failure. Never adopt unmanaged directories or package-import snapshots, and do not recreate deleted paths. Reconciliation removes missing installation paths and stale repository registrations. Tracked installed frontmatter is repaired without replacing bodies or extra fields: names match directory names, author/version use the installation record, and missing descriptions use the bundled skill. Incomplete records fall back to installed or bundled metadata. Valid files are not rewritten. Invalid lockfiles, unsafe paths, and unsupported frontmatter must be reported, not discarded. Do not silently untrack Git files; that remains an explicit migration operation.
|
|
16
|
+
|
|
17
|
+
`skillshop init` is optional for upgrade. It configures agent destinations but does not install the npm package or refresh managed skill copies. Upgrade never generates or edits `AGENTS.md` files or project instruction templates.
|
|
16
18
|
|
|
17
19
|
## Configure agents
|
|
18
20
|
|
|
@@ -21,7 +23,48 @@ skillshop init
|
|
|
21
23
|
skillshop config agents
|
|
22
24
|
```
|
|
23
25
|
|
|
24
|
-
The first `skillshop init` prompts for one or more agents and stores the system-wide selection in `~/.skillshop/config.json`. `skillshop config agents` replaces
|
|
26
|
+
The first `skillshop init` prompts for one or more agents and stores the system-wide selection in `~/.skillshop/config.json`. `skillshop init` is optional for project skill installs: without saved agent selection, Skillshop uses the nearest existing `.agents`, `.github`, or `.codex` directory and falls back to project-root `.agents/skills`. `skillshop config agents` replaces the system-wide selection. Copilot uses `.github/skills` locally and `~/.copilot/skills` globally; Claude uses `.claude/skills` in both scopes; Codex, OpenCode, and Others use `.agents/skills` in both scopes. Shared paths are deduplicated.
|
|
27
|
+
|
|
28
|
+
## Manage project agent instructions
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
skillshop agents init
|
|
32
|
+
skillshop agents
|
|
33
|
+
skillshop agents --check
|
|
34
|
+
skillshop agents --global
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`skillshop agents init` opts into project-owned instruction sources at `.agents/instructions/AGENTS.template.md` and sibling `*.block.md` files. Skillshop writes their output to a separate managed region in the project-root `AGENTS.md`, preserving surrounding text and older managed guidance. Edit the template and source blocks, not the generated region. Repeated `skillshop agents init` preserves source edits. If the initialized source template is removed, plain `skillshop agents` reports an error instead of restoring bundled personal guidance.
|
|
38
|
+
|
|
39
|
+
Before project sources are configured, plain `skillshop agents` keeps the bounded legacy behavior: it updates the nearest existing `AGENTS.md` inside the selected boundary, or creates the root output when none exists. The boundary is the nearest Git repository/worktree root, or the current directory outside Git. `--project <directory>` explicitly selects another boundary relative to the current working directory. Use it for nested projects so every script stays inside the nested package:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"scripts": {
|
|
44
|
+
"agents": "skillshop agents --project .",
|
|
45
|
+
"agents:init": "skillshop agents init --project .",
|
|
46
|
+
"agents:check": "skillshop agents --check --project ."
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`--check` is read-only and suitable for CI. It compares generated output in the working tree with current sources; it does not inspect the Git index or commit. Symlink components in project source, block, template, and output paths are rejected, including dangling symlinks. `--global` retains user-level instruction updates.
|
|
52
|
+
|
|
53
|
+
Source templates support `{{ include "name.block.md" }}`, `{{ include 'name.block.md' }}`, the legacy `{{ fragments["name.block.md"] }}` lookup, and `{{ blocks }}` for remaining blocks in filename order. Explicitly included blocks are excluded from the aggregate. Repeated includes are allowed. Block text is inserted literally and is not evaluated recursively.
|
|
54
|
+
|
|
55
|
+
For npm consumers, declare explicit scripts rather than relying on an install hook:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"scripts": {
|
|
60
|
+
"agents": "skillshop agents",
|
|
61
|
+
"agents:init": "skillshop agents init",
|
|
62
|
+
"agents:check": "skillshop agents --check"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Commit the template and block sources together with the rendered root `AGENTS.md`; commit `package.json` and the npm lockfile when Skillshop is a development dependency. Run `npm run agents:check` in CI to detect stale or missing generated output.
|
|
25
68
|
|
|
26
69
|
## Install a skill
|
|
27
70
|
|
|
@@ -45,7 +88,7 @@ skillshop install <name> --version <version>
|
|
|
45
88
|
skillshop <name> --global
|
|
46
89
|
```
|
|
47
90
|
|
|
48
|
-
`--global` (or `-g`) installs into every configured system skill path without a project lockfile. Installs are recorded in `~/.skillshop/global-skills.json`
|
|
91
|
+
`--global` (or `-g`) installs into every configured system skill path without a project lockfile. Installs are recorded in `~/.skillshop/global-skills.json`. Except for `skillshop upgrade`, ordinary commands other than `skillshop agents` can repair their frontmatter and prune missing paths. Agent-instruction commands bypass that reconciliation. `skillshop upgrade` instead refreshes recorded global and registered project copies only after npm succeeds and a fresh CLI starts. Same-version copies keep local body edits; a changed-version refresh replaces the managed copy and overwrites local edits. Older installs are recorded on explicit reinstall; do not adopt other global skills. Global copies are not migrated when the agent setting changes, so remove unwanted global skills before reconfiguration. The local registry contains one current version per skill. `--version` (or `-v`) is an exact compatibility check; do not claim that an unavailable historical version can be resolved.
|
|
49
92
|
|
|
50
93
|
## Update managed skills
|
|
51
94
|
|
|
@@ -60,6 +103,16 @@ skillshop up --push
|
|
|
60
103
|
|
|
61
104
|
`update` with no name replaces every skill listed in the current repository's `.skillshop-lock` and updates its locked metadata. A name updates only that managed skill. `--push` updates every centrally registered repository and cannot be combined with a skill name. Updates and installs replace local edits; keep edits in the registry if they should survive these operations. Global installs cannot be updated with this command; reinstall them by name with `--global`.
|
|
62
105
|
|
|
106
|
+
## Upgrade Skillshop
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
skillshop upgrade
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Upgrade targets the npm installation that supplied the invoked CLI. A local npm dependency updates in its owning project and stays in its existing dependency section; only that project's locked Skillshop copies are refreshed. A global npm installation updates its active prefix and refreshes recorded global copies plus copies in registered projects without changing those projects' npm manifests. A source checkout retains the compatibility behavior of installing Skillshop globally, without editing the checkout's `package.json`.
|
|
113
|
+
|
|
114
|
+
After npm succeeds, the new package's CLI starts in a fresh process. It replaces existing recorded copies only when the bundled skill version changed, preserving local bodies for same-version copies. Changed-version replacement overwrites local edits in that managed copy; make durable changes in the bundled skill source. Missing paths are pruned from records rather than recreated. Unmanaged skills and imported snapshots remain untouched. Install failure prevents refresh; refresh failure reports the npm stage as successful and returns nonzero with a partial-result summary. Upgrade does not run agent initialization or project instruction generation.
|
|
115
|
+
|
|
63
116
|
## Migrate existing repositories
|
|
64
117
|
|
|
65
118
|
```bash
|
|
@@ -6,6 +6,7 @@ export class AgentsInstructionsError extends Error {}
|
|
|
6
6
|
|
|
7
7
|
const blockSuffix = '.block.md';
|
|
8
8
|
const referencePattern = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
9
|
+
const includePattern = /^include\s+(["'])([^"']+)\1$/;
|
|
9
10
|
const namedReferencePattern = /^fragments\s*\[\s*(["'])([^"'\[\]]+)\1\s*\]$/;
|
|
10
11
|
const aggregateExpression = 'blocks';
|
|
11
12
|
const regionBeginPrefix = '<!-- skillshop agents: begin v';
|
|
@@ -13,6 +14,9 @@ const regionBeginPattern = /^<!-- (?:skillshop|skillz) agents: begin v\S+ -->$/;
|
|
|
13
14
|
const regionEndLine = '<!-- skillshop agents: end -->';
|
|
14
15
|
const oldRegionEndLine = '<!-- skillz agents: end -->';
|
|
15
16
|
const legacyMarkerPattern = /^<!-- skillz agents: AGENTS\.md@\S+ -->$/;
|
|
17
|
+
const projectRegionBeginPrefix = '<!-- skillshop project agents: begin v';
|
|
18
|
+
const projectRegionBeginPattern = /^<!-- skillshop project agents: begin v\S+ -->$/;
|
|
19
|
+
const projectRegionEndLine = '<!-- skillshop project agents: end -->';
|
|
16
20
|
const fragmentsBegin = '<!-- skillshop fragments: begin -->';
|
|
17
21
|
const fragmentsEnd = '<!-- skillshop fragments: end -->';
|
|
18
22
|
|
|
@@ -32,12 +36,12 @@ export function discoverBlocks(contentDirectory) {
|
|
|
32
36
|
function invalidReference(expression, line) {
|
|
33
37
|
return new AgentsInstructionsError(
|
|
34
38
|
`invalid fragment reference {{ ${expression} }} at line ${line} of the agents base template; ` +
|
|
35
|
-
'use {{
|
|
39
|
+
'use {{ include "name.block.md" }} or {{ blocks }}'
|
|
36
40
|
);
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
export function composeAgentsInstructions(contentDirectory) {
|
|
40
|
-
const basePath = path.join(contentDirectory,
|
|
43
|
+
export function composeAgentsInstructions(contentDirectory, baseFileName = 'AGENTS.md') {
|
|
44
|
+
const basePath = path.join(contentDirectory, baseFileName);
|
|
41
45
|
if (!fs.existsSync(basePath)) {
|
|
42
46
|
throw new AgentsInstructionsError(`missing agents base template: ${basePath}`);
|
|
43
47
|
}
|
|
@@ -54,7 +58,7 @@ export function composeAgentsInstructions(contentDirectory) {
|
|
|
54
58
|
references.push({ kind: 'aggregate', start: match.index, end: match.index + match[0].length });
|
|
55
59
|
continue;
|
|
56
60
|
}
|
|
57
|
-
const named = expression.match(namedReferencePattern);
|
|
61
|
+
const named = expression.match(includePattern) ?? expression.match(namedReferencePattern);
|
|
58
62
|
if (!named || named[2].includes('/') || named[2].includes('\\')) {
|
|
59
63
|
throw invalidReference(expression, line);
|
|
60
64
|
}
|
|
@@ -91,6 +95,39 @@ export function composeAgentsInstructions(contentDirectory) {
|
|
|
91
95
|
return { content };
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
export function applyProjectInstructions(existing, composed, version) {
|
|
99
|
+
const composedText = composed.trimEnd();
|
|
100
|
+
const region = `${projectRegionBeginPrefix}${version} -->\n${composedText}\n${projectRegionEndLine}`;
|
|
101
|
+
const lines = existing.split('\n');
|
|
102
|
+
const begins = [];
|
|
103
|
+
const ends = [];
|
|
104
|
+
lines.forEach((line, index) => {
|
|
105
|
+
if (projectRegionBeginPattern.test(line)) begins.push(index);
|
|
106
|
+
else if (line === projectRegionEndLine) ends.push(index);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (begins.length !== ends.length || begins.length > 1 || (begins.length && begins[0] >= ends[0])) {
|
|
110
|
+
throw new AgentsInstructionsError('project agents markers are malformed');
|
|
111
|
+
}
|
|
112
|
+
if (!begins.length) return { action: 'installed', text: appendRegion(existing, region) };
|
|
113
|
+
|
|
114
|
+
const [start] = begins;
|
|
115
|
+
const [end] = ends;
|
|
116
|
+
const installedVersion = lines[start].match(/<!-- skillshop project agents: begin v(\S+) -->/)[1];
|
|
117
|
+
const inner = lines.slice(start + 1, end).join('\n');
|
|
118
|
+
if (installedVersion === version && inner === composedText && lines[end] === projectRegionEndLine) {
|
|
119
|
+
return { action: 'unchanged', text: existing };
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
action: 'refreshed',
|
|
123
|
+
text: [...lines.slice(0, start), ...region.split('\n'), ...lines.slice(end + 1)].join('\n')
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function hasProjectInstructions(existing) {
|
|
128
|
+
return existing.split('\n').some((line) => projectRegionBeginPattern.test(line) || line === projectRegionEndLine);
|
|
129
|
+
}
|
|
130
|
+
|
|
94
131
|
function regionText(version, composed) {
|
|
95
132
|
return `${regionBeginPrefix}${version} -->\n${composed}\n${regionEndLine}`;
|
|
96
133
|
}
|
package/src/legacy-cli.js
CHANGED
|
@@ -3,10 +3,17 @@
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import os from 'node:os';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
7
7
|
import { createInterface } from 'node:readline/promises';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
AgentsInstructionsError,
|
|
11
|
+
applyInstructions,
|
|
12
|
+
applyLocalFragments,
|
|
13
|
+
applyProjectInstructions,
|
|
14
|
+
composeAgentsInstructions,
|
|
15
|
+
hasProjectInstructions
|
|
16
|
+
} from './agents-instructions.js';
|
|
10
17
|
|
|
11
18
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
19
|
const skillsContentRoot = path.join(packageRoot, 'skills-content');
|
|
@@ -21,6 +28,11 @@ const excludeBlockStart = '# >>> skillshop managed dependencies >>>';
|
|
|
21
28
|
const excludeBlockEnd = '# <<< skillshop managed dependencies <<<';
|
|
22
29
|
const legacyExcludeBlockStart = '# >>> skillz managed dependencies >>>';
|
|
23
30
|
const legacyExcludeBlockEnd = '# <<< skillz managed dependencies <<<';
|
|
31
|
+
const projectTemplateDirectory = path.join('.agents', 'instructions');
|
|
32
|
+
const projectTemplateName = 'AGENTS.template.md';
|
|
33
|
+
const defaultProjectTemplate = '# Project instructions\n\n{{ blocks }}\n';
|
|
34
|
+
const internalUpgradeRefreshCommand = '__skillshop_upgrade_refresh';
|
|
35
|
+
const internalUpgradeRefreshToken = 'skillshop-managed-upgrade-v1';
|
|
24
36
|
const globalAgentsTargets = [
|
|
25
37
|
path.join('.codex', 'AGENTS.md'),
|
|
26
38
|
path.join('.agents', 'AGENTS.md'),
|
|
@@ -107,7 +119,9 @@ function printHelp() {
|
|
|
107
119
|
console.log(`Usage:
|
|
108
120
|
skillshop init
|
|
109
121
|
skillshop config agents
|
|
110
|
-
skillshop agents [--
|
|
122
|
+
skillshop agents init [--project <directory>]
|
|
123
|
+
skillshop agents [--project <directory>] [--check]
|
|
124
|
+
skillshop agents --global
|
|
111
125
|
skillshop <name> [--version <version>] [--global]
|
|
112
126
|
skillshop install <name> [--version <version>] [--global]
|
|
113
127
|
skillshop uninstall <name> [--global]
|
|
@@ -119,8 +133,21 @@ function printHelp() {
|
|
|
119
133
|
|
|
120
134
|
Install, uninstall, and update skills from the local registry.
|
|
121
135
|
|
|
136
|
+
Agent instructions:
|
|
137
|
+
skillshop agents init creates a neutral .agents/instructions/AGENTS.template.md and renders it into a separate managed region in the project-root AGENTS.md. Existing text and prior managed guidance are preserved. Edit the template and sibling *.block.md sources, not the generated region.
|
|
138
|
+
Plain skillshop agents keeps the bounded bundled behavior until project sources exist; afterward it renders project sources. Missing sources in an initialized project are errors.
|
|
139
|
+
skillshop agents --check reports whether generated output is current and never writes. skillshop agents --global retains user-level instruction updates.
|
|
140
|
+
The default boundary is the nearest Git/worktree root, or the current directory outside Git. --project selects another directory relative to the current working directory. Nested projects need an explicit selection.
|
|
141
|
+
Source, block, template, and output paths containing symlink components, including dangling links, are rejected.
|
|
142
|
+
Templates accept {{ include "name.block.md" }}, {{ include 'name.block.md' }}, legacy {{ fragments["name.block.md"] }}, and {{ blocks }} for unreferenced blocks. Inserted block text is literal.
|
|
143
|
+
|
|
144
|
+
Upgrade:
|
|
145
|
+
skillshop upgrade updates the npm installation that supplied the CLI, then refreshes recorded Skillshop-managed copies with the newly installed CLI. skillshop init is optional and upgrade does not generate AGENTS.md files.
|
|
146
|
+
|
|
122
147
|
Options:
|
|
123
148
|
-g, --global Use configured system skill directories without a project lockfile
|
|
149
|
+
--project <directory> Select the agents boundary relative to the current directory
|
|
150
|
+
--check Check project-generated instructions without writing
|
|
124
151
|
--push Update every registered repository
|
|
125
152
|
-v, --version <version> Require an exact skill version
|
|
126
153
|
-h, --help Show this help
|
|
@@ -775,28 +802,86 @@ function installGlobal(name, requestedVersion) {
|
|
|
775
802
|
}
|
|
776
803
|
}
|
|
777
804
|
|
|
778
|
-
function
|
|
779
|
-
|
|
780
|
-
|
|
805
|
+
function instructionProjectRoot(cwd, selectedProject) {
|
|
806
|
+
if (selectedProject) {
|
|
807
|
+
const selected = path.resolve(cwd, selectedProject);
|
|
808
|
+
let stats;
|
|
809
|
+
try {
|
|
810
|
+
stats = fs.statSync(selected);
|
|
811
|
+
} catch {
|
|
812
|
+
throw new CliError(`project directory does not exist: ${selected}`);
|
|
813
|
+
}
|
|
814
|
+
if (!stats.isDirectory()) throw new CliError(`project path is not a directory: ${selected}`);
|
|
815
|
+
return canonicalPath(selected);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return canonicalPath(projectRoot(cwd, { required: false }) ?? cwd);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function isWithinDirectory(root, target) {
|
|
822
|
+
const relative = path.relative(root, target);
|
|
823
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function nearestAgentsFile(cwd, root) {
|
|
827
|
+
const start = canonicalPath(cwd);
|
|
828
|
+
let directory = isWithinDirectory(root, start) ? start : root;
|
|
829
|
+
while (isWithinDirectory(root, directory)) {
|
|
781
830
|
const candidate = path.join(directory, 'AGENTS.md');
|
|
782
831
|
if (fs.existsSync(candidate)) return candidate;
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
832
|
+
if (directory === root) break;
|
|
833
|
+
directory = path.dirname(directory);
|
|
834
|
+
}
|
|
835
|
+
return path.join(root, 'AGENTS.md');
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function assertLocalInstructionSources(root, contentDirectory) {
|
|
839
|
+
assertNoInstructionSymlink(root, contentDirectory, 'local instruction source directory');
|
|
840
|
+
if (!fs.existsSync(contentDirectory)) return;
|
|
841
|
+
for (const entry of fs.readdirSync(contentDirectory, { withFileTypes: true })) {
|
|
842
|
+
if (entry.name.endsWith('.block.md')) {
|
|
843
|
+
const sourcePath = path.join(contentDirectory, entry.name);
|
|
844
|
+
const label = `local instruction source ${entry.name}`;
|
|
845
|
+
assertLocalDestination(root, sourcePath, label);
|
|
846
|
+
assertNoInstructionSymlink(root, sourcePath, label);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function assertNoInstructionSymlink(root, target, label) {
|
|
852
|
+
const boundary = path.resolve(root);
|
|
853
|
+
const absoluteTarget = path.resolve(target);
|
|
854
|
+
const relative = path.relative(boundary, absoluteTarget);
|
|
855
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
856
|
+
throw new CliError(`${label} resolves outside the project`);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
let current = boundary;
|
|
860
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
861
|
+
current = path.join(current, segment);
|
|
862
|
+
try {
|
|
863
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
864
|
+
throw new CliError(`${label} contains a symlink path component`);
|
|
865
|
+
}
|
|
866
|
+
} catch (error) {
|
|
867
|
+
if (error instanceof CliError) throw error;
|
|
868
|
+
if (error.code === 'ENOENT') break;
|
|
869
|
+
throw error;
|
|
870
|
+
}
|
|
786
871
|
}
|
|
787
872
|
}
|
|
788
873
|
|
|
789
|
-
function installAgents(cwd, globalInstall) {
|
|
874
|
+
function installAgents(cwd, globalInstall, selectedProject) {
|
|
790
875
|
const composed = composeAgentsInstructions(agentsContentRoot);
|
|
791
876
|
const targetPaths = [];
|
|
877
|
+
const root = globalInstall ? null : instructionProjectRoot(cwd, selectedProject);
|
|
792
878
|
if (globalInstall) {
|
|
793
879
|
targetPaths.push(...globalAgentsTargets.map((target) => path.join(os.homedir(), target)));
|
|
794
880
|
} else {
|
|
795
|
-
const targetPath = nearestAgentsFile(cwd);
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
881
|
+
const targetPath = nearestAgentsFile(cwd, root);
|
|
882
|
+
assertLocalDestination(root, targetPath, 'local agents instructions');
|
|
883
|
+
assertNoInstructionSymlink(root, targetPath, 'local agents instructions');
|
|
884
|
+
assertLocalInstructionSources(root, path.dirname(targetPath));
|
|
800
885
|
targetPaths.push(targetPath);
|
|
801
886
|
}
|
|
802
887
|
|
|
@@ -824,6 +909,97 @@ function installAgents(cwd, globalInstall) {
|
|
|
824
909
|
}
|
|
825
910
|
}
|
|
826
911
|
|
|
912
|
+
function projectInstructionPaths(cwd, selectedProject) {
|
|
913
|
+
const root = instructionProjectRoot(cwd, selectedProject);
|
|
914
|
+
const sourceDirectory = path.join(root, projectTemplateDirectory);
|
|
915
|
+
const templatePath = path.join(sourceDirectory, projectTemplateName);
|
|
916
|
+
const outputPath = path.join(root, 'AGENTS.md');
|
|
917
|
+
const paths = [
|
|
918
|
+
[sourceDirectory, 'project instruction source directory'],
|
|
919
|
+
[templatePath, 'project instruction template'],
|
|
920
|
+
[outputPath, 'project agents output']
|
|
921
|
+
];
|
|
922
|
+
for (const [target, label] of paths) {
|
|
923
|
+
assertLocalDestination(root, target, label);
|
|
924
|
+
assertNoInstructionSymlink(root, target, label);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (fs.existsSync(sourceDirectory) && !fs.statSync(sourceDirectory).isDirectory()) {
|
|
928
|
+
throw new CliError(`project instruction source path is not a directory: ${sourceDirectory}`);
|
|
929
|
+
}
|
|
930
|
+
if (fs.existsSync(templatePath) && !fs.statSync(templatePath).isFile()) {
|
|
931
|
+
throw new CliError(`project instruction template is not a file: ${templatePath}`);
|
|
932
|
+
}
|
|
933
|
+
if (fs.existsSync(outputPath) && !fs.statSync(outputPath).isFile()) {
|
|
934
|
+
throw new CliError(`project agents output is not a file: ${outputPath}`);
|
|
935
|
+
}
|
|
936
|
+
if (fs.existsSync(sourceDirectory)) {
|
|
937
|
+
for (const entry of fs.readdirSync(sourceDirectory, { withFileTypes: true })) {
|
|
938
|
+
if (!entry.name.endsWith('.block.md')) continue;
|
|
939
|
+
const sourcePath = path.join(sourceDirectory, entry.name);
|
|
940
|
+
const label = `project instruction source ${entry.name}`;
|
|
941
|
+
assertLocalDestination(root, sourcePath, label);
|
|
942
|
+
assertNoInstructionSymlink(root, sourcePath, label);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
return { root, sourceDirectory, templatePath, outputPath };
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function renderProjectAgents(cwd, selectedProject, { initialize = false, check = false } = {}) {
|
|
950
|
+
let paths = projectInstructionPaths(cwd, selectedProject);
|
|
951
|
+
if (initialize) {
|
|
952
|
+
fs.mkdirSync(paths.sourceDirectory, { recursive: true });
|
|
953
|
+
if (!fs.existsSync(paths.templatePath)) {
|
|
954
|
+
fs.writeFileSync(paths.templatePath, defaultProjectTemplate);
|
|
955
|
+
}
|
|
956
|
+
paths = projectInstructionPaths(cwd, selectedProject);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (!fs.existsSync(paths.templatePath)) {
|
|
960
|
+
throw new CliError(`project instruction template is missing; run "skillshop agents init" first: ${paths.templatePath}`);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const composed = composeAgentsInstructions(paths.sourceDirectory, projectTemplateName).content;
|
|
964
|
+
const existing = fs.existsSync(paths.outputPath) ? fs.readFileSync(paths.outputPath, 'utf8') : '';
|
|
965
|
+
const planned = applyProjectInstructions(existing, composed, packageJson.version);
|
|
966
|
+
|
|
967
|
+
if (check) {
|
|
968
|
+
if (fs.existsSync(paths.outputPath) && planned.text === existing) {
|
|
969
|
+
console.log(`Project agents instructions are current at ${paths.outputPath}`);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
console.error(
|
|
973
|
+
`Project agents instructions are ${fs.existsSync(paths.outputPath) ? 'stale' : 'missing'} at ${paths.outputPath}`
|
|
974
|
+
);
|
|
975
|
+
process.exitCode = 1;
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (planned.text === existing) {
|
|
980
|
+
console.log(`Project agents instructions already current at ${paths.outputPath}`);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
fs.writeFileSync(paths.outputPath, planned.text);
|
|
984
|
+
console.log(`${planned.action === 'refreshed' ? 'Refreshed' : 'Rendered'} project agents instructions at ${paths.outputPath}`);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
function projectAgentsConfigured(cwd, selectedProject) {
|
|
988
|
+
const root = instructionProjectRoot(cwd, selectedProject);
|
|
989
|
+
const sourceDirectory = path.join(root, projectTemplateDirectory);
|
|
990
|
+
const outputPath = path.join(root, 'AGENTS.md');
|
|
991
|
+
for (const candidate of [sourceDirectory, outputPath]) {
|
|
992
|
+
try {
|
|
993
|
+
const stat = fs.lstatSync(candidate);
|
|
994
|
+
if (candidate === sourceDirectory) return true;
|
|
995
|
+
if (stat.isFile() && hasProjectInstructions(fs.readFileSync(candidate, 'utf8'))) return true;
|
|
996
|
+
} catch (error) {
|
|
997
|
+
if (error.code !== 'ENOENT') throw error;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return false;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
827
1003
|
function installLocal(name, requestedVersion, cwd) {
|
|
828
1004
|
const root = findLockRoot(cwd) ?? projectRoot(cwd);
|
|
829
1005
|
const lock = readLock(root);
|
|
@@ -983,6 +1159,208 @@ function updateAllRepositories() {
|
|
|
983
1159
|
console.log(`Updated ${retained.length} registered ${retained.length === 1 ? 'repository' : 'repositories'}`);
|
|
984
1160
|
}
|
|
985
1161
|
|
|
1162
|
+
function assertNoManagedSymlink(base, target, label) {
|
|
1163
|
+
const boundary = path.resolve(base);
|
|
1164
|
+
const absoluteTarget = path.resolve(target);
|
|
1165
|
+
const relative = path.relative(boundary, absoluteTarget);
|
|
1166
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
1167
|
+
throw new CliError(`${label} resolves outside its managed root`);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
let current = boundary;
|
|
1171
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
1172
|
+
current = path.join(current, segment);
|
|
1173
|
+
try {
|
|
1174
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
1175
|
+
throw new CliError(`${label} contains a symlink path component`);
|
|
1176
|
+
}
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
if (error instanceof CliError) throw error;
|
|
1179
|
+
if (error.code === 'ENOENT') break;
|
|
1180
|
+
throw error;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function replaceManagedSkillDirectory(sourceDir, targetDir) {
|
|
1186
|
+
const temporaryDir = `${targetDir}.skillshop-upgrade-tmp-${process.pid}`;
|
|
1187
|
+
const backupDir = `${targetDir}.skillshop-upgrade-backup-${process.pid}`;
|
|
1188
|
+
fs.rmSync(temporaryDir, { recursive: true, force: true });
|
|
1189
|
+
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
1190
|
+
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
1191
|
+
fs.cpSync(sourceDir, temporaryDir, { recursive: true, force: true });
|
|
1192
|
+
|
|
1193
|
+
const hadTarget = fs.existsSync(targetDir);
|
|
1194
|
+
if (hadTarget) fs.renameSync(targetDir, backupDir);
|
|
1195
|
+
try {
|
|
1196
|
+
fs.renameSync(temporaryDir, targetDir);
|
|
1197
|
+
fs.rmSync(backupDir, { recursive: true, force: true });
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
if (hadTarget && !fs.existsSync(targetDir) && fs.existsSync(backupDir)) {
|
|
1200
|
+
fs.renameSync(backupDir, targetDir);
|
|
1201
|
+
}
|
|
1202
|
+
throw error;
|
|
1203
|
+
} finally {
|
|
1204
|
+
fs.rmSync(temporaryDir, { recursive: true, force: true });
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function upgradeSummary() {
|
|
1209
|
+
return { updated: 0, unchanged: 0, missing: 0, failed: [] };
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function validateManagedCopy(root, target, name) {
|
|
1213
|
+
assertNoManagedSymlink(root, target, `managed ${name} copy`);
|
|
1214
|
+
let targetStats;
|
|
1215
|
+
try {
|
|
1216
|
+
targetStats = fs.lstatSync(target);
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
if (error.code === 'ENOENT') return false;
|
|
1219
|
+
throw error;
|
|
1220
|
+
}
|
|
1221
|
+
if (!targetStats.isDirectory()) throw new CliError(`${target} is not a managed skill directory`);
|
|
1222
|
+
|
|
1223
|
+
const manifestPath = path.join(target, 'SKILL.md');
|
|
1224
|
+
assertNoManagedSymlink(root, manifestPath, `managed ${name} manifest`);
|
|
1225
|
+
let manifestStats;
|
|
1226
|
+
try {
|
|
1227
|
+
manifestStats = fs.lstatSync(manifestPath);
|
|
1228
|
+
} catch (error) {
|
|
1229
|
+
if (error.code === 'ENOENT') throw new CliError(`${manifestPath} is missing from a recorded skill copy`);
|
|
1230
|
+
throw error;
|
|
1231
|
+
}
|
|
1232
|
+
if (!manifestStats.isFile()) throw new CliError(`${manifestPath} is not a regular skill manifest`);
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function refreshManagedLock(root, lock, { global = false, summary }) {
|
|
1237
|
+
let changed = false;
|
|
1238
|
+
for (const name of Object.keys(lock.skills).sort()) {
|
|
1239
|
+
const entry = lock.skills[name];
|
|
1240
|
+
try {
|
|
1241
|
+
const metadata = skillMetadata(name);
|
|
1242
|
+
const targets = global ? globalTargets(name, entry) : lockedTargets(root, name, entry);
|
|
1243
|
+
const base = global ? os.homedir() : root;
|
|
1244
|
+
const existingTargets = [];
|
|
1245
|
+
for (const target of targets) {
|
|
1246
|
+
if (validateManagedCopy(base, target, name)) existingTargets.push(target);
|
|
1247
|
+
else summary.missing += 1;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (!existingTargets.length) {
|
|
1251
|
+
delete lock.skills[name];
|
|
1252
|
+
changed = true;
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
const successfulTargets = [];
|
|
1257
|
+
let skillFailed = false;
|
|
1258
|
+
for (const target of existingTargets) {
|
|
1259
|
+
const recordedVersion = global ? entry.copies?.[target]?.version : entry.version;
|
|
1260
|
+
if (recordedVersion === metadata.version) {
|
|
1261
|
+
summary.unchanged += 1;
|
|
1262
|
+
successfulTargets.push(target);
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
try {
|
|
1266
|
+
replaceManagedSkillDirectory(metadata.sourceDir, target);
|
|
1267
|
+
summary.updated += 1;
|
|
1268
|
+
successfulTargets.push(target);
|
|
1269
|
+
} catch (error) {
|
|
1270
|
+
summary.failed.push(`${name} at ${target}: ${error.message}`);
|
|
1271
|
+
skillFailed = true;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
if (global) {
|
|
1276
|
+
const keptTargets = skillFailed ? targets : successfulTargets;
|
|
1277
|
+
entry.paths = keptTargets;
|
|
1278
|
+
entry.copies ??= {};
|
|
1279
|
+
for (const target of successfulTargets) {
|
|
1280
|
+
const previous = entry.copies[target] ?? {};
|
|
1281
|
+
entry.copies[target] = { ...previous, author: metadata.author, version: metadata.version };
|
|
1282
|
+
}
|
|
1283
|
+
if (!skillFailed) changed = true;
|
|
1284
|
+
if (!entry.paths.length) {
|
|
1285
|
+
delete lock.skills[name];
|
|
1286
|
+
changed = true;
|
|
1287
|
+
}
|
|
1288
|
+
} else if (!skillFailed) {
|
|
1289
|
+
entry.paths = successfulTargets.map((target) => relativePath(root, target)).sort();
|
|
1290
|
+
entry.author = metadata.author;
|
|
1291
|
+
entry.version = metadata.version;
|
|
1292
|
+
changed = true;
|
|
1293
|
+
} else if (existingTargets.length !== targets.length) {
|
|
1294
|
+
entry.paths = existingTargets.map((target) => relativePath(root, target)).sort();
|
|
1295
|
+
changed = true;
|
|
1296
|
+
}
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
summary.failed.push(`${name}: ${error.message}`);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (changed) {
|
|
1303
|
+
try {
|
|
1304
|
+
if (global) writeGlobalSkills(lock);
|
|
1305
|
+
else writeLock(root, lock);
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
summary.failed.push(`could not save ${global ? 'global skill manifest' : path.join(root, lockFileName)}: ${error.message}`);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function refreshManagedCopies(scope) {
|
|
1313
|
+
const summary = upgradeSummary();
|
|
1314
|
+
if (scope === 'local') {
|
|
1315
|
+
const owner = localPackageOwner();
|
|
1316
|
+
if (!owner) {
|
|
1317
|
+
summary.failed.push('fresh Skillshop package is no longer attached to a local npm dependency');
|
|
1318
|
+
} else {
|
|
1319
|
+
try {
|
|
1320
|
+
const lock = readLock(owner.root);
|
|
1321
|
+
refreshManagedLock(owner.root, lock, { summary });
|
|
1322
|
+
} catch (error) {
|
|
1323
|
+
summary.failed.push(`${owner.root}: ${error.message}`);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
} else if (scope === 'global') {
|
|
1327
|
+
try {
|
|
1328
|
+
const globalLock = readGlobalSkills();
|
|
1329
|
+
refreshManagedLock(null, globalLock, { global: true, summary });
|
|
1330
|
+
} catch (error) {
|
|
1331
|
+
summary.failed.push(`global managed copies: ${error.message}`);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
try {
|
|
1335
|
+
const registry = readRegistry();
|
|
1336
|
+
for (const root of registry.repositories) {
|
|
1337
|
+
if (!hasLock(root)) continue;
|
|
1338
|
+
try {
|
|
1339
|
+
const lock = readLock(root);
|
|
1340
|
+
refreshManagedLock(root, lock, { summary });
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
summary.failed.push(`${root}: ${error.message}`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
summary.failed.push(`registered project copies: ${error.message}`);
|
|
1347
|
+
}
|
|
1348
|
+
} else {
|
|
1349
|
+
summary.failed.push(`unsupported managed-copy scope: ${scope}`);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
console.log(
|
|
1353
|
+
`Managed-copy refresh summary: ${summary.updated} updated, ${summary.unchanged} unchanged, ${summary.missing} missing paths pruned, ${summary.failed.length} failed.`
|
|
1354
|
+
);
|
|
1355
|
+
if (summary.failed.length) {
|
|
1356
|
+
for (const failure of summary.failed) console.error(`skillshop: upgrade refresh: ${failure}`);
|
|
1357
|
+
console.error('skillshop: npm upgrade succeeded, but managed-copy refresh was incomplete');
|
|
1358
|
+
process.exitCode = 1;
|
|
1359
|
+
} else {
|
|
1360
|
+
console.log(`Refreshed Skillshop-managed copies for ${scope} scope.`);
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
986
1364
|
const migrationSkipDirectories = new Set([
|
|
987
1365
|
'.git',
|
|
988
1366
|
'.skillshop',
|
|
@@ -1074,6 +1452,8 @@ function parseArgs(argv) {
|
|
|
1074
1452
|
let pushUpdate = false;
|
|
1075
1453
|
const agentsTargets = [];
|
|
1076
1454
|
let requestedVersion = null;
|
|
1455
|
+
let selectedProject = null;
|
|
1456
|
+
let checkProject = false;
|
|
1077
1457
|
|
|
1078
1458
|
for (let index = 0; index < argv.length; index += 1) {
|
|
1079
1459
|
const argument = argv[index];
|
|
@@ -1081,6 +1461,15 @@ function parseArgs(argv) {
|
|
|
1081
1461
|
globalInstall = true;
|
|
1082
1462
|
} else if (argument === '--push') {
|
|
1083
1463
|
pushUpdate = true;
|
|
1464
|
+
} else if (argument === '--check') {
|
|
1465
|
+
checkProject = true;
|
|
1466
|
+
} else if (argument === '--project') {
|
|
1467
|
+
selectedProject = argv[index + 1];
|
|
1468
|
+
if (!selectedProject) throw new CliError('--project requires a directory');
|
|
1469
|
+
index += 1;
|
|
1470
|
+
} else if (argument.startsWith('--project=')) {
|
|
1471
|
+
selectedProject = argument.slice('--project='.length);
|
|
1472
|
+
if (!selectedProject) throw new CliError('--project requires a directory');
|
|
1084
1473
|
} else if (argument === '--codex' || argument === '--copilot') {
|
|
1085
1474
|
throw new CliError(`${argument} is no longer supported; use agents --global`);
|
|
1086
1475
|
} else if (argument === '--version' || argument === '-v') {
|
|
@@ -1097,7 +1486,15 @@ function parseArgs(argv) {
|
|
|
1097
1486
|
}
|
|
1098
1487
|
}
|
|
1099
1488
|
|
|
1100
|
-
return {
|
|
1489
|
+
return {
|
|
1490
|
+
positional,
|
|
1491
|
+
globalInstall,
|
|
1492
|
+
pushUpdate,
|
|
1493
|
+
requestedVersion,
|
|
1494
|
+
selectedProject,
|
|
1495
|
+
checkProject,
|
|
1496
|
+
agentsTargets: [...new Set(agentsTargets)]
|
|
1497
|
+
};
|
|
1101
1498
|
}
|
|
1102
1499
|
|
|
1103
1500
|
function listSkills() {
|
|
@@ -1107,8 +1504,128 @@ function listSkills() {
|
|
|
1107
1504
|
}
|
|
1108
1505
|
}
|
|
1109
1506
|
|
|
1507
|
+
function packageDependencySection(manifest) {
|
|
1508
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
1509
|
+
if (Object.hasOwn(manifest?.[section] ?? {}, 'skillshop')) return section;
|
|
1510
|
+
}
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function localPackageOwner() {
|
|
1515
|
+
const installedPackageRoot = canonicalPath(packageRoot);
|
|
1516
|
+
let current = path.dirname(installedPackageRoot);
|
|
1517
|
+
while (true) {
|
|
1518
|
+
const manifestPath = path.join(current, 'package.json');
|
|
1519
|
+
if (fs.existsSync(manifestPath)) {
|
|
1520
|
+
try {
|
|
1521
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
1522
|
+
const section = packageDependencySection(manifest);
|
|
1523
|
+
const declaredInstall = path.join(current, 'node_modules', 'skillshop');
|
|
1524
|
+
if (section && fs.existsSync(declaredInstall) && canonicalPath(declaredInstall) === installedPackageRoot) {
|
|
1525
|
+
return { root: current, section };
|
|
1526
|
+
}
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
if (error instanceof SyntaxError) throw new CliError(`cannot parse ${manifestPath}: ${error.message}`);
|
|
1529
|
+
throw error;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
const parent = path.dirname(current);
|
|
1534
|
+
if (parent === current) return null;
|
|
1535
|
+
current = parent;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function npmGlobalRoot() {
|
|
1540
|
+
try {
|
|
1541
|
+
return path.resolve(execFileSync('npm', ['root', '--global'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim());
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
const detail = error.stderr?.toString().trim();
|
|
1544
|
+
throw new CliError(`cannot determine the active npm global package directory${detail ? `: ${detail}` : ''}`);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function npmPrefixFromRoot(globalRoot) {
|
|
1549
|
+
const parent = path.dirname(globalRoot);
|
|
1550
|
+
return path.basename(parent) === 'lib' ? path.dirname(parent) : parent;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function resolveUpgradeScope() {
|
|
1554
|
+
const localOwner = localPackageOwner();
|
|
1555
|
+
if (localOwner) return { type: 'local', root: localOwner.root, dependencySection: localOwner.section, packageRoot };
|
|
1556
|
+
|
|
1557
|
+
const insideNodeModules = canonicalPath(packageRoot).split(path.sep).includes('node_modules');
|
|
1558
|
+
const activeGlobalRoot = npmGlobalRoot();
|
|
1559
|
+
if (canonicalPath(path.join(activeGlobalRoot, 'skillshop')) === canonicalPath(packageRoot)) {
|
|
1560
|
+
return { type: 'global', prefix: npmPrefixFromRoot(activeGlobalRoot), packageRoot };
|
|
1561
|
+
}
|
|
1562
|
+
if (insideNodeModules) {
|
|
1563
|
+
throw new CliError(
|
|
1564
|
+
`cannot match this installed Skillshop package to the active npm global prefix (${activeGlobalRoot}); no upgrade was run`
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
// Preserve the historical source-checkout behavior: install the package into
|
|
1569
|
+
// the active npm global prefix without editing the checkout's package files.
|
|
1570
|
+
return { type: 'global-checkout', packageRoot };
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function runUpgradeInstall(scope) {
|
|
1574
|
+
const args = scope.type === 'local'
|
|
1575
|
+
? ['install', `--save-${scope.dependencySection === 'dependencies' ? 'prod' : scope.dependencySection === 'devDependencies' ? 'dev' : scope.dependencySection === 'optionalDependencies' ? 'optional' : 'peer'}`, 'skillshop@latest']
|
|
1576
|
+
: scope.type === 'global'
|
|
1577
|
+
? ['install', '--global', '--prefix', scope.prefix, 'skillshop@latest']
|
|
1578
|
+
: ['install', '--global', 'skillshop@latest'];
|
|
1579
|
+
try {
|
|
1580
|
+
execFileSync('npm', args, {
|
|
1581
|
+
cwd: scope.type === 'local' ? scope.root : process.cwd(),
|
|
1582
|
+
stdio: 'inherit'
|
|
1583
|
+
});
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
const status = Number.isInteger(error.status) ? ` (exit ${error.status})` : '';
|
|
1586
|
+
throw new CliError(`npm upgrade failed${status}; managed copies were not refreshed`);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function freshUpgradePackageRoot(scope) {
|
|
1591
|
+
if (scope.type !== 'global-checkout') return scope.packageRoot;
|
|
1592
|
+
return path.join(npmGlobalRoot(), 'skillshop');
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1110
1595
|
function upgrade() {
|
|
1111
|
-
|
|
1596
|
+
const scope = resolveUpgradeScope();
|
|
1597
|
+
const scopeLabel = scope.type === 'local'
|
|
1598
|
+
? `project dependency in ${scope.root}`
|
|
1599
|
+
: scope.type === 'global'
|
|
1600
|
+
? `global installation at ${scope.packageRoot}`
|
|
1601
|
+
: 'global installation (source-checkout compatibility mode)';
|
|
1602
|
+
console.log(`Upgrading Skillshop ${scopeLabel}...`);
|
|
1603
|
+
runUpgradeInstall(scope);
|
|
1604
|
+
|
|
1605
|
+
let freshCliPath;
|
|
1606
|
+
try {
|
|
1607
|
+
freshCliPath = path.join(freshUpgradePackageRoot(scope), 'src', 'cli.js');
|
|
1608
|
+
if (!fs.statSync(freshCliPath).isFile()) throw new Error('not a file');
|
|
1609
|
+
} catch {
|
|
1610
|
+
throw new CliError('npm upgrade succeeded, but the newly installed Skillshop CLI could not be found for managed-copy refresh');
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
const child = spawnSync(process.execPath, [freshCliPath, internalUpgradeRefreshCommand], {
|
|
1614
|
+
cwd: scope.type === 'local' ? scope.root : process.cwd(),
|
|
1615
|
+
env: {
|
|
1616
|
+
...process.env,
|
|
1617
|
+
SKILLSHOP_INTERNAL_UPGRADE_TOKEN: internalUpgradeRefreshToken,
|
|
1618
|
+
SKILLSHOP_INTERNAL_UPGRADE_SCOPE: scope.type === 'local' ? 'local' : 'global'
|
|
1619
|
+
},
|
|
1620
|
+
stdio: 'inherit'
|
|
1621
|
+
});
|
|
1622
|
+
if (child.error) {
|
|
1623
|
+
throw new CliError(`npm upgrade succeeded, but fresh Skillshop managed-copy refresh could not start: ${child.error.message}`);
|
|
1624
|
+
}
|
|
1625
|
+
if (child.status !== 0) {
|
|
1626
|
+
const status = child.status === null ? 'without an exit status' : `with exit ${child.status}`;
|
|
1627
|
+
throw new CliError(`npm upgrade succeeded, but fresh Skillshop managed-copy refresh failed ${status}`);
|
|
1628
|
+
}
|
|
1112
1629
|
}
|
|
1113
1630
|
|
|
1114
1631
|
async function main(argv) {
|
|
@@ -1121,9 +1638,25 @@ async function main(argv) {
|
|
|
1121
1638
|
return;
|
|
1122
1639
|
}
|
|
1123
1640
|
|
|
1124
|
-
const { positional, globalInstall, pushUpdate, requestedVersion, agentsTargets } = parseArgs(argv);
|
|
1641
|
+
const { positional, globalInstall, pushUpdate, requestedVersion, selectedProject, checkProject, agentsTargets } = parseArgs(argv);
|
|
1125
1642
|
const command = positional[0];
|
|
1126
1643
|
|
|
1644
|
+
if ((selectedProject || checkProject) && command !== 'agents') {
|
|
1645
|
+
throw new CliError('--project and --check are only valid with agents');
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
if (command === internalUpgradeRefreshCommand) {
|
|
1649
|
+
if (
|
|
1650
|
+
process.env.SKILLSHOP_INTERNAL_UPGRADE_TOKEN !== internalUpgradeRefreshToken ||
|
|
1651
|
+
positional.length !== 1 ||
|
|
1652
|
+
requestedVersion || globalInstall || pushUpdate || selectedProject || checkProject || agentsTargets.length
|
|
1653
|
+
) {
|
|
1654
|
+
throw new CliError('invalid internal upgrade continuation');
|
|
1655
|
+
}
|
|
1656
|
+
refreshManagedCopies(process.env.SKILLSHOP_INTERNAL_UPGRADE_SCOPE);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1127
1660
|
if (command === 'init') {
|
|
1128
1661
|
if (positional.length !== 1 || requestedVersion || globalInstall || pushUpdate || agentsTargets.length) {
|
|
1129
1662
|
throw new CliError('init does not accept arguments or options');
|
|
@@ -1178,10 +1711,32 @@ async function main(argv) {
|
|
|
1178
1711
|
}
|
|
1179
1712
|
|
|
1180
1713
|
if (command === 'agents') {
|
|
1181
|
-
|
|
1714
|
+
const initializeProject = positional.length === 2 && positional[1] === 'init';
|
|
1715
|
+
if (
|
|
1716
|
+
positional.length > 2 ||
|
|
1717
|
+
(positional.length === 2 && !initializeProject) ||
|
|
1718
|
+
requestedVersion ||
|
|
1719
|
+
pushUpdate ||
|
|
1720
|
+
agentsTargets.length
|
|
1721
|
+
) {
|
|
1182
1722
|
throw new CliError('agents does not accept skill or update options');
|
|
1183
1723
|
}
|
|
1184
|
-
|
|
1724
|
+
if (globalInstall) {
|
|
1725
|
+
if (initializeProject || selectedProject || checkProject) {
|
|
1726
|
+
throw new CliError('--global cannot be combined with project agents options');
|
|
1727
|
+
}
|
|
1728
|
+
installAgents(process.cwd(), true);
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
if (initializeProject && checkProject) throw new CliError('agents init does not accept --check');
|
|
1732
|
+
if (initializeProject || checkProject || projectAgentsConfigured(process.cwd(), selectedProject)) {
|
|
1733
|
+
renderProjectAgents(process.cwd(), selectedProject, {
|
|
1734
|
+
initialize: initializeProject,
|
|
1735
|
+
check: checkProject
|
|
1736
|
+
});
|
|
1737
|
+
} else {
|
|
1738
|
+
installAgents(process.cwd(), false, selectedProject);
|
|
1739
|
+
}
|
|
1185
1740
|
return;
|
|
1186
1741
|
}
|
|
1187
1742
|
|
|
@@ -1214,13 +1769,46 @@ async function main(argv) {
|
|
|
1214
1769
|
else installLocal(name, version, process.cwd());
|
|
1215
1770
|
}
|
|
1216
1771
|
|
|
1772
|
+
function isIsolatedAgentsInvocation(argv) {
|
|
1773
|
+
// Preserve the top-level help/version routing before looking for a command.
|
|
1774
|
+
// main() handles help anywhere and the standalone version form without parsing.
|
|
1775
|
+
if (argv[0] === '--help' || argv[0] === '-h') return false;
|
|
1776
|
+
if (argv.length === 1 && (argv[0] === '--version' || argv[0] === '-v')) return false;
|
|
1777
|
+
|
|
1778
|
+
// parseArgs accepts project options on either side of the command. Ignore help
|
|
1779
|
+
// flags here because main() handles them before parsing, then classify using
|
|
1780
|
+
// the same positional-command rules as execution.
|
|
1781
|
+
try {
|
|
1782
|
+
const args = argv.filter((argument) => argument !== '--help' && argument !== '-h');
|
|
1783
|
+
return parseArgs(args).positional[0] === 'agents';
|
|
1784
|
+
} catch {
|
|
1785
|
+
return false;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
function isUpgradeFlowInvocation(argv) {
|
|
1790
|
+
if (argv.includes('--help') || argv.includes('-h')) return false;
|
|
1791
|
+
if (
|
|
1792
|
+
argv[0] === internalUpgradeRefreshCommand &&
|
|
1793
|
+
process.env.SKILLSHOP_INTERNAL_UPGRADE_TOKEN === internalUpgradeRefreshToken
|
|
1794
|
+
) return true;
|
|
1795
|
+
try {
|
|
1796
|
+
return parseArgs(argv).positional[0] === 'upgrade';
|
|
1797
|
+
} catch {
|
|
1798
|
+
return false;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1217
1802
|
export async function runLegacyCli(argv) {
|
|
1803
|
+
const isolatedAgentsCommand = isIsolatedAgentsInvocation(argv);
|
|
1804
|
+
const upgradeFlow = isUpgradeFlowInvocation(argv);
|
|
1805
|
+
const skipLegacyReconciliation = isolatedAgentsCommand || upgradeFlow;
|
|
1218
1806
|
try {
|
|
1219
|
-
reconcileCurrentRepository();
|
|
1807
|
+
if (!skipLegacyReconciliation) reconcileCurrentRepository();
|
|
1220
1808
|
try {
|
|
1221
1809
|
await main(argv);
|
|
1222
1810
|
} finally {
|
|
1223
|
-
reconcileCurrentRepository();
|
|
1811
|
+
if (!skipLegacyReconciliation) reconcileCurrentRepository();
|
|
1224
1812
|
}
|
|
1225
1813
|
} catch (error) {
|
|
1226
1814
|
if (!(error instanceof CliError) && !(error instanceof AgentsInstructionsError)) throw error;
|