@hasna/skills 0.10.33 → 0.10.34

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
@@ -1148,6 +1148,32 @@ semantic version. If discovery or verification fails, it exits nonzero and
1148
1148
  explains that installation may already have completed. It does not automatically
1149
1149
  reinstall or change your PATH.
1150
1150
 
1151
+ ### Exact-version runtime updates
1152
+
1153
+ `skills self-update --version 0.10.34` installs a verified package into a retained
1154
+ copyfile runtime, preserves configuration and launcher preimages, and keeps the
1155
+ previous runtime available for receipt-bound rollback.
1156
+
1157
+ To require a minimum dependency age, pass a positive integer number of days and
1158
+ repeat the exclusion option for package names or package globs:
1159
+
1160
+ ```bash
1161
+ skills self-update --version 0.10.34 --min-release-age 7 \
1162
+ --min-release-age-exclude '@hasna/*' \
1163
+ --min-release-age-exclude '@hasna-internal/*' \
1164
+ --min-release-age-exclude '@openai/*' \
1165
+ --min-release-age-exclude '@anthropic-ai/*' \
1166
+ --min-release-age-exclude openai
1167
+ ```
1168
+
1169
+ The policy reaches both dependency lock resolution and `npm ci` through private
1170
+ npm configuration. Exemptions match each dependency's own package identity;
1171
+ unrelated transitive dependencies retain the minimum age. These options require
1172
+ an exact version and npm 11.19.0 or newer with both age capabilities. They are
1173
+ refused for rollback, alias adoption or the no-argument updater. Existing ambient
1174
+ npm settings and credentials are not forwarded. Omitting the policy preserves
1175
+ existing exact-version update behavior.
1176
+
1151
1177
  ### Local environment assignments
1152
1178
 
1153
1179
  `skills env-check --set 'KEY=value'` writes one literal value to the current
package/bin/index.js CHANGED
@@ -36868,7 +36868,7 @@ var package_default;
36868
36868
  var init_package = __esm(() => {
36869
36869
  package_default = {
36870
36870
  name: "@hasna/skills",
36871
- version: "0.10.33",
36871
+ version: "0.10.34",
36872
36872
  description: "Skills library for AI coding agents",
36873
36873
  type: "module",
36874
36874
  bin: {
@@ -109753,16 +109753,47 @@ function assertConfigUnchanged(configs) {
109753
109753
  }
109754
109754
  }
109755
109755
  }
109756
- async function runNpm(args2, cwd2, home, staging) {
109756
+ function releaseAgePolicy(options) {
109757
+ if (options.minReleaseAgeExclude !== undefined && !Array.isArray(options.minReleaseAgeExclude))
109758
+ throw new Error("MIN_RELEASE_AGE_EXCLUDE_INVALID");
109759
+ if (options.minReleaseAge === undefined) {
109760
+ if (options.minReleaseAgeExclude?.length)
109761
+ throw new Error("MIN_RELEASE_AGE_REQUIRED_FOR_EXCLUSIONS");
109762
+ return;
109763
+ }
109764
+ if (!Number.isSafeInteger(options.minReleaseAge) || options.minReleaseAge < 1)
109765
+ throw new Error("MIN_RELEASE_AGE_INVALID");
109766
+ const exclusions = options.minReleaseAgeExclude ?? [];
109767
+ if (!Array.isArray(exclusions) || exclusions.some((pattern) => typeof pattern !== "string" || pattern.length > 214 || !/^(?:@[a-z0-9*?][a-z0-9._*?-]*\/)?[a-z0-9*?][a-z0-9._*?-]*$/.test(pattern))) {
109768
+ throw new Error("MIN_RELEASE_AGE_EXCLUDE_INVALID");
109769
+ }
109770
+ return { minReleaseAge: options.minReleaseAge, minReleaseAgeExclude: [...new Set(exclusions)] };
109771
+ }
109772
+ async function npmReleaseAgeCapability(cwd2, env3) {
109773
+ try {
109774
+ const versionProbe = Bun.spawn(["npm", "--version"], { cwd: cwd2, env: env3, stdin: "ignore", stdout: "pipe", stderr: "ignore" });
109775
+ const [versionText, versionStatus] = await Promise.all([new Response(versionProbe.stdout).text(), versionProbe.exited]);
109776
+ const version2 = versionText.trim();
109777
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version2);
109778
+ if (versionStatus !== 0 || !match || Number(match[1]) < 11 || Number(match[1]) === 11 && Number(match[2]) < 19)
109779
+ throw new Error("NPM_RELEASE_AGE_UNSUPPORTED");
109780
+ const configProbe = Bun.spawn(["npm", "config", "list", "--json"], { cwd: cwd2, env: env3, stdin: "ignore", stdout: "pipe", stderr: "ignore" });
109781
+ const [configText, configStatus] = await Promise.all([new Response(configProbe.stdout).text(), configProbe.exited]);
109782
+ const config2 = object9(JSON.parse(configText), "NPM_RELEASE_AGE_UNSUPPORTED");
109783
+ if (configStatus !== 0 || !Object.hasOwn(config2, "min-release-age") || !Object.hasOwn(config2, "min-release-age-exclude") || !Array.isArray(config2["min-release-age-exclude"]))
109784
+ throw new Error("NPM_RELEASE_AGE_UNSUPPORTED");
109785
+ return version2;
109786
+ } catch {
109787
+ throw new Error("NPM_RELEASE_AGE_UNSUPPORTED");
109788
+ }
109789
+ }
109790
+ async function runNpm(args2, cwd2, home, staging, policy) {
109757
109791
  const userNpmrc = join56(staging, "npm-user.npmrc"), globalNpmrc = join56(staging, "npm-global.npmrc");
109758
- if (!existsSync34(userNpmrc))
109759
- writeFileSync23(userNpmrc, `registry=${REGISTRY_ORIGIN}/
109792
+ const baseConfig = `registry=${REGISTRY_ORIGIN}/
109760
109793
  ignore-scripts=true
109761
- `, { mode: 384, flag: "wx" });
109794
+ `;
109762
109795
  if (!existsSync34(globalNpmrc))
109763
- writeFileSync23(globalNpmrc, `registry=${REGISTRY_ORIGIN}/
109764
- ignore-scripts=true
109765
- `, { mode: 384, flag: "wx" });
109796
+ writeFileSync23(globalNpmrc, baseConfig, { mode: 384, flag: "wx" });
109766
109797
  const env3 = {
109767
109798
  PATH: process.env.PATH ?? "",
109768
109799
  HOME: home,
@@ -109776,6 +109807,19 @@ ignore-scripts=true
109776
109807
  NPM_CONFIG_REGISTRY: `${REGISTRY_ORIGIN}/`,
109777
109808
  NPM_CONFIG_IGNORE_SCRIPTS: "true"
109778
109809
  };
109810
+ const capabilityNpmrc = join56(staging, "npm-capability.npmrc");
109811
+ if (policy && !existsSync34(capabilityNpmrc))
109812
+ writeFileSync23(capabilityNpmrc, baseConfig, { mode: 384, flag: "wx" });
109813
+ if (policy && readFileSync37(capabilityNpmrc, "utf8") !== baseConfig)
109814
+ throw new Error("NPM_ISOLATED_CONFIG_DRIFT");
109815
+ const npmVersion = policy ? await npmReleaseAgeCapability(cwd2, { ...env3, NPM_CONFIG_USERCONFIG: capabilityNpmrc }) : undefined;
109816
+ const config2 = baseConfig + (policy ? `min-release-age=${policy.minReleaseAge}
109817
+ ${policy.minReleaseAgeExclude.map((pattern) => `min-release-age-exclude[]=${pattern}
109818
+ `).join("")}` : "");
109819
+ if (!existsSync34(userNpmrc))
109820
+ writeFileSync23(userNpmrc, config2, { mode: 384, flag: "wx" });
109821
+ if (readFileSync37(userNpmrc, "utf8") !== config2 || readFileSync37(globalNpmrc, "utf8") !== baseConfig)
109822
+ throw new Error("NPM_ISOLATED_CONFIG_DRIFT");
109779
109823
  const proc = Bun.spawn(["npm", ...args2, "--registry", `${REGISTRY_ORIGIN}/`, "--ignore-scripts", "--no-audit", "--no-fund"], {
109780
109824
  cwd: cwd2,
109781
109825
  env: env3,
@@ -109786,6 +109830,7 @@ ignore-scripts=true
109786
109830
  const exitCode = await proc.exited;
109787
109831
  if (exitCode !== 0)
109788
109832
  throw new Error("DEPENDENCY_GRAPH_INSTALL_FAILED");
109833
+ return npmVersion;
109789
109834
  }
109790
109835
  function comparePackageTrees(expectedRoot, installedRoot) {
109791
109836
  const expected = readTree(expectedRoot, false);
@@ -110164,6 +110209,7 @@ async function updateCopyfileRuntime(version2, options = {}) {
110164
110209
  throw new Error("EXACT_VERSION_REQUIRED");
110165
110210
  if (!STABLE_SEMVER_PATTERN.test(version2))
110166
110211
  throw new Error("EXACT_STABLE_VERSION_REQUIRED");
110212
+ const policy = releaseAgePolicy(options);
110167
110213
  const home = resolve24(options.homeDir ?? process.env.HOME ?? "");
110168
110214
  if (!home || !existsSync34(home))
110169
110215
  throw new Error("HOME_NOT_FOUND");
@@ -110201,8 +110247,10 @@ async function updateCopyfileRuntime(version2, options = {}) {
110201
110247
  mkdirSync24(stagePackage, { mode: 448 });
110202
110248
  copyFileSync(artifact.tarballPath, join56(stagePackage, "verified.tgz"));
110203
110249
  writeFileSync23(join56(stagePackage, "package.json"), JSON.stringify({ private: true, dependencies: { [PACKAGE_NAME]: "file:./verified.tgz" } }, null, 2), { mode: 384, flag: "wx" });
110204
- await runNpm(["install", "--package-lock-only", "--prefix", stagePackage], stagePackage, home, stagePath);
110205
- await runNpm(["ci", "--prefix", stagePackage], stagePackage, home, stagePath);
110250
+ const npmVersion = await runNpm(["install", "--package-lock-only", "--prefix", stagePackage], stagePackage, home, stagePath, policy);
110251
+ const ciNpmVersion = await runNpm(["ci", "--prefix", stagePackage], stagePackage, home, stagePath, policy);
110252
+ if (npmVersion !== ciNpmVersion)
110253
+ throw new Error("NPM_VERSION_DRIFT_DURING_UPDATE");
110206
110254
  const lockBytes = readFileSync37(join56(stagePackage, "package-lock.json"));
110207
110255
  const installedPackage = join56(stagePackage, "node_modules", "@hasna", "skills");
110208
110256
  const installed = readPackage(join56(installedPackage, "package.json"));
@@ -110243,6 +110291,7 @@ async function updateCopyfileRuntime(version2, options = {}) {
110243
110291
  targetPackageRoot,
110244
110292
  configs: configPreimages,
110245
110293
  preimageSha256,
110294
+ ...policy ? { dependencyPolicy: { ...policy, npmVersion } } : {},
110246
110295
  launchers,
110247
110296
  switchedLaunchers: [],
110248
110297
  rollbackCompletedLaunchers: []
@@ -110281,7 +110330,7 @@ async function updateCopyfileRuntime(version2, options = {}) {
110281
110330
  }
110282
110331
  receipt.state = "switched";
110283
110332
  persistReceipt(join56(finalPath, "rollout-receipt.json"), receipt);
110284
- return { updated: true, version: version2, currentVersion: layout.currentVersion, receiptId: id2, runtimeRoot: finalPath, tarballSha256: artifact.sha256, tarballIntegrity: artifact.integrity, runtimeTreeSha256: stagedTree.digest, launcherCount: launchers.length, configCount: configPreimages.length };
110333
+ return { updated: true, version: version2, currentVersion: layout.currentVersion, receiptId: id2, runtimeRoot: finalPath, tarballSha256: artifact.sha256, tarballIntegrity: artifact.integrity, runtimeTreeSha256: stagedTree.digest, launcherCount: launchers.length, configCount: configPreimages.length, ...policy ? { dependencyPolicy: { ...policy, npmVersion } } : {} };
110285
110334
  } catch (error61) {
110286
110335
  if (switched.length > 0 || moved) {
110287
110336
  let rollbackFailed = false;
@@ -110505,7 +110554,12 @@ function registerRuntime(parent) {
110505
110554
  });
110506
110555
  const setup = parent.command("setup").description("Point this CLI at a Skills API server, or register agent integrations").option("--api-url <url>", "Skills API origin to send remote work to").option("--global", "Accepted for compatibility; the API origin is always per-user", false).option("--json", "Output setup result as JSON", false).action(async (options) => handleSetup(options));
110507
110556
  setup.command("agents").option("--json", "Output registration result as JSON", false).description("Register the Skills MCP server with all supported agents").action(async (options) => handleMcp({ register: "all", json: options.json }));
110508
- parent.command("self-update").description("Update @hasna/skills to the latest version").option("--json", "Output result as JSON", false).option("--version <version>", "Install this exact @hasna/skills version as a staged copyfile runtime").option("--rollback <receipt-id>", "Restore launcher links from an exact copyfile rollout receipt").option("--adopt-aliases", "Switch verified legacy @hasna/skills aliases to the active copyfile runtime").option("--rollback-aliases <receipt-id>", "Restore aliases from an exact adoption receipt").action(async (options) => {
110557
+ parent.command("self-update").description("Update @hasna/skills to the latest version").option("--json", "Output result as JSON", false).option("--version <version>", "Install this exact @hasna/skills version as a staged copyfile runtime").option("--min-release-age <days>", "Require a positive integer dependency release age in days for an exact-version update").option("--min-release-age-exclude <pattern>", "Exclude matching package identities from the release-age policy (repeatable)", (pattern, prior) => [...prior, pattern], []).option("--rollback <receipt-id>", "Restore launcher links from an exact copyfile rollout receipt").option("--adopt-aliases", "Switch verified legacy @hasna/skills aliases to the active copyfile runtime").option("--rollback-aliases <receipt-id>", "Restore aliases from an exact adoption receipt").action(async (options) => {
110558
+ if ((options.minReleaseAge !== undefined || options.minReleaseAgeExclude.length > 0) && (!options.version || options.rollback || options.adoptAliases || options.rollbackAliases)) {
110559
+ console.error(JSON.stringify({ error: "RELEASE_AGE_POLICY_REQUIRES_EXACT_VERSION" }));
110560
+ process.exitCode = 1;
110561
+ return;
110562
+ }
110509
110563
  if ([options.version, options.rollback, options.adoptAliases, options.rollbackAliases].filter(Boolean).length > 1) {
110510
110564
  console.error(JSON.stringify({ error: "SELF_UPDATE_OPERATIONS_ARE_MUTUALLY_EXCLUSIVE" }));
110511
110565
  process.exitCode = 1;
@@ -110541,7 +110595,8 @@ function registerRuntime(parent) {
110541
110595
  }
110542
110596
  if (options.version) {
110543
110597
  try {
110544
- const result2 = await updateCopyfileRuntime(options.version);
110598
+ const minReleaseAge = options.minReleaseAge === undefined ? undefined : /^\d+$/.test(options.minReleaseAge) ? Number(options.minReleaseAge) : NaN;
110599
+ const result2 = await updateCopyfileRuntime(options.version, { minReleaseAge, minReleaseAgeExclude: options.minReleaseAgeExclude });
110545
110600
  if (options.json)
110546
110601
  console.log(JSON.stringify(result2));
110547
110602
  else
package/bin/mcp.js CHANGED
@@ -20019,7 +20019,7 @@ var package_default;
20019
20019
  var init_package = __esm(() => {
20020
20020
  package_default = {
20021
20021
  name: "@hasna/skills",
20022
- version: "0.10.33",
20022
+ version: "0.10.34",
20023
20023
  description: "Skills library for AI coding agents",
20024
20024
  type: "module",
20025
20025
  bin: {
package/bin/migrate.js CHANGED
@@ -7,7 +7,7 @@ import { join as join5 } from "path";
7
7
  // package.json
8
8
  var package_default = {
9
9
  name: "@hasna/skills",
10
- version: "0.10.33",
10
+ version: "0.10.34",
11
11
  description: "Skills library for AI coding agents",
12
12
  type: "module",
13
13
  bin: {
package/bin/server.js CHANGED
@@ -24122,7 +24122,7 @@ var init_dist_es9 = __esm(() => {
24122
24122
  // package.json
24123
24123
  var package_default = {
24124
24124
  name: "@hasna/skills",
24125
- version: "0.10.33",
24125
+ version: "0.10.34",
24126
24126
  description: "Skills library for AI coding agents",
24127
24127
  type: "module",
24128
24128
  bin: {
package/bin/worker.js CHANGED
@@ -24125,7 +24125,7 @@ import { randomUUID as randomUUID6 } from "crypto";
24125
24125
  // package.json
24126
24126
  var package_default = {
24127
24127
  name: "@hasna/skills",
24128
- version: "0.10.33",
24128
+ version: "0.10.34",
24129
24129
  description: "Skills library for AI coding agents",
24130
24130
  type: "module",
24131
24131
  bin: {
@@ -26,6 +26,8 @@ export declare function updateCopyfileRuntime(version: string, options?: {
26
26
  pathValue?: string;
27
27
  registryOrigin?: string;
28
28
  fetcher?: typeof fetch;
29
+ minReleaseAge?: number;
30
+ minReleaseAgeExclude?: string[];
29
31
  onLauncherSwitched?: (path: string) => void;
30
32
  }): Promise<JsonObject>;
31
33
  export declare function rollbackCopyfileRuntime(receiptId: string, options?: {
package/dist/index.js CHANGED
@@ -10975,7 +10975,7 @@ function replaceSkillSession(expected, replacement, plan, options = {}, beforeCo
10975
10975
  // package.json
10976
10976
  var package_default = {
10977
10977
  name: "@hasna/skills",
10978
- version: "0.10.33",
10978
+ version: "0.10.34",
10979
10979
  description: "Skills library for AI coding agents",
10980
10980
  type: "module",
10981
10981
  bin: {
package/dist/sdk/index.js CHANGED
@@ -32373,7 +32373,7 @@ var init_event_sink = __esm(() => {
32373
32373
  // package.json
32374
32374
  var package_default = {
32375
32375
  name: "@hasna/skills",
32376
- version: "0.10.33",
32376
+ version: "0.10.34",
32377
32377
  description: "Skills library for AI coding agents",
32378
32378
  type: "module",
32379
32379
  bin: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/skills",
3
- "version": "0.10.33",
3
+ "version": "0.10.34",
4
4
  "description": "Skills library for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {