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/README.md CHANGED
@@ -10,17 +10,23 @@ npm start # runs offline via a committed replay ledger - no API key neede
10
10
  ```
11
11
 
12
12
  Interactively the flow asks for a name (Enter keeps the default), a template,
13
- an inference provider (Nola first — 25 free runs, no account or provider key —
14
- then OpenAI, Anthropic, Gemini, or skip), whether to set up your editor and
15
- coding agents (yes opens one list: VS Code, preselected, writes `.vscode/`
16
- with a debug config + the extension recommendation; coding-agent skills,
17
- Claude Code preselected), and — once the files are written, when an editor
18
- was chosen — whether to install dependencies and open the project in VS Code
19
- right away (on `src/main.ts`, whose opening comment lists the next steps:
20
- F5 to run, a breakpoint to debug, the recommended extension).
13
+ an inference provider (`nola: dev` first — 25 free hosted runs, no API key required, suited for dev experiments —
14
+ then OpenAI, Anthropic, Gemini, typesafe.ai — bracketed with the caveat that
15
+ it serves literal unions and booleans only — or skip), and — once the files are
16
+ written, unless `--ide none` — whether to install dependencies and open the
17
+ project in VS Code right away (just the install when VS Code's `code` command
18
+ is not on PATH; on the entry file — `src/main.tsi`, or
19
+ `src/main.ts` for `typescript-interop` — whose opening comment lists the next
20
+ steps: F5 to run, a breakpoint to debug, the recommended extension). The editor
21
+ setup (`.vscode/` with a debug config + the extension recommendation) and the
22
+ coding-agent skill (`.agents/skills/nola/`, with `.claude/skills/nola` a
23
+ symlink to it for Claude Code) are written without a question; `--ide none` and
24
+ `--agents none` opt out, `--agents claude,universal,agents-md` adds `AGENTS.md`.
21
25
 
22
- You get a typed extraction example (`src/person.tsi` + a plain-TS consumer),
23
- `nola.config.ts`, tsconfig, and a recorded replay ledger so the first run
24
- works without any API key. `npm create nola` is a short alias of this package
25
- (`create-nola`); `nola init` (from the `nola-lang` package) lays
26
- down the same starter.
26
+ You get one template per feature — `feature-extraction` (the default) is one
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`
31
+ is a short alias of this package (`create-nola`); `nola init` (from the
32
+ `nola-lang` package) lays down the same templates.
package/dist/agents.d.ts CHANGED
@@ -9,6 +9,13 @@ export declare const SKILL_AGENTS_QUESTION = "Set up coding agents? (Space toggl
9
9
  * that Cursor, Copilot, Codex, Gemini CLI and most other agents read.
10
10
  */
11
11
  export declare const SKILL_DIRS: Readonly<Record<Exclude<AgentId, "agents-md">, string>>;
12
+ /**
13
+ * The Claude Code directory is a LINK to the universal one (2026-09-18): one
14
+ * copy of the content, `.claude/skills/nola → ../../.agents/skills/nola`,
15
+ * relative so the project can move. Only when both targets are written — a
16
+ * `claude`-only install has nothing to link to and stays a copy.
17
+ */
18
+ export declare const CLAUDE_LINK_TARGET: string;
12
19
  /** Shared option list for every agents multiselect. */
13
20
  export declare const AGENT_OPTIONS: {
14
21
  value: AgentId;
@@ -30,7 +37,12 @@ export interface SkillSource {
30
37
  export declare function readSkillSource(): Promise<SkillSource>;
31
38
  /** The `## Nola` section for AGENTS.md — also the paste snippet when skipped. */
32
39
  export declare function agentsSection(s: SkillSource, version: string): string;
33
- /** The interactive preselection: both skill copies, so a plain Enter covers every skills-aware agent. */
40
+ /**
41
+ * What the scaffold writes when `--agents` is absent (the setup question is
42
+ * retired, 2026-09-18) and `nola skill install`'s preselection: both skill
43
+ * targets — the universal directory plus the Claude Code link — so every
44
+ * skills-aware agent is covered. AGENTS.md stays opt-in.
45
+ */
34
46
  export declare function defaultAgents(_targetDir: string): AgentId[];
35
47
  /** "all" | "none" | comma list of ids; unknown ids throw listing valid values. */
36
48
  export declare function parseAgentsFlag(value: string): AgentId[];
package/dist/agents.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
- import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
3
- import { dirname, join, resolve } from "node:path";
2
+ import { lstat, mkdir, readdir, readFile, realpath, rm, symlink, writeFile } from "node:fs/promises";
3
+ import { dirname, join, posix, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { ownVersion } from "./scaffold.js";
6
6
  export const AGENT_IDS = ["claude", "universal", "agents-md"];
@@ -16,6 +16,13 @@ export const SKILL_DIRS = {
16
16
  claude: ".claude/skills/nola",
17
17
  universal: ".agents/skills/nola",
18
18
  };
19
+ /**
20
+ * The Claude Code directory is a LINK to the universal one (2026-09-18): one
21
+ * copy of the content, `.claude/skills/nola → ../../.agents/skills/nola`,
22
+ * relative so the project can move. Only when both targets are written — a
23
+ * `claude`-only install has nothing to link to and stays a copy.
24
+ */
25
+ export const CLAUDE_LINK_TARGET = posix.relative(posix.dirname(SKILL_DIRS.claude), SKILL_DIRS.universal);
19
26
  /** Shared option list for every agents multiselect. */
20
27
  export const AGENT_OPTIONS = [
21
28
  { value: "claude", label: "Claude Code", hint: ".claude/skills/nola/ (the only directory Claude Code reads)" },
@@ -63,7 +70,12 @@ export function agentsSection(s, version) {
63
70
  function agentsMd(s, version) {
64
71
  return `# AGENTS.md\n\n${agentsSection(s, version)}`;
65
72
  }
66
- /** The interactive preselection: both skill copies, so a plain Enter covers every skills-aware agent. */
73
+ /**
74
+ * What the scaffold writes when `--agents` is absent (the setup question is
75
+ * retired, 2026-09-18) and `nola skill install`'s preselection: both skill
76
+ * targets — the universal directory plus the Claude Code link — so every
77
+ * skills-aware agent is covered. AGENTS.md stays opt-in.
78
+ */
67
79
  export function defaultAgents(_targetDir) {
68
80
  return ["claude", "universal"];
69
81
  }
@@ -104,6 +116,69 @@ function disposition(path, existing, version, force) {
104
116
  stale: true,
105
117
  };
106
118
  }
119
+ const untouched = (path) => ({
120
+ kind: "kept",
121
+ note: `${path} already exists and was not generated by nola — left untouched`,
122
+ stale: false,
123
+ });
124
+ /**
125
+ * Make `.claude/skills/nola` the link to `.agents/skills/nola`. What may
126
+ * already be there: the link itself (up to date); a link elsewhere or an
127
+ * unstamped copy (the user's, left alone); a stamped copy (reported, replaced
128
+ * under --force); a plain FILE holding the link text — what a checkout
129
+ * without symlink support (Windows, core.symlinks=false) makes of a committed
130
+ * link — or a dangling link, both ours to repair without --force. A directory
131
+ * symlink needs Developer Mode or admin on Windows: that failure is reported
132
+ * and the caller falls back to a copy.
133
+ */
134
+ async function linkClaudeDir(root, version, force) {
135
+ const base = SKILL_DIRS.claude;
136
+ const link = join(root, ...base.split("/"));
137
+ const existing = await lstat(link).catch(() => undefined);
138
+ if (existing?.isSymbolicLink()) {
139
+ const target = join(root, ...SKILL_DIRS.universal.split("/"));
140
+ const [resolved, wanted] = await Promise.all([
141
+ realpath(link).catch(() => undefined),
142
+ realpath(target).catch(() => undefined),
143
+ ]);
144
+ if (resolved !== undefined && resolved === wanted) {
145
+ return { kind: "kept", note: `${base} is a link to ${SKILL_DIRS.universal} — up to date`, stale: false };
146
+ }
147
+ if (resolved !== undefined)
148
+ return untouched(base);
149
+ await rm(link); // dangling: nothing behind it to keep
150
+ }
151
+ else if (existing?.isDirectory()) {
152
+ const marker = join(link, "SKILL.md");
153
+ const found = existsSync(marker) ? STAMP_RE.exec(await readFile(marker, "utf8")) : null;
154
+ if (!found)
155
+ return untouched(base);
156
+ if (!force) {
157
+ const note = found[1] === version
158
+ ? `${base} is a copy (v${version}) — re-run with --force to replace it with a link to ${SKILL_DIRS.universal}`
159
+ : `${base}/SKILL.md is stale (v${found[1]} → v${version}) — re-run with --force to replace it`;
160
+ return { kind: "kept", note, stale: true };
161
+ }
162
+ await rm(link, { recursive: true });
163
+ }
164
+ else if (existing?.isFile()) {
165
+ if ((await readFile(link, "utf8")).trim() !== CLAUDE_LINK_TARGET)
166
+ return untouched(base);
167
+ await rm(link);
168
+ }
169
+ else if (existing) {
170
+ return untouched(base);
171
+ }
172
+ await mkdir(dirname(link), { recursive: true });
173
+ try {
174
+ await symlink(CLAUDE_LINK_TARGET, link, "dir");
175
+ }
176
+ catch (err) {
177
+ const code = err.code ?? "error";
178
+ return { kind: "unlinkable", note: `could not link ${base} → ${SKILL_DIRS.universal} (${code}) — wrote a copy instead` };
179
+ }
180
+ return { kind: "linked" };
181
+ }
107
182
  /** Copy the canonical skill dir verbatim (stamp aside) into `<root>/<base>/`. */
108
183
  async function writeSkillDir(root, base, s, version) {
109
184
  const dir = join(root, ...base.split("/"));
@@ -130,20 +205,43 @@ export async function writeAgentSkills(targetDir, agents, opts = {}) {
130
205
  const removed = [];
131
206
  const skipped = [];
132
207
  let stale = false;
133
- for (const id of ["claude", "universal"]) {
134
- if (!agents.includes(id))
135
- continue;
136
- const base = SKILL_DIRS[id];
137
- const marker = join(root, ...base.split("/"), "SKILL.md");
208
+ /** A real copy at `<base>` — classified first, never blindly overwritten. */
209
+ const copy = async (base) => {
210
+ const dir = join(root, ...base.split("/"));
211
+ const marker = join(dir, "SKILL.md");
138
212
  if (existsSync(marker)) {
139
213
  const d = disposition(`${base}/SKILL.md`, await readFile(marker, "utf8"), version, force);
140
214
  if (!d.write) {
141
215
  skipped.push(d.note);
142
216
  stale ||= d.stale;
143
- continue;
217
+ return;
144
218
  }
145
219
  }
220
+ else if ((await lstat(dir).catch(() => undefined))?.isSymbolicLink()) {
221
+ await rm(dir); // a dangling link — nothing to keep, and mkdir would follow it
222
+ }
146
223
  wrote.push(...(await writeSkillDir(root, base, source, version)));
224
+ };
225
+ // universal first: it is the one real copy the claude link points at
226
+ if (agents.includes("universal"))
227
+ await copy(SKILL_DIRS.universal);
228
+ if (agents.includes("claude")) {
229
+ if (!agents.includes("universal")) {
230
+ await copy(SKILL_DIRS.claude);
231
+ }
232
+ else {
233
+ const link = await linkClaudeDir(root, version, force);
234
+ if (link.kind === "linked") {
235
+ wrote.push(`${SKILL_DIRS.claude} → ${SKILL_DIRS.universal}`);
236
+ }
237
+ else {
238
+ skipped.push(link.note);
239
+ if (link.kind === "kept")
240
+ stale ||= link.stale;
241
+ else
242
+ await copy(SKILL_DIRS.claude);
243
+ }
244
+ }
147
245
  }
148
246
  if (agents.includes("universal")) {
149
247
  // Stamped legacy adapters are ours: superseded by .agents/skills, removed under --force.
@@ -21,4 +21,24 @@ export declare const CHECKOUT_LINKED_PACKAGES: readonly string[];
21
21
  * Returns the linked package names.
22
22
  */
23
23
  export declare function linkCheckoutPackages(dir: string, root: string): Promise<string[]>;
24
+ /**
25
+ * The skipFiles glob the in-repo dogfood launch configs carry. Node resolves
26
+ * a junction to its real path, so a linked scaffold runs the runtime from
27
+ * <checkout>/packages/\*\/dist — outside node_modules, which is all the
28
+ * scaffolded launch.json skips. Left unskipped, js-debug loses F10 over the
29
+ * process's first network ask: V8's step-over of the top-level await stays a
30
+ * plain step, lands on js-debug's injected WebAssembly pause (undici compiling
31
+ * its HTTP parser for the first fetch), and js-debug resumes that pause
32
+ * without re-stepping, so the program runs to the end (VS Code trace,
33
+ * 2026-09-18). Blackboxing the dist restores the step.
34
+ */
35
+ export declare const CHECKOUT_DIST_SKIP_GLOB = "**/packages/*/dist/**";
36
+ /**
37
+ * Dev mode only, after linkCheckoutPackages: add CHECKOUT_DIST_SKIP_GLOB to
38
+ * every configuration's skipFiles in the scaffold's .vscode/launch.json.
39
+ * Returns true when the file changed; false when there is no launch.json, it
40
+ * is not plain JSON (a hand-edited one with comments is the user's), or every
41
+ * configuration already carries the glob.
42
+ */
43
+ export declare function skipCheckoutDistInLaunch(dir: string): Promise<boolean>;
24
44
  //# sourceMappingURL=checkout.d.ts.map
package/dist/checkout.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
- import { mkdir, readFile, rm, symlink } from "node:fs/promises";
2
+ import { mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  /**
@@ -62,4 +62,51 @@ export async function linkCheckoutPackages(dir, root) {
62
62
  }
63
63
  return linked;
64
64
  }
65
+ /**
66
+ * The skipFiles glob the in-repo dogfood launch configs carry. Node resolves
67
+ * a junction to its real path, so a linked scaffold runs the runtime from
68
+ * <checkout>/packages/\*\/dist — outside node_modules, which is all the
69
+ * scaffolded launch.json skips. Left unskipped, js-debug loses F10 over the
70
+ * process's first network ask: V8's step-over of the top-level await stays a
71
+ * plain step, lands on js-debug's injected WebAssembly pause (undici compiling
72
+ * its HTTP parser for the first fetch), and js-debug resumes that pause
73
+ * without re-stepping, so the program runs to the end (VS Code trace,
74
+ * 2026-09-18). Blackboxing the dist restores the step.
75
+ */
76
+ export const CHECKOUT_DIST_SKIP_GLOB = "**/packages/*/dist/**";
77
+ /**
78
+ * Dev mode only, after linkCheckoutPackages: add CHECKOUT_DIST_SKIP_GLOB to
79
+ * every configuration's skipFiles in the scaffold's .vscode/launch.json.
80
+ * Returns true when the file changed; false when there is no launch.json, it
81
+ * is not plain JSON (a hand-edited one with comments is the user's), or every
82
+ * configuration already carries the glob.
83
+ */
84
+ export async function skipCheckoutDistInLaunch(dir) {
85
+ const path = join(dir, ".vscode", "launch.json");
86
+ if (!existsSync(path))
87
+ return false;
88
+ let launch;
89
+ try {
90
+ launch = JSON.parse(await readFile(path, "utf8"));
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ if (!Array.isArray(launch.configurations))
96
+ return false;
97
+ let changed = false;
98
+ for (const config of launch.configurations) {
99
+ if (typeof config !== "object" || config === null)
100
+ continue;
101
+ const skipFiles = Array.isArray(config.skipFiles) ? config.skipFiles : [];
102
+ if (skipFiles.includes(CHECKOUT_DIST_SKIP_GLOB))
103
+ continue;
104
+ config.skipFiles = [...skipFiles, CHECKOUT_DIST_SKIP_GLOB];
105
+ changed = true;
106
+ }
107
+ if (!changed)
108
+ return false;
109
+ await writeFile(path, `${JSON.stringify(launch, null, 2)}\n`);
110
+ return true;
111
+ }
65
112
  //# sourceMappingURL=checkout.js.map