@ze-norm/cli 0.8.0 → 0.10.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 CHANGED
@@ -12,6 +12,17 @@ The CLI includes the ZeNorm agent skills in the npm package. `zenorm install-ski
12
12
  installs those bundled skills through the `skills` package; it does not require
13
13
  a separate skills repository.
14
14
 
15
+ ## Staying up to date
16
+
17
+ The CLI keeps itself current. When a newer version is on npm and your global
18
+ install location is writable (the common case for nvm/fnm/volta or a
19
+ `~/.npm-global` prefix), it upgrades in the background — the new version takes
20
+ effect on your next command. If the install location needs root (system or
21
+ Homebrew node, or a `sudo`-installed global), it prints the upgrade command
22
+ instead of escalating privileges. Set `ZENORM_NO_AUTO_UPDATE=1` to disable the
23
+ background upgrade entirely. Auto-update is skipped in CI and non-interactive
24
+ shells.
25
+
15
26
  Examples:
16
27
 
17
28
  ```sh
@@ -1 +1 @@
1
- {"version":3,"file":"install-skills.d.ts","sourceRoot":"","sources":["../../src/commands/install-skills.ts"],"names":[],"mappings":"AA4MA,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCxE"}
1
+ {"version":3,"file":"install-skills.d.ts","sourceRoot":"","sources":["../../src/commands/install-skills.ts"],"names":[],"mappings":"AAqOA,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCxE"}
@@ -93,6 +93,21 @@ function installToTargetDir(targetDir, force) {
93
93
  }
94
94
  log.success(`Installed ${skillNames.length} ZeNorm skill(s): ${targetDir}`);
95
95
  }
96
+ // `skills list` ignores its `--agent` flag and returns every agent's skills, so
97
+ // we scope by the displayName entries in each skill's `agents` array instead.
98
+ // Match on alphanumeric-lowercase normalization: "claude-code" <-> "Claude Code",
99
+ // "github-copilot" <-> "GitHub Copilot", "gemini-cli" <-> "Gemini CLI", etc.
100
+ function normalizeAgentLabel(label) {
101
+ return label.toLowerCase().replace(/[^a-z0-9]/g, "");
102
+ }
103
+ function skillBelongsToAgent(skill, agent) {
104
+ if (agent === "*")
105
+ return true;
106
+ if (!Array.isArray(skill.agents))
107
+ return false;
108
+ const wanted = normalizeAgentLabel(agent);
109
+ return skill.agents.some((label) => typeof label === "string" && normalizeAgentLabel(label) === wanted);
110
+ }
96
111
  function listInstalledSkills(agent, project) {
97
112
  const scopeArgs = project ? [] : ["--global"];
98
113
  const agentArgs = agent === "*" ? [] : ["--agent", agent];
@@ -116,13 +131,19 @@ function assertNoInstalledZeNormSkills(agent, project, force) {
116
131
  if (force)
117
132
  return;
118
133
  const bundledSkillNames = new Set(listSkillDirs(getBundledSkillsRoot()));
119
- const existing = listInstalledSkills(agent, project).filter((skill) => typeof skill.name === "string" && bundledSkillNames.has(skill.name));
134
+ const existing = listInstalledSkills(agent, project).filter((skill) => typeof skill.name === "string" &&
135
+ bundledSkillNames.has(skill.name) &&
136
+ skillBelongsToAgent(skill, agent));
120
137
  if (existing.length === 0)
121
138
  return;
122
- const details = existing
123
- .map((skill) => typeof skill.path === "string" ? `${String(skill.name)} (${skill.path})` : String(skill.name))
124
- .join(", ");
125
- throw new CliError(`Refusing to overwrite existing ZeNorm skills: ${details}. Re-run with --force if this is intentional.`);
139
+ const agentLabel = agent === "*" ? "your agents" : agent;
140
+ const skillLines = existing
141
+ .map((skill) => typeof skill.path === "string"
142
+ ? ` ${String(skill.name)} (${skill.path})`
143
+ : ` • ${String(skill.name)}`)
144
+ .join("\n");
145
+ throw new CliError(`ZeNorm skills are already installed for ${agentLabel}:\n${skillLines}\n\n` +
146
+ `Leaving them as-is. To reinstall the bundled versions, re-run with --force.`);
126
147
  }
127
148
  function runSkillsInstaller(agent, project, force) {
128
149
  assertNoInstalledZeNormSkills(agent, project, force);
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { initCommand } from "./commands/init.js";
10
10
  import { installSkillsCommand } from "./commands/install-skills.js";
11
11
  import { sessionCheckCommand } from "./commands/session-check.js";
12
12
  import { getCliVersion } from "./util/package.js";
13
- import { maybeNotifyUpdate } from "./util/update-check.js";
13
+ import { maybeSelfUpdate } from "./util/self-update.js";
14
14
  const helpText = `zenorm - ZeNorm CLI
15
15
 
16
16
  Usage:
@@ -73,8 +73,9 @@ async function main() {
73
73
  // speaks the Claude Code Stop-hook protocol on stdout/stderr and must emit
74
74
  // nothing else.
75
75
  if (commandName !== "session-check") {
76
- // Fire-and-forget: never block or fail the command on the update check.
77
- void maybeNotifyUpdate();
76
+ // Fire-and-forget: auto-upgrade in the background when possible, else nudge.
77
+ // Never blocks or fails the command.
78
+ void maybeSelfUpdate();
78
79
  }
79
80
  // Pass everything after the command name to the handler.
80
81
  // Routed to stderr (debugErr): `session-check` reserves stdout for the
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Auto-upgrade the CLI in the background when a newer version is on npm.
3
+ *
4
+ * Distribution is `npm install -g`, so there is no safe way to hot-swap the
5
+ * running process. The workable model (used by codex, qwen-code, pi, etc.) is
6
+ * apply-on-next-run: detect a newer version, spawn a *detached* `npm i -g`
7
+ * that outlives this process, let the current command finish on the current
8
+ * version, and the upgrade is live next invocation.
9
+ *
10
+ * The load-bearing constraint is whether the npm global prefix is writable:
11
+ * - Writable (user-owned prefix: nvm/fnm/volta, or `npm config set prefix`
12
+ * to a home dir) -> spawn the background upgrade. This is the silent path.
13
+ * - Not writable (system/Homebrew node, or a sudo-installed global) -> never
14
+ * sudo; fall back to printing the manual upgrade command.
15
+ *
16
+ * All of this is best-effort and never blocks or fails a real command. Every
17
+ * dependency is lazy-imported so a missing optional dep can't crash startup
18
+ * (the published package ships only `dist`).
19
+ *
20
+ * Opt out with ZENORM_NO_AUTO_UPDATE=1. Skipped in CI and non-interactive
21
+ * shells, and for non-global installs (a local dep / npx invocation — updating
22
+ * someone's project dependency from under them would be wrong).
23
+ */
24
+ export declare function maybeSelfUpdate(): Promise<void>;
25
+ //# sourceMappingURL=self-update.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"self-update.d.ts","sourceRoot":"","sources":["../../src/util/self-update.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAiDrD"}
@@ -0,0 +1,89 @@
1
+ import { spawn } from "node:child_process";
2
+ import { accessSync, constants } from "node:fs";
3
+ import { log } from "./logger.js";
4
+ import { getCliVersion } from "./package.js";
5
+ const PACKAGE_NAME = "@ze-norm/cli";
6
+ const UPGRADE_COMMAND = `npm install -g ${PACKAGE_NAME}@latest`;
7
+ const OPT_OUT_ENV = "ZENORM_NO_AUTO_UPDATE";
8
+ /**
9
+ * Auto-upgrade the CLI in the background when a newer version is on npm.
10
+ *
11
+ * Distribution is `npm install -g`, so there is no safe way to hot-swap the
12
+ * running process. The workable model (used by codex, qwen-code, pi, etc.) is
13
+ * apply-on-next-run: detect a newer version, spawn a *detached* `npm i -g`
14
+ * that outlives this process, let the current command finish on the current
15
+ * version, and the upgrade is live next invocation.
16
+ *
17
+ * The load-bearing constraint is whether the npm global prefix is writable:
18
+ * - Writable (user-owned prefix: nvm/fnm/volta, or `npm config set prefix`
19
+ * to a home dir) -> spawn the background upgrade. This is the silent path.
20
+ * - Not writable (system/Homebrew node, or a sudo-installed global) -> never
21
+ * sudo; fall back to printing the manual upgrade command.
22
+ *
23
+ * All of this is best-effort and never blocks or fails a real command. Every
24
+ * dependency is lazy-imported so a missing optional dep can't crash startup
25
+ * (the published package ships only `dist`).
26
+ *
27
+ * Opt out with ZENORM_NO_AUTO_UPDATE=1. Skipped in CI and non-interactive
28
+ * shells, and for non-global installs (a local dep / npx invocation — updating
29
+ * someone's project dependency from under them would be wrong).
30
+ */
31
+ export async function maybeSelfUpdate() {
32
+ try {
33
+ if (process.env[OPT_OUT_ENV])
34
+ return;
35
+ // CI and non-TTY: don't mutate the environment behind an automated caller.
36
+ if (process.env["CI"] || !process.stdout.isTTY)
37
+ return;
38
+ const current = getCliVersion();
39
+ const { default: updateNotifier } = await import("update-notifier");
40
+ const notifier = updateNotifier({
41
+ pkg: { name: PACKAGE_NAME, version: current },
42
+ updateCheckInterval: 1000 * 60 * 60 * 24,
43
+ });
44
+ // `update` is populated from update-notifier's cached daily check; null
45
+ // when no newer version is known yet. Reusing it avoids a second registry
46
+ // round-trip.
47
+ const update = notifier.update;
48
+ if (!update || update.latest === current)
49
+ return;
50
+ const { default: isInstalledGlobally } = await import("is-installed-globally");
51
+ if (!isInstalledGlobally) {
52
+ // Local dep or npx: just nudge, never touch it.
53
+ printManualUpgrade(current, update.latest);
54
+ return;
55
+ }
56
+ const { default: globalDirectory } = await import("global-directory");
57
+ const prefix = globalDirectory.npm.prefix;
58
+ if (!prefix || !isWritable(prefix)) {
59
+ // System/Homebrew/sudo install — can't write without root, and we never
60
+ // sudo. Tell the user how to upgrade themselves.
61
+ printManualUpgrade(current, update.latest);
62
+ return;
63
+ }
64
+ // Detached, non-blocking. Outlives this process; the upgrade lands for the
65
+ // next invocation. stdio ignored so it can't corrupt this command's output.
66
+ const child = spawn("npm", ["install", "-g", `${PACKAGE_NAME}@${update.latest}`], {
67
+ detached: true,
68
+ stdio: "ignore",
69
+ });
70
+ child.unref();
71
+ log.warn(`Updating ZeNorm CLI ${current} -> ${update.latest} in the background. ` +
72
+ `The new version takes effect on your next command.`);
73
+ }
74
+ catch {
75
+ // Never break a real command on an update attempt.
76
+ }
77
+ }
78
+ function isWritable(dir) {
79
+ try {
80
+ accessSync(dir, constants.W_OK);
81
+ return true;
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
87
+ function printManualUpgrade(current, latest) {
88
+ log.warn(`A newer ZeNorm CLI is available (${current} -> ${latest}). Upgrade with: ${UPGRADE_COMMAND}`);
89
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ze-norm/cli",
3
3
  "private": false,
4
- "version": "0.8.0",
4
+ "version": "0.10.0",
5
5
  "license": "SEE LICENSE IN README.md",
6
6
  "type": "module",
7
7
  "repository": {
@@ -30,6 +30,8 @@
30
30
  "typecheck": "tsc -p tsconfig.json --noEmit"
31
31
  },
32
32
  "dependencies": {
33
+ "global-directory": "^4.0.1",
34
+ "is-installed-globally": "^1.0.0",
33
35
  "skills": "1.5.10",
34
36
  "update-notifier": "^7.3.1"
35
37
  },
@@ -1,27 +0,0 @@
1
- /**
2
- * Passively check npm for a newer @ze-norm/cli and print a boxed notice when
3
- * one exists. This is the offline-from-the-API nudge layer: it works even
4
- * before `zenorm login` and on commands that never call the API. The
5
- * server-side gate (a 426 or the `x-zenorm-cli-upgrade` header) covers the
6
- * authenticated request path.
7
- *
8
- * update-notifier checks at most once per `updateCheckInterval`, runs the
9
- * actual registry lookup in a detached process (never blocking the command),
10
- * caches result in the user's XDG config dir, and self-suppresses in CI and
11
- * non-TTY environments. `notify()` writes to stderr, so it never corrupts a
12
- * command's stdout.
13
- *
14
- * Skipped for `session-check`: that command speaks the Claude Code Stop-hook
15
- * protocol on stdout, and a deferred boxen notice (printed via an exit hook) is
16
- * unwanted noise mid-hook. (stderr itself is safe there — session-check logs
17
- * diagnostics to stderr — but the notifier adds nothing useful in that path.)
18
- *
19
- * `update-notifier` is imported lazily (dynamic `import()`), not at module top.
20
- * The npm package ships only `dist`; its runtime deps live in node_modules of a
21
- * real install. A static top-level import would crash *every* invocation — even
22
- * `--help` — if the dep is ever absent (e.g. the published-tarball smoke test
23
- * runs `dist/index.js` with no node_modules). Lazy-loading inside the try/catch
24
- * keeps a missing or broken notifier strictly non-fatal.
25
- */
26
- export declare function maybeNotifyUpdate(): Promise<void>;
27
- //# sourceMappingURL=update-check.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"update-check.d.ts","sourceRoot":"","sources":["../../src/util/update-check.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAkBvD"}
@@ -1,46 +0,0 @@
1
- import { getCliVersion } from "./package.js";
2
- const PACKAGE_NAME = "@ze-norm/cli";
3
- /**
4
- * Passively check npm for a newer @ze-norm/cli and print a boxed notice when
5
- * one exists. This is the offline-from-the-API nudge layer: it works even
6
- * before `zenorm login` and on commands that never call the API. The
7
- * server-side gate (a 426 or the `x-zenorm-cli-upgrade` header) covers the
8
- * authenticated request path.
9
- *
10
- * update-notifier checks at most once per `updateCheckInterval`, runs the
11
- * actual registry lookup in a detached process (never blocking the command),
12
- * caches result in the user's XDG config dir, and self-suppresses in CI and
13
- * non-TTY environments. `notify()` writes to stderr, so it never corrupts a
14
- * command's stdout.
15
- *
16
- * Skipped for `session-check`: that command speaks the Claude Code Stop-hook
17
- * protocol on stdout, and a deferred boxen notice (printed via an exit hook) is
18
- * unwanted noise mid-hook. (stderr itself is safe there — session-check logs
19
- * diagnostics to stderr — but the notifier adds nothing useful in that path.)
20
- *
21
- * `update-notifier` is imported lazily (dynamic `import()`), not at module top.
22
- * The npm package ships only `dist`; its runtime deps live in node_modules of a
23
- * real install. A static top-level import would crash *every* invocation — even
24
- * `--help` — if the dep is ever absent (e.g. the published-tarball smoke test
25
- * runs `dist/index.js` with no node_modules). Lazy-loading inside the try/catch
26
- * keeps a missing or broken notifier strictly non-fatal.
27
- */
28
- export async function maybeNotifyUpdate() {
29
- try {
30
- const { default: updateNotifier } = await import("update-notifier");
31
- const notifier = updateNotifier({
32
- pkg: { name: PACKAGE_NAME, version: getCliVersion() },
33
- // Once per day is plenty for a CLI; the lookup is detached regardless.
34
- updateCheckInterval: 1000 * 60 * 60 * 24,
35
- });
36
- notifier.notify({
37
- defer: true,
38
- message: "Update available {currentVersion} → {latestVersion}\nRun npm i -g " +
39
- PACKAGE_NAME + "@latest to update",
40
- });
41
- }
42
- catch {
43
- // An update check must never break a real command — swallow any failure
44
- // (offline, unwritable config dir, missing optional dep, etc.).
45
- }
46
- }