@kittl/cli 0.0.1 → 0.0.2

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,13 +1,427 @@
1
1
  import {
2
- BaseCommand
3
- } from "../../chunk-JGD3QFQS.js";
2
+ formatExtensionArtifactUploadError,
3
+ isSystemError,
4
+ readInternalConfig
5
+ } from "../../chunk-XU2ZHSRY.js";
6
+ import {
7
+ useTerminalWidth
8
+ } from "../../chunk-EKU4DKQK.js";
9
+ import {
10
+ layoutStyles,
11
+ spacing,
12
+ textStyles
13
+ } from "../../chunk-3BPIJLS7.js";
14
+ import {
15
+ BaseCommand,
16
+ chunkArray
17
+ } from "../../chunk-TK44DTSK.js";
18
+
19
+ // src/commands/app/upload.ts
20
+ import { stat } from "node:fs/promises";
21
+ import { resolve } from "node:path";
22
+ import { Flags } from "@oclif/core";
23
+
24
+ // src/core/extension-upload.core.ts
25
+ import { readFile } from "node:fs/promises";
26
+ import { basename, relative } from "node:path";
27
+
28
+ // src/services/extension-artifacts.service.ts
29
+ import axios from "axios";
30
+ import { z } from "zod";
31
+ var uploadSignedUrlsResponseSchema = z.object({
32
+ success: z.literal(true),
33
+ results: z.array(
34
+ z.object({
35
+ url: z.string().min(1),
36
+ key: z.string(),
37
+ relativePath: z.string(),
38
+ contentType: z.string().min(1)
39
+ })
40
+ )
41
+ });
42
+ async function requestExtensionUploadSignedUrls(client, extensionId, files) {
43
+ const path = `/extensions/${extensionId}/versions/upload-signed-urls`;
44
+ const { data } = await client.post(path, { files });
45
+ const parsed = uploadSignedUrlsResponseSchema.safeParse(data);
46
+ if (!parsed.success) {
47
+ throw parsed.error;
48
+ }
49
+ const map = new Map(
50
+ parsed.data.results.map((r) => [r.relativePath, r])
51
+ );
52
+ const ordered = [];
53
+ for (const f of files) {
54
+ const row = map.get(f.relativePath);
55
+ if (row === void 0) {
56
+ throw new Error(`Missing presigned URL for path: ${f.relativePath}`);
57
+ }
58
+ ordered.push(row);
59
+ }
60
+ return ordered;
61
+ }
62
+ var s3Client = axios.create({
63
+ maxBodyLength: Infinity,
64
+ maxContentLength: Infinity,
65
+ timeout: 0
66
+ });
67
+ async function putPresignedUpload(url, body, contentType) {
68
+ await s3Client.put(url, body, {
69
+ headers: { "Content-Type": contentType }
70
+ });
71
+ }
72
+
73
+ // src/core/files.ts
74
+ import { lookup } from "mime-types";
75
+ import { glob } from "tinyglobby";
76
+ async function listAllFilesUnderDir(rootAbs) {
77
+ return glob("**/*", {
78
+ cwd: rootAbs,
79
+ absolute: true,
80
+ onlyFiles: true,
81
+ dot: true,
82
+ followSymbolicLinks: false
83
+ });
84
+ }
85
+ function contentTypeForPath(filePath) {
86
+ const mime = lookup(filePath);
87
+ return mime === false ? "application/octet-stream" : mime;
88
+ }
89
+
90
+ // src/core/extension-upload.core.ts
91
+ var MAX_RELATIVE_PATH_LEN = 512;
92
+ var RELATIVE_PATH_SEGMENT = /^[a-zA-Z0-9._-]+$/;
93
+ var S3_PUT_MAX_ATTEMPTS = 3;
94
+ var S3_PUT_RETRY_BASE_MS = 500;
95
+ function normalizeDistPath(rawPath) {
96
+ return rawPath.replace(/\\/g, "/").trim().replace(/^(\.\/|\/)+/, "");
97
+ }
98
+ function validateUploadPath(relativePath) {
99
+ if (!relativePath) {
100
+ return "Path is empty.";
101
+ }
102
+ if (relativePath.includes("\\")) {
103
+ return "Path must use forward slashes only, not backslashes.";
104
+ }
105
+ if (relativePath.length > MAX_RELATIVE_PATH_LEN) {
106
+ return `Path exceeds max length (${MAX_RELATIVE_PATH_LEN}).`;
107
+ }
108
+ for (const segment of relativePath.split("/")) {
109
+ if (!segment || segment === "." || segment === "..") {
110
+ return `Invalid path segment "${segment}" (no empty, ".", or ".." segments).`;
111
+ }
112
+ if (!RELATIVE_PATH_SEGMENT.test(segment)) {
113
+ return `Invalid path segment "${segment}" (allowed: letters, digits, ".", "_", "-").`;
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+ async function collectDistUploadDescriptors(distRoot) {
119
+ const files = await listAllFilesUnderDir(distRoot);
120
+ if (files.length === 0) {
121
+ throw new Error(
122
+ `No files found under ${distRoot}. Build your app before uploading.`
123
+ );
124
+ }
125
+ const descriptorByRelativePath = /* @__PURE__ */ new Map();
126
+ for (const absolutePath of files) {
127
+ const relativePath = normalizeDistPath(relative(distRoot, absolutePath));
128
+ const validationError = validateUploadPath(relativePath);
129
+ if (validationError) {
130
+ throw new Error(`${basename(absolutePath)}: ${validationError}`);
131
+ }
132
+ if (descriptorByRelativePath.has(relativePath)) {
133
+ throw new Error(
134
+ `Duplicate upload path after normalization: ${relativePath}`
135
+ );
136
+ }
137
+ descriptorByRelativePath.set(relativePath, {
138
+ absolutePath,
139
+ relativePath,
140
+ contentType: contentTypeForPath(absolutePath)
141
+ });
142
+ }
143
+ return [...descriptorByRelativePath.values()].sort(
144
+ (a, b) => a.relativePath.localeCompare(b.relativePath)
145
+ );
146
+ }
147
+ async function putPresignedUploadWithRetry(url, body, contentType) {
148
+ let lastError;
149
+ for (let attempt = 1; attempt <= S3_PUT_MAX_ATTEMPTS; attempt++) {
150
+ try {
151
+ await putPresignedUpload(url, body, contentType);
152
+ return;
153
+ } catch (err) {
154
+ lastError = err;
155
+ if (attempt >= S3_PUT_MAX_ATTEMPTS) {
156
+ break;
157
+ }
158
+ await new Promise((r) => setTimeout(r, S3_PUT_RETRY_BASE_MS * attempt));
159
+ }
160
+ }
161
+ throw lastError;
162
+ }
163
+ async function runArtifactUpload(client, extensionId, batches, opts) {
164
+ const { total, onProgress } = opts;
165
+ let uploaded = 0;
166
+ let lastRelativePath = "";
167
+ const uploadedRelativePaths = [];
168
+ for (let b = 0; b < batches.length; b++) {
169
+ const batch = batches[b];
170
+ if (!batch?.length) {
171
+ continue;
172
+ }
173
+ const specs = batch.map((d) => ({
174
+ relativePath: d.relativePath,
175
+ contentType: d.contentType
176
+ }));
177
+ let signed;
178
+ try {
179
+ signed = await requestExtensionUploadSignedUrls(
180
+ client,
181
+ extensionId,
182
+ specs
183
+ );
184
+ } catch (e) {
185
+ return { ok: false, message: formatExtensionArtifactUploadError(e) };
186
+ }
187
+ for (let i = 0; i < batch.length; i++) {
188
+ const d = batch[i];
189
+ const row = signed[i];
190
+ if (!d || !row) {
191
+ return {
192
+ ok: false,
193
+ message: `Internal error: missing batch entry at index ${i} (batch ${b + 1}).`
194
+ };
195
+ }
196
+ if (row.relativePath !== d.relativePath) {
197
+ return {
198
+ ok: false,
199
+ failedPath: d.relativePath,
200
+ message: `Presigned URL order mismatch for ${d.relativePath} (batch ${b + 1}).`
201
+ };
202
+ }
203
+ lastRelativePath = d.relativePath;
204
+ onProgress({
205
+ completedSoFar: uploaded,
206
+ total,
207
+ activeRelativePath: d.relativePath,
208
+ uploadedRelativePaths: [...uploadedRelativePaths]
209
+ });
210
+ let body;
211
+ try {
212
+ body = await readFile(d.absolutePath);
213
+ } catch (e) {
214
+ return {
215
+ ok: false,
216
+ failedPath: d.relativePath,
217
+ message: e instanceof Error ? `Failed to read ${d.relativePath}: ${e.message}` : String(e)
218
+ };
219
+ }
220
+ try {
221
+ await putPresignedUploadWithRetry(row.url, body, row.contentType);
222
+ } catch (e) {
223
+ return {
224
+ ok: false,
225
+ failedPath: d.relativePath,
226
+ message: e instanceof Error ? `S3 upload failed for ${d.relativePath}: ${e.message}` : `S3 upload failed for ${d.relativePath}: ${String(e)}`
227
+ };
228
+ }
229
+ uploaded++;
230
+ uploadedRelativePaths.push(d.relativePath);
231
+ onProgress({
232
+ completedSoFar: uploaded,
233
+ total,
234
+ activeRelativePath: d.relativePath,
235
+ uploadedRelativePaths: [...uploadedRelativePaths]
236
+ });
237
+ }
238
+ }
239
+ onProgress({
240
+ completedSoFar: uploaded,
241
+ total,
242
+ activeRelativePath: total <= 1 ? lastRelativePath : `${uploaded} files uploaded`,
243
+ uploadedRelativePaths: [...uploadedRelativePaths]
244
+ });
245
+ return { ok: true, uploaded };
246
+ }
247
+
248
+ // src/ui/views/app-upload/AppUploadProgressView.tsx
249
+ import { Box, Text } from "ink";
250
+ import { useEffect, useRef, useState } from "react";
251
+
252
+ // src/ui/views/app-upload/verbose-upload-paths.ts
253
+ var VERBOSE_UPLOAD_PATH_TAIL_MAX = 300;
254
+ function formatVerboseUploadPaths(paths, maxTailLines = VERBOSE_UPLOAD_PATH_TAIL_MAX) {
255
+ if (paths.length === 0) {
256
+ return "";
257
+ }
258
+ if (paths.length <= maxTailLines) {
259
+ return paths.join("\n");
260
+ }
261
+ const omitted = paths.length - maxTailLines;
262
+ return `\u2026 ${omitted} earlier path(s) omitted
263
+ ${paths.slice(-maxTailLines).join("\n")}`;
264
+ }
265
+
266
+ // src/ui/views/app-upload/AppUploadProgressView.tsx
267
+ import { jsx, jsxs } from "react/jsx-runtime";
268
+ function ProgressBar({
269
+ done,
270
+ total,
271
+ width
272
+ }) {
273
+ const label = `${done}/${total}`;
274
+ const reserved = label.length + 2;
275
+ const barWidth = Math.max(2, width - reserved);
276
+ const inner = Math.min(barWidth, 40);
277
+ const filled = total === 0 ? 0 : Math.min(inner, Math.round(done / total * inner));
278
+ const bar = `${"\u2588".repeat(filled)}${"\u2591".repeat(inner - filled)}`;
279
+ return /* @__PURE__ */ jsxs(Text, { children: [
280
+ /* @__PURE__ */ jsx(Text, { ...textStyles.muted, children: bar }),
281
+ " ",
282
+ /* @__PURE__ */ jsx(Text, { bold: true, children: label })
283
+ ] });
284
+ }
285
+ function AppUploadProgressView({
286
+ client,
287
+ extensionId,
288
+ batches,
289
+ total,
290
+ verbose = false,
291
+ onDone
292
+ }) {
293
+ const termWidth = useTerminalWidth();
294
+ const layoutWidth = termWidth > 0 ? termWidth : "100%";
295
+ const progressBarWidth = termWidth > 0 ? termWidth : 80;
296
+ const [progress, setProgress] = useState(null);
297
+ const settledRef = useRef(false);
298
+ useEffect(() => {
299
+ settledRef.current = false;
300
+ let cancelled = false;
301
+ let finishTimer;
302
+ void runArtifactUpload(client, extensionId, batches, {
303
+ total,
304
+ onProgress: (p) => {
305
+ if (!cancelled) {
306
+ setProgress(p);
307
+ }
308
+ }
309
+ }).then((result) => {
310
+ if (cancelled) {
311
+ return;
312
+ }
313
+ if (!result.ok) {
314
+ if (cancelled || settledRef.current) {
315
+ return;
316
+ }
317
+ settledRef.current = true;
318
+ onDone({
319
+ kind: "error",
320
+ message: result.message,
321
+ failedPath: result.failedPath
322
+ });
323
+ return;
324
+ }
325
+ setProgress(
326
+ (prev) => prev ? { ...prev, completedSoFar: result.uploaded, total } : prev
327
+ );
328
+ finishTimer = setTimeout(() => {
329
+ if (cancelled || settledRef.current) {
330
+ return;
331
+ }
332
+ settledRef.current = true;
333
+ onDone({ kind: "success", uploaded: result.uploaded });
334
+ }, 0);
335
+ });
336
+ return () => {
337
+ cancelled = true;
338
+ if (finishTimer !== void 0) {
339
+ clearTimeout(finishTimer);
340
+ }
341
+ };
342
+ }, [batches, client, extensionId, onDone, total]);
343
+ const done = progress?.completedSoFar ?? 0;
344
+ const active = progress?.activeRelativePath ?? "\u2026";
345
+ const uploadedPaths = progress?.uploadedRelativePaths ?? [];
346
+ const showVerboseFileList = verbose && uploadedPaths.length > 0;
347
+ const verboseText = formatVerboseUploadPaths(uploadedPaths);
348
+ return /* @__PURE__ */ jsxs(Box, { width: layoutWidth, minWidth: 0, ...layoutStyles.viewColumn, children: [
349
+ /* @__PURE__ */ jsx(Box, { marginBottom: spacing.sm, children: /* @__PURE__ */ jsx(Text, { ...textStyles.title, children: "Upload extension artifacts" }) }),
350
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
351
+ /* @__PURE__ */ jsx(ProgressBar, { done, total, width: progressBarWidth }),
352
+ /* @__PURE__ */ jsx(Box, { marginTop: spacing.sm, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "white", children: active }) }),
353
+ showVerboseFileList ? /* @__PURE__ */ jsx(Box, { marginTop: spacing.xs, children: /* @__PURE__ */ jsx(Text, { ...textStyles.muted, wrap: "wrap", children: verboseText }) }) : null,
354
+ !progress ? /* @__PURE__ */ jsx(Box, { marginTop: spacing.sm, children: /* @__PURE__ */ jsx(Text, { ...textStyles.muted, children: "Preparing\u2026" }) }) : null
355
+ ] })
356
+ ] });
357
+ }
4
358
 
5
359
  // src/commands/app/upload.ts
6
360
  var AppUpload = class _AppUpload extends BaseCommand {
7
- static description = "Upload app files for review (coming soon)";
361
+ static description = "Upload build output to the extension draft.";
362
+ static flags = {
363
+ dist: Flags.string({
364
+ description: "Path to the build output directory (relative to cwd)",
365
+ default: "dist"
366
+ }),
367
+ verbose: Flags.boolean({
368
+ char: "v",
369
+ default: false,
370
+ description: "List each dist file path on its own line as uploads complete (no S3 keys)"
371
+ })
372
+ };
8
373
  async run() {
9
- await this.parse(_AppUpload);
10
- this.log("Not implemented yet.");
374
+ const { flags } = await this.parse(_AppUpload);
375
+ await this.ensureAuthenticated();
376
+ const cwd = process.cwd();
377
+ let extensionId;
378
+ try {
379
+ ({ extensionId } = await readInternalConfig(cwd));
380
+ } catch (e) {
381
+ this.error(e instanceof Error ? e.message : String(e), { exit: 2 });
382
+ }
383
+ const distAbs = resolve(cwd, flags.dist);
384
+ try {
385
+ const st = await stat(distAbs);
386
+ if (!st.isDirectory()) {
387
+ this.error(`Not a directory: ${distAbs}`, { exit: 2 });
388
+ }
389
+ } catch (e) {
390
+ if (isSystemError(e, "ENOENT")) {
391
+ this.error(
392
+ `Dist directory not found: ${distAbs}. Build your app first, or pass --dist.`,
393
+ { exit: 2 }
394
+ );
395
+ }
396
+ if (isSystemError(e, "EACCES") || isSystemError(e, "EPERM")) {
397
+ this.error(`Cannot access dist directory: ${distAbs}`, { exit: 2 });
398
+ }
399
+ if (isSystemError(e, "ENOTDIR")) {
400
+ this.error(`Invalid dist path (not a directory): ${distAbs}`, {
401
+ exit: 2
402
+ });
403
+ }
404
+ this.error(e instanceof Error ? e.message : String(e), { exit: 2 });
405
+ }
406
+ let descriptors;
407
+ try {
408
+ descriptors = await collectDistUploadDescriptors(distAbs);
409
+ } catch (e) {
410
+ this.error(e instanceof Error ? e.message : String(e), { exit: 2 });
411
+ }
412
+ const result = await this.renderView(AppUploadProgressView, {
413
+ client: this.getKittlApiClient(),
414
+ extensionId,
415
+ batches: chunkArray(descriptors, 50),
416
+ total: descriptors.length,
417
+ verbose: flags.verbose
418
+ });
419
+ if (result.kind === "error") {
420
+ const msg = result.failedPath ? `${result.failedPath}
421
+ ${result.message}` : result.message;
422
+ this.error(msg, { exit: 2 });
423
+ }
424
+ this.log(`Uploaded ${result.uploaded} file(s) to your extension draft.`);
11
425
  }
12
426
  };
13
427
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/commands/app/upload.ts"],"sourcesContent":["import { BaseCommand } from '../../base-command';\n\nexport default class AppUpload extends BaseCommand {\n public static override description =\n 'Upload app files for review (coming soon)';\n\n public async run(): Promise<void> {\n await this.parse(AppUpload);\n\n this.log('Not implemented yet.');\n }\n}\n"],"mappings":";;;;;AAEA,IAAqB,YAArB,MAAqB,mBAAkB,YAAY;AAAA,EACjD,OAAuB,cACrB;AAAA,EAEF,MAAa,MAAqB;AAChC,UAAM,KAAK,MAAM,UAAS;AAE1B,SAAK,IAAI,sBAAsB;AAAA,EACjC;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../src/commands/app/upload.ts","../../../src/core/extension-upload.core.ts","../../../src/services/extension-artifacts.service.ts","../../../src/core/files.ts","../../../src/ui/views/app-upload/AppUploadProgressView.tsx","../../../src/ui/views/app-upload/verbose-upload-paths.ts"],"sourcesContent":["import { stat } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { Flags } from '@oclif/core';\nimport { BaseCommand } from '../../core/core.command';\nimport { isSystemError } from '../../core/error';\nimport {\n collectDistUploadDescriptors,\n type DistUploadFileDescriptor,\n} from '../../core/extension-upload.core';\nimport { readInternalConfig } from '../../core/internal.config';\nimport { chunkArray } from '../../core/utils';\nimport { AppUploadProgressView } from '../../ui/views/app-upload';\n\nexport default class AppUpload extends BaseCommand {\n public static override description =\n 'Upload build output to the extension draft.';\n\n public static override flags = {\n dist: Flags.string({\n description: 'Path to the build output directory (relative to cwd)',\n default: 'dist',\n }),\n verbose: Flags.boolean({\n char: 'v',\n default: false,\n description:\n 'List each dist file path on its own line as uploads complete (no S3 keys)',\n }),\n };\n\n public async run(): Promise<void> {\n const { flags } = await this.parse(AppUpload);\n\n await this.ensureAuthenticated();\n\n const cwd = process.cwd();\n\n let extensionId: string;\n try {\n ({ extensionId } = await readInternalConfig(cwd));\n } catch (e) {\n this.error(e instanceof Error ? e.message : String(e), { exit: 2 });\n }\n\n const distAbs = resolve(cwd, flags.dist);\n try {\n const st = await stat(distAbs);\n if (!st.isDirectory()) {\n this.error(`Not a directory: ${distAbs}`, { exit: 2 });\n }\n } catch (e) {\n if (isSystemError(e, 'ENOENT')) {\n this.error(\n `Dist directory not found: ${distAbs}. Build your app first, or pass --dist.`,\n { exit: 2 },\n );\n }\n if (isSystemError(e, 'EACCES') || isSystemError(e, 'EPERM')) {\n this.error(`Cannot access dist directory: ${distAbs}`, { exit: 2 });\n }\n if (isSystemError(e, 'ENOTDIR')) {\n this.error(`Invalid dist path (not a directory): ${distAbs}`, {\n exit: 2,\n });\n }\n this.error(e instanceof Error ? e.message : String(e), { exit: 2 });\n }\n\n let descriptors: DistUploadFileDescriptor[];\n try {\n descriptors = await collectDistUploadDescriptors(distAbs);\n } catch (e) {\n this.error(e instanceof Error ? e.message : String(e), { exit: 2 });\n }\n\n const result = await this.renderView(AppUploadProgressView, {\n client: this.getKittlApiClient(),\n extensionId,\n batches: chunkArray(descriptors, 50),\n total: descriptors.length,\n verbose: flags.verbose,\n });\n\n if (result.kind === 'error') {\n const msg = result.failedPath\n ? `${result.failedPath}\\n${result.message}`\n : result.message;\n this.error(msg, { exit: 2 });\n }\n\n this.log(`Uploaded ${result.uploaded} file(s) to your extension draft.`);\n }\n}\n","import { readFile } from 'node:fs/promises';\nimport { basename, relative } from 'node:path';\nimport type { AxiosInstance } from 'axios';\nimport {\n type ExtensionUploadSignedUrlRow,\n putPresignedUpload,\n requestExtensionUploadSignedUrls,\n} from '../services/extension-artifacts.service';\nimport { formatExtensionArtifactUploadError } from './error';\nimport { contentTypeForPath, listAllFilesUnderDir } from './files';\n\nconst MAX_RELATIVE_PATH_LEN = 512;\nconst RELATIVE_PATH_SEGMENT = /^[a-zA-Z0-9._-]+$/;\nconst S3_PUT_MAX_ATTEMPTS = 3;\nconst S3_PUT_RETRY_BASE_MS = 500;\n\nexport type DistUploadFileDescriptor = {\n absolutePath: string;\n relativePath: string;\n contentType: string;\n};\n\nexport type ArtifactUploadProgress = {\n completedSoFar: number;\n total: number;\n // path currently being read or uploaded.\n activeRelativePath: string;\n // paths uploaded so far (same order as uploads).\n uploadedRelativePaths: string[];\n};\n\nexport type ArtifactUploadResult =\n | { ok: true; uploaded: number }\n | { ok: false; message: string; failedPath?: string };\n\n// --- Path + MIME (upload-signed-urls contract) ---------------------------------------------------\n\n/** POSIX slashes, trim, strip leading `./` and `/` (single pass after `path.relative`). */\nexport function normalizeDistPath(rawPath: string): string {\n return rawPath\n .replace(/\\\\/g, '/')\n .trim()\n .replace(/^(\\.\\/|\\/)+/, '');\n}\n\n/** API path validation (segments, length, traversal). */\nexport function validateUploadPath(relativePath: string): string | null {\n if (!relativePath) {\n return 'Path is empty.';\n }\n if (relativePath.includes('\\\\')) {\n return 'Path must use forward slashes only, not backslashes.';\n }\n if (relativePath.length > MAX_RELATIVE_PATH_LEN) {\n return `Path exceeds max length (${MAX_RELATIVE_PATH_LEN}).`;\n }\n for (const segment of relativePath.split('/')) {\n if (!segment || segment === '.' || segment === '..') {\n return `Invalid path segment \"${segment}\" (no empty, \".\", or \"..\" segments).`;\n }\n if (!RELATIVE_PATH_SEGMENT.test(segment)) {\n return `Invalid path segment \"${segment}\" (allowed: letters, digits, \".\", \"_\", \"-\").`;\n }\n }\n return null;\n}\n\n/**\n * Lists `distRoot` recursively, validates paths, dedupes, sorts for stable progress / API order.\n */\nexport async function collectDistUploadDescriptors(\n distRoot: string,\n): Promise<DistUploadFileDescriptor[]> {\n const files = await listAllFilesUnderDir(distRoot);\n\n if (files.length === 0) {\n throw new Error(\n `No files found under ${distRoot}. Build your app before uploading.`,\n );\n }\n\n const descriptorByRelativePath = new Map<string, DistUploadFileDescriptor>();\n\n for (const absolutePath of files) {\n const relativePath = normalizeDistPath(relative(distRoot, absolutePath));\n const validationError = validateUploadPath(relativePath);\n if (validationError) {\n throw new Error(`${basename(absolutePath)}: ${validationError}`);\n }\n if (descriptorByRelativePath.has(relativePath)) {\n throw new Error(\n `Duplicate upload path after normalization: ${relativePath}`,\n );\n }\n descriptorByRelativePath.set(relativePath, {\n absolutePath,\n relativePath,\n contentType: contentTypeForPath(absolutePath),\n });\n }\n\n return [...descriptorByRelativePath.values()].sort((a, b) =>\n a.relativePath.localeCompare(b.relativePath),\n );\n}\n\nasync function putPresignedUploadWithRetry(\n url: string,\n body: Buffer,\n contentType: string,\n): Promise<void> {\n let lastError: unknown;\n for (let attempt = 1; attempt <= S3_PUT_MAX_ATTEMPTS; attempt++) {\n try {\n await putPresignedUpload(url, body, contentType);\n return;\n } catch (err) {\n lastError = err;\n if (attempt >= S3_PUT_MAX_ATTEMPTS) {\n break;\n }\n await new Promise((r) => setTimeout(r, S3_PUT_RETRY_BASE_MS * attempt));\n }\n }\n throw lastError;\n}\n\n// --- Presigned batch upload ----------------------------------------------------------------------\n\n/**\n * Presigned URL batching + S3 PUTs with progress callbacks for Ink UI.\n * S3 PUTs retry with linear backoff.\n */\nexport async function runArtifactUpload(\n client: AxiosInstance,\n extensionId: string,\n batches: DistUploadFileDescriptor[][],\n opts: {\n total: number;\n onProgress: (p: ArtifactUploadProgress) => void;\n },\n): Promise<ArtifactUploadResult> {\n const { total, onProgress } = opts;\n let uploaded = 0;\n let lastRelativePath = '';\n const uploadedRelativePaths: string[] = [];\n\n for (let b = 0; b < batches.length; b++) {\n const batch = batches[b];\n if (!batch?.length) {\n continue;\n }\n const specs = batch.map((d) => ({\n relativePath: d.relativePath,\n contentType: d.contentType,\n }));\n let signed: ExtensionUploadSignedUrlRow[];\n try {\n signed = await requestExtensionUploadSignedUrls(\n client,\n extensionId,\n specs,\n );\n } catch (e) {\n return { ok: false, message: formatExtensionArtifactUploadError(e) };\n }\n\n for (let i = 0; i < batch.length; i++) {\n const d = batch[i];\n const row = signed[i];\n if (!d || !row) {\n return {\n ok: false,\n message: `Internal error: missing batch entry at index ${i} (batch ${b + 1}).`,\n };\n }\n if (row.relativePath !== d.relativePath) {\n return {\n ok: false,\n failedPath: d.relativePath,\n message: `Presigned URL order mismatch for ${d.relativePath} (batch ${b + 1}).`,\n };\n }\n\n lastRelativePath = d.relativePath;\n\n onProgress({\n completedSoFar: uploaded,\n total,\n activeRelativePath: d.relativePath,\n uploadedRelativePaths: [...uploadedRelativePaths],\n });\n\n let body: Buffer;\n try {\n body = await readFile(d.absolutePath);\n } catch (e) {\n return {\n ok: false,\n failedPath: d.relativePath,\n message:\n e instanceof Error\n ? `Failed to read ${d.relativePath}: ${e.message}`\n : String(e),\n };\n }\n\n try {\n await putPresignedUploadWithRetry(row.url, body, row.contentType);\n } catch (e) {\n return {\n ok: false,\n failedPath: d.relativePath,\n message:\n e instanceof Error\n ? `S3 upload failed for ${d.relativePath}: ${e.message}`\n : `S3 upload failed for ${d.relativePath}: ${String(e)}`,\n };\n }\n\n uploaded++;\n uploadedRelativePaths.push(d.relativePath);\n onProgress({\n completedSoFar: uploaded,\n total,\n activeRelativePath: d.relativePath,\n uploadedRelativePaths: [...uploadedRelativePaths],\n });\n }\n }\n\n onProgress({\n completedSoFar: uploaded,\n total,\n activeRelativePath:\n total <= 1 ? lastRelativePath : `${uploaded} files uploaded`,\n uploadedRelativePaths: [...uploadedRelativePaths],\n });\n return { ok: true, uploaded };\n}\n","import type { AxiosInstance } from 'axios';\nimport axios from 'axios';\nimport { z } from 'zod';\n\nconst uploadSignedUrlsResponseSchema = z.object({\n success: z.literal(true),\n results: z.array(\n z.object({\n url: z.string().min(1),\n key: z.string(),\n relativePath: z.string(),\n contentType: z.string().min(1),\n }),\n ),\n});\n\nexport type UploadSignedUrlFileSpec = {\n relativePath: string;\n contentType: string;\n};\n\nexport type ExtensionUploadSignedUrlRow = {\n url: string;\n key: string;\n relativePath: string;\n contentType: string;\n};\n\nexport async function requestExtensionUploadSignedUrls(\n client: AxiosInstance,\n extensionId: string,\n files: UploadSignedUrlFileSpec[],\n): Promise<ExtensionUploadSignedUrlRow[]> {\n const path = `/extensions/${extensionId}/versions/upload-signed-urls`;\n const { data } = await client.post(path, { files });\n const parsed = uploadSignedUrlsResponseSchema.safeParse(data);\n if (!parsed.success) {\n throw parsed.error;\n }\n const map = new Map(\n parsed.data.results.map((r) => [r.relativePath, r] as const),\n );\n const ordered: ExtensionUploadSignedUrlRow[] = [];\n for (const f of files) {\n const row = map.get(f.relativePath);\n if (row === undefined) {\n throw new Error(`Missing presigned URL for path: ${f.relativePath}`);\n }\n ordered.push(row);\n }\n return ordered;\n}\n\n/**\n * Dedicated Axios instance for presigned S3 PUT requests only.\n * `maxBodyLength` / `maxContentLength`: avoid Axios body-size caps on large assets.\n * `timeout: 0`: no Axios deadline on slow uploads (TCP / network still apply).\n */\nconst s3Client = axios.create({\n maxBodyLength: Infinity,\n maxContentLength: Infinity,\n timeout: 0,\n});\n\n/**\n * PUT raw bytes to a presigned URL using {@link s3Client} only.\n * `contentType` must match the value the API used when signing, byte-for-byte, or S3 returns 403.\n */\nexport async function putPresignedUpload(\n url: string,\n body: Buffer,\n contentType: string,\n): Promise<void> {\n await s3Client.put(url, body, {\n headers: { 'Content-Type': contentType },\n });\n}\n","import { lookup } from 'mime-types';\nimport { glob } from 'tinyglobby';\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 type { AxiosInstance } from 'axios';\nimport { Box, Text } from 'ink';\nimport { useEffect, useRef, useState } from 'react';\nimport {\n type ArtifactUploadProgress,\n type DistUploadFileDescriptor,\n runArtifactUpload,\n} from '../../../core/extension-upload.core';\nimport { useTerminalWidth } from '../../hooks';\nimport { layoutStyles, textStyles } from '../../theme/styles';\nimport { spacing } from '../../theme/tokens';\nimport { formatVerboseUploadPaths } from './verbose-upload-paths';\n\nexport type AppUploadProgressResult =\n | { kind: 'success'; uploaded: number }\n | { kind: 'error'; message: string; failedPath?: string };\n\nexport type AppUploadProgressViewProps = {\n client: AxiosInstance;\n extensionId: string;\n batches: DistUploadFileDescriptor[][];\n total: number;\n // When true, completed dist paths are listed one per line\n verbose?: boolean;\n onDone: (result: AppUploadProgressResult) => void;\n};\n\nfunction ProgressBar({\n done,\n total,\n width,\n}: {\n done: number;\n total: number;\n width: number;\n}) {\n const label = `${done}/${total}`;\n const reserved = label.length + 2;\n const barWidth = Math.max(2, width - reserved);\n const inner = Math.min(barWidth, 40);\n const filled =\n total === 0 ? 0 : Math.min(inner, Math.round((done / total) * inner));\n const bar = `${'█'.repeat(filled)}${'░'.repeat(inner - filled)}`;\n return (\n <Text>\n <Text {...textStyles.muted}>{bar}</Text> <Text bold>{label}</Text>\n </Text>\n );\n}\n\nexport function AppUploadProgressView({\n client,\n extensionId,\n batches,\n total,\n verbose = false,\n onDone,\n}: AppUploadProgressViewProps) {\n const termWidth = useTerminalWidth();\n const layoutWidth = termWidth > 0 ? termWidth : '100%';\n const progressBarWidth = termWidth > 0 ? termWidth : 80;\n const [progress, setProgress] = useState<ArtifactUploadProgress | null>(null);\n const settledRef = useRef(false);\n\n useEffect(() => {\n settledRef.current = false;\n let cancelled = false;\n let finishTimer: ReturnType<typeof setTimeout> | undefined;\n\n void runArtifactUpload(client, extensionId, batches, {\n total,\n onProgress: (p) => {\n if (!cancelled) {\n setProgress(p);\n }\n },\n }).then((result) => {\n if (cancelled) {\n return;\n }\n if (!result.ok) {\n if (cancelled || settledRef.current) {\n return;\n }\n settledRef.current = true;\n onDone({\n kind: 'error',\n message: result.message,\n failedPath: result.failedPath,\n });\n return;\n }\n setProgress((prev) =>\n prev ? { ...prev, completedSoFar: result.uploaded, total } : prev,\n );\n finishTimer = setTimeout(() => {\n if (cancelled || settledRef.current) {\n return;\n }\n settledRef.current = true;\n onDone({ kind: 'success', uploaded: result.uploaded });\n }, 0);\n });\n\n return () => {\n cancelled = true;\n if (finishTimer !== undefined) {\n clearTimeout(finishTimer);\n }\n };\n }, [batches, client, extensionId, onDone, total]);\n\n const done = progress?.completedSoFar ?? 0;\n const active = progress?.activeRelativePath ?? '…';\n const uploadedPaths = progress?.uploadedRelativePaths ?? [];\n const showVerboseFileList = verbose && uploadedPaths.length > 0;\n const verboseText = formatVerboseUploadPaths(uploadedPaths);\n\n return (\n <Box width={layoutWidth} minWidth={0} {...layoutStyles.viewColumn}>\n <Box marginBottom={spacing.sm}>\n <Text {...textStyles.title}>Upload extension artifacts</Text>\n </Box>\n\n <Box flexDirection=\"column\">\n <ProgressBar done={done} total={total} width={progressBarWidth} />\n <Box marginTop={spacing.sm}>\n <Text bold color=\"white\">\n {active}\n </Text>\n </Box>\n {showVerboseFileList ? (\n <Box marginTop={spacing.xs}>\n <Text {...textStyles.muted} wrap=\"wrap\">\n {verboseText}\n </Text>\n </Box>\n ) : null}\n {!progress ? (\n <Box marginTop={spacing.sm}>\n <Text {...textStyles.muted}>Preparing…</Text>\n </Box>\n ) : null}\n </Box>\n </Box>\n );\n}\n","/** Tail cap for verbose CLI output (one multiline Text, not N Ink nodes). */\nexport const VERBOSE_UPLOAD_PATH_TAIL_MAX = 300;\n\n/**\n * Renders completed dist paths as newline-separated text; when there are more\n * than `maxTailLines`, keeps only the last `maxTailLines` and prefixes a\n * one-line omission summary.\n */\nexport function formatVerboseUploadPaths(\n paths: string[],\n maxTailLines = VERBOSE_UPLOAD_PATH_TAIL_MAX,\n): string {\n if (paths.length === 0) {\n return '';\n }\n if (paths.length <= maxTailLines) {\n return paths.join('\\n');\n }\n const omitted = paths.length - maxTailLines;\n return `… ${omitted} earlier path(s) omitted\\n${paths.slice(-maxTailLines).join('\\n')}`;\n}\n"],"mappings":"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","names":[]}
@@ -1,62 +1,23 @@
1
+ import {
2
+ useTerminalWidth
3
+ } from "../../chunk-EKU4DKQK.js";
4
+ import {
5
+ layoutStyles,
6
+ textStyles
7
+ } from "../../chunk-3BPIJLS7.js";
1
8
  import {
2
9
  BaseCommand,
3
10
  INK_VIEW_UNMOUNT_REASON,
4
11
  authService
5
- } from "../../chunk-JGD3QFQS.js";
12
+ } from "../../chunk-TK44DTSK.js";
6
13
 
7
- // src/ui/views/LoginView.tsx
14
+ // src/ui/views/login/LoginView.tsx
8
15
  import { Box, Text as Text2 } from "ink";
9
- import { useCallback, useEffect as useEffect3, useRef, useState as useState3 } from "react";
16
+ import { useCallback, useEffect as useEffect2, useRef, useState as useState2 } from "react";
10
17
 
11
18
  // src/ui/components/InlineSpinner.tsx
12
19
  import { Text } from "ink";
13
20
  import { useEffect, useState } from "react";
14
-
15
- // src/ui/theme/tokens.ts
16
- var colors = {
17
- textPrimary: "white",
18
- textMuted: "gray",
19
- success: "green",
20
- warning: "yellow",
21
- danger: "red",
22
- accent: "cyan"
23
- };
24
- var spacing = {
25
- xs: 0,
26
- sm: 1,
27
- md: 2
28
- };
29
-
30
- // src/ui/theme/styles.ts
31
- var layoutStyles = {
32
- viewColumn: {
33
- flexDirection: "column",
34
- padding: spacing.sm
35
- },
36
- section: {
37
- marginTop: spacing.sm
38
- }
39
- };
40
- var textStyles = {
41
- title: {
42
- bold: true,
43
- color: colors.accent
44
- },
45
- muted: {
46
- color: colors.textMuted
47
- },
48
- success: {
49
- color: colors.success
50
- },
51
- warning: {
52
- color: colors.warning
53
- },
54
- error: {
55
- color: colors.danger
56
- }
57
- };
58
-
59
- // src/ui/components/InlineSpinner.tsx
60
21
  import { jsxs } from "react/jsx-runtime";
61
22
  var frames = ["-", "\\", "|", "/"];
62
23
  function InlineSpinner({ label }) {
@@ -74,30 +35,14 @@ function InlineSpinner({ label }) {
74
35
  ] });
75
36
  }
76
37
 
77
- // src/ui/hooks/useTerminalWidth.ts
78
- import { useStdout } from "ink";
79
- import { useEffect as useEffect2, useState as useState2 } from "react";
80
- function useTerminalWidth() {
81
- const { stdout } = useStdout();
82
- const [width, setWidth] = useState2(() => stdout.columns ?? 80);
83
- useEffect2(() => {
84
- const onResize = () => setWidth(stdout.columns ?? 80);
85
- stdout.on("resize", onResize);
86
- return () => {
87
- stdout.off("resize", onResize);
88
- };
89
- }, [stdout]);
90
- return width;
91
- }
92
-
93
- // src/ui/views/LoginView.tsx
38
+ // src/ui/views/login/LoginView.tsx
94
39
  import { jsx, jsxs as jsxs2 } from "react/jsx-runtime";
95
40
  var CHECKING_SESSION = "Checking session\u2026";
96
41
  var SIGNING_IN = "Signing in\u2026";
97
42
  function LoginView({ onDone }) {
98
43
  const width = useTerminalWidth();
99
44
  const doneRef = useRef(false);
100
- const [phase, setPhase] = useState3("checking");
45
+ const [phase, setPhase] = useState2("checking");
101
46
  const finish = useCallback(
102
47
  (result, force = false) => {
103
48
  if (doneRef.current && !force)
@@ -107,7 +52,7 @@ function LoginView({ onDone }) {
107
52
  },
108
53
  [onDone]
109
54
  );
110
- useEffect3(() => {
55
+ useEffect2(() => {
111
56
  doneRef.current = false;
112
57
  const ac = new AbortController();
113
58
  void (async () => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/ui/views/LoginView.tsx","../../../src/ui/components/InlineSpinner.tsx","../../../src/ui/theme/tokens.ts","../../../src/ui/theme/styles.ts","../../../src/ui/hooks/useTerminalWidth.ts","../../../src/commands/auth/login.ts"],"sourcesContent":["import { Box, Text } from 'ink';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { authService } from '../../services/auth.service';\nimport { INK_VIEW_UNMOUNT_REASON } from '../../utils';\nimport { InlineSpinner } from '../components/InlineSpinner';\nimport { useTerminalWidth } from '../hooks';\nimport { layoutStyles, textStyles } from '../theme/styles';\n\nconst CHECKING_SESSION = 'Checking session…';\nconst SIGNING_IN = 'Signing in…';\n\nexport type LoginResult =\n | { kind: 'success'; reusedSession?: boolean }\n | { kind: 'cancelled' }\n | { kind: 'error'; error: Error };\n\nexport type LoginViewProps = {\n onDone: (result: LoginResult) => void;\n};\n\n/**\n * OAuth login TUI — only live feedback (spinner). Final outcome is owned by the command via `onDone` + `this.log`.\n * **Unmount:** aborts the local `AbortController` so discovery + the callback server stop cleanly.\n */\nexport function LoginView({ onDone }: LoginViewProps) {\n const width = useTerminalWidth();\n const doneRef = useRef(false);\n const [phase, setPhase] = useState<'checking' | 'signing-in'>('checking');\n\n const finish = useCallback(\n (result: LoginResult, force = false) => {\n if (doneRef.current && !force) return;\n doneRef.current = true;\n onDone(result);\n },\n [onDone],\n );\n\n useEffect(() => {\n doneRef.current = false;\n const ac = new AbortController();\n\n void (async () => {\n try {\n const existing = await authService.getSession();\n if (ac.signal.aborted) {\n finish({ kind: 'cancelled' }, true);\n return;\n }\n if (existing?.accessToken) {\n finish({ kind: 'success', reusedSession: true });\n return;\n }\n\n setPhase('signing-in');\n await authService.login({ signal: ac.signal });\n finish({ kind: 'success' });\n } catch (error) {\n if (error === INK_VIEW_UNMOUNT_REASON) {\n finish({ kind: 'cancelled' }, true);\n return;\n }\n if (doneRef.current) return;\n finish({\n kind: 'error',\n error: error instanceof Error ? error : new Error('Auth failed'),\n });\n }\n })();\n\n return () => {\n doneRef.current = true;\n ac.abort(INK_VIEW_UNMOUNT_REASON);\n };\n }, [finish]);\n\n return (\n <Box\n {...layoutStyles.viewColumn}\n width={width > 0 ? width : '100%'}\n minWidth={0}\n >\n <Text {...textStyles.title}>Kittl CLI Authentication</Text>\n <Box {...layoutStyles.section}>\n <InlineSpinner\n label={phase === 'checking' ? CHECKING_SESSION : SIGNING_IN}\n />\n </Box>\n </Box>\n );\n}\n","import { Text } from 'ink';\nimport { useEffect, useState } from 'react';\nimport { textStyles } from '../theme/styles';\n\nexport type InlineSpinnerProps = {\n label: string;\n};\nconst frames = ['-', '\\\\', '|', '/'];\nexport function InlineSpinner({ label }: InlineSpinnerProps) {\n const [frameIndex, setFrameIndex] = useState(0);\n\n useEffect(() => {\n const timer = setInterval(() => {\n setFrameIndex((current) => (current + 1) % frames.length);\n }, 80);\n return () => clearInterval(timer);\n }, []);\n\n return (\n <Text {...textStyles.muted}>\n {frames[frameIndex]} {label}\n </Text>\n );\n}\n","export const colors = {\n textPrimary: 'white',\n textMuted: 'gray',\n success: 'green',\n warning: 'yellow',\n danger: 'red',\n accent: 'cyan',\n} as const;\n\nexport const spacing = {\n xs: 0,\n sm: 1,\n md: 2,\n} as const;\n","import { colors, spacing } from './tokens';\n\nexport const layoutStyles = {\n viewColumn: {\n flexDirection: 'column' as const,\n padding: spacing.sm,\n },\n section: {\n marginTop: spacing.sm,\n },\n};\n\nexport const textStyles = {\n title: {\n bold: true,\n color: colors.accent,\n },\n muted: {\n color: colors.textMuted,\n },\n success: {\n color: colors.success,\n },\n warning: {\n color: colors.warning,\n },\n error: {\n color: colors.danger,\n },\n};\n","import { useStdout } from 'ink';\nimport { useEffect, useState } from 'react';\n\nexport function useTerminalWidth(): number {\n const { stdout } = useStdout();\n const [width, setWidth] = useState(() => stdout.columns ?? 80);\n\n useEffect(() => {\n const onResize = () => setWidth(stdout.columns ?? 80);\n stdout.on('resize', onResize);\n return () => {\n stdout.off('resize', onResize);\n };\n }, [stdout]);\n\n return width;\n}\n","import { BaseCommand } from '../../base-command';\nimport { type LoginResult, LoginView } from '../../ui/views/LoginView';\n\nexport default class AuthLogin extends BaseCommand {\n public static description = 'Authenticate with your Kittl account';\n\n public async run(): Promise<void> {\n await this.parse(AuthLogin);\n\n const result = await this.renderView<LoginResult>(LoginView);\n\n if (result.kind === 'cancelled') {\n this.exit(130);\n }\n if (result.kind === 'error') {\n throw result.error;\n }\n\n if (result.reusedSession) {\n this.log('Already signed in.');\n } else {\n this.log('Signed in successfully.');\n }\n }\n}\n"],"mappings":"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","names":["Text","useEffect","useState","useEffect","useState","jsxs","useState","useEffect","Text"]}
1
+ {"version":3,"sources":["../../../src/ui/views/login/LoginView.tsx","../../../src/ui/components/InlineSpinner.tsx","../../../src/commands/auth/login.ts"],"sourcesContent":["import { Box, Text } from 'ink';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { INK_VIEW_UNMOUNT_REASON } from '../../../core/utils';\nimport { authService } from '../../../services/auth.service';\nimport { InlineSpinner } from '../../components/InlineSpinner';\nimport { useTerminalWidth } from '../../hooks';\nimport { layoutStyles, textStyles } from '../../theme/styles';\n\nconst CHECKING_SESSION = 'Checking session…';\nconst SIGNING_IN = 'Signing in…';\n\nexport type LoginResult =\n | { kind: 'success'; reusedSession?: boolean }\n | { kind: 'cancelled' }\n | { kind: 'error'; error: Error };\n\nexport type LoginViewProps = {\n onDone: (result: LoginResult) => void;\n};\n\n/**\n * OAuth login TUI.\n * **Unmount:** aborts the local `AbortController` so discovery + the callback server stop cleanly.\n */\nexport function LoginView({ onDone }: LoginViewProps) {\n const width = useTerminalWidth();\n const doneRef = useRef(false);\n const [phase, setPhase] = useState<'checking' | 'signing-in'>('checking');\n\n const finish = useCallback(\n (result: LoginResult, force = false) => {\n if (doneRef.current && !force) return;\n doneRef.current = true;\n onDone(result);\n },\n [onDone],\n );\n\n useEffect(() => {\n doneRef.current = false;\n const ac = new AbortController();\n\n void (async () => {\n try {\n const existing = await authService.getSession();\n if (ac.signal.aborted) {\n finish({ kind: 'cancelled' }, true);\n return;\n }\n if (existing?.accessToken) {\n finish({ kind: 'success', reusedSession: true });\n return;\n }\n\n setPhase('signing-in');\n await authService.login({ signal: ac.signal });\n finish({ kind: 'success' });\n } catch (error) {\n if (error === INK_VIEW_UNMOUNT_REASON) {\n finish({ kind: 'cancelled' }, true);\n return;\n }\n if (doneRef.current) return;\n finish({\n kind: 'error',\n error: error instanceof Error ? error : new Error('Auth failed'),\n });\n }\n })();\n\n return () => {\n doneRef.current = true;\n ac.abort(INK_VIEW_UNMOUNT_REASON);\n };\n }, [finish]);\n\n return (\n <Box\n {...layoutStyles.viewColumn}\n width={width > 0 ? width : '100%'}\n minWidth={0}\n >\n <Text {...textStyles.title}>Kittl CLI Authentication</Text>\n <Box {...layoutStyles.section}>\n <InlineSpinner\n label={phase === 'checking' ? CHECKING_SESSION : SIGNING_IN}\n />\n </Box>\n </Box>\n );\n}\n","import { Text } from 'ink';\nimport { useEffect, useState } from 'react';\nimport { textStyles } from '../theme/styles';\n\nexport type InlineSpinnerProps = {\n label: string;\n};\nconst frames = ['-', '\\\\', '|', '/'];\nexport function InlineSpinner({ label }: InlineSpinnerProps) {\n const [frameIndex, setFrameIndex] = useState(0);\n\n useEffect(() => {\n const timer = setInterval(() => {\n setFrameIndex((current) => (current + 1) % frames.length);\n }, 80);\n return () => clearInterval(timer);\n }, []);\n\n return (\n <Text {...textStyles.muted}>\n {frames[frameIndex]} {label}\n </Text>\n );\n}\n","import { BaseCommand } from '../../core/core.command';\nimport { LoginView } from '../../ui/views/login';\n\nexport default class AuthLogin extends BaseCommand {\n public static description = 'Authenticate with your Kittl account';\n\n public async run(): Promise<void> {\n await this.parse(AuthLogin);\n\n const result = await this.renderView(LoginView);\n\n if (result.kind === 'cancelled') {\n this.exit(130);\n }\n if (result.kind === 'error') {\n throw result.error;\n }\n\n if (result.reusedSession) {\n this.log('Already signed in.');\n } else {\n this.log('Signed in successfully.');\n }\n }\n}\n"],"mappings":"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","names":["Text","useEffect","useState","jsxs","useState","useEffect","Text"]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  BaseCommand,
3
3
  authService
4
- } from "../../chunk-JGD3QFQS.js";
4
+ } from "../../chunk-TK44DTSK.js";
5
5
 
6
6
  // src/commands/auth/logout.ts
7
7
  var AuthLogout = class _AuthLogout extends BaseCommand {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/commands/auth/logout.ts"],"sourcesContent":["import { BaseCommand } from '../../base-command';\nimport { authService } from '../../services/auth.service';\n\nexport default class AuthLogout extends BaseCommand {\n public static override description = 'Clear local Kittl CLI session';\n\n public async run(): Promise<void> {\n await this.parse(AuthLogout);\n\n await authService.logout();\n this.log('Logged out.');\n }\n}\n"],"mappings":";;;;;;AAGA,IAAqB,aAArB,MAAqB,oBAAmB,YAAY;AAAA,EAClD,OAAuB,cAAc;AAAA,EAErC,MAAa,MAAqB;AAChC,UAAM,KAAK,MAAM,WAAU;AAE3B,UAAM,YAAY,OAAO;AACzB,SAAK,IAAI,aAAa;AAAA,EACxB;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../src/commands/auth/logout.ts"],"sourcesContent":["import { BaseCommand } from '../../core/core.command';\nimport { authService } from '../../services/auth.service';\n\nexport default class AuthLogout extends BaseCommand {\n public static override description = 'Clear local Kittl CLI session';\n\n public async run(): Promise<void> {\n await this.parse(AuthLogout);\n\n await authService.logout();\n this.log('Logged out.');\n }\n}\n"],"mappings":";;;;;;AAGA,IAAqB,aAArB,MAAqB,oBAAmB,YAAY;AAAA,EAClD,OAAuB,cAAc;AAAA,EAErC,MAAa,MAAqB;AAChC,UAAM,KAAK,MAAM,WAAU;AAE3B,UAAM,YAAY,OAAO;AACzB,SAAK,IAAI,aAAa;AAAA,EACxB;AACF;","names":[]}