@kittl/cli 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,32 +1,134 @@
1
- // src/services/auth.service.ts
2
- import { createServer } from "node:http";
3
- import { URL } from "node:url";
4
- import open from "open";
5
- import * as oidc from "openid-client";
6
- import { z } from "zod";
1
+ // src/core/files.ts
2
+ import { basename } from "node:path";
3
+ import { lookup } from "mime-types";
4
+ import { glob } from "tinyglobby";
5
+ function getFileNameFromPath(filePath) {
6
+ const leaf = basename(filePath.trim());
7
+ if (leaf === "" || leaf === "." || leaf === "..") {
8
+ return void 0;
9
+ }
10
+ return leaf;
11
+ }
12
+ async function listAllFilesUnderDir(rootAbs) {
13
+ return glob("**/*", {
14
+ cwd: rootAbs,
15
+ absolute: true,
16
+ onlyFiles: true,
17
+ dot: true,
18
+ followSymbolicLinks: false
19
+ });
20
+ }
21
+ function contentTypeForPath(filePath) {
22
+ const mime = lookup(filePath);
23
+ return mime === false ? "application/octet-stream" : mime;
24
+ }
25
+
26
+ // src/core/utils.ts
27
+ import { mkdir, readFile, stat } from "node:fs/promises";
28
+ function parseJsonObject(jsonText, errorContext) {
29
+ let parsed;
30
+ try {
31
+ parsed = JSON.parse(jsonText);
32
+ } catch {
33
+ throw new Error(`Invalid JSON (${errorContext})`);
34
+ }
35
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
36
+ throw new Error(
37
+ `JSON must be an object, not array or null (${errorContext})`
38
+ );
39
+ }
40
+ return parsed;
41
+ }
42
+ async function parseJsonObjectFromFile(filePath) {
43
+ const jsonText = await readFile(filePath, "utf8");
44
+ return parseJsonObject(jsonText, filePath);
45
+ }
46
+ async function ensureDirectory(dirPath) {
47
+ try {
48
+ const s = await stat(dirPath);
49
+ if (!s.isDirectory()) {
50
+ throw new Error(`${dirPath} exists and is not a directory.`);
51
+ }
52
+ } catch (e) {
53
+ if (e instanceof Error && "code" in e && e.code === "ENOENT") {
54
+ await mkdir(dirPath, { recursive: true });
55
+ return;
56
+ }
57
+ throw e;
58
+ }
59
+ }
60
+ var INK_VIEW_UNMOUNT_REASON = Symbol("INK_VIEW_UNMOUNT_REASON");
61
+ function parsePortFromEnv(envKey, defaultPort) {
62
+ const raw = process.env[envKey];
63
+ const n = Number(raw?.trim() || defaultPort);
64
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
65
+ throw new Error(
66
+ `${envKey} must be an integer between 1 and 65535 (got ${JSON.stringify(raw)}).`
67
+ );
68
+ }
69
+ return n;
70
+ }
71
+ function localhostOAuthRedirectUri(port) {
72
+ return `http://localhost:${port}/callback`;
73
+ }
74
+ function getKittlEnvEntries() {
75
+ return Object.entries(process.env).filter(([key]) => key.startsWith("KITTL_")).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value ?? ""}`);
76
+ }
77
+ function isSubmitKey(key) {
78
+ if (key.return) {
79
+ return true;
80
+ }
81
+ return Boolean(key.enter);
82
+ }
83
+ function resolveFileNameFromEnv(envValue, defaultFileName) {
84
+ const raw = envValue?.trim() ?? "";
85
+ if (raw === "") {
86
+ return defaultFileName;
87
+ }
88
+ return getFileNameFromPath(raw) ?? defaultFileName;
89
+ }
90
+ function chunkArray(items, chunkSize) {
91
+ if (chunkSize < 1) {
92
+ throw new Error("chunkSize must be at least 1");
93
+ }
94
+ const n = Math.ceil(items.length / chunkSize);
95
+ return Array.from(
96
+ { length: n },
97
+ (_, i) => items.slice(i * chunkSize, i * chunkSize + chunkSize)
98
+ );
99
+ }
7
100
 
8
101
  // package.json
9
102
  var package_default = {
10
103
  name: "@kittl/cli",
11
- version: "0.0.1",
12
- private: true,
104
+ version: "0.0.3",
105
+ license: "Apache-2.0",
106
+ private: false,
13
107
  type: "module",
108
+ engines: {
109
+ node: ">=18"
110
+ },
14
111
  bin: {
15
112
  kittl: "./bin/run.js",
16
113
  "kittl-dev": "./bin/dev.js"
17
114
  },
18
115
  publishConfig: {
116
+ access: "public",
19
117
  bin: {
20
118
  kittl: "./bin/run.js"
21
119
  }
22
120
  },
23
121
  files: [
122
+ "LICENSE",
123
+ "oclif.manifest.json",
24
124
  "bin/bootstrap.js",
25
125
  "bin/run.cmd",
26
126
  "bin/run.js",
27
127
  "dist"
28
128
  ],
29
129
  scripts: {
130
+ prepack: "pnpm run build && clean-package",
131
+ postpack: "clean-package restore",
30
132
  build: "tsup && oclif manifest",
31
133
  "build:watch": 'tsup --watch --onSuccess "oclif manifest"',
32
134
  dev: "node ./bin/dev.js",
@@ -40,18 +142,22 @@ var package_default = {
40
142
  axios: "^1.13.6",
41
143
  "cross-keychain": "^1.1.0",
42
144
  ink: "^6.8.0",
145
+ "ink-text-input": "^6.0.0",
43
146
  "jwt-decode": "^4.0.0",
147
+ "mime-types": "^3.0.1",
44
148
  open: "^11.0.0",
45
149
  "openid-client": "^6.8.2",
46
150
  react: "catalog:",
151
+ tinyglobby: "^0.2.15",
47
152
  zod: "catalog:"
48
153
  },
49
154
  devDependencies: {
155
+ "@types/mime-types": "^3.0.1",
50
156
  "@types/node": "^25.5.0",
51
157
  "@types/react": "catalog:",
158
+ "clean-package": "^2.2.0",
52
159
  "ink-testing-library": "^4.0.0",
53
160
  oclif: "^4.22.96",
54
- tinyglobby: "^0.2.15",
55
161
  tsup: "catalog:",
56
162
  tsx: "catalog:",
57
163
  typescript: "catalog:",
@@ -65,42 +171,47 @@ var package_default = {
65
171
  }
66
172
  };
67
173
 
68
- // src/utils.ts
69
- var INK_VIEW_UNMOUNT_REASON = Symbol("INK_VIEW_UNMOUNT_REASON");
70
- function parseRedirectPortFromEnv(defaultPort) {
71
- const raw = process.env.KITTL_REDIRECT_PORT ?? String(defaultPort);
72
- const n = Number(raw);
73
- if (!Number.isInteger(n) || n < 1 || n > 65535) {
74
- throw new Error(
75
- `KITTL_REDIRECT_PORT must be an integer between 1 and 65535 (got ${JSON.stringify(raw)}).`
76
- );
77
- }
78
- return n;
79
- }
80
- function localhostOAuthRedirectUri(port) {
81
- return `http://localhost:${port}/callback`;
82
- }
83
- function getKittlEnvEntries() {
84
- return Object.entries(process.env).filter(([key]) => key.startsWith("KITTL_")).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${key}=${value ?? ""}`);
85
- }
86
-
87
174
  // src/constants.ts
88
175
  var { version } = package_default;
176
+ var CLI_CONFIG_DIR = ".kittl";
177
+ var CLI_CONFIG_DEFAULT_FILENAME = "config.json";
178
+ var CLI_MANIFEST_DEFAULT_FILENAME = "manifest.json";
89
179
  var PRODUCTION = {
90
- issuer: "https://auth.kittl.com/realms/kittl",
91
- //TODO: TBD
180
+ issuer: "https://keycloak.kittl.dev/auth/realms/kittl",
92
181
  apiBaseUrl: "https://api.kittl.com",
93
182
  clientId: "kittl-cli",
94
- redirectPort: 51771
183
+ redirectPort: 51771,
184
+ scaffoldViteDevPort: 5173
95
185
  };
96
- var AUTH_CONFIG = {
97
- issuer: process.env.KITTL_ISSUER_URL ?? PRODUCTION.issuer,
98
- clientId: process.env.KITTL_CLIENT_ID ?? PRODUCTION.clientId,
99
- redirectPort: parseRedirectPortFromEnv(PRODUCTION.redirectPort),
100
- redirectUri: localhostOAuthRedirectUri(
101
- parseRedirectPortFromEnv(PRODUCTION.redirectPort)
186
+ var CLI_CONFIG = {
187
+ configDir: CLI_CONFIG_DIR,
188
+ defaultConfigFileName: CLI_CONFIG_DEFAULT_FILENAME,
189
+ configFileName: resolveFileNameFromEnv(
190
+ process.env.KITTL_CONFIG_FILENAME,
191
+ CLI_CONFIG_DEFAULT_FILENAME
192
+ ),
193
+ defaultManifestFileName: CLI_MANIFEST_DEFAULT_FILENAME,
194
+ manifestFileName: resolveFileNameFromEnv(
195
+ process.env.KITTL_EXTENSION_MANIFEST_FILENAME,
196
+ CLI_MANIFEST_DEFAULT_FILENAME
102
197
  ),
103
- scope: process.env.KITTL_AUTH_SCOPE ?? "openid profile email offline_access",
198
+ scaffoldViteDevPort: parsePortFromEnv(
199
+ "KITTL_SCAFFOLD_VITE_DEV_PORT",
200
+ PRODUCTION.scaffoldViteDevPort
201
+ )
202
+ };
203
+ var authRedirectPort = parsePortFromEnv(
204
+ "KITTL_REDIRECT_PORT",
205
+ PRODUCTION.redirectPort
206
+ );
207
+ var AUTH_CONFIG = {
208
+ issuer: process.env.KITTL_OAUTH_ISSUER_URL ?? PRODUCTION.issuer,
209
+ clientId: process.env.KITTL_OAUTH_CLIENT_ID ?? PRODUCTION.clientId,
210
+ redirectPort: authRedirectPort,
211
+ redirectUri: localhostOAuthRedirectUri(authRedirectPort),
212
+ scope: process.env.KITTL_OAUTH_SCOPE ?? "openid profile email offline_access",
213
+ // https://www.keycloak.org/docs/latest/server_admin/index.html#_authentication-sessions
214
+ authPrompt: process.env.KITTL_OAUTH_PROMPT || "login",
104
215
  oauthCallbackTimeoutMs: Number(
105
216
  process.env.KITTL_OAUTH_CALLBACK_TIMEOUT_MS ?? 12e4
106
217
  ),
@@ -115,12 +226,82 @@ var API_CONFIG = {
115
226
  userAgent: `kittl-cli/${version}`
116
227
  };
117
228
 
229
+ // src/services/auth.service.ts
230
+ import { createServer } from "node:http";
231
+ import { URL as URL2 } from "node:url";
232
+ import open from "open";
233
+ import * as oidc from "openid-client";
234
+ import { z } from "zod";
235
+
118
236
  // src/services/session-vault.service.ts
119
237
  import { deletePassword, getPassword, setPassword } from "cross-keychain";
238
+ var SESSION_META_VERSION = 2;
239
+ var SESSION_V2_KEY_SUFFIX = {
240
+ meta: "meta",
241
+ access: "access",
242
+ refresh: "refresh",
243
+ id: "id"
244
+ };
120
245
  var SessionVault = class {
121
- keychainService = AUTH_CONFIG.serviceName;
246
+ keychainService = this.buildKeychainServiceName();
122
247
  keychainAccount = AUTH_CONFIG.accountName;
248
+ buildKeychainServiceName() {
249
+ try {
250
+ const issuerUrl = new URL(AUTH_CONFIG.issuer);
251
+ const issuerKey = this.sanitizeKeychainSegment(
252
+ `${issuerUrl.hostname}${issuerUrl.port ? `-${issuerUrl.port}` : ""}`
253
+ );
254
+ return `${AUTH_CONFIG.serviceName}-${issuerKey}`;
255
+ } catch {
256
+ return AUTH_CONFIG.serviceName;
257
+ }
258
+ }
259
+ sanitizeKeychainSegment(value) {
260
+ return value.replace(/[^a-zA-Z0-9._@-]/g, "-");
261
+ }
262
+ account(suffix) {
263
+ return `${this.keychainAccount}.${suffix}`;
264
+ }
123
265
  async getSession() {
266
+ const metaRaw = await getPassword(
267
+ this.keychainService,
268
+ this.account(SESSION_V2_KEY_SUFFIX.meta)
269
+ );
270
+ let meta = null;
271
+ if (metaRaw) {
272
+ try {
273
+ const parsed = JSON.parse(metaRaw);
274
+ if (parsed.v === SESSION_META_VERSION) {
275
+ meta = parsed;
276
+ }
277
+ } catch {
278
+ meta = null;
279
+ }
280
+ }
281
+ if (meta) {
282
+ const accessRaw = await getPassword(
283
+ this.keychainService,
284
+ this.account(SESSION_V2_KEY_SUFFIX.access)
285
+ );
286
+ const accessToken = accessRaw?.trim();
287
+ if (accessToken) {
288
+ const refreshRaw = await getPassword(
289
+ this.keychainService,
290
+ this.account(SESSION_V2_KEY_SUFFIX.refresh)
291
+ );
292
+ const idRaw = await getPassword(
293
+ this.keychainService,
294
+ this.account(SESSION_V2_KEY_SUFFIX.id)
295
+ );
296
+ return {
297
+ accessToken,
298
+ refreshToken: refreshRaw ?? void 0,
299
+ idToken: idRaw ?? void 0,
300
+ tokenType: meta.tokenType,
301
+ expiresAt: meta.expiresAt
302
+ };
303
+ }
304
+ }
124
305
  const raw = await getPassword(this.keychainService, this.keychainAccount);
125
306
  if (!raw)
126
307
  return null;
@@ -131,14 +312,74 @@ var SessionVault = class {
131
312
  }
132
313
  }
133
314
  async saveSession(session) {
315
+ const meta = {
316
+ v: SESSION_META_VERSION,
317
+ tokenType: session.tokenType,
318
+ expiresAt: session.expiresAt
319
+ };
320
+ const accessToken = session.accessToken.trim();
134
321
  await setPassword(
135
322
  this.keychainService,
136
- this.keychainAccount,
137
- JSON.stringify(session)
323
+ this.account(SESSION_V2_KEY_SUFFIX.access),
324
+ accessToken
325
+ );
326
+ await this.setOrDeleteTokenAccount(
327
+ SESSION_V2_KEY_SUFFIX.refresh,
328
+ session.refreshToken
138
329
  );
330
+ await this.setOrDeleteTokenAccount(
331
+ SESSION_V2_KEY_SUFFIX.id,
332
+ session.idToken
333
+ );
334
+ await setPassword(
335
+ this.keychainService,
336
+ this.account(SESSION_V2_KEY_SUFFIX.meta),
337
+ JSON.stringify(meta)
338
+ );
339
+ await this.deleteLegacyMonolithicIfPresent();
340
+ }
341
+ // when auth omits an optional token, remove any previously stored value as well
342
+ async setOrDeleteTokenAccount(suffix, token) {
343
+ const acc = this.account(suffix);
344
+ const trimmed = token?.trim();
345
+ if (trimmed) {
346
+ await setPassword(this.keychainService, acc, trimmed);
347
+ return;
348
+ }
349
+ const existing = await getPassword(this.keychainService, acc);
350
+ if (existing) {
351
+ await deletePassword(this.keychainService, acc);
352
+ }
353
+ }
354
+ // older releases stored the whole session JSON under {@link keychainAccount} only
355
+ async deleteLegacyMonolithicIfPresent() {
356
+ const legacy = await getPassword(
357
+ this.keychainService,
358
+ this.keychainAccount
359
+ );
360
+ if (!legacy)
361
+ return;
362
+ try {
363
+ await deletePassword(this.keychainService, this.keychainAccount);
364
+ } catch {
365
+ }
139
366
  }
140
367
  async clear() {
141
- await deletePassword(this.keychainService, this.keychainAccount);
368
+ const accounts = [
369
+ ...Object.values(SESSION_V2_KEY_SUFFIX).map(
370
+ (suffix) => this.account(suffix)
371
+ ),
372
+ this.keychainAccount
373
+ ];
374
+ for (const account of accounts) {
375
+ const existing = await getPassword(this.keychainService, account);
376
+ if (existing) {
377
+ try {
378
+ await deletePassword(this.keychainService, account);
379
+ } catch {
380
+ }
381
+ }
382
+ }
142
383
  }
143
384
  };
144
385
  var sessionVault = new SessionVault();
@@ -152,6 +393,7 @@ var authConfigSchema = z.object({
152
393
  redirectUri: z.url(),
153
394
  redirectPort: z.number().int().min(1).max(65535),
154
395
  scope: z.string().min(1),
396
+ authPrompt: z.string().min(1),
155
397
  oauthCallbackTimeoutMs: z.number().int().min(5e3).max(36e5),
156
398
  oauthSuccessRedirectUrl: z.url().optional()
157
399
  });
@@ -240,7 +482,7 @@ var AuthService = class {
240
482
  }
241
483
  }
242
484
  async discoverOidcConfiguration() {
243
- const issuerUrl = new URL(this.config.issuer);
485
+ const issuerUrl = new URL2(this.config.issuer);
244
486
  return oidc.discovery(
245
487
  issuerUrl,
246
488
  this.config.clientId,
@@ -266,7 +508,8 @@ var AuthService = class {
266
508
  code_challenge: codeChallenge,
267
509
  code_challenge_method: "S256",
268
510
  state,
269
- nonce
511
+ nonce,
512
+ prompt: this.config.authPrompt
270
513
  });
271
514
  signal?.throwIfAborted();
272
515
  await this.openBrowser(authorizationUrl.toString());
@@ -295,7 +538,7 @@ var AuthService = class {
295
538
  * - **Success response:** HTTP 302 to {@link AUTH_CONFIG.oauthSuccessRedirectUrl} when set; otherwise a minimal inline HTML page.
296
539
  */
297
540
  async waitForCallback(signal) {
298
- const redirectUrl = new URL(this.config.redirectUri);
541
+ const redirectUrl = new URL2(this.config.redirectUri);
299
542
  const hostname = redirectUrl.hostname;
300
543
  const port = Number(redirectUrl.port);
301
544
  const pathname = redirectUrl.pathname;
@@ -307,13 +550,13 @@ var AuthService = class {
307
550
  let closed = false;
308
551
  const server = createServer((req, res) => {
309
552
  try {
310
- const requestUrl = new URL(req.url ?? "", this.config.redirectUri);
553
+ const requestUrl = new URL2(req.url ?? "", this.config.redirectUri);
311
554
  if (requestUrl.pathname !== pathname) {
312
555
  res.statusCode = 404;
313
556
  res.end("Not Found");
314
557
  return;
315
558
  }
316
- const callbackUrl = new URL(this.config.redirectUri);
559
+ const callbackUrl = new URL2(this.config.redirectUri);
317
560
  callbackUrl.search = requestUrl.search;
318
561
  const brandUrl = this.config.oauthSuccessRedirectUrl;
319
562
  if (brandUrl) {
@@ -405,10 +648,8 @@ var AuthService = class {
405
648
  };
406
649
  var authService = new AuthService();
407
650
 
408
- // src/base-command.ts
651
+ // src/core/core.command.ts
409
652
  import { Command } from "@oclif/core";
410
- import { render } from "ink";
411
- import React from "react";
412
653
 
413
654
  // src/services/api.service.ts
414
655
  import axios, {
@@ -469,7 +710,28 @@ var KittlApiService = class {
469
710
  };
470
711
  var kittlApiService = new KittlApiService();
471
712
 
472
- // src/base-command.ts
713
+ // src/ui/renderer.ts
714
+ import { render } from "ink";
715
+ import React from "react";
716
+ async function runInteractiveView(View, ...args) {
717
+ const props = args[0];
718
+ return new Promise((resolve, reject) => {
719
+ const app = render(
720
+ React.createElement(View, {
721
+ ...props ?? {},
722
+ onDone: (result) => {
723
+ void (async () => {
724
+ app.unmount();
725
+ await app.waitUntilExit();
726
+ resolve(result);
727
+ })().catch(reject);
728
+ }
729
+ })
730
+ );
731
+ });
732
+ }
733
+
734
+ // src/core/core.command.ts
473
735
  var BaseCommand = class extends Command {
474
736
  session = null;
475
737
  async init() {
@@ -493,37 +755,30 @@ ${envEntries.join("\n") || "(none)"}`);
493
755
  const session = await authService.getSession();
494
756
  if (!session?.accessToken) {
495
757
  this.error(
496
- `Session expired. Run \`${this.config.bin} auth:login\` first.`,
758
+ `Session expired. Run \`${this.config.bin} auth login\` first.`,
497
759
  { exit: 2 }
498
760
  );
499
761
  }
500
762
  this.session = session;
501
763
  return session;
502
764
  }
503
- /**
504
- * Run an Ink view that reports a result via `onDone`, then unmount and return.
505
- * PRO TIP: use `this.log` / `this.error` after this resolves, not inside the view!! for final line stays on stdout.
506
- */
507
- async renderView(View) {
508
- return new Promise((resolve, reject) => {
509
- const app = render(
510
- React.createElement(View, {
511
- onDone: (result) => {
512
- void (async () => {
513
- app.unmount();
514
- await app.waitUntilExit();
515
- resolve(result);
516
- })().catch(reject);
517
- }
518
- })
519
- );
520
- });
765
+ async renderView(View, ...args) {
766
+ return runInteractiveView(View, ...args);
521
767
  }
522
768
  };
523
769
 
524
770
  export {
771
+ getFileNameFromPath,
772
+ listAllFilesUnderDir,
773
+ contentTypeForPath,
774
+ parseJsonObjectFromFile,
775
+ ensureDirectory,
525
776
  INK_VIEW_UNMOUNT_REASON,
777
+ isSubmitKey,
778
+ chunkArray,
779
+ CLI_CONFIG,
780
+ API_CONFIG,
526
781
  authService,
527
782
  BaseCommand
528
783
  };
529
- //# sourceMappingURL=chunk-JGD3QFQS.js.map
784
+ //# sourceMappingURL=chunk-YUKLWJFM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/files.ts","../src/core/utils.ts","../package.json","../src/constants.ts","../src/services/auth.service.ts","../src/services/session-vault.service.ts","../src/core/core.command.ts","../src/services/api.service.ts","../src/ui/renderer.ts"],"sourcesContent":["import { basename } from 'node:path';\nimport { lookup } from 'mime-types';\nimport { glob } from 'tinyglobby';\n\n/**\n * Last path segment\n */\nexport function getFileNameFromPath(filePath: string): string | undefined {\n const leaf = basename(filePath.trim());\n if (leaf === '' || leaf === '.' || leaf === '..') {\n return undefined;\n }\n return leaf;\n}\n\n/**\n * All files under `rootAbs` (recursive), absolute paths.\n * Does not traverse symlinked directories.\n */\nexport async function listAllFilesUnderDir(rootAbs: string): Promise<string[]> {\n return glob('**/*', {\n cwd: rootAbs,\n absolute: true,\n onlyFiles: true,\n dot: true,\n followSymbolicLinks: false,\n });\n}\n\n/**\n * Extension-based MIME for a path or basename. Unknown fallback to `application/octet-stream` (S3-safe).\n */\nexport function contentTypeForPath(filePath: string): string {\n const mime = lookup(filePath);\n return mime === false ? 'application/octet-stream' : mime;\n}\n","import { mkdir, readFile, stat } from 'node:fs/promises';\nimport type { Key } from 'ink';\nimport { getFileNameFromPath } from './files';\n\n/**\n * Parses a JSON string into a plain object.\n */\nexport function parseJsonObject(\n jsonText: string,\n errorContext: string,\n): Record<string, unknown> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(jsonText) as unknown;\n } catch {\n throw new Error(`Invalid JSON (${errorContext})`);\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error(\n `JSON must be an object, not array or null (${errorContext})`,\n );\n }\n return parsed as Record<string, unknown>;\n}\n\n// Reads a file and parses its contents as a JSON object.\nexport async function parseJsonObjectFromFile(\n filePath: string,\n): Promise<Record<string, unknown>> {\n const jsonText = await readFile(filePath, 'utf8');\n return parseJsonObject(jsonText, filePath);\n}\n\n/**\n * Ensures `dirPath` exists as a directory: creates it (and parents) when\n * missing, or throws if the path exists and is not a directory.\n */\nexport async function ensureDirectory(dirPath: string): Promise<void> {\n try {\n const s = await stat(dirPath);\n if (!s.isDirectory()) {\n throw new Error(`${dirPath} exists and is not a directory.`);\n }\n } catch (e) {\n // Not `isSystemError` from `./error`: this module is imported by `constants.ts`.\n if (\n e instanceof Error &&\n 'code' in e &&\n (e as NodeJS.ErrnoException).code === 'ENOENT'\n ) {\n await mkdir(dirPath, { recursive: true });\n return;\n }\n throw e;\n }\n}\n\n// Silent UI teardown (e.g. Ink unmount). not user-initiated cancel.\nexport const INK_VIEW_UNMOUNT_REASON = Symbol('INK_VIEW_UNMOUNT_REASON');\n\n/**\n * Reads `process.env[envKey]` as a TCP port, or {@link defaultPort} when unset/blank.\n * Must be an integer in 1–65535 when set.\n */\nexport function parsePortFromEnv(envKey: string, defaultPort: number): number {\n const raw = process.env[envKey];\n const n = Number(raw?.trim() || defaultPort);\n if (!Number.isInteger(n) || n < 1 || n > 65_535) {\n throw new Error(\n `${envKey} must be an integer between 1 and 65535 (got ${JSON.stringify(raw)}).`,\n );\n }\n return n;\n}\n\n// Fixed localhost OAuth callback path for Authorization Code + PKCE.\nexport function localhostOAuthRedirectUri(port: number): string {\n return `http://localhost:${port}/callback`;\n}\n\nexport function getKittlEnvEntries(): string[] {\n return Object.entries(process.env)\n .filter(([key]) => key.startsWith('KITTL_'))\n .sort(([a], [b]) => a.localeCompare(b))\n .map(([key, value]) => `${key}=${value ?? ''}`);\n}\n\n// Enter key across terminal variants (`return` + optional `enter`).\nexport function isSubmitKey(key: Key): boolean {\n if (key.return) {\n return true;\n }\n return Boolean((key as Key & { enter?: boolean }).enter);\n}\n\n/**\n * Resolves a file name from an env value via {@link getFileNameFromPath};\n * empty or invalid values fall back to `defaultFileName`.\n */\nexport function resolveFileNameFromEnv(\n envValue: string | undefined,\n defaultFileName: string,\n): string {\n const raw = envValue?.trim() ?? '';\n if (raw === '') {\n return defaultFileName;\n }\n return getFileNameFromPath(raw) ?? defaultFileName;\n}\n\n// Split an array into consecutive slices of at most `chunkSize` items.\nexport function chunkArray<T>(items: T[], chunkSize: number): T[][] {\n if (chunkSize < 1) {\n throw new Error('chunkSize must be at least 1');\n }\n const n = Math.ceil(items.length / chunkSize);\n return Array.from({ length: n }, (_, i) =>\n items.slice(i * chunkSize, i * chunkSize + chunkSize),\n );\n}\n","{\n \"name\": \"@kittl/cli\",\n \"version\": \"0.0.3\",\n \"license\": \"Apache-2.0\",\n \"private\": false,\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=18\"\n },\n \"bin\": {\n \"kittl\": \"./bin/run.js\",\n \"kittl-dev\": \"./bin/dev.js\"\n },\n \"publishConfig\": {\n \"access\": \"public\",\n \"bin\": {\n \"kittl\": \"./bin/run.js\"\n }\n },\n \"files\": [\n \"LICENSE\",\n \"oclif.manifest.json\",\n \"bin/bootstrap.js\",\n \"bin/run.cmd\",\n \"bin/run.js\",\n \"dist\"\n ],\n \"scripts\": {\n \"prepack\": \"pnpm run build && clean-package\",\n \"postpack\": \"clean-package restore\",\n \"build\": \"tsup && oclif manifest\",\n \"build:watch\": \"tsup --watch --onSuccess \\\"oclif manifest\\\"\",\n \"dev\": \"node ./bin/dev.js\",\n \"dev:watch\": \"node --watch --watch-path=./src --watch-path=./bin ./bin/dev.js\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\"\n },\n \"dependencies\": {\n \"@oclif/core\": \"^4.10.2\",\n \"axios\": \"^1.13.6\",\n \"cross-keychain\": \"^1.1.0\",\n \"ink\": \"^6.8.0\",\n \"ink-text-input\": \"^6.0.0\",\n \"jwt-decode\": \"^4.0.0\",\n \"mime-types\": \"^3.0.1\",\n \"open\": \"^11.0.0\",\n \"openid-client\": \"^6.8.2\",\n \"react\": \"catalog:\",\n \"tinyglobby\": \"^0.2.15\",\n \"zod\": \"catalog:\"\n },\n \"devDependencies\": {\n \"@types/mime-types\": \"^3.0.1\",\n \"@types/node\": \"^25.5.0\",\n \"@types/react\": \"catalog:\",\n \"clean-package\": \"^2.2.0\",\n \"ink-testing-library\": \"^4.0.0\",\n \"oclif\": \"^4.22.96\",\n \"tsup\": \"catalog:\",\n \"tsx\": \"catalog:\",\n \"typescript\": \"catalog:\",\n \"vitest\": \"^4.1.1\"\n },\n \"oclif\": {\n \"bin\": \"kittl\",\n \"commands\": \"./dist/commands\",\n \"dirname\": \"kittl\",\n \"topicSeparator\": \" \"\n }\n}\n","import pkg from '../package.json' with { type: 'json' };\nimport {\n localhostOAuthRedirectUri,\n parsePortFromEnv,\n resolveFileNameFromEnv,\n} from './core/utils';\n\nconst { version } = pkg;\n\nconst CLI_CONFIG_DIR = '.kittl' as const;\nconst CLI_CONFIG_DEFAULT_FILENAME = 'config.json' as const;\nconst CLI_MANIFEST_DEFAULT_FILENAME = 'manifest.json' as const;\n\nexport const PRODUCTION = {\n issuer: 'https://keycloak.kittl.dev/auth/realms/kittl',\n apiBaseUrl: 'https://api.kittl.com',\n clientId: 'kittl-cli',\n redirectPort: 51771,\n scaffoldViteDevPort: 5173,\n} as const;\n\n// -----------------------------------------------------------------------------\n// CLI configs\n// -----------------------------------------------------------------------------\n\nexport const CLI_CONFIG = {\n configDir: CLI_CONFIG_DIR,\n defaultConfigFileName: CLI_CONFIG_DEFAULT_FILENAME,\n configFileName: resolveFileNameFromEnv(\n process.env.KITTL_CONFIG_FILENAME,\n CLI_CONFIG_DEFAULT_FILENAME,\n ),\n defaultManifestFileName: CLI_MANIFEST_DEFAULT_FILENAME,\n manifestFileName: resolveFileNameFromEnv(\n process.env.KITTL_EXTENSION_MANIFEST_FILENAME,\n CLI_MANIFEST_DEFAULT_FILENAME,\n ),\n scaffoldViteDevPort: parsePortFromEnv(\n 'KITTL_SCAFFOLD_VITE_DEV_PORT',\n PRODUCTION.scaffoldViteDevPort,\n ),\n} as const;\n\n// -----------------------------------------------------------------------------\n// OAuth / OIDC (PKCE login, keychain session)\n// -----------------------------------------------------------------------------\n\nconst authRedirectPort = parsePortFromEnv(\n 'KITTL_REDIRECT_PORT',\n PRODUCTION.redirectPort,\n);\n\nexport const AUTH_CONFIG = {\n issuer: process.env.KITTL_OAUTH_ISSUER_URL ?? PRODUCTION.issuer,\n clientId: process.env.KITTL_OAUTH_CLIENT_ID ?? PRODUCTION.clientId,\n redirectPort: authRedirectPort,\n redirectUri: localhostOAuthRedirectUri(authRedirectPort),\n scope: process.env.KITTL_OAUTH_SCOPE ?? 'openid profile email offline_access',\n // https://www.keycloak.org/docs/latest/server_admin/index.html#_authentication-sessions\n authPrompt: process.env.KITTL_OAUTH_PROMPT || 'login',\n oauthCallbackTimeoutMs: Number(\n process.env.KITTL_OAUTH_CALLBACK_TIMEOUT_MS ?? 120_000,\n ),\n /** Optional 302 after OAuth; if undefined it renders a fallback HTML. */\n oauthSuccessRedirectUrl:\n process.env.KITTL_OAUTH_SUCCESS_REDIRECT_URL || undefined,\n serviceName: 'kittl-cli',\n accountName: 'oauth-session',\n} as const;\n\n// -----------------------------------------------------------------------------\n// HTTP API (Axios base URL for Kittl APIs)\n// -----------------------------------------------------------------------------\n\nexport const API_CONFIG = {\n baseUrl: process.env.KITTL_API_BASE_URL ?? PRODUCTION.apiBaseUrl,\n timeoutMs: 30_000,\n userAgent: `kittl-cli/${version}`,\n} as const;\n","import { createServer } from 'node:http';\nimport { URL } from 'node:url';\nimport open from 'open';\nimport * as oidc from 'openid-client';\nimport { z } from 'zod';\nimport { AUTH_CONFIG } from '../constants';\nimport type { Session } from '../types/session';\nimport { sessionVault } from './session-vault.service';\n\n// Seconds before access token expiry when we proactively refresh.\nconst ACCESS_TOKEN_REFRESH_BUFFER_SEC = 60;\n\nconst OAUTH2_TOKEN_ERROR_INVALID_GRANT = 'invalid_grant';\n\nexport type LoginOptions = {\n signal?: AbortSignal;\n};\n\nexport type CallbackResult = {\n callbackUrl: URL;\n};\n\nconst authConfigSchema = z.object({\n issuer: z.url(),\n clientId: z.string().min(1),\n redirectUri: z.url(),\n redirectPort: z.number().int().min(1).max(65_535),\n scope: z.string().min(1),\n authPrompt: z.string().min(1),\n oauthCallbackTimeoutMs: z.number().int().min(5_000).max(3_600_000),\n oauthSuccessRedirectUrl: z.url().optional(),\n});\n\nexport class LoginCancelledError extends Error {\n public constructor() {\n super('Login cancelled.');\n this.name = 'LoginCancelledError';\n }\n}\n\nexport class AuthService {\n private readonly config = authConfigSchema.parse(AUTH_CONFIG);\n\n // concurrent refresh attempts are merged into a single promise\n private refreshSessionPromise: Promise<Session | null> | null = null;\n\n /**\n * raw vault read.\n */\n public async getStoredSession(): Promise<Session | null> {\n return sessionVault.getSession();\n }\n\n /**\n * ensures token freshness (by silently refresh if needed)\n */\n public async getSession(): Promise<Session | null> {\n await this.getAccessToken();\n return this.getStoredSession();\n }\n\n /**\n * Returns a usable access token, silently refreshing when `expiresAt` is within {@link ACCESS_TOKEN_REFRESH_BUFFER_SEC}\n * seconds (or in the past). based on Session.expiresAt.\n */\n public async getAccessToken(): Promise<string | undefined> {\n const session = await this.getStoredSession();\n if (!session?.accessToken) return undefined;\n\n if (!this.shouldRefreshAccessToken(session)) {\n return session.accessToken;\n }\n\n if (!session.refreshToken) {\n await sessionVault.clear();\n return undefined;\n }\n\n const refreshed = await this.refreshSession();\n return refreshed?.accessToken;\n }\n\n /**\n * Refresh tokens via the OIDC token endpoint. On hard failure (e.g. `invalid_grant`), clears the vault.\n */\n public async refreshSession(): Promise<Session | null> {\n if (this.refreshSessionPromise) {\n return this.refreshSessionPromise;\n }\n this.refreshSessionPromise = this.performRefreshSession().finally(() => {\n this.refreshSessionPromise = null;\n });\n return this.refreshSessionPromise;\n }\n\n private shouldRefreshAccessToken(session: Session): boolean {\n if (session.expiresAt === undefined) return false;\n const now = Math.floor(Date.now() / 1000);\n return session.expiresAt <= now + ACCESS_TOKEN_REFRESH_BUFFER_SEC;\n }\n\n private async performRefreshSession(): Promise<Session | null> {\n const session = await this.getStoredSession();\n if (!session?.refreshToken) {\n await sessionVault.clear();\n return null;\n }\n\n try {\n const oidcConfig = await this.discoverOidcConfiguration();\n\n const tokenSet = await oidc.refreshTokenGrant(\n oidcConfig,\n session.refreshToken,\n { scope: this.config.scope },\n undefined,\n );\n\n const newSession = this.mapTokenSetToSession(tokenSet, session);\n\n await sessionVault.saveSession(newSession);\n return newSession;\n } catch (error) {\n if (\n error instanceof oidc.ResponseBodyError &&\n error.error === OAUTH2_TOKEN_ERROR_INVALID_GRANT\n ) {\n await sessionVault.clear();\n return null;\n }\n return null;\n }\n }\n\n private async discoverOidcConfiguration(): Promise<oidc.Configuration> {\n const issuerUrl = new URL(this.config.issuer);\n return oidc.discovery(\n issuerUrl,\n this.config.clientId,\n undefined,\n undefined,\n {\n ...(issuerUrl.protocol === 'http:'\n ? { execute: [oidc.allowInsecureRequests] as const }\n : {}),\n } as Parameters<typeof oidc.discovery>[4],\n );\n }\n\n public async login(options?: LoginOptions): Promise<Session> {\n const { signal } = options ?? {};\n\n const oidcConfig = await this.discoverOidcConfiguration();\n signal?.throwIfAborted();\n\n const codeVerifier = oidc.randomPKCECodeVerifier();\n const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);\n const state = oidc.randomState();\n const nonce = oidc.randomNonce();\n\n const authorizationUrl = oidc.buildAuthorizationUrl(oidcConfig, {\n redirect_uri: this.config.redirectUri,\n response_type: 'code',\n scope: this.config.scope,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n state,\n nonce,\n prompt: this.config.authPrompt,\n });\n\n signal?.throwIfAborted();\n await this.openBrowser(authorizationUrl.toString());\n\n const callback = await this.waitForCallback(signal);\n\n const tokenSet = await oidc.authorizationCodeGrant(\n oidcConfig,\n callback.callbackUrl,\n {\n pkceCodeVerifier: codeVerifier,\n expectedState: state,\n expectedNonce: nonce,\n },\n );\n\n const session = this.mapTokenSetToSession(tokenSet);\n\n await sessionVault.saveSession(session);\n return session;\n }\n\n public async logout(): Promise<void> {\n await sessionVault.clear();\n }\n\n /**\n * Starts a short-lived `http` server on the host/port from the configured `redirectUri` so the IdP can\n * redirect the browser to `…/callback?code=…&state=…` (Authorization Code + PKCE). The first matching request\n * stops the server and resolves with that URL for the token exchange (`authorizationCodeGrant`).\n *\n * - **Success response:** HTTP 302 to {@link AUTH_CONFIG.oauthSuccessRedirectUrl} when set; otherwise a minimal inline HTML page.\n */\n private async waitForCallback(signal?: AbortSignal): Promise<CallbackResult> {\n const redirectUrl = new URL(this.config.redirectUri);\n const hostname = redirectUrl.hostname;\n const port = Number(redirectUrl.port);\n const pathname = redirectUrl.pathname;\n\n const timeoutSignal = AbortSignal.timeout(\n this.config.oauthCallbackTimeoutMs,\n );\n const combinedSignal =\n signal !== undefined\n ? AbortSignal.any([signal, timeoutSignal])\n : timeoutSignal;\n\n return new Promise((resolve, reject) => {\n let closed = false;\n\n const server = createServer((req, res) => {\n try {\n const requestUrl = new URL(req.url ?? '', this.config.redirectUri);\n if (requestUrl.pathname !== pathname) {\n res.statusCode = 404;\n res.end('Not Found');\n return;\n }\n\n const callbackUrl = new URL(this.config.redirectUri);\n callbackUrl.search = requestUrl.search;\n\n const brandUrl = this.config.oauthSuccessRedirectUrl;\n if (brandUrl) {\n res.writeHead(302, { Location: brandUrl });\n res.end();\n } else {\n res.statusCode = 200;\n res.setHeader('content-type', 'text/html; charset=utf-8');\n res.end(\n '<!doctype html><html><body><h2>Authentication complete.</h2><p>You can close this tab and return to the terminal.</p></body></html>',\n );\n }\n\n closeServer((closeErr?: Error) => {\n if (closeErr) {\n reject(closeErr);\n return;\n }\n resolve({ callbackUrl });\n });\n } catch (error) {\n closeServer(() => reject(error));\n }\n });\n\n const closeServer = (onClosed: (closeErr?: Error) => void): void => {\n if (closed) return;\n closed = true;\n combinedSignal.removeEventListener('abort', onCombinedAbort);\n if (!server.listening) {\n onClosed();\n return;\n }\n server.closeAllConnections();\n server.close((closeErr) => {\n onClosed(closeErr ?? undefined);\n });\n };\n\n function onCombinedAbort(): void {\n closeServer((closeErr?: Error) => {\n if (closeErr) {\n reject(closeErr);\n return;\n }\n if (signal?.aborted) {\n reject(signal.reason ?? new LoginCancelledError());\n return;\n }\n if (timeoutSignal.aborted) {\n reject(new Error('Login timed out. Please try again.'));\n return;\n }\n reject(new LoginCancelledError());\n });\n }\n\n server.on('error', (error: NodeJS.ErrnoException) => {\n closeServer(() => {\n if (error.code === 'EADDRINUSE') {\n const inUsePort =\n (error as NodeJS.ErrnoException & { port?: number }).port ?? port;\n reject(\n new Error(\n `Port ${String(inUsePort)} is already in use. Close the other app using it or set KITTL_REDIRECT_PORT to a free port, then try again.`,\n ),\n );\n return;\n }\n reject(error);\n });\n });\n\n // 1. Abort before any listen: avoids a half-started server; early exit skips `listen` entirely.\n combinedSignal.addEventListener('abort', onCombinedAbort, { once: true });\n if (combinedSignal.aborted) {\n onCombinedAbort();\n return;\n }\n\n server.listen(port, hostname);\n });\n }\n\n private async openBrowser(url: string): Promise<void> {\n await open(url);\n }\n\n private mapTokenSetToSession(\n tokenSet: oidc.TokenEndpointResponse,\n currentSession?: Session,\n ): Session {\n return {\n accessToken: tokenSet.access_token,\n refreshToken: tokenSet.refresh_token ?? currentSession?.refreshToken,\n idToken: tokenSet.id_token ?? currentSession?.idToken,\n tokenType: tokenSet.token_type ?? currentSession?.tokenType,\n expiresAt: tokenSet.expires_in\n ? Math.floor(Date.now() / 1000) + tokenSet.expires_in\n : undefined,\n };\n }\n}\n\nexport const authService = new AuthService();\n","import { deletePassword, getPassword, setPassword } from 'cross-keychain';\nimport { AUTH_CONFIG } from '../constants';\nimport type { Session } from '../types/session';\n\nconst SESSION_META_VERSION = 2 as const;\n\n// suffix for split v2 accounts: `{AUTH_CONFIG.accountName}.{suffix}`.\nconst SESSION_V2_KEY_SUFFIX = {\n meta: 'meta',\n access: 'access',\n refresh: 'refresh',\n id: 'id',\n} as const;\n\ntype SessionMetaV2 = {\n v: typeof SESSION_META_VERSION;\n tokenType?: string;\n expiresAt?: number;\n};\n\n/**\n * Persists OIDC session (tokens) in the OS credential store (e.g. Keychain).\n */\nexport class SessionVault {\n private readonly keychainService = this.buildKeychainServiceName();\n private readonly keychainAccount = AUTH_CONFIG.accountName;\n\n private buildKeychainServiceName(): string {\n try {\n const issuerUrl = new URL(AUTH_CONFIG.issuer);\n const issuerKey = this.sanitizeKeychainSegment(\n `${issuerUrl.hostname}${issuerUrl.port ? `-${issuerUrl.port}` : ''}`,\n );\n return `${AUTH_CONFIG.serviceName}-${issuerKey}`;\n } catch {\n // Keep previous behavior when issuer is not a valid URL.\n return AUTH_CONFIG.serviceName;\n }\n }\n\n private sanitizeKeychainSegment(value: string): string {\n return value.replace(/[^a-zA-Z0-9._@-]/g, '-');\n }\n\n private account(suffix: string): string {\n return `${this.keychainAccount}.${suffix}`;\n }\n\n public async getSession(): Promise<Session | null> {\n const metaRaw = await getPassword(\n this.keychainService,\n this.account(SESSION_V2_KEY_SUFFIX.meta),\n );\n\n let meta: SessionMetaV2 | null = null;\n if (metaRaw) {\n try {\n const parsed = JSON.parse(metaRaw) as SessionMetaV2;\n if (parsed.v === SESSION_META_VERSION) {\n meta = parsed;\n }\n } catch {\n meta = null;\n }\n }\n // if meta is present, read based on v2\n if (meta) {\n const accessRaw = await getPassword(\n this.keychainService,\n this.account(SESSION_V2_KEY_SUFFIX.access),\n );\n // fallback, if not defined, to legacy monolithic read\n const accessToken = accessRaw?.trim();\n if (accessToken) {\n const refreshRaw = await getPassword(\n this.keychainService,\n this.account(SESSION_V2_KEY_SUFFIX.refresh),\n );\n const idRaw = await getPassword(\n this.keychainService,\n this.account(SESSION_V2_KEY_SUFFIX.id),\n );\n\n return {\n accessToken,\n refreshToken: refreshRaw ?? undefined,\n idToken: idRaw ?? undefined,\n tokenType: meta.tokenType,\n expiresAt: meta.expiresAt,\n };\n }\n }\n // read based on legacy monolithic\n const raw = await getPassword(this.keychainService, this.keychainAccount);\n if (!raw) return null;\n\n try {\n return JSON.parse(raw) as Session;\n } catch {\n return null;\n }\n }\n\n public async saveSession(session: Session): Promise<void> {\n const meta: SessionMetaV2 = {\n v: SESSION_META_VERSION,\n tokenType: session.tokenType,\n expiresAt: session.expiresAt,\n };\n\n const accessToken = session.accessToken.trim();\n await setPassword(\n this.keychainService,\n this.account(SESSION_V2_KEY_SUFFIX.access),\n accessToken,\n );\n await this.setOrDeleteTokenAccount(\n SESSION_V2_KEY_SUFFIX.refresh,\n session.refreshToken,\n );\n await this.setOrDeleteTokenAccount(\n SESSION_V2_KEY_SUFFIX.id,\n session.idToken,\n );\n await setPassword(\n this.keychainService,\n this.account(SESSION_V2_KEY_SUFFIX.meta),\n JSON.stringify(meta),\n );\n\n await this.deleteLegacyMonolithicIfPresent();\n }\n\n // when auth omits an optional token, remove any previously stored value as well\n private async setOrDeleteTokenAccount(\n suffix:\n | typeof SESSION_V2_KEY_SUFFIX.refresh\n | typeof SESSION_V2_KEY_SUFFIX.id,\n token: string | undefined,\n ): Promise<void> {\n const acc = this.account(suffix);\n const trimmed = token?.trim();\n if (trimmed) {\n await setPassword(this.keychainService, acc, trimmed);\n return;\n }\n const existing = await getPassword(this.keychainService, acc);\n if (existing) {\n await deletePassword(this.keychainService, acc);\n }\n }\n\n // older releases stored the whole session JSON under {@link keychainAccount} only\n private async deleteLegacyMonolithicIfPresent(): Promise<void> {\n const legacy = await getPassword(\n this.keychainService,\n this.keychainAccount,\n );\n if (!legacy) return;\n try {\n await deletePassword(this.keychainService, this.keychainAccount);\n } catch {\n // ignore, best effort cleanup\n }\n }\n\n public async clear(): Promise<void> {\n const accounts = [\n ...Object.values(SESSION_V2_KEY_SUFFIX).map((suffix) =>\n this.account(suffix),\n ),\n this.keychainAccount,\n ];\n for (const account of accounts) {\n const existing = await getPassword(this.keychainService, account);\n if (existing) {\n try {\n await deletePassword(this.keychainService, account);\n } catch {\n // ignore\n }\n }\n }\n }\n}\n\nexport const sessionVault = new SessionVault();\n","import { Command } from '@oclif/core';\nimport type { AxiosInstance } from 'axios';\nimport type { FC } from 'react';\nimport { API_CONFIG } from '../constants';\nimport { kittlApiService } from '../services/api.service';\nimport { authService } from '../services/auth.service';\nimport type { Session } from '../types/session';\nimport { runInteractiveView, type ViewWithDone } from '../ui/renderer';\nimport { getKittlEnvEntries } from './utils';\n\nexport abstract class BaseCommand extends Command {\n protected session: Session | null = null;\n\n public override async init(): Promise<void> {\n await super.init();\n this.session = await authService.getSession();\n kittlApiService.setAccessTokenProvider(async () =>\n authService.getAccessToken(),\n );\n\n const envEntries = getKittlEnvEntries();\n this.debug(`API base URL: ${API_CONFIG.baseUrl}`);\n this.debug(`KITTL_* variables:\\n${envEntries.join('\\n') || '(none)'}`);\n }\n\n protected getKittlApiClient(): AxiosInstance {\n return kittlApiService.getClient();\n }\n\n /**\n * Ensures a valid access token (+ refreshes when the accessToken is near expiry).\n */\n protected async ensureAuthenticated(): Promise<Session> {\n const session = await authService.getSession();\n if (!session?.accessToken) {\n this.error(\n `Session expired. Run \\`${this.config.bin} auth login\\` first.`,\n { exit: 2 },\n );\n }\n\n this.session = session;\n return session;\n }\n\n /**\n * Run an Ink view that reports a result via `onDone`, then unmount and return.\n * PRO TIP: use `this.log` / `this.error` after this resolves, not inside the view!! for final line stays on stdout.\n */\n protected async renderView<R>(View: FC<ViewWithDone<R>>): Promise<R>;\n protected async renderView<R, P extends object>(\n View: FC<P & ViewWithDone<R>>,\n props: P,\n ): Promise<R>;\n protected async renderView<R, P extends object>(\n View: FC<P & ViewWithDone<R>>,\n ...args: Partial<P> extends P ? [props?: P] : [props: P]\n ): Promise<R> {\n return runInteractiveView<R, P>(View, ...args);\n }\n}\n","import axios, {\n type AxiosError,\n AxiosHeaders,\n type AxiosInstance,\n type InternalAxiosRequestConfig,\n} from 'axios';\nimport { API_CONFIG } from '../constants';\nimport { authService } from './auth.service';\n\ntype AccessTokenProvider = () => Promise<string | undefined>;\ntype RetriableRequestConfig = InternalAxiosRequestConfig & {\n _retry?: boolean;\n};\n\nexport class KittlApiService {\n private readonly client: AxiosInstance;\n private accessTokenProvider?: AccessTokenProvider;\n\n public constructor() {\n this.client = axios.create({\n baseURL: API_CONFIG.baseUrl,\n timeout: API_CONFIG.timeoutMs,\n headers: {\n 'User-Agent': API_CONFIG.userAgent,\n },\n });\n\n this.client.interceptors.request.use(\n async (config: RetriableRequestConfig) => {\n if (config.skipAuth) {\n return config;\n }\n const token = await this.accessTokenProvider?.();\n if (token) {\n this.setAuthorizationHeader(config, token);\n }\n return config;\n },\n );\n\n this.client.interceptors.response.use(\n (response) => response,\n async (error: AxiosError) => {\n const originalRequest = error.config as\n | RetriableRequestConfig\n | undefined;\n // Retry only first-time 401 responses with a valid original request.\n if (\n error.response?.status !== 401 ||\n !originalRequest ||\n originalRequest.skipAuth ||\n originalRequest._retry\n ) {\n throw error;\n }\n\n // Mark as retried to avoid infinite retry loops.\n originalRequest._retry = true;\n // Force-refresh the token before replaying the request.\n const refreshedSession = await authService.refreshSession();\n const refreshedToken = refreshedSession?.accessToken;\n if (!refreshedToken) {\n throw error;\n }\n\n // Re-run the original request with updated Authorization header.\n this.setAuthorizationHeader(originalRequest, refreshedToken);\n return this.client.request(originalRequest);\n },\n );\n }\n\n public setAccessTokenProvider(provider: AccessTokenProvider): void {\n this.accessTokenProvider = provider;\n }\n\n public getClient(): AxiosInstance {\n return this.client;\n }\n\n private setAuthorizationHeader(\n config: InternalAxiosRequestConfig,\n token: string,\n ): void {\n const headers = AxiosHeaders.from(config.headers);\n headers.set('Authorization', `Bearer ${token}`);\n config.headers = headers;\n }\n}\n\nexport const kittlApiService = new KittlApiService();\n","import { render } from 'ink';\nimport React, { type FC } from 'react';\n\n/**\n * Standard shape for a view that reports a result.\n */\nexport type ViewWithDone<R> = { onDone: (result: R) => void };\n\n/**\n * The entrypoint that touches Ink's render process.\n */\nexport async function runInteractiveView<R, P extends object>(\n View: FC<P & ViewWithDone<R>>,\n ...args: Partial<P> extends P ? [props?: P] : [props: P]\n): Promise<R> {\n const props = args[0];\n\n return new Promise<R>((resolve, reject) => {\n const app = render(\n React.createElement(View, {\n ...(props ?? ({} as P)),\n onDone: (result: R) => {\n void (async () => {\n app.unmount();\n await app.waitUntilExit();\n resolve(result);\n })().catch(reject);\n },\n } as P & ViewWithDone<R>),\n );\n });\n}\n"],"mappings":"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","names":["URL","URL"]}