create-nola-lang 0.1.12 → 0.1.14

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 (49) hide show
  1. package/README.md +8 -3
  2. package/dist/{chunk-LUSVIOAN.js → chunk-CB26Z47Y.js} +53 -87
  3. package/dist/examples.d.ts +4 -2
  4. package/dist/examples.js +4 -2
  5. package/dist/flow.d.ts +8 -2
  6. package/dist/flow.js +15 -12
  7. package/dist/github.d.ts +1 -1
  8. package/dist/github.js +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/launch.d.ts +2 -2
  11. package/dist/launch.js +1 -1
  12. package/dist/main.js +1 -1
  13. package/dist/node-version.d.ts +3 -2
  14. package/dist/node-version.js +4 -3
  15. package/dist/providers.d.ts +1 -1
  16. package/dist/providers.js +1 -1
  17. package/dist/registry.d.ts +19 -10
  18. package/dist/registry.js +20 -5
  19. package/dist/scaffold.d.ts +19 -15
  20. package/dist/scaffold.js +63 -113
  21. package/package.json +1 -1
  22. package/skills/nola/SKILL.md +48 -16
  23. package/skills/nola/references/config.md +5 -5
  24. package/skills/nola/references/patterns.md +67 -31
  25. package/skills/nola/references/pitfalls.md +200 -52
  26. package/skills/nola/references/syntax.md +221 -122
  27. package/templates/empty/package.json +5 -5
  28. package/templates/empty/src/main.tsi +8 -0
  29. package/templates/empty/src/main.ts +0 -5
  30. package/templates/feature-extraction/README.md +0 -20
  31. package/templates/feature-extraction/nola.config.ts +0 -14
  32. package/templates/feature-extraction/nola.replay.jsonl +0 -2
  33. package/templates/feature-extraction/package.json +0 -22
  34. package/templates/feature-extraction/src/main.tsi +0 -22
  35. package/templates/feature-extraction/tsconfig.json +0 -12
  36. package/templates/function-calling/README.md +0 -21
  37. package/templates/function-calling/nola.config.ts +0 -14
  38. package/templates/function-calling/nola.replay.jsonl +0 -1
  39. package/templates/function-calling/package.json +0 -22
  40. package/templates/function-calling/src/main.tsi +0 -12
  41. package/templates/function-calling/src/tickets.ts +0 -17
  42. package/templates/function-calling/tsconfig.json +0 -12
  43. package/templates/typescript-interop/README.md +0 -15
  44. package/templates/typescript-interop/nola.config.ts +0 -14
  45. package/templates/typescript-interop/nola.replay.jsonl +0 -1
  46. package/templates/typescript-interop/package.json +0 -22
  47. package/templates/typescript-interop/src/main.ts +0 -8
  48. package/templates/typescript-interop/src/person.tsi +0 -11
  49. 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.
@@ -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);
@@ -170,17 +170,32 @@ async function fetchExampleFromGitHub(name, version, fetchImpl = fetch) {
170
170
  // src/registry.ts
171
171
  var DEFAULT_ENTRY = "src/main.ts";
172
172
  var TEMPLATES = [
173
- { name: "feature-extraction", label: "extract typed data from context", source: "builtin", entry: "src/main.tsi" },
173
+ {
174
+ name: "feature-extraction",
175
+ label: "extract typed data from context",
176
+ source: "example",
177
+ featured: true,
178
+ entry: "src/main.tsi"
179
+ },
174
180
  {
175
181
  name: "function-calling",
176
182
  label: "call an async function from a TypeScript file",
177
- source: "builtin",
183
+ source: "example",
184
+ featured: true,
185
+ entry: "src/main.tsi"
186
+ },
187
+ {
188
+ name: "agent-loop",
189
+ label: "an agent loop in one .tsi file: a top-level while with an ask, and a context statement read on every pass",
190
+ source: "example",
191
+ featured: true,
178
192
  entry: "src/main.tsi"
179
193
  },
180
194
  {
181
195
  name: "typescript-interop",
182
196
  label: "an infer function example, imported and awaited from plain TypeScript",
183
- source: "builtin"
197
+ source: "example",
198
+ featured: true
184
199
  },
185
200
  {
186
201
  name: "triage-ticket",
@@ -190,7 +205,7 @@ var TEMPLATES = [
190
205
  entry: "src/main.tsi",
191
206
  provider: { label: "typesafe.ai", envVar: "TYPESAFE_API_KEY" }
192
207
  },
193
- { name: "empty", label: "nola.config + tsconfig only, bring your own code", source: "builtin" },
208
+ { name: "empty", label: "nola.config + tsconfig only, bring your own code", source: "builtin", entry: "src/main.tsi" },
194
209
  { name: "file-ticket", label: "call intents: the model fills a function's arguments", source: "example" },
195
210
  { name: "extract-resume", label: "nested arrays of objects, JSDoc schema descriptions", source: "example" },
196
211
  { name: "extract-invoice", label: "same-file type references, optional fields", source: "example" },
@@ -231,16 +246,14 @@ function envExample(envVar) {
231
246
  ${envVar}=
232
247
  `;
233
248
  }
234
- var RENAMES = { _gitignore: ".gitignore" };
235
249
  var GITIGNORE_URL = new URL("../templates/_gitignore", import.meta.url);
236
250
  async function withRecommendedGitignore(files) {
237
251
  if (!files.has(".gitignore")) files.set(".gitignore", await readFile3(GITIGNORE_URL, "utf8"));
238
252
  return files;
239
253
  }
240
- var SUBSTITUTED = /* @__PURE__ */ new Set(["package.json", "README.md", "main.ts", "main.tsi"]);
241
254
  function nextStepsComment(ide, template) {
242
255
  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";
256
+ 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";
244
257
  return [
245
258
  "// Next steps in VS Code:",
246
259
  "// 1. Press F5 to run this file (.vscode/launch.json is already set up).",
@@ -255,29 +268,7 @@ function nextStepsComment(ide, template) {
255
268
  "// 2. Set up your editor (VS Code extension, debugging): https://nola.sh/docs/start/editor-setup/"
256
269
  ].join("\n");
257
270
  }
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
- }
271
+ var OFFLINE_ONLY = ["nola.replay.jsonl"];
281
272
  async function ownVersion() {
282
273
  const pkg = JSON.parse(await readFile3(new URL("../package.json", import.meta.url), "utf8"));
283
274
  return pkg.version;
@@ -289,67 +280,42 @@ async function prepareTarget(absRoot, force) {
289
280
  if (!force) throw new Error(`target directory ${absRoot} is not empty`);
290
281
  for (const entry of entries) await rm2(join3(absRoot, entry), { recursive: true, force: true });
291
282
  }
283
+ async function collectTemplate(def, version) {
284
+ if (def.source === "builtin") return collectExampleFromDisk(TEMPLATES_DIR, def.name);
285
+ const dev = await devExamplesDir();
286
+ return dev ? collectExampleFromDisk(dev, def.name) : fetchExampleFromGitHub(def.name, version);
287
+ }
292
288
  async function scaffold(targetDir, opts = {}) {
293
289
  const root = targetDir;
294
290
  const absRoot = resolve(targetDir);
295
291
  const template = opts.template ?? "feature-extraction";
296
292
  const def = templateByName(template);
297
293
  if (!def) throw new Error(`unknown template "${template}" (valid: ${templateNames().join(", ")})`);
294
+ const version = await ownVersion();
295
+ const files = await collectTemplate(def, version);
298
296
  await prepareTarget(absRoot, opts.force ?? false);
299
297
  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
298
  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");
299
+ if (provider !== "none") {
300
+ for (const offline of OFFLINE_ONLY) files.delete(offline);
301
+ files.set("nola.config.ts", await readFile3(providerConfigUrl(provider), "utf8"));
302
+ }
303
+ const manifest = files.get("package.json");
304
+ if (manifest) files.set("package.json", rewriteExamplePackageJson(manifest, { name, version }));
305
+ await withRecommendedGitignore(files);
306
+ const envVar = vendorEnvVar(template, provider);
307
+ if (envVar !== void 0 && !files.has(".env.example")) files.set(".env.example", envExample(envVar));
308
+ const entry = entryFile(template);
309
+ const program = files.get(entry);
310
+ if (program !== void 0) files.set(entry, `${nextStepsComment(opts.ide ?? "none", template)}
311
+
312
+ ${program}`);
313
+ for (const [relPath, content] of files) {
314
+ const target = join3(absRoot, relPath);
315
+ await mkdir2(join3(target, ".."), { recursive: true });
316
+ await writeFile2(target, content);
351
317
  }
352
- return { root, files: files.sort() };
318
+ return { root, files: [...files.keys()].sort() };
353
319
  }
354
320
 
355
321
  // src/add.ts
@@ -1571,7 +1537,7 @@ function nodeVersionWarning(version = process.versions.node) {
1571
1537
  if (nodeSupportsTypeStripping(version)) return void 0;
1572
1538
  const lines = [
1573
1539
  `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."
1540
+ "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
1541
  ];
1576
1542
  if (hasStripTypesFlag(version)) {
1577
1543
  lines.push(
@@ -1614,15 +1580,15 @@ var TEMPLATE_QUESTION = "Select a template:";
1614
1580
  var EXAMPLE_QUESTION = "Select an example:";
1615
1581
  var MORE_EXAMPLES = "more-examples";
1616
1582
  var BACK = "back";
1617
- var builtinNames = () => TEMPLATES.filter((t) => t.source === "builtin").map((t) => `"${t.name}"`).join(", ");
1618
1583
  function templateMenu() {
1619
1584
  const featured = featuredNames().map((name) => templateOption(templateByName(name)));
1620
1585
  return [...featured, { value: MORE_EXAMPLES, label: "More examples\u2026", hint: exampleNames().join(", ") }];
1621
1586
  }
1622
1587
  function exampleMenu() {
1623
1588
  const examples = exampleNames().map((name) => templateOption(templateByName(name)));
1624
- return [...examples, { value: BACK, label: "\u2190 Back", hint: "the builtin templates" }];
1589
+ return [...examples, { value: BACK, label: "\u2190 Back", hint: "the first menu" }];
1625
1590
  }
1591
+ var FETCH_FAILURE_HINT = 'Only "empty" ships inside the CLI and needs no download \u2014 every other template is fetched from GitHub.';
1626
1592
  async function selectTemplate(prompter) {
1627
1593
  for (; ; ) {
1628
1594
  const choice = await prompter.select(TEMPLATE_QUESTION, templateMenu());
@@ -1832,7 +1798,7 @@ function plainPrompter() {
1832
1798
  var FLOW_OPTIONS = {
1833
1799
  template: {
1834
1800
  type: "string",
1835
- description: "template to scaffold (feature-extraction, function-calling, typescript-interop, empty, or a curated example)"
1801
+ description: "template to scaffold: feature-extraction (the default), function-calling, agent-loop, typescript-interop, triage-ticket, empty, or another curated example"
1836
1802
  },
1837
1803
  add: { type: "boolean", description: "add Nola to the existing project in [dir] instead of scaffolding" },
1838
1804
  ide: { type: "string", description: "editor setup: vscode (the default) | none" },
@@ -2070,7 +2036,7 @@ Next steps:
2070
2036
  } catch (err) {
2071
2037
  if (err instanceof ExampleFetchError && interactive) {
2072
2038
  prompter.note(`${err.message}
2073
- The builtin templates (${builtinNames()}) work offline.`);
2039
+ ${FETCH_FAILURE_HINT}`);
2074
2040
  input.dir = outcome.dir;
2075
2041
  input.template = void 0;
2076
2042
  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
@@ -97,10 +97,16 @@ export declare const EXAMPLE_QUESTION = "Select an example:";
97
97
  export declare const MORE_EXAMPLES = "more-examples";
98
98
  /** the examples menu's last row — back to the first menu */
99
99
  export declare const BACK = "back";
100
- /** The first menu: the builtin templates and the featured example in registry order, then the examples row. */
100
+ /** The first menu: the featured examples and `empty` in registry order, then the examples row. */
101
101
  export declare function templateMenu(): PrompterOption[];
102
102
  /** The second menu: the curated examples the first menu does not carry, in registry order, then Back. */
103
103
  export declare function exampleMenu(): PrompterOption[];
104
+ /**
105
+ * The note a failed example fetch gets (interactive: the menu is asked
106
+ * again). Only `empty` ships inside the CLI; every other template is fetched
107
+ * from GitHub, so an offline machine can still scaffold that one.
108
+ */
109
+ 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
110
  export declare const PROVIDER_QUESTION = "Select an inference provider:";
105
111
  /** `nola key`'s confirmation before the browser opens (the scaffold's consent is the provider select itself). */
106
112
  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?";
@@ -132,7 +138,7 @@ export interface RunFlowArgs {
132
138
  export declare const FLOW_OPTIONS: {
133
139
  readonly template: {
134
140
  readonly type: "string";
135
- readonly description: "template to scaffold (feature-extraction, function-calling, typescript-interop, empty, or a curated example)";
141
+ readonly description: "template to scaffold: feature-extraction (the default), function-calling, agent-loop, typescript-interop, triage-ticket, empty, or another curated example";
136
142
  };
137
143
  readonly add: {
138
144
  readonly type: "boolean";
package/dist/flow.js CHANGED
@@ -18,7 +18,7 @@ import { nodeVersionWarning } from "./node-version.js";
18
18
  import { openBrowser } from "./open-url.js";
19
19
  import { detectPackageManager, packageManagerCommands } from "./package-manager.js";
20
20
  import { isProviderId, PROVIDERS, providerById, providerIds } from "./providers.js";
21
- import { entryFile, exampleNames, featuredNames, TEMPLATES, templateByName, templateNames } from "./registry.js";
21
+ import { entryFile, exampleNames, featuredNames, templateByName, templateNames } from "./registry.js";
22
22
  import { ownVersion, scaffold, vendorEnvVar } from "./scaffold.js";
23
23
  import { applyTrial } from "./trial.js";
24
24
  const DEFAULT_DIR = "nola-app";
@@ -62,9 +62,10 @@ function templateOption(t) {
62
62
  /*
63
63
  * The template step is TWO menus (clack's select has no sections): the first
64
64
  * lists the first-menu templates — feature-extraction, function-calling,
65
- * typescript-interop, triage-ticket (the featured example), empty — and one
66
- * "More examples…" row; that row opens the remaining curated examples, with a
67
- * Back row to return. `--template <name>` answers either level directly.
65
+ * agent-loop, typescript-interop, triage-ticket (the featured examples),
66
+ * empty (the one builtin) — and one "More examples…" row; that row opens the
67
+ * remaining curated examples, with a Back row to return. `--template <name>`
68
+ * answers either level directly.
68
69
  */
69
70
  export const TEMPLATE_QUESTION = "Select a template:";
70
71
  export const EXAMPLE_QUESTION = "Select an example:";
@@ -72,11 +73,7 @@ export const EXAMPLE_QUESTION = "Select an example:";
72
73
  export const MORE_EXAMPLES = "more-examples";
73
74
  /** the examples menu's last row — back to the first menu */
74
75
  export const BACK = "back";
75
- /** `"feature-extraction", "function-calling", …` — for the fetch-failure note. */
76
- const builtinNames = () => TEMPLATES.filter((t) => t.source === "builtin")
77
- .map((t) => `"${t.name}"`)
78
- .join(", ");
79
- /** The first menu: the builtin templates and the featured example in registry order, then the examples row. */
76
+ /** The first menu: the featured examples and `empty` in registry order, then the examples row. */
80
77
  export function templateMenu() {
81
78
  const featured = featuredNames().map((name) => templateOption(templateByName(name)));
82
79
  return [...featured, { value: MORE_EXAMPLES, label: "More examples…", hint: exampleNames().join(", ") }];
@@ -84,8 +81,14 @@ export function templateMenu() {
84
81
  /** The second menu: the curated examples the first menu does not carry, in registry order, then Back. */
85
82
  export function exampleMenu() {
86
83
  const examples = exampleNames().map((name) => templateOption(templateByName(name)));
87
- return [...examples, { value: BACK, label: "← Back", hint: "the builtin templates" }];
84
+ return [...examples, { value: BACK, label: "← Back", hint: "the first menu" }];
88
85
  }
86
+ /**
87
+ * The note a failed example fetch gets (interactive: the menu is asked
88
+ * again). Only `empty` ships inside the CLI; every other template is fetched
89
+ * from GitHub, so an offline machine can still scaffold that one.
90
+ */
91
+ export const FETCH_FAILURE_HINT = 'Only "empty" ships inside the CLI and needs no download — every other template is fetched from GitHub.';
89
92
  /** Ask the two-level template menu until a template is chosen; null = cancelled. */
90
93
  async function selectTemplate(prompter) {
91
94
  for (;;) {
@@ -370,7 +373,7 @@ export function plainPrompter() {
370
373
  export const FLOW_OPTIONS = {
371
374
  template: {
372
375
  type: "string",
373
- description: "template to scaffold (feature-extraction, function-calling, typescript-interop, empty, or a curated example)",
376
+ description: "template to scaffold: feature-extraction (the default), function-calling, agent-loop, typescript-interop, triage-ticket, empty, or another curated example",
374
377
  },
375
378
  add: { type: "boolean", description: "add Nola to the existing project in [dir] instead of scaffolding" },
376
379
  ide: { type: "string", description: "editor setup: vscode (the default) | none" },
@@ -675,7 +678,7 @@ export async function runFlow(args, opts = {}) {
675
678
  }
676
679
  catch (err) {
677
680
  if (err instanceof ExampleFetchError && interactive) {
678
- prompter.note(`${err.message}\nThe builtin templates (${builtinNames()}) work offline.`);
681
+ prompter.note(`${err.message}\n${FETCH_FAILURE_HINT}`);
679
682
  input.dir = outcome.dir; // keep the chosen dir, re-pick the template
680
683
  input.template = undefined;
681
684
  continue;
package/dist/github.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- /** Network/acquisition failure — the builtin templates always work offline. */
1
+ /** Network/acquisition failure — `empty`, the one builtin template, always works offline. */
2
2
  export declare class ExampleFetchError extends Error {
3
3
  }
4
4
  export type FetchLike = (url: string) => Promise<{
package/dist/github.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** Production example acquisition: GitHub Trees API + raw file fetches. */
2
2
  const REPO = "nola-lang/nola";
3
- /** Network/acquisition failure — the builtin templates always work offline. */
3
+ /** Network/acquisition failure — `empty`, the one builtin template, always works offline. */
4
4
  export class ExampleFetchError extends Error {
5
5
  }
6
6
  /** Try the lockstep release tag first; a repo without that tag serves main. */
package/dist/index.js CHANGED
@@ -87,7 +87,7 @@ import {
87
87
  writeEnvKey,
88
88
  writeSession,
89
89
  writeVscodeSetup
90
- } from "./chunk-LUSVIOAN.js";
90
+ } from "./chunk-CB26Z47Y.js";
91
91
  import {
92
92
  clackPrompter
93
93
  } from "./chunk-7IQIRMK6.js";
package/dist/launch.d.ts CHANGED
@@ -17,7 +17,7 @@ export interface Launcher {
17
17
  /**
18
18
  * `code <dir> [<dir>/<entry>]`, detached; "not-found" when VS Code's `code`
19
19
  * command is not on PATH. `entry` is a project-relative file to open as the
20
- * active editor on top of the folder (the scaffold's src/main.ts).
20
+ * active editor on top of the folder (the template's entry file).
21
21
  */
22
22
  openVscode(dir: string, entry?: string): Promise<"opened" | "not-found">;
23
23
  }
@@ -25,7 +25,7 @@ export interface Launcher {
25
25
  * The arguments that open a project in VS Code: the folder alone, or the
26
26
  * folder plus one file. `code <folder> <file>` opens the folder as the
27
27
  * workspace and the file as the active editor — that is how the scaffold
28
- * lands the user on src/main.ts instead of an empty window.
28
+ * lands the user on the entry file instead of an empty window.
29
29
  */
30
30
  export declare function vscodeArgs(dir: string, entry?: string): string[];
31
31
  /**
package/dist/launch.js CHANGED
@@ -5,7 +5,7 @@ import { join } from "node:path";
5
5
  * The arguments that open a project in VS Code: the folder alone, or the
6
6
  * folder plus one file. `code <folder> <file>` opens the folder as the
7
7
  * workspace and the file as the active editor — that is how the scaffold
8
- * lands the user on src/main.ts instead of an empty window.
8
+ * lands the user on the entry file instead of an empty window.
9
9
  */
10
10
  export function vscodeArgs(dir, entry) {
11
11
  return entry === undefined ? [dir] : [dir, join(dir, entry)];
package/dist/main.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  CREATE_COMMAND,
4
4
  dispatch,
5
5
  ownVersion
6
- } from "./chunk-LUSVIOAN.js";
6
+ } from "./chunk-CB26Z47Y.js";
7
7
  import "./chunk-FPS2V3S4.js";
8
8
 
9
9
  // src/main.ts
@@ -1,6 +1,7 @@
1
1
  /**
2
- * The Node floor the docs state. Plain `.ts` (the templates' `src/main.ts`)
3
- * is loaded by Node's native type stripping, which is on by default only from
2
+ * The Node floor the docs state. Plain `.ts` (an example's `src/main.ts`, or the
3
+ * plain modules a `.tsi` entry imports) is loaded by Node's native type
4
+ * stripping, which is on by default only from
4
5
  * 22.18 on the 22 line and 23.6 on the 23 line; before that it sits behind
5
6
  * `--experimental-strip-types` and the default loader rejects `.ts` outright
6
7
  * (ERR_UNKNOWN_FILE_EXTENSION) — `npm start` and the F5 launch both die on
@@ -1,6 +1,7 @@
1
1
  /**
2
- * The Node floor the docs state. Plain `.ts` (the templates' `src/main.ts`)
3
- * is loaded by Node's native type stripping, which is on by default only from
2
+ * The Node floor the docs state. Plain `.ts` (an example's `src/main.ts`, or the
3
+ * plain modules a `.tsi` entry imports) is loaded by Node's native type
4
+ * stripping, which is on by default only from
4
5
  * 22.18 on the 22 line and 23.6 on the 23 line; before that it sits behind
5
6
  * `--experimental-strip-types` and the default loader rejects `.ts` outright
6
7
  * (ERR_UNKNOWN_FILE_EXTENSION) — `npm start` and the F5 launch both die on
@@ -47,7 +48,7 @@ export function nodeVersionWarning(version = process.versions.node) {
47
48
  return undefined;
48
49
  const lines = [
49
50
  `Node ${version} is older than Nola needs: Node >= ${NODE_FLOOR}, the first release that runs .ts files natively.`,
50
- "On this Node, `npm start` and the VS Code F5 launch fail with ERR_UNKNOWN_FILE_EXTENSION for src/main.ts.",
51
+ "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.",
51
52
  ];
52
53
  if (hasStripTypesFlag(version)) {
53
54
  lines.push("Upgrade Node (recommended), or set NODE_OPTIONS=--experimental-strip-types for `npm start` and add", '"--experimental-strip-types" to runtimeArgs in .vscode/launch.json.');
@@ -4,7 +4,7 @@
4
4
  * template registry: `nola` is the platform, labelled "nola: dev" so the row
5
5
  * reads as a mode of the project rather than a fourth vendor (the free trial, or a key on the
6
6
  * account — see `keyPath`), the four vendors write a bring-your-own config,
7
- * `none` is the offline default (the builtin templates replay their ledgers). typesafe.ai
7
+ * `none` is the offline default (the template's own config: a recorded ledger or a mock). typesafe.ai
8
8
  * serves literal unions and booleans only; it is listed for every template all
9
9
  * the same, and `providerOptions` (flow.ts) brackets that caveat into its hint.
10
10
  */
package/dist/providers.js CHANGED
@@ -10,7 +10,7 @@ export const PROVIDERS = [
10
10
  },
11
11
  { id: "google", label: "Gemini", hint: 'google("gemini-2.5-flash"), reads GEMINI_API_KEY', envVar: "GEMINI_API_KEY", model: 'google("gemini-2.5-flash")' },
12
12
  { id: "typesafe", label: "typesafe.ai", hint: "typesafe(), reads TYPESAFE_API_KEY", envVar: "TYPESAFE_API_KEY", model: "typesafe()" },
13
- { id: "none", label: "Skip for now", hint: "the builtin templates run offline from a replay ledger; pick a model in nola.config.ts later" },
13
+ { 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" },
14
14
  ];
15
15
  export function providerById(id) {
16
16
  return PROVIDERS.find((p) => p.id === id);
@@ -2,31 +2,40 @@
2
2
  * The template menu. Static by design: it is versioned in lockstep with the
3
3
  * git tag the example fetcher pulls from, so names and labels cannot drift.
4
4
  *
5
+ * `empty` is the ONLY builtin template (config + tsconfig + a stub entry,
6
+ * shipped inside this package). Every other template IS a curated example:
7
+ * its files come from `examples/` — on disk in a dev checkout, from GitHub
8
+ * (the lockstep release tag) in production — and the scaffolder adds the
9
+ * project name, the lockstep ranges, the recommended `.gitignore`, the
10
+ * chosen provider's config and the next-steps comment on top.
11
+ *
5
12
  * The first menu leads (in this order) and is organized BY FEATURE:
6
13
  * `feature-extraction` (the default) and `function-calling` are each one
7
14
  * `.tsi` file with a top-level ask — the two things Nola does —
15
+ * `agent-loop` is the same shape around a top-level `while`,
8
16
  * `typescript-interop` is the infer-function shape called from plain
9
- * TypeScript, `triage-ticket` (a FEATURED example: fetched from examples/
10
- * like the rest, listed here because it is the one-file shape on a
11
- * non-chat vendor) and `empty`. The other curated examples follow (behind
12
- * "More examples…"). extract-person is deliberately absent —
13
- * typescript-interop IS extract-person plus the replay ledger.
17
+ * TypeScript, `triage-ticket` the one-file shape on a non-chat vendor, then
18
+ * `empty` (a stub `src/main.tsi` to write your own in). The other curated
19
+ * examples follow (behind "More examples…").
20
+ * extract-person is deliberately absent — typescript-interop IS
21
+ * extract-person plus the replay ledger.
14
22
  */
15
23
  export interface TemplateDef {
16
24
  name: string;
17
25
  /** one-line menu description */
18
26
  label: string;
27
+ /** where the files come from: this package's `templates/` dir, or `examples/` */
19
28
  source: "builtin" | "example";
20
29
  /**
21
- * An example listed on the FIRST menu, in registry order beside the
22
- * builtin templates, instead of behind "More examples…". Its files still
23
- * come from examples/ (`source` is unchanged).
30
+ * An example listed on the FIRST menu, in registry order beside `empty`,
31
+ * instead of behind "More examples…". Its files still come from examples/
32
+ * (`source` is unchanged).
24
33
  */
25
34
  featured?: true;
26
35
  /**
27
36
  * The file that IS the program — what `start` runs, launch.json's
28
37
  * `program`, what VS Code opens, where the next-steps comment lands.
29
- * Absent = the plain-TS `src/main.ts` every other template has.
38
+ * Absent = the plain-TS `src/main.ts` the other examples have.
30
39
  */
31
40
  entry?: string;
32
41
  /**
@@ -43,7 +52,7 @@ export interface TemplateDef {
43
52
  /** What a template runs unless it names its own entry: the plain-TS main. */
44
53
  export declare const DEFAULT_ENTRY = "src/main.ts";
45
54
  export declare const TEMPLATES: readonly TemplateDef[];
46
- /** The first menu's templates: the builtins and the featured example, in registry order. */
55
+ /** The first menu's templates: the featured examples and `empty`, in registry order. */
47
56
  export declare function featuredNames(): string[];
48
57
  /** The examples behind "More examples…": every example the first menu does not carry. */
49
58
  export declare function exampleNames(): string[];
package/dist/registry.js CHANGED
@@ -1,17 +1,32 @@
1
1
  /** What a template runs unless it names its own entry: the plain-TS main. */
2
2
  export const DEFAULT_ENTRY = "src/main.ts";
3
3
  export const TEMPLATES = [
4
- { name: "feature-extraction", label: "extract typed data from context", source: "builtin", entry: "src/main.tsi" },
4
+ {
5
+ name: "feature-extraction",
6
+ label: "extract typed data from context",
7
+ source: "example",
8
+ featured: true,
9
+ entry: "src/main.tsi",
10
+ },
5
11
  {
6
12
  name: "function-calling",
7
13
  label: "call an async function from a TypeScript file",
8
- source: "builtin",
14
+ source: "example",
15
+ featured: true,
16
+ entry: "src/main.tsi",
17
+ },
18
+ {
19
+ name: "agent-loop",
20
+ label: "an agent loop in one .tsi file: a top-level while with an ask, and a context statement read on every pass",
21
+ source: "example",
22
+ featured: true,
9
23
  entry: "src/main.tsi",
10
24
  },
11
25
  {
12
26
  name: "typescript-interop",
13
27
  label: "an infer function example, imported and awaited from plain TypeScript",
14
- source: "builtin",
28
+ source: "example",
29
+ featured: true,
15
30
  },
16
31
  {
17
32
  name: "triage-ticket",
@@ -21,7 +36,7 @@ export const TEMPLATES = [
21
36
  entry: "src/main.tsi",
22
37
  provider: { label: "typesafe.ai", envVar: "TYPESAFE_API_KEY" },
23
38
  },
24
- { name: "empty", label: "nola.config + tsconfig only, bring your own code", source: "builtin" },
39
+ { name: "empty", label: "nola.config + tsconfig only, bring your own code", source: "builtin", entry: "src/main.tsi" },
25
40
  { name: "file-ticket", label: "call intents: the model fills a function's arguments", source: "example" },
26
41
  { name: "extract-resume", label: "nested arrays of objects, JSDoc schema descriptions", source: "example" },
27
42
  { name: "extract-invoice", label: "same-file type references, optional fields", source: "example" },
@@ -29,7 +44,7 @@ export const TEMPLATES = [
29
44
  { name: "chain-of-thought", label: "two asks sharing accumulating context", source: "example" },
30
45
  { name: "research-notes", label: "TS control flow orchestrating nola functions", source: "example" },
31
46
  ];
32
- /** The first menu's templates: the builtins and the featured example, in registry order. */
47
+ /** The first menu's templates: the featured examples and `empty`, in registry order. */
33
48
  export function featuredNames() {
34
49
  return TEMPLATES.filter((t) => t.source === "builtin" || t.featured).map((t) => t.name);
35
50
  }