@duyquangnvx/create-agent-cli 0.8.0 → 0.9.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/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ What changed in each release of `@duyquangnvx/agent-cli` (the Runtime) and `@duy
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## v0.9.1 (2026-10-20)
8
+
9
+ ### Fixed
10
+
11
+ - A clean build of the Scaffolder, and of a scaffolded CLI, stamps its commit's time as `builtAt` instead of the time it was built, so rebuilding a commit gives the same bundle and a rerun of a release cut off after its publish can check what it published. A dirty build still stamps the time it was built. v0.9.0 of `@duyquangnvx/create-agent-cli` was published, but its release could not finish for this reason, so its skill never reached the Skill Hub.
12
+
13
+ ## v0.9.0 (2026-10-20)
14
+
15
+ ### Added
16
+
17
+ - The Scaffolder's `--skill-hub <git URL>` names the Skill Hub each release commits the new CLI's skill to: it becomes `skill.source` in `defineCli`, `skillHub` in `scripts/release.ts` and the README's skill line. See ADR 0009. It refuses a URL that does not end in `.git` or holds a query or fragment, and with it a name the release cannot put in a hub (`SKILL_NAME_INVALID`).
18
+
19
+ ### Changed
20
+
21
+ - **Breaking:** `doctor`'s `skill` row and `update`'s `next` install the skill only from a declared `skill.source`, no longer from package.json `repository` at `#release`, a branch `@duyquangnvx/release` 0.4 stops moving. A CLI without `skill.source` still gets `skills update` for an older copy.
22
+ - **Breaking:** the Scaffolder's `--repository` only sets package.json `repository`; the skill line comes from `--skill-hub`. The scaffolded template releases with `@duyquangnvx/release` 0.4.0.
23
+ - **Breaking:** the agent-cli skill installs from the Skill Hub: `npx skills add https://github.com/duyquangnvx/skills.git --skill agent-cli -g`.
24
+
7
25
  ## v0.8.0 (2026-10-20)
8
26
 
9
27
  ### Changed
package/README.md CHANGED
@@ -3,9 +3,9 @@
3
3
  Creates a new Agent CLI repository already wired to [`@duyquangnvx/agent-cli`](https://www.npmjs.com/package/@duyquangnvx/agent-cli):
4
4
 
5
5
  ```sh
6
- pnpm create @duyquangnvx/agent-cli <name> --description "One line saying what it does" --repository https://github.com/<owner>/<name>
6
+ pnpm create @duyquangnvx/agent-cli <name> --description "One line saying what it does" --skill-hub https://github.com/<owner>/skills.git
7
7
  ```
8
8
 
9
- The new repository has an Error table, a port, two example Commands (one writing a file at a cost) and their test, a bundled build that stamps the commit and build time into `--version`, CI, a release script, a CHANGELOG, an `AGENTS.md`, and a skeleton skill for the new CLI whose setup reference installs, checks and updates it, so a user installs the skill first and lets their agent do the rest. `--repository` names where that skill is installed from. Pass `--dry-run` to see what it would write.
9
+ The new repository has an Error table, a port, two example Commands (one writing a file at a cost) and their test, a bundled build that stamps the commit and build time into `--version`, CI, a release script, a CHANGELOG, an `AGENTS.md`, and a skeleton skill for the new CLI whose setup reference installs, checks and updates it, so a user installs the skill first and lets their agent do the rest. `--skill-hub` names the Skill Hub each release commits that skill to and agents install it from. Pass `--dry-run` to see what it would write.
10
10
 
11
- See the [repository](https://github.com/duyquangnvx/cli-for-agent) for an overview and the [agent-cli skill](https://github.com/duyquangnvx/cli-for-agent/blob/release/skills/agent-cli/SKILL.md) for how to build on it.
11
+ See the [repository](https://github.com/duyquangnvx/cli-for-agent) for an overview and the [agent-cli skill](https://github.com/duyquangnvx/cli-for-agent/blob/main/skills/agent-cli/SKILL.md) for how to build on it.
package/dist/bin.js CHANGED
@@ -7049,36 +7049,8 @@ async function globalInstallOf(bin, ctx) {
7049
7049
  };
7050
7050
  }
7051
7051
  //#endregion
7052
- //#region ../agent-cli/dist/manifest.js
7053
- const link$1 = union([string(), object({ url: string() })]).transform((value) => typeof value === "string" ? value : value.url).optional().catch(void 0);
7054
- const manifestSchema = object({
7055
- repository: link$1,
7056
- bugs: link$1
7057
- });
7058
- const NO_MANIFEST = {};
7059
- function manifestAt(packageRoot) {
7060
- let manifest;
7061
- try {
7062
- manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
7063
- } catch {
7064
- return NO_MANIFEST;
7065
- }
7066
- const parsed = manifestSchema.safeParse(manifest);
7067
- return parsed.success ? parsed.data : NO_MANIFEST;
7068
- }
7069
- //#endregion
7070
7052
  //#region ../agent-cli/dist/built-ins/skills.js
7071
7053
  const SKILL_DIRS = [[".claude", "skills"], [".agents", "skills"]];
7072
- /** The branch a release moves to the released commit, which holds the skill of that version. */
7073
- const RELEASE_BRANCH$1 = "release";
7074
- const SHORTHAND = /^(?:(?:github:)?[\w.-]+\/[\w.-]+|gitlab:[\w.-]+(?:\/[\w.-]+)+)$/;
7075
- const GIT_URL_PROTOCOLS = /* @__PURE__ */ new Set([
7076
- "https:",
7077
- "http:",
7078
- "ssh:",
7079
- "git:"
7080
- ]);
7081
- const SCP_STYLE = /^[\w.-]+@[\w.-]+:[\w./-]+$/;
7082
7054
  const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---/;
7083
7055
  const METADATA_VERSION = /^metadata:[ \t]*\r?\n(?:[ \t]+.*\r?\n)*?[ \t]+version:[ \t]*["']?([^"'\s]+)["']?[ \t]*$/m;
7084
7056
  /** The version a SKILL.md's frontmatter `metadata.version` states. */
@@ -7127,18 +7099,6 @@ async function skillCopies(cliName, version, { cwd, env }) {
7127
7099
  }
7128
7100
  return copies;
7129
7101
  }
7130
- function sourceFromRepository(repository) {
7131
- if (SHORTHAND.test(repository)) return `${repository}#${RELEASE_BRANCH$1}`;
7132
- const url = repository.replace(/^git\+/, "");
7133
- if (!(URL.canParse(url) && GIT_URL_PROTOCOLS.has(new URL(url).protocol)) && !SCP_STYLE.test(url)) return void 0;
7134
- return `${url.endsWith(".git") ? url : `${url}.git`}#${RELEASE_BRANCH$1}`;
7135
- }
7136
- /** What the installer installs the CLI's skill from: the declared source, else the package's repository. */
7137
- function skillSource({ skill }, packageRoot) {
7138
- if (skill !== void 0) return skill.source;
7139
- const { repository } = manifestAt(packageRoot);
7140
- return repository === void 0 ? void 0 : sourceFromRepository(repository);
7141
- }
7142
7102
  function fixFor(cliName, copies, source) {
7143
7103
  const has = (wanted) => copies.some(({ state }) => state === wanted);
7144
7104
  const update = `npx -y skills update ${cliName}`;
@@ -7151,7 +7111,7 @@ async function skillReport(cli, version, ctx) {
7151
7111
  const copies = await skillCopies(cli.name, version, ctx);
7152
7112
  return {
7153
7113
  copies,
7154
- fix: fixFor(cli.name, copies, skillSource(cli, ctx.packageRoot))
7114
+ fix: fixFor(cli.name, copies, cli.skill?.source)
7155
7115
  };
7156
7116
  }
7157
7117
  //#endregion
@@ -8785,6 +8745,18 @@ async function execute(command, input, ctx, { dryRun, confirm, yes, page, invoca
8785
8745
  }
8786
8746
  return outcomeOf(await command.apply(plan, ctx), (data, style) => command.render(data, style));
8787
8747
  }
8748
+ const manifestSchema = object({ bugs: union([string(), object({ url: string() })]).transform((value) => typeof value === "string" ? value : value.url).optional().catch(void 0) });
8749
+ const NO_MANIFEST = {};
8750
+ function manifestAt(packageRoot) {
8751
+ let manifest;
8752
+ try {
8753
+ manifest = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
8754
+ } catch {
8755
+ return NO_MANIFEST;
8756
+ }
8757
+ const parsed = manifestSchema.safeParse(manifest);
8758
+ return parsed.success ? parsed.data : NO_MANIFEST;
8759
+ }
8788
8760
  //#endregion
8789
8761
  //#region ../agent-cli/dist/parse-input.js
8790
8762
  const NO_INPUT = object({});
@@ -9437,14 +9409,14 @@ async function runProcess(cli) {
9437
9409
  }
9438
9410
  //#endregion
9439
9411
  //#region package.json
9440
- var version$1 = "0.8.0";
9412
+ var version$1 = "0.9.1";
9441
9413
  var description = "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.";
9442
9414
  //#endregion
9443
9415
  //#region src/build.ts
9444
9416
  const build = {
9445
- "sha": "d2cc610",
9417
+ "sha": "3014a0a",
9446
9418
  "dirty": false,
9447
- "builtAt": "2026-10-20T09:33:37.544Z"
9419
+ "builtAt": "2026-10-20T10:05:03.000Z"
9448
9420
  };
9449
9421
  //#endregion
9450
9422
  //#region src/steps.ts
@@ -9578,8 +9550,17 @@ const version = version$1;
9578
9550
  //#region src/create.ts
9579
9551
  const { defineCommand } = agentCli();
9580
9552
  const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?([a-z0-9][a-z0-9._-]*)$/;
9581
- const RELEASE_BRANCH = "release";
9582
- const NO_REPOSITORY = "<!-- Set `repository` in package.json, then put here the `! npx -y skills add` line that `node dist/bin.js doctor` gives as the `skill` row's fix. -->";
9553
+ const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
9554
+ const NO_SKILL_HUB = "<!-- Set `skill.source` in `defineCli` and `skillHub` in `scripts/release.ts` to the git URL of the Skill Hub each release commits the skill to, then put here the `! npx -y skills add` line that `node dist/bin.js doctor` gives as the `skill` row's fix. -->";
9555
+ const SKILL_HUB_ANCHORS = [{
9556
+ file: "src/cli.ts",
9557
+ before: " commands: {",
9558
+ line: (hub) => ` skill: { source: ${JSON.stringify(hub)} },\n`
9559
+ }, {
9560
+ file: "scripts/release.ts",
9561
+ before: " smoke: {",
9562
+ line: (hub) => ` skillHub: ${JSON.stringify(hub)},\n`
9563
+ }];
9583
9564
  function errorCode(error) {
9584
9565
  return error instanceof Error && "code" in error ? error.code : void 0;
9585
9566
  }
@@ -9592,9 +9573,16 @@ async function isFree(path) {
9592
9573
  throw error;
9593
9574
  }
9594
9575
  }
9595
- function skillInstallLine(repository, binName) {
9596
- if (repository === void 0) return NO_REPOSITORY;
9597
- return `\`! npx -y skills add ${`${repository.endsWith(".git") ? repository : `${repository}.git`}#${RELEASE_BRANCH}`} --skill ${binName} -g\``;
9576
+ function skillInstallLine(skillHub, binName) {
9577
+ return skillHub === void 0 ? NO_SKILL_HUB : `\`! npx -y skills add ${skillHub} --skill ${binName} -g\``;
9578
+ }
9579
+ async function setSkillHub(root, skillHub) {
9580
+ for (const { file, before, line } of SKILL_HUB_ANCHORS) {
9581
+ const path = join(root, file);
9582
+ const text = await readFile(path, "utf8");
9583
+ if (!text.includes(before)) throw new Error(`the template's ${file} has no \`${before}\` line to put the Skill Hub before`);
9584
+ await writeFile(path, text.replace(before, `${line(skillHub)}${before}`));
9585
+ }
9598
9586
  }
9599
9587
  const MANIFEST = record(string(), unknown());
9600
9588
  async function setRepository(root, repository) {
@@ -9603,6 +9591,13 @@ async function setRepository(root, repository) {
9603
9591
  const entries = Object.entries(manifest).flatMap((entry) => entry[0] === "description" ? [entry, ["repository", repository]] : [entry]);
9604
9592
  await writeFile(path, `${JSON.stringify(Object.fromEntries(entries), null, 2)}\n`);
9605
9593
  }
9594
+ function ensureSkillName(binName) {
9595
+ if (!SKILL_NAME.test(binName)) throw new UsageError({
9596
+ code: "SKILL_NAME_INVALID",
9597
+ message: `${binName} cannot name a skill in a Skill Hub, whose names are lowercase letters, digits and single hyphens`,
9598
+ hint: "choose a name such as my-cli, or leave out --skill-hub"
9599
+ });
9600
+ }
9606
9601
  async function ensureFree(path) {
9607
9602
  if (!await isFree(path)) throw new UsageError({
9608
9603
  code: "TARGET_NOT_EMPTY",
@@ -9623,10 +9618,12 @@ const create = defineCommand({
9623
9618
  description: string().default("An Agent CLI.").describe("one line saying what the CLI does"),
9624
9619
  install: boolean().default(true).describe("run pnpm install"),
9625
9620
  git: boolean().default(true).describe("create a git repository with an initial commit"),
9626
- repository: url({ protocol: /^https$/ }).optional().describe("the https URL of the CLI's git repository, where its skill is installed from")
9621
+ repository: url({ protocol: /^https$/ }).optional().describe("the https URL of the CLI's git repository"),
9622
+ skillHub: url({ protocol: /^https$/ }).refine((url) => new URL(url).search === "" && new URL(url).hash === "", "must name the hub's repository with no query or fragment").refine((url) => new URL(url).pathname.endsWith(".git"), "must end in .git").optional().describe("the https git URL of the Skill Hub each release commits the CLI's skill to")
9627
9623
  }),
9628
- plan: async ({ name, description, install, git, repository }, ctx) => {
9624
+ plan: async ({ name, description, install, git, repository, skillHub }, ctx) => {
9629
9625
  const binName = PACKAGE_NAME.exec(name)?.[1] ?? name;
9626
+ if (skillHub !== void 0) ensureSkillName(binName);
9630
9627
  const path = resolve(ctx.cwd, binName);
9631
9628
  await ensureFree(path);
9632
9629
  const values = {
@@ -9635,7 +9632,7 @@ const create = defineCommand({
9635
9632
  description,
9636
9633
  debugVariable: debugVariable(binName),
9637
9634
  runtimeVersion: version,
9638
- skillInstall: skillInstallLine(repository, binName)
9635
+ skillInstall: skillInstallLine(skillHub, binName)
9639
9636
  };
9640
9637
  return {
9641
9638
  path,
@@ -9646,13 +9643,15 @@ const create = defineCommand({
9646
9643
  git
9647
9644
  }),
9648
9645
  install,
9649
- repository
9646
+ repository,
9647
+ skillHub
9650
9648
  };
9651
9649
  },
9652
9650
  apply: async (plan, ctx) => {
9653
9651
  await mkdir(plan.path, { recursive: true });
9654
9652
  await writeTemplate(plan.path, plan.files, plan.values);
9655
9653
  if (plan.repository !== void 0) await setRepository(plan.path, plan.repository);
9654
+ if (plan.skillHub !== void 0) await setSkillHub(plan.path, plan.skillHub);
9656
9655
  await runSteps(plan.steps, plan.path, ctx);
9657
9656
  return {
9658
9657
  path: plan.path,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duyquangnvx/create-agent-cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -19,7 +19,7 @@
19
19
  "access": "public"
20
20
  },
21
21
  "devDependencies": {
22
- "@duyquangnvx/agent-cli": "0.8.0",
22
+ "@duyquangnvx/agent-cli": "0.9.1",
23
23
  "tsdown": "0.23.0",
24
24
  "zod": "4.6.7"
25
25
  },
@@ -29,7 +29,7 @@
29
29
  "@biomejs/biome": "2.5.15",
30
30
  "@duyquangnvx/agent-cli": "{{runtimeVersion}}",
31
31
  "@duyquangnvx/harness": "0.3.1",
32
- "@duyquangnvx/release": "0.2.0",
32
+ "@duyquangnvx/release": "0.4.0",
33
33
  "@types/node": "^22.19.0",
34
34
  "tsdown": "0.23.0",
35
35
  "typescript": "7.0.2",
@@ -1,4 +1,5 @@
1
- # A fresh project pins the Runtime release it was created with, which can be younger than
2
- # pnpm's minimum release age.
1
+ # A fresh project pins the Runtime and release versions it was created with, which can be younger
2
+ # than pnpm's minimum release age.
3
3
  minimumReleaseAgeExclude:
4
4
  - "@duyquangnvx/agent-cli"
5
+ - "@duyquangnvx/release"
@@ -186,7 +186,7 @@ describe("install lines", () => {
186
186
  expect(await text("README.md")).toContain(`\`! ${install}\``);
187
187
  });
188
188
 
189
- test.skipIf(!("repository" in manifest))(
189
+ test.skipIf(cli.skill === undefined)(
190
190
  "the README installs the skill with doctor's skill line",
191
191
  async () => {
192
192
  const { skill } = await doctorFixes();
@@ -14,10 +14,12 @@ function git(args: readonly string[]): string {
14
14
  }
15
15
  }
16
16
 
17
+ const dirty = git(["status", "--porcelain"]) !== "";
17
18
  const build = {
18
19
  sha: git(["rev-parse", "--short=7", "HEAD"]),
19
- dirty: git(["status", "--porcelain"]) !== "",
20
- builtAt: new Date().toISOString(),
20
+ dirty,
21
+ // A clean build stamps its commit's time, so a release that rebuilds a commit gets the same bundle.
22
+ builtAt: new Date(dirty ? Date.now() : git(["log", "-1", "--format=%cI", "HEAD"])).toISOString(),
21
23
  };
22
24
 
23
25
  export default defineConfig({