@ze-norm/cli 0.5.0 → 0.7.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,3 +1,5 @@
1
+ /** Suppress the soft "upgrade recommended" nudge for the rest of this process. */
2
+ export declare function setQuietUpgradeNudges(): void;
1
3
  export interface ApiClientOptions {
2
4
  baseUrl?: string;
3
5
  token?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA0BD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,KAAK,CAAgB;gBAEjB,IAAI,CAAC,EAAE,gBAAgB;IAKnC,OAAO,CAAC,UAAU;IAqBlB,OAAO,CAAC,cAAc;IAStB,2EAA2E;IAC3E,gBAAgB,IAAI,OAAO;YAIb,OAAO;IAwCf,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAIhC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIjD,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIhD,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIlD,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAIzC;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC;CAG7C"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAqBA,kFAAkF;AAClF,wBAAgB,qBAAqB,IAAI,IAAI,CAE5C;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA0BD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,KAAK,CAAgB;gBAEjB,IAAI,CAAC,EAAE,gBAAgB;IAKnC,OAAO,CAAC,UAAU;IA0BlB,OAAO,CAAC,cAAc;IAStB,2EAA2E;IAC3E,gBAAgB,IAAI,OAAO;YAIb,OAAO;IAsEf,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAIhC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIjD,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIhD,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIlD,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAIzC;;OAEG;IACG,cAAc,IAAI,OAAO,CAAC,WAAW,CAAC;CAG7C"}
@@ -1,8 +1,24 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { ApiError, AuthError } from "../util/errors.js";
3
+ import { ApiError, AuthError, UpgradeRequiredError } from "../util/errors.js";
4
4
  import { resolveToken } from "../auth/store.js";
5
5
  import { PRODUCTION_API_URL } from "../config/defaults.js";
6
+ import { getCliVersion } from "../util/package.js";
7
+ import { log } from "../util/logger.js";
8
+ const UPGRADE_COMMAND = "npm install -g @ze-norm/cli@latest";
9
+ // Print the "an upgrade is recommended" nudge at most once per process, even
10
+ // when a single command makes several API calls. The server sets the
11
+ // `x-zenorm-cli-upgrade: recommended` header on success when this CLI is below
12
+ // the recommended (but at/above the supported) version.
13
+ let recommendedNudgeShown = false;
14
+ // `session-check` runs as a Claude Code Stop hook; an unsolicited upgrade
15
+ // warning mid-hook is noise. It calls this to suppress the soft nudge (the
16
+ // 426 hard-block still applies — a truly unsupported CLI must not run).
17
+ let quietUpgradeNudges = false;
18
+ /** Suppress the soft "upgrade recommended" nudge for the rest of this process. */
19
+ export function setQuietUpgradeNudges() {
20
+ quietUpgradeNudges = true;
21
+ }
6
22
  function resolveBaseUrl(explicit) {
7
23
  // 1. Explicit option
8
24
  if (explicit)
@@ -32,6 +48,11 @@ export class ZenormClient {
32
48
  getHeaders() {
33
49
  const headers = {
34
50
  "Content-Type": "application/json",
51
+ // Always advertise the CLI version so the API can gate stale clients
52
+ // (426 below the supported floor) and nudge near-stale ones (a warning
53
+ // header below the recommended version). Sent on every request,
54
+ // authenticated or dev-bypassed.
55
+ "x-zenorm-cli-version": getCliVersion(),
35
56
  };
36
57
  if (this.token) {
37
58
  headers["Authorization"] = `Bearer ${this.token}`;
@@ -81,6 +102,31 @@ export class ZenormClient {
81
102
  if (res.status === 401) {
82
103
  throw new AuthError("Authentication required. Run `zenorm login` first.");
83
104
  }
105
+ // 426 Upgrade Required: this CLI is below the API's supported version
106
+ // floor. Surface the server's reason (if any) plus the upgrade command and
107
+ // stop — retrying the same binary cannot succeed.
108
+ if (res.status === 426) {
109
+ let serverMessage = null;
110
+ try {
111
+ const body = (await res.json());
112
+ if (typeof body.message === "string")
113
+ serverMessage = body.message;
114
+ }
115
+ catch {
116
+ // body may not be JSON
117
+ }
118
+ const reason = serverMessage ??
119
+ `Your ZeNorm CLI (${getCliVersion()}) is no longer supported by the server.`;
120
+ throw new UpgradeRequiredError(`${reason}\n\nUpgrade with:\n ${UPGRADE_COMMAND}`);
121
+ }
122
+ // A successful response may still carry a soft upgrade nudge: the CLI is
123
+ // below the recommended version but at/above the supported floor.
124
+ if (res.headers.get("x-zenorm-cli-upgrade") === "recommended" &&
125
+ !recommendedNudgeShown &&
126
+ !quietUpgradeNudges) {
127
+ recommendedNudgeShown = true;
128
+ log.warn(`A newer ZeNorm CLI is available (you have ${getCliVersion()}). Upgrade with: ${UPGRADE_COMMAND}`);
129
+ }
84
130
  if (!res.ok) {
85
131
  let responseBody = null;
86
132
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"session-check.d.ts","sourceRoot":"","sources":["../../src/commands/session-check.ts"],"names":[],"mappings":"AA2CA;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAqFxE"}
1
+ {"version":3,"file":"session-check.d.ts","sourceRoot":"","sources":["../../src/commands/session-check.ts"],"names":[],"mappings":"AA2CA;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuFxE"}
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, existsSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { ZenormClient } from "../api/client.js";
3
+ import { ZenormClient, setQuietUpgradeNudges } from "../api/client.js";
4
4
  import { log } from "../util/logger.js";
5
5
  /**
6
6
  * Checks whether the session transcript references zenorm task work.
@@ -82,6 +82,8 @@ export async function sessionCheckCommand(_argv) {
82
82
  process.exit(0);
83
83
  }
84
84
  try {
85
+ // Stop-hook context: keep the soft upgrade nudge quiet (a 426 still blocks).
86
+ setQuietUpgradeNudges();
85
87
  const client = new ZenormClient();
86
88
  const { tasks } = await client.get(`/v1/specs/${config.specId}/tasks`);
87
89
  const activeTasks = tasks.filter((t) => t.status === "active");
package/dist/index.js CHANGED
@@ -10,6 +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
14
  const helpText = `zenorm - ZeNorm CLI
14
15
 
15
16
  Usage:
@@ -68,6 +69,13 @@ async function main() {
68
69
  if (!handler) {
69
70
  throw new CliError(`Unknown command: ${commandName}\n\nRun \`zenorm --help\` for usage.`);
70
71
  }
72
+ // Passive "newer version on npm" nudge. Skipped for `session-check`, which
73
+ // speaks the Claude Code Stop-hook protocol on stdout/stderr and must emit
74
+ // nothing else.
75
+ if (commandName !== "session-check") {
76
+ // Fire-and-forget: never block or fail the command on the update check.
77
+ void maybeNotifyUpdate();
78
+ }
71
79
  // Pass everything after the command name to the handler.
72
80
  // Routed to stderr (debugErr): `session-check` reserves stdout for the
73
81
  // Claude Code hook protocol, and this dispatch line runs for every command.
@@ -10,4 +10,12 @@ export declare class ApiError extends CliError {
10
10
  export declare class AuthError extends CliError {
11
11
  constructor(message: string);
12
12
  }
13
+ /**
14
+ * The API rejected this CLI with 426 Upgrade Required: the installed version is
15
+ * below the server's supported floor. The message carries the upgrade command;
16
+ * retrying the same binary cannot succeed.
17
+ */
18
+ export declare class UpgradeRequiredError extends CliError {
19
+ constructor(message: string);
20
+ }
13
21
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/util/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,QAAS,SAAQ,KAAK;IAGxB,QAAQ,EAAE,MAAM;gBADvB,OAAO,EAAE,MAAM,EACR,QAAQ,GAAE,MAAU;CAK9B;AAED,qBAAa,QAAS,SAAQ,QAAQ;IAG3B,MAAM,EAAE,MAAM;IACd,IAAI,EAAE,OAAO;gBAFpB,OAAO,EAAE,MAAM,EACR,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO;CAKvB;AAED,qBAAa,SAAU,SAAQ,QAAQ;gBACzB,OAAO,EAAE,MAAM;CAI5B"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/util/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,QAAS,SAAQ,KAAK;IAGxB,QAAQ,EAAE,MAAM;gBADvB,OAAO,EAAE,MAAM,EACR,QAAQ,GAAE,MAAU;CAK9B;AAED,qBAAa,QAAS,SAAQ,QAAQ;IAG3B,MAAM,EAAE,MAAM;IACd,IAAI,EAAE,OAAO;gBAFpB,OAAO,EAAE,MAAM,EACR,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO;CAKvB;AAED,qBAAa,SAAU,SAAQ,QAAQ;gBACzB,OAAO,EAAE,MAAM;CAI5B;AAED;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,QAAQ;gBACpC,OAAO,EAAE,MAAM;CAI5B"}
@@ -22,3 +22,14 @@ export class AuthError extends CliError {
22
22
  this.name = "AuthError";
23
23
  }
24
24
  }
25
+ /**
26
+ * The API rejected this CLI with 426 Upgrade Required: the installed version is
27
+ * below the server's supported floor. The message carries the upgrade command;
28
+ * retrying the same binary cannot succeed.
29
+ */
30
+ export class UpgradeRequiredError extends CliError {
31
+ constructor(message) {
32
+ super(message, 1);
33
+ this.name = "UpgradeRequiredError";
34
+ }
35
+ }
@@ -0,0 +1,27 @@
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
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,46 @@
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
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ze-norm/cli",
3
3
  "private": false,
4
- "version": "0.5.0",
4
+ "version": "0.7.0",
5
5
  "license": "SEE LICENSE IN README.md",
6
6
  "type": "module",
7
7
  "repository": {
@@ -30,10 +30,12 @@
30
30
  "typecheck": "tsc -p tsconfig.json --noEmit"
31
31
  },
32
32
  "dependencies": {
33
- "skills": "1.5.10"
33
+ "skills": "1.5.10",
34
+ "update-notifier": "^7.3.1"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/node": "^22.15.3",
38
+ "@types/update-notifier": "^6.0.8",
37
39
  "@zenorm/eslint-config": "workspace:*",
38
40
  "@zenorm/tsconfig": "workspace:*",
39
41
  "eslint": "^9.25.1",