@ze-norm/cli 0.11.2 → 0.11.5
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 +8 -2
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +46 -11
- package/dist/api/types.d.ts +8 -0
- package/dist/api/types.d.ts.map +1 -1
- package/dist/auth/device-flow.d.ts +14 -0
- package/dist/auth/device-flow.d.ts.map +1 -1
- package/dist/auth/device-flow.js +31 -0
- package/dist/auth/store.d.ts +31 -1
- package/dist/auth/store.d.ts.map +1 -1
- package/dist/auth/store.js +88 -1
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +8 -3
- package/dist/commands/work-render.d.ts +124 -0
- package/dist/commands/work-render.d.ts.map +1 -0
- package/dist/commands/work-render.js +590 -0
- package/dist/commands/work.d.ts +121 -0
- package/dist/commands/work.d.ts.map +1 -0
- package/dist/commands/work.js +523 -0
- package/dist/index.js +3 -0
- package/package.json +4 -2
package/dist/api/client.d.ts
CHANGED
|
@@ -11,12 +11,18 @@ export interface AuthContext {
|
|
|
11
11
|
}
|
|
12
12
|
export declare class ZenormClient {
|
|
13
13
|
private baseUrl;
|
|
14
|
-
private
|
|
14
|
+
private explicitToken;
|
|
15
15
|
constructor(opts?: ApiClientOptions);
|
|
16
|
-
|
|
16
|
+
/** Resolve the bearer token for a request, refreshing it if expired. */
|
|
17
|
+
private resolveRequestToken;
|
|
18
|
+
private buildHeaders;
|
|
17
19
|
private isLocalBaseUrl;
|
|
18
20
|
/** True when this client targets a localhost API (dev-bypass eligible). */
|
|
19
21
|
isLocalDevTarget(): boolean;
|
|
22
|
+
/** True when requests will intentionally ignore stored credentials. */
|
|
23
|
+
isUsingLocalDevBypass(): boolean;
|
|
24
|
+
private shouldBypassAuthForLocalDev;
|
|
25
|
+
private sendOnce;
|
|
20
26
|
private request;
|
|
21
27
|
get<T>(path: string): Promise<T>;
|
|
22
28
|
post<T>(path: string, body?: unknown): Promise<T>;
|
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":"AAyBA,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;IAIxB,OAAO,CAAC,aAAa,CAAgB;gBAEzB,IAAI,CAAC,EAAE,gBAAgB;IAKnC,wEAAwE;YAC1D,mBAAmB;IAMjC,OAAO,CAAC,YAAY;IA0BpB,OAAO,CAAC,cAAc;IAStB,2EAA2E;IAC3E,gBAAgB,IAAI,OAAO;IAI3B,uEAAuE;IACvE,qBAAqB,IAAI,OAAO;IAQhC,OAAO,CAAC,2BAA2B;YAIrB,QAAQ;YAqBR,OAAO;IAqEf,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,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { ApiError, AuthError, UpgradeRequiredError } from "../util/errors.js";
|
|
4
|
-
import {
|
|
4
|
+
import { forceRefreshToken, hasRefreshableCredentials, resolveValidToken, } from "../auth/store.js";
|
|
5
5
|
import { PRODUCTION_API_URL } from "../config/defaults.js";
|
|
6
6
|
import { getCliVersion } from "../util/package.js";
|
|
7
7
|
import { log } from "../util/logger.js";
|
|
@@ -40,12 +40,23 @@ function resolveBaseUrl(explicit) {
|
|
|
40
40
|
}
|
|
41
41
|
export class ZenormClient {
|
|
42
42
|
baseUrl;
|
|
43
|
-
token
|
|
43
|
+
// An explicit token override (e.g. from `login` before creds are persisted).
|
|
44
|
+
// When set it is used verbatim and never refreshed. When null, the token is
|
|
45
|
+
// resolved per-request from stored credentials so expiry/refresh is handled.
|
|
46
|
+
explicitToken;
|
|
44
47
|
constructor(opts) {
|
|
45
48
|
this.baseUrl = resolveBaseUrl(opts?.baseUrl).replace(/\/+$/, "");
|
|
46
|
-
this.
|
|
49
|
+
this.explicitToken = opts?.token ?? null;
|
|
47
50
|
}
|
|
48
|
-
|
|
51
|
+
/** Resolve the bearer token for a request, refreshing it if expired. */
|
|
52
|
+
async resolveRequestToken() {
|
|
53
|
+
if (this.explicitToken)
|
|
54
|
+
return this.explicitToken;
|
|
55
|
+
if (this.shouldBypassAuthForLocalDev())
|
|
56
|
+
return null;
|
|
57
|
+
return resolveValidToken();
|
|
58
|
+
}
|
|
59
|
+
buildHeaders(token) {
|
|
49
60
|
const headers = {
|
|
50
61
|
"Content-Type": "application/json",
|
|
51
62
|
// Always advertise the CLI version so the API can gate stale clients
|
|
@@ -54,8 +65,8 @@ export class ZenormClient {
|
|
|
54
65
|
// authenticated or dev-bypassed.
|
|
55
66
|
"x-zenorm-cli-version": getCliVersion(),
|
|
56
67
|
};
|
|
57
|
-
if (
|
|
58
|
-
headers["Authorization"] = `Bearer ${
|
|
68
|
+
if (token) {
|
|
69
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
59
70
|
return headers;
|
|
60
71
|
}
|
|
61
72
|
if (this.isLocalBaseUrl()) {
|
|
@@ -82,23 +93,47 @@ export class ZenormClient {
|
|
|
82
93
|
isLocalDevTarget() {
|
|
83
94
|
return this.isLocalBaseUrl();
|
|
84
95
|
}
|
|
85
|
-
|
|
86
|
-
|
|
96
|
+
/** True when requests will intentionally ignore stored credentials. */
|
|
97
|
+
isUsingLocalDevBypass() {
|
|
98
|
+
return (this.isLocalBaseUrl() &&
|
|
99
|
+
process.env["ZENORM_LOCAL_DEV_BYPASS_AUTH"] === "true" &&
|
|
100
|
+
!process.env["ZENORM_API_TOKEN"]);
|
|
101
|
+
}
|
|
102
|
+
shouldBypassAuthForLocalDev() {
|
|
103
|
+
return this.isUsingLocalDevBypass();
|
|
104
|
+
}
|
|
105
|
+
async sendOnce(method, url, token, body) {
|
|
87
106
|
const init = {
|
|
88
107
|
method,
|
|
89
|
-
headers: this.
|
|
108
|
+
headers: this.buildHeaders(token),
|
|
90
109
|
};
|
|
91
110
|
if (body !== undefined) {
|
|
92
111
|
init.body = JSON.stringify(body);
|
|
93
112
|
}
|
|
94
|
-
let res;
|
|
95
113
|
try {
|
|
96
|
-
|
|
114
|
+
return await fetch(url, init);
|
|
97
115
|
}
|
|
98
116
|
catch (err) {
|
|
99
117
|
const msg = err instanceof Error ? err.message : String(err);
|
|
100
118
|
throw new ApiError(`Request failed: ${msg}`, 0, null);
|
|
101
119
|
}
|
|
120
|
+
}
|
|
121
|
+
async request(method, path, body) {
|
|
122
|
+
const url = `${this.baseUrl}${path}`;
|
|
123
|
+
let token = await this.resolveRequestToken();
|
|
124
|
+
let res = await this.sendOnce(method, url, token, body);
|
|
125
|
+
// A 401 on a token we resolved from stored credentials may mean the server
|
|
126
|
+
// revoked it before its stated expiry. Try one forced refresh + retry
|
|
127
|
+
// before giving up — but never for an explicit/env token we can't refresh.
|
|
128
|
+
if (res.status === 401 && !this.explicitToken && hasRefreshableCredentials()) {
|
|
129
|
+
try {
|
|
130
|
+
token = await forceRefreshToken();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
throw new AuthError("Authentication required. Run `zenorm login` first.");
|
|
134
|
+
}
|
|
135
|
+
res = await this.sendOnce(method, url, token, body);
|
|
136
|
+
}
|
|
102
137
|
if (res.status === 401) {
|
|
103
138
|
throw new AuthError("Authentication required. Run `zenorm login` first.");
|
|
104
139
|
}
|
package/dist/api/types.d.ts
CHANGED
|
@@ -51,4 +51,12 @@ export interface ListTasksResponse {
|
|
|
51
51
|
export interface TaskResponse {
|
|
52
52
|
task: SpecTaskRecord;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Response from `POST /v1/tasks/claim`. The server atomically claims the oldest
|
|
56
|
+
* ready task for the given repo and flips it to `active`; `task` is `null` when
|
|
57
|
+
* there is no claimable work right now.
|
|
58
|
+
*/
|
|
59
|
+
export interface ClaimTaskResponse {
|
|
60
|
+
task: SpecTaskRecord | null;
|
|
61
|
+
}
|
|
54
62
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/api/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAID,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAID,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,cAAc,CAAC;CACtB"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/api/types.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAID,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAID,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,cAAc,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;CAC7B"}
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import type { StoredCredentials } from "./store.js";
|
|
2
|
+
/**
|
|
3
|
+
* Exchange a stored refresh token for a fresh access token at the Clerk token
|
|
4
|
+
* endpoint (`grant_type=refresh_token`). Clerk may rotate the refresh token, so
|
|
5
|
+
* the caller must persist whichever refresh token comes back; if the response
|
|
6
|
+
* omits one, the existing refresh token is still valid and is returned as-is.
|
|
7
|
+
*
|
|
8
|
+
* Throws on any non-2xx response — an expired/revoked refresh token means the
|
|
9
|
+
* caller must fall back to a full `zenorm login`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function refreshAccessToken(refreshToken: string): Promise<{
|
|
12
|
+
accessToken: string;
|
|
13
|
+
refreshToken: string;
|
|
14
|
+
expiresIn: number;
|
|
15
|
+
}>;
|
|
2
16
|
/**
|
|
3
17
|
* Run the full OAuth 2.0 Authorization Code + PKCE flow.
|
|
4
18
|
*
|
|
@@ -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;AAkUpD;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAkDpE"}
|
|
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;AAkUpD;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC;IACtE,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC,CAuBD;AAED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAkDpE"}
|
package/dist/auth/device-flow.js
CHANGED
|
@@ -280,6 +280,37 @@ async function exchangeCodeForTokens(code, codeVerifier, redirectUri) {
|
|
|
280
280
|
expiresIn: data.expires_in ?? 3600,
|
|
281
281
|
};
|
|
282
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Exchange a stored refresh token for a fresh access token at the Clerk token
|
|
285
|
+
* endpoint (`grant_type=refresh_token`). Clerk may rotate the refresh token, so
|
|
286
|
+
* the caller must persist whichever refresh token comes back; if the response
|
|
287
|
+
* omits one, the existing refresh token is still valid and is returned as-is.
|
|
288
|
+
*
|
|
289
|
+
* Throws on any non-2xx response — an expired/revoked refresh token means the
|
|
290
|
+
* caller must fall back to a full `zenorm login`.
|
|
291
|
+
*/
|
|
292
|
+
export async function refreshAccessToken(refreshToken) {
|
|
293
|
+
const res = await fetch(`${getClerkIssuer()}/oauth/token`, {
|
|
294
|
+
method: "POST",
|
|
295
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
296
|
+
body: new URLSearchParams({
|
|
297
|
+
grant_type: "refresh_token",
|
|
298
|
+
refresh_token: refreshToken,
|
|
299
|
+
client_id: getClerkClientId(),
|
|
300
|
+
}),
|
|
301
|
+
});
|
|
302
|
+
if (!res.ok) {
|
|
303
|
+
const text = await res.text().catch(() => "");
|
|
304
|
+
throw new Error(`Token refresh failed (${res.status}): ${text}`);
|
|
305
|
+
}
|
|
306
|
+
const data = (await res.json());
|
|
307
|
+
return {
|
|
308
|
+
accessToken: data.access_token,
|
|
309
|
+
// Clerk rotates refresh tokens; reuse the old one only if none is returned.
|
|
310
|
+
refreshToken: data.refresh_token ?? refreshToken,
|
|
311
|
+
expiresIn: data.expires_in ?? 3600,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
283
314
|
/**
|
|
284
315
|
* Run the full OAuth 2.0 Authorization Code + PKCE flow.
|
|
285
316
|
*
|
package/dist/auth/store.d.ts
CHANGED
|
@@ -9,8 +9,38 @@ export declare function loadCredentials(): StoredCredentials | null;
|
|
|
9
9
|
export declare function saveCredentials(creds: StoredCredentials): void;
|
|
10
10
|
export declare function deleteCredentials(): boolean;
|
|
11
11
|
/**
|
|
12
|
-
* Resolve the current access token.
|
|
12
|
+
* Resolve the current access token without any network/refresh side effects.
|
|
13
13
|
* Priority: ZENORM_API_TOKEN env var > stored credentials.
|
|
14
|
+
*
|
|
15
|
+
* Prefer `resolveValidToken()` for actual API calls — this raw accessor does
|
|
16
|
+
* NOT check expiry and is kept for callers that only need the literal token
|
|
17
|
+
* (e.g. displaying it).
|
|
14
18
|
*/
|
|
15
19
|
export declare function resolveToken(): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a usable access token, refreshing it first when the stored one is
|
|
22
|
+
* expired (or about to be). Rotated credentials are persisted.
|
|
23
|
+
*
|
|
24
|
+
* Priority: ZENORM_API_TOKEN env var (used as-is, never refreshed) > stored
|
|
25
|
+
* credentials (refreshed on demand). Returns null when there is nothing to
|
|
26
|
+
* resolve, leaving the caller to fall back to dev-bypass headers or error.
|
|
27
|
+
*
|
|
28
|
+
* A failed refresh (revoked/expired refresh token) throws; the caller surfaces
|
|
29
|
+
* it as "run `zenorm login`".
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveValidToken(): Promise<string | null>;
|
|
32
|
+
/**
|
|
33
|
+
* True when a refresh-token grant is possible: stored credentials with a
|
|
34
|
+
* refresh token exist and no explicit ZENORM_API_TOKEN override is in play
|
|
35
|
+
* (an env token is opaque to us and cannot be refreshed).
|
|
36
|
+
*/
|
|
37
|
+
export declare function hasRefreshableCredentials(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Force a refresh regardless of expiry — used after a 401, where the server
|
|
40
|
+
* rejected a token that may have been revoked before its stated expiry.
|
|
41
|
+
* Persists rotated credentials and returns the new access token.
|
|
42
|
+
*
|
|
43
|
+
* Throws if there is nothing to refresh or the refresh grant fails.
|
|
44
|
+
*/
|
|
45
|
+
export declare function forceRefreshToken(): Promise<string>;
|
|
16
46
|
//# sourceMappingURL=store.d.ts.map
|
package/dist/auth/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/auth/store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/auth/store.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAaD,wBAAgB,eAAe,IAAI,iBAAiB,GAAG,IAAI,CAO1D;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAM9D;AAED,wBAAgB,iBAAiB,IAAI,OAAO,CAO3C;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAU5C;AAWD;;;;;;;;;;GAUG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAwBhE;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,OAAO,CAInD;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,CAezD"}
|
package/dist/auth/store.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { refreshAccessToken } from "./device-flow.js";
|
|
5
|
+
import { log } from "../util/logger.js";
|
|
6
|
+
// Refresh the access token this many ms before it actually expires, so an
|
|
7
|
+
// in-flight request never races the expiry boundary.
|
|
8
|
+
const EXPIRY_SKEW_MS = 60_000;
|
|
4
9
|
function getCredentialsPath() {
|
|
5
10
|
return join(homedir(), ".zenorm", "credentials.json");
|
|
6
11
|
}
|
|
@@ -34,8 +39,12 @@ export function deleteCredentials() {
|
|
|
34
39
|
return true;
|
|
35
40
|
}
|
|
36
41
|
/**
|
|
37
|
-
* Resolve the current access token.
|
|
42
|
+
* Resolve the current access token without any network/refresh side effects.
|
|
38
43
|
* Priority: ZENORM_API_TOKEN env var > stored credentials.
|
|
44
|
+
*
|
|
45
|
+
* Prefer `resolveValidToken()` for actual API calls — this raw accessor does
|
|
46
|
+
* NOT check expiry and is kept for callers that only need the literal token
|
|
47
|
+
* (e.g. displaying it).
|
|
39
48
|
*/
|
|
40
49
|
export function resolveToken() {
|
|
41
50
|
const envToken = process.env["ZENORM_API_TOKEN"];
|
|
@@ -48,3 +57,81 @@ export function resolveToken() {
|
|
|
48
57
|
}
|
|
49
58
|
return null;
|
|
50
59
|
}
|
|
60
|
+
/** True when the stored access token is expired (within the skew window). */
|
|
61
|
+
function isExpired(creds) {
|
|
62
|
+
const expiresAtMs = Date.parse(creds.expiresAt);
|
|
63
|
+
// An unparseable expiry is treated as expired so we refresh rather than send
|
|
64
|
+
// a token we can't reason about.
|
|
65
|
+
if (Number.isNaN(expiresAtMs))
|
|
66
|
+
return true;
|
|
67
|
+
return expiresAtMs - EXPIRY_SKEW_MS <= Date.now();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Resolve a usable access token, refreshing it first when the stored one is
|
|
71
|
+
* expired (or about to be). Rotated credentials are persisted.
|
|
72
|
+
*
|
|
73
|
+
* Priority: ZENORM_API_TOKEN env var (used as-is, never refreshed) > stored
|
|
74
|
+
* credentials (refreshed on demand). Returns null when there is nothing to
|
|
75
|
+
* resolve, leaving the caller to fall back to dev-bypass headers or error.
|
|
76
|
+
*
|
|
77
|
+
* A failed refresh (revoked/expired refresh token) throws; the caller surfaces
|
|
78
|
+
* it as "run `zenorm login`".
|
|
79
|
+
*/
|
|
80
|
+
export async function resolveValidToken() {
|
|
81
|
+
const envToken = process.env["ZENORM_API_TOKEN"];
|
|
82
|
+
if (envToken) {
|
|
83
|
+
return envToken;
|
|
84
|
+
}
|
|
85
|
+
const creds = loadCredentials();
|
|
86
|
+
if (!creds) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (!creds.refreshToken || !isExpired(creds)) {
|
|
90
|
+
return creds.accessToken;
|
|
91
|
+
}
|
|
92
|
+
log.debug("Access token expired; refreshing via refresh_token grant");
|
|
93
|
+
const refreshed = await refreshAccessToken(creds.refreshToken);
|
|
94
|
+
const updated = {
|
|
95
|
+
...creds,
|
|
96
|
+
accessToken: refreshed.accessToken,
|
|
97
|
+
refreshToken: refreshed.refreshToken,
|
|
98
|
+
expiresAt: new Date(Date.now() + refreshed.expiresIn * 1000).toISOString(),
|
|
99
|
+
};
|
|
100
|
+
saveCredentials(updated);
|
|
101
|
+
log.debug("Access token refreshed", { expiresAt: updated.expiresAt });
|
|
102
|
+
return refreshed.accessToken;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* True when a refresh-token grant is possible: stored credentials with a
|
|
106
|
+
* refresh token exist and no explicit ZENORM_API_TOKEN override is in play
|
|
107
|
+
* (an env token is opaque to us and cannot be refreshed).
|
|
108
|
+
*/
|
|
109
|
+
export function hasRefreshableCredentials() {
|
|
110
|
+
if (process.env["ZENORM_API_TOKEN"])
|
|
111
|
+
return false;
|
|
112
|
+
const creds = loadCredentials();
|
|
113
|
+
return Boolean(creds?.refreshToken);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Force a refresh regardless of expiry — used after a 401, where the server
|
|
117
|
+
* rejected a token that may have been revoked before its stated expiry.
|
|
118
|
+
* Persists rotated credentials and returns the new access token.
|
|
119
|
+
*
|
|
120
|
+
* Throws if there is nothing to refresh or the refresh grant fails.
|
|
121
|
+
*/
|
|
122
|
+
export async function forceRefreshToken() {
|
|
123
|
+
const creds = loadCredentials();
|
|
124
|
+
if (!creds?.refreshToken) {
|
|
125
|
+
throw new Error("No refresh token available");
|
|
126
|
+
}
|
|
127
|
+
log.debug("Forcing token refresh after 401");
|
|
128
|
+
const refreshed = await refreshAccessToken(creds.refreshToken);
|
|
129
|
+
const updated = {
|
|
130
|
+
...creds,
|
|
131
|
+
accessToken: refreshed.accessToken,
|
|
132
|
+
refreshToken: refreshed.refreshToken,
|
|
133
|
+
expiresAt: new Date(Date.now() + refreshed.expiresIn * 1000).toISOString(),
|
|
134
|
+
};
|
|
135
|
+
saveCredentials(updated);
|
|
136
|
+
return refreshed.accessToken;
|
|
137
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"whoami.d.ts","sourceRoot":"","sources":["../../src/commands/whoami.ts"],"names":[],"mappings":"AAIA,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"whoami.d.ts","sourceRoot":"","sources":["../../src/commands/whoami.ts"],"names":[],"mappings":"AAIA,wBAAsB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBlE"}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -2,15 +2,20 @@ import { loadCredentials, resolveToken } from "../auth/store.js";
|
|
|
2
2
|
import { ZenormClient } from "../api/client.js";
|
|
3
3
|
import { log } from "../util/logger.js";
|
|
4
4
|
export async function whoamiCommand(_argv) {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// Let the client resolve (and refresh if needed) the stored token itself —
|
|
6
|
+
// passing an explicit token here would bypass expiry handling.
|
|
7
|
+
const client = new ZenormClient();
|
|
7
8
|
const ctx = await client.getAuthContext();
|
|
9
|
+
const token = resolveToken();
|
|
8
10
|
log.info(`User: ${ctx.userId}`);
|
|
9
11
|
log.info(`Org: ${ctx.orgId}`);
|
|
10
12
|
log.info(`Admin: ${ctx.isAdmin ? "yes" : "no"}`);
|
|
11
13
|
// Show token expiry from stored credentials (not available for env token)
|
|
12
14
|
const creds = loadCredentials();
|
|
13
|
-
if (
|
|
15
|
+
if (client.isUsingLocalDevBypass()) {
|
|
16
|
+
log.dim("Using local development mode.");
|
|
17
|
+
}
|
|
18
|
+
else if (creds?.expiresAt) {
|
|
14
19
|
log.info(`Expires: ${creds.expiresAt}`);
|
|
15
20
|
}
|
|
16
21
|
else if (process.env["ZENORM_API_TOKEN"]) {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-output renderers for `zenorm work`.
|
|
3
|
+
*
|
|
4
|
+
* Background: `zenorm work` runs each claimed task by spawning a headless coding
|
|
5
|
+
* agent (`claude` / `codex`). Those agents' *interactive* TUIs show a clean,
|
|
6
|
+
* readable transcript (assistant prose + one-line tool calls). Their headless
|
|
7
|
+
* modes do NOT: `claude -p` text mode hides every tool call, and the JSON event
|
|
8
|
+
* streams (`--output-format stream-json`, `codex exec --json`) are raw JSONL —
|
|
9
|
+
* unreadable noise.
|
|
10
|
+
*
|
|
11
|
+
* These renderers consume the JSON event streams and re-emit a compact transcript
|
|
12
|
+
* that mirrors what you'd see in each agent's interactive session — a read-only
|
|
13
|
+
* view of the agent working — while dropping protocol/metadata noise (hooks,
|
|
14
|
+
* init banners, token counts, rate-limit pings, raw tool-result bodies).
|
|
15
|
+
*
|
|
16
|
+
* Pure functions over single events so they can be unit-tested without spawning
|
|
17
|
+
* anything. The caller (`AgentRunner`) feeds newline-delimited stdout chunks to a
|
|
18
|
+
* `LineSplitter` + `renderClaudeEvent` / `renderCodexEvent`.
|
|
19
|
+
*/
|
|
20
|
+
/** A single rendered transcript line (no trailing newline). */
|
|
21
|
+
export type RenderedLine = string;
|
|
22
|
+
/**
|
|
23
|
+
* Accumulates raw stdout bytes and yields complete lines. The agent JSON streams
|
|
24
|
+
* are newline-delimited JSON; a chunk may split a line, so we buffer the tail.
|
|
25
|
+
*/
|
|
26
|
+
export declare class LineSplitter {
|
|
27
|
+
private buffer;
|
|
28
|
+
/** Push a chunk; returns any newly-completed lines (without the newline). */
|
|
29
|
+
push(chunk: string): string[];
|
|
30
|
+
/** Flush any buffered partial line at end-of-stream. */
|
|
31
|
+
flush(): string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Render ONE parsed Claude `--output-format stream-json` event into transcript
|
|
35
|
+
* lines (possibly empty). We render off the *complete* `assistant` events (not
|
|
36
|
+
* the partial `stream_event` deltas) so each block is whole; everything else —
|
|
37
|
+
* `system`/`stream_event`/`rate_limit_event`/`user` tool-result bodies — is
|
|
38
|
+
* dropped as noise. The final `result` event is summarized as a dim footer.
|
|
39
|
+
*/
|
|
40
|
+
export declare function renderClaudeEvent(event: unknown): RenderedLine[];
|
|
41
|
+
/**
|
|
42
|
+
* Render ONE parsed Codex `exec --json` event (JSONL) into transcript lines.
|
|
43
|
+
* Codex emits `thread.started` / `turn.started` / `item.*` / `turn.completed` /
|
|
44
|
+
* `error`. We surface assistant messages, command/file-change items, and a dim
|
|
45
|
+
* completion/error footer; thread/turn lifecycle + token metadata are dropped.
|
|
46
|
+
*/
|
|
47
|
+
export declare function renderCodexEvent(event: unknown): RenderedLine[];
|
|
48
|
+
/** Per-agent line renderer: parse JSONL safely, then dispatch. */
|
|
49
|
+
export type EventRenderer = (jsonLine: string) => RenderedLine[];
|
|
50
|
+
export declare const claudeRenderer: EventRenderer;
|
|
51
|
+
export declare const codexRenderer: EventRenderer;
|
|
52
|
+
/**
|
|
53
|
+
* The dim footer that closes each agent's transcript. Done turns green with a
|
|
54
|
+
* check; an errored/aborted turn turns red with a cross. `meta` is the trailing
|
|
55
|
+
* detail (turns/duration, or an error message). Kept as a shared helper so the
|
|
56
|
+
* Claude `result` and Codex `turn.completed`/`error` footers render alike.
|
|
57
|
+
*
|
|
58
|
+
* NOTE: the literal words "done"/"error" are asserted by the renderer tests, so
|
|
59
|
+
* keep them as the leading token.
|
|
60
|
+
*/
|
|
61
|
+
export declare function transcriptFooter(isError: boolean, meta: string[]): RenderedLine;
|
|
62
|
+
/**
|
|
63
|
+
* Heavy task header printed once when a task starts. The leading rule + blank
|
|
64
|
+
* line give multi-task runs clear visual separation (tasks stop blurring into
|
|
65
|
+
* one another); `index` is the 1-based run counter within this session.
|
|
66
|
+
*/
|
|
67
|
+
export declare function taskHeader(shortTaskId: string, title: string, agentName: string, index?: number): string;
|
|
68
|
+
/**
|
|
69
|
+
* The full multi-line splash logo (no surrounding blank lines): the rasterized
|
|
70
|
+
* brand mark on the left (brand-cyan half-blocks), the `ZeNorm` wordmark on the
|
|
71
|
+
* right (3D-extruded, 5-space gutter). The icon stands one row taller than the
|
|
72
|
+
* wordmark (9 vs 8), so the shorter wordmark is vertically centered against it
|
|
73
|
+
* with blank padding. Under NO_COLOR the color escapes collapse to "" (the
|
|
74
|
+
* half-block mark and the wordmark's box outline still draw). Exported so it can
|
|
75
|
+
* be rendered/asserted on its own.
|
|
76
|
+
*/
|
|
77
|
+
export declare function splashLogo(): string;
|
|
78
|
+
/**
|
|
79
|
+
* The startup banner for the daemon. Shown once at launch (not just under
|
|
80
|
+
* `--dry-run`) so a real run announces what it's doing instead of sitting
|
|
81
|
+
* silently until the first task lands. Leads with the ZeNorm splash logo, then
|
|
82
|
+
* the run parameters.
|
|
83
|
+
*/
|
|
84
|
+
export declare function daemonBanner(opts: {
|
|
85
|
+
agent: string;
|
|
86
|
+
repo: string;
|
|
87
|
+
intervalSeconds: number;
|
|
88
|
+
once: boolean;
|
|
89
|
+
dryRun: boolean;
|
|
90
|
+
}): string;
|
|
91
|
+
/** A spinner-free, dim status line for the daemon's idle/lifecycle states. */
|
|
92
|
+
export declare function statusLine(message: string): string;
|
|
93
|
+
/**
|
|
94
|
+
* The per-task outcome line printed after a task's transcript closes: a bold
|
|
95
|
+
* green check for success or a bold red cross for failure, with the title, id,
|
|
96
|
+
* and wall-clock duration. This is the daemon's own verdict (distinct from the
|
|
97
|
+
* agent's transcript footer) so the run log reads as a sequence of outcomes.
|
|
98
|
+
*/
|
|
99
|
+
export declare function taskOutcome(opts: {
|
|
100
|
+
ok: boolean;
|
|
101
|
+
shortTaskId: string;
|
|
102
|
+
title: string;
|
|
103
|
+
durationMs: number;
|
|
104
|
+
}): string;
|
|
105
|
+
/**
|
|
106
|
+
* A loud, framed failure banner for a task that threw. Spells out which task
|
|
107
|
+
* failed, the error, and that it was released back to `todo` so the failure is
|
|
108
|
+
* impossible to miss in a scrolling daemon log (vs. a single dim line).
|
|
109
|
+
*/
|
|
110
|
+
export declare function failureBanner(opts: {
|
|
111
|
+
shortTaskId: string;
|
|
112
|
+
title: string;
|
|
113
|
+
error: string;
|
|
114
|
+
}): string;
|
|
115
|
+
/**
|
|
116
|
+
* The session summary printed when the daemon shuts down (Ctrl-C or `--once`
|
|
117
|
+
* exhausted). Closes the run with a rule + a one-line tally so the user sees the
|
|
118
|
+
* total instead of having to count outcome lines by eye.
|
|
119
|
+
*/
|
|
120
|
+
export declare function sessionSummary(opts: {
|
|
121
|
+
completed: number;
|
|
122
|
+
failed: number;
|
|
123
|
+
}): string;
|
|
124
|
+
//# sourceMappingURL=work-render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"work-render.d.ts","sourceRoot":"","sources":["../../src/commands/work-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAoBH,+DAA+D;AAC/D,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AA2DlC;;;GAGG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAAM;IAEpB,6EAA6E;IAC7E,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE;IAQ7B,wDAAwD;IACxD,KAAK,IAAI,MAAM,EAAE;CAMlB;AA+ED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,EAAE,CAkChE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,EAAE,CAoE/D;AAED,kEAAkE;AAClE,MAAM,MAAM,aAAa,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,YAAY,EAAE,CAAC;AAuBjE,eAAO,MAAM,cAAc,EAAE,aAA+C,CAAC;AAC7E,eAAO,MAAM,aAAa,EAAE,aAA8C,CAAC;AAE3E;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,YAAY,CAI/E;AAMD;;;;GAIG;AACH,wBAAgB,UAAU,CACxB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,KAAK,CAAC,EAAE,MAAM,GACb,MAAM,CAMR;AA2JD;;;;;;;;GAQG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAenC;AAoBD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;CACjB,GAAG,MAAM,CAsBT;AAED,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAChC,EAAE,EAAE,OAAO,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,MAAM,CAKT;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAMT;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,MAAM,CAKT"}
|