@lunora/cli 1.0.0-alpha.4 → 1.0.0-alpha.6
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/bin.mjs +1 -1
- package/dist/index.d.mts +50 -10
- package/dist/index.d.ts +50 -10
- package/dist/index.mjs +5 -5
- package/dist/packem_chunks/handler.mjs +4 -3
- package/dist/packem_chunks/handler14.mjs +1 -1
- package/dist/packem_chunks/handler16.mjs +1 -1
- package/dist/packem_chunks/handler18.mjs +3 -5
- package/dist/packem_chunks/handler19.mjs +1 -1
- package/dist/packem_chunks/handler2.mjs +1 -1
- package/dist/packem_chunks/handler21.mjs +1 -1
- package/dist/packem_chunks/handler5.mjs +1 -1
- package/dist/packem_chunks/handler6.mjs +1 -1
- package/dist/packem_chunks/planDevCommand.mjs +5 -48
- package/dist/packem_chunks/runDeployCommand.mjs +1 -1
- package/dist/packem_chunks/runInitCommand.mjs +488 -46
- package/dist/packem_chunks/runMigrateGenerateCommand.mjs +4 -4
- package/dist/packem_chunks/runResetCommand.mjs +3 -3
- package/dist/packem_shared/{COMMANDS-CHw4zOZ9.mjs → COMMANDS-Bn8luojF.mjs} +10 -2
- package/dist/packem_shared/{commands-SUPdjsu5.mjs → commands-DqsEzojt.mjs} +5 -4
- package/dist/packem_shared/detect-package-manager-DYp7n3mJ.mjs +61 -0
- package/dist/packem_shared/{diffSnapshots-RR2ZE8Ya.mjs → diffSnapshots-BeDvvNiF.mjs} +1 -1
- package/dist/packem_shared/{runAddCommand-Txwh1Xw1.mjs → runAddCommand-G544_v6e.mjs} +1 -1
- package/dist/packem_shared/{schemaIrToSnapshot-aBTo7TM5.mjs → schemaIrToSnapshot-DdsljJT-.mjs} +1 -1
- package/dist/packem_shared/tui-prompts-XHFxlOg5.mjs +269 -0
- package/package.json +6 -5
- /package/dist/packem_shared/{defaultSpawner-DxI3mebw.mjs → createRecordingSpawner-DxI3mebw.mjs} +0 -0
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync, readdirSync, mkdtempSync, rmSync,
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, mkdtempSync, cpSync, rmSync, renameSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
|
-
import { detectFramework as detectFramework$1, isInteractive
|
|
3
|
+
import { detectFramework as detectFramework$1, isInteractive } from '@lunora/config';
|
|
4
4
|
import { walkSync } from '@visulima/fs';
|
|
5
|
-
import {
|
|
5
|
+
import { join as join$1, dirname as dirname$1, basename, resolve, relative } from '@visulima/path';
|
|
6
6
|
import { downloadTemplate } from 'giget';
|
|
7
7
|
import { modify, applyEdits } from 'jsonc-parser';
|
|
8
8
|
import { join, dirname } from 'node:path';
|
|
9
9
|
import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
|
|
10
|
+
import { a as detectInstalledManagers, i as installArgsFor } from '../packem_shared/detect-package-manager-DYp7n3mJ.mjs';
|
|
10
11
|
import MagicString from 'magic-string';
|
|
11
12
|
import { Project, SyntaxKind } from 'ts-morph';
|
|
12
|
-
import { c as
|
|
13
|
+
import { c as resolveDistTag, d as resolveSourceRef, r as runAddCommand } from '../packem_shared/commands-DqsEzojt.mjs';
|
|
14
|
+
import { defaultSpawner } from '../packem_shared/createRecordingSpawner-DxI3mebw.mjs';
|
|
15
|
+
import { b as tuiOutro, d as tuiBanner, e as tuiText, f as tuiIntro, t as tuiSelect, w as withTuiSpinner, a as tuiConfirm, g as tuiMultiSelect } from '../packem_shared/tui-prompts-XHFxlOg5.mjs';
|
|
13
16
|
import { p as promptAuthProvider, E as EMAIL_ITEM } from '../packem_shared/features-ocSSpZtS.mjs';
|
|
14
17
|
|
|
15
18
|
const GITHUB_CONTENT = `name: Deploy
|
|
@@ -284,11 +287,16 @@ const patchViteConfig = (source) => {
|
|
|
284
287
|
|
|
285
288
|
const STACK_FEATURE_OPTIONS = [
|
|
286
289
|
{ description: "Sign-up / sign-in (asks which provider)", label: "Authentication", value: "auth" },
|
|
287
|
-
{ description: "Cloudflare Email Workers + a dev mail catcher", label: "Transactional email", value: "email" }
|
|
290
|
+
{ description: "Cloudflare Email Workers + a dev mail catcher", label: "Transactional email", value: "email" },
|
|
291
|
+
{ description: "Typed R2 buckets + signed URLs (@lunora/storage)", label: "File storage", value: "storage" },
|
|
292
|
+
{ description: "Token-bucket / sliding-window limits (@lunora/ratelimit)", label: "Rate limiting", value: "ratelimit" },
|
|
293
|
+
{ description: "Scheduled jobs via Cron Triggers (@lunora/scheduler)", label: "Cron jobs", value: "crons" },
|
|
294
|
+
{ description: "Live presence / who's-online over hibernated WebSockets", label: "Presence", value: "presence" },
|
|
295
|
+
{ description: "Snapshot + restore your Durable Object data", label: "Backups", value: "backup" }
|
|
288
296
|
];
|
|
289
297
|
const offerRegistryExtras = async (deps) => {
|
|
290
298
|
if (!deps.interactive) {
|
|
291
|
-
deps.logger.info("tip: add
|
|
299
|
+
deps.logger.info("tip: add features later with `lunora add <auth|email|storage|ratelimit|crons|presence|backup>`.");
|
|
292
300
|
return;
|
|
293
301
|
}
|
|
294
302
|
const picked = await deps.multiSelect("Which features do you want to add?", STACK_FEATURE_OPTIONS, { defaults: [] });
|
|
@@ -297,11 +305,268 @@ const offerRegistryExtras = async (deps) => {
|
|
|
297
305
|
const provider = await promptAuthProvider(deps.select);
|
|
298
306
|
await deps.apply([provider]);
|
|
299
307
|
} else {
|
|
300
|
-
await deps.apply([EMAIL_ITEM]);
|
|
308
|
+
await deps.apply([feature === "email" ? EMAIL_ITEM : feature]);
|
|
301
309
|
}
|
|
302
310
|
}
|
|
303
311
|
};
|
|
304
312
|
|
|
313
|
+
const READ_URL = `const url = (import.meta.env.VITE_LUNORA_URL as string | undefined) ?? globalThis.location.origin;`;
|
|
314
|
+
const REACT_MAIN = `import "./index.css";
|
|
315
|
+
|
|
316
|
+
import { LunoraProvider } from "@lunora/react";
|
|
317
|
+
import { LunoraClient } from "lunorash/client";
|
|
318
|
+
import { StrictMode } from "react";
|
|
319
|
+
import { createRoot } from "react-dom/client";
|
|
320
|
+
|
|
321
|
+
import App from "./App.tsx";
|
|
322
|
+
|
|
323
|
+
// \`@lunora/vite\` runs the Worker on the same origin as Vite, so default to
|
|
324
|
+
// \`location.origin\`. Point \`VITE_LUNORA_URL\` at a deployed Worker to develop
|
|
325
|
+
// the client against production data.
|
|
326
|
+
${READ_URL}
|
|
327
|
+
const client = new LunoraClient({ url });
|
|
328
|
+
|
|
329
|
+
const root = document.getElementById("root");
|
|
330
|
+
|
|
331
|
+
if (!root) {
|
|
332
|
+
throw new Error("missing #root mount node");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
createRoot(root).render(
|
|
336
|
+
<StrictMode>
|
|
337
|
+
<LunoraProvider client={client}>
|
|
338
|
+
<App />
|
|
339
|
+
</LunoraProvider>
|
|
340
|
+
</StrictMode>,
|
|
341
|
+
);
|
|
342
|
+
`;
|
|
343
|
+
const VUE_MAIN = `import "./style.css";
|
|
344
|
+
|
|
345
|
+
import { createLunora } from "@lunora/vue";
|
|
346
|
+
import { LunoraClient } from "lunorash/client";
|
|
347
|
+
import { createApp } from "vue";
|
|
348
|
+
|
|
349
|
+
import App from "./App.vue";
|
|
350
|
+
|
|
351
|
+
// Provide one LunoraClient at the app root via the Vue plugin form.
|
|
352
|
+
${READ_URL}
|
|
353
|
+
createApp(App).use(createLunora(new LunoraClient({ url }))).mount("#app");
|
|
354
|
+
`;
|
|
355
|
+
const SOLID_INDEX = `import "./index.css";
|
|
356
|
+
|
|
357
|
+
import { LunoraContext } from "@lunora/solid";
|
|
358
|
+
import { LunoraClient } from "lunorash/client";
|
|
359
|
+
import { render } from "solid-js/web";
|
|
360
|
+
|
|
361
|
+
import App from "./App";
|
|
362
|
+
|
|
363
|
+
${READ_URL}
|
|
364
|
+
const client = new LunoraClient({ url });
|
|
365
|
+
const root = document.getElementById("root");
|
|
366
|
+
|
|
367
|
+
render(
|
|
368
|
+
() => (
|
|
369
|
+
<LunoraContext.Provider value={client}>
|
|
370
|
+
<App />
|
|
371
|
+
</LunoraContext.Provider>
|
|
372
|
+
),
|
|
373
|
+
root!,
|
|
374
|
+
);
|
|
375
|
+
`;
|
|
376
|
+
const SVELTE_ROOT = `<script lang="ts">
|
|
377
|
+
import { setLunoraClient } from "@lunora/svelte";
|
|
378
|
+
import { LunoraClient } from "lunorash/client";
|
|
379
|
+
|
|
380
|
+
import App from "./App.svelte";
|
|
381
|
+
|
|
382
|
+
${READ_URL}
|
|
383
|
+
setLunoraClient(new LunoraClient({ url }));
|
|
384
|
+
<\/script>
|
|
385
|
+
|
|
386
|
+
<App />
|
|
387
|
+
`;
|
|
388
|
+
const SVELTE_MAIN = `import "./app.css";
|
|
389
|
+
|
|
390
|
+
import { mount } from "svelte";
|
|
391
|
+
|
|
392
|
+
import Root from "./Root.svelte";
|
|
393
|
+
|
|
394
|
+
// Mount \`Root\` (it sets the ambient LunoraClient) rather than \`App\` directly.
|
|
395
|
+
const app = mount(Root, { target: document.getElementById("app")! });
|
|
396
|
+
|
|
397
|
+
export default app;
|
|
398
|
+
`;
|
|
399
|
+
const VANILLA_MAIN = `import "./style.css";
|
|
400
|
+
|
|
401
|
+
import { LunoraClient } from "lunorash/client";
|
|
402
|
+
|
|
403
|
+
import { api } from "../lunora/_generated/api";
|
|
404
|
+
|
|
405
|
+
// Vanilla starter: no framework provider — talk to Lunora through the client
|
|
406
|
+
// directly. \`@lunora/vite\` runs the Worker on the same origin as Vite.
|
|
407
|
+
${READ_URL}
|
|
408
|
+
const client = new LunoraClient({ url });
|
|
409
|
+
|
|
410
|
+
const root = document.querySelector<HTMLDivElement>("#app")!;
|
|
411
|
+
|
|
412
|
+
const heading = document.createElement("h1");
|
|
413
|
+
heading.textContent = "Vite + Lunora";
|
|
414
|
+
|
|
415
|
+
const output = document.createElement("pre");
|
|
416
|
+
root.replaceChildren(heading, output);
|
|
417
|
+
|
|
418
|
+
const render = (messages: unknown): void => {
|
|
419
|
+
// textContent (not innerHTML) — never inject server data as markup.
|
|
420
|
+
output.textContent = JSON.stringify(messages, null, 2);
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// Live subscription: the list re-renders on every server delta.
|
|
424
|
+
client.onUpdate(api.messages.list, { channelId: "channel:demo" }, render);
|
|
425
|
+
`;
|
|
426
|
+
const ADAPTERS = {
|
|
427
|
+
react: { adapter: "@lunora/react", createViteTemplate: "react-ts", files: [{ contents: REACT_MAIN, path: "src/main.tsx" }], label: "React" },
|
|
428
|
+
solid: { adapter: "@lunora/solid", createViteTemplate: "solid", files: [{ contents: SOLID_INDEX, path: "src/index.tsx" }], label: "Solid" },
|
|
429
|
+
svelte: {
|
|
430
|
+
adapter: "@lunora/svelte",
|
|
431
|
+
createViteTemplate: "svelte-ts",
|
|
432
|
+
files: [
|
|
433
|
+
{ contents: SVELTE_ROOT, path: "src/Root.svelte" },
|
|
434
|
+
{ contents: SVELTE_MAIN, path: "src/main.ts" }
|
|
435
|
+
],
|
|
436
|
+
label: "Svelte"
|
|
437
|
+
},
|
|
438
|
+
vanilla: { adapter: "lunorash/client", createViteTemplate: "vanilla-ts", files: [{ contents: VANILLA_MAIN, path: "src/main.ts" }], label: "Vanilla" },
|
|
439
|
+
vue: { adapter: "@lunora/vue", createViteTemplate: "vue-ts", files: [{ contents: VUE_MAIN, path: "src/main.ts" }], label: "Vue" }
|
|
440
|
+
};
|
|
441
|
+
const isOverlayFramework = (value) => Object.hasOwn(ADAPTERS, value);
|
|
442
|
+
|
|
443
|
+
const LUNORA_SCHEMA = `import { defineSchema, defineTable, v } from "lunorash/server";
|
|
444
|
+
|
|
445
|
+
export default defineSchema({
|
|
446
|
+
messages: defineTable({
|
|
447
|
+
channelId: v.string(),
|
|
448
|
+
text: v.string(),
|
|
449
|
+
})
|
|
450
|
+
.shardBy("channelId")
|
|
451
|
+
.index("by_channel", ["channelId"]),
|
|
452
|
+
});
|
|
453
|
+
`;
|
|
454
|
+
const LUNORA_MESSAGES = `import { mutation, query, v } from "./_generated/server.js";
|
|
455
|
+
|
|
456
|
+
export const list = query.input({ channelId: v.string(), limit: v.optional(v.number()) }).query(async ({ args }) => {
|
|
457
|
+
return { channelId: args.channelId, limit: args.limit ?? 50, messages: [] };
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
export const send = mutation.input({ channelId: v.string(), text: v.string() }).mutation(async ({ args }) => {
|
|
461
|
+
return { channelId: args.channelId, text: args.text };
|
|
462
|
+
});
|
|
463
|
+
`;
|
|
464
|
+
const SERVER_ENTRY = `import type { ShardNamespaceLike } from "lunorash/runtime";
|
|
465
|
+
|
|
466
|
+
import { defineApp } from "../lunora/_generated/app.js";
|
|
467
|
+
|
|
468
|
+
interface Env extends Record<string, unknown> {
|
|
469
|
+
SHARD: ShardNamespaceLike;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const app = defineApp<Env>()
|
|
473
|
+
.shard((env) => env.SHARD)
|
|
474
|
+
.build();
|
|
475
|
+
|
|
476
|
+
export const ShardDO = app.ShardDO;
|
|
477
|
+
export default app;
|
|
478
|
+
`;
|
|
479
|
+
const WRANGLER = `{
|
|
480
|
+
"$schema": "node_modules/wrangler/config-schema.json",
|
|
481
|
+
"name": "__NAME__",
|
|
482
|
+
"main": "src/server.ts",
|
|
483
|
+
"compatibility_date": "2026-06-10",
|
|
484
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
485
|
+
"durable_objects": {
|
|
486
|
+
"bindings": [{ "name": "SHARD", "class_name": "ShardDO" }],
|
|
487
|
+
},
|
|
488
|
+
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["ShardDO"] }],
|
|
489
|
+
"observability": { "enabled": true, "head_sampling_rate": 1 },
|
|
490
|
+
}
|
|
491
|
+
`;
|
|
492
|
+
const GITIGNORE_ADDITIONS = [".wrangler", ".lunora/", ".lunora-cache", "lunora/_generated"];
|
|
493
|
+
const COMMON_DEV_DEPENDENCIES = {
|
|
494
|
+
"@cloudflare/workers-types": "^4.20260611.1",
|
|
495
|
+
wrangler: "^4.100.0"
|
|
496
|
+
};
|
|
497
|
+
const writeFile = (target, relativePath, contents, written) => {
|
|
498
|
+
const destination = join$1(target, relativePath);
|
|
499
|
+
mkdirSync(dirname$1(destination), { recursive: true });
|
|
500
|
+
writeFileSync(destination, contents, "utf8");
|
|
501
|
+
written.push(destination);
|
|
502
|
+
};
|
|
503
|
+
const NEWLINE = /\r?\n/;
|
|
504
|
+
const ensureGitignore = (target) => {
|
|
505
|
+
const path = join$1(target, ".gitignore");
|
|
506
|
+
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
507
|
+
const missing = GITIGNORE_ADDITIONS.filter((entry) => !existing.split(NEWLINE).includes(entry));
|
|
508
|
+
if (missing.length === 0) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
512
|
+
writeFileSync(path, `${existing}${prefix}
|
|
513
|
+
# Lunora
|
|
514
|
+
${missing.join("\n")}
|
|
515
|
+
`, "utf8");
|
|
516
|
+
};
|
|
517
|
+
const stampRange = (name, range, distTag) => name === "lunorash" || name.startsWith("@lunora/") ? distTag : range;
|
|
518
|
+
const withDependency = (map, name, range, distTag) => {
|
|
519
|
+
return { ...map, [name]: stampRange(name, range, distTag) };
|
|
520
|
+
};
|
|
521
|
+
const restampLunora = (map, distTag) => Object.fromEntries(Object.entries(map).map(([name, range]) => [name, stampRange(name, range, distTag)]));
|
|
522
|
+
const patchPackageJson = (target, name, adapter, distTag) => {
|
|
523
|
+
const path = join$1(target, "package.json");
|
|
524
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
525
|
+
let dependencies = withDependency(parsed.dependencies ?? {}, "lunorash", distTag, distTag);
|
|
526
|
+
if (adapter.adapter.startsWith("@lunora/")) {
|
|
527
|
+
dependencies = withDependency(dependencies, adapter.adapter, distTag, distTag);
|
|
528
|
+
}
|
|
529
|
+
for (const [depName, range] of Object.entries(adapter.extraDependencies ?? {})) {
|
|
530
|
+
dependencies = withDependency(dependencies, depName, range, distTag);
|
|
531
|
+
}
|
|
532
|
+
let devDependencies = withDependency(parsed.devDependencies ?? {}, "@lunora/vite", distTag, distTag);
|
|
533
|
+
for (const [depName, range] of Object.entries(COMMON_DEV_DEPENDENCIES)) {
|
|
534
|
+
devDependencies = withDependency(devDependencies, depName, range, distTag);
|
|
535
|
+
}
|
|
536
|
+
parsed.name = name;
|
|
537
|
+
parsed.dependencies = restampLunora(dependencies, distTag);
|
|
538
|
+
parsed.devDependencies = restampLunora(devDependencies, distTag);
|
|
539
|
+
parsed.scripts = { ...parsed.scripts, codegen: "lunora codegen", deploy: "vite build && lunora deploy" };
|
|
540
|
+
writeFileSync(path, `${JSON.stringify(parsed, void 0, 4)}
|
|
541
|
+
`, "utf8");
|
|
542
|
+
};
|
|
543
|
+
const patchBaseViteConfig = (target, logger) => {
|
|
544
|
+
const candidate = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"].map((file) => join$1(target, file)).find((path) => existsSync(path));
|
|
545
|
+
if (candidate === void 0) {
|
|
546
|
+
logger.warn("overlay: no vite.config found in the create-vite base — add `lunora()` to your Vite plugins manually.");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
const result = patchViteConfig(readFileSync(candidate, "utf8"));
|
|
550
|
+
if (result.changed) {
|
|
551
|
+
writeFileSync(candidate, result.code, "utf8");
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
const applyLunoraOverlay = (options) => {
|
|
555
|
+
const { adapter, distTag, logger, name, target } = options;
|
|
556
|
+
const written = [];
|
|
557
|
+
writeFile(target, join$1("lunora", "schema.ts"), LUNORA_SCHEMA, written);
|
|
558
|
+
writeFile(target, join$1("lunora", "messages.ts"), LUNORA_MESSAGES, written);
|
|
559
|
+
writeFile(target, join$1("src", "server.ts"), SERVER_ENTRY, written);
|
|
560
|
+
writeFile(target, "wrangler.jsonc", WRANGLER.replaceAll("__NAME__", name), written);
|
|
561
|
+
for (const file of adapter.files) {
|
|
562
|
+
writeFile(target, file.path, file.contents, written);
|
|
563
|
+
}
|
|
564
|
+
patchBaseViteConfig(target, logger);
|
|
565
|
+
patchPackageJson(target, name, adapter, distTag);
|
|
566
|
+
ensureGitignore(target);
|
|
567
|
+
return written;
|
|
568
|
+
};
|
|
569
|
+
|
|
305
570
|
const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".gitignore", ".html", ".js", ".json", ".jsonc", ".md", ".mjs", ".ts", ".tsx"]);
|
|
306
571
|
const VITE_CONFIG_CANDIDATES = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"];
|
|
307
572
|
const MINIMAL_VITE_CONFIG = `import { defineConfig } from "vite";
|
|
@@ -404,12 +669,60 @@ const isSafeSource = (source) => {
|
|
|
404
669
|
}
|
|
405
670
|
return source.startsWith("gh:") || source.startsWith("github:") || source.startsWith("https://");
|
|
406
671
|
};
|
|
407
|
-
const logScaffoldSuccess = (logger, written, target
|
|
672
|
+
const logScaffoldSuccess = (logger, written, target) => {
|
|
408
673
|
logger.success(`scaffolded ${String(written.length)} files into ${target}`);
|
|
674
|
+
};
|
|
675
|
+
const runScriptCommand = (manager, script) => {
|
|
676
|
+
if (manager === "npm") {
|
|
677
|
+
return `npm run ${script}`;
|
|
678
|
+
}
|
|
679
|
+
if (manager === "bun") {
|
|
680
|
+
return `bun run ${script}`;
|
|
681
|
+
}
|
|
682
|
+
return `${manager} ${script}`;
|
|
683
|
+
};
|
|
684
|
+
const printNextSteps = (logger, name, installed) => {
|
|
685
|
+
const manager = installed ?? "pnpm";
|
|
409
686
|
logger.info("next steps:");
|
|
410
687
|
logger.info(` cd ${name}`);
|
|
411
|
-
|
|
412
|
-
|
|
688
|
+
if (installed === void 0) {
|
|
689
|
+
logger.info(` ${manager} install`);
|
|
690
|
+
}
|
|
691
|
+
logger.info(` ${runScriptCommand(manager, "dev")}`);
|
|
692
|
+
};
|
|
693
|
+
const offerInstallIsInteractive = (options) => options.yes !== true && (options.installPrompt !== void 0 || isInteractive());
|
|
694
|
+
const maybeOfferInstall = async (options, target) => {
|
|
695
|
+
if (!offerInstallIsInteractive(options)) {
|
|
696
|
+
return void 0;
|
|
697
|
+
}
|
|
698
|
+
const managers = detectInstalledManagers(options.packageManagerProbe);
|
|
699
|
+
const [defaultManager] = managers;
|
|
700
|
+
if (defaultManager === void 0) {
|
|
701
|
+
return void 0;
|
|
702
|
+
}
|
|
703
|
+
const confirm = options.installPrompt?.confirmInstall ?? (async () => tuiConfirm("Install dependencies now?", { defaultYes: true }));
|
|
704
|
+
if (!await confirm()) {
|
|
705
|
+
return void 0;
|
|
706
|
+
}
|
|
707
|
+
let manager = defaultManager;
|
|
708
|
+
if (managers.length > 1) {
|
|
709
|
+
manager = options.installPrompt ? await options.installPrompt.selectManager(managers) : await tuiSelect(
|
|
710
|
+
"Which package manager?",
|
|
711
|
+
managers.map((candidate) => {
|
|
712
|
+
return { label: candidate, value: candidate };
|
|
713
|
+
}),
|
|
714
|
+
{ default: defaultManager }
|
|
715
|
+
) ?? defaultManager;
|
|
716
|
+
}
|
|
717
|
+
const spawner = options.spawner ?? defaultSpawner;
|
|
718
|
+
const { args, command } = installArgsFor(manager);
|
|
719
|
+
const result = await withTuiSpinner(`Installing dependencies with ${manager}…`, () => spawner({ args, command, cwd: target }));
|
|
720
|
+
if (result.code !== 0) {
|
|
721
|
+
options.logger.warn(`\`${command} install\` exited with code ${String(result.code)} — run it yourself in ${basename(target)}/.`);
|
|
722
|
+
return void 0;
|
|
723
|
+
}
|
|
724
|
+
options.logger.success(`installed dependencies with ${manager}`);
|
|
725
|
+
return manager;
|
|
413
726
|
};
|
|
414
727
|
const scaffoldFromLocal = (fromRoot, templateType, target, name, logger) => {
|
|
415
728
|
const templateDirectory = join$1(fromRoot, templateType);
|
|
@@ -418,7 +731,7 @@ const scaffoldFromLocal = (fromRoot, templateType, target, name, logger) => {
|
|
|
418
731
|
return { code: 1, files: [], target };
|
|
419
732
|
}
|
|
420
733
|
const written = copyTemplate(templateDirectory, target, name);
|
|
421
|
-
logScaffoldSuccess(logger, written, target
|
|
734
|
+
logScaffoldSuccess(logger, written, target);
|
|
422
735
|
return { code: 0, files: written, target };
|
|
423
736
|
};
|
|
424
737
|
const scaffoldFromRemote = async (options) => {
|
|
@@ -427,22 +740,25 @@ const scaffoldFromRemote = async (options) => {
|
|
|
427
740
|
const stagingDirectory = join$1(stagingRoot, "template");
|
|
428
741
|
try {
|
|
429
742
|
const remote = resolveTemplateSource(templateType, source, ref);
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
743
|
+
const downloaded = await withTuiSpinner(
|
|
744
|
+
`Fetching the ${templateType} template…`,
|
|
745
|
+
() => downloadTemplate(remote, {
|
|
746
|
+
cwd: stagingRoot,
|
|
747
|
+
dir: stagingDirectory,
|
|
748
|
+
force: true,
|
|
749
|
+
install: false,
|
|
750
|
+
silent: true
|
|
751
|
+
})
|
|
752
|
+
);
|
|
438
753
|
const staged = collectFiles(stagingDirectory);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
754
|
+
logger.info(
|
|
755
|
+
downloaded.commit ? `template: ${downloaded.source} @ ${downloaded.commit} (${String(staged.length)} files)` : `template: ${downloaded.source} (${String(staged.length)} files)`
|
|
756
|
+
);
|
|
757
|
+
const written = await withTuiSpinner(
|
|
758
|
+
`Scaffolding ${String(staged.length)} files into ${name}/…`,
|
|
759
|
+
() => Promise.resolve(copyTemplate(stagingDirectory, target, name))
|
|
760
|
+
);
|
|
761
|
+
logScaffoldSuccess(logger, written, target);
|
|
446
762
|
return { code: 0, files: written, target };
|
|
447
763
|
} catch (error) {
|
|
448
764
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -452,6 +768,50 @@ const scaffoldFromRemote = async (options) => {
|
|
|
452
768
|
rmSync(stagingRoot, { force: true, recursive: true });
|
|
453
769
|
}
|
|
454
770
|
};
|
|
771
|
+
const renameCreateViteDotfiles = (directory) => {
|
|
772
|
+
for (const file of ["_gitignore", "_npmrc", "_gitattributes"]) {
|
|
773
|
+
const from = join$1(directory, file);
|
|
774
|
+
if (existsSync(from)) {
|
|
775
|
+
renameSync(from, join$1(directory, `.${file.slice(1)}`));
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
const scaffoldViteOverlay = async (options) => {
|
|
780
|
+
const { framework, logger, name, overlayBaseFrom, target } = options;
|
|
781
|
+
const adapter = ADAPTERS[framework];
|
|
782
|
+
const stagingRoot = mkdtempSync(join$1(tmpdir(), "lunora-vite-base-"));
|
|
783
|
+
try {
|
|
784
|
+
if (overlayBaseFrom === void 0) {
|
|
785
|
+
const stagingDirectory = join$1(stagingRoot, "base");
|
|
786
|
+
const remote = `github:vitejs/vite/packages/create-vite/template-${adapter.createViteTemplate}#main`;
|
|
787
|
+
await withTuiSpinner(
|
|
788
|
+
`Fetching the ${adapter.label} (create-vite) base…`,
|
|
789
|
+
() => downloadTemplate(remote, { cwd: stagingRoot, dir: stagingDirectory, force: true, install: false, silent: true })
|
|
790
|
+
);
|
|
791
|
+
renameCreateViteDotfiles(stagingDirectory);
|
|
792
|
+
cpSync(stagingDirectory, target, { recursive: true });
|
|
793
|
+
} else {
|
|
794
|
+
const localBase = join$1(overlayBaseFrom, `template-${adapter.createViteTemplate}`);
|
|
795
|
+
if (!existsSync(localBase)) {
|
|
796
|
+
logger.error(`create-vite base not found on disk: ${localBase}`);
|
|
797
|
+
return { code: 1, files: [], target };
|
|
798
|
+
}
|
|
799
|
+
cpSync(localBase, target, { recursive: true });
|
|
800
|
+
}
|
|
801
|
+
const written = await withTuiSpinner(
|
|
802
|
+
`Applying the Lunora overlay (${adapter.label})…`,
|
|
803
|
+
() => Promise.resolve(applyLunoraOverlay({ adapter, distTag: resolveDistTag(), logger, name, target }))
|
|
804
|
+
);
|
|
805
|
+
logScaffoldSuccess(logger, written, target);
|
|
806
|
+
return { code: 0, files: [...collectFiles(target)], target };
|
|
807
|
+
} catch (error) {
|
|
808
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
809
|
+
logger.error(`failed to scaffold the ${adapter.label} base: ${message}`);
|
|
810
|
+
return { code: 1, files: [], target };
|
|
811
|
+
} finally {
|
|
812
|
+
rmSync(stagingRoot, { force: true, recursive: true });
|
|
813
|
+
}
|
|
814
|
+
};
|
|
455
815
|
const createMinimalViteConfig = (cwd, logger) => {
|
|
456
816
|
const target = join$1(cwd, "vite.config.ts");
|
|
457
817
|
try {
|
|
@@ -570,33 +930,97 @@ const offerIsInteractive = (options) => options.yes !== true && (options.prompt
|
|
|
570
930
|
const maybeOfferExtras = async (options, projectDirectory) => {
|
|
571
931
|
const interactive = offerIsInteractive(options);
|
|
572
932
|
const apply = async (names) => {
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
933
|
+
const applyLogger = isInteractive() ? {
|
|
934
|
+
error: (message) => {
|
|
935
|
+
options.logger.error(message);
|
|
936
|
+
},
|
|
937
|
+
info: () => {
|
|
938
|
+
},
|
|
939
|
+
success: () => {
|
|
940
|
+
},
|
|
941
|
+
warn: (message) => {
|
|
942
|
+
options.logger.warn(message);
|
|
943
|
+
}
|
|
944
|
+
} : options.logger;
|
|
945
|
+
const result = await withTuiSpinner(
|
|
946
|
+
`adding ${names.join(", ")}…`,
|
|
947
|
+
() => runAddCommand({
|
|
948
|
+
allowUnsafeSource: options.allowUnsafeSource,
|
|
949
|
+
cwd: projectDirectory,
|
|
950
|
+
from: options.registryFrom,
|
|
951
|
+
logger: applyLogger,
|
|
952
|
+
names: [...names],
|
|
953
|
+
ref: options.ref,
|
|
954
|
+
source: options.registrySource,
|
|
955
|
+
yes: true
|
|
956
|
+
})
|
|
957
|
+
);
|
|
583
958
|
return result.code === 0;
|
|
584
959
|
};
|
|
960
|
+
await tuiIntro("let's finish setting up your app");
|
|
585
961
|
await offerRegistryExtras({
|
|
586
962
|
apply,
|
|
587
963
|
interactive,
|
|
588
964
|
logger: options.logger,
|
|
589
|
-
multiSelect: options.prompt?.multiSelect ?? ((message, choices, settings) =>
|
|
590
|
-
select: options.prompt?.select ?? ((message, choices, settings) =>
|
|
965
|
+
multiSelect: options.prompt?.multiSelect ?? ((message, choices, settings) => tuiMultiSelect(message, choices, settings)),
|
|
966
|
+
select: options.prompt?.select ?? ((message, choices, settings) => tuiSelect(message, choices, settings))
|
|
591
967
|
});
|
|
592
968
|
};
|
|
969
|
+
const DEFAULT_FRAMEWORK = "react";
|
|
970
|
+
const FRAMEWORK_CHOICES = [
|
|
971
|
+
{ description: "React SPA — official create-vite base + the Lunora layer (the default)", label: "React", value: "react" },
|
|
972
|
+
{ description: "Vue SPA — create-vite base + Lunora", label: "Vue", value: "vue" },
|
|
973
|
+
{ description: "Solid SPA — create-vite base + Lunora", label: "Solid", value: "solid" },
|
|
974
|
+
{ description: "Svelte SPA — create-vite base + Lunora", label: "Svelte", value: "svelte" },
|
|
975
|
+
{ description: "TanStack Start (React) — SSR with live-loader routes", label: "TanStack Start · React", value: "tanstack-start-react" },
|
|
976
|
+
{ description: "TanStack Start (Solid)", label: "TanStack Start · Solid", value: "tanstack-start-solid" },
|
|
977
|
+
{ description: "React Router (v7, framework mode) — SSR composed into the Lunora worker", label: "React Router", value: "react-router" },
|
|
978
|
+
{ description: "Astro + a standalone Lunora worker", label: "Astro", value: "astro" },
|
|
979
|
+
{ description: "AnalogJS (Angular) — single-worker, Lunora mounted in Nitro", label: "Analog", value: "analog" },
|
|
980
|
+
{ description: "Nuxt (Vue) — single-worker, Lunora mounted in Nitro", label: "Nuxt", value: "nuxt" },
|
|
981
|
+
{ description: "SvelteKit + a standalone Lunora worker", label: "SvelteKit", value: "sveltekit" },
|
|
982
|
+
{ description: "Worker only — no frontend", label: "Standalone", value: "standalone" }
|
|
983
|
+
];
|
|
984
|
+
const OVERLAY_VALUES = Object.keys(ADAPTERS).join("|");
|
|
985
|
+
const TEMPLATE_VALUES = FRAMEWORK_CHOICES.filter((choice) => !isOverlayFramework(choice.value)).map((choice) => choice.value).join("|");
|
|
986
|
+
const toScaffoldChoice = (value) => isOverlayFramework(value) ? { framework: value, kind: "overlay" } : { kind: "template", templateType: value };
|
|
987
|
+
const resolveScaffoldChoice = async (options) => {
|
|
988
|
+
if (options.vite !== void 0) {
|
|
989
|
+
return { framework: options.vite, kind: "overlay" };
|
|
990
|
+
}
|
|
991
|
+
if (options.templateType !== void 0) {
|
|
992
|
+
return { kind: "template", templateType: options.templateType };
|
|
993
|
+
}
|
|
994
|
+
if (!isInteractive() || options.yes === true) {
|
|
995
|
+
return { framework: DEFAULT_FRAMEWORK, kind: "overlay" };
|
|
996
|
+
}
|
|
997
|
+
return toScaffoldChoice(await tuiSelect("Which framework would you like?", FRAMEWORK_CHOICES, { default: DEFAULT_FRAMEWORK }) ?? DEFAULT_FRAMEWORK);
|
|
998
|
+
};
|
|
999
|
+
const nonInteractiveInitError = (options) => {
|
|
1000
|
+
if (isInteractive() || options.yes === true) {
|
|
1001
|
+
return void 0;
|
|
1002
|
+
}
|
|
1003
|
+
const missing = [];
|
|
1004
|
+
if (options.name === void 0) {
|
|
1005
|
+
missing.push("a project name (`lunora init <name>`)");
|
|
1006
|
+
}
|
|
1007
|
+
if (options.templateType === void 0 && options.vite === void 0) {
|
|
1008
|
+
missing.push(`a framework — \`--vite <${OVERLAY_VALUES}>\` for an SPA, or \`-t <${TEMPLATE_VALUES}>\` for a bespoke template`);
|
|
1009
|
+
}
|
|
1010
|
+
if (missing.length === 0) {
|
|
1011
|
+
return void 0;
|
|
1012
|
+
}
|
|
1013
|
+
return `lunora init can't prompt in a non-interactive terminal — provide ${missing.join(" and ")}, or pass --yes to accept the defaults.`;
|
|
1014
|
+
};
|
|
593
1015
|
const scaffoldNewProject = async (options, cwd) => {
|
|
594
|
-
|
|
595
|
-
const
|
|
596
|
-
if (
|
|
597
|
-
options.logger.
|
|
1016
|
+
await tuiBanner("realtime backend on Cloudflare Workers + Durable Objects");
|
|
1017
|
+
const blocked = nonInteractiveInitError(options);
|
|
1018
|
+
if (blocked !== void 0) {
|
|
1019
|
+
options.logger.error(blocked);
|
|
598
1020
|
return { code: 1, files: [], target: "" };
|
|
599
1021
|
}
|
|
1022
|
+
const name = options.name ?? await tuiText("What should we call your project?", { default: "lunora-app", placeholder: "lunora-app" });
|
|
1023
|
+
const choice = await resolveScaffoldChoice(options);
|
|
600
1024
|
if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
|
|
601
1025
|
options.logger.error(`init: refusing project name "${name}" — must not contain path separators or be "." / "..".`);
|
|
602
1026
|
return { code: 1, files: [], target: "" };
|
|
@@ -609,6 +1033,19 @@ const scaffoldNewProject = async (options, cwd) => {
|
|
|
609
1033
|
return { code: 1, files: [], target };
|
|
610
1034
|
}
|
|
611
1035
|
}
|
|
1036
|
+
if (choice.kind === "overlay") {
|
|
1037
|
+
if (!isOverlayFramework(choice.framework)) {
|
|
1038
|
+
options.logger.error(`init: unknown framework "${choice.framework}". Supported overlays: ${Object.keys(ADAPTERS).join(", ")}.`);
|
|
1039
|
+
return { code: 1, files: [], target };
|
|
1040
|
+
}
|
|
1041
|
+
mkdirSync(target, { recursive: true });
|
|
1042
|
+
return scaffoldViteOverlay({ framework: choice.framework, logger: options.logger, name, overlayBaseFrom: options.overlayBaseFrom, target });
|
|
1043
|
+
}
|
|
1044
|
+
const { templateType } = choice;
|
|
1045
|
+
if (templateType === "next") {
|
|
1046
|
+
options.logger.warn('template "next" is not yet available — re-run with `--vite react` or `-t standalone`.');
|
|
1047
|
+
return { code: 1, files: [], target };
|
|
1048
|
+
}
|
|
612
1049
|
if (options.from !== void 0) {
|
|
613
1050
|
return scaffoldFromLocal(options.from, templateType, target, name, options.logger);
|
|
614
1051
|
}
|
|
@@ -625,13 +1062,18 @@ const runInitCommand = async (options) => {
|
|
|
625
1062
|
const result = options.inPlace === true ? runInPlaceInit(cwd, options.logger) : await scaffoldNewProject(options, cwd);
|
|
626
1063
|
if (result.code === 0 && result.target !== "") {
|
|
627
1064
|
await maybeOfferExtras(options, result.target);
|
|
1065
|
+
const installedManager = options.inPlace === true ? void 0 : await maybeOfferInstall(options, result.target);
|
|
1066
|
+
if (options.inPlace !== true) {
|
|
1067
|
+
await tuiOutro("you're all set 🎉");
|
|
1068
|
+
printNextSteps(options.logger, basename(result.target), installedManager);
|
|
1069
|
+
}
|
|
628
1070
|
}
|
|
629
1071
|
if (result.code === 0 && options.ci !== void 0) {
|
|
630
1072
|
scaffoldCiWorkflow(options.inPlace === true ? cwd : result.target, options.ci, options.logger);
|
|
631
1073
|
}
|
|
632
1074
|
return result;
|
|
633
1075
|
};
|
|
634
|
-
const isTemplate = (value) => value === "astro" || value === "next" || value === "nuxt" || value === "
|
|
1076
|
+
const isTemplate = (value) => value === "analog" || value === "astro" || value === "next" || value === "nuxt" || value === "react-router" || value === "standalone" || value === "sveltekit" || value === "tanstack-start-react" || value === "tanstack-start-solid";
|
|
635
1077
|
const resolveCiProvider = (raw, logger) => {
|
|
636
1078
|
if (raw === void 0) {
|
|
637
1079
|
return void 0;
|
|
@@ -643,8 +1085,7 @@ const resolveCiProvider = (raw, logger) => {
|
|
|
643
1085
|
return void 0;
|
|
644
1086
|
};
|
|
645
1087
|
const execute = defineHandler(({ argument, cwd, logger, options }) => {
|
|
646
|
-
const
|
|
647
|
-
const template = isTemplate(templateRaw) ? templateRaw : "vite";
|
|
1088
|
+
const templateType = options.template !== void 0 && isTemplate(options.template) ? options.template : void 0;
|
|
648
1089
|
return runInitCommand({
|
|
649
1090
|
allowUnsafeSource: options.allowUnsafeSource === true,
|
|
650
1091
|
cwd,
|
|
@@ -656,7 +1097,8 @@ const execute = defineHandler(({ argument, cwd, logger, options }) => {
|
|
|
656
1097
|
name: argument[0],
|
|
657
1098
|
ref: options.ref,
|
|
658
1099
|
source: options.source,
|
|
659
|
-
templateType
|
|
1100
|
+
templateType,
|
|
1101
|
+
vite: options.vite,
|
|
660
1102
|
yes: options.yes === true
|
|
661
1103
|
});
|
|
662
1104
|
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync,
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync, mkdtempSync, rmSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
|
-
import {
|
|
3
|
+
import { discoverSchema, discoverMigrations } from '@lunora/codegen';
|
|
4
4
|
import { join } from '@visulima/path';
|
|
5
5
|
import { Project } from 'ts-morph';
|
|
6
6
|
import { r as resolveAdminBaseUrl } from '../packem_shared/admin-url-4UzT-CI4.mjs';
|
|
7
7
|
import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
|
|
8
|
-
import { diffSnapshots, renderMigrationFile } from '../packem_shared/diffSnapshots-
|
|
8
|
+
import { diffSnapshots, renderMigrationFile } from '../packem_shared/diffSnapshots-BeDvvNiF.mjs';
|
|
9
9
|
import { a as resolveProductionWorkerUrl } from '../packem_shared/resolve-target-qbsJ_5sF.mjs';
|
|
10
|
-
import schemaIrToSnapshot from '../packem_shared/schemaIrToSnapshot-
|
|
10
|
+
import schemaIrToSnapshot from '../packem_shared/schemaIrToSnapshot-DdsljJT-.mjs';
|
|
11
11
|
import { runExportCommand, runImportCommand } from '../packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs';
|
|
12
12
|
|
|
13
13
|
const SNAPSHOT_FILENAME = ".snapshot.json";
|