universal-plugin 0.17.1 → 0.18.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-plugin",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
5
5
  "author": {
6
6
  "name": "unional"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-plugin",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
5
5
  "author": {
6
6
  "name": "unional"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-plugin",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
5
5
  "author": {
6
6
  "name": "unional"
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # universal-plugin
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 760b3d7: `doctor` checks semantic-release wiring. It reports `semantic-release-no-stamp` when the semantic-release config has no `@semantic-release/exec` `prepareCmd` running `plugin version`, and `semantic-release-npm-unshipped` when a plugin that ships to no npm package still runs `@semantic-release/npm`, listed or through the default plugins list.
8
+ - c7739a2: The `init-universal-plugin` skill now wires release-it and release-please when the survey finds them or the user picks them, once the user approves. For release-it it adds an `after:bump` hook running `plugin version ${version}`, sets `"npm": false` for a plugin that ships to no npm package, and offers to tag the current commit with the plugin's version when the repository has no tag, since release-it then reads the current version from the tag. For release-please it writes `release-please-config.json` (`release-type: simple` with an `extra-files` entry for `plugin.json`, or `release-type: node` for an npm package) and seeds `.release-please-manifest.json` with the current version, and it hands the user the workflow step that restamps the release PR. `plugin init` gets no new flag.
9
+ - 9f5f59a: The `init-universal-plugin` skill now wires semantic-release when the survey finds it or the user picks it. Once the user approves, it adds `@semantic-release/exec` running `plugin version ${nextRelease.version}` in `prepare`, and `@semantic-release/git` to commit the stamped files back. For a plugin that ships to no npm package it drops `@semantic-release/npm`. For release-it and release-please, the skill names the stamp each tool must call. The wiring stays in the skill, and `plugin init` gets no new flag.
10
+ - 11c42a6: A plugin that ships to no npm package can now be released with changesets. In a changesets repository with no `package.json` at the plugin root, `plugin init` writes a minimal private one (`name`, `"private": true`, `version`), wires the root `version` script, and adds `"release": "changeset publish"`; `--no-changesets` opts out. `plugin version --from-package` with no package in a changesets repository names that `package.json` and the `privatePackages` setting. `doctor` reports `changesets-no-package`, `changesets-private-untagged`, and `changesets-no-publish-script`.
11
+ - fa31d36: release-it is now a documented release model. With `"npm": false` release-it reads the version from the latest git tag and needs no `package.json`; an `after:bump` hook running `universal-plugin plugin version ${version}` stamps it before the release commit and tag. `doctor` reports `release-it-no-stamp` for a release-it config with no hook running `plugin version`.
12
+ - bea7278: `doctor` reports `release-please-restamp-no-ci` when a workflow pushes the `--from-manifest` restamp with the default `GITHUB_TOKEN`. That push starts no workflow run, so the release PR's checks never run on its head commit and a required check blocks the merge. The `version` skill's release-please workflow now mints a GitHub App token with `actions/create-github-app-token` and passes it to release-please and to `actions/checkout`.
13
+ - 8e20a28: A plugin that ships to no npm package can now be released with release-please. `plugin version --from-manifest` restamps every place at the version `plugin.json` already holds, for a CI step on the release PR after `extra-files` bumps `plugin.json`; it is refused where `packagePath` is declared. `doctor` reports `release-please-no-restamp` when a release-please config bumps `plugin.json` and no workflow runs `plugin version`. The `version` skill gives the config and the workflow.
14
+
15
+ ### Patch Changes
16
+
17
+ - 7e8b2e1: `doctor` no longer reports `unreleased-content` for a non-npm plugin that a release tool versions. A plugin released with changesets (through a private `package.json`), release-please (its config bumps `plugin.json`), semantic-release, or release-it is skipped like a plugin that declares `packagePath`, because the tool picks the next number at release time.
18
+
3
19
  ## 0.17.1
4
20
 
5
21
  ### Patch Changes
package/dist/cli.mjs CHANGED
@@ -8608,10 +8608,31 @@ function ownPackage(state) {
8608
8608
  json: state.rootPackageJson ?? null
8609
8609
  };
8610
8610
  }
8611
+ /** Changesets releases only what has a `package.json`, so a plugin that ships to no npm package
8612
+ * needs a private one to carry its version (#234). Names the file and the config that makes
8613
+ * `changeset publish` tag it. */
8614
+ function changesetsPackageHint(state) {
8615
+ const name = typeof state.manifest?.name === "string" ? state.manifest.name : "<plugin-name>";
8616
+ const version = typeof state.manifest?.version === "string" ? state.manifest.version : "0.0.0";
8617
+ return [
8618
+ "changesets releases only a package, so a plugin that is not published to npm needs a private package.json at the plugin root:",
8619
+ ` ${JSON.stringify({
8620
+ name,
8621
+ private: true,
8622
+ version
8623
+ })}`,
8624
+ "and .changeset/config.json needs \"privatePackages\": { \"version\": true, \"tag\": true } so changeset publish tags the release.",
8625
+ "plugin init writes the package.json when it finds .changeset/."
8626
+ ].join("\n");
8627
+ }
8611
8628
  /** Reads the version a release tool already wrote into the package. */
8612
8629
  function versionFromPackage(state) {
8613
8630
  const pkg = ownPackage(state);
8614
- if (pkg.json === null) throw new Error(state.packagePath === null ? "No package.json to read the version from: packagePath is not set in .agents/universal-plugin.json and the plugin root has no package.json" : `No package.json found at packagePath "${state.packagePath}"`);
8631
+ if (pkg.json === null) {
8632
+ if (state.packagePath !== null) throw new Error(`No package.json found at packagePath "${state.packagePath}"`);
8633
+ const message = "No package.json to read the version from: packagePath is not set in .agents/universal-plugin.json and the plugin root has no package.json";
8634
+ throw new Error(state.changesets ? `${message}\n${changesetsPackageHint(state)}` : message);
8635
+ }
8615
8636
  const version = pkg.json["version"];
8616
8637
  if (typeof version !== "string" || version.length === 0) throw new Error(`No version found in ${pkg.path}`);
8617
8638
  if (version === "0.0.0-development") throw new Error(`${pkg.path} holds the placeholder ${UNSTAMPED_VERSION} — pass the release's version explicitly (plugin version \${nextRelease.version})`);
@@ -8619,15 +8640,33 @@ function versionFromPackage(state) {
8619
8640
  if (valid === null) throw new Error(`Version "${version}" in ${pkg.path} is not valid semver`);
8620
8641
  return valid;
8621
8642
  }
8622
- /** Picks the number. Exactly one source: a bump argument or `--from-package`. Under ADR-0010 §2 a
8643
+ /** Reads the version a release tool already wrote into the canonical manifest (#238). Under a
8644
+ * declared `packagePath` the package owns the number, so the manifest is not a source there. */
8645
+ function versionFromManifest(state, from) {
8646
+ if (state.packagePath !== null) {
8647
+ const pkg = joinRelative(state.packagePath, "package.json");
8648
+ throw new Error(`packagePath is declared, so ${pkg} owns the number, not plugin.json — run plugin version --from-package`);
8649
+ }
8650
+ if (from === null) throw new Error("plugin.json has no version to restamp — pass an explicit version (e.g. 0.1.0) to set the first one");
8651
+ if (from === "0.0.0-development") throw new Error(`plugin.json holds the placeholder ${UNSTAMPED_VERSION} — pass the release's version explicitly`);
8652
+ const valid = import_semver.valid(from);
8653
+ if (valid === null) throw new Error(`Version "${from}" in plugin.json is not valid semver`);
8654
+ return valid;
8655
+ }
8656
+ /** Picks the number. Exactly one source: a bump argument, `--from-package`, or `--from-manifest`. Under ADR-0010 §2 a
8623
8657
  * plugin with `packagePath` gets its number from the package, so a release type there is refused —
8624
8658
  * the explicit and `--from-package` forms stay, because release tools call them. */
8625
8659
  function resolveVersion(state, from, opts) {
8660
+ if (opts.fromPackage && opts.fromManifest) throw new Error("Pass --from-package or --from-manifest, not both");
8661
+ if (opts.fromManifest) {
8662
+ if (opts.bump !== void 0) throw new Error(`Pass a version or --from-manifest, not both (got "${opts.bump}")`);
8663
+ return versionFromManifest(state, from);
8664
+ }
8626
8665
  if (opts.fromPackage) {
8627
8666
  if (opts.bump !== void 0) throw new Error(`Pass a version or --from-package, not both (got "${opts.bump}")`);
8628
8667
  return versionFromPackage(state);
8629
8668
  }
8630
- if (opts.bump === void 0) throw new Error(`Pass a release type (${RELEASE_TYPES.join(", ")}), an explicit version, or --from-package`);
8669
+ if (opts.bump === void 0) throw new Error(`Pass a release type (${RELEASE_TYPES.join(", ")}), an explicit version, --from-package, or --from-manifest`);
8631
8670
  if (state.packagePath !== null && isReleaseType(opts.bump)) {
8632
8671
  const pkg = joinRelative(state.packagePath, "package.json");
8633
8672
  throw new Error(`packagePath is declared, so the release picks the number — bump ${pkg} with your release tool (e.g. changeset version), then run plugin version --from-package`);
@@ -8896,6 +8935,10 @@ function gatherMcpFiles(root, declaration) {
8896
8935
  }
8897
8936
  return files;
8898
8937
  }
8938
+ /** Whether `.changeset/` sits at the plugin root or at the repository root above it. */
8939
+ function hasChangesets(root) {
8940
+ return [root, gitToplevel(root)].some((dir) => dir !== void 0 && fs$1.existsSync(path$1.join(dir, ".changeset")));
8941
+ }
8899
8942
  function toRelative(root, abs) {
8900
8943
  return path$1.relative(root, abs).split(path$1.sep).join("/");
8901
8944
  }
@@ -8921,7 +8964,8 @@ const realVersionFs = {
8921
8964
  })),
8922
8965
  workspace: Object.fromEntries(discoverWorkspace(root)),
8923
8966
  mcpFiles: gatherMcpFiles(root, uext["mcpServers"]),
8924
- pinsMapExists: fs$1.existsSync(path$1.join(root, PINS_MAP_PATH))
8967
+ pinsMapExists: fs$1.existsSync(path$1.join(root, PINS_MAP_PATH)),
8968
+ changesets: hasChangesets(root)
8925
8969
  };
8926
8970
  },
8927
8971
  apply(root, plan) {
@@ -9164,9 +9208,22 @@ function gatherChangesets(root, repo) {
9164
9208
  if (!fs$1.existsSync(path$1.join(workspace, ".changeset"))) return void 0;
9165
9209
  return {
9166
9210
  pluginPath,
9167
- packageJson: readJson$1(packageJsonPath(workspace))
9211
+ packageJson: readJson$1(packageJsonPath(workspace)),
9212
+ config: readJsonSafe(path$1.join(workspace, ".changeset", "config.json"))
9168
9213
  };
9169
9214
  }
9215
+ function readJsonSafe(file) {
9216
+ try {
9217
+ return readJson$1(file);
9218
+ } catch {
9219
+ return null;
9220
+ }
9221
+ }
9222
+ /** The version an existing `plugin.json` carries, which a new private package.json is seeded from. */
9223
+ function readManifestVersion(root) {
9224
+ const version = readJsonSafe(manifestPath(root))?.version;
9225
+ return typeof version === "string" && version !== "" ? version : void 0;
9226
+ }
9170
9227
  function writeJson(file, value) {
9171
9228
  const existing = fs$1.existsSync(file) ? fs$1.readFileSync(file, "utf8") : void 0;
9172
9229
  fs$1.writeFileSync(file, formatJson(value, {
@@ -9179,6 +9236,7 @@ const realInitFs = {
9179
9236
  const repo = gatherRepo(root);
9180
9237
  return {
9181
9238
  manifestExists: fs$1.existsSync(manifestPath(root)),
9239
+ manifestVersion: readManifestVersion(root),
9182
9240
  packageJson: readJson$1(packageJsonPath(root)),
9183
9241
  repo,
9184
9242
  changesets: gatherChangesets(root, repo)
@@ -9284,6 +9342,89 @@ function wireChangesets(pkg, pluginPath, cliVersion) {
9284
9342
  };
9285
9343
  return { content: next };
9286
9344
  }
9345
+ const CHANGESET_PUBLISH = "changeset publish";
9346
+ /** The version a private package starts at when the manifest carries none to seed it from. */
9347
+ const FIRST_VERSION = "0.0.0";
9348
+ /** The package.json a plugin that ships to no npm package needs before changesets can release it
9349
+ * (#234). Changesets finds only packages, and `@changesets/apply-release-plan` always writes
9350
+ * `<dir>/package.json`, so this file is what carries the version. `private` keeps
9351
+ * `changeset publish` to a git tag; no `files`, `main`, or `exports`, because nothing is packed. */
9352
+ function privatePackage(name, version) {
9353
+ return {
9354
+ name,
9355
+ private: true,
9356
+ version
9357
+ };
9358
+ }
9359
+ /** Adds `"release": "changeset publish"`, the script changesets/action runs as its publish step.
9360
+ * For a private package it creates the git tag the GitHub release hangs on. A custom `release`
9361
+ * script is left alone with a note. Returns `null` content when nothing changes. */
9362
+ function wireReleaseScript(pkg) {
9363
+ const scripts = typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
9364
+ const script = typeof scripts.release === "string" ? scripts.release : void 0;
9365
+ if (script?.includes(CHANGESET_PUBLISH)) return { content: null };
9366
+ if (script !== void 0) return {
9367
+ content: null,
9368
+ note: `changesets: the root "release" script is custom; make sure it runs "${CHANGESET_PUBLISH}" so a release is tagged`
9369
+ };
9370
+ return { content: {
9371
+ ...pkg,
9372
+ scripts: {
9373
+ ...scripts,
9374
+ release: CHANGESET_PUBLISH
9375
+ }
9376
+ } };
9377
+ }
9378
+ /** Whether `.changeset/config.json` has changesets bump and tag private packages. Changesets bumps
9379
+ * them by default but tags them only when `privatePackages.tag` is `true`. */
9380
+ function tagsPrivatePackages(config) {
9381
+ const setting = config?.privatePackages;
9382
+ if (typeof setting !== "object" || setting === null) return false;
9383
+ const { version, tag } = setting;
9384
+ return version !== false && tag === true;
9385
+ }
9386
+ /** Wires a plugin that ships to no npm package for a changesets release: a private package.json at
9387
+ * the plugin root, and the `version` and `release` scripts at the workspace root. Appends to `rows`
9388
+ * and `notes` in place. */
9389
+ function planPrivateRelease(opts, name, version, changesets, rows, notes) {
9390
+ const sameFile = changesets.pluginPath === "";
9391
+ let packageJson = privatePackage(name, version);
9392
+ rows.push({
9393
+ path: "package.json",
9394
+ action: "created"
9395
+ });
9396
+ if (sameFile) packageJson = {
9397
+ ...packageJson,
9398
+ scripts: { version: CHANGESET_VERSION }
9399
+ };
9400
+ else notes.push(`changesets: ${changesets.pluginPath}/package.json must be a workspace package (pnpm-workspace.yaml or "workspaces") for changesets to release it`);
9401
+ let workspacePackageJson = null;
9402
+ const rootPkg = sameFile ? packageJson : changesets.packageJson;
9403
+ if (rootPkg === null) notes.push(`changesets: no package.json at the workspace root to wire; add a "version" script running "${CHANGESET_VERSION} && ${syncVersionCommand(changesets.pluginPath)}" and a "release" script running "${CHANGESET_PUBLISH}"`);
9404
+ else {
9405
+ const versioned = wireChangesets(rootPkg, changesets.pluginPath, opts.cliVersion);
9406
+ if (versioned.note) notes.push(versioned.note);
9407
+ const released = wireReleaseScript(versioned.content ?? rootPkg);
9408
+ if (released.note) notes.push(released.note);
9409
+ const wired = released.content ?? versioned.content;
9410
+ if (wired && sameFile) packageJson = wired;
9411
+ else if (wired) {
9412
+ workspacePackageJson = {
9413
+ path: `${changesets.pluginPath.split("/").map(() => "..").join("/")}/package.json`,
9414
+ content: wired
9415
+ };
9416
+ rows.push({
9417
+ path: workspacePackageJson.path,
9418
+ action: "updated"
9419
+ });
9420
+ }
9421
+ }
9422
+ if (!tagsPrivatePackages(changesets.config)) notes.push("changesets: set \"privatePackages\": { \"version\": true, \"tag\": true } in .changeset/config.json so changeset publish tags each release");
9423
+ return {
9424
+ packageJson,
9425
+ workspacePackageJson
9426
+ };
9427
+ }
9287
9428
  /** The local marketplace a repository carries is named after the repository, not after the plugin:
9288
9429
  * the catalog sits at the repository root and lists every plugin the repository develops. `-local`
9289
9430
  * separates it from a published marketplace of the same plugins. */
@@ -9356,9 +9497,12 @@ function planCatalogs(state, opts, manifest, notes) {
9356
9497
  function planInit(state, opts, rootDirName, resolveShippedPaths) {
9357
9498
  if (opts.npm && state.packageJson === null) throw new Error("error: --npm requires a package.json at the project root");
9358
9499
  if (state.manifestExists && !opts.force) throw new Error("plugin.json already exists — pass --force to overwrite");
9359
- const manifest = buildManifest(opts.name ?? rootDirName, opts.vendors);
9500
+ const name = opts.name ?? rootDirName;
9501
+ const manifest = buildManifest(name, opts.vendors);
9360
9502
  const packageVersion = state.packageJson?.version;
9361
9503
  if (opts.npm && typeof packageVersion === "string" && packageVersion !== "") manifest.version = packageVersion;
9504
+ const privateVersion = !opts.npm && opts.changesets !== false && state.changesets && state.packageJson === null ? state.manifestVersion ?? FIRST_VERSION : void 0;
9505
+ if (privateVersion !== void 0) manifest.version = privateVersion;
9362
9506
  const dirs = opts.scaffold ? [...SCAFFOLD_DIRS] : [];
9363
9507
  const rows = [{
9364
9508
  path: "plugin.json",
@@ -9384,7 +9528,7 @@ function planInit(state, opts, rootDirName, resolveShippedPaths) {
9384
9528
  });
9385
9529
  }
9386
9530
  let workspacePackageJson = null;
9387
- const changesets = state.changesets;
9531
+ const changesets = opts.changesets === false ? void 0 : state.changesets;
9388
9532
  if (opts.npm && changesets) {
9389
9533
  const sameFile = changesets.pluginPath === "";
9390
9534
  const rootPkg = sameFile ? packageJson : changesets.packageJson;
@@ -9405,6 +9549,11 @@ function planInit(state, opts, rootDirName, resolveShippedPaths) {
9405
9549
  }
9406
9550
  }
9407
9551
  }
9552
+ if (privateVersion !== void 0 && changesets) {
9553
+ const wired = planPrivateRelease(opts, name, privateVersion, changesets, rows, notes);
9554
+ packageJson = wired.packageJson;
9555
+ workspacePackageJson = wired.workspacePackageJson;
9556
+ }
9408
9557
  const created = rows.filter((r) => r.action === "created").length;
9409
9558
  const updated = rows.filter((r) => r.action === "updated").length;
9410
9559
  const unchanged = rows.filter((r) => r.action === "unchanged").length;
@@ -9431,7 +9580,7 @@ function collect$1(value, previous) {
9431
9580
  }
9432
9581
  function initCommand$1(deps = { fs: realInitFs }) {
9433
9582
  const cmd = new Command("init").description("Scaffold the canonical plugin.json (with --npm, wire an npm package to ship it)");
9434
- cmd.option("--name <name>", "Plugin name (default: the root directory name)").option("--vendor <id>", "Target vendor; repeatable", collect$1, []).option("--scaffold", "Create the standard skills/ agents/ references/ commands/ directories").option("--force", "Overwrite an existing plugin.json").option("--yes", "Non-interactive (compatibility no-op; init never prompts)").option("--npm", "Wire package.json to ship the plugin (files, version; changesets version sync)").option("--no-marketplace", "Skip the repository's local marketplace catalogs").option("--format <format>", "Output format: json or toon (default: toon)").addOption(new Option("--json").hideHelp()).addOption(ROOT_OPTION).addHelpText("after", "\nExample:\n $ universal-plugin plugin init --name my-plugin --scaffold\n").action((opts) => {
9583
+ cmd.option("--name <name>", "Plugin name (default: the root directory name)").option("--vendor <id>", "Target vendor; repeatable", collect$1, []).option("--scaffold", "Create the standard skills/ agents/ references/ commands/ directories").option("--force", "Overwrite an existing plugin.json").option("--yes", "Non-interactive (compatibility no-op; init never prompts)").option("--npm", "Wire package.json to ship the plugin (files, version; changesets version sync)").option("--no-marketplace", "Skip the repository's local marketplace catalogs").option("--no-changesets", "Skip the changesets release wiring (private package.json, version and release scripts)").option("--format <format>", "Output format: json or toon (default: toon)").addOption(new Option("--json").hideHelp()).addOption(ROOT_OPTION).addHelpText("after", "\nExample:\n $ universal-plugin plugin init --name my-plugin --scaffold\n").action((opts) => {
9435
9584
  try {
9436
9585
  const root = resolveRoot(opts.root);
9437
9586
  const plan = planInit(deps.fs.gather(root), {
@@ -9441,6 +9590,7 @@ function initCommand$1(deps = { fs: realInitFs }) {
9441
9590
  force: Boolean(opts.force),
9442
9591
  npm: Boolean(opts.npm),
9443
9592
  marketplace: opts.marketplace !== false,
9593
+ changesets: opts.changesets !== false,
9444
9594
  cliVersion: deps.version
9445
9595
  }, path$1.basename(root), (vendor) => VENDOR_SHIPPED_PATHS[vendor] ?? []);
9446
9596
  deps.fs.apply(root, plan);
@@ -18114,12 +18264,13 @@ function prepareCommand() {
18114
18264
  //#region src/version/cli.ts
18115
18265
  function versionCommand(deps = { fs: realVersionFs }) {
18116
18266
  const cmd = new Command("version").description("Stamp the plugin version into every file that carries one").argument("[bump]", `Release type (${RELEASE_TYPES.join(", ")}) or an explicit version (1.4.0)`);
18117
- cmd.option("--from-package", "Take the version the package's package.json already holds (changesets, release-please)").option("--preid <id>", "Prerelease identifier for the pre* release types").option("--force", "Allow a version that does not advance on the current one").option("--no-build", "Skip re-deriving the vendor manifests").option("--dry-run", "Print what would be written without writing").option("--format <format>", "Output format: json or toon (default: toon)").addOption(new Option("--json").hideHelp()).addOption(ROOT_OPTION).addHelpText("after", [
18267
+ cmd.option("--from-package", "Take the version the package's package.json already holds (changesets, release-please)").option("--from-manifest", "Restamp every place with the version plugin.json already holds (release-please extra-files)").option("--preid <id>", "Prerelease identifier for the pre* release types").option("--force", "Allow a version that does not advance on the current one").option("--no-build", "Skip re-deriving the vendor manifests").option("--dry-run", "Print what would be written without writing").option("--format <format>", "Output format: json or toon (default: toon)").addOption(new Option("--json").hideHelp()).addOption(ROOT_OPTION).addHelpText("after", [
18118
18268
  "",
18119
18269
  "Examples:",
18120
18270
  " $ universal-plugin plugin version minor # plain repository: the author picks",
18121
18271
  " $ universal-plugin plugin version 1.4.0 # semantic-release: prepareCmd",
18122
- " $ universal-plugin plugin version --from-package # changesets / release-please",
18272
+ " $ universal-plugin plugin version --from-package # changesets / release-please, npm package",
18273
+ " $ universal-plugin plugin version --from-manifest # release-please, plugin.json bumped by extra-files",
18123
18274
  ""
18124
18275
  ].join("\n")).action((bump, opts) => runVersion(bump, opts, deps.fs));
18125
18276
  return cmd;
@@ -18132,6 +18283,7 @@ function runVersion(bump, opts, versionFs = realVersionFs) {
18132
18283
  const plan = planVersion(versionFs.gather(root), {
18133
18284
  ...bump === void 0 ? {} : { bump },
18134
18285
  fromPackage: Boolean(opts.fromPackage),
18286
+ fromManifest: Boolean(opts.fromManifest),
18135
18287
  ...opts.preid === void 0 ? {} : { preid: opts.preid },
18136
18288
  force: Boolean(opts.force)
18137
18289
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-plugin",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Universal AI agent plugin build tool",
5
5
  "keywords": [
6
6
  "agent-plugin",
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": "universal-plugin",
4
- "version": "0.17.1",
4
+ "version": "0.18.0",
5
5
  "description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
6
6
  "author": {
7
7
  "name": "unional"
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Compiled by `universal-plugin plugin build` from src/skill-scripts/rewrite-upx.ts. Edit the source, not this file.
3
- globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.17.1","entry":"skills/adopt-upx/scripts/rewrite-upx.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace update":"skills/marketplace/scripts/update.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
3
+ globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.18.0","entry":"skills/adopt-upx/scripts/rewrite-upx.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace update":"skills/marketplace/scripts/update.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
4
4
  import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
5
5
  import { join, resolve } from "node:path";
6
6
  //#region src/skill-scripts/rewrite-upx.ts
@@ -47,7 +47,7 @@ node scripts/build.mjs --root <plugin-root>
47
47
 
48
48
  Resolve `scripts/build.mjs` against this skill's own directory. It runs the CLI the project installed
49
49
  (`pnpm exec universal-plugin`), else the CLI pinned to the version this skill shipped with
50
- (`npx universal-plugin@0.17.1`), so it works in a skill that was linked or copied on its own. `--root` defaults to the working directory and
50
+ (`npx universal-plugin@0.18.0`), so it works in a skill that was linked or copied on its own. `--root` defaults to the working directory and
51
51
  must name the directory that holds the canonical `plugin.json`.
52
52
 
53
53
  The command never prompts, so it is safe to run unattended.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Compiled by `universal-plugin plugin build` from src/skill-scripts/run-cli.ts. Edit the source, not this file.
3
- globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.17.1","entry":"skills/build-plugin/scripts/build.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace update":"skills/marketplace/scripts/update.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
3
+ globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.18.0","entry":"skills/build-plugin/scripts/build.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace update":"skills/marketplace/scripts/update.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
4
4
  import { spawnSync } from "node:child_process";
5
5
  //#region src/compile/scripts.ts
6
6
  /** The global the header assigns the script data to. Framework-neutral: a script's own code, or the
@@ -95,6 +95,14 @@ Each `code` below is what the script emits.
95
95
  | `package-path-missing` | `packagePath` names a directory with no readable `package.json` | fix `packagePath`, or create the package |
96
96
  | `package-path-unknown` | the CLI could not report `packagePath` (a version too old to read it), so `version-drift` and `unreleased-content` were skipped rather than guessed | upgrade universal-plugin |
97
97
  | `misplaced-package-path` | `plugin.json` declares `packagePath` under `extensions["org.cyberuni.universal-plugin"]`, where the CLI never reads it — the plugin is silently treated as not shipping to npm | move it to `.agents/universal-plugin.json`, relative to the plugin root |
98
+ | `changesets-no-package` | the repository is released with changesets, the plugin ships to no npm package, and its root has no `package.json` — changesets releases only a package, so `changeset version` and `changeset publish` fail | `/universal-plugin:init-universal-plugin` (pnpm install: `pnpm-universal-plugin-init-universal-plugin`) — `plugin init` writes a private `package.json` |
99
+ | `changesets-private-untagged` | a non-npm plugin in a changesets repository, and `.changeset/config.json` does not set `"privatePackages": { "version": true, "tag": true }` — `changeset publish` tags no private package, so no release is made | set `privatePackages` in `.changeset/config.json` |
100
+ | `changesets-no-publish-script` | a non-npm plugin in a changesets repository, and a workflow runs `changesets/action` with no `publish-script` (`publish` in v1) — after the Version Packages PR merges the action does nothing, so no tag or GitHub release is made | add `publish-script: pnpm release` to the step, with a `release` script running `changeset publish` |
101
+ | `release-please-no-restamp` | the plugin ships to no npm package, `release-please-config.json` bumps its `plugin.json` through `extra-files`, and no workflow runs `plugin version` — release-please runs no command before its release commit, so the release PR leaves the vendor manifests, catalogs, skill pins and compiled scripts on the old version | add a workflow step on the release PR branch running `universal-plugin plugin version --from-manifest` and pushing the stamped files — `/universal-plugin:version` (pnpm install: `pnpm-universal-plugin-version`) has the workflow |
102
+ | `release-please-restamp-no-ci` | a workflow restamps the release PR with `plugin version --from-manifest` and pushes with the default `GITHUB_TOKEN` — that push starts no workflow run, so the PR's checks never run on its head commit and a required check blocks the merge. A `workflow_dispatch` re-run does not fix it: its checks do not count toward a required check | mint a GitHub App token with `actions/create-github-app-token` and pass it as `token:` to `release-please-action` and `actions/checkout` — `/universal-plugin:version` (pnpm install: `pnpm-universal-plugin-version`) has the workflow |
103
+ | `release-it-no-stamp` | a release-it config (`.release-it.*`, or the `release-it` key of `package.json`) has no hook running `plugin version` — release-it commits and tags the release with `plugin.json` and the vendor manifests still on the previous version | add `"hooks": { "after:bump": "universal-plugin plugin version ${version}" }` to the config |
104
+ | `semantic-release-no-stamp` | a semantic-release config (`.releaserc*`, `.config/releaserc*`, `release.config.*`, or the `release` key of `package.json`) has no `@semantic-release/exec` `prepareCmd` running `plugin version` — the release is tagged with `plugin.json`, the vendor manifests and the catalogs still on the previous version | add `["@semantic-release/exec", { "prepareCmd": "universal-plugin plugin version ${nextRelease.version}" }]` before `@semantic-release/git` — `/universal-plugin:init-universal-plugin` (pnpm install: `pnpm-universal-plugin-init-universal-plugin`) wires it |
105
+ | `semantic-release-npm-unshipped` | the plugin ships to no npm package, and its semantic-release config lists `@semantic-release/npm` or relies on the default `plugins` list, which includes it — with no `package.json` the release fails (`ENOPKG`), and with one the plugin still rewrites its version | remove `@semantic-release/npm`, writing the `plugins` list out if the config relies on the defaults — `/universal-plugin:init-universal-plugin` (pnpm install: `pnpm-universal-plugin-init-universal-plugin`) wires it |
98
106
  | `unparsable-manifest` | root `plugin.json` is not valid JSON | fix the syntax error |
99
107
  | `invalid-catalog` | a marketplace catalog — at the repository root, or at a `--marketplace-root` clone — is not a shape its runtime loads — it is found, read, and refused at install time, in the user's terminal | `/universal-plugin:marketplace` (pnpm install: `pnpm-universal-plugin-marketplace`) |
100
108
  | `marketplace-root-missing` | a `--marketplace-root` path does not exist, so it could not be checked at all | clone the marketplace repository, or fix the path |
@@ -172,7 +180,7 @@ it does not, offer to add it — ask first, because both are the user's files:
172
180
  semantic-release bot opens.
173
181
 
174
182
  Use the locked `universal-plugin` from the repository's devDependencies when it has one; otherwise
175
- `npx universal-plugin@0.17.1`, pinned. This check matters most where the release tool picks the
183
+ `npx universal-plugin@0.18.0`, pinned. This check matters most where the release tool picks the
176
184
  number, because nothing else runs between that bump and publish.
177
185
 
178
186
  ## Unreleased content
@@ -187,9 +195,13 @@ The script compares the shipped paths — the canonical manifest, the skills dir
187
195
  emits `unreleased-content` for anything committed since. Uncommitted work is not reported; it has not
188
196
  shipped.
189
197
 
190
- Two cases are deliberately silent. A plugin that declares `packagePath` is skipped, because there the
191
- release picks the number ([ADR-0010](https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/.agents/spec/design/decisions/0010-version-policy.md) §2) and content waiting ahead of the last released version is the
192
- normal state of a branch. A tree with no git history is skipped rather than guessed at.
198
+ Two cases are deliberately silent. A plugin whose release picks the number is skipped, because
199
+ content waiting ahead of the last released version is the normal state of a branch there
200
+ ([ADR-0010](https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/.agents/spec/design/decisions/0010-version-policy.md) §6). That is a plugin that declares `packagePath`, or a plugin a release tool versions:
201
+ changesets (`.changeset/` and a private or absent plugin-root `package.json`), release-please (its
202
+ config bumps this `plugin.json` through `extra-files`), semantic-release (`.releaserc*`,
203
+ `release.config.*`, or a `release` key in `package.json`), or release-it (a `.release-it.*` file, or a
204
+ `release-it` key in `package.json`). A tree with no git history is skipped rather than guessed at.
193
205
 
194
206
  The repair is the bump, and it belongs to `/universal-plugin:version` (pnpm install: `pnpm-universal-plugin-version`). Judge first whether the change
195
207
  is meant to ship — content that is still being worked on is not a finding to act on.