@ze-norm/cli 0.4.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.
- package/dist/api/client.d.ts +2 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +47 -1
- package/dist/auth/device-flow.d.ts.map +1 -1
- package/dist/auth/device-flow.js +119 -7
- package/dist/commands/session-check.d.ts.map +1 -1
- package/dist/commands/session-check.js +3 -1
- package/dist/config/defaults.d.ts +1 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +10 -0
- package/dist/index.js +8 -0
- package/dist/util/errors.d.ts +8 -0
- package/dist/util/errors.d.ts.map +1 -1
- package/dist/util/errors.js +11 -0
- package/dist/util/update-check.d.ts +27 -0
- package/dist/util/update-check.d.ts.map +1 -0
- package/dist/util/update-check.js +46 -0
- package/package.json +4 -2
package/dist/api/client.d.ts
CHANGED
package/dist/api/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/api/client.js
CHANGED
|
@@ -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":"device-flow.d.ts","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"device-flow.d.ts","sourceRoot":"","sources":["../../src/auth/device-flow.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AA+RpD;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAkDpE"}
|
package/dist/auth/device-flow.js
CHANGED
|
@@ -4,7 +4,7 @@ import { exec } from "node:child_process";
|
|
|
4
4
|
import { platform } from "node:os";
|
|
5
5
|
import { URL, URLSearchParams } from "node:url";
|
|
6
6
|
import { log } from "../util/logger.js";
|
|
7
|
-
import { CLI_CALLBACK_PORTS, PRODUCTION_CLERK_CLIENT_ID, PRODUCTION_CLERK_ISSUER, } from "../config/defaults.js";
|
|
7
|
+
import { CLI_CALLBACK_PORTS, PRODUCTION_CLERK_CLIENT_ID, PRODUCTION_CLERK_ISSUER, PRODUCTION_WEB_URL, } from "../config/defaults.js";
|
|
8
8
|
/**
|
|
9
9
|
* Find the first pre-registered callback port that is free to bind. Clerk
|
|
10
10
|
* matches redirect_uris exactly, so the local listener must use one of the
|
|
@@ -28,6 +28,19 @@ async function pickFreeCallbackPort() {
|
|
|
28
28
|
function getClerkIssuer() {
|
|
29
29
|
return process.env["ZENORM_CLERK_ISSUER"] ?? PRODUCTION_CLERK_ISSUER;
|
|
30
30
|
}
|
|
31
|
+
/** Where the browser tab lands after a successful login. */
|
|
32
|
+
function getWebUrl() {
|
|
33
|
+
return process.env["ZENORM_WEB_URL"] ?? PRODUCTION_WEB_URL;
|
|
34
|
+
}
|
|
35
|
+
/** Escape user-influenced text before interpolating into the result page HTML. */
|
|
36
|
+
function escapeHtml(value) {
|
|
37
|
+
return value
|
|
38
|
+
.replace(/&/g, "&")
|
|
39
|
+
.replace(/</g, "<")
|
|
40
|
+
.replace(/>/g, ">")
|
|
41
|
+
.replace(/"/g, """)
|
|
42
|
+
.replace(/'/g, "'");
|
|
43
|
+
}
|
|
31
44
|
function getClerkClientId() {
|
|
32
45
|
return (process.env["ZENORM_CLERK_CLIENT_ID"] ??
|
|
33
46
|
process.env["VITE_CLERK_PUBLISHABLE_KEY"] ??
|
|
@@ -62,6 +75,100 @@ function tryOpenBrowser(url) {
|
|
|
62
75
|
}
|
|
63
76
|
});
|
|
64
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Render a self-contained, branded error page for the localhost callback.
|
|
80
|
+
*
|
|
81
|
+
* Only the failure paths render a page — a successful login 302-redirects the
|
|
82
|
+
* browser into the app instead. Served by the raw `http` server (no
|
|
83
|
+
* React/bundler available), so the page is fully inline: dark theme, ZeNorm
|
|
84
|
+
* wordmark, and the same card framing the web app uses on /sign-in.
|
|
85
|
+
*/
|
|
86
|
+
function renderCallbackPage(heading, body) {
|
|
87
|
+
const accent = "#f87171";
|
|
88
|
+
const glyph = "!";
|
|
89
|
+
return `<!doctype html>
|
|
90
|
+
<html lang="en">
|
|
91
|
+
<head>
|
|
92
|
+
<meta charset="utf-8" />
|
|
93
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
94
|
+
<title>ZeNorm CLI · ${heading}</title>
|
|
95
|
+
<style>
|
|
96
|
+
:root { color-scheme: dark; }
|
|
97
|
+
* { box-sizing: border-box; }
|
|
98
|
+
body {
|
|
99
|
+
margin: 0;
|
|
100
|
+
min-height: 100vh;
|
|
101
|
+
display: grid;
|
|
102
|
+
place-items: center;
|
|
103
|
+
padding: 24px;
|
|
104
|
+
background:
|
|
105
|
+
radial-gradient(ellipse 540px 360px at 50% 38%, rgba(99,102,241,0.16), transparent 70%),
|
|
106
|
+
#0a0a0b;
|
|
107
|
+
color: #ededee;
|
|
108
|
+
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
109
|
+
Roboto, Helvetica, Arial, sans-serif;
|
|
110
|
+
-webkit-font-smoothing: antialiased;
|
|
111
|
+
}
|
|
112
|
+
.card {
|
|
113
|
+
position: relative;
|
|
114
|
+
width: 100%;
|
|
115
|
+
max-width: 380px;
|
|
116
|
+
border: 1px solid rgba(255,255,255,0.10);
|
|
117
|
+
border-radius: 12px;
|
|
118
|
+
background: rgba(20,20,22,0.72);
|
|
119
|
+
backdrop-filter: blur(6px);
|
|
120
|
+
box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset,
|
|
121
|
+
0 28px 70px -32px rgba(0,0,0,0.6);
|
|
122
|
+
padding: 40px 28px 32px;
|
|
123
|
+
text-align: center;
|
|
124
|
+
}
|
|
125
|
+
.eyebrow {
|
|
126
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
127
|
+
font-size: 10px;
|
|
128
|
+
letter-spacing: 0.22em;
|
|
129
|
+
text-transform: uppercase;
|
|
130
|
+
color: rgba(237,237,238,0.55);
|
|
131
|
+
margin-bottom: 22px;
|
|
132
|
+
}
|
|
133
|
+
.badge {
|
|
134
|
+
width: 48px; height: 48px;
|
|
135
|
+
margin: 0 auto 20px;
|
|
136
|
+
display: grid; place-items: center;
|
|
137
|
+
border-radius: 999px;
|
|
138
|
+
border: 1px solid ${accent}55;
|
|
139
|
+
background: ${accent}1a;
|
|
140
|
+
color: ${accent};
|
|
141
|
+
font-size: 22px; font-weight: 600;
|
|
142
|
+
line-height: 1;
|
|
143
|
+
}
|
|
144
|
+
h1 {
|
|
145
|
+
margin: 0 0 8px;
|
|
146
|
+
font-size: 1.5rem;
|
|
147
|
+
letter-spacing: -0.01em;
|
|
148
|
+
font-weight: 600;
|
|
149
|
+
}
|
|
150
|
+
p { margin: 0; color: rgba(237,237,238,0.62); font-size: 0.9rem; line-height: 1.5; }
|
|
151
|
+
.footer {
|
|
152
|
+
margin-top: 24px;
|
|
153
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
154
|
+
font-size: 10px;
|
|
155
|
+
letter-spacing: 0.18em;
|
|
156
|
+
text-transform: uppercase;
|
|
157
|
+
color: rgba(237,237,238,0.4);
|
|
158
|
+
}
|
|
159
|
+
</style>
|
|
160
|
+
</head>
|
|
161
|
+
<body>
|
|
162
|
+
<div class="card">
|
|
163
|
+
<div class="eyebrow">ZeNorm CLI</div>
|
|
164
|
+
<div class="badge">${glyph}</div>
|
|
165
|
+
<h1>${heading}</h1>
|
|
166
|
+
<p>${body}</p>
|
|
167
|
+
<div class="footer">Spec authoring, traced end-to-end</div>
|
|
168
|
+
</div>
|
|
169
|
+
</body>
|
|
170
|
+
</html>`;
|
|
171
|
+
}
|
|
65
172
|
/**
|
|
66
173
|
* Start a localhost HTTP server and wait for the OAuth callback.
|
|
67
174
|
* Resolves with the authorization code once the redirect arrives.
|
|
@@ -80,21 +187,26 @@ function waitForCallback(port, expectedState) {
|
|
|
80
187
|
const error = url.searchParams.get("error");
|
|
81
188
|
if (error) {
|
|
82
189
|
const desc = url.searchParams.get("error_description") ?? error;
|
|
83
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
84
|
-
res.end(
|
|
190
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
191
|
+
res.end(renderCallbackPage("Login failed", `${escapeHtml(desc)}. You can close this tab and try again.`));
|
|
85
192
|
server.close();
|
|
86
193
|
reject(new Error(`Authorization failed: ${desc}`));
|
|
87
194
|
return;
|
|
88
195
|
}
|
|
89
196
|
if (!code || state !== expectedState) {
|
|
90
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
91
|
-
res.end(
|
|
197
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
198
|
+
res.end(renderCallbackPage("Invalid callback", "Missing code or state mismatch. You can close this tab and try again."));
|
|
92
199
|
server.close();
|
|
93
200
|
reject(new Error("Invalid callback: missing code or state mismatch"));
|
|
94
201
|
return;
|
|
95
202
|
}
|
|
96
|
-
|
|
97
|
-
|
|
203
|
+
// No standalone success page — send the browser straight into the app
|
|
204
|
+
// (the user is already signed in), so the tab lands on the real product
|
|
205
|
+
// instead of a "you can close this tab" interstitial. The CLI reports
|
|
206
|
+
// success in the terminal. Errors still render the branded page below,
|
|
207
|
+
// since those can't sensibly drop the user into the dashboard.
|
|
208
|
+
res.writeHead(302, { Location: `${getWebUrl()}/dashboard` });
|
|
209
|
+
res.end();
|
|
98
210
|
server.close();
|
|
99
211
|
resolve(code);
|
|
100
212
|
});
|
|
@@ -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,
|
|
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");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const PRODUCTION_API_URL = "https://api.zenorm.com";
|
|
2
2
|
export declare const PRODUCTION_CLERK_ISSUER = "https://clerk.zenorm.com";
|
|
3
|
+
export declare const PRODUCTION_WEB_URL = "https://zenorm.com";
|
|
3
4
|
export declare const PRODUCTION_CLERK_CLIENT_ID = "nUFCpxIFWJ4YSB3A";
|
|
4
5
|
export declare const CLI_CALLBACK_PORTS: readonly [4571, 4572, 4573];
|
|
5
6
|
//# sourceMappingURL=defaults.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB,2BAA2B,CAAC;AAC3D,eAAO,MAAM,uBAAuB,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"defaults.d.ts","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB,2BAA2B,CAAC;AAC3D,eAAO,MAAM,uBAAuB,6BAA6B,CAAC;AAIlE,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAWvD,eAAO,MAAM,0BAA0B,qBAAqB,CAAC;AAK7D,eAAO,MAAM,kBAAkB,6BAA8B,CAAC"}
|
package/dist/config/defaults.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
export const PRODUCTION_API_URL = "https://api.zenorm.com";
|
|
2
2
|
export const PRODUCTION_CLERK_ISSUER = "https://clerk.zenorm.com";
|
|
3
|
+
// Prod web app. After a successful login the localhost callback 302-redirects
|
|
4
|
+
// the browser here so the tab lands on the real product (already signed in)
|
|
5
|
+
// instead of a standalone "you can close this tab" page.
|
|
6
|
+
export const PRODUCTION_WEB_URL = "https://zenorm.com";
|
|
3
7
|
// Clerk OAuth Application client id for the public CLI (prod instance
|
|
4
8
|
// clerk.zenorm.com). NOT the publishable key — this is the OAuth 2.0 client id
|
|
5
9
|
// from the "ZeNorm CLI" OAuth app. Its registered redirect_uris are the fixed
|
|
6
10
|
// localhost callback ports in CLI_CALLBACK_PORTS below.
|
|
11
|
+
//
|
|
12
|
+
// NOTE: this OAuth app has `consent_screen_enabled` set to FALSE out-of-band on
|
|
13
|
+
// the Clerk instance (via the Clerk Backend API, not tracked in this repo).
|
|
14
|
+
// That is what lets an already-signed-in `zenorm login` skip Clerk's OAuth
|
|
15
|
+
// consent screen and redirect straight to the localhost callback. To restore
|
|
16
|
+
// the consent screen, PATCH the OAuth app back to `consent_screen_enabled:true`.
|
|
7
17
|
export const PRODUCTION_CLERK_CLIENT_ID = "nUFCpxIFWJ4YSB3A";
|
|
8
18
|
// Fixed localhost callback ports the device-auth flow binds, in order. Clerk
|
|
9
19
|
// enforces exact redirect_uri matching, so these MUST stay in sync with the
|
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.
|
package/dist/util/errors.d.ts
CHANGED
|
@@ -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"}
|
package/dist/util/errors.js
CHANGED
|
@@ -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.
|
|
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",
|