create-nola-lang 0.1.11 → 0.1.13

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 (48) hide show
  1. package/README.md +19 -13
  2. package/dist/agents.d.ts +13 -1
  3. package/dist/agents.js +107 -9
  4. package/dist/checkout.d.ts +20 -0
  5. package/dist/checkout.js +48 -1
  6. package/dist/{chunk-MAULRZZD.js → chunk-LUSVIOAN.js} +248 -77
  7. package/dist/flow.d.ts +19 -5
  8. package/dist/flow.js +139 -51
  9. package/dist/ide.d.ts +2 -2
  10. package/dist/ide.js +12 -11
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.js +5 -1
  13. package/dist/launch.d.ts +2 -0
  14. package/dist/launch.js +1 -0
  15. package/dist/main.js +1 -1
  16. package/dist/providers.d.ts +7 -4
  17. package/dist/providers.js +3 -2
  18. package/dist/registry.d.ts +30 -2
  19. package/dist/registry.js +34 -7
  20. package/dist/scaffold.d.ts +20 -6
  21. package/dist/scaffold.js +42 -13
  22. package/package.json +1 -1
  23. package/skills/nola/SKILL.md +8 -5
  24. package/skills/nola/references/config.md +22 -13
  25. package/skills/nola/references/patterns.md +92 -17
  26. package/skills/nola/references/pitfalls.md +66 -19
  27. package/skills/nola/references/syntax.md +98 -28
  28. package/templates/_providers/typesafe.config.ts +18 -0
  29. package/templates/feature-extraction/README.md +20 -0
  30. package/templates/feature-extraction/nola.config.ts +14 -0
  31. package/templates/feature-extraction/nola.replay.jsonl +2 -0
  32. package/templates/feature-extraction/package.json +22 -0
  33. package/templates/feature-extraction/src/main.tsi +22 -0
  34. package/templates/function-calling/README.md +21 -0
  35. package/templates/function-calling/nola.config.ts +14 -0
  36. package/templates/function-calling/nola.replay.jsonl +1 -0
  37. package/templates/function-calling/package.json +22 -0
  38. package/templates/function-calling/src/main.tsi +12 -0
  39. package/templates/function-calling/src/tickets.ts +17 -0
  40. package/templates/function-calling/tsconfig.json +12 -0
  41. package/templates/{starter → typescript-interop}/nola.config.ts +1 -1
  42. package/templates/{starter → typescript-interop}/src/person.tsi +1 -1
  43. package/templates/typescript-interop/tsconfig.json +12 -0
  44. /package/templates/{starter → feature-extraction}/tsconfig.json +0 -0
  45. /package/templates/{starter → typescript-interop}/README.md +0 -0
  46. /package/templates/{starter → typescript-interop}/nola.replay.jsonl +0 -0
  47. /package/templates/{starter → typescript-interop}/package.json +0 -0
  48. /package/templates/{starter → typescript-interop}/src/main.ts +0 -0
package/dist/flow.d.ts CHANGED
@@ -91,6 +91,16 @@ export declare const EDITOR_QUESTION = "Set up your Editor?";
91
91
  export declare const AGENTS_QUESTION = "Set up Coding Agents?";
92
92
  /** The grouped list a yes opens. */
93
93
  export declare const SETUP_LIST_QUESTION = "What to set up? (Space toggles, Enter confirms)";
94
+ export declare const TEMPLATE_QUESTION = "Select a template:";
95
+ export declare const EXAMPLE_QUESTION = "Select an example:";
96
+ /** the first menu's last row — opens the examples menu */
97
+ export declare const MORE_EXAMPLES = "more-examples";
98
+ /** the examples menu's last row — back to the first menu */
99
+ export declare const BACK = "back";
100
+ /** The first menu: the builtin templates and the featured example in registry order, then the examples row. */
101
+ export declare function templateMenu(): PrompterOption[];
102
+ /** The second menu: the curated examples the first menu does not carry, in registry order, then Back. */
103
+ export declare function exampleMenu(): PrompterOption[];
94
104
  export declare const PROVIDER_QUESTION = "Select an inference provider:";
95
105
  /** `nola key`'s confirmation before the browser opens (the scaffold's consent is the provider select itself). */
96
106
  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?";
@@ -100,8 +110,8 @@ export declare const SIGN_IN_QUESTION = "This machine already used its 25 free N
100
110
  * machine used them, a key on the account when already signed in.
101
111
  */
102
112
  export declare function nolaHint(path: KeyPath): string;
103
- /** The provider menu with the nola row worded for this machine. */
104
- export declare function providerOptions(path: KeyPath): PrompterOption[];
113
+ /** The provider menu with the nola row worded for this machine and the typesafe row for this template. */
114
+ export declare function providerOptions(path: KeyPath, template?: string): PrompterOption[];
105
115
  /** Args fill prompts; whatever is missing is asked (never asked non-interactively). */
106
116
  export declare function resolveScaffoldOptions(input: FlowInput, prompter: Prompter): Promise<FlowOutcome>;
107
117
  /** Non-interactive prompter: output only; being asked anything is a bug. */
@@ -122,7 +132,7 @@ export interface RunFlowArgs {
122
132
  export declare const FLOW_OPTIONS: {
123
133
  readonly template: {
124
134
  readonly type: "string";
125
- readonly description: "template to scaffold (starter, empty, or a curated example)";
135
+ readonly description: "template to scaffold (feature-extraction, function-calling, typescript-interop, empty, or a curated example)";
126
136
  };
127
137
  readonly add: {
128
138
  readonly type: "boolean";
@@ -130,11 +140,11 @@ export declare const FLOW_OPTIONS: {
130
140
  };
131
141
  readonly ide: {
132
142
  readonly type: "string";
133
- readonly description: "editor setup: vscode | none";
143
+ readonly description: "editor setup: vscode (the default) | none";
134
144
  };
135
145
  readonly agents: {
136
146
  readonly type: "string";
137
- readonly description: `agent skill files: ${string} | all | none`;
147
+ readonly description: `agent skill files: ${string} | all | none (default: claude,universal)`;
138
148
  };
139
149
  readonly provider: {
140
150
  readonly type: "string";
@@ -173,6 +183,10 @@ export interface RunFlowOptions {
173
183
  }
174
184
  /** The last non-empty output line, colour codes stripped, trimmed to one spinner line. */
175
185
  export declare function lastOutputLine(output: string): string | undefined;
186
+ /** The last question when VS Code's `code` command is on PATH… */
187
+ export declare const INSTALL_OPEN_QUESTION = "Install dependencies and open VS Code?";
188
+ /** …and when it is not (2026-09-18): the editor is neither promised nor opened. */
189
+ export declare const INSTALL_QUESTION = "Install dependencies?";
176
190
  /**
177
191
  * A Retry-After wait in words. The per-address daily limit answers with the
178
192
  * seconds left in the day ("49832 s" — nobody reads that as fourteen hours),
package/dist/flow.js CHANGED
@@ -6,7 +6,7 @@ import { addNola } from "./add.js";
6
6
  import { AGENT_IDS, AGENT_OPTIONS, defaultAgents, parseAgentsFlag, writeAgentSkills } from "./agents.js";
7
7
  import { NolaApiError, nolaApiUrl } from "./api.js";
8
8
  import { signIn } from "./auth.js";
9
- import { LINK_CHECKOUT_ENV, linkCheckoutFromEnv, linkCheckoutPackages } from "./checkout.js";
9
+ import { CHECKOUT_DIST_SKIP_GLOB, LINK_CHECKOUT_ENV, linkCheckoutFromEnv, linkCheckoutPackages, skipCheckoutDistInLaunch, } from "./checkout.js";
10
10
  import { defineCommand } from "./cli.js";
11
11
  import { readSession } from "./credentials.js";
12
12
  import { ExampleFetchError } from "./github.js";
@@ -18,23 +18,32 @@ 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 { TEMPLATES, templateByName, templateNames } from "./registry.js";
22
- import { ownVersion, scaffold } from "./scaffold.js";
21
+ import { entryFile, exampleNames, featuredNames, TEMPLATES, templateByName, templateNames } from "./registry.js";
22
+ import { ownVersion, scaffold, vendorEnvVar } from "./scaffold.js";
23
23
  import { applyTrial } from "./trial.js";
24
24
  const DEFAULT_DIR = "nola-app";
25
- const DEFAULT_TEMPLATE = "starter";
25
+ const DEFAULT_TEMPLATE = "feature-extraction";
26
26
  /*
27
27
  * The wizard, in order (spec 2026-08-12-interactive-init-design.md, reordered
28
28
  * 2026-09-03, provider select 2026-09-08): name → template → "Select an
29
29
  * inference provider" (Nola first — the free runs, or a key on the account —
30
- * then the vendors, then skip) → "Set up your editor and coding agents?"
31
- * (yes/no, default yes — the whole no is one Enter) → on yes, ONE grouped
32
- * checkbox list (editor + coding agents), then — once the files are on
33
- * disk — "Install dependencies and open VS Code?". Every question is
34
- * answered before anything is written; the nola key request runs after the
35
- * last question.
30
+ * then the vendors, then skip) → then — once the files are on disk —
31
+ * "Install dependencies and open VS Code?". The editor setup and the agent
32
+ * skill are written WITHOUT a question (2026-09-18; `--ide none` /
33
+ * `--agents …` override): the "Set up your Editor and Coding Agents?" gate
34
+ * and its grouped list are kept behind `SETUP_STEP_ASKED` should the step
35
+ * come back. Every question is answered before anything is written; the
36
+ * nola key request runs after the last question.
36
37
  */
37
38
  export const NAME_QUESTION = `Project name (press Enter to keep "${DEFAULT_DIR}"):`;
39
+ /**
40
+ * The retired setup step (2026-09-18). false = the editor setup and the agent
41
+ * skill are defaults (`DEFAULT_IDE`, `defaultAgents`) and nothing is asked;
42
+ * flip to true to bring the gate + grouped list back exactly as they were.
43
+ */
44
+ const SETUP_STEP_ASKED = false;
45
+ /** The editor the scaffold sets up unless `--ide none` says otherwise. */
46
+ const DEFAULT_IDE = "vscode";
38
47
  /** The yes/no gate in front of the setup list; narrowed when a flag already answered one half. */
39
48
  export const SETUP_QUESTION = "Set up your Editor and Coding Agents?";
40
49
  export const EDITOR_QUESTION = "Set up your Editor?";
@@ -46,25 +55,69 @@ const EDITOR_OPTIONS = [
46
55
  { value: "vscode", label: "VS Code", hint: ".vscode/ with a debug launch config + extension recommendation" },
47
56
  ];
48
57
  function templateOption(t) {
49
- // a pinned template names its vendor in the row itself: `example: triage-ticket (typesafe.ai)`
58
+ // a pinned template names its vendor in the row itself: `triage-ticket (typesafe.ai)`
50
59
  const vendor = t.provider ? ` (${t.provider.label})` : "";
51
- return { value: t.name, label: t.source === "example" ? `example: ${t.name}${vendor}` : `${t.name}${vendor}`, hint: t.label };
60
+ return { value: t.name, label: `${t.name}${vendor}`, hint: t.label };
61
+ }
62
+ /*
63
+ * The template step is TWO menus (clack's select has no sections): the first
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.
68
+ */
69
+ export const TEMPLATE_QUESTION = "Select a template:";
70
+ export const EXAMPLE_QUESTION = "Select an example:";
71
+ /** the first menu's last row — opens the examples menu */
72
+ export const MORE_EXAMPLES = "more-examples";
73
+ /** the examples menu's last row — back to the first menu */
74
+ 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. */
80
+ export function templateMenu() {
81
+ const featured = featuredNames().map((name) => templateOption(templateByName(name)));
82
+ return [...featured, { value: MORE_EXAMPLES, label: "More examples…", hint: exampleNames().join(", ") }];
83
+ }
84
+ /** The second menu: the curated examples the first menu does not carry, in registry order, then Back. */
85
+ export function exampleMenu() {
86
+ const examples = exampleNames().map((name) => templateOption(templateByName(name)));
87
+ return [...examples, { value: BACK, label: "← Back", hint: "the builtin templates" }];
88
+ }
89
+ /** Ask the two-level template menu until a template is chosen; null = cancelled. */
90
+ async function selectTemplate(prompter) {
91
+ for (;;) {
92
+ const choice = await prompter.select(TEMPLATE_QUESTION, templateMenu());
93
+ if (choice === null)
94
+ return null;
95
+ if (choice !== MORE_EXAMPLES)
96
+ return choice;
97
+ const example = await prompter.select(EXAMPLE_QUESTION, exampleMenu());
98
+ if (example === null)
99
+ return null;
100
+ if (example !== BACK)
101
+ return example;
102
+ }
52
103
  }
53
104
  /**
54
- * The setup step: a yes/no gate (default yes), then editor + coding agents in
55
- * one grouped checkbox list, VS Code and both skill copies preselected — so the
56
- * defaults are two Enters and the whole no is one. `--ide` / `--agents` each
57
- * fill their half — a group a flag already answered is not shown and the gate
58
- * narrows to the half that remains; a no leaves the asked halves at none.
59
- * null = cancelled. Non-interactive defaults: no editor, no agents.
105
+ * The editor + agents answer. Today (the step is retired): the flags, else the
106
+ * defaults — VS Code and both skill targets — interactive or not, so a scaffold
107
+ * carries `.vscode/` and the skill unless `--ide none` / `--agents none` opt
108
+ * out. With `SETUP_STEP_ASKED`: a yes/no gate (default yes), then editor +
109
+ * coding agents in one grouped checkbox list with the same defaults
110
+ * preselected; `--ide` / `--agents` each fill their half — a group a flag
111
+ * already answered is not shown and the gate narrows to the half that
112
+ * remains; a no leaves the asked halves at none. null = cancelled.
60
113
  */
61
114
  async function resolveSetup(input, prompter, dir) {
62
115
  const ide = input.ide === "vscode" || input.ide === "none" ? input.ide : undefined;
63
116
  const agents = input.agents !== undefined ? parseAgentsFlag(input.agents) : undefined;
64
117
  if (ide !== undefined && agents !== undefined)
65
118
  return { ide, agents };
66
- if (!input.interactive)
67
- return { ide: ide ?? "none", agents: agents ?? [] };
119
+ if (!input.interactive || !SETUP_STEP_ASKED)
120
+ return { ide: ide ?? DEFAULT_IDE, agents: agents ?? defaultAgents(dir) };
68
121
  const groups = [];
69
122
  const initial = [];
70
123
  if (ide === undefined) {
@@ -107,9 +160,27 @@ export function nolaHint(path) {
107
160
  return providerById("nola")?.hint ?? "";
108
161
  }
109
162
  }
110
- /** The provider menu with the nola row worded for this machine. */
111
- export function providerOptions(path) {
112
- return PROVIDERS.map((p) => ({ value: p.id, label: p.label, hint: p.id === "nola" ? nolaHint(path) : p.hint }));
163
+ /**
164
+ * The typesafe row's bracket: the vendor serves literal unions and booleans
165
+ * only, so for a template whose asks go further the hint says so by name
166
+ * (the template was chosen one question earlier); no template (add mode)
167
+ * gets the generic form; a template whose own config already names
168
+ * typesafe.ai needs no warning.
169
+ */
170
+ function typesafeHint(base, template) {
171
+ if (template === undefined)
172
+ return `${base} (literal unions and booleans only)`;
173
+ if (templateByName(template)?.provider?.label === "typesafe.ai")
174
+ return base;
175
+ return `${base} (does not support every construct in ${template}: literal unions and booleans only)`;
176
+ }
177
+ /** The provider menu with the nola row worded for this machine and the typesafe row for this template. */
178
+ export function providerOptions(path, template) {
179
+ return PROVIDERS.map((p) => ({
180
+ value: p.id,
181
+ label: p.label,
182
+ hint: p.id === "nola" ? nolaHint(path) : p.id === "typesafe" ? typesafeHint(p.hint, template) : p.hint,
183
+ }));
113
184
  }
114
185
  /** The provider a flag already chose: `--provider`, else the `--trial` / `--no-trial` shorthands; throws when they disagree. */
115
186
  function flaggedProvider(input) {
@@ -135,17 +206,18 @@ function flaggedProvider(input) {
135
206
  * one Enter away, Ctrl+C cancels). The key itself is minted later, on the
136
207
  * stored session, after every question — nothing is consumed by a cancel.
137
208
  */
138
- async function resolveProvider(input, prompter, pinned = false) {
209
+ async function resolveProvider(input, prompter, template) {
139
210
  const flagged = flaggedProvider(input);
140
211
  if (flagged !== undefined)
141
212
  return flagged;
142
213
  // A template whose own config names its vendor (triage-ticket → typesafe())
143
214
  // is not asked: "none" keeps that config, and the outro names its env var.
215
+ const pinned = template !== undefined && templateByName(template)?.provider !== undefined;
144
216
  if (pinned || !input.interactive)
145
217
  return "none";
146
218
  const path = input.keyPath ?? { kind: "trial" };
147
219
  while (true) {
148
- const choice = await prompter.select(PROVIDER_QUESTION, providerOptions(path));
220
+ const choice = await prompter.select(PROVIDER_QUESTION, providerOptions(path, template));
149
221
  if (choice === null)
150
222
  return null;
151
223
  if (!isProviderId(choice))
@@ -156,8 +228,8 @@ async function resolveProvider(input, prompter, pinned = false) {
156
228
  }
157
229
  }
158
230
  /** The provider and setup (editor + agents) questions, in that order, shared by every outcome site; null = cancelled. */
159
- async function resolveExtras(input, prompter, dir, pinnedProvider = false) {
160
- const provider = await resolveProvider(input, prompter, pinnedProvider);
231
+ async function resolveExtras(input, prompter, dir, template) {
232
+ const provider = await resolveProvider(input, prompter, template);
161
233
  if (provider === null)
162
234
  return null;
163
235
  const setup = await resolveSetup(input, prompter, dir);
@@ -261,13 +333,13 @@ export async function resolveScaffoldOptions(input, prompter) {
261
333
  template = DEFAULT_TEMPLATE;
262
334
  }
263
335
  else {
264
- const choice = await prompter.select("Select a template:", TEMPLATES.map(templateOption));
336
+ const choice = await selectTemplate(prompter);
265
337
  if (choice === null)
266
338
  return { kind: "cancelled" };
267
339
  template = choice;
268
340
  }
269
341
  }
270
- const extras = await resolveExtras(input, prompter, dir, templateByName(template)?.provider !== undefined);
342
+ const extras = await resolveExtras(input, prompter, dir, template);
271
343
  if (extras === null)
272
344
  return { kind: "cancelled" };
273
345
  return { kind: "scaffold", dir, template, force, ...extras };
@@ -296,10 +368,16 @@ export function plainPrompter() {
296
368
  * `nola init`): parseArgs descriptors plus the help line each shows.
297
369
  */
298
370
  export const FLOW_OPTIONS = {
299
- template: { type: "string", description: "template to scaffold (starter, empty, or a curated example)" },
371
+ template: {
372
+ type: "string",
373
+ description: "template to scaffold (feature-extraction, function-calling, typescript-interop, empty, or a curated example)",
374
+ },
300
375
  add: { type: "boolean", description: "add Nola to the existing project in [dir] instead of scaffolding" },
301
- ide: { type: "string", description: "editor setup: vscode | none" },
302
- agents: { type: "string", description: `agent skill files: ${AGENT_IDS.join(",")} | all | none` },
376
+ ide: { type: "string", description: "editor setup: vscode (the default) | none" },
377
+ agents: {
378
+ type: "string",
379
+ description: `agent skill files: ${AGENT_IDS.join(",")} | all | none (default: claude,universal)`,
380
+ },
303
381
  provider: {
304
382
  type: "string",
305
383
  description: `inference provider: ${providerIds().join(" | ")} (nola = 25 free runs; non-interactive default: none)`,
@@ -325,8 +403,7 @@ function commented(command, note) {
325
403
  function nextSteps(outcome, fileCount, name, pm, installed = false, grant = null) {
326
404
  const cmd = packageManagerCommands(pm);
327
405
  // a chosen vendor's env var, else the one the template's own config reads (a pinned template under "none")
328
- const vendorEnv = providerById(outcome.provider)?.envVar ??
329
- (outcome.provider === "none" ? templateByName(outcome.template)?.provider?.envVar : undefined);
406
+ const vendorEnv = vendorEnvVar(outcome.template, outcome.provider);
330
407
  const startNote = grant
331
408
  ? grant.source === "trial"
332
409
  ? "25 free Nola runs — key in .env"
@@ -346,8 +423,6 @@ function nextSteps(outcome, fileCount, name, pm, installed = false, grant = null
346
423
  }
347
424
  /** How many trailing lines of a failed install's output the note shows. */
348
425
  const INSTALL_LOG_TAIL = 20;
349
- /** Every template's entry (builtin and curated examples alike) — what launch.json runs and VS Code opens. */
350
- const ENTRY_FILE = "src/main.ts";
351
426
  /** The last non-empty output line, colour codes stripped, trimmed to one spinner line. */
352
427
  export function lastOutputLine(output) {
353
428
  const lines = stripVTControlCharacters(output).split(/\r?\n/);
@@ -358,18 +433,24 @@ export function lastOutputLine(output) {
358
433
  }
359
434
  return undefined;
360
435
  }
436
+ /** The last question when VS Code's `code` command is on PATH… */
437
+ export const INSTALL_OPEN_QUESTION = "Install dependencies and open VS Code?";
438
+ /** …and when it is not (2026-09-18): the editor is neither promised nor opened. */
439
+ export const INSTALL_QUESTION = "Install dependencies?";
361
440
  /**
362
- * The optional last step: "Install dependencies and open VS Code?". Offered
363
- * only when the scaffold is interactive AND an editor was chosen — the editor
364
- * step is what makes opening it meaningful. A yes runs the package manager's
365
- * install under a spinner (its output is captured, the latest line shown
366
- * beneath the title, the tail printed only on failure) and then opens the
367
- * project in VS Code; Ctrl+C here is a plain no (the project is already on
368
- * disk). Returns whether the install succeeded so the outro can drop that
369
- * line.
441
+ * The optional last step: "Install dependencies and open VS Code?" — or just
442
+ * "Install dependencies?" on a machine without VS Code's `code` command,
443
+ * which then opens nothing. Offered only when the scaffold is interactive AND
444
+ * an editor was chosen — the editor step is what makes opening it meaningful.
445
+ * A yes runs the package manager's install under a spinner (its output is
446
+ * captured, the latest line shown beneath the title, the tail printed only on
447
+ * failure) and then opens the project in VS Code; Ctrl+C here is a plain no
448
+ * (the project is already on disk). Returns whether the install succeeded so
449
+ * the outro can drop that line.
370
450
  */
371
- async function offerInstallAndOpen(dir, pm, prompter, launcher, linkCheckout) {
372
- const yes = await prompter.confirm("Install dependencies and open VS Code?", true);
451
+ async function offerInstallAndOpen(dir, entryPath, pm, prompter, launcher, linkCheckout) {
452
+ const vscode = launcher.hasVscode();
453
+ const yes = await prompter.confirm(vscode ? INSTALL_OPEN_QUESTION : INSTALL_QUESTION, true);
373
454
  if (!yes)
374
455
  return { installed: false };
375
456
  const cmd = packageManagerCommands(pm);
@@ -390,8 +471,12 @@ async function offerInstallAndOpen(dir, pm, prompter, launcher, linkCheckout) {
390
471
  // at that workspace build instead.
391
472
  if (linkCheckout !== null) {
392
473
  const linked = await linkCheckoutPackages(dir, linkCheckout);
474
+ // The linked runtime now lives outside node_modules, so the launch
475
+ // config must blackbox it too or F10 over the first ask runs to the end.
476
+ const skipped = await skipCheckoutDistInLaunch(dir);
393
477
  prompter.note(`${LINK_CHECKOUT_ENV}: linked ${linked.join(", ")} to ${join(linkCheckout, "packages")} — ` +
394
- `a later ${cmd.install} restores the registry copies.`);
478
+ `a later ${cmd.install} restores the registry copies.` +
479
+ (skipped ? `\nAdded "${CHECKOUT_DIST_SKIP_GLOB}" to skipFiles in .vscode/launch.json so stepping stays in your code.` : ""));
395
480
  }
396
481
  }
397
482
  else {
@@ -399,11 +484,14 @@ async function offerInstallAndOpen(dir, pm, prompter, launcher, linkCheckout) {
399
484
  const tail = stripVTControlCharacters(output).trim().split(/\r?\n/).slice(-INSTALL_LOG_TAIL).join("\n");
400
485
  prompter.note(`${tail ? `${tail}\n` : ""}Run ${cmd.install} yourself once the cause is fixed.`);
401
486
  }
487
+ if (!vscode)
488
+ return { installed };
402
489
  // Land on the entry file, not an empty window: it opens with the next
403
490
  // steps (F5, breakpoints, the extension) as its first lines.
404
- const entry = existsSync(join(dir, ENTRY_FILE)) ? ENTRY_FILE : undefined;
491
+ const entry = existsSync(join(dir, entryPath)) ? entryPath : undefined;
405
492
  const opened = await launcher.openVscode(dir, entry);
406
493
  if (opened === "not-found") {
494
+ // `code` was on PATH a moment ago and is gone now — rare, but say so.
407
495
  prompter.note("VS Code's `code` command is not on PATH, so the folder was not opened. " +
408
496
  "In VS Code run \"Shell Command: Install 'code' command in PATH\" (macOS) or re-run the installer with \"Add to PATH\" (Windows), then open the folder from VS Code.");
409
497
  }
@@ -563,7 +651,7 @@ export async function runFlow(args, opts = {}) {
563
651
  }
564
652
  let ideWrote = [];
565
653
  if (outcome.ide === "vscode") {
566
- const ide = await writeVscodeSetup(outcome.dir);
654
+ const ide = await writeVscodeSetup(outcome.dir, entryFile(outcome.template));
567
655
  ideWrote = ide.wrote;
568
656
  for (const note of ide.skipped)
569
657
  prompter.note(note);
@@ -576,7 +664,7 @@ export async function runFlow(args, opts = {}) {
576
664
  prompter.note(note);
577
665
  }
578
666
  const launch = interactive && outcome.ide !== "none"
579
- ? await offerInstallAndOpen(outcome.dir, pm, prompter, opts.launcher ?? realLauncher, opts.linkCheckout ?? linkCheckoutFromEnv())
667
+ ? await offerInstallAndOpen(outcome.dir, entryFile(outcome.template), pm, prompter, opts.launcher ?? realLauncher, opts.linkCheckout ?? linkCheckoutFromEnv())
580
668
  : { installed: false };
581
669
  const message = nextSteps({ ...outcome, provider }, files.length + trialWrote.length + ideWrote.length + agWrote.length, basename(resolve(outcome.dir)), pm, launch.installed, grant);
582
670
  if (prompter.outro)
@@ -587,7 +675,7 @@ export async function runFlow(args, opts = {}) {
587
675
  }
588
676
  catch (err) {
589
677
  if (err instanceof ExampleFetchError && interactive) {
590
- prompter.note(`${err.message}\nThe "starter" and "empty" templates work offline.`);
678
+ prompter.note(`${err.message}\nThe builtin templates (${builtinNames()}) work offline.`);
591
679
  input.dir = outcome.dir; // keep the chosen dir, re-pick the template
592
680
  input.template = undefined;
593
681
  continue;
package/dist/ide.d.ts CHANGED
@@ -4,6 +4,6 @@ export interface IdeSetupResult {
4
4
  /** human-readable notes about files left untouched */
5
5
  skipped: string[];
6
6
  }
7
- /** Write .vscode/launch.json + extensions.json; never rewrites an existing file. */
8
- export declare function writeVscodeSetup(targetDir: string): Promise<IdeSetupResult>;
7
+ /** Write .vscode/launch.json (running `entry`) + extensions.json; never rewrites an existing file. */
8
+ export declare function writeVscodeSetup(targetDir: string, entry?: string): Promise<IdeSetupResult>;
9
9
  //# sourceMappingURL=ide.d.ts.map
package/dist/ide.js CHANGED
@@ -1,25 +1,26 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { mkdir, writeFile } from "node:fs/promises";
3
3
  import { join, resolve } from "node:path";
4
+ import { DEFAULT_ENTRY } from "./registry.js";
4
5
  /**
5
6
  * The resolved form of the VS Code extension's own "Nola: Launch File"
6
- * configuration snippet, pointed at the templates' real entry (src/main.ts —
7
+ * configuration snippet, pointed at the template's real entry (src/main.ts —
7
8
  * plain .ts runs under modern Node's native type stripping; the loader
8
- * handles .tsi). resolveSourceMapLocations and skipFiles are the two
9
+ * handles .tsi — or a one-file template's src/main.tsi, which the loader runs as
10
+ * the entry module). resolveSourceMapLocations and skipFiles are the two
9
11
  * launch-config invariants AGENTS.md documents as mandatory for .tsi
10
12
  * debugging — keep them.
11
13
  */
12
- const LAUNCH_JSON = {
14
+ const launchJson = (entry) => ({
13
15
  version: "0.2.0",
14
16
  configurations: [
15
17
  {
16
18
  type: "node",
17
19
  request: "launch",
18
20
  name: "Nola: Launch main",
19
- // ${workspaceFolder} below is VS Code's own launch-config variable —
20
- // deliberately a literal string, never a JS template.
21
- // biome-ignore lint/suspicious/noTemplateCurlyInString: VS Code variable syntax
22
- program: "${workspaceFolder}/src/main.ts",
21
+ // ${workspaceFolder} is VS Code's own launch-config variable — it must
22
+ // reach the file verbatim, hence the escaped dollar.
23
+ program: `\${workspaceFolder}/${entry}`,
23
24
  runtimeArgs: ["--import", "nola-lang/register", "--enable-source-maps"],
24
25
  console: "integratedTerminal",
25
26
  // biome-ignore lint/suspicious/noTemplateCurlyInString: VS Code variable syntax
@@ -29,17 +30,17 @@ const LAUNCH_JSON = {
29
30
  skipFiles: ["<node_internals>/**", "**/node_modules/**"],
30
31
  },
31
32
  ],
32
- };
33
+ });
33
34
  /** publisher.name of packages/vscode — resolves once the extension is on the marketplace. */
34
35
  const EXTENSIONS_JSON = { recommendations: ["nola.nola-vscode"] };
35
- /** Write .vscode/launch.json + extensions.json; never rewrites an existing file. */
36
- export async function writeVscodeSetup(targetDir) {
36
+ /** Write .vscode/launch.json (running `entry`) + extensions.json; never rewrites an existing file. */
37
+ export async function writeVscodeSetup(targetDir, entry = DEFAULT_ENTRY) {
37
38
  const vscodeDir = join(resolve(targetDir), ".vscode");
38
39
  await mkdir(vscodeDir, { recursive: true });
39
40
  const wrote = [];
40
41
  const skipped = [];
41
42
  const files = [
42
- ["launch.json", LAUNCH_JSON],
43
+ ["launch.json", launchJson(entry)],
43
44
  ["extensions.json", EXTENSIONS_JSON],
44
45
  ];
45
46
  for (const [name, content] of files) {
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export type { Session } from "./credentials.js";
12
12
  export { CREDENTIALS_FILE, credentialsPath, deleteSession, readSession, writeSession } from "./credentials.js";
13
13
  export { collectExampleFromDisk, devExamplesDir, rewriteExamplePackageJson } from "./examples.js";
14
14
  export type { FlowInput, FlowOutcome, KeyPath, Prompter, PrompterOption, RunFlowArgs, RunFlowOptions } from "./flow.js";
15
- export { CREATE_COMMAND, FLOW_OPTIONS, nolaHint, PROVIDER_QUESTION, plainPrompter, providerOptions, resolveScaffoldOptions, runFlow, SIGN_IN_QUESTION } from "./flow.js";
15
+ export { CREATE_COMMAND, FLOW_OPTIONS, INSTALL_OPEN_QUESTION, INSTALL_QUESTION, nolaHint, PROVIDER_QUESTION, plainPrompter, providerOptions, resolveScaffoldOptions, runFlow, SIGN_IN_QUESTION, } from "./flow.js";
16
16
  export type { FetchLike } from "./github.js";
17
17
  export { ExampleFetchError, fetchExampleFromGitHub } from "./github.js";
18
18
  export type { NolaHomeConfig, TrialAccountRecord } from "./home-config.js";
package/dist/index.js CHANGED
@@ -10,6 +10,8 @@ import {
10
10
  ExampleFetchError,
11
11
  FLOW_OPTIONS,
12
12
  HOME_CONFIG_FILE,
13
+ INSTALL_OPEN_QUESTION,
14
+ INSTALL_QUESTION,
13
15
  NolaApiError,
14
16
  PROVIDERS,
15
17
  PROVIDER_QUESTION,
@@ -85,7 +87,7 @@ import {
85
87
  writeEnvKey,
86
88
  writeSession,
87
89
  writeVscodeSetup
88
- } from "./chunk-MAULRZZD.js";
90
+ } from "./chunk-LUSVIOAN.js";
89
91
  import {
90
92
  clackPrompter
91
93
  } from "./chunk-7IQIRMK6.js";
@@ -144,6 +146,8 @@ export {
144
146
  ExampleFetchError,
145
147
  FLOW_OPTIONS,
146
148
  HOME_CONFIG_FILE,
149
+ INSTALL_OPEN_QUESTION,
150
+ INSTALL_QUESTION,
147
151
  NolaApiError,
148
152
  PROVIDERS,
149
153
  PROVIDER_QUESTION,
package/dist/launch.d.ts CHANGED
@@ -5,6 +5,8 @@ import type { PackageManager } from "./package-manager.js";
5
5
  * calls; tests inject a recorder, the CLI uses `realLauncher`.
6
6
  */
7
7
  export interface Launcher {
8
+ /** Is VS Code's `code` command on PATH? Checked BEFORE the question, so a machine without VS Code is never offered it. */
9
+ hasVscode(): boolean;
8
10
  /**
9
11
  * `<pm> install` in the project dir. Its output (stdout + stderr, as it
10
12
  * arrives) goes to `onOutput` instead of the terminal, so the flow can keep
package/dist/launch.js CHANGED
@@ -65,6 +65,7 @@ export function installArgs(pm) {
65
65
  return pm === "npm" ? ["install", "--no-audit", "--no-fund"] : ["install"];
66
66
  }
67
67
  export const realLauncher = {
68
+ hasVscode: () => findOnPath("code") !== undefined,
68
69
  install: (pm, dir, onOutput) => run(pm, installArgs(pm), dir, onOutput),
69
70
  async openVscode(dir, entry) {
70
71
  const code = findOnPath("code");
package/dist/main.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  CREATE_COMMAND,
4
4
  dispatch,
5
5
  ownVersion
6
- } from "./chunk-MAULRZZD.js";
6
+ } from "./chunk-LUSVIOAN.js";
7
7
  import "./chunk-FPS2V3S4.js";
8
8
 
9
9
  // src/main.ts
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * The inference-provider menu the wizard shows right after the template
3
3
  * (2026-09-08, replacing the yes/no trial question). Static, like the
4
- * template registry: `nola` is the platform (the free trial, or a key on the
5
- * account — see `keyPath`), the three vendors write a bring-your-own config,
6
- * `none` is the offline default (the starter replays its ledger).
4
+ * template registry: `nola` is the platform, labelled "nola: dev" so the row
5
+ * reads as a mode of the project rather than a fourth vendor (the free trial, or a key on the
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
8
+ * serves literal unions and booleans only; it is listed for every template all
9
+ * the same, and `providerOptions` (flow.ts) brackets that caveat into its hint.
7
10
  */
8
- export type ProviderId = "nola" | "openai" | "anthropic" | "google" | "none";
11
+ export type ProviderId = "nola" | "openai" | "anthropic" | "google" | "typesafe" | "none";
9
12
  export interface ProviderDef {
10
13
  id: ProviderId;
11
14
  /** menu label */
package/dist/providers.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PROVIDERS = [
2
- { id: "nola", label: "Nola", hint: "25 free runs, no account or provider key needed", model: '"nola"' },
2
+ { id: "nola", label: "nola: dev", hint: "25 free hosted runs, no API key required, suited for dev experiments", model: '"nola"' },
3
3
  { id: "openai", label: "OpenAI", hint: 'openai("gpt-5-mini"), reads OPENAI_API_KEY', envVar: "OPENAI_API_KEY", model: 'openai("gpt-5-mini")' },
4
4
  {
5
5
  id: "anthropic",
@@ -9,7 +9,8 @@ export const PROVIDERS = [
9
9
  model: 'anthropic("claude-sonnet-4-5")',
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
- { id: "none", label: "Skip for now", hint: "the starter runs offline from its replay ledger; pick a model in nola.config.ts later" },
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
14
  ];
14
15
  export function providerById(id) {
15
16
  return PROVIDERS.find((p) => p.id === id);
@@ -1,14 +1,34 @@
1
1
  /**
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
- * extract-person is deliberately absent — the starter IS extract-person plus
5
- * the replay ledger.
4
+ *
5
+ * The first menu leads (in this order) and is organized BY FEATURE:
6
+ * `feature-extraction` (the default) and `function-calling` are each one
7
+ * `.tsi` file with a top-level ask — the two things Nola does —
8
+ * `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.
6
14
  */
7
15
  export interface TemplateDef {
8
16
  name: string;
9
17
  /** one-line menu description */
10
18
  label: string;
11
19
  source: "builtin" | "example";
20
+ /**
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).
24
+ */
25
+ featured?: true;
26
+ /**
27
+ * The file that IS the program — what `start` runs, launch.json's
28
+ * `program`, what VS Code opens, where the next-steps comment lands.
29
+ * Absent = the plain-TS `src/main.ts` every other template has.
30
+ */
31
+ entry?: string;
12
32
  /**
13
33
  * The vendor the template's OWN config names. The flow skips the provider
14
34
  * question for such a template (its config is the point of the example),
@@ -20,7 +40,15 @@ export interface TemplateDef {
20
40
  envVar: string;
21
41
  };
22
42
  }
43
+ /** What a template runs unless it names its own entry: the plain-TS main. */
44
+ export declare const DEFAULT_ENTRY = "src/main.ts";
23
45
  export declare const TEMPLATES: readonly TemplateDef[];
46
+ /** The first menu's templates: the builtins and the featured example, in registry order. */
47
+ export declare function featuredNames(): string[];
48
+ /** The examples behind "More examples…": every example the first menu does not carry. */
49
+ export declare function exampleNames(): string[];
24
50
  export declare function templateByName(name: string): TemplateDef | undefined;
25
51
  export declare function templateNames(): string[];
52
+ /** The template's entry file — what `start` runs, launch.json runs and VS Code opens. */
53
+ export declare function entryFile(template: string | undefined): string;
26
54
  //# sourceMappingURL=registry.d.ts.map