@lunora/cli 1.0.0-alpha.3 → 1.0.0-alpha.5

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from './packem_shared/COMMANDS-CHw4zOZ9.mjs';
2
+ import { runCli } from './packem_shared/COMMANDS-DXaq12xm.mjs';
3
3
 
4
4
  try {
5
5
  const code = await runCli();
package/dist/index.d.mts CHANGED
@@ -532,7 +532,7 @@ interface OfferDeps {
532
532
  * maps to the `mail` item. Picked items are applied in selection order.
533
533
  * Non-interactive: prints how to add them later and changes nothing.
534
534
  */
535
- type Template = "astro" | "next" | "nuxt" | "standalone" | "sveltekit" | "tanstack-start-react" | "tanstack-start-solid" | "vite";
535
+ type Template = "astro" | "next" | "nuxt" | "standalone" | "sveltekit" | "tanstack-start-react" | "tanstack-start-solid" | "vite-react";
536
536
  interface InitCommandOptions {
537
537
  /**
538
538
  * When true, accept `--source` values that don't start with `gh:` /
@@ -605,7 +605,7 @@ interface InitCommandResult {
605
605
  * scaffold's exit code.
606
606
  */
607
607
  declare const runInitCommand: (options: InitCommandOptions) => Promise<InitCommandResult>;
608
- /** Narrow a raw `--template` value to a known {@link Template} (defaults to vite). */
608
+ /** Narrow a raw `--template` value to a known {@link Template} (defaults to vite-react). */
609
609
  interface MigrateGenerateCommandOptions {
610
610
  cwd?: string;
611
611
  logger: Logger;
package/dist/index.d.ts CHANGED
@@ -532,7 +532,7 @@ interface OfferDeps {
532
532
  * maps to the `mail` item. Picked items are applied in selection order.
533
533
  * Non-interactive: prints how to add them later and changes nothing.
534
534
  */
535
- type Template = "astro" | "next" | "nuxt" | "standalone" | "sveltekit" | "tanstack-start-react" | "tanstack-start-solid" | "vite";
535
+ type Template = "astro" | "next" | "nuxt" | "standalone" | "sveltekit" | "tanstack-start-react" | "tanstack-start-solid" | "vite-react";
536
536
  interface InitCommandOptions {
537
537
  /**
538
538
  * When true, accept `--source` values that don't start with `gh:` /
@@ -605,7 +605,7 @@ interface InitCommandResult {
605
605
  * scaffold's exit code.
606
606
  */
607
607
  declare const runInitCommand: (options: InitCommandOptions) => Promise<InitCommandResult>;
608
- /** Narrow a raw `--template` value to a known {@link Template} (defaults to vite). */
608
+ /** Narrow a raw `--template` value to a known {@link Template} (defaults to vite-react). */
609
609
  interface MigrateGenerateCommandOptions {
610
610
  cwd?: string;
611
611
  logger: Logger;
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { COMMANDS, VERSION, runCli } from './packem_shared/COMMANDS-CHw4zOZ9.mjs';
1
+ export { COMMANDS, VERSION, runCli } from './packem_shared/COMMANDS-DXaq12xm.mjs';
2
2
  export { runCodegenCommand } from './packem_chunks/runCodegenCommand.mjs';
3
3
  export { DEFAULT_IMPORT_BATCH_SIZE, runExportCommand, runImportCommand } from './packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs';
4
4
  export { runDeployCommand } from './packem_chunks/runDeployCommand.mjs';
@@ -16,4 +16,4 @@ export { createRecordingSpawner, defaultSpawner } from './packem_shared/defaultS
16
16
  export { default as parseManifest } from './packem_shared/parseManifest--vZf2FY1.mjs';
17
17
  export { REQUIRED_COMPATIBILITY_DATE, REQUIRED_FLAG, validateWranglerProject as validateWrangler, validateWranglerConfig } from '@lunora/config';
18
18
  export { buildRegistryIndex } from './packem_shared/buildRegistryIndex-BcYe607_.mjs';
19
- export { r as runAddCommand, a as runBuildIndexCommand, b as runRegistryViewCommand } from './packem_shared/commands-DIQ3nf0C.mjs';
19
+ export { r as runAddCommand, a as runBuildIndexCommand, b as runRegistryViewCommand } from './packem_shared/commands-B9nASOYd.mjs';
@@ -1,9 +1,10 @@
1
1
  import { existsSync } from 'node:fs';
2
- import { findWranglerFile, promptSelect } from '@lunora/config';
2
+ import { findWranglerFile } from '@lunora/config';
3
3
  import { join } from '@visulima/path';
4
4
  import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
5
+ import { t as tuiSelect } from '../packem_shared/tui-prompts-Bm15GPJA.mjs';
5
6
  import { n as normalizeFeature, E as EMAIL_ITEM, D as DEFAULT_AUTH_ITEM, p as promptAuthProvider, A as AUTH_PROVIDER_OPTIONS } from '../packem_shared/features-ocSSpZtS.mjs';
6
- import { r as runAddCommand } from '../packem_shared/commands-DIQ3nf0C.mjs';
7
+ import { r as runAddCommand } from '../packem_shared/commands-B9nASOYd.mjs';
7
8
 
8
9
  const providerToItem = (provider) => {
9
10
  const value = provider.trim().toLowerCase();
@@ -24,7 +25,7 @@ const resolveAuthItem = async (options) => {
24
25
  if (options.yes === true) {
25
26
  return DEFAULT_AUTH_ITEM;
26
27
  }
27
- const select = options.promptSelect ?? ((message, choices, settings) => promptSelect(message, choices, settings));
28
+ const select = options.promptSelect ?? ((message, choices, settings) => tuiSelect(message, choices, settings));
28
29
  return promptAuthProvider(select);
29
30
  };
30
31
  const resolveFeatureItems = async (feature, options) => {
@@ -1,5 +1,5 @@
1
1
  import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
2
- import { r as runAddCommand, b as runRegistryViewCommand, a as runBuildIndexCommand } from '../packem_shared/commands-DIQ3nf0C.mjs';
2
+ import { r as runAddCommand, b as runRegistryViewCommand, a as runBuildIndexCommand } from '../packem_shared/commands-B9nASOYd.mjs';
3
3
 
4
4
  const execute = defineHandler(({ argument, cwd, logger, options }) => {
5
5
  const subcommand = argument[0];
@@ -2,12 +2,12 @@ import { existsSync } from 'node:fs';
2
2
  import { mkdtemp, writeFile, rm } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { discoverSchema, schemaFromIr } from '@lunora/codegen';
5
- import { promptYesNo } from '@lunora/config';
6
5
  import { seedPlan } from '@lunora/seed';
7
6
  import { join } from '@visulima/path';
8
7
  import { Project } from 'ts-morph';
9
8
  import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
10
9
  import { a as resolveProductionWorkerUrl } from '../packem_shared/resolve-target-qbsJ_5sF.mjs';
10
+ import { a as tuiConfirm } from '../packem_shared/tui-prompts-Bm15GPJA.mjs';
11
11
  import { runImportCommand } from '../packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs';
12
12
  import { runResetCommand } from './runResetCommand.mjs';
13
13
 
@@ -88,10 +88,8 @@ const confirmRemoteSeedTarget = async (options, generated) => {
88
88
  options.logger.error("seed: refusing to insert into a non-local target without confirmation — re-run with --yes");
89
89
  return seedFailure(1);
90
90
  }
91
- const confirmer = options.confirm ?? promptYesNo;
92
- const confirmed = await confirmer(
93
- `This will insert ${String(generated)} generated row(s) into ${options.url ?? "the production worker"}. Continue? [y/N] `
94
- );
91
+ const confirmer = options.confirm ?? tuiConfirm;
92
+ const confirmed = await confirmer(`This will insert ${String(generated)} generated row(s) into ${options.url ?? "the production worker"}. Continue?`);
95
93
  if (!confirmed) {
96
94
  options.logger.info("seed: aborted");
97
95
  return seedFailure(1);
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { detectAgentRules, inferLunoraBindings, packageNamesFromBindings, ensureDevVarsExample, ensureDevVariables, createConfirm, isInteractive, DEV_VARS_FILE, DEV_VARS_EXAMPLE_FILE, claimAgentRulesHint, AGENT_RULES_HINT, materializeRemoteWranglerConfig, formatLunoraEvent, resolveRemoteEnabled, readProjectRemotePreference } from '@lunora/config';
2
+ import { detectAgentRules, inferLunoraBindings, packageNamesFromBindings, ensureDevVarsExample, ensureDevVariables, isInteractive, DEV_VARS_FILE, DEV_VARS_EXAMPLE_FILE, claimAgentRulesHint, AGENT_RULES_HINT, materializeRemoteWranglerConfig, formatLunoraEvent, resolveRemoteEnabled, readProjectRemotePreference } from '@lunora/config';
3
3
  import { p as parseApiSpec } from '../packem_shared/api-spec-CtA6ilu4.mjs';
4
4
  import { existsSync, watch, readFileSync } from 'node:fs';
5
5
  import { join } from 'node:path';
@@ -9,6 +9,7 @@ import { dirname, join as join$1 } from '@visulima/path';
9
9
  import { createServer, request } from 'node:http';
10
10
  import { connect } from 'node:net';
11
11
  import { loadStudioAssets, studioAssetsStamp, renderStudioHtml, resolveAdminToken, SCHEMA_EDIT_ENDPOINT, POLICY_SCAFFOLD_ENDPOINT, SEED_ENDPOINT, serveJsonHandler, handleSchemaEditRequest, handlePolicyScaffoldRequest, handleSeedRequest } from '@lunora/config/studio-host';
12
+ import { c as createTuiConfirm } from '../packem_shared/tui-prompts-Bm15GPJA.mjs';
12
13
 
13
14
  const DEFAULT_DEBOUNCE_MS = 100;
14
15
  const PATH_SEGMENT_SEPARATOR = /[/\\]/u;
@@ -441,7 +442,7 @@ const offerDevVariablesScaffold = async (options, cwd) => {
441
442
  } catch {
442
443
  }
443
444
  const result = await (options.ensureEnv ?? ensureDevVariables)({
444
- confirm: createConfirm(),
445
+ confirm: createTuiConfirm(),
445
446
  cwd,
446
447
  info: (message) => {
447
448
  options.logger.info(message);
@@ -1,14 +1,16 @@
1
1
  import { existsSync, mkdirSync, writeFileSync, readdirSync, mkdtempSync, rmSync, readFileSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
- import { detectFramework as detectFramework$1, isInteractive, promptSelect, promptMultiSelect } from '@lunora/config';
3
+ import { detectFramework as detectFramework$1, isInteractive } from '@lunora/config';
4
4
  import { walkSync } from '@visulima/fs';
5
- import { resolve, join as join$1, relative, dirname as dirname$1 } from '@visulima/path';
5
+ import { resolve, join as join$1, relative, dirname as dirname$1, basename } from '@visulima/path';
6
6
  import { downloadTemplate } from 'giget';
7
+ import { modify, applyEdits } from 'jsonc-parser';
7
8
  import { join, dirname } from 'node:path';
8
9
  import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
9
10
  import MagicString from 'magic-string';
10
11
  import { Project, SyntaxKind } from 'ts-morph';
11
- import { c as resolveSourceRef, r as runAddCommand } from '../packem_shared/commands-DIQ3nf0C.mjs';
12
+ import { c as resolveSourceRef, d as resolveDistTag, r as runAddCommand } from '../packem_shared/commands-B9nASOYd.mjs';
13
+ import { b as tuiIntro, d as tuiOutro, t as tuiSelect, e as tuiMultiSelect, w as withTuiSpinner } from '../packem_shared/tui-prompts-Bm15GPJA.mjs';
12
14
  import { p as promptAuthProvider, E as EMAIL_ITEM } from '../packem_shared/features-ocSSpZtS.mjs';
13
15
 
14
16
  const GITHUB_CONTENT = `name: Deploy
@@ -19,6 +21,10 @@ on:
19
21
  pull_request:
20
22
  workflow_dispatch:
21
23
 
24
+ # Prerequisite: commit your pnpm-lock.yaml. \`pnpm install --frozen-lockfile\`
25
+ # (below) and the pnpm cache both require it — run \`pnpm install\` locally and
26
+ # commit the lockfile before pushing, or the first CI run fails.
27
+ #
22
28
  # Set these repository secrets (Settings → Secrets and variables → Actions):
23
29
  # CLOUDFLARE_API_TOKEN — a Workers-scoped API token
24
30
  # CLOUDFLARE_ACCOUNT_ID — your Cloudflare account id
@@ -65,6 +71,10 @@ jobs:
65
71
  const GITLAB_CONTENT = `stages:
66
72
  - deploy
67
73
 
74
+ # Prerequisite: commit your pnpm-lock.yaml. \`pnpm install --frozen-lockfile\`
75
+ # (below) requires it — run \`pnpm install\` locally and commit the lockfile
76
+ # before pushing, or the first pipeline fails.
77
+ #
68
78
  # Set these as masked CI/CD variables (Settings → CI/CD → Variables):
69
79
  # CLOUDFLARE_API_TOKEN — a Workers-scoped API token
70
80
  # CLOUDFLARE_ACCOUNT_ID — your Cloudflare account id
@@ -126,6 +136,9 @@ const scaffoldCiWorkflow = (projectRoot, provider, logger, options = {}) => {
126
136
  } else {
127
137
  logger.success(`--ci ${provider}: wrote ${spec.file}`);
128
138
  logger.info(`--ci ${provider}: set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID as ${spec.secretsHint} to enable deploys.`);
139
+ logger.info(
140
+ `--ci ${provider}: run \`pnpm install\` and commit pnpm-lock.yaml before pushing — the pipeline runs \`pnpm install --frozen-lockfile\`.`
141
+ );
129
142
  }
130
143
  return result;
131
144
  } catch (error) {
@@ -331,6 +344,26 @@ const isTextFile = (filePath) => {
331
344
  return TEXT_EXTENSIONS.has(filePath.slice(lastDot));
332
345
  };
333
346
  const substitute = (content, name) => content.replaceAll("{{name}}", name);
347
+ const isLunoraDep = (name) => name === "lunorash" || name.startsWith("@lunora/");
348
+ const stampLunoraDeps = (packageJsonText, distTag) => {
349
+ let parsed;
350
+ try {
351
+ parsed = JSON.parse(packageJsonText);
352
+ } catch {
353
+ return packageJsonText;
354
+ }
355
+ let text = packageJsonText;
356
+ for (const section of ["dependencies", "devDependencies"]) {
357
+ for (const name of Object.keys(parsed[section] ?? {})) {
358
+ if (!isLunoraDep(name)) {
359
+ continue;
360
+ }
361
+ const edits = modify(text, [section, name], distTag, { formattingOptions: { insertSpaces: true, tabSize: 4 } });
362
+ text = applyEdits(text, edits);
363
+ }
364
+ }
365
+ return text;
366
+ };
334
367
  const collectFiles = (directory) => {
335
368
  const out = [];
336
369
  for (const entry of walkSync(directory, { includeDirs: false, includeFiles: true })) {
@@ -341,12 +374,16 @@ const collectFiles = (directory) => {
341
374
  const copyTemplate = (sourceDirectory, target, name) => {
342
375
  const files = collectFiles(sourceDirectory);
343
376
  const written = [];
377
+ const distTag = resolveDistTag();
344
378
  for (const source of files) {
345
379
  const relativePath = relative(sourceDirectory, source);
346
380
  const destination = join$1(target, relativePath);
347
381
  mkdirSync(dirname$1(destination), { recursive: true });
348
382
  const raw = readFileSync(source);
349
- const text = isTextFile(source) ? substitute(raw.toString("utf8"), name) : void 0;
383
+ let text = isTextFile(source) ? substitute(raw.toString("utf8"), name) : void 0;
384
+ if (text !== void 0 && basename(source) === "package.json") {
385
+ text = stampLunoraDeps(text, distTag);
386
+ }
350
387
  if (text === void 0) {
351
388
  writeFileSync(destination, raw);
352
389
  } else {
@@ -534,31 +571,48 @@ const offerIsInteractive = (options) => options.yes !== true && (options.prompt
534
571
  const maybeOfferExtras = async (options, projectDirectory) => {
535
572
  const interactive = offerIsInteractive(options);
536
573
  const apply = async (names) => {
537
- const result = await runAddCommand({
538
- allowUnsafeSource: options.allowUnsafeSource,
539
- cwd: projectDirectory,
540
- from: options.registryFrom,
541
- logger: options.logger,
542
- names: [...names],
543
- ref: options.ref,
544
- source: options.registrySource,
545
- yes: true
546
- });
574
+ const applyLogger = isInteractive() ? {
575
+ error: (message) => {
576
+ options.logger.error(message);
577
+ },
578
+ info: () => {
579
+ },
580
+ success: () => {
581
+ },
582
+ warn: (message) => {
583
+ options.logger.warn(message);
584
+ }
585
+ } : options.logger;
586
+ const result = await withTuiSpinner(
587
+ `adding ${names.join(", ")}…`,
588
+ () => runAddCommand({
589
+ allowUnsafeSource: options.allowUnsafeSource,
590
+ cwd: projectDirectory,
591
+ from: options.registryFrom,
592
+ logger: applyLogger,
593
+ names: [...names],
594
+ ref: options.ref,
595
+ source: options.registrySource,
596
+ yes: true
597
+ })
598
+ );
547
599
  return result.code === 0;
548
600
  };
601
+ await tuiIntro("let's finish setting up your app");
549
602
  await offerRegistryExtras({
550
603
  apply,
551
604
  interactive,
552
605
  logger: options.logger,
553
- multiSelect: options.prompt?.multiSelect ?? ((message, choices, settings) => promptMultiSelect(message, choices, settings)),
554
- select: options.prompt?.select ?? ((message, choices, settings) => promptSelect(message, choices, settings))
606
+ multiSelect: options.prompt?.multiSelect ?? ((message, choices, settings) => tuiMultiSelect(message, choices, settings)),
607
+ select: options.prompt?.select ?? ((message, choices, settings) => tuiSelect(message, choices, settings))
555
608
  });
609
+ await tuiOutro("you're all set — run `pnpm dev` to start.");
556
610
  };
557
611
  const scaffoldNewProject = async (options, cwd) => {
558
612
  const name = options.name ?? "lunora-app";
559
- const templateType = options.templateType ?? "vite";
613
+ const templateType = options.templateType ?? "vite-react";
560
614
  if (templateType === "next") {
561
- options.logger.warn('template "next" is not yet available — re-run with `-t vite` or `-t standalone`.');
615
+ options.logger.warn('template "next" is not yet available — re-run with `-t vite-react` or `-t standalone`.');
562
616
  return { code: 1, files: [], target: "" };
563
617
  }
564
618
  if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
@@ -595,7 +649,7 @@ const runInitCommand = async (options) => {
595
649
  }
596
650
  return result;
597
651
  };
598
- const isTemplate = (value) => value === "astro" || value === "next" || value === "nuxt" || value === "standalone" || value === "sveltekit" || value === "tanstack-start-react" || value === "tanstack-start-solid" || value === "vite";
652
+ const isTemplate = (value) => value === "astro" || value === "next" || value === "nuxt" || value === "standalone" || value === "sveltekit" || value === "tanstack-start-react" || value === "tanstack-start-solid" || value === "vite-react";
599
653
  const resolveCiProvider = (raw, logger) => {
600
654
  if (raw === void 0) {
601
655
  return void 0;
@@ -607,8 +661,8 @@ const resolveCiProvider = (raw, logger) => {
607
661
  return void 0;
608
662
  };
609
663
  const execute = defineHandler(({ argument, cwd, logger, options }) => {
610
- const templateRaw = options.template ?? "vite";
611
- const template = isTemplate(templateRaw) ? templateRaw : "vite";
664
+ const templateRaw = options.template ?? "vite-react";
665
+ const template = isTemplate(templateRaw) ? templateRaw : "vite-react";
612
666
  return runInitCommand({
613
667
  allowUnsafeSource: options.allowUnsafeSource === true,
614
668
  cwd,
@@ -1,7 +1,7 @@
1
1
  import { existsSync, rmSync } from 'node:fs';
2
- import { promptYesNo } from '@lunora/config';
3
2
  import { join } from '@visulima/path';
4
3
  import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
4
+ import { a as tuiConfirm } from '../packem_shared/tui-prompts-Bm15GPJA.mjs';
5
5
 
6
6
  const runResetCommand = async (options) => {
7
7
  const cwd = options.cwd ?? process.cwd();
@@ -15,8 +15,8 @@ const runResetCommand = async (options) => {
15
15
  options.logger.error("reset: stdin is not a TTY — re-run with --yes to confirm deleting .wrangler/state");
16
16
  return { code: 1, removed: [] };
17
17
  }
18
- const confirmer = options.confirm ?? promptYesNo;
19
- const confirmed = await confirmer("This will delete .wrangler/state. Continue? [y/N] ");
18
+ const confirmer = options.confirm ?? tuiConfirm;
19
+ const confirmed = await confirmer("This will delete .wrangler/state. Continue?");
20
20
  if (!confirmed) {
21
21
  options.logger.info("reset: aborted");
22
22
  return { code: 1, removed: [] };
@@ -361,8 +361,8 @@ const initCommand = {
361
361
  options: [
362
362
  {
363
363
  alias: "t",
364
- defaultValue: "vite",
365
- description: "Template to scaffold (vite | standalone | astro | nuxt | sveltekit | tanstack-start-react | tanstack-start-solid)",
364
+ defaultValue: "vite-react",
365
+ description: "Template to scaffold (vite-react | standalone | astro | nuxt | sveltekit | tanstack-start-react | tanstack-start-solid)",
366
366
  name: "template",
367
367
  type: String
368
368
  },
@@ -1,13 +1,14 @@
1
1
  import { existsSync, readFileSync, writeFileSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
2
2
  import { dirname, join } from '@visulima/path';
3
- import { DEV_VARS_FILE, parseDevVariableEntries, promptYesNo } from '@lunora/config';
3
+ import { DEV_VARS_FILE, parseDevVariableEntries } from '@lunora/config';
4
4
  import { modify, applyEdits, parse } from 'jsonc-parser';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { a as tuiConfirm } from './tui-prompts-Bm15GPJA.mjs';
5
7
  import { collectCatalog, buildRegistryIndex } from './buildRegistryIndex-BcYe607_.mjs';
6
8
  import { insertSchemaExtension } from './insertSchemaExtension-BuzF6-t2.mjs';
7
9
  import { createHash } from 'node:crypto';
8
10
  import { tmpdir } from 'node:os';
9
11
  import { downloadTemplate } from 'giget';
10
- import { fileURLToPath } from 'node:url';
11
12
  import parseManifest from './parseManifest--vZf2FY1.mjs';
12
13
 
13
14
  const DEFAULT_SOURCE_REF_FALLBACK = "alpha";
@@ -59,8 +60,38 @@ const resolveSourceRef = (ref) => {
59
60
  }
60
61
  return resolveVersionRef(resolveCliVersion());
61
62
  };
63
+ const STABLE_DIST_TAG = "latest";
64
+ const resolveDistTag = () => {
65
+ const ref = resolveVersionRef(resolveCliVersion());
66
+ return ref === STABLE_BRANCH ? STABLE_DIST_TAG : ref;
67
+ };
62
68
 
63
- const applyDeps = (deps, projectRoot, logger, section = "dependencies") => {
69
+ const resolveDepRange = (range) => {
70
+ if (!range.startsWith("workspace:")) {
71
+ return range;
72
+ }
73
+ const rest = range.slice("workspace:".length);
74
+ if (rest === "" || rest === "*" || rest === "^" || rest === "~") {
75
+ return resolveDistTag();
76
+ }
77
+ return rest;
78
+ };
79
+ const UMBRELLA_REEXPORTED_DEPS = /* @__PURE__ */ new Set(["@lunora/client", "@lunora/do", "@lunora/runtime", "@lunora/server", "@lunora/values"]);
80
+ const UMBRELLA_IMPORT_RE = /(['"])@lunora\/(client|do|runtime|server|values)(\/[^'"]*)?\1/gu;
81
+ const projectUsesUmbrella = (projectRoot) => {
82
+ const packageJsonPath = join(projectRoot, "package.json");
83
+ if (!existsSync(packageJsonPath)) {
84
+ return false;
85
+ }
86
+ try {
87
+ const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
88
+ return parsed.dependencies?.lunorash !== void 0 || parsed.devDependencies?.lunorash !== void 0;
89
+ } catch {
90
+ return false;
91
+ }
92
+ };
93
+ const rewriteUmbrellaImports = (source) => source.replaceAll(UMBRELLA_IMPORT_RE, (_match, quote, base, subpath) => `${quote}lunorash/${base}${subpath ?? ""}${quote}`);
94
+ const applyDeps = (deps, projectRoot, logger, section = "dependencies", useUmbrella = false) => {
64
95
  const entries = Object.entries(deps);
65
96
  if (entries.length === 0) {
66
97
  return [];
@@ -74,11 +105,15 @@ const applyDeps = (deps, projectRoot, logger, section = "dependencies") => {
74
105
  const parsed = JSON.parse(text);
75
106
  const added = [];
76
107
  for (const [name, range] of entries) {
108
+ if (useUmbrella && UMBRELLA_REEXPORTED_DEPS.has(name)) {
109
+ logger.info(`dep provided by the lunorash umbrella, skipping: ${name}`);
110
+ continue;
111
+ }
77
112
  if (parsed.dependencies?.[name] !== void 0 || parsed.devDependencies?.[name] !== void 0) {
78
113
  logger.info(`dep already present: ${name}`);
79
114
  continue;
80
115
  }
81
- const edits = modify(text, [section, name], range, {
116
+ const edits = modify(text, [section, name], resolveDepRange(range), {
82
117
  formattingOptions: { insertSpaces: true, tabSize: 4 }
83
118
  });
84
119
  text = applyEdits(text, edits);
@@ -199,14 +234,14 @@ const applyBindings = (bindings, projectRoot, logger) => {
199
234
  }
200
235
  return applied;
201
236
  };
202
- const applyItemResources = (manifest, cwd, logger) => {
237
+ const applyItemResources = (manifest, cwd, logger, useUmbrella = false) => {
203
238
  const deps = [];
204
239
  const bindings = [];
205
240
  if (manifest.deps) {
206
- deps.push(...applyDeps(manifest.deps, cwd, logger));
241
+ deps.push(...applyDeps(manifest.deps, cwd, logger, "dependencies", useUmbrella));
207
242
  }
208
243
  if (manifest.devDependencies) {
209
- deps.push(...applyDeps(manifest.devDependencies, cwd, logger, "devDependencies"));
244
+ deps.push(...applyDeps(manifest.devDependencies, cwd, logger, "devDependencies", useUmbrella));
210
245
  }
211
246
  if (manifest.bindings) {
212
247
  bindings.push(...applyBindings(manifest.bindings, cwd, logger));
@@ -238,8 +273,8 @@ const confirmDepMutation = async (items, options) => {
238
273
  options.logger.error(`add: stdin is not a TTY and the requested items ${reasonText} — re-run with --yes to confirm`);
239
274
  return false;
240
275
  }
241
- const confirmer = options.confirm ?? promptYesNo;
242
- const confirmed = await confirmer(`The requested items ${reasonText}. Continue? [y/N] `);
276
+ const confirmer = options.confirm ?? tuiConfirm;
277
+ const confirmed = await confirmer(`The requested items ${reasonText}. Continue?`);
243
278
  if (!confirmed) {
244
279
  options.logger.info("add: aborted");
245
280
  }
@@ -318,7 +353,12 @@ const renderDiff = (oldText, newText) => {
318
353
  return out;
319
354
  };
320
355
 
321
- const reconcileSchemaExtension = (file, itemKey, itemDirectory, projectRoot, logger, diff) => {
356
+ const CODE_FILE_RE = /\.[cm]?[jt]sx?$/u;
357
+ const readItemFile = (itemDirectory, file, useUmbrella) => {
358
+ const source = readFileSync(join(itemDirectory, file.from), "utf8");
359
+ return useUmbrella && CODE_FILE_RE.test(file.to) ? rewriteUmbrellaImports(source) : source;
360
+ };
361
+ const reconcileSchemaExtension = (file, itemKey, itemDirectory, projectRoot, logger, diff, useUmbrella) => {
322
362
  const schemaPath = join(projectRoot, "lunora", "schema.ts");
323
363
  if (diff) {
324
364
  logger.info(`~ would merge .extend(${itemKey}.extension) into lunora/schema.ts (and create ${file.to} if absent)`);
@@ -327,9 +367,13 @@ const reconcileSchemaExtension = (file, itemKey, itemDirectory, projectRoot, log
327
367
  const destinationPath = join(projectRoot, file.to);
328
368
  if (!existsSync(destinationPath)) {
329
369
  mkdirSync(dirname(destinationPath), { recursive: true });
330
- writeFileSync(destinationPath, readFileSync(join(itemDirectory, file.from), "utf8"), "utf8");
370
+ writeFileSync(destinationPath, readItemFile(itemDirectory, file, useUmbrella), "utf8");
331
371
  }
332
- const existingSchema = existsSync(schemaPath) ? readFileSync(schemaPath, "utf8") : 'import { defineSchema } from "@lunora/server";\n\nexport const schema = defineSchema({});\n';
372
+ const baseModule = useUmbrella ? "lunorash/server" : "@lunora/server";
373
+ const existingSchema = existsSync(schemaPath) ? readFileSync(schemaPath, "utf8") : `import { defineSchema } from "${baseModule}";
374
+
375
+ export const schema = defineSchema({});
376
+ `;
333
377
  const result = insertSchemaExtension(existingSchema, itemKey);
334
378
  if (result.ok) {
335
379
  mkdirSync(dirname(schemaPath), { recursive: true });
@@ -359,9 +403,9 @@ const previewWholeFile = (file, current, incoming, exists, logger) => {
359
403
  logger.info(` ${line}`);
360
404
  }
361
405
  };
362
- const reconcileWholeFile = (file, itemKey, itemDirectory, projectRoot, logger, lock, reconcileOptions) => {
406
+ const reconcileWholeFile = (file, itemKey, itemDirectory, projectRoot, logger, lock, reconcileOptions, useUmbrella) => {
363
407
  const destinationPath = join(projectRoot, file.to);
364
- const incoming = readFileSync(join(itemDirectory, file.from), "utf8");
408
+ const incoming = readItemFile(itemDirectory, file, useUmbrella);
365
409
  const exists = existsSync(destinationPath);
366
410
  const current = exists ? readFileSync(destinationPath, "utf8") : "";
367
411
  const write = (message) => {
@@ -399,11 +443,11 @@ const reconcileWholeFile = (file, itemKey, itemDirectory, projectRoot, logger, l
399
443
  logger.warn(`conflict: ${file.to} has local edits and an upstream update — wrote ${file.to}.new (use --overwrite to take theirs)`);
400
444
  return { kind: "skipped", path: destinationPath };
401
445
  };
402
- const reconcileFile = (file, itemKey, itemDirectory, projectRoot, logger, lock, reconcileOptions = {}) => {
446
+ const reconcileFile = (file, itemKey, itemDirectory, projectRoot, logger, lock, reconcileOptions = {}, useUmbrella = false) => {
403
447
  if (file.merge === "schema-extension") {
404
- return reconcileSchemaExtension(file, itemKey, itemDirectory, projectRoot, logger, reconcileOptions.diff === true);
448
+ return reconcileSchemaExtension(file, itemKey, itemDirectory, projectRoot, logger, reconcileOptions.diff === true, useUmbrella);
405
449
  }
406
- return reconcileWholeFile(file, itemKey, itemDirectory, projectRoot, logger, lock, reconcileOptions);
450
+ return reconcileWholeFile(file, itemKey, itemDirectory, projectRoot, logger, lock, reconcileOptions, useUmbrella);
407
451
  };
408
452
  const reconcileItems = (items, cwd, logger, reconcileOptions = {}) => {
409
453
  const written = [];
@@ -411,15 +455,16 @@ const reconcileItems = (items, cwd, logger, reconcileOptions = {}) => {
411
455
  const depsAdded = [];
412
456
  const bindingsApplied = [];
413
457
  const lock = readLock(cwd);
458
+ const useUmbrella = projectUsesUmbrella(cwd);
414
459
  for (const { directory, manifest } of items) {
415
460
  for (const file of manifest.files) {
416
- const outcome = reconcileFile(file, manifest.name, directory, cwd, logger, lock, reconcileOptions);
461
+ const outcome = reconcileFile(file, manifest.name, directory, cwd, logger, lock, reconcileOptions, useUmbrella);
417
462
  (outcome.kind === "written" ? written : skipped).push(outcome.path);
418
463
  }
419
464
  if (reconcileOptions.diff) {
420
465
  continue;
421
466
  }
422
- const applied = applyItemResources(manifest, cwd, logger);
467
+ const applied = applyItemResources(manifest, cwd, logger, useUmbrella);
423
468
  depsAdded.push(...applied.deps);
424
469
  bindingsApplied.push(...applied.bindings);
425
470
  }
@@ -740,4 +785,4 @@ const runBuildIndexCommand = async (options) => {
740
785
  return empty;
741
786
  };
742
787
 
743
- export { runBuildIndexCommand as a, runRegistryViewCommand as b, resolveSourceRef as c, runListCommand as d, runAddCommand as r };
788
+ export { runBuildIndexCommand as a, runRegistryViewCommand as b, resolveSourceRef as c, resolveDistTag as d, runListCommand as e, runAddCommand as r };
@@ -0,0 +1,4 @@
1
+ import 'node:fs';
2
+ import '@visulima/path';
3
+ export { r as runAddCommand, a as runBuildIndexCommand, e as runListCommand, b as runRegistryViewCommand } from './commands-B9nASOYd.mjs';
4
+ import './buildRegistryIndex-BcYe607_.mjs';
@@ -0,0 +1,229 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { isInteractive } from '@lunora/config';
3
+ import { render } from '@visulima/tui';
4
+ import { Box } from '@visulima/tui/components/box';
5
+ import { CommandPalette } from '@visulima/tui/components/command-palette';
6
+ import { ConfirmInput } from '@visulima/tui/components/confirm-input';
7
+ import { MultiSelect } from '@visulima/tui/components/multi-select';
8
+ import { SelectInput } from '@visulima/tui/components/select-input';
9
+ import { Spinner } from '@visulima/tui/components/spinner';
10
+ import { Text } from '@visulima/tui/components/text';
11
+ import { useApp } from '@visulima/tui/hooks/use-app';
12
+ import { useInput } from '@visulima/tui/hooks/use-input';
13
+ import { useEffect } from 'react';
14
+
15
+ const ACCENT = "#a855f7";
16
+ const SCROLL_LIMIT = 10;
17
+ const FILTER_THRESHOLD = 8;
18
+ const PromptFrame = ({ children }) => jsx(Box, { borderColor: ACCENT, borderStyle: "round", flexDirection: "column", paddingX: 1, children });
19
+ const runInkPrompt = async (build, fallback) => {
20
+ let result = fallback;
21
+ const instance = render(
22
+ build((value) => {
23
+ result = value;
24
+ }),
25
+ { exitOnCtrlC: true }
26
+ );
27
+ try {
28
+ await instance.waitUntilExit();
29
+ } catch {
30
+ } finally {
31
+ instance.unmount();
32
+ }
33
+ return result;
34
+ };
35
+ const itemLabel = (option) => option.description === void 0 ? option.label : `${option.label} — ${option.description}`;
36
+ const useCommit = (finish) => {
37
+ const { exit } = useApp();
38
+ return (result) => {
39
+ finish(result);
40
+ exit();
41
+ };
42
+ };
43
+ const useEscapeToExit = () => {
44
+ const { exit } = useApp();
45
+ useInput((_input, key) => {
46
+ if (key.escape) {
47
+ exit();
48
+ }
49
+ });
50
+ };
51
+ const SelectView = ({ finish, initialIndex, message, options }) => {
52
+ const commit = useCommit(finish);
53
+ useEscapeToExit();
54
+ return jsxs(PromptFrame, { children: [
55
+ jsx(Text, { bold: true, children: message }),
56
+ jsx(
57
+ SelectInput,
58
+ {
59
+ accentColor: ACCENT,
60
+ initialIndex,
61
+ items: options.map((option) => {
62
+ return { key: option.value, label: itemLabel(option), value: option.value };
63
+ }),
64
+ limit: SCROLL_LIMIT,
65
+ onSelect: (item) => {
66
+ commit(item.value);
67
+ }
68
+ }
69
+ )
70
+ ] });
71
+ };
72
+ const PaletteView = ({ finish, message, options }) => {
73
+ const { exit } = useApp();
74
+ const commit = useCommit(finish);
75
+ return jsxs(Box, { flexDirection: "column", children: [
76
+ jsx(Text, { bold: true, children: message }),
77
+ jsx(
78
+ CommandPalette,
79
+ {
80
+ accentColor: ACCENT,
81
+ autoFocus: true,
82
+ commands: options.map((option) => {
83
+ return { description: option.description, id: option.value, label: option.label };
84
+ }),
85
+ limit: SCROLL_LIMIT,
86
+ onCancel: () => {
87
+ exit();
88
+ },
89
+ onSelect: (id) => {
90
+ commit(id);
91
+ },
92
+ placeholder: "Type to filter…"
93
+ }
94
+ )
95
+ ] });
96
+ };
97
+ const tuiSelect = async (message, options, settings) => {
98
+ if (!isInteractive() || options.length === 0) {
99
+ return settings?.default;
100
+ }
101
+ if (options.length > FILTER_THRESHOLD) {
102
+ return runInkPrompt((finish) => jsx(PaletteView, { finish, message, options }), settings?.default);
103
+ }
104
+ const defaultIndex = settings?.default === void 0 ? -1 : options.findIndex((option) => option.value === settings.default);
105
+ return runInkPrompt(
106
+ (finish) => jsx(SelectView, { finish, initialIndex: defaultIndex >= 0 ? defaultIndex : void 0, message, options }),
107
+ settings?.default
108
+ );
109
+ };
110
+ const MultiSelectView = ({ defaults, finish, message, options }) => {
111
+ const commit = useCommit(finish);
112
+ useEscapeToExit();
113
+ return jsxs(PromptFrame, { children: [
114
+ jsx(Text, { bold: true, children: message }),
115
+ jsx(Text, { dimColor: true, children: "space toggles · enter confirms · esc cancels" }),
116
+ jsx(
117
+ MultiSelect,
118
+ {
119
+ accentColor: ACCENT,
120
+ defaultValue: [...defaults],
121
+ limit: SCROLL_LIMIT,
122
+ onSubmit: (values) => {
123
+ const chosen = new Set(values);
124
+ commit(options.filter((option) => chosen.has(option.value)).map((option) => option.value));
125
+ },
126
+ options: options.map((option) => {
127
+ return { key: option.value, label: itemLabel(option), value: option.value };
128
+ })
129
+ }
130
+ )
131
+ ] });
132
+ };
133
+ const tuiMultiSelect = async (message, options, settings) => {
134
+ const defaults = settings?.defaults ?? [];
135
+ if (!isInteractive() || options.length === 0) {
136
+ return [...defaults];
137
+ }
138
+ return runInkPrompt((finish) => jsx(MultiSelectView, { defaults, finish, message, options }), [...defaults]);
139
+ };
140
+ const ConfirmView = ({ defaultYes, finish, message }) => {
141
+ const commit = useCommit(finish);
142
+ return jsx(PromptFrame, { children: jsxs(Box, { children: [
143
+ jsxs(Text, { bold: true, children: [
144
+ message,
145
+ " "
146
+ ] }),
147
+ jsx(
148
+ ConfirmInput,
149
+ {
150
+ defaultChoice: defaultYes ? "confirm" : "cancel",
151
+ onCancel: () => {
152
+ commit(false);
153
+ },
154
+ onConfirm: () => {
155
+ commit(true);
156
+ }
157
+ }
158
+ )
159
+ ] }) });
160
+ };
161
+ const tuiConfirm = async (message, options) => {
162
+ const defaultYes = options?.defaultYes === true;
163
+ if (!isInteractive()) {
164
+ return defaultYes;
165
+ }
166
+ return runInkPrompt((finish) => jsx(ConfirmView, { defaultYes, finish, message }), false);
167
+ };
168
+ const createTuiConfirm = () => isInteractive() ? (message) => tuiConfirm(message, { defaultYes: true }) : () => Promise.resolve(false);
169
+ const SelfExit = ({ children }) => {
170
+ const { exit } = useApp();
171
+ useEffect(() => {
172
+ exit();
173
+ }, [exit]);
174
+ return children;
175
+ };
176
+ const printFrame = async (element) => {
177
+ const instance = render(jsx(SelfExit, { children: element }));
178
+ try {
179
+ await instance.waitUntilExit();
180
+ } catch {
181
+ } finally {
182
+ instance.unmount();
183
+ }
184
+ };
185
+ const tuiIntro = async (message) => {
186
+ if (!isInteractive()) {
187
+ return;
188
+ }
189
+ await printFrame(
190
+ jsxs(Box, { borderColor: ACCENT, borderStyle: "round", paddingX: 1, children: [
191
+ jsxs(Text, { bold: true, color: ACCENT, children: [
192
+ "◆ Lunora",
193
+ " "
194
+ ] }),
195
+ jsx(Text, { children: message })
196
+ ] })
197
+ );
198
+ };
199
+ const tuiOutro = async (message) => {
200
+ if (!isInteractive()) {
201
+ return;
202
+ }
203
+ await printFrame(
204
+ jsxs(Box, { paddingX: 1, children: [
205
+ jsx(Text, { color: "green", children: "✔ " }),
206
+ jsx(Text, { children: message })
207
+ ] })
208
+ );
209
+ };
210
+ const SpinnerView = ({ label }) => jsxs(Box, { children: [
211
+ jsx(Text, { color: ACCENT, children: jsx(Spinner, { type: "dots" }) }),
212
+ jsxs(Text, { children: [
213
+ " ",
214
+ label
215
+ ] })
216
+ ] });
217
+ const withTuiSpinner = async (label, task) => {
218
+ if (!isInteractive()) {
219
+ return task();
220
+ }
221
+ const instance = render(jsx(SpinnerView, { label }));
222
+ try {
223
+ return await task();
224
+ } finally {
225
+ instance.unmount();
226
+ }
227
+ };
228
+
229
+ export { tuiConfirm as a, tuiIntro as b, createTuiConfirm as c, tuiOutro as d, tuiMultiSelect as e, tuiSelect as t, withTuiSpinner as w };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/cli",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.5",
4
4
  "description": "The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -65,9 +65,12 @@
65
65
  "@visulima/pail": "4.0.0-alpha.22",
66
66
  "@visulima/path": "3.0.0-alpha.13",
67
67
  "@visulima/spinner": "1.0.0-alpha.4",
68
+ "@visulima/tui": "1.0.0-alpha.26",
68
69
  "giget": "3.3.0",
69
70
  "jsonc-parser": "^3.3.1",
70
71
  "magic-string": "^0.30.21",
72
+ "react": "^19.2.7",
73
+ "react-reconciler": "^0.33.0",
71
74
  "ts-morph": "^28.0.0"
72
75
  },
73
76
  "engines": {
@@ -1,4 +0,0 @@
1
- import 'node:fs';
2
- import '@visulima/path';
3
- export { r as runAddCommand, a as runBuildIndexCommand, d as runListCommand, b as runRegistryViewCommand } from './commands-DIQ3nf0C.mjs';
4
- import './buildRegistryIndex-BcYe607_.mjs';