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
@@ -5,7 +5,7 @@ import {
5
5
 
6
6
  // src/providers.ts
7
7
  var PROVIDERS = [
8
- { id: "nola", label: "Nola", hint: "25 free runs, no account or provider key needed", model: '"nola"' },
8
+ { id: "nola", label: "nola: dev", hint: "25 free hosted runs, no API key required, suited for dev experiments", model: '"nola"' },
9
9
  { id: "openai", label: "OpenAI", hint: 'openai("gpt-5-mini"), reads OPENAI_API_KEY', envVar: "OPENAI_API_KEY", model: 'openai("gpt-5-mini")' },
10
10
  {
11
11
  id: "anthropic",
@@ -15,7 +15,8 @@ var PROVIDERS = [
15
15
  model: 'anthropic("claude-sonnet-4-5")'
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
- { id: "none", label: "Skip for now", hint: "the starter runs offline from its replay ledger; pick a model in nola.config.ts later" }
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
20
  ];
20
21
  function providerById(id2) {
21
22
  return PROVIDERS.find((p) => p.id === id2);
@@ -34,7 +35,7 @@ import { join as join2 } from "node:path";
34
35
 
35
36
  // src/checkout.ts
36
37
  import { existsSync } from "node:fs";
37
- import { mkdir, readFile, rm, symlink } from "node:fs/promises";
38
+ import { mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises";
38
39
  import { dirname, join } from "node:path";
39
40
  import { fileURLToPath } from "node:url";
40
41
  async function checkoutRoot() {
@@ -71,6 +72,30 @@ async function linkCheckoutPackages(dir, root) {
71
72
  }
72
73
  return linked;
73
74
  }
75
+ var CHECKOUT_DIST_SKIP_GLOB = "**/packages/*/dist/**";
76
+ async function skipCheckoutDistInLaunch(dir) {
77
+ const path = join(dir, ".vscode", "launch.json");
78
+ if (!existsSync(path)) return false;
79
+ let launch;
80
+ try {
81
+ launch = JSON.parse(await readFile(path, "utf8"));
82
+ } catch {
83
+ return false;
84
+ }
85
+ if (!Array.isArray(launch.configurations)) return false;
86
+ let changed = false;
87
+ for (const config of launch.configurations) {
88
+ if (typeof config !== "object" || config === null) continue;
89
+ const skipFiles = Array.isArray(config.skipFiles) ? config.skipFiles : [];
90
+ if (skipFiles.includes(CHECKOUT_DIST_SKIP_GLOB)) continue;
91
+ config.skipFiles = [...skipFiles, CHECKOUT_DIST_SKIP_GLOB];
92
+ changed = true;
93
+ }
94
+ if (!changed) return false;
95
+ await writeFile(path, `${JSON.stringify(launch, null, 2)}
96
+ `);
97
+ return true;
98
+ }
74
99
 
75
100
  // src/examples.ts
76
101
  var COPY_EXCLUDE = /* @__PURE__ */ new Set(["node_modules", "dist"]);
@@ -143,52 +168,83 @@ async function fetchExampleFromGitHub(name, version, fetchImpl = fetch) {
143
168
  }
144
169
 
145
170
  // src/registry.ts
171
+ var DEFAULT_ENTRY = "src/main.ts";
146
172
  var TEMPLATES = [
147
- { name: "starter", label: "typed extraction, runs offline out of the box", source: "builtin" },
173
+ { name: "feature-extraction", label: "extract typed data from context", source: "builtin", entry: "src/main.tsi" },
174
+ {
175
+ name: "function-calling",
176
+ label: "call an async function from a TypeScript file",
177
+ source: "builtin",
178
+ entry: "src/main.tsi"
179
+ },
180
+ {
181
+ name: "typescript-interop",
182
+ label: "an infer function example, imported and awaited from plain TypeScript",
183
+ source: "builtin"
184
+ },
185
+ {
186
+ name: "triage-ticket",
187
+ label: "ticket triage on a non-chat model: literal unions and booleans as typed questions",
188
+ source: "example",
189
+ featured: true,
190
+ entry: "src/main.tsi",
191
+ provider: { label: "typesafe.ai", envVar: "TYPESAFE_API_KEY" }
192
+ },
148
193
  { name: "empty", label: "nola.config + tsconfig only, bring your own code", source: "builtin" },
149
194
  { name: "file-ticket", label: "call intents: the model fills a function's arguments", source: "example" },
150
195
  { name: "extract-resume", label: "nested arrays of objects, JSDoc schema descriptions", source: "example" },
151
196
  { name: "extract-invoice", label: "same-file type references, optional fields", source: "example" },
152
197
  { name: "classify-message", label: "closed label sets: union alias, string enum, inline union", source: "example" },
153
198
  { name: "chain-of-thought", label: "two asks sharing accumulating context", source: "example" },
154
- { name: "research-notes", label: "TS control flow orchestrating nola functions", source: "example" },
155
- {
156
- name: "triage-ticket",
157
- label: "ticket triage: literal unions and booleans as typed questions",
158
- source: "example",
159
- provider: { label: "typesafe.ai", envVar: "TYPESAFE_API_KEY" }
160
- }
199
+ { name: "research-notes", label: "TS control flow orchestrating nola functions", source: "example" }
161
200
  ];
201
+ function featuredNames() {
202
+ return TEMPLATES.filter((t) => t.source === "builtin" || t.featured).map((t) => t.name);
203
+ }
204
+ function exampleNames() {
205
+ return TEMPLATES.filter((t) => t.source === "example" && !t.featured).map((t) => t.name);
206
+ }
162
207
  function templateByName(name) {
163
208
  return TEMPLATES.find((t) => t.name === name);
164
209
  }
165
210
  function templateNames() {
166
211
  return TEMPLATES.map((t) => t.name);
167
212
  }
213
+ function entryFile(template) {
214
+ return (template === void 0 ? void 0 : templateByName(template)?.entry) ?? DEFAULT_ENTRY;
215
+ }
168
216
 
169
217
  // src/scaffold.ts
170
218
  import { existsSync as existsSync3 } from "node:fs";
171
- import { mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rm as rm2, writeFile } from "node:fs/promises";
219
+ import { mkdir as mkdir2, readdir as readdir2, readFile as readFile3, rm as rm2, writeFile as writeFile2 } from "node:fs/promises";
172
220
  import { basename, join as join3, resolve } from "node:path";
173
221
  import { fileURLToPath as fileURLToPath2 } from "node:url";
174
222
  var TEMPLATES_DIR = fileURLToPath2(new URL("../templates/", import.meta.url));
175
223
  function providerConfigUrl(provider) {
176
224
  return new URL(`../templates/_providers/${provider}.config.ts`, import.meta.url);
177
225
  }
226
+ function vendorEnvVar(template, provider) {
227
+ return providerById(provider)?.envVar ?? (provider === "none" ? templateByName(template)?.provider?.envVar : void 0);
228
+ }
229
+ function envExample(envVar) {
230
+ return `# Copy to .env and fill in \u2014 \`nola run\` applies .env before evaluating nola.config.ts.
231
+ ${envVar}=
232
+ `;
233
+ }
178
234
  var RENAMES = { _gitignore: ".gitignore" };
179
235
  var GITIGNORE_URL = new URL("../templates/_gitignore", import.meta.url);
180
236
  async function withRecommendedGitignore(files) {
181
237
  if (!files.has(".gitignore")) files.set(".gitignore", await readFile3(GITIGNORE_URL, "utf8"));
182
238
  return files;
183
239
  }
184
- var SUBSTITUTED = /* @__PURE__ */ new Set(["package.json", "README.md", "main.ts"]);
240
+ var SUBSTITUTED = /* @__PURE__ */ new Set(["package.json", "README.md", "main.ts", "main.tsi"]);
185
241
  function nextStepsComment(ide, template) {
186
242
  if (ide === "vscode") {
187
- const breakpointIn = template === "starter" ? "src/person.tsi" : "your .tsi file";
243
+ const breakpointIn = templateByName(template)?.entry ? "on the `ask` line below" : template === "typescript-interop" ? "in src/person.tsi" : "in your .tsi file";
188
244
  return [
189
245
  "// Next steps in VS Code:",
190
246
  "// 1. Press F5 to run this file (.vscode/launch.json is already set up).",
191
- `// 2. Set a breakpoint in ${breakpointIn} and press F5 again to step through the ask.`,
247
+ `// 2. Set a breakpoint ${breakpointIn} and press F5 again to step through the ask.`,
192
248
  '// 3. Install the recommended "Nola" extension when VS Code offers it \u2014 IntelliSense,',
193
249
  "// go to definition and diagnostics inside .tsi files."
194
250
  ].join("\n");
@@ -203,7 +259,7 @@ var OFFLINE_ONLY = /* @__PURE__ */ new Set(["nola.replay.jsonl"]);
203
259
  var README_NOTES = {
204
260
  offline: {
205
261
  START_NOTE: "works offline, no API key needed",
206
- PROVIDER_NOTE: 'The starter 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 `src/person.tsi` 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.'
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.'
207
263
  },
208
264
  nola: {
209
265
  START_NOTE: "uses your 25 free Nola runs (key in .env)",
@@ -218,8 +274,8 @@ function readmeNotes(provider) {
218
274
  return {
219
275
  START_NOTE: `set ${def.envVar} in .env first`,
220
276
  PROVIDER_NOTE: `\`nola.config.ts\` sets \`model: ${def.model}\` \u2014 ${def.label} serves inference with the key it reads
221
- from \`${def.envVar}\`. Put \`${def.envVar}=\u2026\` in \`.env\` (git-ignored; \`nola run\` applies it) before the
222
- first \`npm start\`, or switch models in \`nola.config.ts\`.`
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\`.`
223
279
  };
224
280
  }
225
281
  async function ownVersion() {
@@ -236,12 +292,13 @@ async function prepareTarget(absRoot, force) {
236
292
  async function scaffold(targetDir, opts = {}) {
237
293
  const root = targetDir;
238
294
  const absRoot = resolve(targetDir);
239
- const template = opts.template ?? "starter";
295
+ const template = opts.template ?? "feature-extraction";
240
296
  const def = templateByName(template);
241
297
  if (!def) throw new Error(`unknown template "${template}" (valid: ${templateNames().join(", ")})`);
242
298
  await prepareTarget(absRoot, opts.force ?? false);
243
299
  const name = opts.name ?? basename(absRoot);
244
300
  const version = await ownVersion();
301
+ const envVar = vendorEnvVar(template, opts.provider ?? "none");
245
302
  if (def.source === "example") {
246
303
  const dev = await devExamplesDir();
247
304
  const exampleFiles = await withRecommendedGitignore(
@@ -249,10 +306,11 @@ async function scaffold(targetDir, opts = {}) {
249
306
  );
250
307
  const manifest = exampleFiles.get("package.json");
251
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));
252
310
  for (const [relPath, content] of exampleFiles) {
253
311
  const target = join3(absRoot, relPath);
254
312
  await mkdir2(join3(target, ".."), { recursive: true });
255
- await writeFile(target, content);
313
+ await writeFile2(target, content);
256
314
  }
257
315
  return { root, files: [...exampleFiles.keys()].sort() };
258
316
  }
@@ -278,21 +336,25 @@ async function scaffold(targetDir, opts = {}) {
278
336
  if (SUBSTITUTED.has(entry.name)) {
279
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);
280
338
  }
281
- await writeFile(join3(absRoot, toRel), content);
339
+ await writeFile2(join3(absRoot, toRel), content);
282
340
  files.push(toRel.replaceAll("\\", "/"));
283
341
  }
284
342
  };
285
343
  await copyDir(".", ".");
286
344
  if (!files.includes(".gitignore")) {
287
- await writeFile(join3(absRoot, ".gitignore"), await readFile3(GITIGNORE_URL, "utf8"));
345
+ await writeFile2(join3(absRoot, ".gitignore"), await readFile3(GITIGNORE_URL, "utf8"));
288
346
  files.push(".gitignore");
289
347
  }
348
+ if (envVar !== void 0) {
349
+ await writeFile2(join3(absRoot, ".env.example"), envExample(envVar));
350
+ files.push(".env.example");
351
+ }
290
352
  return { root, files: files.sort() };
291
353
  }
292
354
 
293
355
  // src/add.ts
294
356
  import { existsSync as existsSync4 } from "node:fs";
295
- import { readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
357
+ import { readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
296
358
  import { join as join4, resolve as resolve2 } from "node:path";
297
359
  var EMPTY_CONFIG_URL = new URL("../templates/empty/nola.config.ts", import.meta.url);
298
360
  function vendorOf(provider) {
@@ -337,7 +399,7 @@ async function addNola(targetDir, opts = {}) {
337
399
  vendor ? `nola.config.ts already exists \u2014 set \`model: ${vendor.model}\` (${vendor.id} from @nola-lang/providers) to use ${vendor.label}` : "nola.config.ts already exists \u2014 left untouched"
338
400
  );
339
401
  } else {
340
- await writeFile2(configPath, await readFile4(vendor ? providerConfigUrl(vendor.id) : EMPTY_CONFIG_URL, "utf8"));
402
+ await writeFile3(configPath, await readFile4(vendor ? providerConfigUrl(vendor.id) : EMPTY_CONFIG_URL, "utf8"));
341
403
  wrote.push("nola.config.ts");
342
404
  }
343
405
  for (const [section, name, fixedRange] of REQUIRED) {
@@ -353,7 +415,7 @@ async function addNola(targetDir, opts = {}) {
353
415
  added.push(`${name}@${range}`);
354
416
  }
355
417
  if (added.length > 0) {
356
- await writeFile2(manifestPath, `${JSON.stringify(pkg, null, 2)}
418
+ await writeFile3(manifestPath, `${JSON.stringify(pkg, null, 2)}
357
419
  `);
358
420
  wrote.push("package.json");
359
421
  }
@@ -362,8 +424,8 @@ async function addNola(targetDir, opts = {}) {
362
424
 
363
425
  // src/agents.ts
364
426
  import { existsSync as existsSync5 } from "node:fs";
365
- import { mkdir as mkdir3, readdir as readdir3, readFile as readFile5, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
366
- import { dirname as dirname2, join as join5, resolve as resolve3 } from "node:path";
427
+ import { lstat, mkdir as mkdir3, readdir as readdir3, readFile as readFile5, realpath, rm as rm3, symlink as symlink2, writeFile as writeFile4 } from "node:fs/promises";
428
+ import { dirname as dirname2, join as join5, posix, resolve as resolve3 } from "node:path";
367
429
  import { fileURLToPath as fileURLToPath3 } from "node:url";
368
430
  var AGENT_IDS = ["claude", "universal", "agents-md"];
369
431
  var SKILL_AGENTS_QUESTION = "Set up coding agents? (Space toggles, Enter confirms)";
@@ -371,6 +433,7 @@ var SKILL_DIRS = {
371
433
  claude: ".claude/skills/nola",
372
434
  universal: ".agents/skills/nola"
373
435
  };
436
+ var CLAUDE_LINK_TARGET = posix.relative(posix.dirname(SKILL_DIRS.claude), SKILL_DIRS.universal);
374
437
  var AGENT_OPTIONS = [
375
438
  { value: "claude", label: "Claude Code", hint: ".claude/skills/nola/ (the only directory Claude Code reads)" },
376
439
  {
@@ -437,15 +500,59 @@ function disposition(path, existing, version, force) {
437
500
  stale: true
438
501
  };
439
502
  }
503
+ var untouched = (path) => ({
504
+ kind: "kept",
505
+ note: `${path} already exists and was not generated by nola \u2014 left untouched`,
506
+ stale: false
507
+ });
508
+ async function linkClaudeDir(root, version, force) {
509
+ const base = SKILL_DIRS.claude;
510
+ const link = join5(root, ...base.split("/"));
511
+ const existing = await lstat(link).catch(() => void 0);
512
+ if (existing?.isSymbolicLink()) {
513
+ const target = join5(root, ...SKILL_DIRS.universal.split("/"));
514
+ const [resolved, wanted] = await Promise.all([
515
+ realpath(link).catch(() => void 0),
516
+ realpath(target).catch(() => void 0)
517
+ ]);
518
+ if (resolved !== void 0 && resolved === wanted) {
519
+ return { kind: "kept", note: `${base} is a link to ${SKILL_DIRS.universal} \u2014 up to date`, stale: false };
520
+ }
521
+ if (resolved !== void 0) return untouched(base);
522
+ await rm3(link);
523
+ } else if (existing?.isDirectory()) {
524
+ const marker = join5(link, "SKILL.md");
525
+ const found = existsSync5(marker) ? STAMP_RE.exec(await readFile5(marker, "utf8")) : null;
526
+ if (!found) return untouched(base);
527
+ if (!force) {
528
+ const note = found[1] === version ? `${base} is a copy (v${version}) \u2014 re-run with --force to replace it with a link to ${SKILL_DIRS.universal}` : `${base}/SKILL.md is stale (v${found[1]} \u2192 v${version}) \u2014 re-run with --force to replace it`;
529
+ return { kind: "kept", note, stale: true };
530
+ }
531
+ await rm3(link, { recursive: true });
532
+ } else if (existing?.isFile()) {
533
+ if ((await readFile5(link, "utf8")).trim() !== CLAUDE_LINK_TARGET) return untouched(base);
534
+ await rm3(link);
535
+ } else if (existing) {
536
+ return untouched(base);
537
+ }
538
+ await mkdir3(dirname2(link), { recursive: true });
539
+ try {
540
+ await symlink2(CLAUDE_LINK_TARGET, link, "dir");
541
+ } catch (err) {
542
+ const code = err.code ?? "error";
543
+ return { kind: "unlinkable", note: `could not link ${base} \u2192 ${SKILL_DIRS.universal} (${code}) \u2014 wrote a copy instead` };
544
+ }
545
+ return { kind: "linked" };
546
+ }
440
547
  async function writeSkillDir(root, base, s, version) {
441
548
  const dir = join5(root, ...base.split("/"));
442
549
  await mkdir3(join5(dir, "references"), { recursive: true });
443
550
  const stamped = s.full.replace(/^(---\n[\s\S]*?\n---\n)/, `$1${stamp(version)}
444
551
  `);
445
- await writeFile3(join5(dir, "SKILL.md"), stamped);
552
+ await writeFile4(join5(dir, "SKILL.md"), stamped);
446
553
  const wrote = [`${base}/SKILL.md`];
447
554
  for (const name of (await readdir3(join5(SKILLS_DIR, "references"))).sort()) {
448
- await writeFile3(join5(dir, "references", name), await readFile5(join5(SKILLS_DIR, "references", name), "utf8"));
555
+ await writeFile4(join5(dir, "references", name), await readFile5(join5(SKILLS_DIR, "references", name), "utf8"));
449
556
  wrote.push(`${base}/references/${name}`);
450
557
  }
451
558
  return wrote;
@@ -459,19 +566,35 @@ async function writeAgentSkills(targetDir, agents, opts = {}) {
459
566
  const removed = [];
460
567
  const skipped = [];
461
568
  let stale = false;
462
- for (const id2 of ["claude", "universal"]) {
463
- if (!agents.includes(id2)) continue;
464
- const base = SKILL_DIRS[id2];
465
- const marker = join5(root, ...base.split("/"), "SKILL.md");
569
+ const copy = async (base) => {
570
+ const dir = join5(root, ...base.split("/"));
571
+ const marker = join5(dir, "SKILL.md");
466
572
  if (existsSync5(marker)) {
467
573
  const d = disposition(`${base}/SKILL.md`, await readFile5(marker, "utf8"), version, force);
468
574
  if (!d.write) {
469
575
  skipped.push(d.note);
470
576
  stale ||= d.stale;
471
- continue;
577
+ return;
472
578
  }
579
+ } else if ((await lstat(dir).catch(() => void 0))?.isSymbolicLink()) {
580
+ await rm3(dir);
473
581
  }
474
582
  wrote.push(...await writeSkillDir(root, base, source, version));
583
+ };
584
+ if (agents.includes("universal")) await copy(SKILL_DIRS.universal);
585
+ if (agents.includes("claude")) {
586
+ if (!agents.includes("universal")) {
587
+ await copy(SKILL_DIRS.claude);
588
+ } else {
589
+ const link = await linkClaudeDir(root, version, force);
590
+ if (link.kind === "linked") {
591
+ wrote.push(`${SKILL_DIRS.claude} \u2192 ${SKILL_DIRS.universal}`);
592
+ } else {
593
+ skipped.push(link.note);
594
+ if (link.kind === "kept") stale ||= link.stale;
595
+ else await copy(SKILL_DIRS.claude);
596
+ }
597
+ }
475
598
  }
476
599
  if (agents.includes("universal")) {
477
600
  for (const path of LEGACY_ADAPTERS) {
@@ -496,7 +619,7 @@ ${agentsSection(source, version)}`
496
619
  );
497
620
  } else {
498
621
  await mkdir3(dirname2(abs), { recursive: true });
499
- await writeFile3(abs, agentsMd(source, version));
622
+ await writeFile4(abs, agentsMd(source, version));
500
623
  wrote.push("AGENTS.md");
501
624
  }
502
625
  }
@@ -631,7 +754,7 @@ function sessionIdOf(url) {
631
754
  }
632
755
 
633
756
  // src/credentials.ts
634
- import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
757
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile5 } from "node:fs/promises";
635
758
  import { homedir } from "node:os";
636
759
  import { join as join6 } from "node:path";
637
760
  var CREDENTIALS_FILE = "credentials.json";
@@ -681,7 +804,7 @@ async function rewrite(home, mutate) {
681
804
  const next = { ...raw, version: CREDENTIALS_VERSION, sessions };
682
805
  try {
683
806
  await mkdir4(join6(home, ".nola"), { recursive: true });
684
- await writeFile4(credentialsPath(home), `${JSON.stringify(next, null, 2)}
807
+ await writeFile5(credentialsPath(home), `${JSON.stringify(next, null, 2)}
685
808
  `, { mode: 384 });
686
809
  return "written";
687
810
  } catch {
@@ -703,7 +826,7 @@ async function deleteSession(home, apiUrl) {
703
826
  }
704
827
 
705
828
  // src/home-config.ts
706
- import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile5 } from "node:fs/promises";
829
+ import { mkdir as mkdir5, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
707
830
  import { homedir as homedir2 } from "node:os";
708
831
  import { join as join7 } from "node:path";
709
832
  var HOME_CONFIG_FILE = "config.json";
@@ -755,7 +878,7 @@ async function recordTrialAccount(home, apiUrl, record) {
755
878
  const next = { ...raw, version: HOME_CONFIG_VERSION, accounts };
756
879
  try {
757
880
  await mkdir5(join7(dir, ".nola"), { recursive: true });
758
- await writeFile5(homeConfigPath(dir), `${JSON.stringify(next, null, 2)}
881
+ await writeFile6(homeConfigPath(dir), `${JSON.stringify(next, null, 2)}
759
882
  `);
760
883
  return "written";
761
884
  } catch {
@@ -1187,19 +1310,18 @@ ${renderCommandHelp(cmd, meta.name, p)}`);
1187
1310
 
1188
1311
  // src/ide.ts
1189
1312
  import { existsSync as existsSync6 } from "node:fs";
1190
- import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
1313
+ import { mkdir as mkdir6, writeFile as writeFile7 } from "node:fs/promises";
1191
1314
  import { join as join8, resolve as resolve5 } from "node:path";
1192
- var LAUNCH_JSON = {
1315
+ var launchJson = (entry) => ({
1193
1316
  version: "0.2.0",
1194
1317
  configurations: [
1195
1318
  {
1196
1319
  type: "node",
1197
1320
  request: "launch",
1198
1321
  name: "Nola: Launch main",
1199
- // ${workspaceFolder} below is VS Code's own launch-config variable —
1200
- // deliberately a literal string, never a JS template.
1201
- // biome-ignore lint/suspicious/noTemplateCurlyInString: VS Code variable syntax
1202
- program: "${workspaceFolder}/src/main.ts",
1322
+ // ${workspaceFolder} is VS Code's own launch-config variable — it must
1323
+ // reach the file verbatim, hence the escaped dollar.
1324
+ program: `\${workspaceFolder}/${entry}`,
1203
1325
  runtimeArgs: ["--import", "nola-lang/register", "--enable-source-maps"],
1204
1326
  console: "integratedTerminal",
1205
1327
  // biome-ignore lint/suspicious/noTemplateCurlyInString: VS Code variable syntax
@@ -1209,15 +1331,15 @@ var LAUNCH_JSON = {
1209
1331
  skipFiles: ["<node_internals>/**", "**/node_modules/**"]
1210
1332
  }
1211
1333
  ]
1212
- };
1334
+ });
1213
1335
  var EXTENSIONS_JSON = { recommendations: ["nola.nola-vscode"] };
1214
- async function writeVscodeSetup(targetDir) {
1336
+ async function writeVscodeSetup(targetDir, entry = DEFAULT_ENTRY) {
1215
1337
  const vscodeDir = join8(resolve5(targetDir), ".vscode");
1216
1338
  await mkdir6(vscodeDir, { recursive: true });
1217
1339
  const wrote = [];
1218
1340
  const skipped = [];
1219
1341
  const files = [
1220
- ["launch.json", LAUNCH_JSON],
1342
+ ["launch.json", launchJson(entry)],
1221
1343
  ["extensions.json", EXTENSIONS_JSON]
1222
1344
  ];
1223
1345
  for (const [name, content] of files) {
@@ -1227,7 +1349,7 @@ async function writeVscodeSetup(targetDir) {
1227
1349
  skipped.push(`${rel} already exists \u2014 left untouched`);
1228
1350
  continue;
1229
1351
  }
1230
- await writeFile6(path, `${JSON.stringify(content, null, 2)}
1352
+ await writeFile7(path, `${JSON.stringify(content, null, 2)}
1231
1353
  `);
1232
1354
  wrote.push(rel);
1233
1355
  }
@@ -1236,7 +1358,7 @@ async function writeVscodeSetup(targetDir) {
1236
1358
 
1237
1359
  // src/trial.ts
1238
1360
  import { existsSync as existsSync7 } from "node:fs";
1239
- import { appendFile, readFile as readFile8, writeFile as writeFile7 } from "node:fs/promises";
1361
+ import { appendFile, readFile as readFile8, writeFile as writeFile8 } from "node:fs/promises";
1240
1362
  import { join as join9, resolve as resolve6 } from "node:path";
1241
1363
  var TRIAL_CONFIG_URL = new URL("../templates/_providers/nola.config.ts", import.meta.url);
1242
1364
  var ENV_KEY = "NOLA_API_KEY";
@@ -1263,7 +1385,7 @@ async function writeEnvKey(dir, apiKey, opts = {}) {
1263
1385
  const current = await readFile8(envPath, "utf8");
1264
1386
  if (ENV_KEY_LINE.test(current)) {
1265
1387
  if (opts.replace) {
1266
- await writeFile7(envPath, current.replace(new RegExp(`^(\\s*(?:export\\s+)?${ENV_KEY}\\s*=).*$`, "gm"), `$1${apiKey}`));
1388
+ await writeFile8(envPath, current.replace(new RegExp(`^(\\s*(?:export\\s+)?${ENV_KEY}\\s*=).*$`, "gm"), `$1${apiKey}`));
1267
1389
  wrote.push(".env");
1268
1390
  } else {
1269
1391
  skipped.push(`.env already sets ${ENV_KEY} \u2014 left untouched; the new trial key was not written`);
@@ -1275,7 +1397,7 @@ async function writeEnvKey(dir, apiKey, opts = {}) {
1275
1397
  wrote.push(".env");
1276
1398
  }
1277
1399
  } else {
1278
- await writeFile7(envPath, `${ENV_KEY}=${apiKey}
1400
+ await writeFile8(envPath, `${ENV_KEY}=${apiKey}
1279
1401
  `);
1280
1402
  wrote.push(".env");
1281
1403
  }
@@ -1288,7 +1410,7 @@ async function ensureEnvIgnored(dir) {
1288
1410
  const missing = IGNORE_LINES.filter((line) => !present.has(line));
1289
1411
  if (missing.length === 0) return [];
1290
1412
  const lead = existing === "" ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
1291
- await writeFile7(ignorePath, `${existing}${lead}${missing.join("\n")}
1413
+ await writeFile8(ignorePath, `${existing}${lead}${missing.join("\n")}
1292
1414
  `);
1293
1415
  return [".gitignore"];
1294
1416
  }
@@ -1299,7 +1421,7 @@ async function applyTrial(dir, init) {
1299
1421
  if (init.hasConfig) {
1300
1422
  skipped.push('nola.config.ts already exists \u2014 set `model: "nola"` to use the trial key');
1301
1423
  } else {
1302
- await writeFile7(join9(root, "nola.config.ts"), await readFile8(TRIAL_CONFIG_URL, "utf8"));
1424
+ await writeFile8(join9(root, "nola.config.ts"), await readFile8(TRIAL_CONFIG_URL, "utf8"));
1303
1425
  wrote.push("nola.config.ts");
1304
1426
  }
1305
1427
  return { wrote, skipped };
@@ -1411,6 +1533,7 @@ function installArgs(pm) {
1411
1533
  return pm === "npm" ? ["install", "--no-audit", "--no-fund"] : ["install"];
1412
1534
  }
1413
1535
  var realLauncher = {
1536
+ hasVscode: () => findOnPath("code") !== void 0,
1414
1537
  install: (pm, dir, onOutput) => run(pm, installArgs(pm), dir, onOutput),
1415
1538
  async openVscode(dir, entry) {
1416
1539
  const code = findOnPath("code");
@@ -1472,8 +1595,10 @@ function packageManagerCommands(pm) {
1472
1595
 
1473
1596
  // src/flow.ts
1474
1597
  var DEFAULT_DIR = "nola-app";
1475
- var DEFAULT_TEMPLATE = "starter";
1598
+ var DEFAULT_TEMPLATE = "feature-extraction";
1476
1599
  var NAME_QUESTION = `Project name (press Enter to keep "${DEFAULT_DIR}"):`;
1600
+ var SETUP_STEP_ASKED = false;
1601
+ var DEFAULT_IDE = "vscode";
1477
1602
  var SETUP_QUESTION = "Set up your Editor and Coding Agents?";
1478
1603
  var EDITOR_QUESTION = "Set up your Editor?";
1479
1604
  var AGENTS_QUESTION = "Set up Coding Agents?";
@@ -1483,13 +1608,36 @@ var EDITOR_OPTIONS = [
1483
1608
  ];
1484
1609
  function templateOption(t) {
1485
1610
  const vendor = t.provider ? ` (${t.provider.label})` : "";
1486
- return { value: t.name, label: t.source === "example" ? `example: ${t.name}${vendor}` : `${t.name}${vendor}`, hint: t.label };
1611
+ return { value: t.name, label: `${t.name}${vendor}`, hint: t.label };
1612
+ }
1613
+ var TEMPLATE_QUESTION = "Select a template:";
1614
+ var EXAMPLE_QUESTION = "Select an example:";
1615
+ var MORE_EXAMPLES = "more-examples";
1616
+ var BACK = "back";
1617
+ var builtinNames = () => TEMPLATES.filter((t) => t.source === "builtin").map((t) => `"${t.name}"`).join(", ");
1618
+ function templateMenu() {
1619
+ const featured = featuredNames().map((name) => templateOption(templateByName(name)));
1620
+ return [...featured, { value: MORE_EXAMPLES, label: "More examples\u2026", hint: exampleNames().join(", ") }];
1621
+ }
1622
+ function exampleMenu() {
1623
+ const examples = exampleNames().map((name) => templateOption(templateByName(name)));
1624
+ return [...examples, { value: BACK, label: "\u2190 Back", hint: "the builtin templates" }];
1625
+ }
1626
+ async function selectTemplate(prompter) {
1627
+ for (; ; ) {
1628
+ const choice = await prompter.select(TEMPLATE_QUESTION, templateMenu());
1629
+ if (choice === null) return null;
1630
+ if (choice !== MORE_EXAMPLES) return choice;
1631
+ const example = await prompter.select(EXAMPLE_QUESTION, exampleMenu());
1632
+ if (example === null) return null;
1633
+ if (example !== BACK) return example;
1634
+ }
1487
1635
  }
1488
1636
  async function resolveSetup(input, prompter, dir) {
1489
1637
  const ide = input.ide === "vscode" || input.ide === "none" ? input.ide : void 0;
1490
1638
  const agents = input.agents !== void 0 ? parseAgentsFlag(input.agents) : void 0;
1491
1639
  if (ide !== void 0 && agents !== void 0) return { ide, agents };
1492
- if (!input.interactive) return { ide: ide ?? "none", agents: agents ?? [] };
1640
+ if (!input.interactive || !SETUP_STEP_ASKED) return { ide: ide ?? DEFAULT_IDE, agents: agents ?? defaultAgents(dir) };
1493
1641
  const groups = [];
1494
1642
  const initial = [];
1495
1643
  if (ide === void 0) {
@@ -1523,8 +1671,17 @@ function nolaHint(path) {
1523
1671
  return providerById("nola")?.hint ?? "";
1524
1672
  }
1525
1673
  }
1526
- function providerOptions(path) {
1527
- return PROVIDERS.map((p) => ({ value: p.id, label: p.label, hint: p.id === "nola" ? nolaHint(path) : p.hint }));
1674
+ function typesafeHint(base, template) {
1675
+ if (template === void 0) return `${base} (literal unions and booleans only)`;
1676
+ if (templateByName(template)?.provider?.label === "typesafe.ai") return base;
1677
+ return `${base} (does not support every construct in ${template}: literal unions and booleans only)`;
1678
+ }
1679
+ function providerOptions(path, template) {
1680
+ return PROVIDERS.map((p) => ({
1681
+ value: p.id,
1682
+ label: p.label,
1683
+ hint: p.id === "nola" ? nolaHint(path) : p.id === "typesafe" ? typesafeHint(p.hint, template) : p.hint
1684
+ }));
1528
1685
  }
1529
1686
  function flaggedProvider(input) {
1530
1687
  const fromTrial = input.trial === void 0 ? void 0 : input.trial ? "nola" : "none";
@@ -1537,21 +1694,22 @@ function flaggedProvider(input) {
1537
1694
  }
1538
1695
  return input.provider;
1539
1696
  }
1540
- async function resolveProvider(input, prompter, pinned = false) {
1697
+ async function resolveProvider(input, prompter, template) {
1541
1698
  const flagged = flaggedProvider(input);
1542
1699
  if (flagged !== void 0) return flagged;
1700
+ const pinned = template !== void 0 && templateByName(template)?.provider !== void 0;
1543
1701
  if (pinned || !input.interactive) return "none";
1544
1702
  const path = input.keyPath ?? { kind: "trial" };
1545
1703
  while (true) {
1546
- const choice = await prompter.select(PROVIDER_QUESTION, providerOptions(path));
1704
+ const choice = await prompter.select(PROVIDER_QUESTION, providerOptions(path, template));
1547
1705
  if (choice === null) return null;
1548
1706
  if (!isProviderId(choice)) throw new Error(`unexpected provider choice "${choice}"`);
1549
1707
  if (choice === "nola" && path.kind === "sign-in" && input.signIn && !await input.signIn()) continue;
1550
1708
  return choice;
1551
1709
  }
1552
1710
  }
1553
- async function resolveExtras(input, prompter, dir, pinnedProvider = false) {
1554
- const provider = await resolveProvider(input, prompter, pinnedProvider);
1711
+ async function resolveExtras(input, prompter, dir, template) {
1712
+ const provider = await resolveProvider(input, prompter, template);
1555
1713
  if (provider === null) return null;
1556
1714
  const setup = await resolveSetup(input, prompter, dir);
1557
1715
  return setup === null ? null : { provider, ...setup };
@@ -1643,12 +1801,12 @@ async function resolveScaffoldOptions(input, prompter) {
1643
1801
  if (!input.interactive) {
1644
1802
  template = DEFAULT_TEMPLATE;
1645
1803
  } else {
1646
- const choice = await prompter.select("Select a template:", TEMPLATES.map(templateOption));
1804
+ const choice = await selectTemplate(prompter);
1647
1805
  if (choice === null) return { kind: "cancelled" };
1648
1806
  template = choice;
1649
1807
  }
1650
1808
  }
1651
- const extras = await resolveExtras(input, prompter, dir, templateByName(template)?.provider !== void 0);
1809
+ const extras = await resolveExtras(input, prompter, dir, template);
1652
1810
  if (extras === null) return { kind: "cancelled" };
1653
1811
  return { kind: "scaffold", dir, template, force, ...extras };
1654
1812
  }
@@ -1672,10 +1830,16 @@ function plainPrompter() {
1672
1830
  };
1673
1831
  }
1674
1832
  var FLOW_OPTIONS = {
1675
- template: { type: "string", description: "template to scaffold (starter, empty, or a curated example)" },
1833
+ template: {
1834
+ type: "string",
1835
+ description: "template to scaffold (feature-extraction, function-calling, typescript-interop, empty, or a curated example)"
1836
+ },
1676
1837
  add: { type: "boolean", description: "add Nola to the existing project in [dir] instead of scaffolding" },
1677
- ide: { type: "string", description: "editor setup: vscode | none" },
1678
- agents: { type: "string", description: `agent skill files: ${AGENT_IDS.join(",")} | all | none` },
1838
+ ide: { type: "string", description: "editor setup: vscode (the default) | none" },
1839
+ agents: {
1840
+ type: "string",
1841
+ description: `agent skill files: ${AGENT_IDS.join(",")} | all | none (default: claude,universal)`
1842
+ },
1679
1843
  provider: {
1680
1844
  type: "string",
1681
1845
  description: `inference provider: ${providerIds().join(" | ")} (nola = 25 free runs; non-interactive default: none)`
@@ -1697,7 +1861,7 @@ function commented(command, note) {
1697
1861
  }
1698
1862
  function nextSteps(outcome, fileCount, name, pm, installed = false, grant = null) {
1699
1863
  const cmd = packageManagerCommands(pm);
1700
- const vendorEnv = providerById(outcome.provider)?.envVar ?? (outcome.provider === "none" ? templateByName(outcome.template)?.provider?.envVar : void 0);
1864
+ const vendorEnv = vendorEnvVar(outcome.template, outcome.provider);
1701
1865
  const startNote = grant ? grant.source === "trial" ? "25 free Nola runs \u2014 key in .env" : "key in .env (run `npx nola-lang account` to check the balance)" : vendorEnv ? `set ${vendorEnv} in .env first` : outcome.template === "empty" ? "set OPENAI_API_KEY first" : "runs offline \u2014 no API key needed";
1702
1866
  const lines = [
1703
1867
  ...outcome.dir === "." ? [] : [`cd ${outcome.dir}`],
@@ -1711,7 +1875,6 @@ Next steps:
1711
1875
  ${lines.join("\n ")}`;
1712
1876
  }
1713
1877
  var INSTALL_LOG_TAIL = 20;
1714
- var ENTRY_FILE = "src/main.ts";
1715
1878
  function lastOutputLine(output) {
1716
1879
  const lines = stripVTControlCharacters(output).split(/\r?\n/);
1717
1880
  for (let i = lines.length - 1; i >= 0; i--) {
@@ -1720,8 +1883,11 @@ function lastOutputLine(output) {
1720
1883
  }
1721
1884
  return void 0;
1722
1885
  }
1723
- async function offerInstallAndOpen(dir, pm, prompter, launcher, linkCheckout) {
1724
- const yes = await prompter.confirm("Install dependencies and open VS Code?", true);
1886
+ var INSTALL_OPEN_QUESTION = "Install dependencies and open VS Code?";
1887
+ var INSTALL_QUESTION = "Install dependencies?";
1888
+ async function offerInstallAndOpen(dir, entryPath, pm, prompter, launcher, linkCheckout) {
1889
+ const vscode = launcher.hasVscode();
1890
+ const yes = await prompter.confirm(vscode ? INSTALL_OPEN_QUESTION : INSTALL_QUESTION, true);
1725
1891
  if (!yes) return { installed: false };
1726
1892
  const cmd = packageManagerCommands(pm);
1727
1893
  const task = prompter.progress(`Installing dependencies (${cmd.install})`);
@@ -1736,8 +1902,10 @@ async function offerInstallAndOpen(dir, pm, prompter, launcher, linkCheckout) {
1736
1902
  task.done(`Installed dependencies (${cmd.install})`);
1737
1903
  if (linkCheckout !== null) {
1738
1904
  const linked = await linkCheckoutPackages(dir, linkCheckout);
1905
+ const skipped = await skipCheckoutDistInLaunch(dir);
1739
1906
  prompter.note(
1740
- `${LINK_CHECKOUT_ENV}: linked ${linked.join(", ")} to ${join11(linkCheckout, "packages")} \u2014 a later ${cmd.install} restores the registry copies.`
1907
+ `${LINK_CHECKOUT_ENV}: linked ${linked.join(", ")} to ${join11(linkCheckout, "packages")} \u2014 a later ${cmd.install} restores the registry copies.` + (skipped ? `
1908
+ Added "${CHECKOUT_DIST_SKIP_GLOB}" to skipFiles in .vscode/launch.json so stepping stays in your code.` : "")
1741
1909
  );
1742
1910
  }
1743
1911
  } else {
@@ -1746,7 +1914,8 @@ async function offerInstallAndOpen(dir, pm, prompter, launcher, linkCheckout) {
1746
1914
  prompter.note(`${tail ? `${tail}
1747
1915
  ` : ""}Run ${cmd.install} yourself once the cause is fixed.`);
1748
1916
  }
1749
- const entry = existsSync9(join11(dir, ENTRY_FILE)) ? ENTRY_FILE : void 0;
1917
+ if (!vscode) return { installed };
1918
+ const entry = existsSync9(join11(dir, entryPath)) ? entryPath : void 0;
1750
1919
  const opened = await launcher.openVscode(dir, entry);
1751
1920
  if (opened === "not-found") {
1752
1921
  prompter.note(
@@ -1876,7 +2045,7 @@ Next steps:
1876
2045
  }
1877
2046
  let ideWrote = [];
1878
2047
  if (outcome.ide === "vscode") {
1879
- const ide = await writeVscodeSetup(outcome.dir);
2048
+ const ide = await writeVscodeSetup(outcome.dir, entryFile(outcome.template));
1880
2049
  ideWrote = ide.wrote;
1881
2050
  for (const note of ide.skipped) prompter.note(note);
1882
2051
  }
@@ -1886,7 +2055,7 @@ Next steps:
1886
2055
  agWrote = ag.wrote;
1887
2056
  for (const note of ag.skipped) prompter.note(note);
1888
2057
  }
1889
- const launch = interactive && outcome.ide !== "none" ? await offerInstallAndOpen(outcome.dir, pm, prompter, opts.launcher ?? realLauncher, opts.linkCheckout ?? linkCheckoutFromEnv()) : { installed: false };
2058
+ const launch = interactive && outcome.ide !== "none" ? await offerInstallAndOpen(outcome.dir, entryFile(outcome.template), pm, prompter, opts.launcher ?? realLauncher, opts.linkCheckout ?? linkCheckoutFromEnv()) : { installed: false };
1890
2059
  const message = nextSteps(
1891
2060
  { ...outcome, provider },
1892
2061
  files.length + trialWrote.length + ideWrote.length + agWrote.length,
@@ -1901,7 +2070,7 @@ Next steps:
1901
2070
  } catch (err) {
1902
2071
  if (err instanceof ExampleFetchError && interactive) {
1903
2072
  prompter.note(`${err.message}
1904
- The "starter" and "empty" templates work offline.`);
2073
+ The builtin templates (${builtinNames()}) work offline.`);
1905
2074
  input.dir = outcome.dir;
1906
2075
  input.template = void 0;
1907
2076
  continue;
@@ -1997,5 +2166,7 @@ export {
1997
2166
  plainPrompter,
1998
2167
  FLOW_OPTIONS,
1999
2168
  CREATE_COMMAND,
2169
+ INSTALL_OPEN_QUESTION,
2170
+ INSTALL_QUESTION,
2000
2171
  runFlow
2001
2172
  };