@epicparty/skills 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -65,7 +65,7 @@ The skills in this repository are written with the prefix `ep`, and the router's
65
65
  - links into a sibling skill's folder, `../<skill>/`, to `../<prefix>-<skill>/`, and `../router/` to `../<prefix>/`;
66
66
  - each `SKILL.md`'s `name` to its installed name: `router` becomes `<prefix>`.
67
67
 
68
- Then it checks the result: every name matches its folder, every link and heading anchor resolves, and every skill named in the text is installed. Each skill also carries `pack: epicparty/skills` under `metadata`, which is how `/<prefix>-setup` finds the pack's skills whatever the prefix.
68
+ Then it checks the result: every name matches its folder, every link and heading anchor resolves, and every skill named in the text is installed. Only a pack skill's name counts as a skill reference, so any prefix works, including one that starts English words: with `up`, "an up-to-date base" stays text. Each skill also carries `pack: epicparty/skills` under `metadata`, which is how `/<prefix>-setup` finds the pack's skills whatever the prefix.
69
69
 
70
70
  `npx @epicparty/skills check` runs the same check on an installed project.
71
71
 
@@ -99,9 +99,10 @@ Releases run on [Changesets](https://github.com/changesets/changesets), the way
99
99
 
100
100
  1. **Every pull request that changes the pack adds a changeset** — `npx changeset` — whose entry starts with the skills it changes. [`.changeset/README.md`](./.changeset/README.md) has the format and the bump levels. CI fails a pull request that changes a skill and names it in no changeset.
101
101
  2. **On each push to `main`**, the Release workflow opens or updates a `chore: release` pull request. It bumps the pack's version and each named skill's, and writes `CHANGELOG.md`.
102
- 3. **Merging that pull request is the release.** The workflow tags `v<version>` and publishes a GitHub release from the changelog.
102
+ 3. **Merging that pull request stages the release.** The workflow stages the new version on npm through [trusted publishing](https://docs.npmjs.com/trusted-publishers) — no npm token is stored anywhere — tags `v<version>`, and publishes a GitHub release from the changelog.
103
+ 4. **A maintainer approves it with 2FA**, and only then can anyone install it: on npmjs.com under the package's staged versions, or `npm stage list @epicparty/skills`, then `npm stage approve <stage-id>`. The run's summary says so. If staging failed, run the Release workflow by hand once the cause is fixed.
103
104
 
104
- Until npm publishing is on, a project installs a release from GitHub: `npx github:epicparty/skills#v<version> add --prefix acme`. To turn npm on, set up [trusted publishing](https://docs.npmjs.com/trusted-publishers) for `@epicparty/skills`, add `id-token: write` to the workflow's permissions, and change its `publish-script` to `npx changeset publish`.
105
+ npm must trust the workflow to stage, and only to stage: on npmjs.com, `@epicparty/skills` → **Settings** → **Trusted publishing** names GitHub Actions, `epicparty/skills`, `release.yml`, no environment, with **Allow npm publish** unticked. A run that could publish directly would skip the 2FA approval.
105
106
 
106
107
  The GitHub repository must allow it: **Settings → Actions → General → Allow GitHub Actions to create and approve pull requests**.
107
108
 
package/bin/cli.mjs CHANGED
@@ -7,7 +7,7 @@ import { dirname, join, resolve } from 'node:path';
7
7
  import { createInterface } from 'node:readline/promises';
8
8
  import { parseArgs } from 'node:util';
9
9
  import { fileURLToPath } from 'node:url';
10
- import { build, check, packSkills, skillVersion, validatePrefix } from '../lib/transform.mjs';
10
+ import { build, check, checkSource, packSkills, skillVersion, validatePrefix } from '../lib/transform.mjs';
11
11
 
12
12
  const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
13
13
  const PKG = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'));
@@ -100,6 +100,8 @@ function sourceDir(from) {
100
100
  }
101
101
 
102
102
  function buildChecked(source, prefix, out) {
103
+ const sourceProblems = checkSource(source);
104
+ if (sourceProblems.length) fail(`the skills in ${source} are inconsistent:\n ${sourceProblems.join('\n ')}`);
103
105
  const names = build(source, prefix, out);
104
106
  const problems = check(out, prefix, { canonicalSkills: packSkills(source) });
105
107
  if (problems.length) fail(`the built skills are inconsistent:\n ${problems.join('\n ')}`);
@@ -167,7 +169,7 @@ else if (command === 'update') {
167
169
  const names = buildChecked(sourceDir(opts.from), validatePrefix(opts.prefix), resolve(opts.out));
168
170
  console.log(`Built ${names.length} skills in ${resolve(opts.out)}`);
169
171
  } else if (command === 'check') {
170
- const prefix = validatePrefix(opts.prefix ?? readLock(cwd)?.prefix ?? fail('check needs --prefix'));
172
+ const prefix = validatePrefix(opts.prefix ?? readLock(cwd)?.prefix ?? fail(`no ${LOCK} here to read the prefix from: pass --prefix <p>`));
171
173
  const dir = resolve(rest[0] ?? '.agents/skills');
172
174
  const problems = check(dir, prefix, { canonicalSkills: packSkills(sourceDir(opts.from)) });
173
175
  if (problems.length) fail(`${problems.length} problem(s):\n ${problems.join('\n ')}`);
package/lib/transform.mjs CHANGED
@@ -130,12 +130,42 @@ function anchors(text) {
130
130
  return out;
131
131
  }
132
132
 
133
+ /** Matches a skill reference written with `prefix`: `<prefix>-<name>` or `/<prefix>-<name>`, or `<prefix>-*`. */
134
+ function referenceRe(prefix, names) {
135
+ return new RegExp(`(?<![\\w-])/?${escape(prefix)}-(${names}|\\*)(?![\\w-])`, 'g');
136
+ }
137
+
133
138
  /**
134
- * Checks installed pack skills in dir (folders named <prefix>-<skill> and <prefix>).
139
+ * Checks the pack's own source: every `ep-<name>` it writes names a skill of the pack. The
140
+ * canonical prefix is the pack's to choose, so in the source any `ep-<name>` is a reference, and
141
+ * one the rewrite does not know (a typo, a removed skill) is reported here.
135
142
  * Returns a list of problems; empty means consistent.
136
143
  */
144
+ export function checkSource(sourceDir) {
145
+ const skills = new Set(packSkills(sourceDir));
146
+ const problems = [];
147
+ for (const file of walk(sourceDir).filter((f) => f.endsWith('.md'))) {
148
+ for (const m of readFileSync(file, 'utf8').matchAll(referenceRe(CANONICAL, '[a-z0-9]+(?:-[a-z0-9]+)*'))) {
149
+ if (m[1] !== '*' && (m[1] === ROUTER || !skills.has(m[1]))) {
150
+ problems.push(`${relative(sourceDir, file)}: names ${m[0]}, which is not a skill in the pack`);
151
+ }
152
+ }
153
+ }
154
+ return problems;
155
+ }
156
+
157
+ /**
158
+ * Checks installed pack skills in dir (folders named <prefix>-<skill> and <prefix>), given the
159
+ * pack's skill folders. Returns a list of problems; empty means consistent.
160
+ *
161
+ * A `<prefix>-<name>` counts as a skill reference only when <name> is one of the pack's skills,
162
+ * because only those are what the rewrite produces. Any other `<prefix>-<word>` is the text's own:
163
+ * with the prefix `up`, "an up-to-date base" is English, not a skill named `to-date`.
164
+ * `checkSource` catches a source reference to a skill the pack does not have.
165
+ */
137
166
  export function check(dir, prefix, { canonicalSkills } = {}) {
138
167
  validatePrefix(prefix);
168
+ if (!canonicalSkills?.length) throw new TypeError('check needs canonicalSkills: the skill folders of the pack');
139
169
  const problems = [];
140
170
  const installed = readdirSync(dir).filter((name) => {
141
171
  const file = join(dir, name, 'SKILL.md');
@@ -143,10 +173,14 @@ export function check(dir, prefix, { canonicalSkills } = {}) {
143
173
  });
144
174
  const set = new Set(installed);
145
175
  if (!set.has(prefix)) problems.push(`router "${prefix}" is not installed`);
146
- const nameRe = new RegExp(`(?<![\\w-])/?${escape(prefix)}-([a-z0-9]+(?:-[a-z0-9]+)*|\\*)(?![\\w-])`, 'g');
147
- const canonical = canonicalSkills?.filter((s) => s !== ROUTER).map(escape).join('|');
176
+ const canonical = canonicalSkills
177
+ .filter((s) => s !== ROUTER)
178
+ .sort((a, b) => b.length - a.length)
179
+ .map(escape)
180
+ .join('|');
181
+ const nameRe = referenceRe(prefix, canonical);
148
182
  const leftovers = [];
149
- if (prefix !== CANONICAL && canonical) leftovers.push(`(?<![\\w-])${CANONICAL}-(?:${canonical})(?![\\w-])`, `(?<![\\w/.-])/${CANONICAL}(?![\\w-])`);
183
+ if (prefix !== CANONICAL) leftovers.push(`(?<![\\w-])${CANONICAL}-(?:${canonical})(?![\\w-])`, `(?<![\\w/.-])/${CANONICAL}(?![\\w-])`);
150
184
  if (prefix !== ROUTER) leftovers.push(`\\.\\./${ROUTER}/`);
151
185
  const leftover = leftovers.length ? new RegExp(leftovers.join('|'), 'g') : null;
152
186
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epicparty/skills",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "A delivery workflow for coding agents — explore, story, implement, PR, ship — installed under your project's own prefix.",
5
5
  "license": "MIT",
6
6
  "type": "module",