@sublime-ui/devkit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,1438 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/lib/probe.ts
7
+ import { join } from "path";
8
+
9
+ // src/util/exec.ts
10
+ import { execa } from "execa";
11
+ async function run(file, args, opts = {}) {
12
+ const result = await execa(file, args, {
13
+ ...opts.cwd === void 0 ? {} : { cwd: opts.cwd },
14
+ env: { ...process.env, ...opts.env },
15
+ reject: false,
16
+ all: false
17
+ });
18
+ return {
19
+ stdout: result.stdout ?? "",
20
+ stderr: result.stderr ?? "",
21
+ exitCode: result.exitCode ?? 1
22
+ };
23
+ }
24
+ async function runInherit(file, args, opts = {}) {
25
+ const result = await execa(file, args, {
26
+ ...opts.cwd === void 0 ? {} : { cwd: opts.cwd },
27
+ env: { ...process.env, ...opts.env },
28
+ stdio: "inherit",
29
+ reject: false
30
+ });
31
+ return result.exitCode ?? 1;
32
+ }
33
+
34
+ // src/lib/detect.ts
35
+ function parseJavaVersion(stderr) {
36
+ const match = stderr.match(/version "([^"]+)"/);
37
+ return match ? match[1] ?? null : null;
38
+ }
39
+ function parseAdbVersion(stdout) {
40
+ const match = stdout.match(/version (\d+\.\d+\.\d+)/);
41
+ return match ? match[1] ?? null : null;
42
+ }
43
+ function parseSdkmanagerInstalled(stdout) {
44
+ const result = {};
45
+ for (const rawLine of stdout.split("\n")) {
46
+ if (!rawLine.includes("|")) continue;
47
+ const cells = rawLine.split("|").map((c) => c.trim());
48
+ const path = cells[0];
49
+ const version = cells[1];
50
+ if (path === void 0 || version === void 0) continue;
51
+ if (path === "" || path === "Path" || path.startsWith("---")) continue;
52
+ if (version === "Version") continue;
53
+ result[path] = version;
54
+ }
55
+ return result;
56
+ }
57
+
58
+ // src/lib/requirements.ts
59
+ var REQUIREMENTS = {
60
+ node: { min: 18 },
61
+ jdk: { major: 17 },
62
+ ndk: "27.1.12297006",
63
+ cmake: "3.22.1",
64
+ buildTools: "35.0.0",
65
+ platform: "android-35"
66
+ };
67
+ var JDK_DOWNLOAD = {
68
+ windowsX64: "https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.13%2B11/OpenJDK17U-jdk_x64_windows_hotspot_17.0.13_11.zip"
69
+ };
70
+ function leadingMajor(actual) {
71
+ if (actual === null) return null;
72
+ const match = actual.match(/\d+/);
73
+ if (match === null) return null;
74
+ return Number.parseInt(match[0], 10);
75
+ }
76
+ function satisfiesMajor(actual, requiredMajor) {
77
+ const major = leadingMajor(actual);
78
+ return major !== null && major >= requiredMajor;
79
+ }
80
+ function satisfiesExact(actual, required) {
81
+ return actual !== null && actual === required;
82
+ }
83
+
84
+ // src/lib/probe.ts
85
+ function resolveAndroidHome(env) {
86
+ return env["ANDROID_HOME"] ?? env["ANDROID_SDK_ROOT"] ?? null;
87
+ }
88
+ function sdkmanagerPath(androidHome) {
89
+ const bin = process.platform === "win32" ? "sdkmanager.bat" : "sdkmanager";
90
+ return join(androidHome, "cmdline-tools", "latest", "bin", bin);
91
+ }
92
+ async function gatherProbes() {
93
+ const nodeRes = await run(process.execPath, ["-v"]);
94
+ const javaRes = await run("java", ["-version"]);
95
+ const androidHome = resolveAndroidHome(process.env);
96
+ let sdkmanager = false;
97
+ let installed = {};
98
+ if (androidHome !== null) {
99
+ const smPath = sdkmanagerPath(androidHome);
100
+ const listRes = await run(smPath, ["--list_installed"]);
101
+ sdkmanager = listRes.exitCode === 0;
102
+ installed = parseSdkmanagerInstalled(listRes.stdout);
103
+ }
104
+ const adbRes = await run("adb", ["--version"]);
105
+ return {
106
+ node: nodeRes.stdout.trim() || null,
107
+ jdk17: parseJavaVersion(javaRes.stderr),
108
+ androidHome,
109
+ sdkmanager,
110
+ platformTools: parseAdbVersion(adbRes.stdout) !== null,
111
+ ndk: installed[`ndk;${REQUIREMENTS.ndk}`] ?? null,
112
+ cmake: installed[`cmake;${REQUIREMENTS.cmake}`] ?? null
113
+ };
114
+ }
115
+
116
+ // src/lib/doctor-report.ts
117
+ function buildDoctorReport(probes) {
118
+ const rows = [
119
+ {
120
+ label: "Node >=18",
121
+ ok: satisfiesMajor(probes.node, REQUIREMENTS.node.min),
122
+ detail: probes.node ?? "not found"
123
+ },
124
+ {
125
+ label: "JDK 17",
126
+ ok: satisfiesMajor(probes.jdk17, REQUIREMENTS.jdk.major),
127
+ detail: probes.jdk17 ?? "not found (run: sublime setup)"
128
+ },
129
+ {
130
+ label: "ANDROID_HOME",
131
+ ok: probes.androidHome !== null,
132
+ detail: probes.androidHome ?? "not set"
133
+ },
134
+ {
135
+ label: "sdkmanager",
136
+ ok: probes.sdkmanager,
137
+ detail: probes.sdkmanager ? "cmdline-tools present" : "missing"
138
+ },
139
+ {
140
+ label: "platform-tools",
141
+ ok: probes.platformTools,
142
+ detail: probes.platformTools ? "adb present" : "missing"
143
+ },
144
+ {
145
+ label: `NDK ${REQUIREMENTS.ndk}`,
146
+ ok: satisfiesExact(probes.ndk, REQUIREMENTS.ndk),
147
+ detail: probes.ndk ?? "missing (auto-installed on build)"
148
+ },
149
+ {
150
+ label: `CMake ${REQUIREMENTS.cmake}`,
151
+ ok: satisfiesExact(probes.cmake, REQUIREMENTS.cmake),
152
+ detail: probes.cmake ?? "missing (auto-installed on build)"
153
+ }
154
+ ];
155
+ return { rows, ok: rows.every((r) => r.ok) };
156
+ }
157
+
158
+ // src/util/log.ts
159
+ import pc from "picocolors";
160
+ var log = {
161
+ info: (m) => console.log(m),
162
+ step: (m) => console.log(pc.cyan(`\u2192 ${m}`)),
163
+ success: (m) => console.log(pc.green(`\u2713 ${m}`)),
164
+ warn: (m) => console.log(pc.yellow(`! ${m}`)),
165
+ error: (m) => console.error(pc.red(`\u2717 ${m}`)),
166
+ table: (rows) => {
167
+ const width = rows.reduce((m, r) => Math.max(m, r.label.length), 0);
168
+ for (const r of rows) {
169
+ const mark = r.ok ? pc.green("\u2713") : pc.red("\u2717");
170
+ console.log(`${mark} ${r.label.padEnd(width)} ${pc.dim(r.detail)}`);
171
+ }
172
+ }
173
+ };
174
+
175
+ // src/commands/doctor.ts
176
+ async function doctorCommand() {
177
+ log.step("Checking environment for offline Android builds\u2026");
178
+ const probes = await gatherProbes();
179
+ const report = buildDoctorReport(probes);
180
+ log.table(report.rows);
181
+ if (report.ok) {
182
+ log.success("Environment ready. Run: sublime build");
183
+ return 0;
184
+ }
185
+ log.warn("Some requirements are missing. Run: sublime setup");
186
+ return 1;
187
+ }
188
+
189
+ // src/lib/jdk.ts
190
+ import { existsSync, mkdirSync, rmSync } from "fs";
191
+ import { homedir } from "os";
192
+ import { join as join2 } from "path";
193
+ function sublimeHomeDir() {
194
+ return join2(homedir(), ".sublime");
195
+ }
196
+ async function ensurePortableJdk17() {
197
+ if (process.platform !== "win32") {
198
+ const home = process.env["JAVA_HOME"];
199
+ if (home && existsSync(home)) return home;
200
+ throw new Error(
201
+ "JDK 17 required. Install it (e.g. `brew install temurin@17`) and set JAVA_HOME."
202
+ );
203
+ }
204
+ const root = join2(sublimeHomeDir(), "jdk-17");
205
+ const marker = join2(root, "bin", "java.exe");
206
+ if (existsSync(marker)) return root;
207
+ mkdirSync(sublimeHomeDir(), { recursive: true });
208
+ const zipPath = join2(sublimeHomeDir(), "jdk-17.zip");
209
+ const tmp = join2(sublimeHomeDir(), "jdk-17-tmp");
210
+ rmSync(tmp, { recursive: true, force: true });
211
+ rmSync(root, { recursive: true, force: true });
212
+ log.step("Downloading portable JDK 17 (Temurin)\u2026");
213
+ const dl = await runInherit("powershell", [
214
+ "-NoProfile",
215
+ "-Command",
216
+ `Invoke-WebRequest -Uri '${JDK_DOWNLOAD.windowsX64}' -OutFile '${zipPath}'`
217
+ ]);
218
+ if (dl !== 0) {
219
+ throw new Error("Failed to download JDK 17 (check your network connection).");
220
+ }
221
+ log.step("Extracting JDK 17\u2026");
222
+ const ex = await runInherit("powershell", [
223
+ "-NoProfile",
224
+ "-Command",
225
+ `Expand-Archive -Path '${zipPath}' -DestinationPath '${tmp}' -Force`
226
+ ]);
227
+ if (ex !== 0) {
228
+ throw new Error("Failed to extract the JDK 17 archive.");
229
+ }
230
+ const inner = (await run("powershell", [
231
+ "-NoProfile",
232
+ "-Command",
233
+ `(Get-ChildItem -Directory '${tmp}' | Select-Object -First 1).FullName`
234
+ ])).stdout.trim();
235
+ if (inner === "") {
236
+ throw new Error("JDK 17 archive contained no extracted folder.");
237
+ }
238
+ const mv = await runInherit("powershell", [
239
+ "-NoProfile",
240
+ "-Command",
241
+ `Move-Item -Path '${inner}' -Destination '${root}' -Force`
242
+ ]);
243
+ if (mv !== 0) {
244
+ throw new Error("Failed to move the extracted JDK 17 into place.");
245
+ }
246
+ rmSync(tmp, { recursive: true, force: true });
247
+ rmSync(zipPath, { force: true });
248
+ if (!existsSync(marker)) {
249
+ throw new Error("Portable JDK 17 install incomplete (java.exe missing).");
250
+ }
251
+ return root;
252
+ }
253
+
254
+ // src/commands/setup.ts
255
+ async function setupCommand() {
256
+ if (process.platform !== "win32") {
257
+ log.info("Guided setup (macOS/Linux):");
258
+ log.info(" 1. Install Temurin JDK 17 and set JAVA_HOME.");
259
+ log.info(" 2. Install Android cmdline-tools; set ANDROID_HOME.");
260
+ log.info(' 3. sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0"');
261
+ log.info(" 4. Re-run: sublime doctor");
262
+ return 0;
263
+ }
264
+ log.step("Setting up Windows build environment\u2026");
265
+ const jdk = await ensurePortableJdk17();
266
+ log.success(`Portable JDK 17 ready at ${jdk}`);
267
+ if (resolveAndroidHome(process.env) === null) {
268
+ log.warn("ANDROID_HOME is not set. Install Android SDK cmdline-tools and set ANDROID_HOME, then re-run: sublime doctor");
269
+ return 1;
270
+ }
271
+ log.success("Setup complete. Run: sublime doctor");
272
+ return 0;
273
+ }
274
+
275
+ // src/commands/build.ts
276
+ import { existsSync as existsSync3, readFileSync, writeFileSync, statSync } from "fs";
277
+ import { join as join4 } from "path";
278
+
279
+ // src/lib/gradle.ts
280
+ import { resolve } from "path";
281
+
282
+ // src/lib/sdkmanager.ts
283
+ import { existsSync as existsSync2, readdirSync, rmSync as rmSync2 } from "fs";
284
+ import { join as join3 } from "path";
285
+ function isValidNdk(ndkDir) {
286
+ if (!existsSync2(join3(ndkDir, "source.properties"))) return false;
287
+ const hasNdkBuild = existsSync2(join3(ndkDir, "ndk-build")) || existsSync2(join3(ndkDir, "ndk-build.cmd"));
288
+ if (!hasNdkBuild) return false;
289
+ const llvmBin = join3(ndkDir, "toolchains", "llvm", "prebuilt");
290
+ if (!existsSync2(llvmBin)) return false;
291
+ for (const host of readdirSync(llvmBin)) {
292
+ const bin = join3(llvmBin, host, "bin");
293
+ if (existsSync2(join3(bin, "clang.exe")) || existsSync2(join3(bin, "clang"))) {
294
+ return true;
295
+ }
296
+ }
297
+ return false;
298
+ }
299
+ function ndkDirFor(androidHome, id) {
300
+ const m = id.match(/^ndk;(.+)$/);
301
+ return m ? join3(androidHome, "ndk", m[1] ?? "") : null;
302
+ }
303
+ async function ensureComponents(androidHome, ids, jdk17Home) {
304
+ if (ids.length === 0) return;
305
+ const smPath = sdkmanagerPath(androidHome);
306
+ const env = { JAVA_HOME: jdk17Home, ANDROID_HOME: androidHome };
307
+ for (const id of ids) {
308
+ const ndkDir = ndkDirFor(androidHome, id);
309
+ if (ndkDir !== null && existsSync2(ndkDir) && !isValidNdk(ndkDir)) {
310
+ log.warn(`Removing corrupt NDK at ${ndkDir}`);
311
+ rmSync2(ndkDir, { recursive: true, force: true });
312
+ }
313
+ log.step(`Installing ${id} \u2026`);
314
+ const res = await run(smPath, [`--sdk_root=${androidHome}`, id, "--channel=0"], {
315
+ env
316
+ });
317
+ if (res.exitCode !== 0) {
318
+ await run(smPath, [`--sdk_root=${androidHome}`, "--licenses"], { env });
319
+ const retry = await run(
320
+ smPath,
321
+ [`--sdk_root=${androidHome}`, id, "--channel=0"],
322
+ { env }
323
+ );
324
+ if (retry.exitCode !== 0) {
325
+ throw new Error(`Failed to install ${id}:
326
+ ${retry.stderr || retry.stdout}`);
327
+ }
328
+ }
329
+ if (ndkDir !== null && !isValidNdk(ndkDir)) {
330
+ throw new Error(`NDK install incomplete at ${ndkDir}`);
331
+ }
332
+ }
333
+ }
334
+
335
+ // src/lib/gradle.ts
336
+ function parseMissingSdkComponents(output) {
337
+ const idPattern = /\b([a-z][a-z-]*(?:;[A-Za-z0-9._-]+)+)/g;
338
+ const seen = /* @__PURE__ */ new Set();
339
+ const result = [];
340
+ for (const match of output.matchAll(idPattern)) {
341
+ const id = match[1];
342
+ if (id === void 0 || seen.has(id)) continue;
343
+ seen.add(id);
344
+ result.push(id);
345
+ }
346
+ return result;
347
+ }
348
+ function gradlewPath(projectAndroidDir) {
349
+ const script = process.platform === "win32" ? "gradlew.bat" : "gradlew";
350
+ return resolve(projectAndroidDir, script);
351
+ }
352
+ async function defaultRunner(androidDir, task, jdk17Home, androidHome) {
353
+ const gw = gradlewPath(androidDir);
354
+ const env = { JAVA_HOME: jdk17Home, ANDROID_HOME: androidHome };
355
+ const res = await run(gw, [task, "--no-daemon", "--stacktrace"], {
356
+ cwd: resolve(androidDir),
357
+ env
358
+ });
359
+ process.stdout.write(res.stdout);
360
+ process.stderr.write(res.stderr);
361
+ return { exitCode: res.exitCode, output: `${res.stdout}
362
+ ${res.stderr}` };
363
+ }
364
+ async function runGradleWithHealing(opts) {
365
+ const maxAttempts = opts.maxAttempts ?? 4;
366
+ const runner = opts.runner ?? ((task) => defaultRunner(opts.androidDir, task, opts.jdk17Home, opts.androidHome));
367
+ const installer = opts.installer ?? ((ids) => ensureComponents(opts.androidHome, ids, opts.jdk17Home));
368
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
369
+ log.step(`Gradle ${opts.task} (attempt ${attempt}/${maxAttempts})\u2026`);
370
+ const result = await runner(opts.task);
371
+ if (result.exitCode === 0) {
372
+ log.success(`Gradle ${opts.task} succeeded.`);
373
+ return;
374
+ }
375
+ const missing = parseMissingSdkComponents(result.output);
376
+ if (missing.length === 0) {
377
+ throw new Error(
378
+ `Gradle failed with no installable SDK component to recover.
379
+ ${result.output.slice(-2e3)}`
380
+ );
381
+ }
382
+ log.warn(`Missing SDK components: ${missing.join(", ")} \u2014 installing\u2026`);
383
+ await installer(missing);
384
+ }
385
+ throw new Error(`Gradle ${opts.task} failed after ${maxAttempts} attempts.`);
386
+ }
387
+
388
+ // src/commands/build.ts
389
+ function gradleTaskFor(opts) {
390
+ if (opts.aab) return "bundleRelease";
391
+ return opts.release ? "assembleRelease" : "assembleDebug";
392
+ }
393
+ function readAndroidPackageId(appJsonPath) {
394
+ if (!existsSync3(appJsonPath)) return null;
395
+ const json = JSON.parse(readFileSync(appJsonPath, "utf8"));
396
+ return json.expo?.android?.package ?? null;
397
+ }
398
+ function findApk(projectDir, variant) {
399
+ const file = variant === "release" ? "app-release.apk" : "app-debug.apk";
400
+ const p = join4(
401
+ projectDir,
402
+ "android",
403
+ "app",
404
+ "build",
405
+ "outputs",
406
+ "apk",
407
+ variant,
408
+ file
409
+ );
410
+ return existsSync3(p) ? p : null;
411
+ }
412
+ function findAab(projectDir) {
413
+ const p = join4(
414
+ projectDir,
415
+ "android",
416
+ "app",
417
+ "build",
418
+ "outputs",
419
+ "bundle",
420
+ "release",
421
+ "app-release.aab"
422
+ );
423
+ return existsSync3(p) ? p : null;
424
+ }
425
+ function ensureLocalProperties(projectDir, androidHome) {
426
+ const target = join4(projectDir, "android", "local.properties");
427
+ if (existsSync3(target)) return;
428
+ const escaped = androidHome.replace(/\\/g, "\\\\");
429
+ writeFileSync(target, `sdk.dir=${escaped}
430
+ `);
431
+ }
432
+ async function buildCommand(opts) {
433
+ const androidHome = resolveAndroidHome(process.env);
434
+ if (androidHome === null) {
435
+ log.error("ANDROID_HOME/ANDROID_SDK_ROOT not set. Run: sublime doctor");
436
+ return 1;
437
+ }
438
+ const androidDir = join4(opts.project, "android");
439
+ if (!existsSync3(androidDir)) {
440
+ log.step("Generating native Android project (expo prebuild)\u2026");
441
+ const code = await runInherit("npx", [
442
+ "expo",
443
+ "prebuild",
444
+ "--platform",
445
+ "android",
446
+ "--no-install"
447
+ ], { cwd: opts.project });
448
+ if (code !== 0 || !existsSync3(androidDir)) {
449
+ log.error("expo prebuild failed.");
450
+ return 1;
451
+ }
452
+ }
453
+ ensureLocalProperties(opts.project, androidHome);
454
+ const jdk17Home = await ensurePortableJdk17();
455
+ const task = gradleTaskFor({ release: opts.release, aab: opts.aab });
456
+ await runGradleWithHealing({ androidDir, task, jdk17Home, androidHome });
457
+ if (!opts.aab) {
458
+ const variant = opts.release ? "release" : "debug";
459
+ const apk = findApk(opts.project, variant);
460
+ if (apk === null) {
461
+ log.error(`Build reported success but no ${variant} APK was found.`);
462
+ return 1;
463
+ }
464
+ const mb = (statSync(apk).size / (1024 * 1024)).toFixed(1);
465
+ log.success(`APK ready: ${apk} (${mb} MB)`);
466
+ } else {
467
+ const aab = findAab(opts.project);
468
+ if (aab === null) {
469
+ log.error("Build reported success but no AAB was found.");
470
+ return 1;
471
+ }
472
+ const mb = (statSync(aab).size / (1024 * 1024)).toFixed(1);
473
+ log.success(`AAB ready: ${aab} (${mb} MB)`);
474
+ }
475
+ return 0;
476
+ }
477
+
478
+ // src/commands/run.ts
479
+ import { join as join5 } from "path";
480
+
481
+ // src/lib/android.ts
482
+ function parseAdbDevices(stdout) {
483
+ const serials = [];
484
+ for (const line of stdout.split("\n")) {
485
+ const trimmed = line.trim();
486
+ if (trimmed === "" || trimmed.startsWith("List of devices")) continue;
487
+ const [serial, state] = trimmed.split(/\s+/);
488
+ if (serial !== void 0 && state === "device") serials.push(serial);
489
+ }
490
+ return serials;
491
+ }
492
+ async function listDevices() {
493
+ const res = await run("adb", ["devices"]);
494
+ return parseAdbDevices(res.stdout);
495
+ }
496
+ async function installApk(serial, apkPath) {
497
+ const res = await run("adb", ["-s", serial, "install", "-r", apkPath]);
498
+ if (res.exitCode !== 0) {
499
+ throw new Error(`adb install failed:
500
+ ${res.stderr || res.stdout}`);
501
+ }
502
+ }
503
+ async function launchActivity(serial, pkg) {
504
+ const res = await run("adb", [
505
+ "-s",
506
+ serial,
507
+ "shell",
508
+ "monkey",
509
+ "-p",
510
+ pkg,
511
+ "-c",
512
+ "android.intent.category.LAUNCHER",
513
+ "1"
514
+ ]);
515
+ if (res.exitCode !== 0) {
516
+ throw new Error(`Failed to launch ${pkg}:
517
+ ${res.stderr || res.stdout}`);
518
+ }
519
+ }
520
+
521
+ // src/commands/run.ts
522
+ async function runCommand(opts) {
523
+ const apk = findApk(opts.project, "release") ?? findApk(opts.project, "debug");
524
+ if (apk === null) {
525
+ log.error("No APK found. Run: sublime build");
526
+ return 1;
527
+ }
528
+ const devices = await listDevices();
529
+ if (devices.length === 0) {
530
+ log.error("No connected device/emulator. Start one and retry.");
531
+ return 1;
532
+ }
533
+ const serial = opts.device ?? devices[0];
534
+ if (serial === void 0) return 1;
535
+ log.step(`Installing ${apk} on ${serial}\u2026`);
536
+ await installApk(serial, apk);
537
+ const pkg = readAndroidPackageId(join5(opts.project, "app.json"));
538
+ if (pkg === null) {
539
+ log.error("Could not read android.package from app.json");
540
+ return 1;
541
+ }
542
+ log.step(`Launching ${pkg}\u2026`);
543
+ await launchActivity(serial, pkg);
544
+ log.success("Launched.");
545
+ return 0;
546
+ }
547
+
548
+ // src/cli.ts
549
+ import { input } from "@inquirer/prompts";
550
+
551
+ // src/commands/make-model.ts
552
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
553
+ import { join as join7 } from "path";
554
+
555
+ // src/lib/generators/config.ts
556
+ import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
557
+ import { join as join6 } from "path";
558
+ var DEFAULT_CONFIG = {
559
+ modelsDir: "src/models",
560
+ componentsDir: "src/components",
561
+ themeDir: "src/theme",
562
+ navigationDir: "src/navigation",
563
+ importAlias: "@sublime-ui"
564
+ };
565
+ function loadConfig(cwd) {
566
+ const path = join6(cwd, "sublime.config.json");
567
+ if (!existsSync4(path)) return { ...DEFAULT_CONFIG };
568
+ const raw = JSON.parse(readFileSync2(path, "utf8"));
569
+ const result = { ...DEFAULT_CONFIG };
570
+ for (const key of Object.keys(DEFAULT_CONFIG)) {
571
+ const value = raw[key];
572
+ if (typeof value === "string") result[key] = value;
573
+ }
574
+ if (raw.desktop && typeof raw.desktop === "object") {
575
+ const desktop = {};
576
+ if (typeof raw.desktop.dir === "string") desktop.dir = raw.desktop.dir;
577
+ if (typeof raw.desktop.nativeDir === "string") desktop.nativeDir = raw.desktop.nativeDir;
578
+ result.desktop = desktop;
579
+ }
580
+ return result;
581
+ }
582
+
583
+ // src/lib/generators/names.ts
584
+ function pascalCase(input2) {
585
+ return input2.split(/[^A-Za-z0-9]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
586
+ }
587
+ function pluralize(word) {
588
+ const w = word.toLowerCase();
589
+ if (/[^aeiou]y$/.test(w)) return w.slice(0, -1) + "ies";
590
+ if (/(s|x|ch|sh)$/.test(w)) return w + "es";
591
+ return w + "s";
592
+ }
593
+ function deriveNames(name) {
594
+ const className = pascalCase(name);
595
+ const plural = pluralize(className);
596
+ return {
597
+ className,
598
+ resource: `/${plural}`,
599
+ sliceName: plural,
600
+ fileName: className
601
+ };
602
+ }
603
+
604
+ // src/lib/generators/fields.ts
605
+ var SCALARS = /* @__PURE__ */ new Set(["string", "number", "boolean"]);
606
+ function parseFields(input2) {
607
+ const fields = [];
608
+ const warnings = [];
609
+ for (const part of input2.split(",")) {
610
+ const trimmed = part.trim();
611
+ if (trimmed === "") continue;
612
+ const [rawName, rawType] = trimmed.split(":").map((s) => s.trim());
613
+ if (rawName === void 0 || rawName === "") continue;
614
+ const type = rawType ?? "string";
615
+ if (SCALARS.has(type) || type.endsWith("[]")) {
616
+ fields.push({ name: rawName, tsType: type });
617
+ } else {
618
+ warnings.push(`Field "${rawName}" has unknown type "${type}" \u2014 defaulting to string.`);
619
+ fields.push({ name: rawName, tsType: "string" });
620
+ }
621
+ }
622
+ return { fields, warnings };
623
+ }
624
+
625
+ // src/lib/generators/render-model.ts
626
+ function renderModel(opts) {
627
+ const hasId = opts.fields.some((f) => f.name === "id");
628
+ const fields = hasId ? opts.fields : [{ name: "id", tsType: "number" }, ...opts.fields];
629
+ const declares = fields.map((f) => ` declare ${f.name}: ${f.tsType};`).join("\n");
630
+ return `import { Model, registerModel } from '${opts.importAlias}/framework';
631
+
632
+ export class ${opts.className} extends Model {
633
+ protected static resource = '${opts.resource}';
634
+ ${declares}
635
+ }
636
+
637
+ registerModel(${opts.className});
638
+ `;
639
+ }
640
+
641
+ // src/lib/generators/barrel.ts
642
+ function updateBarrel(existing, line) {
643
+ const lines = existing.split("\n").map((l) => l.trim());
644
+ if (lines.includes(line.trim())) return existing;
645
+ const base = existing.length > 0 && !existing.endsWith("\n") ? existing + "\n" : existing;
646
+ return `${base}${line}
647
+ `;
648
+ }
649
+
650
+ // src/lib/generators/write.ts
651
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
652
+ import { dirname } from "path";
653
+ var FileExistsError = class _FileExistsError extends Error {
654
+ path;
655
+ constructor(path) {
656
+ super(`File already exists: ${path} (use --force to overwrite)`);
657
+ this.name = "FileExistsError";
658
+ this.path = path;
659
+ Object.setPrototypeOf(this, _FileExistsError.prototype);
660
+ }
661
+ };
662
+ function safeWrite(path, content, force) {
663
+ if (existsSync5(path) && !force) throw new FileExistsError(path);
664
+ mkdirSync2(dirname(path), { recursive: true });
665
+ writeFileSync2(path, content);
666
+ }
667
+
668
+ // src/commands/make-model.ts
669
+ async function makeModel(opts) {
670
+ const cfg = loadConfig(opts.cwd);
671
+ const names = deriveNames(opts.name);
672
+ const raw = opts.fields ?? (opts.promptFields ? await opts.promptFields() : "");
673
+ const { fields, warnings } = parseFields(raw);
674
+ for (const w of warnings) log.warn(w);
675
+ const content = renderModel({
676
+ className: names.className,
677
+ resource: opts.resource ?? names.resource,
678
+ importAlias: cfg.importAlias,
679
+ fields
680
+ });
681
+ const modelPath = join7(opts.cwd, cfg.modelsDir, `${names.fileName}.ts`);
682
+ const barrelPath = join7(opts.cwd, cfg.modelsDir, "index.ts");
683
+ try {
684
+ safeWrite(modelPath, content, opts.force);
685
+ const existing = existsSync6(barrelPath) ? readFileSync3(barrelPath, "utf8") : "";
686
+ safeWrite(barrelPath, updateBarrel(existing, `export * from './${names.fileName}.js';`), true);
687
+ log.success(`Created ${modelPath}`);
688
+ return 0;
689
+ } catch (err) {
690
+ if (err instanceof FileExistsError) {
691
+ log.error(err.message);
692
+ return 1;
693
+ }
694
+ throw err;
695
+ }
696
+ }
697
+
698
+ // src/commands/make-component.ts
699
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
700
+ import { join as join8 } from "path";
701
+
702
+ // src/lib/generators/render-component.ts
703
+ function renderComponentTypes(name) {
704
+ return `import type { ReactNode } from 'react';
705
+
706
+ export type Variant = 'solid' | 'soft' | 'outline' | 'ghost';
707
+ export type Tone = 'primary' | 'success' | 'danger' | 'warning' | 'info' | 'neutral';
708
+ export type Size = 'sm' | 'md' | 'lg';
709
+
710
+ export interface ${name}Props {
711
+ children?: ReactNode;
712
+ variant?: Variant;
713
+ tone?: Tone;
714
+ size?: Size;
715
+ testID?: string;
716
+ }
717
+ `;
718
+ }
719
+ function renderComponentWeb(name, mobileOnly, importAlias) {
720
+ if (mobileOnly) {
721
+ return `import type { ${name}Props } from './${name}.types.js';
722
+
723
+ /** ${name} is mobile-only and renders nothing on web. */
724
+ export function ${name}(_props: ${name}Props) {
725
+ if (process.env.NODE_ENV !== 'production') {
726
+ console.warn('${name} is mobile-only and renders nothing on web.');
727
+ }
728
+ return null;
729
+ }
730
+ `;
731
+ }
732
+ return `import { Box } from '@mui/material';
733
+ import { useTokens } from '${importAlias}/library';
734
+ import type { ${name}Props } from './${name}.types.js';
735
+
736
+ export function ${name}({ children, testID }: ${name}Props) {
737
+ const tokens = useTokens();
738
+ return (
739
+ <Box data-testid={testID} sx={{ borderRadius: \`\${tokens.radii.md}px\` }}>
740
+ {children}
741
+ </Box>
742
+ );
743
+ }
744
+ `;
745
+ }
746
+ function renderComponentNative(name, importAlias) {
747
+ return `import { View } from 'react-native';
748
+ import { useTokens } from '${importAlias}/library';
749
+ import type { ${name}Props } from './${name}.types.js';
750
+
751
+ export function ${name}({ children, testID }: ${name}Props) {
752
+ const tokens = useTokens();
753
+ return (
754
+ <View testID={testID} style={{ borderRadius: tokens.radii.md }}>
755
+ {children}
756
+ </View>
757
+ );
758
+ }
759
+ `;
760
+ }
761
+ function renderComponentIndex(name) {
762
+ return `export { ${name} } from './${name}.js';
763
+ export type { ${name}Props } from './${name}.types.js';
764
+ `;
765
+ }
766
+
767
+ // src/commands/make-component.ts
768
+ async function makeComponent(opts) {
769
+ const cfg = loadConfig(opts.cwd);
770
+ const dir = join8(opts.cwd, cfg.componentsDir, opts.name);
771
+ try {
772
+ safeWrite(join8(dir, `${opts.name}.types.ts`), renderComponentTypes(opts.name), opts.force);
773
+ safeWrite(join8(dir, `${opts.name}.tsx`), renderComponentWeb(opts.name, opts.mobileOnly, cfg.importAlias), opts.force);
774
+ safeWrite(join8(dir, `${opts.name}.native.tsx`), renderComponentNative(opts.name, cfg.importAlias), opts.force);
775
+ safeWrite(join8(dir, "index.ts"), renderComponentIndex(opts.name), opts.force);
776
+ const barrelPath = join8(opts.cwd, cfg.componentsDir, "index.ts");
777
+ const existing = existsSync7(barrelPath) ? readFileSync4(barrelPath, "utf8") : "";
778
+ safeWrite(barrelPath, updateBarrel(existing, `export * from './${opts.name}/index.js';`), true);
779
+ log.success(`Created ${dir}`);
780
+ return 0;
781
+ } catch (err) {
782
+ if (err instanceof FileExistsError) {
783
+ log.error(err.message);
784
+ return 1;
785
+ }
786
+ throw err;
787
+ }
788
+ }
789
+
790
+ // src/commands/theme-init.ts
791
+ import { findPackageJSON } from "module";
792
+ import { pathToFileURL } from "url";
793
+ import { dirname as dirname2, join as join9 } from "path";
794
+
795
+ // src/lib/generators/render-tokens.ts
796
+ function renderTokensWrapper(importAlias) {
797
+ return `import data from './tokens.json';
798
+ import type { SublimeTokens } from '${importAlias}/library';
799
+
800
+ /** App design tokens. Edit tokens.json (the devkit-server customizer writes here). */
801
+ export const tokens = data as SublimeTokens;
802
+ `;
803
+ }
804
+
805
+ // src/commands/theme-init.ts
806
+ async function resolveDefaultTokens(cwd) {
807
+ const base = pathToFileURL(join9(cwd, "noop.js")).href;
808
+ const pkgJsonPath = findPackageJSON("@sublime-ui/library", base);
809
+ if (!pkgJsonPath) throw new Error("@sublime-ui/library not found");
810
+ const tokensJs = join9(dirname2(pkgJsonPath), "dist", "tokens", "tokens.js");
811
+ const mod = await import(pathToFileURL(tokensJs).href);
812
+ return mod.defaultTokens;
813
+ }
814
+ async function themeInit(opts) {
815
+ const cfg = loadConfig(opts.cwd);
816
+ let tokens;
817
+ try {
818
+ tokens = await (opts.loadDefaultTokens ?? (() => resolveDefaultTokens(opts.cwd)))();
819
+ } catch {
820
+ log.error("Could not resolve @sublime-ui/library. Install it in this app first.");
821
+ return 1;
822
+ }
823
+ const jsonPath = join9(opts.cwd, cfg.themeDir, "tokens.json");
824
+ const wrapperPath = join9(opts.cwd, cfg.themeDir, "tokens.ts");
825
+ try {
826
+ safeWrite(jsonPath, JSON.stringify(tokens, null, 2) + "\n", opts.force);
827
+ safeWrite(wrapperPath, renderTokensWrapper(cfg.importAlias), opts.force);
828
+ log.success(`Created ${jsonPath} and ${wrapperPath}`);
829
+ return 0;
830
+ } catch (err) {
831
+ if (err instanceof FileExistsError) {
832
+ log.error(err.message);
833
+ return 1;
834
+ }
835
+ throw err;
836
+ }
837
+ }
838
+
839
+ // src/commands/build-nav.ts
840
+ import { join as join10 } from "path";
841
+ import { readFileSync as readFileSync5, watch as fsWatch } from "fs";
842
+
843
+ // src/lib/navigation/load-storybook.ts
844
+ import { pathToFileURL as pathToFileURL2 } from "url";
845
+ function componentName(component) {
846
+ if (typeof component === "function" && component.name) return component.name;
847
+ if (component != null && typeof component === "object" && "displayName" in component && typeof component.displayName === "string") {
848
+ return component.displayName;
849
+ }
850
+ return "Anonymous";
851
+ }
852
+ function isBookDef(value) {
853
+ return value != null && typeof value === "object" && value.kind === "book" && typeof value.pages === "object";
854
+ }
855
+ function bookToNode(def, key, options) {
856
+ const children = [];
857
+ for (const [childKey, entry] of Object.entries(def.pages)) {
858
+ children.push(entryToNode(entry, childKey));
859
+ }
860
+ return { key, kind: "book", format: def.format, options: { ...options }, children };
861
+ }
862
+ function entryToNode(entry, key) {
863
+ if (entry.kind === "link") {
864
+ if (!isBookDef(entry.book)) {
865
+ return {
866
+ key,
867
+ kind: "book",
868
+ options: { ...entry.options ?? {} },
869
+ children: [],
870
+ linkError: `link("${key}") does not reference a book().`
871
+ };
872
+ }
873
+ return bookToNode(entry.book, key, entry.options ?? {});
874
+ }
875
+ return {
876
+ key,
877
+ kind: "page",
878
+ component: componentName(entry.component),
879
+ options: { ...entry.options ?? {} }
880
+ };
881
+ }
882
+ async function loadStorybook(absFile) {
883
+ const mod = await import(pathToFileURL2(absFile).href);
884
+ const root = mod.default;
885
+ if (!isBookDef(root)) {
886
+ throw new Error(`Storybook ${absFile} must default-export a book().`);
887
+ }
888
+ return bookToNode(root, "root", {});
889
+ }
890
+
891
+ // src/lib/navigation/validate.ts
892
+ var MOBILE_FORMATS = /* @__PURE__ */ new Set(["drawer", "stack", "bottomNav"]);
893
+ var WEB_FORMATS = /* @__PURE__ */ new Set(["sidebar", "stack", "tabs"]);
894
+ function validate(root, platform) {
895
+ const diagnostics = [];
896
+ const formats = platform === "mobile" ? MOBILE_FORMATS : WEB_FORMATS;
897
+ const keyCounts = /* @__PURE__ */ new Map();
898
+ const error = (rule, message) => {
899
+ diagnostics.push({ level: "error", rule, message });
900
+ };
901
+ const walk = (node, isRoot) => {
902
+ if (!isRoot) {
903
+ keyCounts.set(node.key, (keyCounts.get(node.key) ?? 0) + 1);
904
+ }
905
+ if (node.kind === "page") {
906
+ if (!node.component) {
907
+ error("dangling", `Page "${node.key}" has no component.`);
908
+ }
909
+ return;
910
+ }
911
+ const children = node.children ?? [];
912
+ if (node.linkError !== void 0) {
913
+ error(
914
+ "bad-link",
915
+ `Book "${node.key}": ${node.linkError} Fix: pass a value returned by book() to link().`
916
+ );
917
+ return;
918
+ }
919
+ if (children.length === 0) {
920
+ error("dangling", `Book "${node.key}" has no children.`);
921
+ }
922
+ if (node.format && !formats.has(node.format)) {
923
+ error(
924
+ "format-platform",
925
+ `Book "${node.key}" uses format "${node.format}" which is not valid on ${platform}.`
926
+ );
927
+ }
928
+ if (node.format === "bottomNav") {
929
+ const directPages = children.filter((c) => c.kind === "page").length;
930
+ if (directPages > 5) {
931
+ error(
932
+ "bottomNav-max-5",
933
+ `Book "${node.key}" has ${directPages} bottomNav pages; the maximum is 5.`
934
+ );
935
+ }
936
+ }
937
+ const initialCount = children.filter((c) => c.options.initial === true).length;
938
+ if (initialCount > 1) {
939
+ error(
940
+ "multiple-initial",
941
+ `Book "${node.key}" has ${initialCount} children marked initial; only one is allowed.`
942
+ );
943
+ }
944
+ for (const child of children) {
945
+ walk(child, false);
946
+ }
947
+ };
948
+ walk(root, true);
949
+ for (const [key, count] of keyCounts) {
950
+ if (count > 1) {
951
+ error("duplicate-key", `Key "${key}" is used ${count} times across the tree.`);
952
+ }
953
+ }
954
+ return diagnostics;
955
+ }
956
+
957
+ // src/lib/navigation/flatten.ts
958
+ function kebab(key) {
959
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
960
+ }
961
+ function segmentHasParams(segment) {
962
+ return segment.includes(":");
963
+ }
964
+ function flatten(root) {
965
+ const routes = [];
966
+ const walk = (node, prefix, isRoot) => {
967
+ if (node.kind === "page") {
968
+ const segment2 = node.options.path ?? kebab(node.key);
969
+ const path = `${prefix}/${segment2}`;
970
+ routes.push({ key: node.key, path, hasParams: segmentHasParams(path) });
971
+ return;
972
+ }
973
+ const segment = node.options.path ?? kebab(node.key);
974
+ const nextPrefix = isRoot ? prefix : `${prefix}/${segment}`;
975
+ for (const child of node.children ?? []) {
976
+ walk(child, nextPrefix, false);
977
+ }
978
+ };
979
+ walk(root, "", true);
980
+ return { routes };
981
+ }
982
+
983
+ // src/lib/navigation/extract-params.ts
984
+ import ts from "typescript";
985
+ function extractParams(source) {
986
+ const sf = ts.createSourceFile("storybook.ts", source, ts.ScriptTarget.Latest, true);
987
+ const params = /* @__PURE__ */ new Map();
988
+ const visit = (node) => {
989
+ if (ts.isPropertyAssignment(node) && ts.isCallExpression(node.initializer) && isPageCall(node.initializer) && node.initializer.typeArguments && node.initializer.typeArguments.length > 0) {
990
+ const key = propertyName(node.name);
991
+ if (key !== void 0) {
992
+ params.set(key, node.initializer.typeArguments[0].getText(sf).trim());
993
+ }
994
+ }
995
+ ts.forEachChild(node, visit);
996
+ };
997
+ visit(sf);
998
+ return params;
999
+ }
1000
+ function isPageCall(call) {
1001
+ return ts.isIdentifier(call.expression) && call.expression.text === "page";
1002
+ }
1003
+ function propertyName(name) {
1004
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
1005
+ return name.text;
1006
+ }
1007
+ return void 0;
1008
+ }
1009
+
1010
+ // src/lib/navigation/merge-route-params.ts
1011
+ function mergeRouteParams(input2) {
1012
+ const { nativeKeys, webKeys, nativeParams, webParams } = input2;
1013
+ const keys = [.../* @__PURE__ */ new Set([...nativeKeys, ...webKeys])].sort();
1014
+ const routes = [];
1015
+ const conflicts = [];
1016
+ for (const key of keys) {
1017
+ const nativeType = nativeParams.get(key);
1018
+ const webType = webParams.get(key);
1019
+ if (nativeType !== void 0 && webType !== void 0 && nativeType !== webType) {
1020
+ conflicts.push(key);
1021
+ }
1022
+ routes.push({ key, params: nativeType ?? webType ?? "void" });
1023
+ }
1024
+ return { routes, conflicts };
1025
+ }
1026
+
1027
+ // src/lib/navigation/render-native.ts
1028
+ var NATIVE_FACTORY = {
1029
+ bottomNav: { factory: "createBottomTabNavigator", nav: "Tab" },
1030
+ drawer: { factory: "createDrawerNavigator", nav: "Drawer" },
1031
+ stack: { factory: "createNativeStackNavigator", nav: "Stack" }
1032
+ };
1033
+ var FACTORY_MODULE = {
1034
+ createBottomTabNavigator: "@react-navigation/bottom-tabs",
1035
+ createDrawerNavigator: "@react-navigation/drawer",
1036
+ createNativeStackNavigator: "@react-navigation/native-stack"
1037
+ };
1038
+ function factoryFor(format) {
1039
+ return NATIVE_FACTORY[format ?? "stack"] ?? NATIVE_FACTORY.stack;
1040
+ }
1041
+ function screenOptions(node, supportsIcon) {
1042
+ const parts = [];
1043
+ const { title, icon } = node.options;
1044
+ if (title !== void 0) parts.push(`title: ${JSON.stringify(title)}`);
1045
+ if (icon !== void 0 && supportsIcon) {
1046
+ parts.push(`tabBarIcon: () => <NavIcon name={${JSON.stringify(icon)}} />`);
1047
+ }
1048
+ return parts.join(", ");
1049
+ }
1050
+ function pascal(key) {
1051
+ return key.replace(/[_\s-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
1052
+ }
1053
+ function renderNative(root, opts) {
1054
+ const screenNames = /* @__PURE__ */ new Set();
1055
+ const navigatorBlocks = [];
1056
+ const usedFactories = /* @__PURE__ */ new Map();
1057
+ let usesIcon = false;
1058
+ const navVarFor = (factory, nav) => {
1059
+ if (!usedFactories.has(factory)) usedFactories.set(factory, nav);
1060
+ return usedFactories.get(factory);
1061
+ };
1062
+ const renderBook = (book) => {
1063
+ const { factory, nav } = factoryFor(book.format);
1064
+ const navVar = navVarFor(factory, nav);
1065
+ const componentName2 = book.key === "root" ? "RootNavigator" : `${pascal(book.key)}Navigator`;
1066
+ const supportsIcon = factory === "createBottomTabNavigator" || factory === "createDrawerNavigator";
1067
+ const children = book.children ?? [];
1068
+ const screens = [];
1069
+ for (const child of children) {
1070
+ const name = child.kind === "page" ? child.component ?? pascal(child.key) : renderBook(child);
1071
+ if (child.kind === "page") screenNames.add(name);
1072
+ if (supportsIcon && child.options.icon !== void 0) usesIcon = true;
1073
+ const options = screenOptions(child, supportsIcon);
1074
+ const optionsAttr = options ? ` options={{ ${options} }}` : "";
1075
+ screens.push(
1076
+ ` <${navVar}.Screen name="${child.key}" component={${name}}${optionsAttr} />`
1077
+ );
1078
+ }
1079
+ const initialChild = children.find((c) => c.options.initial === true);
1080
+ const navProps = initialChild ? ` initialRouteName="${initialChild.key}"` : "";
1081
+ navigatorBlocks.push(
1082
+ `function ${componentName2}() {
1083
+ return (
1084
+ <${navVar}.Navigator${navProps}>
1085
+ ${screens.join("\n")}
1086
+ </${navVar}.Navigator>
1087
+ );
1088
+ }`
1089
+ );
1090
+ return componentName2;
1091
+ };
1092
+ const rootComponent = renderBook(root);
1093
+ const factoryImports = [...usedFactories.entries()].map(([factory, nav]) => {
1094
+ const mod = FACTORY_MODULE[factory];
1095
+ return `import { ${factory} } from '${mod}';
1096
+ const ${nav} = ${factory}();`;
1097
+ }).join("\n");
1098
+ const screenImport = [...screenNames].sort().join(", ");
1099
+ const header = "// AUTO-GENERATED by sublime build:nav \u2014 do not edit";
1100
+ const navIconBlock = usesIcon ? `function NavIcon(_props: { name: string }): ReactNode {
1101
+ return null;
1102
+ }
1103
+
1104
+ ` : "";
1105
+ return `${header}
1106
+ import type { ReactNode } from 'react';
1107
+ import { NavigationContainer } from '@react-navigation/native';
1108
+ ${factoryImports}
1109
+ import { NavProvider } from '@sublime-ui/ui/navigation';
1110
+ import { useNativeNav } from '@sublime-ui/ui/navigation/bridge.native';
1111
+ import { ${screenImport} } from '${opts.screensImport}';
1112
+
1113
+ ${navIconBlock}${navigatorBlocks.join("\n\n")}
1114
+
1115
+ function NavBridge({ children }: { children: ReactNode }) {
1116
+ const nav = useNativeNav();
1117
+ return <NavProvider value={nav}>{children}</NavProvider>;
1118
+ }
1119
+
1120
+ export function Navigation() {
1121
+ return (
1122
+ <NavigationContainer>
1123
+ <NavBridge>
1124
+ <${rootComponent} />
1125
+ </NavBridge>
1126
+ </NavigationContainer>
1127
+ );
1128
+ }
1129
+ `;
1130
+ }
1131
+
1132
+ // src/lib/navigation/render-web.ts
1133
+ var WEB_LAYOUT = {
1134
+ sidebar: "Sidebar",
1135
+ tabs: "Tabs",
1136
+ stack: "Stack"
1137
+ };
1138
+ function layoutFor(format) {
1139
+ return WEB_LAYOUT[format ?? "stack"] ?? WEB_LAYOUT.stack;
1140
+ }
1141
+ function pascal2(key) {
1142
+ return key.replace(/[_\s-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(" ").filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
1143
+ }
1144
+ function kebab2(key) {
1145
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[_\s]+/g, "-").toLowerCase();
1146
+ }
1147
+ function renderWeb(root, opts) {
1148
+ const screenNames = /* @__PURE__ */ new Set();
1149
+ const layoutNames = /* @__PURE__ */ new Set();
1150
+ const layoutBlocks = [];
1151
+ const renderBook = (book, isRoot) => {
1152
+ const layout = layoutFor(book.format);
1153
+ layoutNames.add(layout);
1154
+ const segment = book.options.path ?? kebab2(book.key);
1155
+ const routePath = isRoot ? "/" : segment;
1156
+ const children = [];
1157
+ for (const child of book.children ?? []) {
1158
+ if (child.kind === "page") {
1159
+ const comp = child.component ?? pascal2(child.key);
1160
+ screenNames.add(comp);
1161
+ const childSegment = child.options.path ?? kebab2(child.key);
1162
+ if (child.options.initial === true) {
1163
+ children.push(` <Route index element={<${comp} />} />`);
1164
+ }
1165
+ children.push(
1166
+ ` <Route path="${childSegment}" element={<${comp} />} />`
1167
+ );
1168
+ } else {
1169
+ children.push(renderBook(child, false));
1170
+ }
1171
+ }
1172
+ return ` <Route path="${routePath}" element={<${layout} />}>
1173
+ ${children.join("\n")}
1174
+ </Route>`;
1175
+ };
1176
+ const routesTree = renderBook(root, true);
1177
+ for (const layout of [...layoutNames].sort()) {
1178
+ layoutBlocks.push(
1179
+ `function ${layout}() {
1180
+ return <Outlet />;
1181
+ }`
1182
+ );
1183
+ }
1184
+ const { routes } = flatten(root);
1185
+ const pathEntries = routes.map((r) => ` ${JSON.stringify(r.key)}: ${JSON.stringify(r.path)},`).join("\n");
1186
+ const titleEntries = [];
1187
+ const collectTitles = (node) => {
1188
+ if (node.kind === "page") {
1189
+ const fields = [];
1190
+ if (node.options.title !== void 0) fields.push(`title: ${JSON.stringify(node.options.title)}`);
1191
+ if (node.options.icon !== void 0) fields.push(`icon: ${JSON.stringify(node.options.icon)}`);
1192
+ if (fields.length > 0) {
1193
+ titleEntries.push(` ${JSON.stringify(node.key)}: { ${fields.join(", ")} },`);
1194
+ }
1195
+ return;
1196
+ }
1197
+ for (const child of node.children ?? []) collectTitles(child);
1198
+ };
1199
+ collectTitles(root);
1200
+ const screenImport = [...screenNames].sort().join(", ");
1201
+ const header = "// AUTO-GENERATED by sublime build:nav \u2014 do not edit";
1202
+ return `${header}
1203
+ import type { ReactNode } from 'react';
1204
+ import { BrowserRouter, Routes, Route, Outlet } from 'react-router-dom';
1205
+ import { NavProvider } from '@sublime-ui/ui/navigation';
1206
+ import { useWebNav } from '@sublime-ui/ui/navigation/bridge.web';
1207
+ import { ${screenImport} } from '${opts.screensImport}';
1208
+
1209
+ const paths: Record<string, string> = {
1210
+ ${pathEntries}
1211
+ };
1212
+
1213
+ export const titles: Record<string, { title?: string; icon?: string }> = {
1214
+ ${titleEntries.join("\n")}${titleEntries.length > 0 ? "\n" : ""}};
1215
+
1216
+ function pathOf(name: string, params?: unknown): string {
1217
+ let path = paths[name] ?? '/';
1218
+ if (params && typeof params === 'object') {
1219
+ for (const [k, v] of Object.entries(params as Record<string, unknown>)) {
1220
+ path = path.replace(\`:\${k}\`, String(v));
1221
+ }
1222
+ }
1223
+ return path;
1224
+ }
1225
+
1226
+ function nameOf(path: string): string {
1227
+ const norm = (p: string) => (p.length > 1 && p.endsWith('/') ? p.slice(0, -1) : p);
1228
+ const target = norm(path);
1229
+ for (const [name, p] of Object.entries(paths)) {
1230
+ const pattern = norm(p);
1231
+ if (pattern === target) return name;
1232
+ // Match parameterized routes (':id' segments) positionally.
1233
+ if (pattern.includes(':')) {
1234
+ const re = new RegExp('^' + pattern.replace(/:[^/]+/g, '[^/]+') + '$');
1235
+ if (re.test(target)) return name;
1236
+ }
1237
+ }
1238
+ return path;
1239
+ }
1240
+
1241
+ ${layoutBlocks.join("\n\n")}
1242
+
1243
+ function NavBridge({ children }: { children: ReactNode }) {
1244
+ const nav = useWebNav(pathOf, nameOf);
1245
+ return <NavProvider value={nav}>{children}</NavProvider>;
1246
+ }
1247
+
1248
+ export function Navigation() {
1249
+ return (
1250
+ <BrowserRouter>
1251
+ <NavBridge>
1252
+ <Routes>
1253
+ ${routesTree}
1254
+ </Routes>
1255
+ </NavBridge>
1256
+ </BrowserRouter>
1257
+ );
1258
+ }
1259
+ `;
1260
+ }
1261
+
1262
+ // src/lib/navigation/render-routes-dts.ts
1263
+ function renderRoutesDts(routes) {
1264
+ const header = "// AUTO-GENERATED by sublime build:nav \u2014 do not edit";
1265
+ const entries = routes.map((r) => ` ${r.key}: ${r.params};`).join("\n");
1266
+ return `${header}
1267
+ export interface AppRoutes {
1268
+ ${entries}
1269
+ }
1270
+ `;
1271
+ }
1272
+
1273
+ // src/commands/build-nav.ts
1274
+ var SCREENS_IMPORT = "./screens";
1275
+ var BARREL = [
1276
+ "// AUTO-GENERATED by sublime build:nav \u2014 do not edit",
1277
+ "export { Navigation } from './navigation.js';",
1278
+ "export type { AppRoutes } from './routes.js';",
1279
+ ""
1280
+ ].join("\n");
1281
+ function printDiagnostics(platform, diagnostics) {
1282
+ for (const d of diagnostics) {
1283
+ log.error(`[${platform}] ${d.rule}: ${d.message}`);
1284
+ }
1285
+ }
1286
+ async function buildNav(opts) {
1287
+ const cfg = loadConfig(opts.cwd);
1288
+ const navDir = join10(opts.cwd, cfg.navigationDir);
1289
+ const nativeFile = join10(navDir, "storybook.native.ts");
1290
+ const webFile = join10(navDir, "storybook.web.ts");
1291
+ const code = await buildOnce(navDir, nativeFile, webFile);
1292
+ if (opts.watch) {
1293
+ watchAndRebuild(navDir, nativeFile, webFile);
1294
+ }
1295
+ return code;
1296
+ }
1297
+ async function buildOnce(navDir, nativeFile, webFile) {
1298
+ let nativeRoot;
1299
+ let webRoot;
1300
+ try {
1301
+ nativeRoot = await loadStorybook(nativeFile);
1302
+ webRoot = await loadStorybook(webFile);
1303
+ } catch (err) {
1304
+ log.error(err instanceof Error ? err.message : String(err));
1305
+ return 1;
1306
+ }
1307
+ const diagnostics = [
1308
+ ...validate(nativeRoot, "mobile").map((d) => ["native", d]),
1309
+ ...validate(webRoot, "web").map((d) => ["web", d])
1310
+ ];
1311
+ if (diagnostics.length > 0) {
1312
+ for (const [platform, d] of diagnostics) printDiagnostics(platform, [d]);
1313
+ log.error(`build:nav failed with ${diagnostics.length} error(s); wrote nothing.`);
1314
+ return 1;
1315
+ }
1316
+ const nativeParams = extractParams(readFileSync5(nativeFile, "utf8"));
1317
+ const webParams = extractParams(readFileSync5(webFile, "utf8"));
1318
+ const { routes, conflicts } = mergeRouteParams({
1319
+ nativeKeys: flatten(nativeRoot).routes.map((r) => r.key),
1320
+ webKeys: flatten(webRoot).routes.map((r) => r.key),
1321
+ nativeParams,
1322
+ webParams
1323
+ });
1324
+ if (conflicts.length > 0) {
1325
+ for (const key of conflicts) {
1326
+ log.error(
1327
+ `route-param-conflict ${key}: native and web declare different param types for "${key}". Fix: make both page<...>() type arguments agree.`
1328
+ );
1329
+ }
1330
+ log.error(`build:nav failed with ${conflicts.length} error(s); wrote nothing.`);
1331
+ return 1;
1332
+ }
1333
+ const nativeSrc = renderNative(nativeRoot, { screensImport: SCREENS_IMPORT });
1334
+ const webSrc = renderWeb(webRoot, { screensImport: SCREENS_IMPORT });
1335
+ const dtsSrc = renderRoutesDts(routes);
1336
+ safeWrite(join10(navDir, "navigation.native.tsx"), nativeSrc, true);
1337
+ safeWrite(join10(navDir, "navigation.web.tsx"), webSrc, true);
1338
+ safeWrite(join10(navDir, "routes.d.ts"), dtsSrc, true);
1339
+ safeWrite(join10(navDir, "index.ts"), BARREL, true);
1340
+ log.success(`build:nav wrote 4 files to ${navDir}`);
1341
+ return 0;
1342
+ }
1343
+ function watchAndRebuild(navDir, nativeFile, webFile) {
1344
+ log.info(`build:nav watching ${navDir} \u2014 Ctrl-C to stop.`);
1345
+ let timer;
1346
+ const trigger = () => {
1347
+ if (timer) clearTimeout(timer);
1348
+ timer = setTimeout(() => {
1349
+ void buildOnce(navDir, nativeFile, webFile);
1350
+ }, 50);
1351
+ };
1352
+ return [nativeFile, webFile].map((file) => fsWatch(file, trigger));
1353
+ }
1354
+
1355
+ // src/commands/desktop-dev.ts
1356
+ import { join as join11 } from "path";
1357
+ var defaultRunner2 = (cmd, args, cwd) => runInherit(cmd, args, { cwd });
1358
+ async function desktopDev(opts) {
1359
+ const cfg = loadConfig(opts.project);
1360
+ const desktopDir = join11(opts.project, cfg.desktop?.dir ?? "desktop");
1361
+ const runner = opts.runner ?? defaultRunner2;
1362
+ return runner("electron-forge", ["start"], desktopDir);
1363
+ }
1364
+
1365
+ // src/commands/desktop-build.ts
1366
+ import { join as join12 } from "path";
1367
+ var defaultRunner3 = (cmd, args, cwd) => runInherit(cmd, args, { cwd });
1368
+ async function desktopBuild(opts) {
1369
+ const cfg = loadConfig(opts.project);
1370
+ const desktopDir = join12(opts.project, cfg.desktop?.dir ?? "desktop");
1371
+ const runner = opts.runner ?? defaultRunner3;
1372
+ return runner("electron-forge", ["make"], desktopDir);
1373
+ }
1374
+
1375
+ // src/cli.ts
1376
+ var program = new Command();
1377
+ program.name("sublime").description("Sublime UI devkit \u2014 offline Android builds and tooling").version("0.0.0");
1378
+ program.command("doctor").description("Check the environment for offline Android builds").action(async () => {
1379
+ process.exit(await doctorCommand());
1380
+ });
1381
+ program.command("setup").description("Install/repair the build environment").action(async () => {
1382
+ process.exit(await setupCommand());
1383
+ });
1384
+ program.command("build").description("Build a standalone Android APK/AAB offline").option("--release", "release APK (default)", true).option("--debug", "debug APK (requires Metro)").option("--aab", "Android App Bundle (bundleRelease)").option("--project <path>", "project directory", process.cwd()).action(async (opts) => {
1385
+ const code = await buildCommand({
1386
+ project: opts.project,
1387
+ release: opts.debug ? false : true,
1388
+ aab: opts.aab ?? false
1389
+ });
1390
+ process.exit(code);
1391
+ });
1392
+ program.command("run").description("Install and launch the built APK on a device").option("--device <id>", "adb device serial").option("--project <path>", "project directory", process.cwd()).action(async (opts) => {
1393
+ process.exit(
1394
+ await runCommand(
1395
+ opts.device === void 0 ? { project: opts.project } : { project: opts.project, device: opts.device }
1396
+ )
1397
+ );
1398
+ });
1399
+ program.command("make:model <name>").description("Scaffold a Model (+ registerModel) for the framework").option("--fields <spec>", 'fields, e.g. "name:string, tags:Tag[]"').option("--resource <path>", "override the REST resource path").option("--force", "overwrite existing files").action(async (name, opts) => {
1400
+ const code = await makeModel({
1401
+ name,
1402
+ cwd: process.cwd(),
1403
+ force: opts.force ?? false,
1404
+ ...opts.fields ? { fields: opts.fields } : {},
1405
+ ...opts.resource ? { resource: opts.resource } : {},
1406
+ promptFields: () => input({ message: "Fields (name:type, comma-separated; blank for id-only):", default: "" })
1407
+ });
1408
+ process.exit(code);
1409
+ });
1410
+ program.command("make:component <name>").description("Scaffold a cross-platform component (types + web + native + index)").option("--mobile-only", "mobile-only component (web renders a null stub)").option("--force", "overwrite existing files").action(async (name, opts) => {
1411
+ process.exit(await makeComponent({
1412
+ name,
1413
+ cwd: process.cwd(),
1414
+ mobileOnly: opts.mobileOnly ?? false,
1415
+ force: opts.force ?? false
1416
+ }));
1417
+ });
1418
+ program.command("theme:init").description("Scaffold the app design tokens (tokens.json + typed wrapper)").option("--force", "overwrite existing files").action(async (opts) => {
1419
+ process.exit(await themeInit({ cwd: process.cwd(), force: opts.force ?? false }));
1420
+ });
1421
+ program.command("build:nav").description("Compile per-platform storybooks into navigation artifacts").option("--watch", "rebuild on storybook changes").option("--force", "overwrite generated files").option("--project <path>", "project directory", process.cwd()).action(async (opts) => {
1422
+ const code = await buildNav({
1423
+ cwd: opts.project,
1424
+ watch: opts.watch ?? false,
1425
+ force: opts.force ?? false
1426
+ });
1427
+ if (!opts.watch) process.exit(code);
1428
+ });
1429
+ program.command("desktop:dev").description("Run the Electron desktop shell in development (electron-forge start)").option("--project <path>", "project directory", process.cwd()).action(async (opts) => {
1430
+ process.exit(await desktopDev({ project: opts.project }));
1431
+ });
1432
+ program.command("desktop:build").description("Build distributable Electron artifacts (electron-forge make)").option("--project <path>", "project directory", process.cwd()).action(async (opts) => {
1433
+ process.exit(await desktopBuild({ project: opts.project }));
1434
+ });
1435
+ program.parseAsync(process.argv).catch((err) => {
1436
+ log.error(err instanceof Error ? err.message : String(err));
1437
+ process.exit(1);
1438
+ });