create-nola-lang 0.1.13 → 0.1.15

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.
Files changed (54) hide show
  1. package/README.md +8 -3
  2. package/dist/add.d.ts +3 -0
  3. package/dist/add.js +2 -1
  4. package/dist/checkout.js +1 -0
  5. package/dist/{chunk-LUSVIOAN.js → chunk-UTVGG44K.js} +124 -95
  6. package/dist/examples.d.ts +4 -2
  7. package/dist/examples.js +4 -2
  8. package/dist/flow.d.ts +20 -3
  9. package/dist/flow.js +49 -17
  10. package/dist/github.d.ts +1 -1
  11. package/dist/github.js +1 -1
  12. package/dist/index.js +1 -1
  13. package/dist/launch.d.ts +2 -2
  14. package/dist/launch.js +1 -1
  15. package/dist/main.js +1 -1
  16. package/dist/node-version.d.ts +3 -2
  17. package/dist/node-version.js +4 -3
  18. package/dist/package-manager.d.ts +5 -0
  19. package/dist/package-manager.js +7 -0
  20. package/dist/providers.d.ts +1 -1
  21. package/dist/providers.js +1 -1
  22. package/dist/registry.d.ts +21 -12
  23. package/dist/registry.js +27 -5
  24. package/dist/scaffold.d.ts +36 -16
  25. package/dist/scaffold.js +96 -116
  26. package/package.json +1 -1
  27. package/skills/nola/SKILL.md +72 -17
  28. package/skills/nola/references/config.md +35 -6
  29. package/skills/nola/references/patterns.md +129 -31
  30. package/skills/nola/references/pitfalls.md +318 -56
  31. package/skills/nola/references/syntax.md +345 -121
  32. package/templates/empty/package.json +5 -5
  33. package/templates/empty/src/main.tsi +8 -0
  34. package/templates/empty/src/main.ts +0 -5
  35. package/templates/feature-extraction/README.md +0 -20
  36. package/templates/feature-extraction/nola.config.ts +0 -14
  37. package/templates/feature-extraction/nola.replay.jsonl +0 -2
  38. package/templates/feature-extraction/package.json +0 -22
  39. package/templates/feature-extraction/src/main.tsi +0 -22
  40. package/templates/feature-extraction/tsconfig.json +0 -12
  41. package/templates/function-calling/README.md +0 -21
  42. package/templates/function-calling/nola.config.ts +0 -14
  43. package/templates/function-calling/nola.replay.jsonl +0 -1
  44. package/templates/function-calling/package.json +0 -22
  45. package/templates/function-calling/src/main.tsi +0 -12
  46. package/templates/function-calling/src/tickets.ts +0 -17
  47. package/templates/function-calling/tsconfig.json +0 -12
  48. package/templates/typescript-interop/README.md +0 -15
  49. package/templates/typescript-interop/nola.config.ts +0 -14
  50. package/templates/typescript-interop/nola.replay.jsonl +0 -1
  51. package/templates/typescript-interop/package.json +0 -22
  52. package/templates/typescript-interop/src/main.ts +0 -8
  53. package/templates/typescript-interop/src/person.tsi +0 -11
  54. package/templates/typescript-interop/tsconfig.json +0 -12
package/README.md CHANGED
@@ -25,8 +25,13 @@ symlink to it for Claude Code) are written without a question; `--ide none` and
25
25
 
26
26
  You get one template per feature — `feature-extraction` (the default) is one
27
27
  `src/main.tsi` with a top-level `ask`, `function-calling` the same with a call
28
- intent over an async function in `src/tickets.ts`, `typescript-interop` is
29
- `src/person.tsi` plus a plain-TS consumer — with `nola.config.ts`, tsconfig,
30
- and a recorded replay ledger so the first run works without any API key. `npm create nola`
28
+ intent over an async function in `src/tickets.ts`, `agent-loop` a top-level
29
+ `while` around an ask, `typescript-interop` is `src/person.tsi` plus a plain-TS
30
+ consumer, `triage-ticket` the one-file shape on typesafe.ai — each with
31
+ `nola.config.ts` and a tsconfig, and the first two and `typescript-interop`
32
+ with a recorded replay ledger so the first run works without any API key.
33
+ Every template but `empty` is one of the curated examples in the Nola
34
+ repository, fetched at the release tag matching this package's version;
35
+ `empty` (config, tsconfig and a stub entry) ships inside the package. `npm create nola`
31
36
  is a short alias of this package (`create-nola`); `nola init` (from the
32
37
  `nola-lang` package) lays down the same templates.
package/dist/add.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Runtime } from "./package-manager.js";
1
2
  import { type ProviderId } from "./providers.js";
2
3
  export interface AddResult {
3
4
  /** files written, e.g. ["nola.config.ts", "package.json"] */
@@ -17,6 +18,8 @@ export interface AddOptions {
17
18
  * empty template's — the nola key step rewrites it afterwards (`applyTrial`).
18
19
  */
19
20
  provider?: ProviderId;
21
+ /** "bun" also merges @nola-lang/bun into devDependencies; the bunfig lines are suggested by the flow's outro, never written */
22
+ runtime?: Runtime;
20
23
  }
21
24
  /**
22
25
  * Retrofit an existing npm project: write nola.config.ts (unless present) and
package/dist/add.js CHANGED
@@ -61,7 +61,8 @@ export async function addNola(targetDir, opts = {}) {
61
61
  await writeFile(configPath, await readFile(vendor ? providerConfigUrl(vendor.id) : EMPTY_CONFIG_URL, "utf8"));
62
62
  wrote.push("nola.config.ts");
63
63
  }
64
- for (const [section, name, fixedRange] of REQUIRED) {
64
+ const required = opts.runtime === "bun" ? [...REQUIRED, ["devDependencies", "@nola-lang/bun", null]] : REQUIRED;
65
+ for (const [section, name, fixedRange] of required) {
65
66
  const existing = existingRange(name);
66
67
  if (existing !== undefined) {
67
68
  skipped.push(`${name} already present (${existing}) — left untouched`);
package/dist/checkout.js CHANGED
@@ -28,6 +28,7 @@ export async function checkoutRoot() {
28
28
  const LINKS = [
29
29
  ["@nola-lang/runtime", "runtime"],
30
30
  ["@nola-lang/providers", "providers"],
31
+ ["@nola-lang/bun", "bun"],
31
32
  ["nola-lang", "nola-lang"],
32
33
  ];
33
34
  /**
@@ -16,7 +16,7 @@ var PROVIDERS = [
16
16
  },
17
17
  { id: "google", label: "Gemini", hint: 'google("gemini-2.5-flash"), reads GEMINI_API_KEY', envVar: "GEMINI_API_KEY", model: 'google("gemini-2.5-flash")' },
18
18
  { id: "typesafe", label: "typesafe.ai", hint: "typesafe(), reads TYPESAFE_API_KEY", envVar: "TYPESAFE_API_KEY", model: "typesafe()" },
19
- { id: "none", label: "Skip for now", hint: "the builtin templates run offline from a replay ledger; pick a model in nola.config.ts later" }
19
+ { id: "none", label: "Skip for now", hint: "keeps the template's own config (the recorded ones run offline); pick a model in nola.config.ts later" }
20
20
  ];
21
21
  function providerById(id2) {
22
22
  return PROVIDERS.find((p) => p.id === id2);
@@ -52,6 +52,7 @@ async function checkoutRoot() {
52
52
  var LINKS = [
53
53
  ["@nola-lang/runtime", "runtime"],
54
54
  ["@nola-lang/providers", "providers"],
55
+ ["@nola-lang/bun", "bun"],
55
56
  ["nola-lang", "nola-lang"]
56
57
  ];
57
58
  var LINK_CHECKOUT_ENV = "NOLA_LINK_CHECKOUT";
@@ -170,17 +171,39 @@ async function fetchExampleFromGitHub(name, version, fetchImpl = fetch) {
170
171
  // src/registry.ts
171
172
  var DEFAULT_ENTRY = "src/main.ts";
172
173
  var TEMPLATES = [
173
- { name: "feature-extraction", label: "extract typed data from context", source: "builtin", entry: "src/main.tsi" },
174
+ {
175
+ name: "feature-extraction",
176
+ label: "extract typed data from context",
177
+ source: "example",
178
+ featured: true,
179
+ entry: "src/main.tsi"
180
+ },
174
181
  {
175
182
  name: "function-calling",
176
183
  label: "call an async function from a TypeScript file",
177
- source: "builtin",
184
+ source: "example",
185
+ featured: true,
186
+ entry: "src/main.tsi"
187
+ },
188
+ {
189
+ name: "tool-use",
190
+ label: "offer functions as tools: the model calls them as it needs before it answers",
191
+ source: "example",
192
+ featured: true,
193
+ entry: "src/main.tsi"
194
+ },
195
+ {
196
+ name: "agent-loop",
197
+ label: "an agent loop in one .tsi file: a top-level while with an ask, and a context statement read on every pass",
198
+ source: "example",
199
+ featured: true,
178
200
  entry: "src/main.tsi"
179
201
  },
180
202
  {
181
203
  name: "typescript-interop",
182
204
  label: "an infer function example, imported and awaited from plain TypeScript",
183
- source: "builtin"
205
+ source: "example",
206
+ featured: true
184
207
  },
185
208
  {
186
209
  name: "triage-ticket",
@@ -190,7 +213,7 @@ var TEMPLATES = [
190
213
  entry: "src/main.tsi",
191
214
  provider: { label: "typesafe.ai", envVar: "TYPESAFE_API_KEY" }
192
215
  },
193
- { name: "empty", label: "nola.config + tsconfig only, bring your own code", source: "builtin" },
216
+ { name: "empty", label: "nola.config + tsconfig only, bring your own code", source: "builtin", entry: "src/main.tsi" },
194
217
  { name: "file-ticket", label: "call intents: the model fills a function's arguments", source: "example" },
195
218
  { name: "extract-resume", label: "nested arrays of objects, JSDoc schema descriptions", source: "example" },
196
219
  { name: "extract-invoice", label: "same-file type references, optional fields", source: "example" },
@@ -231,19 +254,34 @@ function envExample(envVar) {
231
254
  ${envVar}=
232
255
  `;
233
256
  }
234
- var RENAMES = { _gitignore: ".gitignore" };
257
+ var BUNFIG_TOML = 'preload = ["@nola-lang/bun/preload"]\n\n[test]\npreload = ["@nola-lang/bun/preload"]\n';
258
+ var BUN_ENGINE = ">=1.3.0";
259
+ function applyBunRuntime(content, opts) {
260
+ const pkg = JSON.parse(content);
261
+ const scripts = pkg.scripts ?? {};
262
+ scripts.start = `bun ${opts.entry}`;
263
+ pkg.scripts = scripts;
264
+ const dev = pkg.devDependencies ?? {};
265
+ dev["@nola-lang/bun"] = `^${opts.version}`;
266
+ pkg.devDependencies = dev;
267
+ const engines = pkg.engines ?? {};
268
+ engines.bun = BUN_ENGINE;
269
+ pkg.engines = engines;
270
+ return `${JSON.stringify(pkg, null, 2)}
271
+ `;
272
+ }
235
273
  var GITIGNORE_URL = new URL("../templates/_gitignore", import.meta.url);
236
274
  async function withRecommendedGitignore(files) {
237
275
  if (!files.has(".gitignore")) files.set(".gitignore", await readFile3(GITIGNORE_URL, "utf8"));
238
276
  return files;
239
277
  }
240
- var SUBSTITUTED = /* @__PURE__ */ new Set(["package.json", "README.md", "main.ts", "main.tsi"]);
241
- function nextStepsComment(ide, template) {
278
+ function nextStepsComment(ide, template, runtime = "node") {
242
279
  if (ide === "vscode") {
243
- const breakpointIn = templateByName(template)?.entry ? "on the `ask` line below" : template === "typescript-interop" ? "in src/person.tsi" : "in your .tsi file";
280
+ const breakpointIn = template === "empty" ? "on your first `ask` in this file" : templateByName(template)?.entry ? "on the `ask` line below" : template === "typescript-interop" ? "in src/person.tsi" : "in your .tsi file";
281
+ const runLine = runtime === "bun" ? "// 1. Run this file: bun start (F5 runs it too, under Node's loader \u2014 .vscode/launch.json is already set up)." : "// 1. Press F5 to run this file (.vscode/launch.json is already set up).";
244
282
  return [
245
283
  "// Next steps in VS Code:",
246
- "// 1. Press F5 to run this file (.vscode/launch.json is already set up).",
284
+ runLine,
247
285
  `// 2. Set a breakpoint ${breakpointIn} and press F5 again to step through the ask.`,
248
286
  '// 3. Install the recommended "Nola" extension when VS Code offers it \u2014 IntelliSense,',
249
287
  "// go to definition and diagnostics inside .tsi files."
@@ -251,33 +289,11 @@ function nextStepsComment(ide, template) {
251
289
  }
252
290
  return [
253
291
  "// Next steps:",
254
- "// 1. Run this file: npm start",
292
+ `// 1. Run this file: ${runtime === "bun" ? "bun start" : "npm start"}`,
255
293
  "// 2. Set up your editor (VS Code extension, debugging): https://nola.sh/docs/start/editor-setup/"
256
294
  ].join("\n");
257
295
  }
258
- var OFFLINE_ONLY = /* @__PURE__ */ new Set(["nola.replay.jsonl"]);
259
- var README_NOTES = {
260
- offline: {
261
- START_NOTE: "works offline, no API key needed",
262
- PROVIDER_NOTE: 'This project runs offline: `nola.config.ts` replays answers from the committed\n`nola.replay.jsonl` ledger. The ledger is keyed by the exact prompt, so once\nyou edit the `.tsi` file or add your own asks, switch the config to a real\nmodel (see the comment in `nola.config.ts`): `model: "nola"` with a key from\n`npx nola-lang key` (25 free runs), or your own provider and its key.'
263
- },
264
- nola: {
265
- START_NOTE: "uses your 25 free Nola runs (key in .env)",
266
- PROVIDER_NOTE: '`nola.config.ts` sets `model: "nola"` \u2014 platform-served inference with the trial key the\nscaffold wrote to `.env` (git-ignored). Out of runs? `npx nola-lang account` opens\nyour Nola account, where prepaid balance is added, or bring your\nown model (see the comment in `nola.config.ts`).'
267
- }
268
- };
269
- function readmeNotes(provider) {
270
- if (provider === "none") return README_NOTES.offline;
271
- if (provider === "nola") return README_NOTES.nola;
272
- const def = providerById(provider);
273
- if (!def?.envVar || !def.model) throw new Error(`provider "${provider}" has no vendor config`);
274
- return {
275
- START_NOTE: `set ${def.envVar} in .env first`,
276
- PROVIDER_NOTE: `\`nola.config.ts\` sets \`model: ${def.model}\` \u2014 ${def.label} serves inference with the key it reads
277
- from \`${def.envVar}\`. Copy \`.env.example\` to \`.env\` (git-ignored; \`nola run\` applies it) and fill in
278
- \`${def.envVar}\` before the first \`npm start\`, or switch models in \`nola.config.ts\`.`
279
- };
280
- }
296
+ var OFFLINE_ONLY = ["nola.replay.jsonl"];
281
297
  async function ownVersion() {
282
298
  const pkg = JSON.parse(await readFile3(new URL("../package.json", import.meta.url), "utf8"));
283
299
  return pkg.version;
@@ -289,67 +305,48 @@ async function prepareTarget(absRoot, force) {
289
305
  if (!force) throw new Error(`target directory ${absRoot} is not empty`);
290
306
  for (const entry of entries) await rm2(join3(absRoot, entry), { recursive: true, force: true });
291
307
  }
308
+ async function collectTemplate(def, version) {
309
+ if (def.source === "builtin") return collectExampleFromDisk(TEMPLATES_DIR, def.name);
310
+ const dev = await devExamplesDir();
311
+ return dev ? collectExampleFromDisk(dev, def.name) : fetchExampleFromGitHub(def.name, version);
312
+ }
292
313
  async function scaffold(targetDir, opts = {}) {
293
314
  const root = targetDir;
294
315
  const absRoot = resolve(targetDir);
295
316
  const template = opts.template ?? "feature-extraction";
296
317
  const def = templateByName(template);
297
318
  if (!def) throw new Error(`unknown template "${template}" (valid: ${templateNames().join(", ")})`);
319
+ const version = await ownVersion();
320
+ const files = await collectTemplate(def, version);
298
321
  await prepareTarget(absRoot, opts.force ?? false);
299
322
  const name = opts.name ?? basename(absRoot);
300
- const version = await ownVersion();
301
- const envVar = vendorEnvVar(template, opts.provider ?? "none");
302
- if (def.source === "example") {
303
- const dev = await devExamplesDir();
304
- const exampleFiles = await withRecommendedGitignore(
305
- dev ? await collectExampleFromDisk(dev, template) : await fetchExampleFromGitHub(template, version)
306
- );
307
- const manifest = exampleFiles.get("package.json");
308
- if (manifest) exampleFiles.set("package.json", rewriteExamplePackageJson(manifest, { name, version }));
309
- if (envVar !== void 0 && !exampleFiles.has(".env.example")) exampleFiles.set(".env.example", envExample(envVar));
310
- for (const [relPath, content] of exampleFiles) {
311
- const target = join3(absRoot, relPath);
312
- await mkdir2(join3(target, ".."), { recursive: true });
313
- await writeFile2(target, content);
314
- }
315
- return { root, files: [...exampleFiles.keys()].sort() };
316
- }
317
- const templateRoot = join3(TEMPLATES_DIR, template);
318
- const versionRange = `^${version}`;
319
323
  const provider = opts.provider ?? "none";
320
- const notes = readmeNotes(provider);
321
- const nextSteps2 = nextStepsComment(opts.ide ?? "none", template);
322
- const files = [];
323
- const copyDir = async (fromDir, relDir) => {
324
- await mkdir2(join3(absRoot, relDir), { recursive: true });
325
- for (const entry of await readdir2(join3(templateRoot, fromDir), { withFileTypes: true })) {
326
- const fromRel = join3(fromDir, entry.name);
327
- if (entry.isDirectory()) {
328
- await copyDir(fromRel, join3(relDir, entry.name));
329
- continue;
330
- }
331
- const toName = RENAMES[entry.name] ?? entry.name;
332
- if (provider !== "none" && OFFLINE_ONLY.has(entry.name)) continue;
333
- const toRel = join3(relDir, toName);
334
- const source = provider !== "none" && relDir === "." && entry.name === "nola.config.ts" ? providerConfigUrl(provider) : join3(templateRoot, fromRel);
335
- let content = await readFile3(source, "utf8");
336
- if (SUBSTITUTED.has(entry.name)) {
337
- content = content.replaceAll("__NAME__", name).replaceAll("__VERSION__", versionRange).replaceAll("__START_NOTE__", notes.START_NOTE).replaceAll("__PROVIDER_NOTE__", notes.PROVIDER_NOTE).replaceAll("__NEXT_STEPS__", nextSteps2);
338
- }
339
- await writeFile2(join3(absRoot, toRel), content);
340
- files.push(toRel.replaceAll("\\", "/"));
341
- }
342
- };
343
- await copyDir(".", ".");
344
- if (!files.includes(".gitignore")) {
345
- await writeFile2(join3(absRoot, ".gitignore"), await readFile3(GITIGNORE_URL, "utf8"));
346
- files.push(".gitignore");
347
- }
348
- if (envVar !== void 0) {
349
- await writeFile2(join3(absRoot, ".env.example"), envExample(envVar));
350
- files.push(".env.example");
324
+ const runtime = opts.runtime ?? "node";
325
+ const entry = entryFile(template);
326
+ if (provider !== "none") {
327
+ for (const offline of OFFLINE_ONLY) files.delete(offline);
328
+ files.set("nola.config.ts", await readFile3(providerConfigUrl(provider), "utf8"));
329
+ }
330
+ const manifest = files.get("package.json");
331
+ if (manifest) files.set("package.json", rewriteExamplePackageJson(manifest, { name, version }));
332
+ if (runtime === "bun") {
333
+ files.set("bunfig.toml", BUNFIG_TOML);
334
+ const current = files.get("package.json");
335
+ if (current !== void 0) files.set("package.json", applyBunRuntime(current, { entry, version }));
336
+ }
337
+ await withRecommendedGitignore(files);
338
+ const envVar = vendorEnvVar(template, provider);
339
+ if (envVar !== void 0 && !files.has(".env.example")) files.set(".env.example", envExample(envVar));
340
+ const program = files.get(entry);
341
+ if (program !== void 0) files.set(entry, `${nextStepsComment(opts.ide ?? "none", template, runtime)}
342
+
343
+ ${program}`);
344
+ for (const [relPath, content] of files) {
345
+ const target = join3(absRoot, relPath);
346
+ await mkdir2(join3(target, ".."), { recursive: true });
347
+ await writeFile2(target, content);
351
348
  }
352
- return { root, files: files.sort() };
349
+ return { root, files: [...files.keys()].sort() };
353
350
  }
354
351
 
355
352
  // src/add.ts
@@ -402,7 +399,8 @@ async function addNola(targetDir, opts = {}) {
402
399
  await writeFile3(configPath, await readFile4(vendor ? providerConfigUrl(vendor.id) : EMPTY_CONFIG_URL, "utf8"));
403
400
  wrote.push("nola.config.ts");
404
401
  }
405
- for (const [section, name, fixedRange] of REQUIRED) {
402
+ const required = opts.runtime === "bun" ? [...REQUIRED, ["devDependencies", "@nola-lang/bun", null]] : REQUIRED;
403
+ for (const [section, name, fixedRange] of required) {
406
404
  const existing = existingRange(name);
407
405
  if (existing !== void 0) {
408
406
  skipped.push(`${name} already present (${existing}) \u2014 left untouched`);
@@ -1571,7 +1569,7 @@ function nodeVersionWarning(version = process.versions.node) {
1571
1569
  if (nodeSupportsTypeStripping(version)) return void 0;
1572
1570
  const lines = [
1573
1571
  `Node ${version} is older than Nola needs: Node >= ${NODE_FLOOR}, the first release that runs .ts files natively.`,
1574
- "On this Node, `npm start` and the VS Code F5 launch fail with ERR_UNKNOWN_FILE_EXTENSION for src/main.ts."
1572
+ "On this Node, `npm start` and the VS Code F5 launch fail with ERR_UNKNOWN_FILE_EXTENSION on the first plain .ts file they load."
1575
1573
  ];
1576
1574
  if (hasStripTypesFlag(version)) {
1577
1575
  lines.push(
@@ -1592,6 +1590,12 @@ function detectPackageManager(userAgent = process.env.npm_config_user_agent) {
1592
1590
  function packageManagerCommands(pm) {
1593
1591
  return { install: `${pm} install`, start: `${pm} start`, run: `${pm} run` };
1594
1592
  }
1593
+ function isRuntime(value) {
1594
+ return value === "node" || value === "bun";
1595
+ }
1596
+ function defaultRuntime(pm) {
1597
+ return pm === "bun" ? "bun" : "node";
1598
+ }
1595
1599
 
1596
1600
  // src/flow.ts
1597
1601
  var DEFAULT_DIR = "nola-app";
@@ -1614,15 +1618,15 @@ var TEMPLATE_QUESTION = "Select a template:";
1614
1618
  var EXAMPLE_QUESTION = "Select an example:";
1615
1619
  var MORE_EXAMPLES = "more-examples";
1616
1620
  var BACK = "back";
1617
- var builtinNames = () => TEMPLATES.filter((t) => t.source === "builtin").map((t) => `"${t.name}"`).join(", ");
1618
1621
  function templateMenu() {
1619
1622
  const featured = featuredNames().map((name) => templateOption(templateByName(name)));
1620
1623
  return [...featured, { value: MORE_EXAMPLES, label: "More examples\u2026", hint: exampleNames().join(", ") }];
1621
1624
  }
1622
1625
  function exampleMenu() {
1623
1626
  const examples = exampleNames().map((name) => templateOption(templateByName(name)));
1624
- return [...examples, { value: BACK, label: "\u2190 Back", hint: "the builtin templates" }];
1627
+ return [...examples, { value: BACK, label: "\u2190 Back", hint: "the first menu" }];
1625
1628
  }
1629
+ var FETCH_FAILURE_HINT = 'Only "empty" ships inside the CLI and needs no download \u2014 every other template is fetched from GitHub.';
1626
1630
  async function selectTemplate(prompter) {
1627
1631
  for (; ; ) {
1628
1632
  const choice = await prompter.select(TEMPLATE_QUESTION, templateMenu());
@@ -1708,11 +1712,18 @@ async function resolveProvider(input, prompter, template) {
1708
1712
  return choice;
1709
1713
  }
1710
1714
  }
1715
+ function resolveRuntime(input) {
1716
+ if (input.runtime !== void 0) {
1717
+ if (!isRuntime(input.runtime)) throw new Error(`invalid --runtime "${input.runtime}" (valid: node, bun)`);
1718
+ return input.runtime;
1719
+ }
1720
+ return defaultRuntime(input.packageManager ?? "npm");
1721
+ }
1711
1722
  async function resolveExtras(input, prompter, dir, template) {
1712
1723
  const provider = await resolveProvider(input, prompter, template);
1713
1724
  if (provider === null) return null;
1714
1725
  const setup = await resolveSetup(input, prompter, dir);
1715
- return setup === null ? null : { provider, ...setup };
1726
+ return setup === null ? null : { provider, ...setup, runtime: resolveRuntime(input) };
1716
1727
  }
1717
1728
  async function resolveScaffoldOptions(input, prompter) {
1718
1729
  if (input.add && input.template) {
@@ -1723,6 +1734,9 @@ async function resolveScaffoldOptions(input, prompter) {
1723
1734
  if (input.ide !== void 0 && input.ide !== "vscode" && input.ide !== "none") {
1724
1735
  throw new Error(`invalid --ide "${input.ide}" (valid: vscode, none)`);
1725
1736
  }
1737
+ if (input.runtime !== void 0 && !isRuntime(input.runtime)) {
1738
+ throw new Error(`invalid --runtime "${input.runtime}" (valid: node, bun)`);
1739
+ }
1726
1740
  if (input.agents !== void 0) parseAgentsFlag(input.agents);
1727
1741
  flaggedProvider(input);
1728
1742
  if (input.add) {
@@ -1832,10 +1846,14 @@ function plainPrompter() {
1832
1846
  var FLOW_OPTIONS = {
1833
1847
  template: {
1834
1848
  type: "string",
1835
- description: "template to scaffold (feature-extraction, function-calling, typescript-interop, empty, or a curated example)"
1849
+ description: "template to scaffold: feature-extraction (the default), function-calling, agent-loop, typescript-interop, triage-ticket, empty, or another curated example"
1836
1850
  },
1837
1851
  add: { type: "boolean", description: "add Nola to the existing project in [dir] instead of scaffolding" },
1838
1852
  ide: { type: "string", description: "editor setup: vscode (the default) | none" },
1853
+ runtime: {
1854
+ type: "string",
1855
+ description: "runtime the project runs on: node | bun (default: bun when bun ran the scaffolder \u2014 bunfig.toml preload + `bun src/main.tsi`)"
1856
+ },
1839
1857
  agents: {
1840
1858
  type: "string",
1841
1859
  description: `agent skill files: ${AGENT_IDS.join(",")} | all | none (default: claude,universal)`
@@ -1995,6 +2013,8 @@ async function runFlow(args, opts = {}) {
1995
2013
  agents: args.agents,
1996
2014
  provider: args.provider,
1997
2015
  trial: args.trial,
2016
+ runtime: args.runtime,
2017
+ packageManager: pm,
1998
2018
  keyPath,
1999
2019
  signIn: signInNow,
2000
2020
  cwd: opts.cwd,
@@ -2008,7 +2028,7 @@ async function runFlow(args, opts = {}) {
2008
2028
  }
2009
2029
  if (outcome.kind === "add") {
2010
2030
  const grant = outcome.provider === "nola" ? await obtainTrial(prompter, opts, interactive) : null;
2011
- const result = await addNola(outcome.dir, { provider: outcome.provider });
2031
+ const result = await addNola(outcome.dir, { provider: outcome.provider, runtime: outcome.runtime });
2012
2032
  const trial = grant ? await applyTrial(outcome.dir, { apiKey: grant.apiKey, hasConfig: !result.wrote.includes("nola.config.ts") }) : { wrote: [], skipped: [] };
2013
2033
  const ide = outcome.ide === "vscode" ? await writeVscodeSetup(outcome.dir) : { wrote: [], skipped: [] };
2014
2034
  const ag = outcome.agents.length > 0 ? await writeAgentSkills(outcome.dir, outcome.agents) : { wrote: [], removed: [], skipped: [] };
@@ -2021,7 +2041,10 @@ async function runFlow(args, opts = {}) {
2021
2041
  }
2022
2042
  const lines = [
2023
2043
  packageManagerCommands(pm).install,
2024
- 'optional start script: "start": "nola run src/main.ts"',
2044
+ ...outcome.runtime === "bun" ? [
2045
+ 'bunfig.toml: preload = ["@nola-lang/bun/preload"] (runs .tsi from source under Bun)',
2046
+ 'optional start script: "start": "bun src/main.ts"'
2047
+ ] : ['optional start script: "start": "nola run src/main.ts"'],
2025
2048
  'tsconfig tip: directory-style include (e.g. ["src"]) lets the editor see .tsi files',
2026
2049
  commented(CONSOLE_COMMAND, CONSOLE_NOTE)
2027
2050
  ];
@@ -2036,7 +2059,13 @@ Next steps:
2036
2059
  try {
2037
2060
  const grant = outcome.provider === "nola" ? await obtainTrial(prompter, opts, interactive) : null;
2038
2061
  const provider = outcome.provider === "nola" ? grant ? "nola" : "none" : outcome.provider;
2039
- const { files } = await scaffold(outcome.dir, { template: outcome.template, force: outcome.force, provider, ide: outcome.ide });
2062
+ const { files } = await scaffold(outcome.dir, {
2063
+ template: outcome.template,
2064
+ force: outcome.force,
2065
+ provider,
2066
+ ide: outcome.ide,
2067
+ runtime: outcome.runtime
2068
+ });
2040
2069
  let trialWrote = [];
2041
2070
  if (grant) {
2042
2071
  const trial = await applyTrial(outcome.dir, { apiKey: grant.apiKey, hasConfig: false });
@@ -2070,7 +2099,7 @@ Next steps:
2070
2099
  } catch (err) {
2071
2100
  if (err instanceof ExampleFetchError && interactive) {
2072
2101
  prompter.note(`${err.message}
2073
- The builtin templates (${builtinNames()}) work offline.`);
2102
+ ${FETCH_FAILURE_HINT}`);
2074
2103
  input.dir = outcome.dir;
2075
2104
  input.template = void 0;
2076
2105
  continue;
@@ -3,8 +3,10 @@ export declare function devExamplesDir(): Promise<string | null>;
3
3
  /** rel-path → content for one example, from the dev checkout's examples dir. */
4
4
  export declare function collectExampleFromDisk(examplesDir: string, name: string): Promise<Map<string, string>>;
5
5
  /**
6
- * Structured rewrite for a scaffolded example's package.json — examples stay
7
- * runnable workspaces in-repo, so they carry no __NAME__ placeholders.
6
+ * Structured rewrite for a scaffolded template's package.json — examples stay
7
+ * runnable workspaces in-repo (and `empty` follows their convention), so no
8
+ * template carries __NAME__ placeholders: the name is set here and the
9
+ * workspace `*` ranges become the lockstep `^<version>`.
8
10
  */
9
11
  export declare function rewriteExamplePackageJson(content: string, opts: {
10
12
  name: string;
package/dist/examples.js CHANGED
@@ -33,8 +33,10 @@ export async function collectExampleFromDisk(examplesDir, name) {
33
33
  return files;
34
34
  }
35
35
  /**
36
- * Structured rewrite for a scaffolded example's package.json — examples stay
37
- * runnable workspaces in-repo, so they carry no __NAME__ placeholders.
36
+ * Structured rewrite for a scaffolded template's package.json — examples stay
37
+ * runnable workspaces in-repo (and `empty` follows their convention), so no
38
+ * template carries __NAME__ placeholders: the name is set here and the
39
+ * workspace `*` ranges become the lockstep `^<version>`.
38
40
  */
39
41
  export function rewriteExamplePackageJson(content, opts) {
40
42
  const pkg = JSON.parse(content);
package/dist/flow.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type AgentId } from "./agents.js";
2
2
  import { type CommandSpec } from "./cli.js";
3
3
  import { type Launcher } from "./launch.js";
4
- import { type PackageManager } from "./package-manager.js";
4
+ import { type PackageManager, type Runtime } from "./package-manager.js";
5
5
  import { type ProviderId } from "./providers.js";
6
6
  export interface PrompterOption {
7
7
  value: string;
@@ -45,6 +45,10 @@ export interface FlowInput {
45
45
  provider?: string;
46
46
  /** --trial / --no-trial: shorthand for --provider nola / --provider none */
47
47
  trial?: boolean;
48
+ /** --runtime <node|bun>; undefined = bun when bun ran the scaffolder, else node */
49
+ runtime?: string;
50
+ /** the manager that invoked us — decides the default runtime */
51
+ packageManager?: PackageManager;
48
52
  /** how the nola row is worded, decided by runFlow from ~/.nola (spec 2026-09-04-cli-sign-in-design.md §5); default trial */
49
53
  keyPath?: KeyPath;
50
54
  /**
@@ -75,12 +79,14 @@ export type FlowOutcome = {
75
79
  provider: ProviderId;
76
80
  ide: "vscode" | "none";
77
81
  agents: AgentId[];
82
+ runtime: Runtime;
78
83
  } | {
79
84
  kind: "add";
80
85
  dir: string;
81
86
  provider: ProviderId;
82
87
  ide: "vscode" | "none";
83
88
  agents: AgentId[];
89
+ runtime: Runtime;
84
90
  } | {
85
91
  kind: "cancelled";
86
92
  };
@@ -97,10 +103,16 @@ export declare const EXAMPLE_QUESTION = "Select an example:";
97
103
  export declare const MORE_EXAMPLES = "more-examples";
98
104
  /** the examples menu's last row — back to the first menu */
99
105
  export declare const BACK = "back";
100
- /** The first menu: the builtin templates and the featured example in registry order, then the examples row. */
106
+ /** The first menu: the featured examples and `empty` in registry order, then the examples row. */
101
107
  export declare function templateMenu(): PrompterOption[];
102
108
  /** The second menu: the curated examples the first menu does not carry, in registry order, then Back. */
103
109
  export declare function exampleMenu(): PrompterOption[];
110
+ /**
111
+ * The note a failed example fetch gets (interactive: the menu is asked
112
+ * again). Only `empty` ships inside the CLI; every other template is fetched
113
+ * from GitHub, so an offline machine can still scaffold that one.
114
+ */
115
+ export declare const FETCH_FAILURE_HINT = "Only \"empty\" ships inside the CLI and needs no download \u2014 every other template is fetched from GitHub.";
104
116
  export declare const PROVIDER_QUESTION = "Select an inference provider:";
105
117
  /** `nola key`'s confirmation before the browser opens (the scaffold's consent is the provider select itself). */
106
118
  export declare const SIGN_IN_QUESTION = "This machine already used its 25 free Nola runs. Sign in to get an API key for this project?";
@@ -124,6 +136,7 @@ export interface RunFlowArgs {
124
136
  agents?: string;
125
137
  provider?: string;
126
138
  trial?: boolean;
139
+ runtime?: string;
127
140
  }
128
141
  /**
129
142
  * The flow's flags, declared ONCE for both bins (`npm create nola-lang` and
@@ -132,7 +145,7 @@ export interface RunFlowArgs {
132
145
  export declare const FLOW_OPTIONS: {
133
146
  readonly template: {
134
147
  readonly type: "string";
135
- readonly description: "template to scaffold (feature-extraction, function-calling, typescript-interop, empty, or a curated example)";
148
+ readonly description: "template to scaffold: feature-extraction (the default), function-calling, agent-loop, typescript-interop, triage-ticket, empty, or another curated example";
136
149
  };
137
150
  readonly add: {
138
151
  readonly type: "boolean";
@@ -142,6 +155,10 @@ export declare const FLOW_OPTIONS: {
142
155
  readonly type: "string";
143
156
  readonly description: "editor setup: vscode (the default) | none";
144
157
  };
158
+ readonly runtime: {
159
+ readonly type: "string";
160
+ readonly description: "runtime the project runs on: node | bun (default: bun when bun ran the scaffolder — bunfig.toml preload + `bun src/main.tsi`)";
161
+ };
145
162
  readonly agents: {
146
163
  readonly type: "string";
147
164
  readonly description: `agent skill files: ${string} | all | none (default: claude,universal)`;