@lunora/cli 1.0.0-alpha.5 → 1.0.0-alpha.7
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 +466 -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,275 @@ 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", ".env", ".env.*", "!.env.example", ".lunora/", ".lunora-cache", "lunora/_generated"];
|
|
493
|
+
const ENV_EXAMPLE = `# Lunora endpoint for the browser client.
|
|
494
|
+
# Vite statically replaces \`import.meta.env.VITE_LUNORA_URL\` at \`vite dev\` / build.
|
|
495
|
+
# Leave it unset to use the page origin; set it to point at a deployed Worker:
|
|
496
|
+
#
|
|
497
|
+
# VITE_LUNORA_URL=https://my-app.example.workers.dev
|
|
498
|
+
`;
|
|
499
|
+
const COMMON_DEV_DEPENDENCIES = {
|
|
500
|
+
"@cloudflare/workers-types": "^4.20260611.1",
|
|
501
|
+
wrangler: "^4.100.0"
|
|
502
|
+
};
|
|
503
|
+
const writeFile = (target, relativePath, contents, written) => {
|
|
504
|
+
const destination = join$1(target, relativePath);
|
|
505
|
+
mkdirSync(dirname$1(destination), { recursive: true });
|
|
506
|
+
writeFileSync(destination, contents, "utf8");
|
|
507
|
+
written.push(destination);
|
|
508
|
+
};
|
|
509
|
+
const NEWLINE = /\r?\n/;
|
|
510
|
+
const ensureGitignore = (target) => {
|
|
511
|
+
const path = join$1(target, ".gitignore");
|
|
512
|
+
const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
513
|
+
const missing = GITIGNORE_ADDITIONS.filter((entry) => !existing.split(NEWLINE).includes(entry));
|
|
514
|
+
if (missing.length === 0) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
518
|
+
writeFileSync(path, `${existing}${prefix}
|
|
519
|
+
# Lunora
|
|
520
|
+
${missing.join("\n")}
|
|
521
|
+
`, "utf8");
|
|
522
|
+
};
|
|
523
|
+
const stampRange = (name, range, distTag) => name === "lunorash" || name.startsWith("@lunora/") ? distTag : range;
|
|
524
|
+
const withDependency = (map, name, range, distTag) => {
|
|
525
|
+
return { ...map, [name]: stampRange(name, range, distTag) };
|
|
526
|
+
};
|
|
527
|
+
const restampLunora = (map, distTag) => Object.fromEntries(Object.entries(map).map(([name, range]) => [name, stampRange(name, range, distTag)]));
|
|
528
|
+
const patchPackageJson = (target, name, adapter, distTag) => {
|
|
529
|
+
const path = join$1(target, "package.json");
|
|
530
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
531
|
+
let dependencies = withDependency(parsed.dependencies ?? {}, "lunorash", distTag, distTag);
|
|
532
|
+
if (adapter.adapter.startsWith("@lunora/")) {
|
|
533
|
+
dependencies = withDependency(dependencies, adapter.adapter, distTag, distTag);
|
|
534
|
+
}
|
|
535
|
+
for (const [depName, range] of Object.entries(adapter.extraDependencies ?? {})) {
|
|
536
|
+
dependencies = withDependency(dependencies, depName, range, distTag);
|
|
537
|
+
}
|
|
538
|
+
let devDependencies = withDependency(parsed.devDependencies ?? {}, "@lunora/vite", distTag, distTag);
|
|
539
|
+
for (const [depName, range] of Object.entries(COMMON_DEV_DEPENDENCIES)) {
|
|
540
|
+
devDependencies = withDependency(devDependencies, depName, range, distTag);
|
|
541
|
+
}
|
|
542
|
+
parsed.name = name;
|
|
543
|
+
parsed.dependencies = restampLunora(dependencies, distTag);
|
|
544
|
+
parsed.devDependencies = restampLunora(devDependencies, distTag);
|
|
545
|
+
parsed.scripts = { ...parsed.scripts, codegen: "lunora codegen", deploy: "vite build && lunora deploy" };
|
|
546
|
+
writeFileSync(path, `${JSON.stringify(parsed, void 0, 4)}
|
|
547
|
+
`, "utf8");
|
|
548
|
+
};
|
|
549
|
+
const patchBaseViteConfig = (target, logger) => {
|
|
550
|
+
const candidate = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"].map((file) => join$1(target, file)).find((path) => existsSync(path));
|
|
551
|
+
if (candidate === void 0) {
|
|
552
|
+
logger.warn("overlay: no vite.config found in the create-vite base — add `lunora()` to your Vite plugins manually.");
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const result = patchViteConfig(readFileSync(candidate, "utf8"));
|
|
556
|
+
if (result.changed) {
|
|
557
|
+
writeFileSync(candidate, result.code, "utf8");
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
const applyLunoraOverlay = (options) => {
|
|
561
|
+
const { adapter, distTag, logger, name, target } = options;
|
|
562
|
+
const written = [];
|
|
563
|
+
writeFile(target, join$1("lunora", "schema.ts"), LUNORA_SCHEMA, written);
|
|
564
|
+
writeFile(target, join$1("lunora", "messages.ts"), LUNORA_MESSAGES, written);
|
|
565
|
+
writeFile(target, join$1("src", "server.ts"), SERVER_ENTRY, written);
|
|
566
|
+
writeFile(target, "wrangler.jsonc", WRANGLER.replaceAll("__NAME__", name), written);
|
|
567
|
+
writeFile(target, ".env.example", ENV_EXAMPLE, written);
|
|
568
|
+
for (const file of adapter.files) {
|
|
569
|
+
writeFile(target, file.path, file.contents, written);
|
|
570
|
+
}
|
|
571
|
+
patchBaseViteConfig(target, logger);
|
|
572
|
+
patchPackageJson(target, name, adapter, distTag);
|
|
573
|
+
ensureGitignore(target);
|
|
574
|
+
return written;
|
|
575
|
+
};
|
|
576
|
+
|
|
306
577
|
const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".gitignore", ".html", ".js", ".json", ".jsonc", ".md", ".mjs", ".ts", ".tsx"]);
|
|
307
578
|
const VITE_CONFIG_CANDIDATES = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"];
|
|
308
579
|
const MINIMAL_VITE_CONFIG = `import { defineConfig } from "vite";
|
|
@@ -405,12 +676,60 @@ const isSafeSource = (source) => {
|
|
|
405
676
|
}
|
|
406
677
|
return source.startsWith("gh:") || source.startsWith("github:") || source.startsWith("https://");
|
|
407
678
|
};
|
|
408
|
-
const logScaffoldSuccess = (logger, written, target
|
|
679
|
+
const logScaffoldSuccess = (logger, written, target) => {
|
|
409
680
|
logger.success(`scaffolded ${String(written.length)} files into ${target}`);
|
|
681
|
+
};
|
|
682
|
+
const runScriptCommand = (manager, script) => {
|
|
683
|
+
if (manager === "npm") {
|
|
684
|
+
return `npm run ${script}`;
|
|
685
|
+
}
|
|
686
|
+
if (manager === "bun") {
|
|
687
|
+
return `bun run ${script}`;
|
|
688
|
+
}
|
|
689
|
+
return `${manager} ${script}`;
|
|
690
|
+
};
|
|
691
|
+
const printNextSteps = (logger, name, installed) => {
|
|
692
|
+
const manager = installed ?? "pnpm";
|
|
410
693
|
logger.info("next steps:");
|
|
411
694
|
logger.info(` cd ${name}`);
|
|
412
|
-
|
|
413
|
-
|
|
695
|
+
if (installed === void 0) {
|
|
696
|
+
logger.info(` ${manager} install`);
|
|
697
|
+
}
|
|
698
|
+
logger.info(` ${runScriptCommand(manager, "dev")}`);
|
|
699
|
+
};
|
|
700
|
+
const offerInstallIsInteractive = (options) => options.yes !== true && (options.installPrompt !== void 0 || isInteractive());
|
|
701
|
+
const maybeOfferInstall = async (options, target) => {
|
|
702
|
+
if (!offerInstallIsInteractive(options)) {
|
|
703
|
+
return void 0;
|
|
704
|
+
}
|
|
705
|
+
const managers = detectInstalledManagers(options.packageManagerProbe);
|
|
706
|
+
const [defaultManager] = managers;
|
|
707
|
+
if (defaultManager === void 0) {
|
|
708
|
+
return void 0;
|
|
709
|
+
}
|
|
710
|
+
const confirm = options.installPrompt?.confirmInstall ?? (async () => tuiConfirm("Install dependencies now?", { defaultYes: true }));
|
|
711
|
+
if (!await confirm()) {
|
|
712
|
+
return void 0;
|
|
713
|
+
}
|
|
714
|
+
let manager = defaultManager;
|
|
715
|
+
if (managers.length > 1) {
|
|
716
|
+
manager = options.installPrompt ? await options.installPrompt.selectManager(managers) : await tuiSelect(
|
|
717
|
+
"Which package manager?",
|
|
718
|
+
managers.map((candidate) => {
|
|
719
|
+
return { label: candidate, value: candidate };
|
|
720
|
+
}),
|
|
721
|
+
{ default: defaultManager }
|
|
722
|
+
) ?? defaultManager;
|
|
723
|
+
}
|
|
724
|
+
const spawner = options.spawner ?? defaultSpawner;
|
|
725
|
+
const { args, command } = installArgsFor(manager);
|
|
726
|
+
const result = await withTuiSpinner(`Installing dependencies with ${manager}…`, () => spawner({ args, command, cwd: target }));
|
|
727
|
+
if (result.code !== 0) {
|
|
728
|
+
options.logger.warn(`\`${command} install\` exited with code ${String(result.code)} — run it yourself in ${basename(target)}/.`);
|
|
729
|
+
return void 0;
|
|
730
|
+
}
|
|
731
|
+
options.logger.success(`installed dependencies with ${manager}`);
|
|
732
|
+
return manager;
|
|
414
733
|
};
|
|
415
734
|
const scaffoldFromLocal = (fromRoot, templateType, target, name, logger) => {
|
|
416
735
|
const templateDirectory = join$1(fromRoot, templateType);
|
|
@@ -419,7 +738,7 @@ const scaffoldFromLocal = (fromRoot, templateType, target, name, logger) => {
|
|
|
419
738
|
return { code: 1, files: [], target };
|
|
420
739
|
}
|
|
421
740
|
const written = copyTemplate(templateDirectory, target, name);
|
|
422
|
-
logScaffoldSuccess(logger, written, target
|
|
741
|
+
logScaffoldSuccess(logger, written, target);
|
|
423
742
|
return { code: 0, files: written, target };
|
|
424
743
|
};
|
|
425
744
|
const scaffoldFromRemote = async (options) => {
|
|
@@ -428,22 +747,25 @@ const scaffoldFromRemote = async (options) => {
|
|
|
428
747
|
const stagingDirectory = join$1(stagingRoot, "template");
|
|
429
748
|
try {
|
|
430
749
|
const remote = resolveTemplateSource(templateType, source, ref);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
750
|
+
const downloaded = await withTuiSpinner(
|
|
751
|
+
`Fetching the ${templateType} template…`,
|
|
752
|
+
() => downloadTemplate(remote, {
|
|
753
|
+
cwd: stagingRoot,
|
|
754
|
+
dir: stagingDirectory,
|
|
755
|
+
force: true,
|
|
756
|
+
install: false,
|
|
757
|
+
silent: true
|
|
758
|
+
})
|
|
759
|
+
);
|
|
439
760
|
const staged = collectFiles(stagingDirectory);
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
761
|
+
logger.info(
|
|
762
|
+
downloaded.commit ? `template: ${downloaded.source} @ ${downloaded.commit} (${String(staged.length)} files)` : `template: ${downloaded.source} (${String(staged.length)} files)`
|
|
763
|
+
);
|
|
764
|
+
const written = await withTuiSpinner(
|
|
765
|
+
`Scaffolding ${String(staged.length)} files into ${name}/…`,
|
|
766
|
+
() => Promise.resolve(copyTemplate(stagingDirectory, target, name))
|
|
767
|
+
);
|
|
768
|
+
logScaffoldSuccess(logger, written, target);
|
|
447
769
|
return { code: 0, files: written, target };
|
|
448
770
|
} catch (error) {
|
|
449
771
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -453,6 +775,50 @@ const scaffoldFromRemote = async (options) => {
|
|
|
453
775
|
rmSync(stagingRoot, { force: true, recursive: true });
|
|
454
776
|
}
|
|
455
777
|
};
|
|
778
|
+
const renameCreateViteDotfiles = (directory) => {
|
|
779
|
+
for (const file of ["_gitignore", "_npmrc", "_gitattributes"]) {
|
|
780
|
+
const from = join$1(directory, file);
|
|
781
|
+
if (existsSync(from)) {
|
|
782
|
+
renameSync(from, join$1(directory, `.${file.slice(1)}`));
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
const scaffoldViteOverlay = async (options) => {
|
|
787
|
+
const { framework, logger, name, overlayBaseFrom, target } = options;
|
|
788
|
+
const adapter = ADAPTERS[framework];
|
|
789
|
+
const stagingRoot = mkdtempSync(join$1(tmpdir(), "lunora-vite-base-"));
|
|
790
|
+
try {
|
|
791
|
+
if (overlayBaseFrom === void 0) {
|
|
792
|
+
const stagingDirectory = join$1(stagingRoot, "base");
|
|
793
|
+
const remote = `github:vitejs/vite/packages/create-vite/template-${adapter.createViteTemplate}#main`;
|
|
794
|
+
await withTuiSpinner(
|
|
795
|
+
`Fetching the ${adapter.label} (create-vite) base…`,
|
|
796
|
+
() => downloadTemplate(remote, { cwd: stagingRoot, dir: stagingDirectory, force: true, install: false, silent: true })
|
|
797
|
+
);
|
|
798
|
+
renameCreateViteDotfiles(stagingDirectory);
|
|
799
|
+
cpSync(stagingDirectory, target, { recursive: true });
|
|
800
|
+
} else {
|
|
801
|
+
const localBase = join$1(overlayBaseFrom, `template-${adapter.createViteTemplate}`);
|
|
802
|
+
if (!existsSync(localBase)) {
|
|
803
|
+
logger.error(`create-vite base not found on disk: ${localBase}`);
|
|
804
|
+
return { code: 1, files: [], target };
|
|
805
|
+
}
|
|
806
|
+
cpSync(localBase, target, { recursive: true });
|
|
807
|
+
}
|
|
808
|
+
const written = await withTuiSpinner(
|
|
809
|
+
`Applying the Lunora overlay (${adapter.label})…`,
|
|
810
|
+
() => Promise.resolve(applyLunoraOverlay({ adapter, distTag: resolveDistTag(), logger, name, target }))
|
|
811
|
+
);
|
|
812
|
+
logScaffoldSuccess(logger, written, target);
|
|
813
|
+
return { code: 0, files: [...collectFiles(target)], target };
|
|
814
|
+
} catch (error) {
|
|
815
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
816
|
+
logger.error(`failed to scaffold the ${adapter.label} base: ${message}`);
|
|
817
|
+
return { code: 1, files: [], target };
|
|
818
|
+
} finally {
|
|
819
|
+
rmSync(stagingRoot, { force: true, recursive: true });
|
|
820
|
+
}
|
|
821
|
+
};
|
|
456
822
|
const createMinimalViteConfig = (cwd, logger) => {
|
|
457
823
|
const target = join$1(cwd, "vite.config.ts");
|
|
458
824
|
try {
|
|
@@ -606,15 +972,62 @@ const maybeOfferExtras = async (options, projectDirectory) => {
|
|
|
606
972
|
multiSelect: options.prompt?.multiSelect ?? ((message, choices, settings) => tuiMultiSelect(message, choices, settings)),
|
|
607
973
|
select: options.prompt?.select ?? ((message, choices, settings) => tuiSelect(message, choices, settings))
|
|
608
974
|
});
|
|
609
|
-
|
|
975
|
+
};
|
|
976
|
+
const DEFAULT_FRAMEWORK = "react";
|
|
977
|
+
const FRAMEWORK_CHOICES = [
|
|
978
|
+
{ description: "React SPA — official create-vite base + the Lunora layer (the default)", label: "React", value: "react" },
|
|
979
|
+
{ description: "Vue SPA — create-vite base + Lunora", label: "Vue", value: "vue" },
|
|
980
|
+
{ description: "Solid SPA — create-vite base + Lunora", label: "Solid", value: "solid" },
|
|
981
|
+
{ description: "Svelte SPA — create-vite base + Lunora", label: "Svelte", value: "svelte" },
|
|
982
|
+
{ description: "TanStack Start (React) — SSR with live-loader routes", label: "TanStack Start · React", value: "tanstack-start-react" },
|
|
983
|
+
{ description: "TanStack Start (Solid)", label: "TanStack Start · Solid", value: "tanstack-start-solid" },
|
|
984
|
+
{ description: "React Router (v7, framework mode) — SSR composed into the Lunora worker", label: "React Router", value: "react-router" },
|
|
985
|
+
{ description: "Astro + a standalone Lunora worker", label: "Astro", value: "astro" },
|
|
986
|
+
{ description: "AnalogJS (Angular) — single-worker, Lunora mounted in Nitro", label: "Analog", value: "analog" },
|
|
987
|
+
{ description: "Nuxt (Vue) — single-worker, Lunora mounted in Nitro", label: "Nuxt", value: "nuxt" },
|
|
988
|
+
{ description: "SvelteKit + a standalone Lunora worker", label: "SvelteKit", value: "sveltekit" },
|
|
989
|
+
{ description: "Worker only — no frontend", label: "Standalone", value: "standalone" }
|
|
990
|
+
];
|
|
991
|
+
const OVERLAY_VALUES = Object.keys(ADAPTERS).join("|");
|
|
992
|
+
const TEMPLATE_VALUES = FRAMEWORK_CHOICES.filter((choice) => !isOverlayFramework(choice.value)).map((choice) => choice.value).join("|");
|
|
993
|
+
const toScaffoldChoice = (value) => isOverlayFramework(value) ? { framework: value, kind: "overlay" } : { kind: "template", templateType: value };
|
|
994
|
+
const resolveScaffoldChoice = async (options) => {
|
|
995
|
+
if (options.vite !== void 0) {
|
|
996
|
+
return { framework: options.vite, kind: "overlay" };
|
|
997
|
+
}
|
|
998
|
+
if (options.templateType !== void 0) {
|
|
999
|
+
return { kind: "template", templateType: options.templateType };
|
|
1000
|
+
}
|
|
1001
|
+
if (!isInteractive() || options.yes === true) {
|
|
1002
|
+
return { framework: DEFAULT_FRAMEWORK, kind: "overlay" };
|
|
1003
|
+
}
|
|
1004
|
+
return toScaffoldChoice(await tuiSelect("Which framework would you like?", FRAMEWORK_CHOICES, { default: DEFAULT_FRAMEWORK }) ?? DEFAULT_FRAMEWORK);
|
|
1005
|
+
};
|
|
1006
|
+
const nonInteractiveInitError = (options) => {
|
|
1007
|
+
if (isInteractive() || options.yes === true) {
|
|
1008
|
+
return void 0;
|
|
1009
|
+
}
|
|
1010
|
+
const missing = [];
|
|
1011
|
+
if (options.name === void 0) {
|
|
1012
|
+
missing.push("a project name (`lunora init <name>`)");
|
|
1013
|
+
}
|
|
1014
|
+
if (options.templateType === void 0 && options.vite === void 0) {
|
|
1015
|
+
missing.push(`a framework — \`--vite <${OVERLAY_VALUES}>\` for an SPA, or \`-t <${TEMPLATE_VALUES}>\` for a bespoke template`);
|
|
1016
|
+
}
|
|
1017
|
+
if (missing.length === 0) {
|
|
1018
|
+
return void 0;
|
|
1019
|
+
}
|
|
1020
|
+
return `lunora init can't prompt in a non-interactive terminal — provide ${missing.join(" and ")}, or pass --yes to accept the defaults.`;
|
|
610
1021
|
};
|
|
611
1022
|
const scaffoldNewProject = async (options, cwd) => {
|
|
612
|
-
|
|
613
|
-
const
|
|
614
|
-
if (
|
|
615
|
-
options.logger.
|
|
1023
|
+
await tuiBanner("realtime backend on Cloudflare Workers + Durable Objects");
|
|
1024
|
+
const blocked = nonInteractiveInitError(options);
|
|
1025
|
+
if (blocked !== void 0) {
|
|
1026
|
+
options.logger.error(blocked);
|
|
616
1027
|
return { code: 1, files: [], target: "" };
|
|
617
1028
|
}
|
|
1029
|
+
const name = options.name ?? await tuiText("What should we call your project?", { default: "lunora-app", placeholder: "lunora-app" });
|
|
1030
|
+
const choice = await resolveScaffoldChoice(options);
|
|
618
1031
|
if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
|
|
619
1032
|
options.logger.error(`init: refusing project name "${name}" — must not contain path separators or be "." / "..".`);
|
|
620
1033
|
return { code: 1, files: [], target: "" };
|
|
@@ -627,6 +1040,19 @@ const scaffoldNewProject = async (options, cwd) => {
|
|
|
627
1040
|
return { code: 1, files: [], target };
|
|
628
1041
|
}
|
|
629
1042
|
}
|
|
1043
|
+
if (choice.kind === "overlay") {
|
|
1044
|
+
if (!isOverlayFramework(choice.framework)) {
|
|
1045
|
+
options.logger.error(`init: unknown framework "${choice.framework}". Supported overlays: ${Object.keys(ADAPTERS).join(", ")}.`);
|
|
1046
|
+
return { code: 1, files: [], target };
|
|
1047
|
+
}
|
|
1048
|
+
mkdirSync(target, { recursive: true });
|
|
1049
|
+
return scaffoldViteOverlay({ framework: choice.framework, logger: options.logger, name, overlayBaseFrom: options.overlayBaseFrom, target });
|
|
1050
|
+
}
|
|
1051
|
+
const { templateType } = choice;
|
|
1052
|
+
if (templateType === "next") {
|
|
1053
|
+
options.logger.warn('template "next" is not yet available — re-run with `--vite react` or `-t standalone`.');
|
|
1054
|
+
return { code: 1, files: [], target };
|
|
1055
|
+
}
|
|
630
1056
|
if (options.from !== void 0) {
|
|
631
1057
|
return scaffoldFromLocal(options.from, templateType, target, name, options.logger);
|
|
632
1058
|
}
|
|
@@ -643,13 +1069,18 @@ const runInitCommand = async (options) => {
|
|
|
643
1069
|
const result = options.inPlace === true ? runInPlaceInit(cwd, options.logger) : await scaffoldNewProject(options, cwd);
|
|
644
1070
|
if (result.code === 0 && result.target !== "") {
|
|
645
1071
|
await maybeOfferExtras(options, result.target);
|
|
1072
|
+
const installedManager = options.inPlace === true ? void 0 : await maybeOfferInstall(options, result.target);
|
|
1073
|
+
if (options.inPlace !== true) {
|
|
1074
|
+
await tuiOutro("you're all set 🎉");
|
|
1075
|
+
printNextSteps(options.logger, basename(result.target), installedManager);
|
|
1076
|
+
}
|
|
646
1077
|
}
|
|
647
1078
|
if (result.code === 0 && options.ci !== void 0) {
|
|
648
1079
|
scaffoldCiWorkflow(options.inPlace === true ? cwd : result.target, options.ci, options.logger);
|
|
649
1080
|
}
|
|
650
1081
|
return result;
|
|
651
1082
|
};
|
|
652
|
-
const isTemplate = (value) => value === "astro" || value === "next" || value === "nuxt" || value === "
|
|
1083
|
+
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
1084
|
const resolveCiProvider = (raw, logger) => {
|
|
654
1085
|
if (raw === void 0) {
|
|
655
1086
|
return void 0;
|
|
@@ -661,8 +1092,7 @@ const resolveCiProvider = (raw, logger) => {
|
|
|
661
1092
|
return void 0;
|
|
662
1093
|
};
|
|
663
1094
|
const execute = defineHandler(({ argument, cwd, logger, options }) => {
|
|
664
|
-
const
|
|
665
|
-
const template = isTemplate(templateRaw) ? templateRaw : "vite-react";
|
|
1095
|
+
const templateType = options.template !== void 0 && isTemplate(options.template) ? options.template : void 0;
|
|
666
1096
|
return runInitCommand({
|
|
667
1097
|
allowUnsafeSource: options.allowUnsafeSource === true,
|
|
668
1098
|
cwd,
|
|
@@ -674,7 +1104,8 @@ const execute = defineHandler(({ argument, cwd, logger, options }) => {
|
|
|
674
1104
|
name: argument[0],
|
|
675
1105
|
ref: options.ref,
|
|
676
1106
|
source: options.source,
|
|
677
|
-
templateType
|
|
1107
|
+
templateType,
|
|
1108
|
+
vite: options.vite,
|
|
678
1109
|
yes: options.yes === true
|
|
679
1110
|
});
|
|
680
1111
|
});
|
|
@@ -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",
|