@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 +11 -0
- package/dist/commands/install-skills.d.ts.map +1 -1
- package/dist/commands/install-skills.js +26 -5
- package/dist/index.js +4 -3
- package/dist/util/self-update.d.ts +25 -0
- package/dist/util/self-update.d.ts.map +1 -0
- package/dist/util/self-update.js +89 -0
- package/package.json +3 -1
- package/dist/util/update-check.d.ts +0 -27
- package/dist/util/update-check.d.ts.map +0 -1
- package/dist/util/update-check.js +0 -46
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":"
|
|
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" &&
|
|
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
|
|
123
|
-
|
|
124
|
-
.
|
|
125
|
-
|
|
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 {
|
|
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:
|
|
77
|
-
|
|
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.
|
|
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
|
-
}
|