@nuucognition/cli-core 0.0.1-beta.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/index.js ADDED
@@ -0,0 +1,1008 @@
1
+ // src/types.ts
2
+ var NUU_ACCOUNT_URL_PROD = "https://account.nuucognition.com";
3
+ var NUU_ACCOUNT_URL_DEV = "http://localhost:3000";
4
+ var TIER_ALIASES = {
5
+ production: "prod",
6
+ prod: "prod",
7
+ experimental: "exp",
8
+ exp: "exp",
9
+ development: "dev",
10
+ dev: "dev"
11
+ };
12
+ function normalizeTier(input) {
13
+ return TIER_ALIASES[input.toLowerCase()];
14
+ }
15
+ function isValidTier(input) {
16
+ return normalizeTier(input) !== void 0;
17
+ }
18
+ function normalizeRuntimeMode(input) {
19
+ const normalized = input.toLowerCase();
20
+ if (normalized === "prod" || normalized === "production") {
21
+ return "prod";
22
+ }
23
+ if (normalized === "dev" || normalized === "development") {
24
+ return "dev";
25
+ }
26
+ return void 0;
27
+ }
28
+
29
+ // src/env.ts
30
+ function normalizeSegment(value) {
31
+ const normalized = value.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase();
32
+ return normalized || value.toUpperCase();
33
+ }
34
+ function scopedEnvPrefix(cliname) {
35
+ return `NUU_${normalizeSegment(cliname)}`;
36
+ }
37
+ function scopedEnvVarName(cliname, field) {
38
+ return `${scopedEnvPrefix(cliname)}_${normalizeSegment(field)}`;
39
+ }
40
+ function scopedEnvVar(cliname, field) {
41
+ return process.env[scopedEnvVarName(cliname, field)];
42
+ }
43
+ function scopedEnvVarTrue(cliname, field) {
44
+ return parseBoolean(scopedEnvVar(cliname, field)) === true;
45
+ }
46
+ function parseBoolean(value) {
47
+ if (value === void 0) return void 0;
48
+ const normalized = value.trim().toLowerCase();
49
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
50
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
51
+ return void 0;
52
+ }
53
+ function authEnvForMode(mode) {
54
+ return mode === "dev" ? "dev" : "prod";
55
+ }
56
+
57
+ // src/config/index.ts
58
+ import { mkdir as mkdir2 } from "fs/promises";
59
+ import { mkdirSync } from "fs";
60
+ import { homedir } from "os";
61
+ import { join } from "path";
62
+
63
+ // src/config/io.ts
64
+ import { mkdir, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
65
+ import { readFileSync as fsReadFileSync } from "fs";
66
+ import { dirname } from "path";
67
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
68
+ async function readConfigFile(path) {
69
+ try {
70
+ const content = await fsReadFile(path, "utf-8");
71
+ return parseToml(content);
72
+ } catch (error2) {
73
+ if (error2.code === "ENOENT") {
74
+ return null;
75
+ }
76
+ throw error2;
77
+ }
78
+ }
79
+ function readConfigFileSync(path) {
80
+ try {
81
+ const content = fsReadFileSync(path, "utf-8");
82
+ return parseToml(content);
83
+ } catch (error2) {
84
+ if (error2.code === "ENOENT") {
85
+ return null;
86
+ }
87
+ throw error2;
88
+ }
89
+ }
90
+ async function writeConfigFile(path, data) {
91
+ await mkdir(dirname(path), { recursive: true });
92
+ await fsWriteFile(path, `${stringifyToml(data)}
93
+ `, "utf-8");
94
+ }
95
+ async function setConfigField(path, key, value) {
96
+ const existing = await readConfigFile(path) ?? {};
97
+ existing[key] = value;
98
+ await writeConfigFile(path, existing);
99
+ }
100
+ async function removeConfigField(path, key) {
101
+ const existing = await readConfigFile(path);
102
+ if (!existing) return;
103
+ delete existing[key];
104
+ await writeConfigFile(path, existing);
105
+ }
106
+ async function setFeatureOverride(path, featureId, enabled) {
107
+ const existing = await readConfigFile(path) ?? {};
108
+ const features = existing.features && typeof existing.features === "object" && !Array.isArray(existing.features) ? { ...existing.features } : {};
109
+ features[featureId] = enabled;
110
+ existing.features = features;
111
+ await writeConfigFile(path, existing);
112
+ }
113
+ async function removeFeatureOverride(path, featureId) {
114
+ const existing = await readConfigFile(path);
115
+ if (!existing) return;
116
+ const features = existing.features;
117
+ if (!features || typeof features !== "object" || Array.isArray(features)) {
118
+ return;
119
+ }
120
+ const next = { ...features };
121
+ delete next[featureId];
122
+ if (Object.keys(next).length === 0) {
123
+ delete existing.features;
124
+ } else {
125
+ existing.features = next;
126
+ }
127
+ await writeConfigFile(path, existing);
128
+ }
129
+
130
+ // src/config/index.ts
131
+ var CONFIG_FILENAME = "config.toml";
132
+ function getConfigDir(cliname) {
133
+ return join(homedir(), ".nuucognition", `.${cliname}`);
134
+ }
135
+ function getConfigPath(cliname) {
136
+ return join(getConfigDir(cliname), CONFIG_FILENAME);
137
+ }
138
+ async function ensureConfigDir(cliname) {
139
+ const dir = getConfigDir(cliname);
140
+ await mkdir2(dir, { recursive: true });
141
+ return dir;
142
+ }
143
+ function ensureConfigDirSync(cliname) {
144
+ const dir = getConfigDir(cliname);
145
+ mkdirSync(dir, { recursive: true });
146
+ return dir;
147
+ }
148
+ function mergeDefaults(config, defaults) {
149
+ if (!defaults) return config;
150
+ return {
151
+ ...defaults,
152
+ ...config
153
+ };
154
+ }
155
+ async function resolveConfig(options) {
156
+ const fromFile = await readConfigFile(getConfigPath(options.cliname)) ?? {};
157
+ return mergeDefaults(fromFile, options.defaults);
158
+ }
159
+ function resolveConfigSync(options) {
160
+ const fromFile = readConfigFileSync(getConfigPath(options.cliname)) ?? {};
161
+ return mergeDefaults(fromFile, options.defaults);
162
+ }
163
+
164
+ // src/features/registry.ts
165
+ function createFeatureRegistry(features) {
166
+ const seen = /* @__PURE__ */ new Set();
167
+ const duplicates = /* @__PURE__ */ new Set();
168
+ const immutable = features.map((feature) => {
169
+ if (seen.has(feature.id)) {
170
+ duplicates.add(feature.id);
171
+ }
172
+ seen.add(feature.id);
173
+ return Object.freeze({ ...feature });
174
+ });
175
+ if (duplicates.size > 0) {
176
+ throw new Error(`Duplicate feature IDs in registry: ${Array.from(duplicates).join(", ")}`);
177
+ }
178
+ return Object.freeze({
179
+ features: Object.freeze(immutable)
180
+ });
181
+ }
182
+ function getFeature(registry, id) {
183
+ return registry.features.find((feature) => feature.id === id);
184
+ }
185
+ function getFeaturesByTier(registry, tier) {
186
+ return registry.features.filter((feature) => feature.tier === tier);
187
+ }
188
+ function getFeaturesByType(registry, type) {
189
+ return registry.features.filter((feature) => feature.type === type);
190
+ }
191
+ function getCommandFeatures(registry) {
192
+ return getFeaturesByType(registry, "command");
193
+ }
194
+ function hasFeature(registry, id) {
195
+ return registry.features.some((feature) => feature.id === id);
196
+ }
197
+
198
+ // src/auth/index.ts
199
+ import { createHash, randomBytes } from "crypto";
200
+ import { createServer } from "http";
201
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync } from "fs";
202
+ import { mkdir as mkdir3, readFile, unlink, writeFile } from "fs/promises";
203
+ import { homedir as homedir2 } from "os";
204
+ import { join as join2 } from "path";
205
+ import pc from "picocolors";
206
+ import open from "open";
207
+ var DEFAULT_CALLBACK_PORT = 3847;
208
+ var REFRESH_WINDOW_MS = 2 * 60 * 1e3;
209
+ var CLI_STATE_PARAM = "cli_state";
210
+ var CLI_CODE_CHALLENGE_PARAM = "cli_code_challenge";
211
+ var CLI_CODE_CHALLENGE_METHOD_PARAM = "cli_code_challenge_method";
212
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
213
+ var _verbose = false;
214
+ function setAuthVerbose(enabled) {
215
+ _verbose = enabled;
216
+ }
217
+ function step(message) {
218
+ console.log(`${pc.blue("\u203A")} ${message}`);
219
+ }
220
+ function detail(message) {
221
+ if (_verbose) console.log(` ${pc.dim(message)}`);
222
+ }
223
+ function stepFail(message) {
224
+ console.error(`${pc.red("\u2717")} ${message}`);
225
+ }
226
+ function hint(message) {
227
+ console.error(` ${pc.dim(message)}`);
228
+ }
229
+ function getAuthFilename(env) {
230
+ return env === "dev" ? "auth.dev.json" : "auth.json";
231
+ }
232
+ function authPath(env) {
233
+ return join2(getNuuDir(), getAuthFilename(env));
234
+ }
235
+ function getNuuDir() {
236
+ return join2(homedir2(), ".nuucognition");
237
+ }
238
+ async function ensureNuuDir() {
239
+ const dir = getNuuDir();
240
+ await mkdir3(dir, { recursive: true });
241
+ return dir;
242
+ }
243
+ function ensureNuuDirSync() {
244
+ const dir = getNuuDir();
245
+ mkdirSync2(dir, { recursive: true });
246
+ return dir;
247
+ }
248
+ function expiresAtMs(expiresAt) {
249
+ if (!expiresAt) return 0;
250
+ const parsed = Date.parse(expiresAt);
251
+ return Number.isFinite(parsed) ? parsed : 0;
252
+ }
253
+ function isExpiredAt(expiresAt) {
254
+ return expiresAtMs(expiresAt) <= Date.now();
255
+ }
256
+ function decodeJwtExpiry(token) {
257
+ const parts = token.split(".");
258
+ if (parts.length < 2 || !parts[1]) return void 0;
259
+ try {
260
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
261
+ if (typeof payload.exp !== "number") return void 0;
262
+ return new Date(payload.exp * 1e3).toISOString();
263
+ } catch {
264
+ return void 0;
265
+ }
266
+ }
267
+ function normalizeCredentials(credentials) {
268
+ const jwtExpiry = decodeJwtExpiry(credentials.sessionToken);
269
+ if (!jwtExpiry || jwtExpiry === credentials.expiresAt) {
270
+ return credentials;
271
+ }
272
+ return {
273
+ ...credentials,
274
+ expiresAt: jwtExpiry
275
+ };
276
+ }
277
+ function hasUsableCredentials(credentials) {
278
+ if (!credentials) return false;
279
+ if (!isExpiredAt(credentials.expiresAt)) return true;
280
+ return !!credentials.refreshToken && !isExpiredAt(credentials.refreshExpiresAt);
281
+ }
282
+ async function exchangeCode(accountUrl, code, codeVerifier) {
283
+ const exchangeUrl = `${accountUrl.replace(/\/$/, "")}/api/cli/exchange`;
284
+ detail(`POST ${exchangeUrl}`);
285
+ let response;
286
+ try {
287
+ response = await fetch(exchangeUrl, {
288
+ method: "POST",
289
+ headers: { "Content-Type": "application/json" },
290
+ body: JSON.stringify({ code, codeVerifier })
291
+ });
292
+ } catch (cause) {
293
+ const msg = cause instanceof Error ? cause.message : String(cause);
294
+ throw new Error(
295
+ `Could not reach the account service at ${exchangeUrl}
296
+ ${pc.dim(msg)}
297
+ ${pc.dim("Is the account app running?")}`
298
+ );
299
+ }
300
+ let payload;
301
+ try {
302
+ payload = await response.json();
303
+ } catch {
304
+ throw new Error(
305
+ `Account service returned ${response.status} with a non-JSON body.
306
+ ${pc.dim(`URL: ${exchangeUrl}`)}`
307
+ );
308
+ }
309
+ detail(`Exchange response: ${response.status} ok=${String(payload.ok)}`);
310
+ if (!response.ok || !payload.ok) {
311
+ const serverError = typeof payload.error === "string" ? payload.error : `HTTP ${response.status}`;
312
+ throw new Error(
313
+ `Code exchange rejected: ${serverError}
314
+ ${pc.dim(`URL: ${exchangeUrl}`)}
315
+ ${pc.dim(`Status: ${response.status}`)}`
316
+ );
317
+ }
318
+ if (!payload.sessionToken || !payload.userId || !payload.email || !payload.expiresAt) {
319
+ throw new Error(
320
+ `Code exchange response is missing required fields.
321
+ ${pc.dim(`Got: ${Object.keys(payload).join(", ")}`)}`
322
+ );
323
+ }
324
+ return {
325
+ sessionToken: payload.sessionToken,
326
+ userId: payload.userId,
327
+ email: payload.email,
328
+ expiresAt: payload.expiresAt,
329
+ refreshToken: payload.refreshToken,
330
+ refreshExpiresAt: payload.refreshExpiresAt
331
+ };
332
+ }
333
+ function resultHtml(ok, title, message) {
334
+ const accent = ok ? "#22c55e" : "#ef4444";
335
+ return `<!DOCTYPE html>
336
+ <html>
337
+ <head>
338
+ <meta charset="utf-8" />
339
+ <title>${title}</title>
340
+ <style>
341
+ body { font-family: system-ui, -apple-system, sans-serif; display: grid; place-items: center; min-height: 100vh; margin: 0; background: #fafafa; color: #111; }
342
+ main { max-width: 28rem; text-align: center; padding: 2.5rem; }
343
+ .logo { margin-bottom: 1.5rem; }
344
+ .status { font-size: 0.875rem; font-weight: 500; color: ${accent}; margin-bottom: 0.5rem; }
345
+ h1 { font-size: 1.25rem; font-weight: 600; margin: 0 0 0.5rem; }
346
+ p { color: #666; margin: 0; line-height: 1.5; }
347
+ </style>
348
+ </head>
349
+ <body>
350
+ <main>
351
+ <div class="logo">
352
+ <svg width="40" height="41" viewBox="0 0 281 291" fill="none" xmlns="http://www.w3.org/2000/svg">
353
+ <path d="M137.498 291L71.1506 94.4027C66.7475 81.416 60.9434 68.6293 53.7382 56.0422C46.5331 43.4552 36.626 37.1617 24.0171 37.1617C18.0128 37.1617 11.4081 38.4603 4.203 41.0576L0 30.5685L74.7532 0L87.9626 0L166.919 230.462L215.854 130.665C224.86 112.684 229.363 98.5984 229.363 88.4088C229.363 80.2173 227.662 68.7291 224.26 53.9443C221.658 43.1555 220.357 35.0638 220.357 29.6694C220.357 9.88977 231.064 0 252.48 0C271.493 0 281 8.99072 281 26.9722C281 44.7539 267.791 78.7189 241.372 128.867L155.511 291L137.498 291Z" fill="#111" fill-rule="evenodd"/>
354
+ </svg>
355
+ </div>
356
+ <h1>${title}</h1>
357
+ <p>${message}</p>
358
+ </main>
359
+ </body>
360
+ </html>`;
361
+ }
362
+ async function loadAuth(env = "prod") {
363
+ try {
364
+ const content = await readFile(authPath(env), "utf-8");
365
+ return normalizeCredentials(JSON.parse(content));
366
+ } catch {
367
+ return null;
368
+ }
369
+ }
370
+ function loadAuthSync(env = "prod") {
371
+ try {
372
+ ensureNuuDirSync();
373
+ const path = authPath(env);
374
+ if (!existsSync(path)) return null;
375
+ return normalizeCredentials(JSON.parse(readFileSync(path, "utf-8")));
376
+ } catch {
377
+ return null;
378
+ }
379
+ }
380
+ async function saveAuth(creds, env = "prod") {
381
+ await ensureNuuDir();
382
+ await writeFile(authPath(env), `${JSON.stringify(normalizeCredentials(creds), null, 2)}
383
+ `, "utf-8");
384
+ }
385
+ async function clearAuth(env = "prod") {
386
+ try {
387
+ await unlink(authPath(env));
388
+ } catch {
389
+ }
390
+ }
391
+ function hasAuth(env = "prod") {
392
+ return hasUsableCredentials(loadAuthSync(env));
393
+ }
394
+ async function startLoginFlow(config) {
395
+ const callbackPort = config.callbackPort ?? DEFAULT_CALLBACK_PORT;
396
+ const authEnv = config.authEnv ?? "prod";
397
+ const state = randomBytes(24).toString("base64url");
398
+ const codeVerifier = randomBytes(32).toString("base64url");
399
+ const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
400
+ const cliLabel = config.cliname ?? "nuu";
401
+ step(`Logging in to ${pc.bold(config.accountUrl)}`);
402
+ return new Promise((resolve, reject) => {
403
+ let settled = false;
404
+ const finish = (fn) => {
405
+ if (settled) return;
406
+ settled = true;
407
+ clearTimeout(timeout);
408
+ server.close();
409
+ fn();
410
+ };
411
+ const server = createServer((req, res) => {
412
+ const url = new URL(req.url ?? "/", `http://localhost:${callbackPort}`);
413
+ if (url.pathname !== "/callback") {
414
+ res.writeHead(404, { "Content-Type": "text/plain" });
415
+ res.end("Not found");
416
+ return;
417
+ }
418
+ const params = Object.fromEntries(url.searchParams.entries());
419
+ detail(`Callback received with params: ${JSON.stringify(params)}`);
420
+ const errorParam = url.searchParams.get("error");
421
+ if (errorParam) {
422
+ const errorDesc = url.searchParams.get("error_description") ?? errorParam;
423
+ res.writeHead(400, { "Content-Type": "text/html" });
424
+ res.end(resultHtml(false, "Login failed", errorDesc));
425
+ finish(() => {
426
+ stepFail(`Account service returned an error: ${errorDesc}`);
427
+ reject(new Error(errorDesc));
428
+ });
429
+ return;
430
+ }
431
+ const code = url.searchParams.get("code");
432
+ const returnedState = url.searchParams.get(CLI_STATE_PARAM);
433
+ const legacyToken = url.searchParams.get("token");
434
+ const legacyUserId = url.searchParams.get("userId");
435
+ const legacyEmail = url.searchParams.get("email");
436
+ const legacyExpiresAt = url.searchParams.get("expiresAt");
437
+ if (!code && legacyToken && legacyUserId && legacyEmail && legacyExpiresAt) {
438
+ detail("Using legacy token flow (PKCE params were lost in redirect chain)");
439
+ const credentials = {
440
+ sessionToken: legacyToken,
441
+ userId: legacyUserId,
442
+ email: legacyEmail,
443
+ expiresAt: legacyExpiresAt,
444
+ refreshToken: url.searchParams.get("refreshToken") ?? void 0,
445
+ refreshExpiresAt: url.searchParams.get("refreshExpiresAt") ?? void 0
446
+ };
447
+ res.writeHead(200, { "Content-Type": "text/html" });
448
+ res.end(resultHtml(true, "Login successful", "You can close this tab and return to the terminal."));
449
+ void saveAuth(credentials, authEnv).then(() => finish(() => resolve(credentials))).catch((cause) => {
450
+ const message = cause instanceof Error ? cause.message : String(cause);
451
+ finish(() => {
452
+ stepFail(`Failed to save credentials: ${message}`);
453
+ reject(new Error(`Failed to save credentials: ${message}`));
454
+ });
455
+ });
456
+ return;
457
+ }
458
+ if (!code) {
459
+ res.writeHead(400, { "Content-Type": "text/html" });
460
+ res.end(resultHtml(false, "Login failed", "No authorization code received. Return to the terminal and try again."));
461
+ finish(() => {
462
+ stepFail("No authorization code in callback");
463
+ hint(`Received params: ${Object.keys(params).join(", ") || "(none)"}`);
464
+ hint("The account service did not return a code or token.");
465
+ hint(`Try running \`${cliLabel} login\` again.`);
466
+ reject(new Error("Login failed: no authorization code received"));
467
+ });
468
+ return;
469
+ }
470
+ if (returnedState !== state) {
471
+ res.writeHead(400, { "Content-Type": "text/html" });
472
+ res.end(resultHtml(false, "Login failed", "State mismatch \u2014 possible CSRF or stale session. Return to the terminal and try again."));
473
+ finish(() => {
474
+ stepFail("State mismatch in callback");
475
+ hint(`Expected cli_state but got: ${returnedState ? "(different value)" : "(missing)"}`);
476
+ hint("This can happen if the browser sent a stale or cached callback URL.");
477
+ hint(`Try running \`${cliLabel} login\` again.`);
478
+ reject(new Error("Login failed: state mismatch"));
479
+ });
480
+ return;
481
+ }
482
+ res.writeHead(200, { "Content-Type": "text/html" });
483
+ res.end(resultHtml(true, "Login successful", "You can close this tab and return to the terminal."));
484
+ step("Exchanging authorization code for session token...");
485
+ void exchangeCode(config.accountUrl, code, codeVerifier).then(async (credentials) => {
486
+ await saveAuth(credentials, authEnv);
487
+ finish(() => resolve(credentials));
488
+ }).catch((cause) => {
489
+ const message = cause instanceof Error ? cause.message : String(cause);
490
+ finish(() => {
491
+ stepFail("Code exchange failed");
492
+ console.error(` ${message}`);
493
+ reject(new Error(`Code exchange failed: ${message}`));
494
+ });
495
+ });
496
+ });
497
+ server.on("error", (err) => {
498
+ if (err.code === "EADDRINUSE") {
499
+ stepFail(`Port ${callbackPort} is already in use`);
500
+ hint("Another process (or a previous login attempt) is using this port.");
501
+ hint(`Kill it with: lsof -ti :${callbackPort} | xargs kill`);
502
+ finish(() => reject(new Error(`Port ${callbackPort} is already in use`)));
503
+ } else {
504
+ stepFail(`Callback server error: ${err.message}`);
505
+ finish(() => reject(new Error(`Callback server error: ${err.message}`)));
506
+ }
507
+ });
508
+ server.listen(callbackPort, "127.0.0.1", () => {
509
+ detail(`Callback server listening on http://127.0.0.1:${callbackPort}`);
510
+ const cliCallback = `http://localhost:${callbackPort}/callback`;
511
+ const authUrl = `${config.accountUrl.replace(/\/$/, "")}/cli/callback?cli_callback=${encodeURIComponent(cliCallback)}&${CLI_STATE_PARAM}=${encodeURIComponent(state)}&${CLI_CODE_CHALLENGE_PARAM}=${encodeURIComponent(codeChallenge)}&${CLI_CODE_CHALLENGE_METHOD_PARAM}=S256`;
512
+ step("Opening browser for authentication...");
513
+ detail(authUrl);
514
+ open(authUrl).catch((cause) => {
515
+ const message = cause instanceof Error ? cause.message : String(cause);
516
+ finish(() => {
517
+ stepFail("Could not open browser");
518
+ hint(message);
519
+ hint(`Open this URL manually:
520
+ ${authUrl}`);
521
+ reject(new Error(`Could not open browser: ${message}`));
522
+ });
523
+ });
524
+ });
525
+ const timeout = setTimeout(() => {
526
+ finish(() => {
527
+ stepFail("Login timed out after 5 minutes");
528
+ hint("No callback was received from the browser.");
529
+ hint(`Try running \`${cliLabel} login\` again.`);
530
+ reject(new Error("Login timed out"));
531
+ });
532
+ }, LOGIN_TIMEOUT_MS);
533
+ timeout.unref();
534
+ });
535
+ }
536
+ async function refreshAuth(creds, config) {
537
+ const cliname = config.cliname ?? "nuu";
538
+ if (!creds.refreshToken) {
539
+ throw new Error(
540
+ `No refresh token available.
541
+ ${pc.dim(`Run \`${cliname} login\` to start a new session.`)}`
542
+ );
543
+ }
544
+ const refreshUrl = `${config.accountUrl.replace(/\/$/, "")}/api/cli/refresh`;
545
+ detail(`POST ${refreshUrl}`);
546
+ let response;
547
+ try {
548
+ response = await fetch(refreshUrl, {
549
+ method: "POST",
550
+ headers: { "Content-Type": "application/json" },
551
+ body: JSON.stringify({ refreshToken: creds.refreshToken })
552
+ });
553
+ } catch (cause) {
554
+ const msg = cause instanceof Error ? cause.message : String(cause);
555
+ throw new Error(
556
+ `Could not reach the account service for token refresh.
557
+ ${pc.dim(msg)}
558
+ ${pc.dim(`URL: ${refreshUrl}`)}
559
+ ${pc.dim(`Run \`${cliname} login\` to re-authenticate.`)}`
560
+ );
561
+ }
562
+ let payload;
563
+ try {
564
+ payload = await response.json();
565
+ } catch {
566
+ throw new Error(
567
+ `Account service returned ${response.status} with a non-JSON body during refresh.
568
+ ${pc.dim(`URL: ${refreshUrl}`)}
569
+ ${pc.dim(`Run \`${cliname} login\` to re-authenticate.`)}`
570
+ );
571
+ }
572
+ detail(`Refresh response: ${response.status} ok=${String(payload.ok)}`);
573
+ if (!response.ok || !payload.ok) {
574
+ const serverError = typeof payload.error === "string" ? payload.error : `HTTP ${response.status}`;
575
+ throw new Error(
576
+ `Token refresh rejected: ${serverError}
577
+ ${pc.dim(`Status: ${response.status}`)}
578
+ ${pc.dim(`Run \`${cliname} login\` to re-authenticate.`)}`
579
+ );
580
+ }
581
+ if (!payload.sessionToken || !payload.userId || !payload.email || !payload.expiresAt) {
582
+ throw new Error(
583
+ `Token refresh response is missing required fields.
584
+ ${pc.dim(`Got: ${Object.keys(payload).join(", ")}`)}`
585
+ );
586
+ }
587
+ const nextCreds = {
588
+ sessionToken: payload.sessionToken,
589
+ userId: payload.userId,
590
+ email: payload.email,
591
+ expiresAt: payload.expiresAt,
592
+ refreshToken: payload.refreshToken ?? creds.refreshToken,
593
+ refreshExpiresAt: payload.refreshExpiresAt ?? creds.refreshExpiresAt,
594
+ activeOrg: creds.activeOrg
595
+ };
596
+ await saveAuth(nextCreds, config.authEnv ?? "prod");
597
+ return nextCreds;
598
+ }
599
+ async function getValidAuth(config, env = config.authEnv ?? "prod") {
600
+ const creds = await loadAuth(env);
601
+ if (!creds) return null;
602
+ if (!isExpiredAt(creds.expiresAt)) {
603
+ return creds;
604
+ }
605
+ if (!creds.refreshToken || isExpiredAt(creds.refreshExpiresAt)) {
606
+ return null;
607
+ }
608
+ try {
609
+ return await refreshAuth(creds, { ...config, authEnv: env });
610
+ } catch {
611
+ return null;
612
+ }
613
+ }
614
+ async function requireValidAuth(config, env = config.authEnv ?? "prod") {
615
+ const cliname = config.cliname ?? "nuu";
616
+ const creds = await getValidAuth(config, env);
617
+ if (!creds) {
618
+ throw new Error(
619
+ `Not logged in.
620
+ ${pc.dim(`Run \`${cliname} login\` to authenticate.`)}`
621
+ );
622
+ }
623
+ return creds;
624
+ }
625
+ function isAuthExpired(credentials) {
626
+ return isExpiredAt(credentials.expiresAt);
627
+ }
628
+ function shouldRefreshAuth(credentials) {
629
+ if (!credentials.refreshToken) return false;
630
+ if (isExpiredAt(credentials.refreshExpiresAt)) return false;
631
+ return expiresAtMs(credentials.expiresAt) - Date.now() <= REFRESH_WINDOW_MS;
632
+ }
633
+
634
+ // src/features/runtime.ts
635
+ function isTierEnabled(featureTier, mode) {
636
+ if (mode === "dev") return true;
637
+ if (featureTier === "prod") return true;
638
+ if (featureTier === "exp") return false;
639
+ return false;
640
+ }
641
+ function resolveModeFromRecord(record) {
642
+ const raw = record.mode;
643
+ return typeof raw === "string" ? normalizeRuntimeMode(raw) : void 0;
644
+ }
645
+ function resolveExperimentalFromRecord(record) {
646
+ const raw = record.experimental;
647
+ return typeof raw === "boolean" ? raw : void 0;
648
+ }
649
+ function resolveEnabledFeaturesFromRecord(record) {
650
+ if (!record.features || typeof record.features !== "object" || Array.isArray(record.features)) {
651
+ return [];
652
+ }
653
+ return Object.entries(record.features).filter(([, enabled]) => enabled === true).map(([featureId]) => featureId);
654
+ }
655
+ function buildDefaultMode() {
656
+ return normalizeRuntimeMode(process.env.BUILD_MODE ?? "") ?? "dev";
657
+ }
658
+ function readRuntimeSync(config, resolvedConfig) {
659
+ const mode = resolveModeFromRecord(resolvedConfig) ?? buildDefaultMode();
660
+ const configExperimental = resolveExperimentalFromRecord(resolvedConfig);
661
+ const experimental = mode === "dev" ? true : configExperimental ?? false;
662
+ const enabledFeatures = resolveEnabledFeaturesFromRecord(resolvedConfig);
663
+ const authenticated = hasAuth(authEnvForMode(mode));
664
+ return {
665
+ mode,
666
+ experimental,
667
+ enabledFeatures,
668
+ authenticated
669
+ };
670
+ }
671
+ async function resolveRuntime(config) {
672
+ const resolvedConfig = await resolveConfig({ cliname: config.cliname });
673
+ return readRuntimeSync(config, resolvedConfig);
674
+ }
675
+ function resolveRuntimeSync(config) {
676
+ const resolvedConfig = resolveConfigSync({ cliname: config.cliname });
677
+ return readRuntimeSync(config, resolvedConfig);
678
+ }
679
+ async function setMode(config, mode) {
680
+ await setConfigField(getConfigPath(config.cliname), "mode", mode);
681
+ }
682
+ async function resetMode(config) {
683
+ await removeConfigField(getConfigPath(config.cliname), "mode");
684
+ }
685
+ async function setExperimental(config, enabled) {
686
+ await setConfigField(getConfigPath(config.cliname), "experimental", enabled);
687
+ }
688
+ async function resetExperimental(config) {
689
+ await removeConfigField(getConfigPath(config.cliname), "experimental");
690
+ }
691
+ async function setFeatureEnabled(config, featureId, enabled) {
692
+ await setFeatureOverride(getConfigPath(config.cliname), featureId, enabled);
693
+ }
694
+ async function resetFeatureEnabled(config, featureId) {
695
+ await removeFeatureOverride(getConfigPath(config.cliname), featureId);
696
+ }
697
+
698
+ // src/errors.ts
699
+ var CliError = class extends Error {
700
+ exitCode;
701
+ code;
702
+ ref;
703
+ suggestions;
704
+ constructor(message, exitCode = 1, code, ref, suggestions) {
705
+ super(message);
706
+ this.name = "CliError";
707
+ this.exitCode = exitCode;
708
+ this.code = code;
709
+ this.ref = ref;
710
+ this.suggestions = suggestions;
711
+ }
712
+ };
713
+ function tierInstruction(requiredTier, cliname, featureId) {
714
+ if (requiredTier === "dev") {
715
+ return [
716
+ `Run \`${cliname} --dev ${featureId}\` for one invocation`,
717
+ `or persist dev mode with \`${cliname} mode dev\` when that command exists.`
718
+ ];
719
+ }
720
+ return [
721
+ `Run \`${cliname} --enable ${featureId}\` for one invocation`,
722
+ `or set \`[features].${featureId} = true\` in \`~/.nuucognition/.${cliname}/config.toml\`.`
723
+ ];
724
+ }
725
+ var FeatureNotAvailableError = class extends CliError {
726
+ constructor(featureId, requiredTier, context, cliname) {
727
+ const suggestions = tierInstruction(requiredTier, cliname, featureId);
728
+ super(
729
+ requiredTier === "dev" ? `Feature '${featureId}' requires dev mode.` : `Feature '${featureId}' requires experimental access.`,
730
+ 1,
731
+ "FEATURE_NOT_AVAILABLE",
732
+ void 0,
733
+ suggestions
734
+ );
735
+ this.name = "FeatureNotAvailableError";
736
+ }
737
+ };
738
+ var AuthRequiredError = class extends CliError {
739
+ constructor(featureId, cliname) {
740
+ super(
741
+ `Feature '${featureId}' requires authentication.`,
742
+ 1,
743
+ "AUTH_REQUIRED",
744
+ void 0,
745
+ [`Run \`${cliname} login\` or \`nuu login\`.`]
746
+ );
747
+ this.name = "AuthRequiredError";
748
+ }
749
+ };
750
+
751
+ // src/features/guard.ts
752
+ function isTierAvailable(tier, context, featureId) {
753
+ if (tier === "prod") {
754
+ return { available: true };
755
+ }
756
+ if (tier === "dev") {
757
+ return context.mode === "dev" ? { available: true } : { available: false, reason: `Feature '${featureId}' requires dev mode.` };
758
+ }
759
+ if (context.mode === "dev") {
760
+ return { available: true };
761
+ }
762
+ if (context.experimental || context.enabledFeatures.includes(featureId)) {
763
+ return { available: true };
764
+ }
765
+ return {
766
+ available: false,
767
+ reason: `Feature '${featureId}' requires experimental access.`
768
+ };
769
+ }
770
+ function isFeatureEnabled(registry, id, context) {
771
+ const feature = getFeature(registry, id);
772
+ if (!feature) return false;
773
+ const tier = isTierAvailable(feature.tier, context, id);
774
+ if (!tier.available) return false;
775
+ if (feature.requiresAuth && !context.authenticated) return false;
776
+ return true;
777
+ }
778
+ function requireFeature(registry, id, context, cliname) {
779
+ const feature = getFeature(registry, id);
780
+ if (!feature) {
781
+ throw new CliError(`Unknown feature: ${id}`, 1, "UNKNOWN_FEATURE");
782
+ }
783
+ const tier = isTierAvailable(feature.tier, context, id);
784
+ if (!tier.available) {
785
+ throw new FeatureNotAvailableError(id, feature.tier, context, cliname);
786
+ }
787
+ if (feature.requiresAuth && !context.authenticated) {
788
+ throw new AuthRequiredError(id, cliname);
789
+ }
790
+ }
791
+ function checkFeature(registry, id, context) {
792
+ const feature = getFeature(registry, id);
793
+ if (!feature) {
794
+ return {
795
+ available: false,
796
+ requiredTier: "dev",
797
+ currentMode: context.mode,
798
+ experimental: context.experimental,
799
+ requiresAuth: false,
800
+ authenticated: context.authenticated,
801
+ reason: `Unknown feature: ${id}`
802
+ };
803
+ }
804
+ const tier = isTierAvailable(feature.tier, context, id);
805
+ if (!tier.available) {
806
+ return {
807
+ available: false,
808
+ requiredTier: feature.tier,
809
+ currentMode: context.mode,
810
+ experimental: context.experimental,
811
+ requiresAuth: feature.requiresAuth,
812
+ authenticated: context.authenticated,
813
+ reason: tier.reason
814
+ };
815
+ }
816
+ if (feature.requiresAuth && !context.authenticated) {
817
+ return {
818
+ available: false,
819
+ requiredTier: feature.tier,
820
+ currentMode: context.mode,
821
+ experimental: context.experimental,
822
+ requiresAuth: true,
823
+ authenticated: false,
824
+ reason: `Feature '${id}' requires authentication.`
825
+ };
826
+ }
827
+ return {
828
+ available: true,
829
+ requiredTier: feature.tier,
830
+ currentMode: context.mode,
831
+ experimental: context.experimental,
832
+ requiresAuth: feature.requiresAuth,
833
+ authenticated: context.authenticated
834
+ };
835
+ }
836
+
837
+ // src/commands/auth.ts
838
+ import { Command } from "commander";
839
+ import cfonts from "cfonts";
840
+ import pc3 from "picocolors";
841
+
842
+ // src/output/index.ts
843
+ import pc2 from "picocolors";
844
+ function success(message) {
845
+ console.log(`${pc2.green("\u2713")} ${message}`);
846
+ }
847
+ function error(message) {
848
+ console.error(`${pc2.red("\u2717")} ${message}`);
849
+ }
850
+ function warn(message) {
851
+ console.error(`${pc2.yellow("!")} ${message}`);
852
+ }
853
+ function info(message) {
854
+ console.log(`${pc2.blue("i")} ${message}`);
855
+ }
856
+ function dim(message) {
857
+ return pc2.dim(message);
858
+ }
859
+ function bold(message) {
860
+ return pc2.bold(message);
861
+ }
862
+
863
+ // src/commands/auth.ts
864
+ function resolveEnv(options, fallback = "prod") {
865
+ return options.dev ? "dev" : fallback;
866
+ }
867
+ function resolveAccountUrl(config, env) {
868
+ return env === "dev" ? config.devAccountUrl ?? NUU_ACCOUNT_URL_DEV : config.prodAccountUrl ?? NUU_ACCOUNT_URL_PROD;
869
+ }
870
+ function createLoginCommand(config) {
871
+ const cmd = new Command("login").description("Authenticate with NUU").option("--verbose", "Show detailed debug output");
872
+ if (config.devAvailable) {
873
+ cmd.option("--dev", "Use the local development account service");
874
+ }
875
+ cmd.action(async (options) => {
876
+ const authEnv = resolveEnv(options, config.defaultEnv);
877
+ const cliname = config.cliname ?? "nuu";
878
+ if (options.verbose) setAuthVerbose(true);
879
+ try {
880
+ const credentials = await startLoginFlow({
881
+ accountUrl: resolveAccountUrl(config, authEnv),
882
+ authEnv,
883
+ callbackPort: config.callbackPort,
884
+ cliname
885
+ });
886
+ success(`Logged in as ${pc3.bold(credentials.email)}`);
887
+ cfonts.say("NUU Cognition", {
888
+ font: "simple",
889
+ colors: ["white"],
890
+ space: false,
891
+ align: "center"
892
+ });
893
+ console.log();
894
+ } catch (cause) {
895
+ const message = cause instanceof Error ? cause.message : String(cause);
896
+ if (!message.startsWith("Login failed") && !message.startsWith("Code exchange failed")) {
897
+ console.error(`
898
+ ${pc3.red("\u2717")} Login failed: ${message}`);
899
+ }
900
+ process.exit(1);
901
+ }
902
+ });
903
+ return cmd;
904
+ }
905
+ function createLogoutCommand(config) {
906
+ const cmd = new Command("logout").description("Clear stored credentials");
907
+ if (config.devAvailable) {
908
+ cmd.option("--dev", "Clear development credentials");
909
+ }
910
+ cmd.action(async (options) => {
911
+ const authEnv = resolveEnv(options, config.defaultEnv);
912
+ await clearAuth(authEnv);
913
+ success("Logged out");
914
+ });
915
+ return cmd;
916
+ }
917
+ function createWhoamiCommand(config) {
918
+ const cliname = config.cliname ?? "nuu";
919
+ const cmd = new Command("whoami").description("Show the current identity");
920
+ if (config.devAvailable) {
921
+ cmd.option("--dev", "Read development credentials");
922
+ }
923
+ cmd.action(async (options) => {
924
+ const authEnv = resolveEnv(options, config.defaultEnv);
925
+ const accountUrl = resolveAccountUrl(config, authEnv);
926
+ const credentials = await getValidAuth(
927
+ { accountUrl, authEnv, cliname },
928
+ authEnv
929
+ );
930
+ if (!credentials) {
931
+ const raw = await loadAuth(authEnv);
932
+ if (!raw) {
933
+ console.log(pc3.yellow("Not logged in"));
934
+ console.log(dim(` Run \`${cliname} login\` to authenticate.`));
935
+ return;
936
+ }
937
+ console.log(`${pc3.bold(raw.email)} ${pc3.red("expired")}`);
938
+ console.log(pc3.yellow(`
939
+ Session expired. Run \`${cliname} login\` to re-authenticate.`));
940
+ return;
941
+ }
942
+ console.log(`${dim("logged-in:")} ${credentials.email}`);
943
+ if (credentials.activeOrg) {
944
+ console.log(`${dim("selected-org:")} ${credentials.activeOrg.name}`);
945
+ }
946
+ });
947
+ return cmd;
948
+ }
949
+ export {
950
+ AuthRequiredError,
951
+ CliError,
952
+ FeatureNotAvailableError,
953
+ NUU_ACCOUNT_URL_DEV,
954
+ NUU_ACCOUNT_URL_PROD,
955
+ TIER_ALIASES,
956
+ bold,
957
+ checkFeature,
958
+ clearAuth,
959
+ createFeatureRegistry,
960
+ createLoginCommand,
961
+ createLogoutCommand,
962
+ createWhoamiCommand,
963
+ dim,
964
+ ensureConfigDir,
965
+ ensureConfigDirSync,
966
+ ensureNuuDir,
967
+ error,
968
+ getCommandFeatures,
969
+ getConfigDir,
970
+ getConfigPath,
971
+ getFeature,
972
+ getFeaturesByTier,
973
+ getFeaturesByType,
974
+ getNuuDir,
975
+ getValidAuth,
976
+ hasAuth,
977
+ hasFeature,
978
+ info,
979
+ isAuthExpired,
980
+ isFeatureEnabled,
981
+ isTierEnabled,
982
+ isValidTier,
983
+ loadAuth,
984
+ loadAuthSync,
985
+ normalizeRuntimeMode,
986
+ normalizeTier,
987
+ refreshAuth,
988
+ requireFeature,
989
+ requireValidAuth,
990
+ resetExperimental,
991
+ resetFeatureEnabled,
992
+ resetMode,
993
+ resolveConfig,
994
+ resolveConfigSync,
995
+ resolveRuntime,
996
+ resolveRuntimeSync,
997
+ saveAuth,
998
+ scopedEnvVar,
999
+ scopedEnvVarTrue,
1000
+ setAuthVerbose,
1001
+ setExperimental,
1002
+ setFeatureEnabled,
1003
+ setMode,
1004
+ shouldRefreshAuth,
1005
+ startLoginFlow,
1006
+ success,
1007
+ warn
1008
+ };