@lunora/cli 1.0.0-alpha.5 → 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 +2 -2
- package/dist/packem_chunks/handler14.mjs +1 -1
- package/dist/packem_chunks/handler16.mjs +1 -1
- package/dist/packem_chunks/handler18.mjs +1 -1
- 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 +4 -48
- package/dist/packem_chunks/runDeployCommand.mjs +1 -1
- package/dist/packem_chunks/runInitCommand.mjs +459 -35
- package/dist/packem_chunks/runMigrateGenerateCommand.mjs +4 -4
- package/dist/packem_chunks/runResetCommand.mjs +1 -1
- package/dist/packem_shared/{COMMANDS-DXaq12xm.mjs → COMMANDS-Bn8luojF.mjs} +10 -2
- package/dist/packem_shared/{commands-B9nASOYd.mjs → commands-DqsEzojt.mjs} +2 -2
- 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-BF7XreDW.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-Bm15GPJA.mjs → tui-prompts-XHFxlOg5.mjs} +41 -1
- package/package.json +3 -5
- /package/dist/packem_shared/{defaultSpawner-DxI3mebw.mjs → createRecordingSpawner-DxI3mebw.mjs} +0 -0
|
@@ -1,16 +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
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 {
|
|
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';
|
|
14
16
|
import { p as promptAuthProvider, E as EMAIL_ITEM } from '../packem_shared/features-ocSSpZtS.mjs';
|
|
15
17
|
|
|
16
18
|
const GITHUB_CONTENT = `name: Deploy
|
|
@@ -285,11 +287,16 @@ const patchViteConfig = (source) => {
|
|
|
285
287
|
|
|
286
288
|
const STACK_FEATURE_OPTIONS = [
|
|
287
289
|
{ description: "Sign-up / sign-in (asks which provider)", label: "Authentication", value: "auth" },
|
|
288
|
-
{ 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" }
|
|
289
296
|
];
|
|
290
297
|
const offerRegistryExtras = async (deps) => {
|
|
291
298
|
if (!deps.interactive) {
|
|
292
|
-
deps.logger.info("tip: add
|
|
299
|
+
deps.logger.info("tip: add features later with `lunora add <auth|email|storage|ratelimit|crons|presence|backup>`.");
|
|
293
300
|
return;
|
|
294
301
|
}
|
|
295
302
|
const picked = await deps.multiSelect("Which features do you want to add?", STACK_FEATURE_OPTIONS, { defaults: [] });
|
|
@@ -298,11 +305,268 @@ const offerRegistryExtras = async (deps) => {
|
|
|
298
305
|
const provider = await promptAuthProvider(deps.select);
|
|
299
306
|
await deps.apply([provider]);
|
|
300
307
|
} else {
|
|
301
|
-
await deps.apply([EMAIL_ITEM]);
|
|
308
|
+
await deps.apply([feature === "email" ? EMAIL_ITEM : feature]);
|
|
302
309
|
}
|
|
303
310
|
}
|
|
304
311
|
};
|
|
305
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
|
+
|
|
306
570
|
const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".gitignore", ".html", ".js", ".json", ".jsonc", ".md", ".mjs", ".ts", ".tsx"]);
|
|
307
571
|
const VITE_CONFIG_CANDIDATES = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"];
|
|
308
572
|
const MINIMAL_VITE_CONFIG = `import { defineConfig } from "vite";
|
|
@@ -405,12 +669,60 @@ const isSafeSource = (source) => {
|
|
|
405
669
|
}
|
|
406
670
|
return source.startsWith("gh:") || source.startsWith("github:") || source.startsWith("https://");
|
|
407
671
|
};
|
|
408
|
-
const logScaffoldSuccess = (logger, written, target
|
|
672
|
+
const logScaffoldSuccess = (logger, written, target) => {
|
|
409
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";
|
|
410
686
|
logger.info("next steps:");
|
|
411
687
|
logger.info(` cd ${name}`);
|
|
412
|
-
|
|
413
|
-
|
|
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;
|
|
414
726
|
};
|
|
415
727
|
const scaffoldFromLocal = (fromRoot, templateType, target, name, logger) => {
|
|
416
728
|
const templateDirectory = join$1(fromRoot, templateType);
|
|
@@ -419,7 +731,7 @@ const scaffoldFromLocal = (fromRoot, templateType, target, name, logger) => {
|
|
|
419
731
|
return { code: 1, files: [], target };
|
|
420
732
|
}
|
|
421
733
|
const written = copyTemplate(templateDirectory, target, name);
|
|
422
|
-
logScaffoldSuccess(logger, written, target
|
|
734
|
+
logScaffoldSuccess(logger, written, target);
|
|
423
735
|
return { code: 0, files: written, target };
|
|
424
736
|
};
|
|
425
737
|
const scaffoldFromRemote = async (options) => {
|
|
@@ -428,22 +740,25 @@ const scaffoldFromRemote = async (options) => {
|
|
|
428
740
|
const stagingDirectory = join$1(stagingRoot, "template");
|
|
429
741
|
try {
|
|
430
742
|
const remote = resolveTemplateSource(templateType, source, ref);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
+
);
|
|
439
753
|
const staged = collectFiles(stagingDirectory);
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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);
|
|
447
762
|
return { code: 0, files: written, target };
|
|
448
763
|
} catch (error) {
|
|
449
764
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -453,6 +768,50 @@ const scaffoldFromRemote = async (options) => {
|
|
|
453
768
|
rmSync(stagingRoot, { force: true, recursive: true });
|
|
454
769
|
}
|
|
455
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
|
+
};
|
|
456
815
|
const createMinimalViteConfig = (cwd, logger) => {
|
|
457
816
|
const target = join$1(cwd, "vite.config.ts");
|
|
458
817
|
try {
|
|
@@ -606,15 +965,62 @@ const maybeOfferExtras = async (options, projectDirectory) => {
|
|
|
606
965
|
multiSelect: options.prompt?.multiSelect ?? ((message, choices, settings) => tuiMultiSelect(message, choices, settings)),
|
|
607
966
|
select: options.prompt?.select ?? ((message, choices, settings) => tuiSelect(message, choices, settings))
|
|
608
967
|
});
|
|
609
|
-
|
|
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.`;
|
|
610
1014
|
};
|
|
611
1015
|
const scaffoldNewProject = async (options, cwd) => {
|
|
612
|
-
|
|
613
|
-
const
|
|
614
|
-
if (
|
|
615
|
-
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);
|
|
616
1020
|
return { code: 1, files: [], target: "" };
|
|
617
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);
|
|
618
1024
|
if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
|
|
619
1025
|
options.logger.error(`init: refusing project name "${name}" — must not contain path separators or be "." / "..".`);
|
|
620
1026
|
return { code: 1, files: [], target: "" };
|
|
@@ -627,6 +1033,19 @@ const scaffoldNewProject = async (options, cwd) => {
|
|
|
627
1033
|
return { code: 1, files: [], target };
|
|
628
1034
|
}
|
|
629
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
|
+
}
|
|
630
1049
|
if (options.from !== void 0) {
|
|
631
1050
|
return scaffoldFromLocal(options.from, templateType, target, name, options.logger);
|
|
632
1051
|
}
|
|
@@ -643,13 +1062,18 @@ const runInitCommand = async (options) => {
|
|
|
643
1062
|
const result = options.inPlace === true ? runInPlaceInit(cwd, options.logger) : await scaffoldNewProject(options, cwd);
|
|
644
1063
|
if (result.code === 0 && result.target !== "") {
|
|
645
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
|
+
}
|
|
646
1070
|
}
|
|
647
1071
|
if (result.code === 0 && options.ci !== void 0) {
|
|
648
1072
|
scaffoldCiWorkflow(options.inPlace === true ? cwd : result.target, options.ci, options.logger);
|
|
649
1073
|
}
|
|
650
1074
|
return result;
|
|
651
1075
|
};
|
|
652
|
-
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";
|
|
653
1077
|
const resolveCiProvider = (raw, logger) => {
|
|
654
1078
|
if (raw === void 0) {
|
|
655
1079
|
return void 0;
|
|
@@ -661,8 +1085,7 @@ const resolveCiProvider = (raw, logger) => {
|
|
|
661
1085
|
return void 0;
|
|
662
1086
|
};
|
|
663
1087
|
const execute = defineHandler(({ argument, cwd, logger, options }) => {
|
|
664
|
-
const
|
|
665
|
-
const template = isTemplate(templateRaw) ? templateRaw : "vite-react";
|
|
1088
|
+
const templateType = options.template !== void 0 && isTemplate(options.template) ? options.template : void 0;
|
|
666
1089
|
return runInitCommand({
|
|
667
1090
|
allowUnsafeSource: options.allowUnsafeSource === true,
|
|
668
1091
|
cwd,
|
|
@@ -674,7 +1097,8 @@ const execute = defineHandler(({ argument, cwd, logger, options }) => {
|
|
|
674
1097
|
name: argument[0],
|
|
675
1098
|
ref: options.ref,
|
|
676
1099
|
source: options.source,
|
|
677
|
-
templateType
|
|
1100
|
+
templateType,
|
|
1101
|
+
vite: options.vite,
|
|
678
1102
|
yes: options.yes === true
|
|
679
1103
|
});
|
|
680
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";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, rmSync } from 'node:fs';
|
|
2
2
|
import { join } from '@visulima/path';
|
|
3
3
|
import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
|
|
4
|
-
import { a as tuiConfirm } from '../packem_shared/tui-prompts-
|
|
4
|
+
import { a as tuiConfirm } from '../packem_shared/tui-prompts-XHFxlOg5.mjs';
|
|
5
5
|
|
|
6
6
|
const runResetCommand = async (options) => {
|
|
7
7
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -361,11 +361,19 @@ const initCommand = {
|
|
|
361
361
|
options: [
|
|
362
362
|
{
|
|
363
363
|
alias: "t",
|
|
364
|
-
|
|
365
|
-
|
|
364
|
+
// No default: when omitted, an interactive run shows the framework
|
|
365
|
+
// picker (default React overlay) and a non-interactive run errors.
|
|
366
|
+
// For React/Vue/Solid/Svelte SPAs use `--vite <framework>` (overlay);
|
|
367
|
+
// `-t` selects a bespoke template.
|
|
368
|
+
description: "Bespoke template (standalone | astro | nuxt | sveltekit | tanstack-start-react | tanstack-start-solid). For an SPA use --vite react|vue|solid|svelte.",
|
|
366
369
|
name: "template",
|
|
367
370
|
type: String
|
|
368
371
|
},
|
|
372
|
+
{
|
|
373
|
+
description: "Scaffold via the create-vite overlay for a framework (react | vue | solid | svelte | vanilla) — official create-vite base + Lunora layer",
|
|
374
|
+
name: "vite",
|
|
375
|
+
type: String
|
|
376
|
+
},
|
|
369
377
|
{
|
|
370
378
|
description: "Local templates root to copy from (offline-friendly; expects <type>/ subdirs)",
|
|
371
379
|
name: "from",
|