withframe 0.0.1

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.
Files changed (3) hide show
  1. package/README.md +93 -0
  2. package/dist/index.mjs +1756 -0
  3. package/package.json +69 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,1756 @@
1
+ #!/usr/bin/env node
2
+ import chalk from "chalk";
3
+ import { Command } from "commander";
4
+ import { InjectionMode, asClass, asFunction, asValue, createContainer } from "awilix";
5
+ import open from "open";
6
+ import { access, chmod, mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { spawn } from "node:child_process";
9
+ import { input, select } from "@inquirer/prompts";
10
+ import ora from "ora";
11
+ import { readFileSync } from "node:fs";
12
+ import os from "node:os";
13
+ import figlet from "figlet";
14
+ //#region src/constants/index.ts
15
+ const CLI_NAME = "withframe";
16
+ const CLI_VERSION = "1.0.0";
17
+ const REGISTRY_URL = "https://withfra.me";
18
+ const WITHFRAME_DIR = ".withframe";
19
+ const AUTH_FILE_NAME = "auth.json";
20
+ const CONFIG_FILE_NAME = "withframe.config.json";
21
+ const SEPARATOR = " ───────────────────────────────────────";
22
+ const COLORS = {
23
+ PRIMARY_50: "#edf8ff",
24
+ PRIMARY_100: "#d7edff",
25
+ PRIMARY_200: "#b9e0ff",
26
+ PRIMARY_300: "#88cfff",
27
+ PRIMARY_400: "#50b3ff",
28
+ PRIMARY_500: "#2890ff",
29
+ PRIMARY_600: "#0469ff",
30
+ PRIMARY_700: "#0a59eb",
31
+ PRIMARY_800: "#0f48be",
32
+ PRIMARY_900: "#134195",
33
+ PRIMARY_950: "#11295a"
34
+ };
35
+ //#endregion
36
+ //#region src/utils/delay.ts
37
+ const delay = (ms) => new Promise((resolve) => {
38
+ setTimeout(resolve, ms);
39
+ });
40
+ //#endregion
41
+ //#region src/api/auth-service.ts
42
+ var AuthService = class {
43
+ constructor(registryClient) {
44
+ this.registryClient = registryClient;
45
+ }
46
+ async login({ openBrowser = true }) {
47
+ const session = await this.registryClient.startDeviceFlow();
48
+ if (openBrowser) await open(session.verificationUriComplete).catch(() => {});
49
+ const deadline = Date.now() + session.expiresIn * 1e3;
50
+ let intervalSeconds = Math.max(1, session.interval);
51
+ while (Date.now() < deadline) {
52
+ await delay(intervalSeconds * 1e3);
53
+ const poll = await this.registryClient.pollDeviceFlow({ deviceCode: session.deviceCode });
54
+ if (poll.status === "pending") {
55
+ intervalSeconds = Math.max(1, poll.interval ?? intervalSeconds);
56
+ continue;
57
+ }
58
+ if (poll.status === "authorized") return {
59
+ userCode: session.userCode,
60
+ verificationUri: session.verificationUri,
61
+ accessToken: poll.accessToken
62
+ };
63
+ throw new Error(poll.message);
64
+ }
65
+ throw new Error("Device login timed out. Please run `withframe login` again.");
66
+ }
67
+ };
68
+ //#endregion
69
+ //#region src/lib/normalize.ts
70
+ const normalizeText = (value) => {
71
+ if (typeof value !== "string") return;
72
+ return value.trim() || void 0;
73
+ };
74
+ const normalizeProjectTarget = (value) => {
75
+ const normalized = normalizeText(value);
76
+ if (!normalized) return;
77
+ if (normalized === "react_native" || normalized === "expo") return normalized;
78
+ throw new Error("Invalid target value. Use 'react_native' or 'expo'.");
79
+ };
80
+ const normalizeOptions = (opts) => {
81
+ return {
82
+ cwd: normalizeText(opts.cwd),
83
+ target: normalizeProjectTarget(opts.target),
84
+ variant: normalizeText(opts.variant) ?? "default",
85
+ yes: Boolean(opts.yes)
86
+ };
87
+ };
88
+ //#endregion
89
+ //#region src/lib/type-guards.ts
90
+ const isString = (value) => {
91
+ return typeof value === "string";
92
+ };
93
+ const isFunction = (value) => {
94
+ return typeof value === "function";
95
+ };
96
+ //#endregion
97
+ //#region src/lib/project.ts
98
+ const PACKAGE_NAME_ALIASES = { "@react-native-async-storage": "@react-native-async-storage/async-storage" };
99
+ const hasFile = async (filePath) => {
100
+ try {
101
+ await access(filePath);
102
+ return true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ };
107
+ const hasDirectory = async (directoryPath) => {
108
+ try {
109
+ return (await stat(directoryPath)).isDirectory();
110
+ } catch {
111
+ return false;
112
+ }
113
+ };
114
+ const readPackageJson = async (projectRoot) => {
115
+ const packageJsonPath = path.join(projectRoot, "package.json");
116
+ const raw = await readFile(packageJsonPath, "utf8").catch(() => null);
117
+ if (!raw) throw new Error(`Could not find package.json in ${projectRoot}`);
118
+ let data;
119
+ try {
120
+ data = JSON.parse(raw);
121
+ } catch (error) {
122
+ throw new Error(`Failed to parse package.json: ${error instanceof Error ? error.message : "Invalid JSON"}`);
123
+ }
124
+ return {
125
+ path: packageJsonPath,
126
+ data
127
+ };
128
+ };
129
+ const normalizeDependencyVersion = (version) => {
130
+ const normalized = version.trim();
131
+ if (!normalized || normalized === "*") return "";
132
+ return normalized;
133
+ };
134
+ const normalizePackageName = (name) => {
135
+ const normalized = PACKAGE_NAME_ALIASES[name.trim()] ?? name.trim();
136
+ if (!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(normalized)) throw new Error(`Invalid package name in manifest: ${name}`);
137
+ return normalized;
138
+ };
139
+ const toDependencySpecifier = (name, version) => {
140
+ const normalizedVersion = normalizeDependencyVersion(version);
141
+ return normalizedVersion ? `${name}@${normalizedVersion}` : name;
142
+ };
143
+ const collectProjectDependencies = (data) => ({
144
+ ...data.dependencies ?? {},
145
+ ...data.devDependencies ?? {},
146
+ ...data.peerDependencies ?? {}
147
+ });
148
+ const detectTargetFromDependencies = (data) => {
149
+ const dependencies = collectProjectDependencies(data);
150
+ if (isString(dependencies.expo)) return "expo";
151
+ if (isString(dependencies["react-native"])) return "react_native";
152
+ return null;
153
+ };
154
+ const resolveProjectRoot = (cwdOption) => {
155
+ return !cwdOption ? process.cwd() : path.resolve(cwdOption);
156
+ };
157
+ const detectProjectTarget = async ({ projectRoot, optionTarget, config }) => {
158
+ if (optionTarget) return optionTarget;
159
+ if (config.target === "react_native" || config.target === "expo") return config.target;
160
+ const { data } = await readPackageJson(projectRoot);
161
+ const detectedTarget = detectTargetFromDependencies(data);
162
+ if (!detectedTarget) throw new Error("Could not detect project target. Install 'expo' or 'react-native', or set target in withframe.config.json.");
163
+ return detectedTarget;
164
+ };
165
+ const shouldInstallManifestDependencies = async (projectRoot) => {
166
+ if (!await hasFile(path.join(projectRoot, "package.json"))) return false;
167
+ const { data } = await readPackageJson(projectRoot);
168
+ return Boolean(detectTargetFromDependencies(data));
169
+ };
170
+ const resolveOutputDirectory = async ({ projectRoot, config }) => {
171
+ if (config.outputDir) return config.outputDir;
172
+ if (await hasDirectory(path.join(projectRoot, "app", "components"))) return path.join("app", "components", "withframe");
173
+ return path.join("src", "components", "withframe");
174
+ };
175
+ const detectPackageManager = async (projectRoot) => {
176
+ if (await hasFile(path.join(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
177
+ if (await hasFile(path.join(projectRoot, "yarn.lock"))) return "yarn";
178
+ if (await hasFile(path.join(projectRoot, "bun.lockb")) || await hasFile(path.join(projectRoot, "bun.lock"))) return "bun";
179
+ return "npm";
180
+ };
181
+ const isPathInsideRoot = (projectRoot, targetPath) => {
182
+ const resolvedRoot = path.resolve(projectRoot);
183
+ const resolvedTarget = path.resolve(targetPath);
184
+ return resolvedTarget === resolvedRoot || resolvedTarget.startsWith(`${resolvedRoot}${path.sep}`);
185
+ };
186
+ const writeManifestFiles = async ({ projectRoot, outputDir, files, yes, confirmOverwrite }) => {
187
+ const created = [];
188
+ const overwritten = [];
189
+ const skipped = [];
190
+ for (const file of files) {
191
+ const relativeFilePath = file.path.replace(/^\/+/, "");
192
+ const destination = path.resolve(projectRoot, outputDir, relativeFilePath);
193
+ if (!isPathInsideRoot(projectRoot, destination)) throw new Error(`Refusing to write outside project root: ${file.path}`);
194
+ const exists = await hasFile(destination);
195
+ if (exists) {
196
+ let shouldOverwrite = yes;
197
+ if (!shouldOverwrite) shouldOverwrite = await confirmOverwrite(destination);
198
+ if (!shouldOverwrite) {
199
+ skipped.push(destination);
200
+ continue;
201
+ }
202
+ }
203
+ await mkdir(path.dirname(destination), { recursive: true });
204
+ await writeFile(destination, file.content, "utf8");
205
+ if (exists) overwritten.push(destination);
206
+ else created.push(destination);
207
+ }
208
+ return {
209
+ created,
210
+ overwritten,
211
+ skipped
212
+ };
213
+ };
214
+ const mergeManifestDependencies = async ({ projectRoot, manifest }) => {
215
+ const content = (await readPackageJson(projectRoot)).data;
216
+ content.dependencies = content.dependencies ?? {};
217
+ content.devDependencies = content.devDependencies ?? {};
218
+ content.peerDependencies = content.peerDependencies ?? {};
219
+ const hasAnyDependency = (name) => Boolean(content.dependencies?.[name] ?? content.devDependencies?.[name] ?? content.peerDependencies?.[name]);
220
+ const runtimeToInstall = [];
221
+ const devToInstall = [];
222
+ for (const [rawName, version] of Object.entries(manifest.dependencies ?? {})) {
223
+ const name = normalizePackageName(rawName);
224
+ if (hasAnyDependency(name)) continue;
225
+ runtimeToInstall.push(toDependencySpecifier(name, version));
226
+ }
227
+ for (const [rawName, version] of Object.entries(manifest.peerDependencies ?? {})) {
228
+ const name = normalizePackageName(rawName);
229
+ if (hasAnyDependency(name)) continue;
230
+ runtimeToInstall.push(toDependencySpecifier(name, version));
231
+ }
232
+ for (const [rawName, version] of Object.entries(manifest.devDependencies ?? {})) {
233
+ const name = normalizePackageName(rawName);
234
+ if (hasAnyDependency(name)) continue;
235
+ devToInstall.push(toDependencySpecifier(name, version));
236
+ }
237
+ return {
238
+ runtimeToInstall,
239
+ devToInstall
240
+ };
241
+ };
242
+ const runPackageCommand = async ({ projectRoot, command, args }) => {
243
+ await new Promise((resolve, reject) => {
244
+ const child = spawn(command, args, {
245
+ cwd: projectRoot,
246
+ stdio: "inherit"
247
+ });
248
+ child.on("error", (error) => {
249
+ reject(error);
250
+ });
251
+ child.on("close", (code) => {
252
+ if (code === 0) {
253
+ resolve();
254
+ return;
255
+ }
256
+ reject(/* @__PURE__ */ new Error(`${command} ${args.join(" ")} failed with exit code ${code}`));
257
+ });
258
+ });
259
+ };
260
+ const installDependencies = async ({ projectRoot, runtimeDependencies, devDependencies }) => {
261
+ if (!runtimeDependencies.length && !devDependencies.length) return [];
262
+ const manager = await detectPackageManager(projectRoot);
263
+ if (runtimeDependencies.length) {
264
+ if (manager === "npm") await runPackageCommand({
265
+ projectRoot,
266
+ command: "npm",
267
+ args: [
268
+ "install",
269
+ "--no-progress",
270
+ ...runtimeDependencies
271
+ ]
272
+ });
273
+ if (manager === "pnpm") await runPackageCommand({
274
+ projectRoot,
275
+ command: "pnpm",
276
+ args: ["add", ...runtimeDependencies]
277
+ });
278
+ if (manager === "yarn") await runPackageCommand({
279
+ projectRoot,
280
+ command: "yarn",
281
+ args: ["add", ...runtimeDependencies]
282
+ });
283
+ if (manager === "bun") await runPackageCommand({
284
+ projectRoot,
285
+ command: "bun",
286
+ args: ["add", ...runtimeDependencies]
287
+ });
288
+ }
289
+ if (devDependencies.length) {
290
+ if (manager === "npm") await runPackageCommand({
291
+ projectRoot,
292
+ command: "npm",
293
+ args: [
294
+ "install",
295
+ "--no-progress",
296
+ "--save-dev",
297
+ ...devDependencies
298
+ ]
299
+ });
300
+ if (manager === "pnpm") await runPackageCommand({
301
+ projectRoot,
302
+ command: "pnpm",
303
+ args: [
304
+ "add",
305
+ "--save-dev",
306
+ ...devDependencies
307
+ ]
308
+ });
309
+ if (manager === "yarn") await runPackageCommand({
310
+ projectRoot,
311
+ command: "yarn",
312
+ args: [
313
+ "add",
314
+ "--dev",
315
+ ...devDependencies
316
+ ]
317
+ });
318
+ if (manager === "bun") await runPackageCommand({
319
+ projectRoot,
320
+ command: "bun",
321
+ args: [
322
+ "add",
323
+ "--dev",
324
+ ...devDependencies
325
+ ]
326
+ });
327
+ }
328
+ return [...runtimeDependencies, ...devDependencies];
329
+ };
330
+ //#endregion
331
+ //#region src/lib/config.ts
332
+ const getConfigFilePath = (cwd) => path.join(cwd, CONFIG_FILE_NAME);
333
+ const loadWithFrameConfig = async (cwd) => {
334
+ const configPath = getConfigFilePath(cwd);
335
+ try {
336
+ await access(configPath);
337
+ } catch {
338
+ return {};
339
+ }
340
+ let parsed;
341
+ try {
342
+ const raw = await readFile(configPath, "utf8");
343
+ parsed = JSON.parse(raw);
344
+ } catch (error) {
345
+ throw new Error(`Failed to parse ${CONFIG_FILE_NAME}: ${error instanceof Error ? error.message : "Invalid JSON"}`);
346
+ }
347
+ if (!parsed || typeof parsed !== "object") throw new Error(`${CONFIG_FILE_NAME} must be a JSON object.`);
348
+ const safe = parsed;
349
+ return {
350
+ outputDir: normalizeText(safe.outputDir),
351
+ target: normalizeProjectTarget(safe.target)
352
+ };
353
+ };
354
+ //#endregion
355
+ //#region src/lib/prompt.ts
356
+ const confirm = async (question) => {
357
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
358
+ return new Promise((resolve) => {
359
+ process.stdout.write(question);
360
+ const handleData = (data) => {
361
+ const char = data.toString().toLowerCase();
362
+ if (char === "") process.exit();
363
+ if (char === "y") {
364
+ cleanup();
365
+ resolve(true);
366
+ } else if (char === "n") {
367
+ cleanup();
368
+ resolve(false);
369
+ }
370
+ };
371
+ const cleanup = () => {
372
+ process.stdin.setRawMode(false);
373
+ process.stdin.pause();
374
+ process.stdin.removeListener("data", handleData);
375
+ process.stdout.write("\n");
376
+ };
377
+ process.stdin.setRawMode(true);
378
+ process.stdin.resume();
379
+ process.stdin.on("data", handleData);
380
+ });
381
+ };
382
+ //#endregion
383
+ //#region src/api/component-service.ts
384
+ var ComponentService = class {
385
+ constructor(tokenStore, registryClient) {
386
+ this.tokenStore = tokenStore;
387
+ this.registryClient = registryClient;
388
+ }
389
+ async addComponent(componentSlug, opts, hooks = {}) {
390
+ const slug = normalizeText(componentSlug);
391
+ if (!slug) throw new Error("Component slug is required.");
392
+ const options = normalizeOptions(opts);
393
+ const context = await this.resolveContext(options);
394
+ const tokenResult = await this.tokenStore.resolveAccessToken();
395
+ const response = await this.registryClient.fetchComponent({
396
+ slug,
397
+ target: context.target,
398
+ variant: options.variant || "default",
399
+ token: tokenResult.token
400
+ });
401
+ const overwriteDecisions = await this.collectOverwriteDecisions({
402
+ projectRoot: context.projectRoot,
403
+ outputDir: context.outputDir,
404
+ files: response.manifest.files,
405
+ yes: Boolean(options.yes),
406
+ hooks
407
+ });
408
+ hooks.onApplyStart?.();
409
+ const installation = await this.applyManifest({
410
+ context,
411
+ manifest: response.manifest,
412
+ yes: Boolean(options.yes),
413
+ hooks,
414
+ overwriteDecisions
415
+ });
416
+ return {
417
+ component: response.component,
418
+ target: context.target,
419
+ outputDir: context.outputDir,
420
+ ...installation
421
+ };
422
+ }
423
+ async resolveContext(options) {
424
+ const projectRoot = resolveProjectRoot(options.cwd);
425
+ const config = await loadWithFrameConfig(projectRoot);
426
+ return {
427
+ projectRoot,
428
+ target: await detectProjectTarget({
429
+ projectRoot,
430
+ optionTarget: options.target,
431
+ config
432
+ }),
433
+ outputDir: await resolveOutputDirectory({
434
+ projectRoot,
435
+ config
436
+ })
437
+ };
438
+ }
439
+ async collectOverwriteDecisions({ projectRoot, outputDir, files, yes, hooks }) {
440
+ const overwriteDecisions = /* @__PURE__ */ new Map();
441
+ if (yes) return overwriteDecisions;
442
+ for (const file of files) {
443
+ const relativeFilePath = file.path.replace(/^\/+/, "");
444
+ const destination = path.resolve(projectRoot, outputDir, relativeFilePath);
445
+ if (!await hasFile(destination)) continue;
446
+ overwriteDecisions.set(destination, await this.confirmOverwrite({
447
+ projectRoot,
448
+ absolutePath: destination,
449
+ hooks
450
+ }));
451
+ }
452
+ return overwriteDecisions;
453
+ }
454
+ async applyManifest({ context, manifest, yes, hooks, overwriteDecisions }) {
455
+ const fileResult = await writeManifestFiles({
456
+ projectRoot: context.projectRoot,
457
+ outputDir: context.outputDir,
458
+ files: manifest.files,
459
+ yes,
460
+ confirmOverwrite: async (absolutePath) => {
461
+ const existingDecision = overwriteDecisions.get(absolutePath);
462
+ if (typeof existingDecision === "boolean") return existingDecision;
463
+ return this.confirmOverwrite({
464
+ projectRoot: context.projectRoot,
465
+ absolutePath,
466
+ hooks
467
+ });
468
+ }
469
+ });
470
+ let installedDependencies = [];
471
+ if (await shouldInstallManifestDependencies(context.projectRoot)) {
472
+ const dependencies = await mergeManifestDependencies({
473
+ projectRoot: context.projectRoot,
474
+ manifest
475
+ });
476
+ installedDependencies = await installDependencies({
477
+ projectRoot: context.projectRoot,
478
+ runtimeDependencies: dependencies.runtimeToInstall,
479
+ devDependencies: dependencies.devToInstall
480
+ });
481
+ }
482
+ return {
483
+ createdFiles: fileResult.created,
484
+ overwrittenFiles: fileResult.overwritten,
485
+ skippedFiles: fileResult.skipped,
486
+ installedDependencies
487
+ };
488
+ }
489
+ async confirmOverwrite({ projectRoot, absolutePath, hooks }) {
490
+ const relativePath = path.relative(projectRoot, absolutePath);
491
+ if (hooks.confirmOverwrite) return hooks.confirmOverwrite(relativePath);
492
+ return confirm(`File ${relativePath} exists. Overwrite? [y/N] `);
493
+ }
494
+ };
495
+ //#endregion
496
+ //#region src/api/init-service.ts
497
+ const detectTargetIfPossible = async (projectRoot, preferredTarget) => {
498
+ if (preferredTarget) return preferredTarget;
499
+ try {
500
+ return await detectProjectTarget({
501
+ projectRoot,
502
+ optionTarget: void 0,
503
+ config: {}
504
+ });
505
+ } catch {
506
+ return;
507
+ }
508
+ };
509
+ var InitService = class {
510
+ async createConfig(options, hooks = {}) {
511
+ const projectRoot = resolveProjectRoot(options.cwd);
512
+ const configPath = getConfigFilePath(projectRoot);
513
+ const alreadyExists = await hasFile(configPath);
514
+ if (alreadyExists && !options.force) throw new Error(`${CONFIG_FILE_NAME} already exists at ${configPath}. Use --force to overwrite.`);
515
+ const target = await detectTargetIfPossible(projectRoot, await this.resolveTarget(options.target));
516
+ const config = {
517
+ outputDir: options.outputDir ?? await resolveOutputDirectory({
518
+ projectRoot,
519
+ config: {}
520
+ }),
521
+ ...target ? { target } : {}
522
+ };
523
+ hooks.onInitializeStart?.();
524
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
525
+ return {
526
+ configPath,
527
+ config,
528
+ overwritten: alreadyExists
529
+ };
530
+ }
531
+ async resolveTarget(optionTarget) {
532
+ const normalizedOption = normalizeProjectTarget(optionTarget);
533
+ if (normalizedOption) return normalizedOption;
534
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Interactive target selection requires a TTY. Pass --target react_native|expo.");
535
+ return select({
536
+ message: "Pick your project target:",
537
+ default: "expo",
538
+ choices: [{
539
+ name: "Expo",
540
+ value: "expo",
541
+ description: "Use for Expo-based React Native apps"
542
+ }, {
543
+ name: "React Native",
544
+ value: "react_native",
545
+ description: "Use for plain React Native projects"
546
+ }]
547
+ });
548
+ }
549
+ };
550
+ //#endregion
551
+ //#region src/lib/env.ts
552
+ const ENV = { WITHFRAME_TOKEN: "WITHFRAME_TOKEN" };
553
+ const PROJECT_ENV_FILES = [
554
+ ".env.local",
555
+ ".env",
556
+ ".env.development"
557
+ ];
558
+ const getEnvVariableName = (key) => ENV[key];
559
+ const formatEnvString = (value) => {
560
+ if (typeof value !== "string") return;
561
+ return value.trim() || void 0;
562
+ };
563
+ const unquote = (value) => {
564
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1);
565
+ return value;
566
+ };
567
+ const getValueFromProjectEnvFiles = (envName) => {
568
+ const cwd = process.cwd();
569
+ for (const fileName of PROJECT_ENV_FILES) {
570
+ const filePath = path.join(cwd, fileName);
571
+ let content;
572
+ try {
573
+ content = readFileSync(filePath, "utf8");
574
+ } catch {
575
+ continue;
576
+ }
577
+ for (const rawLine of content.split(/\r?\n/g)) {
578
+ const line = rawLine.trim();
579
+ if (!line || line.startsWith("#")) continue;
580
+ const withoutExport = line.startsWith("export ") ? line.slice(7).trim() : line;
581
+ const separatorIndex = withoutExport.indexOf("=");
582
+ if (separatorIndex <= 0) continue;
583
+ if (withoutExport.slice(0, separatorIndex).trim() !== envName) continue;
584
+ const value = formatEnvString(unquote(withoutExport.slice(separatorIndex + 1).trim()));
585
+ if (value) return value;
586
+ }
587
+ }
588
+ };
589
+ const getEnvValue = (key) => {
590
+ const envName = getEnvVariableName(key);
591
+ const direct = formatEnvString(process.env[envName]);
592
+ if (direct) return direct;
593
+ return getValueFromProjectEnvFiles(envName);
594
+ };
595
+ const getRequiredEnvValue = (key) => {
596
+ const value = getEnvValue(key);
597
+ if (value) return value;
598
+ const envName = getEnvVariableName(key);
599
+ throw new Error(`Missing required environment variable: ${envName}.`);
600
+ };
601
+ //#endregion
602
+ //#region src/core/base-command.ts
603
+ var BaseCommand = class {
604
+ requiredEnvVariables() {
605
+ return [];
606
+ }
607
+ configure(command) {
608
+ return command;
609
+ }
610
+ async runTask(task, options) {
611
+ const spinner = ora(options.spinner);
612
+ let started = false;
613
+ const start = () => {
614
+ if (!started) {
615
+ spinner.start();
616
+ started = true;
617
+ }
618
+ return spinner;
619
+ };
620
+ if (options.startMode !== "manual") start();
621
+ try {
622
+ const result = await task({
623
+ spinner,
624
+ start,
625
+ isStarted: () => started
626
+ });
627
+ const successText = isFunction(options.successText) ? options.successText(result) : options.successText;
628
+ if (started) spinner.succeed(successText);
629
+ else console.log(successText);
630
+ return result;
631
+ } catch (error) {
632
+ if (started) spinner.fail(options.failureText);
633
+ throw error;
634
+ }
635
+ }
636
+ register(program) {
637
+ this.configure(program.command(this.name).description(this.description)).action(async (...args) => {
638
+ try {
639
+ this.requiredEnvVariables().forEach((key) => getRequiredEnvValue(key));
640
+ await this.execute(...args);
641
+ } catch (error) {
642
+ const message = error instanceof Error ? error.message : "Unknown error";
643
+ console.error(chalk.red(`\n${message}\n`));
644
+ process.exitCode = 1;
645
+ }
646
+ });
647
+ }
648
+ };
649
+ //#endregion
650
+ //#region src/utils/output.ts
651
+ const printList = ({ title, items, titleColor, prefix }) => {
652
+ if (!items.length) return;
653
+ console.log(titleColor(`\n ${title}:`));
654
+ items.forEach((item) => {
655
+ console.log(chalk.white(` ${prefix} ${item}`));
656
+ });
657
+ };
658
+ const printQuickStart = () => {
659
+ console.log(chalk.dim(SEPARATOR));
660
+ console.log(chalk.cyan("\n 🎯 Quick Start:\n"));
661
+ console.log(chalk.white(" $ withframe init ") + chalk.gray("→ Create local config"));
662
+ console.log(chalk.white(" $ withframe login ") + chalk.gray("→ Authenticate"));
663
+ console.log(chalk.white(" $ withframe add button ") + chalk.gray("→ Add component code\n"));
664
+ console.log(chalk.white(" $ withframe logout ") + chalk.gray("→ Clear local token\n"));
665
+ };
666
+ const printAddResult = (result) => {
667
+ console.log("");
668
+ console.log(chalk.gray(` Target: ${result.target}`));
669
+ console.log(chalk.gray(` Output dir: ${result.outputDir}`));
670
+ printList({
671
+ title: "Created files",
672
+ items: result.createdFiles,
673
+ titleColor: chalk.cyan,
674
+ prefix: "+"
675
+ });
676
+ printList({
677
+ title: "Overwritten files",
678
+ items: result.overwrittenFiles,
679
+ titleColor: chalk.yellow,
680
+ prefix: "~"
681
+ });
682
+ printList({
683
+ title: "Skipped files",
684
+ items: result.skippedFiles,
685
+ titleColor: chalk.yellow,
686
+ prefix: "-"
687
+ });
688
+ printList({
689
+ title: "Installed dependencies",
690
+ items: result.installedDependencies,
691
+ titleColor: chalk.cyan,
692
+ prefix: "•"
693
+ });
694
+ console.log("");
695
+ };
696
+ //#endregion
697
+ //#region src/commands/add.ts
698
+ var AddCommand = class extends BaseCommand {
699
+ constructor(componentService) {
700
+ super();
701
+ this.componentService = componentService;
702
+ this.name = "add <component>";
703
+ this.description = chalk.magenta("Add a component from WithFrame registry");
704
+ }
705
+ requiredEnvVariables() {
706
+ return ["WITHFRAME_TOKEN"];
707
+ }
708
+ configure(command) {
709
+ return command.alias("install").option("-v, --variant <variant>", "Install specific variant id", "default").option("-t, --target <target>", "Project target: react_native or expo").option("--cwd <path>", "Project root directory").option("-y, --yes", "Overwrite files without confirmation");
710
+ }
711
+ async execute(comp, opts) {
712
+ const displayComponent = comp.trim() || "component";
713
+ printAddResult(await this.runTask(async ({ start }) => {
714
+ return this.componentService.addComponent(comp, opts, {
715
+ onApplyStart: () => {
716
+ start();
717
+ },
718
+ confirmOverwrite: async (relativePath) => {
719
+ return confirm(`File ${relativePath} exists. Overwrite? [y/N] `);
720
+ }
721
+ });
722
+ }, {
723
+ spinner: {
724
+ text: `📦 Adding ${chalk.bold(displayComponent)}...`,
725
+ spinner: "dots",
726
+ color: "cyan"
727
+ },
728
+ successText: (value) => chalk.green.bold(`${value.component.slug} added successfully`),
729
+ failureText: chalk.red.bold("Failed to add component"),
730
+ startMode: "manual"
731
+ }));
732
+ }
733
+ };
734
+ //#endregion
735
+ //#region src/commands/init.ts
736
+ var InitCommand = class extends BaseCommand {
737
+ constructor(initService) {
738
+ super();
739
+ this.initService = initService;
740
+ this.name = "init";
741
+ this.description = chalk.blue("Create withframe.config.json in your project");
742
+ }
743
+ configure(command) {
744
+ return command.option("--cwd <path>", "Project root directory").option("-o, --output-dir <path>", "Set output directory in config").option("-t, --target <target>", "Set target: react_native or expo").option("-f, --force", "Overwrite existing withframe.config.json");
745
+ }
746
+ async execute(options) {
747
+ const cwd = normalizeText(options.cwd);
748
+ const outputDir = normalizeText(options.outputDir);
749
+ const result = await this.runTask(async ({ start }) => {
750
+ return this.initService.createConfig({
751
+ cwd,
752
+ outputDir,
753
+ target: options.target,
754
+ force: Boolean(options.force)
755
+ }, { onInitializeStart: () => {
756
+ start();
757
+ } });
758
+ }, {
759
+ spinner: {
760
+ text: chalk.cyan("Initializing withframe.config.json..."),
761
+ spinner: "dots",
762
+ color: "cyan"
763
+ },
764
+ successText: (value) => value.overwritten ? chalk.green.bold("withframe.config.json updated") : chalk.green.bold("withframe.config.json created"),
765
+ failureText: chalk.red.bold("Failed to initialize config"),
766
+ startMode: "manual"
767
+ });
768
+ console.log("");
769
+ console.log(chalk.gray(` Path: ${result.configPath}`));
770
+ console.log(chalk.gray(` Output dir: ${result.config.outputDir ?? "not set"}`));
771
+ console.log(chalk.gray(` Target: ${result.config.target ?? "auto-detect on add"}`));
772
+ console.log("");
773
+ }
774
+ };
775
+ //#endregion
776
+ //#region src/commands/login.ts
777
+ var LoginCommand = class extends BaseCommand {
778
+ constructor(authService) {
779
+ super();
780
+ this.authService = authService;
781
+ this.name = "login";
782
+ this.description = chalk.green("Authenticate and output WITHFRAME_TOKEN export command");
783
+ }
784
+ configure(command) {
785
+ return command.option("--no-open", "Do not open browser automatically").option("--cwd <path>", "Resolve config from directory");
786
+ }
787
+ async execute(options) {
788
+ await loadWithFrameConfig(options.cwd?.trim() || process.cwd());
789
+ const authResult = await this.runTask(async ({ spinner }) => {
790
+ spinner.text = chalk.cyan("Waiting for browser approval...");
791
+ return this.authService.login({ openBrowser: options.open });
792
+ }, {
793
+ spinner: {
794
+ text: chalk.cyan("🔐 Starting device login..."),
795
+ spinner: "dots",
796
+ color: "cyan"
797
+ },
798
+ successText: chalk.green.bold("Logged in successfully"),
799
+ failureText: chalk.red.bold("Login failed")
800
+ });
801
+ console.log("");
802
+ console.log(chalk.cyan(" Use this token in one of two ways:"));
803
+ console.log(chalk.cyan(" export in your shell"));
804
+ console.log(chalk.white(` export WITHFRAME_TOKEN="${authResult.accessToken}"`));
805
+ console.log(chalk.gray(" ─────────────── OR ───────────────"));
806
+ console.log(chalk.cyan(" save in your project .env or .env.local"));
807
+ console.log(chalk.white(` WITHFRAME_TOKEN="${authResult.accessToken}"`));
808
+ console.log("");
809
+ }
810
+ };
811
+ //#endregion
812
+ //#region src/commands/logout.ts
813
+ var LogoutCommand = class extends BaseCommand {
814
+ constructor(tokenStore) {
815
+ super();
816
+ this.tokenStore = tokenStore;
817
+ this.name = "logout";
818
+ this.description = chalk.red("Clear local WithFrame auth token");
819
+ }
820
+ async execute() {
821
+ const authFilePath = await this.runTask(async () => {
822
+ await this.tokenStore.clearToken();
823
+ return this.tokenStore.getAuthFilePath();
824
+ }, {
825
+ spinner: {
826
+ text: chalk.cyan("Clearing local session..."),
827
+ spinner: "dots",
828
+ color: "yellow"
829
+ },
830
+ successText: chalk.yellow.bold("Logged out successfully"),
831
+ failureText: chalk.red.bold("Failed to clear local session")
832
+ });
833
+ console.log(chalk.dim(`\n Token cache cleared: ${authFilePath}\n`));
834
+ }
835
+ };
836
+ //#endregion
837
+ //#region src/commands/shot.ts
838
+ var ShotCommand = class extends BaseCommand {
839
+ constructor(shotService) {
840
+ super();
841
+ this.shotService = shotService;
842
+ this.name = "shot";
843
+ this.description = chalk.cyan("Upload a screenshot and attach it to a collection");
844
+ }
845
+ requiredEnvVariables() {
846
+ return ["WITHFRAME_TOKEN"];
847
+ }
848
+ configure(command) {
849
+ return command.requiredOption("--file <path>", "Screenshot file path").option("--device <id>", "Preferred device id when dimensions match multiple models");
850
+ }
851
+ async execute(opts) {
852
+ const result = await this.runTask(async ({ start }) => {
853
+ return this.shotService.uploadShot(opts, { onUploadStart: () => {
854
+ start();
855
+ } });
856
+ }, {
857
+ spinner: {
858
+ text: "Uploading screenshot ...",
859
+ spinner: "dots",
860
+ color: "cyan"
861
+ },
862
+ successText: () => chalk.green.bold("Screenshot uploaded successfully"),
863
+ failureText: chalk.red.bold("Failed to upload screenshot"),
864
+ startMode: "manual"
865
+ });
866
+ console.log(result.url);
867
+ }
868
+ };
869
+ //#endregion
870
+ //#region src/commands/upload.ts
871
+ var UploadCommand = class extends BaseCommand {
872
+ constructor(uploadService) {
873
+ super();
874
+ this.uploadService = uploadService;
875
+ this.name = "upload";
876
+ this.description = chalk.yellow("Upload a component to WithFrame registry");
877
+ }
878
+ requiredEnvVariables() {
879
+ return ["WITHFRAME_TOKEN"];
880
+ }
881
+ configure(command) {
882
+ return command.option("--path <path>", "Component entry file path").option("--no-open", "Do not open browser automatically");
883
+ }
884
+ async execute(opts) {
885
+ const result = await this.runTask(async () => {
886
+ return this.uploadService.upload(opts);
887
+ }, {
888
+ spinner: {
889
+ text: `📦 Uploading ...`,
890
+ spinner: "dots",
891
+ color: "cyan"
892
+ },
893
+ successText: () => chalk.green.bold(`Component uploaded successfully`),
894
+ failureText: chalk.red.bold("Failed to upload component"),
895
+ startMode: "manual"
896
+ });
897
+ if (opts.open !== false) await open(result.editUrl).catch(() => {});
898
+ console.log("");
899
+ console.log(chalk.gray(` Draft: ${result.componentId}`));
900
+ console.log(chalk.gray(` Target: ${result.target}`));
901
+ console.log(chalk.gray(` Edit URL: ${result.editUrl}`));
902
+ console.log("");
903
+ }
904
+ };
905
+ //#endregion
906
+ //#region src/core/cli-app.ts
907
+ var CliApp = class {
908
+ constructor(program, commandRegistry) {
909
+ this.program = program;
910
+ this.commandRegistry = commandRegistry;
911
+ }
912
+ run(argv) {
913
+ this.commandRegistry.registerAll(this.program);
914
+ this.program.parse(argv);
915
+ if (!argv.slice(2).length) {
916
+ this.program.outputHelp();
917
+ printQuickStart();
918
+ }
919
+ }
920
+ };
921
+ //#endregion
922
+ //#region src/core/command-registry.ts
923
+ var CommandRegistry = class {
924
+ constructor(commands) {
925
+ this.commands = commands;
926
+ }
927
+ registerAll(program) {
928
+ this.commands.forEach((command) => {
929
+ command.register(program);
930
+ });
931
+ }
932
+ };
933
+ //#endregion
934
+ //#region src/lib/token-store.ts
935
+ var TokenStore = class {
936
+ constructor() {
937
+ this.authDirectoryPath = path.join(os.homedir(), WITHFRAME_DIR);
938
+ this.authFilePath = path.join(this.authDirectoryPath, AUTH_FILE_NAME);
939
+ }
940
+ getAuthFilePath() {
941
+ return this.authFilePath;
942
+ }
943
+ async resolveAccessToken() {
944
+ return {
945
+ token: getEnvValue("WITHFRAME_TOKEN"),
946
+ source: "env"
947
+ };
948
+ }
949
+ async saveToken(accessToken, expiresInSeconds) {
950
+ const now = /* @__PURE__ */ new Date();
951
+ const expiresAt = new Date(now.getTime() + Math.max(0, expiresInSeconds) * 1e3);
952
+ const payload = {
953
+ accessToken,
954
+ createdAt: now.toISOString(),
955
+ expiresAt: expiresAt.toISOString()
956
+ };
957
+ await mkdir(this.authDirectoryPath, {
958
+ recursive: true,
959
+ mode: 448
960
+ });
961
+ await writeFile(this.authFilePath, `${JSON.stringify(payload, null, 2)}\n`, {
962
+ mode: 384,
963
+ encoding: "utf8"
964
+ });
965
+ await chmod(this.authFilePath, 384).catch(() => void 0);
966
+ }
967
+ async clearToken() {
968
+ await unlink(this.authFilePath).catch(() => void 0);
969
+ }
970
+ };
971
+ //#endregion
972
+ //#region src/lib/errors.ts
973
+ var CliHttpError = class extends Error {
974
+ constructor(message, statusCode, payload) {
975
+ super(message);
976
+ this.statusCode = statusCode;
977
+ this.payload = payload;
978
+ this.name = "CliHttpError";
979
+ }
980
+ };
981
+ //#endregion
982
+ //#region src/lib/http.ts
983
+ const parseResponsePayload = async (response) => {
984
+ const text = await response.text();
985
+ if (!text) return {};
986
+ try {
987
+ return JSON.parse(text);
988
+ } catch {
989
+ return text;
990
+ }
991
+ };
992
+ const resolveErrorMessage = (payload, fallback) => {
993
+ if (!payload || typeof payload !== "object") return fallback;
994
+ const message = payload.message;
995
+ return isString(message) && message.trim() ? message : fallback;
996
+ };
997
+ const requestJson = async (url, init) => {
998
+ const response = await fetch(url, {
999
+ ...init,
1000
+ headers: {
1001
+ Accept: "application/json",
1002
+ ...init?.headers ?? {}
1003
+ }
1004
+ });
1005
+ const payload = await parseResponsePayload(response);
1006
+ if (!response.ok) throw new CliHttpError(resolveErrorMessage(payload, `Request failed with status ${response.status}`), response.status, payload);
1007
+ return payload;
1008
+ };
1009
+ //#endregion
1010
+ //#region src/api/registry-client.ts
1011
+ const sanitizeBaseUrl = (baseUrl) => baseUrl.replace(/\/+$/, "");
1012
+ const CALLBACK_URL_TYPE = {
1013
+ DEV: "DEV",
1014
+ STAGE: "STAGE",
1015
+ PROD: "PROD"
1016
+ };
1017
+ const isLocalHost = (hostname) => {
1018
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0";
1019
+ };
1020
+ const parseUrl = (value) => {
1021
+ try {
1022
+ return new URL(value);
1023
+ } catch {
1024
+ try {
1025
+ return new URL(`http://${value}`);
1026
+ } catch {
1027
+ return null;
1028
+ }
1029
+ }
1030
+ };
1031
+ const formatCallback = (url) => {
1032
+ const parsed = parseUrl(url.trim());
1033
+ if (!parsed) return CALLBACK_URL_TYPE.PROD;
1034
+ const hostname = parsed.hostname.toLowerCase();
1035
+ if (hostname === "stage.withfra.me") return CALLBACK_URL_TYPE.STAGE;
1036
+ if (hostname === "withfra.me") return CALLBACK_URL_TYPE.PROD;
1037
+ if (isLocalHost(hostname)) return CALLBACK_URL_TYPE.DEV;
1038
+ return CALLBACK_URL_TYPE.PROD;
1039
+ };
1040
+ var RegistryClient = class {
1041
+ getRegistryBaseUrl() {
1042
+ return REGISTRY_URL;
1043
+ }
1044
+ getCallbackUrlType() {
1045
+ return formatCallback(this.getRegistryBaseUrl());
1046
+ }
1047
+ toUrl(endpoint) {
1048
+ return `${sanitizeBaseUrl(this.getRegistryBaseUrl())}${endpoint}`;
1049
+ }
1050
+ startDeviceFlow({ clientName = "withframe-cli" } = {}) {
1051
+ return requestJson(this.toUrl("/api/cli/device/start"), {
1052
+ method: "POST",
1053
+ headers: { "Content-Type": "application/json" },
1054
+ body: JSON.stringify({ clientName })
1055
+ });
1056
+ }
1057
+ pollDeviceFlow({ deviceCode }) {
1058
+ return requestJson(this.toUrl("/api/cli/device/poll"), {
1059
+ method: "POST",
1060
+ headers: { "Content-Type": "application/json" },
1061
+ body: JSON.stringify({ deviceCode })
1062
+ });
1063
+ }
1064
+ fetchComponent({ slug, target, variant, token }) {
1065
+ const params = new URLSearchParams({
1066
+ target,
1067
+ variant
1068
+ });
1069
+ return requestJson(this.toUrl(`/api/cli/registry/components/${encodeURIComponent(slug)}?${params.toString()}`), {
1070
+ method: "GET",
1071
+ headers: { Authorization: `Bearer ${token}` }
1072
+ });
1073
+ }
1074
+ uploadComponent({ content, fileName, token }) {
1075
+ const callback = this.getCallbackUrlType();
1076
+ return requestJson(this.toUrl("/api/cli/registry/components/upload"), {
1077
+ method: "POST",
1078
+ headers: {
1079
+ "Content-Type": "application/json",
1080
+ Authorization: `Bearer ${token}`
1081
+ },
1082
+ body: JSON.stringify({
1083
+ content,
1084
+ fileName,
1085
+ callback
1086
+ })
1087
+ });
1088
+ }
1089
+ fetchShotCollections({ token, offset = 0, limit = 40 }) {
1090
+ const params = new URLSearchParams({
1091
+ offset: String(offset),
1092
+ limit: String(limit)
1093
+ });
1094
+ return requestJson(this.toUrl(`/api/cli/shots/collections?${params.toString()}`), {
1095
+ method: "GET",
1096
+ headers: { Authorization: `Bearer ${token}` }
1097
+ });
1098
+ }
1099
+ uploadShot({ token, fileName, mimeType, content, collectionId, collectionTitle, createNewCollection, color }) {
1100
+ const callback = this.getCallbackUrlType();
1101
+ const formData = new FormData();
1102
+ const fileContent = Uint8Array.from(content);
1103
+ formData.append("file", new Blob([fileContent], { type: mimeType }), fileName);
1104
+ formData.append("callback", callback);
1105
+ if (collectionId) formData.append("collectionId", collectionId);
1106
+ if (collectionTitle) formData.append("collectionTitle", collectionTitle);
1107
+ if (createNewCollection) formData.append("createNewCollection", "true");
1108
+ if (color) formData.append("color", color);
1109
+ return requestJson(this.toUrl("/api/cli/shots/upload"), {
1110
+ method: "POST",
1111
+ headers: { Authorization: `Bearer ${token}` },
1112
+ body: formData
1113
+ });
1114
+ }
1115
+ };
1116
+ //#endregion
1117
+ //#region src/api/upload-service.ts
1118
+ var UploadService = class {
1119
+ constructor(tokenStore, registryClient) {
1120
+ this.tokenStore = tokenStore;
1121
+ this.registryClient = registryClient;
1122
+ }
1123
+ async upload(opts) {
1124
+ const compPath = await this.resolveComponentPath(opts.path);
1125
+ const tokenResult = await this.tokenStore.resolveAccessToken();
1126
+ try {
1127
+ await access(compPath);
1128
+ const content = await readFile(compPath, "utf-8");
1129
+ const fileName = path.basename(compPath);
1130
+ return this.registryClient.uploadComponent({
1131
+ content,
1132
+ fileName,
1133
+ token: tokenResult.token
1134
+ });
1135
+ } catch {
1136
+ throw new Error("File read error. Please check the file path and permissions.");
1137
+ }
1138
+ }
1139
+ async resolveComponentPath(compPath) {
1140
+ const cleanedPath = normalizeText(compPath);
1141
+ if (!cleanedPath) throw new Error("Component path is required.");
1142
+ const absolutePath = path.resolve(process.cwd(), cleanedPath);
1143
+ if (!await hasFile(absolutePath)) throw new Error(`File not found at path: ${absolutePath}`);
1144
+ return absolutePath;
1145
+ }
1146
+ };
1147
+ //#endregion
1148
+ //#region src/lib/shot-devices.ts
1149
+ const supportedDevices = [
1150
+ {
1151
+ id: "iphone.8",
1152
+ name: "iPhone 8",
1153
+ physical: {
1154
+ width: 750,
1155
+ height: 1334
1156
+ },
1157
+ colors: [
1158
+ {
1159
+ id: "silver",
1160
+ name: "Silver",
1161
+ hex: "#E4E4E2",
1162
+ fileSize: 346
1163
+ },
1164
+ {
1165
+ id: "space_gray",
1166
+ name: "Space Gray",
1167
+ hex: "#25282A",
1168
+ fileSize: 335
1169
+ },
1170
+ {
1171
+ id: "gold",
1172
+ name: "Gold",
1173
+ hex: "#F5DDC5",
1174
+ fileSize: 430
1175
+ }
1176
+ ]
1177
+ },
1178
+ {
1179
+ id: "iphone.8.plus",
1180
+ name: "iPhone 8 Plus",
1181
+ physical: {
1182
+ width: 1080,
1183
+ height: 1920
1184
+ },
1185
+ colors: [
1186
+ {
1187
+ id: "silver",
1188
+ name: "Silver",
1189
+ hex: "#E4E4E2",
1190
+ fileSize: 466
1191
+ },
1192
+ {
1193
+ id: "space_gray",
1194
+ name: "Space Gray",
1195
+ hex: "#25282A",
1196
+ fileSize: 397
1197
+ },
1198
+ {
1199
+ id: "gold",
1200
+ name: "Gold",
1201
+ hex: "#F5DDC5",
1202
+ fileSize: 458
1203
+ }
1204
+ ]
1205
+ },
1206
+ {
1207
+ id: "iphone.13",
1208
+ name: "iPhone 13",
1209
+ physical: {
1210
+ width: 1170,
1211
+ height: 2532
1212
+ },
1213
+ colors: [
1214
+ {
1215
+ id: "red",
1216
+ name: "Red",
1217
+ hex: "#A50011",
1218
+ fileSize: 81
1219
+ },
1220
+ {
1221
+ id: "starlight",
1222
+ name: "Starlight",
1223
+ hex: "#F9F3EE",
1224
+ fileSize: 80
1225
+ },
1226
+ {
1227
+ id: "midnight",
1228
+ name: "Midnight",
1229
+ hex: "#171E27",
1230
+ fileSize: 69
1231
+ },
1232
+ {
1233
+ id: "blue",
1234
+ name: "Blue",
1235
+ hex: "#215E7C",
1236
+ fileSize: 84
1237
+ },
1238
+ {
1239
+ id: "pink",
1240
+ name: "Pink",
1241
+ hex: "#FAE0D8",
1242
+ fileSize: 80
1243
+ },
1244
+ {
1245
+ id: "green",
1246
+ name: "Green",
1247
+ hex: "#364935",
1248
+ unavailable: true,
1249
+ fileSize: 0
1250
+ }
1251
+ ]
1252
+ },
1253
+ {
1254
+ id: "iphone.13.mini",
1255
+ name: "iPhone 13 Mini",
1256
+ physical: {
1257
+ width: 1080,
1258
+ height: 2340
1259
+ },
1260
+ colors: [
1261
+ {
1262
+ id: "red",
1263
+ name: "Red",
1264
+ hex: "#A50011",
1265
+ fileSize: 197
1266
+ },
1267
+ {
1268
+ id: "starlight",
1269
+ name: "Starlight",
1270
+ hex: "#F9F3EE",
1271
+ fileSize: 194
1272
+ },
1273
+ {
1274
+ id: "midnight",
1275
+ name: "Midnight",
1276
+ hex: "#171E27",
1277
+ fileSize: 173
1278
+ },
1279
+ {
1280
+ id: "blue",
1281
+ name: "Blue",
1282
+ hex: "#215E7C",
1283
+ fileSize: 203
1284
+ },
1285
+ {
1286
+ id: "pink",
1287
+ name: "Pink",
1288
+ hex: "#FAE0D8",
1289
+ fileSize: 194
1290
+ },
1291
+ {
1292
+ id: "green",
1293
+ name: "Green",
1294
+ hex: "#364935",
1295
+ unavailable: true,
1296
+ fileSize: 0
1297
+ }
1298
+ ]
1299
+ },
1300
+ {
1301
+ id: "iphone.13.pro.max",
1302
+ name: "iPhone 13 Pro Max",
1303
+ physical: {
1304
+ width: 1284,
1305
+ height: 2778
1306
+ },
1307
+ colors: [
1308
+ {
1309
+ id: "sierra_blue",
1310
+ name: "Sierra Blue",
1311
+ hex: "#9BB5CE",
1312
+ fileSize: 423
1313
+ },
1314
+ {
1315
+ id: "graphite",
1316
+ name: "Graphite",
1317
+ hex: "#5C5B57",
1318
+ fileSize: 321
1319
+ },
1320
+ {
1321
+ id: "gold",
1322
+ name: "Gold",
1323
+ hex: "#F9E5C9",
1324
+ fileSize: 435
1325
+ },
1326
+ {
1327
+ id: "silver",
1328
+ name: "Silver",
1329
+ hex: "#F5F5F0",
1330
+ fileSize: 435
1331
+ },
1332
+ {
1333
+ id: "alpine_green",
1334
+ name: "Alpine Green",
1335
+ hex: "#505F4E",
1336
+ unavailable: true,
1337
+ fileSize: 0
1338
+ }
1339
+ ]
1340
+ },
1341
+ {
1342
+ id: "iphone.14.pro",
1343
+ name: "iPhone 14 Pro",
1344
+ physical: {
1345
+ width: 1179,
1346
+ height: 2556
1347
+ },
1348
+ colors: [
1349
+ {
1350
+ id: "space_black",
1351
+ name: "Space Black",
1352
+ hex: "#4b4845",
1353
+ fileSize: 177
1354
+ },
1355
+ {
1356
+ id: "silver",
1357
+ name: "Silver",
1358
+ hex: "#e2e4e1",
1359
+ fileSize: 177
1360
+ },
1361
+ {
1362
+ id: "gold",
1363
+ name: "Gold",
1364
+ hex: "#d4c9b1",
1365
+ fileSize: 177
1366
+ },
1367
+ {
1368
+ id: "deep_purple",
1369
+ name: "Deep Purple",
1370
+ hex: "#5e5566",
1371
+ fileSize: 202
1372
+ }
1373
+ ]
1374
+ },
1375
+ {
1376
+ id: "iphone.14.pro.max",
1377
+ name: "iPhone 14 Pro Max",
1378
+ physical: {
1379
+ width: 1290,
1380
+ height: 2796
1381
+ },
1382
+ colors: [
1383
+ {
1384
+ id: "space_black",
1385
+ name: "Space Black",
1386
+ hex: "#4b4845",
1387
+ fileSize: 177
1388
+ },
1389
+ {
1390
+ id: "silver",
1391
+ name: "Silver",
1392
+ hex: "#e2e4e1",
1393
+ fileSize: 177
1394
+ },
1395
+ {
1396
+ id: "gold",
1397
+ name: "Gold",
1398
+ hex: "#d4c9b1",
1399
+ fileSize: 177
1400
+ },
1401
+ {
1402
+ id: "deep_purple",
1403
+ name: "Deep Purple",
1404
+ hex: "#5e5566",
1405
+ fileSize: 202
1406
+ }
1407
+ ]
1408
+ },
1409
+ {
1410
+ id: "iphone.15.pro",
1411
+ name: "iPhone 15 Pro",
1412
+ physical: {
1413
+ width: 1179,
1414
+ height: 2556
1415
+ },
1416
+ colors: [
1417
+ {
1418
+ id: "black",
1419
+ name: "Black Titanium",
1420
+ hex: "#1b1b1b",
1421
+ fileSize: 177
1422
+ },
1423
+ {
1424
+ id: "white",
1425
+ name: "White Titanium",
1426
+ hex: "#dddddd",
1427
+ fileSize: 177
1428
+ },
1429
+ {
1430
+ id: "natural",
1431
+ name: "Natural Titanium",
1432
+ hex: "#837F7D",
1433
+ fileSize: 177
1434
+ },
1435
+ {
1436
+ id: "blue",
1437
+ name: "Blue Titanium",
1438
+ hex: "#2F4452",
1439
+ fileSize: 202
1440
+ }
1441
+ ]
1442
+ },
1443
+ {
1444
+ id: "iphone.15.pro.max",
1445
+ name: "iPhone 15 Pro Max",
1446
+ physical: {
1447
+ width: 1290,
1448
+ height: 2796
1449
+ },
1450
+ colors: [
1451
+ {
1452
+ id: "black",
1453
+ name: "Black Titanium",
1454
+ hex: "#1b1b1b",
1455
+ fileSize: 177
1456
+ },
1457
+ {
1458
+ id: "white",
1459
+ name: "White Titanium",
1460
+ hex: "#dddddd",
1461
+ fileSize: 177
1462
+ },
1463
+ {
1464
+ id: "natural",
1465
+ name: "Natural Titanium",
1466
+ hex: "#837F7D",
1467
+ fileSize: 177
1468
+ },
1469
+ {
1470
+ id: "blue",
1471
+ name: "Blue Titanium",
1472
+ hex: "#2F4452",
1473
+ fileSize: 202
1474
+ }
1475
+ ]
1476
+ },
1477
+ {
1478
+ id: "iphone.16.pro",
1479
+ name: "iPhone 16 Pro",
1480
+ physical: {
1481
+ width: 1206,
1482
+ height: 2622
1483
+ },
1484
+ colors: [
1485
+ {
1486
+ id: "black",
1487
+ name: "Black Titanium",
1488
+ hex: "#3C3C3D",
1489
+ fileSize: 177
1490
+ },
1491
+ {
1492
+ id: "white",
1493
+ name: "White Titanium",
1494
+ hex: "#F2F1ED",
1495
+ fileSize: 177
1496
+ },
1497
+ {
1498
+ id: "natural",
1499
+ name: "Natural Titanium",
1500
+ hex: "#C2BCB2",
1501
+ fileSize: 177
1502
+ },
1503
+ {
1504
+ id: "desert",
1505
+ name: "Desert Titanium",
1506
+ hex: "#BFA48F",
1507
+ fileSize: 202
1508
+ }
1509
+ ]
1510
+ },
1511
+ {
1512
+ id: "iphone.16.plus",
1513
+ name: "iPhone 16 Plus",
1514
+ physical: {
1515
+ width: 1290,
1516
+ height: 2796
1517
+ },
1518
+ colors: [
1519
+ {
1520
+ id: "black",
1521
+ name: "Black",
1522
+ hex: "#3C4042",
1523
+ fileSize: 177
1524
+ },
1525
+ {
1526
+ id: "white",
1527
+ name: "White",
1528
+ hex: "#FAFAFA",
1529
+ fileSize: 177
1530
+ },
1531
+ {
1532
+ id: "teal",
1533
+ name: "Teal",
1534
+ hex: "#B0D4D2",
1535
+ fileSize: 177
1536
+ },
1537
+ {
1538
+ id: "ultramarine",
1539
+ name: "Ultramarine",
1540
+ hex: "#9AADF6",
1541
+ fileSize: 202
1542
+ },
1543
+ {
1544
+ id: "pink",
1545
+ name: "Pink",
1546
+ hex: "#F2ADDA",
1547
+ fileSize: 202
1548
+ }
1549
+ ]
1550
+ },
1551
+ {
1552
+ id: "iphone.16.pro.max",
1553
+ name: "iPhone 16 Pro Max",
1554
+ physical: {
1555
+ width: 1320,
1556
+ height: 2868
1557
+ },
1558
+ colors: [
1559
+ {
1560
+ id: "black",
1561
+ name: "Black Titanium",
1562
+ hex: "#3C3C3D",
1563
+ fileSize: 177
1564
+ },
1565
+ {
1566
+ id: "white",
1567
+ name: "White Titanium",
1568
+ hex: "#F2F1ED",
1569
+ fileSize: 177
1570
+ },
1571
+ {
1572
+ id: "natural",
1573
+ name: "Natural Titanium",
1574
+ hex: "#C2BCB2",
1575
+ fileSize: 177
1576
+ },
1577
+ {
1578
+ id: "desert",
1579
+ name: "Desert Titanium",
1580
+ hex: "#BFA48F",
1581
+ fileSize: 202
1582
+ }
1583
+ ]
1584
+ }
1585
+ ];
1586
+ const detectDevice = (bounds, deviceId) => {
1587
+ const found = supportedDevices.filter((device) => device.physical.width === bounds.width && device.physical.height === bounds.height);
1588
+ if (found.length === 1) return found[0];
1589
+ if (found.length > 1) return found.find((device) => device.id === deviceId) ?? found[0];
1590
+ throw new Error(`We could not detect the device for your screenshot dimensions (${bounds.width}x${bounds.height}). Supported devices: ${supportedDevices.map((device) => `"${device.id}"`).join(", ")}.`);
1591
+ };
1592
+ //#endregion
1593
+ //#region src/api/shot-service.ts
1594
+ const CREATE_NEW_COLLECTION = "__create_new_collection__";
1595
+ const SUPPORTED_SHOT_MIME_TYPES = {
1596
+ ".png": "image/png",
1597
+ ".jpg": "image/jpg",
1598
+ ".jpeg": "image/jpeg"
1599
+ };
1600
+ var ShotService = class {
1601
+ constructor(tokenStore, registryClient) {
1602
+ this.tokenStore = tokenStore;
1603
+ this.registryClient = registryClient;
1604
+ }
1605
+ async uploadShot(opts, hooks = {}) {
1606
+ const filePath = await this.resolveFilePath(opts.file);
1607
+ const mimeType = this.resolveMimeType(filePath);
1608
+ const tokenResult = await this.tokenStore.resolveAccessToken();
1609
+ const content = await this.readFileContent(filePath);
1610
+ const { Jimp } = await import("jimp");
1611
+ const device = detectDevice((await Jimp.read(content)).bitmap, opts.device);
1612
+ const selectedColor = await this.selectDeviceColor(device);
1613
+ const collections = await this.registryClient.fetchShotCollections({
1614
+ token: tokenResult.token,
1615
+ offset: 0,
1616
+ limit: 40
1617
+ });
1618
+ const selectedCollection = await this.selectCollection(collections.items);
1619
+ hooks.onUploadStart?.();
1620
+ return this.registryClient.uploadShot({
1621
+ token: tokenResult.token,
1622
+ fileName: path.basename(filePath),
1623
+ mimeType,
1624
+ content,
1625
+ collectionId: selectedCollection.collectionId || void 0,
1626
+ collectionTitle: selectedCollection.collectionTitle || void 0,
1627
+ createNewCollection: selectedCollection.createNewCollection,
1628
+ color: selectedColor
1629
+ });
1630
+ }
1631
+ async selectDeviceColor(device) {
1632
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Interactive color selection requires a TTY.");
1633
+ const availableColors = device.colors.filter((color) => !color.unavailable);
1634
+ const fallbackColors = device.colors;
1635
+ const colorChoices = availableColors.length > 0 ? availableColors : fallbackColors;
1636
+ if (!colorChoices.length) throw new Error(`No frame colors available for device "${device.id}".`);
1637
+ if (colorChoices.length === 1) return colorChoices[0].id;
1638
+ return select({
1639
+ message: `Select device color for ${device.name}:`,
1640
+ default: colorChoices[0].id,
1641
+ choices: colorChoices.map((color) => ({
1642
+ name: `${color.name} (${color.id})`,
1643
+ value: color.id,
1644
+ description: color.hex
1645
+ }))
1646
+ });
1647
+ }
1648
+ async resolveFilePath(filePath) {
1649
+ const normalizedPath = normalizeText(filePath);
1650
+ if (!normalizedPath) throw new Error("Screenshot file path is required.");
1651
+ const absolutePath = path.resolve(process.cwd(), normalizedPath);
1652
+ if (!await hasFile(absolutePath)) throw new Error(`File not found at path: ${absolutePath}`);
1653
+ return absolutePath;
1654
+ }
1655
+ async readFileContent(filePath) {
1656
+ try {
1657
+ await access(filePath);
1658
+ return await readFile(filePath);
1659
+ } catch {
1660
+ throw new Error("File read error. Please check the file path and permissions.");
1661
+ }
1662
+ }
1663
+ resolveMimeType(filePath) {
1664
+ const mimeType = SUPPORTED_SHOT_MIME_TYPES[path.extname(filePath).toLowerCase()];
1665
+ if (!mimeType) throw new Error("Unsupported screenshot file type. Supported formats: .png, .jpg, .jpeg");
1666
+ return mimeType;
1667
+ }
1668
+ async selectCollection(items) {
1669
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("Interactive collection selection requires a TTY.");
1670
+ const selected = await select({
1671
+ message: "Select collection for screenshot upload:",
1672
+ default: CREATE_NEW_COLLECTION,
1673
+ choices: [{
1674
+ name: "Create new collection",
1675
+ value: CREATE_NEW_COLLECTION,
1676
+ description: "Upload into a new screenshot collection"
1677
+ }, ...items.map((item) => ({
1678
+ name: `${item.title} (${item.collectionId.slice(0, 8)}...)`,
1679
+ value: item.collectionId,
1680
+ description: `${item.screenshotsCount} screenshot(s)`
1681
+ }))]
1682
+ });
1683
+ if (selected !== CREATE_NEW_COLLECTION) return { collectionId: selected };
1684
+ return {
1685
+ collectionId: null,
1686
+ createNewCollection: true,
1687
+ collectionTitle: normalizeText(await input({
1688
+ message: "Collection title (optional, leave empty for auto-name):",
1689
+ default: ""
1690
+ })) || void 0
1691
+ };
1692
+ }
1693
+ };
1694
+ //#endregion
1695
+ //#region src/container.ts
1696
+ const createAppContainer = (program) => {
1697
+ const container = createContainer({ injectionMode: InjectionMode.CLASSIC });
1698
+ container.register({
1699
+ program: asValue(program),
1700
+ tokenStore: asClass(TokenStore).singleton(),
1701
+ registryClient: asClass(RegistryClient).singleton(),
1702
+ authService: asClass(AuthService).singleton(),
1703
+ initService: asClass(InitService).singleton(),
1704
+ componentService: asClass(ComponentService).singleton(),
1705
+ uploadService: asClass(UploadService).singleton(),
1706
+ shotService: asClass(ShotService).singleton(),
1707
+ initCommand: asClass(InitCommand).singleton(),
1708
+ loginCommand: asClass(LoginCommand).singleton(),
1709
+ logoutCommand: asClass(LogoutCommand).singleton(),
1710
+ uploadCommand: asClass(UploadCommand).singleton(),
1711
+ shotCommand: asClass(ShotCommand).singleton(),
1712
+ addCommand: asClass(AddCommand).singleton(),
1713
+ commandRegistry: asFunction((initCommand, loginCommand, logoutCommand, addCommand, uploadCommand, shotCommand) => new CommandRegistry([
1714
+ initCommand,
1715
+ loginCommand,
1716
+ logoutCommand,
1717
+ addCommand,
1718
+ uploadCommand,
1719
+ shotCommand
1720
+ ])).singleton(),
1721
+ cliApp: asClass(CliApp).singleton()
1722
+ });
1723
+ return container;
1724
+ };
1725
+ //#endregion
1726
+ //#region src/setup.ts
1727
+ const createApp = () => {
1728
+ const program = new Command();
1729
+ program.version(CLI_VERSION).name(CLI_NAME).usage(chalk.yellow("<command> [options]")).description(chalk.hex(COLORS.PRIMARY_300)("🚀 Add unlocked WithFrame components into your project")).helpOption("-h, --help", chalk.gray("Display help information")).addHelpText("before", chalk.cyan("\n✨ WithFrame CLI - Your Component Assistant\n")).addHelpText("after", chalk.dim("\n📖 Example:\n $ export WITHFRAME_TOKEN=\"<token>\"\n $ withframe add button\n"));
1730
+ const container = createAppContainer(program);
1731
+ program.on("command:*", () => {
1732
+ console.error(chalk.red("\n❌ Invalid command: %s\n"), chalk.bold(program.args.join(" ")));
1733
+ console.log(chalk.yellow("See --help for a list of available commands.\n"));
1734
+ process.exit(1);
1735
+ });
1736
+ return container.resolve("cliApp");
1737
+ };
1738
+ //#endregion
1739
+ //#region src/utils/banner.ts
1740
+ const showBanner = () => {
1741
+ const banner = figlet.textSync("Withframe", {
1742
+ font: "Standard",
1743
+ horizontalLayout: "default",
1744
+ verticalLayout: "default",
1745
+ width: 80,
1746
+ whitespaceBreak: true
1747
+ });
1748
+ const coloredBanner = chalk.hex(COLORS.PRIMARY_600)(banner);
1749
+ console.log(coloredBanner);
1750
+ };
1751
+ //#endregion
1752
+ //#region src/index.ts
1753
+ if (process.argv.length === 2) showBanner();
1754
+ createApp().run(process.argv);
1755
+ //#endregion
1756
+ export {};