create-nola-lang 0.1.7 → 0.1.9

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 (70) hide show
  1. package/dist/agents.d.ts +16 -10
  2. package/dist/agents.js +72 -80
  3. package/dist/{chunk-34XSPT6F.js → chunk-KCGD4VJM.js} +73 -85
  4. package/dist/flow.js +17 -9
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +3 -4
  7. package/dist/main.js +1 -1
  8. package/dist/registry.d.ts +10 -0
  9. package/dist/registry.js +6 -0
  10. package/dist/skill-install.js +4 -2
  11. package/package.json +2 -1
  12. package/skills/nola/SKILL.md +19 -3
  13. package/skills/nola/references/config.md +11 -4
  14. package/skills/nola/references/patterns.md +13 -1
  15. package/skills/nola/references/pitfalls.md +12 -10
  16. package/skills/nola/references/syntax.md +80 -7
  17. package/dist/add.d.ts.map +0 -1
  18. package/dist/add.js.map +0 -1
  19. package/dist/agents.d.ts.map +0 -1
  20. package/dist/agents.js.map +0 -1
  21. package/dist/api.d.ts.map +0 -1
  22. package/dist/api.js.map +0 -1
  23. package/dist/auth.d.ts.map +0 -1
  24. package/dist/auth.js.map +0 -1
  25. package/dist/checkout.d.ts.map +0 -1
  26. package/dist/checkout.js.map +0 -1
  27. package/dist/cli.d.ts.map +0 -1
  28. package/dist/cli.js.map +0 -1
  29. package/dist/credentials.d.ts.map +0 -1
  30. package/dist/credentials.js.map +0 -1
  31. package/dist/examples.d.ts.map +0 -1
  32. package/dist/examples.js.map +0 -1
  33. package/dist/flow.d.ts.map +0 -1
  34. package/dist/flow.js.map +0 -1
  35. package/dist/github.d.ts.map +0 -1
  36. package/dist/github.js.map +0 -1
  37. package/dist/home-config.d.ts.map +0 -1
  38. package/dist/home-config.js.map +0 -1
  39. package/dist/ide.d.ts.map +0 -1
  40. package/dist/ide.js.map +0 -1
  41. package/dist/index.d.ts.map +0 -1
  42. package/dist/index.js.map +0 -1
  43. package/dist/key.d.ts.map +0 -1
  44. package/dist/key.js.map +0 -1
  45. package/dist/launch.d.ts.map +0 -1
  46. package/dist/launch.js.map +0 -1
  47. package/dist/main.d.ts.map +0 -1
  48. package/dist/main.js.map +0 -1
  49. package/dist/node-version.d.ts.map +0 -1
  50. package/dist/node-version.js.map +0 -1
  51. package/dist/open-url.d.ts.map +0 -1
  52. package/dist/open-url.js.map +0 -1
  53. package/dist/package-manager.d.ts.map +0 -1
  54. package/dist/package-manager.js.map +0 -1
  55. package/dist/palette.d.ts.map +0 -1
  56. package/dist/palette.js.map +0 -1
  57. package/dist/prompter.d.ts.map +0 -1
  58. package/dist/prompter.js.map +0 -1
  59. package/dist/providers.d.ts.map +0 -1
  60. package/dist/providers.js.map +0 -1
  61. package/dist/registry.d.ts.map +0 -1
  62. package/dist/registry.js.map +0 -1
  63. package/dist/scaffold.d.ts.map +0 -1
  64. package/dist/scaffold.js.map +0 -1
  65. package/dist/skill-install.d.ts.map +0 -1
  66. package/dist/skill-install.js.map +0 -1
  67. package/dist/style.d.ts.map +0 -1
  68. package/dist/style.js.map +0 -1
  69. package/dist/trial.d.ts.map +0 -1
  70. package/dist/trial.js.map +0 -1
package/dist/agents.d.ts CHANGED
@@ -1,7 +1,14 @@
1
- export type AgentId = "claude" | "cursor" | "copilot" | "agents-md";
1
+ export type AgentId = "claude" | "universal" | "agents-md";
2
2
  export declare const AGENT_IDS: readonly AgentId[];
3
3
  /** `nola skill install`'s agents multiselect (the scaffold asks its own gated list, see flow.ts). */
4
4
  export declare const SKILL_AGENTS_QUESTION = "Set up coding agents? (Space toggles, Enter confirms)";
5
+ /**
6
+ * Where each skill target writes the canonical directory (project-relative
7
+ * posix). Claude Code reads only `.claude/skills/`; `.agents/skills/` is the
8
+ * open Agent Skills location — "universal" in the skills CLI's vocabulary —
9
+ * that Cursor, Copilot, Codex, Gemini CLI and most other agents read.
10
+ */
11
+ export declare const SKILL_DIRS: Readonly<Record<Exclude<AgentId, "agents-md">, string>>;
5
12
  /** Shared option list for every agents multiselect. */
6
13
  export declare const AGENT_OPTIONS: {
7
14
  value: AgentId;
@@ -9,30 +16,29 @@ export declare const AGENT_OPTIONS: {
9
16
  hint: string;
10
17
  }[];
11
18
  /**
12
- * Adapters that inject context inline embed SKILL.md's BODY verbatim, so no
13
- * adapter restates the language rules in its own words and the four can
14
- * never drift from each other (spec §2).
19
+ * The AGENTS.md section embeds SKILL.md's BODY verbatim, so it can never
20
+ * drift from the skill directory (spec §2).
15
21
  */
16
22
  export interface SkillSource {
17
23
  /** the frontmatter block of SKILL.md, without the --- fences */
18
24
  frontmatter: string;
19
25
  /** the first heading up to (not including) `## References` */
20
26
  body: string;
21
- /** the whole file, verbatim — what the claude adapter copies */
27
+ /** the whole file, verbatim — what the skill directories copy */
22
28
  full: string;
23
29
  }
24
30
  export declare function readSkillSource(): Promise<SkillSource>;
25
31
  /** The `## Nola` section for AGENTS.md — also the paste snippet when skipped. */
26
32
  export declare function agentsSection(s: SkillSource, version: string): string;
27
- /** Agents whose tooling is already present in the project. */
28
- export declare function detectAgents(targetDir: string): AgentId[];
29
- /** The interactive preselection: detected agents + Claude Code (the wizard's default agent — deduped when detected). */
30
- export declare function defaultAgents(targetDir: string): AgentId[];
33
+ /** The interactive preselection: both skill copies, so a plain Enter covers every skills-aware agent. */
34
+ export declare function defaultAgents(_targetDir: string): AgentId[];
31
35
  /** "all" | "none" | comma list of ids; unknown ids throw listing valid values. */
32
36
  export declare function parseAgentsFlag(value: string): AgentId[];
33
37
  export interface AgentSetupResult {
34
38
  /** project-relative posix paths written */
35
39
  wrote: string[];
40
+ /** superseded stamped adapters deleted under --force (project-relative posix paths) */
41
+ removed: string[];
36
42
  /** human-readable notes about files left untouched */
37
43
  skipped: string[];
38
44
  /** true when something was left stale — the caller can suggest --force */
@@ -43,7 +49,7 @@ export interface WriteAgentSkillsOptions {
43
49
  force?: boolean;
44
50
  }
45
51
  /**
46
- * Write the selected adapters as self-contained, version-stamped content.
52
+ * Write the selected targets as self-contained, version-stamped content.
47
53
  * Existing files are classified, never blindly overwritten (spec §3).
48
54
  */
49
55
  export declare function writeAgentSkills(targetDir: string, agents: AgentId[], opts?: WriteAgentSkillsOptions): Promise<AgentSetupResult>;
package/dist/agents.js CHANGED
@@ -1,17 +1,30 @@
1
1
  import { existsSync } from "node:fs";
2
- import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
+ import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { ownVersion } from "./scaffold.js";
6
- export const AGENT_IDS = ["claude", "cursor", "copilot", "agents-md"];
6
+ export const AGENT_IDS = ["claude", "universal", "agents-md"];
7
7
  /** `nola skill install`'s agents multiselect (the scaffold asks its own gated list, see flow.ts). */
8
8
  export const SKILL_AGENTS_QUESTION = "Set up coding agents? (Space toggles, Enter confirms)";
9
+ /**
10
+ * Where each skill target writes the canonical directory (project-relative
11
+ * posix). Claude Code reads only `.claude/skills/`; `.agents/skills/` is the
12
+ * open Agent Skills location — "universal" in the skills CLI's vocabulary —
13
+ * that Cursor, Copilot, Codex, Gemini CLI and most other agents read.
14
+ */
15
+ export const SKILL_DIRS = {
16
+ claude: ".claude/skills/nola",
17
+ universal: ".agents/skills/nola",
18
+ };
9
19
  /** Shared option list for every agents multiselect. */
10
20
  export const AGENT_OPTIONS = [
11
- { value: "agents-md", label: "AGENTS.md", hint: "cross-agent instructions (Codex, Gemini CLI, …)" },
12
- { value: "claude", label: "Claude Code", hint: ".claude/skills/nola/ (full skill + references)" },
13
- { value: "cursor", label: "Cursor", hint: ".cursor/rules/nola.mdc" },
14
- { value: "copilot", label: "GitHub Copilot", hint: ".github/instructions/nola.instructions.md" },
21
+ { value: "claude", label: "Claude Code", hint: ".claude/skills/nola/ (the only directory Claude Code reads)" },
22
+ {
23
+ value: "universal",
24
+ label: "Cursor, Copilot, Codex, Gemini CLI, …",
25
+ hint: ".agents/skills/nola/ (the open Agent Skills location)",
26
+ },
27
+ { value: "agents-md", label: "AGENTS.md", hint: "the skill body inline, for agents that read only AGENTS.md" },
15
28
  ];
16
29
  /**
17
30
  * The canonical skill content, resolved relative to this module — `src/` in
@@ -22,6 +35,11 @@ const SKILLS_DIR = fileURLToPath(new URL("../skills/nola/", import.meta.url));
22
35
  /** Marks a generated file so staleness is detectable (spec §3). */
23
36
  const STAMP_RE = /^<!-- nola-skill v(\S+)/m;
24
37
  const stamp = (version) => `<!-- nola-skill v${version} — regenerate with: nola skill install --force -->`;
38
+ /**
39
+ * Adapters written by 0.1.0–0.1.7 that the `.agents/skills` layout replaced
40
+ * (spec revision 2026-09-15). Stamped ones are ours to remove under --force.
41
+ */
42
+ const LEGACY_ADAPTERS = [".cursor/rules/nola.mdc", ".github/instructions/nola.instructions.md"];
25
43
  export async function readSkillSource() {
26
44
  const full = (await readFile(join(SKILLS_DIR, "SKILL.md"), "utf8")).replaceAll("\r\n", "\n");
27
45
  const fm = /^---\n([\s\S]*?)\n---\n/.exec(full);
@@ -32,30 +50,12 @@ export async function readSkillSource() {
32
50
  const body = (refsAt === -1 ? afterFm : afterFm.slice(0, refsAt)).trim();
33
51
  return { frontmatter: fm[1], body, full };
34
52
  }
35
- /** Points at the project-local copy — never node_modules (spec §1). */
53
+ /** Points at the project-local copies — never node_modules (spec §1). */
36
54
  const LOCAL_REFERENCES = [
37
55
  "Deeper references, when this repo has them:",
38
- "`.claude/skills/nola/references/` — syntax.md, patterns.md, config.md,",
39
- "pitfalls.md.",
56
+ "`.agents/skills/nola/references/` or `.claude/skills/nola/references/` —",
57
+ "syntax.md, patterns.md, config.md, pitfalls.md.",
40
58
  ].join("\n");
41
- function cursorRule(s, version) {
42
- return [
43
- "---",
44
- "description: Nola (.tsi) language rules",
45
- 'globs: ["**/*.tsi", "nola.config.ts"]',
46
- "alwaysApply: false",
47
- "---",
48
- stamp(version),
49
- "",
50
- s.body,
51
- "",
52
- LOCAL_REFERENCES,
53
- "",
54
- ].join("\n");
55
- }
56
- function copilotInstructions(s, version) {
57
- return ["---", 'applyTo: "**/*.tsi"', "---", stamp(version), "", s.body, "", LOCAL_REFERENCES, ""].join("\n");
58
- }
59
59
  /** The `## Nola` section for AGENTS.md — also the paste snippet when skipped. */
60
60
  export function agentsSection(s, version) {
61
61
  return ["## Nola", "", stamp(version), "", s.body, "", LOCAL_REFERENCES, ""].join("\n");
@@ -63,21 +63,9 @@ export function agentsSection(s, version) {
63
63
  function agentsMd(s, version) {
64
64
  return `# AGENTS.md\n\n${agentsSection(s, version)}`;
65
65
  }
66
- /** Agents whose tooling is already present in the project. */
67
- export function detectAgents(targetDir) {
68
- const root = resolve(targetDir);
69
- const found = [];
70
- if (existsSync(join(root, ".claude")) || existsSync(join(root, "CLAUDE.md")))
71
- found.push("claude");
72
- if (existsSync(join(root, ".cursor")))
73
- found.push("cursor");
74
- if (existsSync(join(root, ".github")))
75
- found.push("copilot");
76
- return found;
77
- }
78
- /** The interactive preselection: detected agents + Claude Code (the wizard's default agent — deduped when detected). */
79
- export function defaultAgents(targetDir) {
80
- return [...new Set([...detectAgents(targetDir), "claude"])];
66
+ /** The interactive preselection: both skill copies, so a plain Enter covers every skills-aware agent. */
67
+ export function defaultAgents(_targetDir) {
68
+ return ["claude", "universal"];
81
69
  }
82
70
  /** "all" | "none" | comma list of ids; unknown ids throw listing valid values. */
83
71
  export function parseAgentsFlag(value) {
@@ -116,21 +104,21 @@ function disposition(path, existing, version, force) {
116
104
  stale: true,
117
105
  };
118
106
  }
119
- /** Copy the canonical skill dir verbatim (stamp aside) into .claude/skills/nola/. */
120
- async function writeClaudeSkill(root, s, version) {
121
- const dir = join(root, ".claude", "skills", "nola");
107
+ /** Copy the canonical skill dir verbatim (stamp aside) into `<root>/<base>/`. */
108
+ async function writeSkillDir(root, base, s, version) {
109
+ const dir = join(root, ...base.split("/"));
122
110
  await mkdir(join(dir, "references"), { recursive: true });
123
111
  const stamped = s.full.replace(/^(---\n[\s\S]*?\n---\n)/, `$1${stamp(version)}\n`);
124
112
  await writeFile(join(dir, "SKILL.md"), stamped);
125
- const wrote = [".claude/skills/nola/SKILL.md"];
113
+ const wrote = [`${base}/SKILL.md`];
126
114
  for (const name of (await readdir(join(SKILLS_DIR, "references"))).sort()) {
127
115
  await writeFile(join(dir, "references", name), await readFile(join(SKILLS_DIR, "references", name), "utf8"));
128
- wrote.push(`.claude/skills/nola/references/${name}`);
116
+ wrote.push(`${base}/references/${name}`);
129
117
  }
130
118
  return wrote;
131
119
  }
132
120
  /**
133
- * Write the selected adapters as self-contained, version-stamped content.
121
+ * Write the selected targets as self-contained, version-stamped content.
134
122
  * Existing files are classified, never blindly overwritten (spec §3).
135
123
  */
136
124
  export async function writeAgentSkills(targetDir, agents, opts = {}) {
@@ -139,48 +127,52 @@ export async function writeAgentSkills(targetDir, agents, opts = {}) {
139
127
  const source = await readSkillSource();
140
128
  const force = opts.force === true;
141
129
  const wrote = [];
130
+ const removed = [];
142
131
  const skipped = [];
143
132
  let stale = false;
144
- const flat = {
145
- cursor: { path: ".cursor/rules/nola.mdc", content: cursorRule(source, version) },
146
- copilot: { path: ".github/instructions/nola.instructions.md", content: copilotInstructions(source, version) },
147
- "agents-md": { path: "AGENTS.md", content: agentsMd(source, version) },
148
- };
149
- for (const id of AGENT_IDS) {
133
+ for (const id of ["claude", "universal"]) {
150
134
  if (!agents.includes(id))
151
135
  continue;
152
- if (id === "claude") {
153
- const marker = join(root, ".claude", "skills", "nola", "SKILL.md");
154
- if (existsSync(marker)) {
155
- const d = disposition(".claude/skills/nola/SKILL.md", await readFile(marker, "utf8"), version, force);
156
- if (!d.write) {
157
- skipped.push(d.note);
158
- stale ||= d.stale;
159
- continue;
160
- }
161
- }
162
- wrote.push(...(await writeClaudeSkill(root, source, version)));
163
- continue;
164
- }
165
- const { path, content } = flat[id];
166
- const abs = join(root, ...path.split("/"));
167
- if (existsSync(abs)) {
168
- // AGENTS.md is a user file: never modified, --force included.
169
- if (id === "agents-md") {
170
- skipped.push(`AGENTS.md already exists — left untouched. Add this section manually:\n\n${agentsSection(source, version)}`);
171
- continue;
172
- }
173
- const d = disposition(path, await readFile(abs, "utf8"), version, force);
136
+ const base = SKILL_DIRS[id];
137
+ const marker = join(root, ...base.split("/"), "SKILL.md");
138
+ if (existsSync(marker)) {
139
+ const d = disposition(`${base}/SKILL.md`, await readFile(marker, "utf8"), version, force);
174
140
  if (!d.write) {
175
141
  skipped.push(d.note);
176
142
  stale ||= d.stale;
177
143
  continue;
178
144
  }
179
145
  }
180
- await mkdir(dirname(abs), { recursive: true });
181
- await writeFile(abs, content);
182
- wrote.push(path);
146
+ wrote.push(...(await writeSkillDir(root, base, source, version)));
147
+ }
148
+ if (agents.includes("universal")) {
149
+ // Stamped legacy adapters are ours: superseded by .agents/skills, removed under --force.
150
+ for (const path of LEGACY_ADAPTERS) {
151
+ const abs = join(root, ...path.split("/"));
152
+ if (!existsSync(abs) || !STAMP_RE.test(await readFile(abs, "utf8")))
153
+ continue;
154
+ if (force) {
155
+ await rm(abs);
156
+ removed.push(path);
157
+ }
158
+ else {
159
+ skipped.push(`${path} is superseded by .agents/skills/nola — re-run with --force to remove it`);
160
+ stale = true;
161
+ }
162
+ }
163
+ }
164
+ if (agents.includes("agents-md")) {
165
+ const abs = join(root, "AGENTS.md");
166
+ if (existsSync(abs)) {
167
+ // AGENTS.md is a user file: never modified, --force included.
168
+ skipped.push(`AGENTS.md already exists — left untouched. Add this section manually:\n\n${agentsSection(source, version)}`);
169
+ }
170
+ else {
171
+ await mkdir(dirname(abs), { recursive: true });
172
+ await writeFile(abs, agentsMd(source, version));
173
+ wrote.push("AGENTS.md");
174
+ }
183
175
  }
184
- return { wrote, skipped, stale };
176
+ return { wrote, removed, skipped, stale };
185
177
  }
186
178
  //# sourceMappingURL=agents.js.map
@@ -151,7 +151,13 @@ var TEMPLATES = [
151
151
  { name: "extract-invoice", label: "same-file type references, optional fields", source: "example" },
152
152
  { name: "classify-message", label: "closed label sets: union alias, string enum, inline union", source: "example" },
153
153
  { 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" }
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; no provider question",
158
+ source: "example",
159
+ provider: { label: "typesafe.ai", envVar: "TYPESAFE_API_KEY" }
160
+ }
155
161
  ];
156
162
  function templateByName(name) {
157
163
  return TEMPLATES.find((t) => t.name === name);
@@ -345,20 +351,28 @@ async function addNola(targetDir, opts = {}) {
345
351
 
346
352
  // src/agents.ts
347
353
  import { existsSync as existsSync5 } from "node:fs";
348
- import { mkdir as mkdir3, readdir as readdir3, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
354
+ import { mkdir as mkdir3, readdir as readdir3, readFile as readFile5, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
349
355
  import { dirname as dirname2, join as join5, resolve as resolve3 } from "node:path";
350
356
  import { fileURLToPath as fileURLToPath3 } from "node:url";
351
- var AGENT_IDS = ["claude", "cursor", "copilot", "agents-md"];
357
+ var AGENT_IDS = ["claude", "universal", "agents-md"];
352
358
  var SKILL_AGENTS_QUESTION = "Set up coding agents? (Space toggles, Enter confirms)";
359
+ var SKILL_DIRS = {
360
+ claude: ".claude/skills/nola",
361
+ universal: ".agents/skills/nola"
362
+ };
353
363
  var AGENT_OPTIONS = [
354
- { value: "agents-md", label: "AGENTS.md", hint: "cross-agent instructions (Codex, Gemini CLI, \u2026)" },
355
- { value: "claude", label: "Claude Code", hint: ".claude/skills/nola/ (full skill + references)" },
356
- { value: "cursor", label: "Cursor", hint: ".cursor/rules/nola.mdc" },
357
- { value: "copilot", label: "GitHub Copilot", hint: ".github/instructions/nola.instructions.md" }
364
+ { value: "claude", label: "Claude Code", hint: ".claude/skills/nola/ (the only directory Claude Code reads)" },
365
+ {
366
+ value: "universal",
367
+ label: "Cursor, Copilot, Codex, Gemini CLI, \u2026",
368
+ hint: ".agents/skills/nola/ (the open Agent Skills location)"
369
+ },
370
+ { value: "agents-md", label: "AGENTS.md", hint: "the skill body inline, for agents that read only AGENTS.md" }
358
371
  ];
359
372
  var SKILLS_DIR = fileURLToPath3(new URL("../skills/nola/", import.meta.url));
360
373
  var STAMP_RE = /^<!-- nola-skill v(\S+)/m;
361
374
  var stamp = (version) => `<!-- nola-skill v${version} \u2014 regenerate with: nola skill install --force -->`;
375
+ var LEGACY_ADAPTERS = [".cursor/rules/nola.mdc", ".github/instructions/nola.instructions.md"];
362
376
  async function readSkillSource() {
363
377
  const full = (await readFile5(join5(SKILLS_DIR, "SKILL.md"), "utf8")).replaceAll("\r\n", "\n");
364
378
  const fm = /^---\n([\s\S]*?)\n---\n/.exec(full);
@@ -370,27 +384,9 @@ async function readSkillSource() {
370
384
  }
371
385
  var LOCAL_REFERENCES = [
372
386
  "Deeper references, when this repo has them:",
373
- "`.claude/skills/nola/references/` \u2014 syntax.md, patterns.md, config.md,",
374
- "pitfalls.md."
387
+ "`.agents/skills/nola/references/` or `.claude/skills/nola/references/` \u2014",
388
+ "syntax.md, patterns.md, config.md, pitfalls.md."
375
389
  ].join("\n");
376
- function cursorRule(s, version) {
377
- return [
378
- "---",
379
- "description: Nola (.tsi) language rules",
380
- 'globs: ["**/*.tsi", "nola.config.ts"]',
381
- "alwaysApply: false",
382
- "---",
383
- stamp(version),
384
- "",
385
- s.body,
386
- "",
387
- LOCAL_REFERENCES,
388
- ""
389
- ].join("\n");
390
- }
391
- function copilotInstructions(s, version) {
392
- return ["---", 'applyTo: "**/*.tsi"', "---", stamp(version), "", s.body, "", LOCAL_REFERENCES, ""].join("\n");
393
- }
394
390
  function agentsSection(s, version) {
395
391
  return ["## Nola", "", stamp(version), "", s.body, "", LOCAL_REFERENCES, ""].join("\n");
396
392
  }
@@ -399,16 +395,8 @@ function agentsMd(s, version) {
399
395
 
400
396
  ${agentsSection(s, version)}`;
401
397
  }
402
- function detectAgents(targetDir) {
403
- const root = resolve3(targetDir);
404
- const found = [];
405
- if (existsSync5(join5(root, ".claude")) || existsSync5(join5(root, "CLAUDE.md"))) found.push("claude");
406
- if (existsSync5(join5(root, ".cursor"))) found.push("cursor");
407
- if (existsSync5(join5(root, ".github"))) found.push("copilot");
408
- return found;
409
- }
410
- function defaultAgents(targetDir) {
411
- return [.../* @__PURE__ */ new Set([...detectAgents(targetDir), "claude"])];
398
+ function defaultAgents(_targetDir) {
399
+ return ["claude", "universal"];
412
400
  }
413
401
  function parseAgentsFlag(value) {
414
402
  if (value === "all") return [...AGENT_IDS];
@@ -438,16 +426,16 @@ function disposition(path, existing, version, force) {
438
426
  stale: true
439
427
  };
440
428
  }
441
- async function writeClaudeSkill(root, s, version) {
442
- const dir = join5(root, ".claude", "skills", "nola");
429
+ async function writeSkillDir(root, base, s, version) {
430
+ const dir = join5(root, ...base.split("/"));
443
431
  await mkdir3(join5(dir, "references"), { recursive: true });
444
432
  const stamped = s.full.replace(/^(---\n[\s\S]*?\n---\n)/, `$1${stamp(version)}
445
433
  `);
446
434
  await writeFile3(join5(dir, "SKILL.md"), stamped);
447
- const wrote = [".claude/skills/nola/SKILL.md"];
435
+ const wrote = [`${base}/SKILL.md`];
448
436
  for (const name of (await readdir3(join5(SKILLS_DIR, "references"))).sort()) {
449
437
  await writeFile3(join5(dir, "references", name), await readFile5(join5(SKILLS_DIR, "references", name), "utf8"));
450
- wrote.push(`.claude/skills/nola/references/${name}`);
438
+ wrote.push(`${base}/references/${name}`);
451
439
  }
452
440
  return wrote;
453
441
  }
@@ -457,51 +445,51 @@ async function writeAgentSkills(targetDir, agents, opts = {}) {
457
445
  const source = await readSkillSource();
458
446
  const force = opts.force === true;
459
447
  const wrote = [];
448
+ const removed = [];
460
449
  const skipped = [];
461
450
  let stale = false;
462
- const flat = {
463
- cursor: { path: ".cursor/rules/nola.mdc", content: cursorRule(source, version) },
464
- copilot: { path: ".github/instructions/nola.instructions.md", content: copilotInstructions(source, version) },
465
- "agents-md": { path: "AGENTS.md", content: agentsMd(source, version) }
466
- };
467
- for (const id2 of AGENT_IDS) {
451
+ for (const id2 of ["claude", "universal"]) {
468
452
  if (!agents.includes(id2)) continue;
469
- if (id2 === "claude") {
470
- const marker = join5(root, ".claude", "skills", "nola", "SKILL.md");
471
- if (existsSync5(marker)) {
472
- const d = disposition(".claude/skills/nola/SKILL.md", await readFile5(marker, "utf8"), version, force);
473
- if (!d.write) {
474
- skipped.push(d.note);
475
- stale ||= d.stale;
476
- continue;
477
- }
478
- }
479
- wrote.push(...await writeClaudeSkill(root, source, version));
480
- continue;
481
- }
482
- const { path, content } = flat[id2];
483
- const abs = join5(root, ...path.split("/"));
484
- if (existsSync5(abs)) {
485
- if (id2 === "agents-md") {
486
- skipped.push(
487
- `AGENTS.md already exists \u2014 left untouched. Add this section manually:
488
-
489
- ${agentsSection(source, version)}`
490
- );
491
- continue;
492
- }
493
- const d = disposition(path, await readFile5(abs, "utf8"), version, force);
453
+ const base = SKILL_DIRS[id2];
454
+ const marker = join5(root, ...base.split("/"), "SKILL.md");
455
+ if (existsSync5(marker)) {
456
+ const d = disposition(`${base}/SKILL.md`, await readFile5(marker, "utf8"), version, force);
494
457
  if (!d.write) {
495
458
  skipped.push(d.note);
496
459
  stale ||= d.stale;
497
460
  continue;
498
461
  }
499
462
  }
500
- await mkdir3(dirname2(abs), { recursive: true });
501
- await writeFile3(abs, content);
502
- wrote.push(path);
463
+ wrote.push(...await writeSkillDir(root, base, source, version));
464
+ }
465
+ if (agents.includes("universal")) {
466
+ for (const path of LEGACY_ADAPTERS) {
467
+ const abs = join5(root, ...path.split("/"));
468
+ if (!existsSync5(abs) || !STAMP_RE.test(await readFile5(abs, "utf8"))) continue;
469
+ if (force) {
470
+ await rm3(abs);
471
+ removed.push(path);
472
+ } else {
473
+ skipped.push(`${path} is superseded by .agents/skills/nola \u2014 re-run with --force to remove it`);
474
+ stale = true;
475
+ }
476
+ }
477
+ }
478
+ if (agents.includes("agents-md")) {
479
+ const abs = join5(root, "AGENTS.md");
480
+ if (existsSync5(abs)) {
481
+ skipped.push(
482
+ `AGENTS.md already exists \u2014 left untouched. Add this section manually:
483
+
484
+ ${agentsSection(source, version)}`
485
+ );
486
+ } else {
487
+ await mkdir3(dirname2(abs), { recursive: true });
488
+ await writeFile3(abs, agentsMd(source, version));
489
+ wrote.push("AGENTS.md");
490
+ }
503
491
  }
504
- return { wrote, skipped, stale };
492
+ return { wrote, removed, skipped, stale };
505
493
  }
506
494
 
507
495
  // src/api.ts
@@ -1483,7 +1471,8 @@ var EDITOR_OPTIONS = [
1483
1471
  { value: "vscode", label: "VS Code", hint: ".vscode/ with a debug launch config + extension recommendation" }
1484
1472
  ];
1485
1473
  function templateOption(t) {
1486
- return { value: t.name, label: t.source === "example" ? `example: ${t.name}` : t.name, hint: t.label };
1474
+ const vendor = t.provider ? ` (${t.provider.label})` : "";
1475
+ return { value: t.name, label: t.source === "example" ? `example: ${t.name}${vendor}` : `${t.name}${vendor}`, hint: t.label };
1487
1476
  }
1488
1477
  async function resolveSetup(input, prompter, dir) {
1489
1478
  const ide = input.ide === "vscode" || input.ide === "none" ? input.ide : void 0;
@@ -1537,10 +1526,10 @@ function flaggedProvider(input) {
1537
1526
  }
1538
1527
  return input.provider;
1539
1528
  }
1540
- async function resolveProvider(input, prompter) {
1529
+ async function resolveProvider(input, prompter, pinned = false) {
1541
1530
  const flagged = flaggedProvider(input);
1542
1531
  if (flagged !== void 0) return flagged;
1543
- if (!input.interactive) return "none";
1532
+ if (pinned || !input.interactive) return "none";
1544
1533
  const path = input.keyPath ?? { kind: "trial" };
1545
1534
  while (true) {
1546
1535
  const choice = await prompter.select(PROVIDER_QUESTION, providerOptions(path));
@@ -1550,8 +1539,8 @@ async function resolveProvider(input, prompter) {
1550
1539
  return choice;
1551
1540
  }
1552
1541
  }
1553
- async function resolveExtras(input, prompter, dir) {
1554
- const provider = await resolveProvider(input, prompter);
1542
+ async function resolveExtras(input, prompter, dir, pinnedProvider = false) {
1543
+ const provider = await resolveProvider(input, prompter, pinnedProvider);
1555
1544
  if (provider === null) return null;
1556
1545
  const setup = await resolveSetup(input, prompter, dir);
1557
1546
  return setup === null ? null : { provider, ...setup };
@@ -1648,7 +1637,7 @@ async function resolveScaffoldOptions(input, prompter) {
1648
1637
  template = choice;
1649
1638
  }
1650
1639
  }
1651
- const extras = await resolveExtras(input, prompter, dir);
1640
+ const extras = await resolveExtras(input, prompter, dir, templateByName(template)?.provider !== void 0);
1652
1641
  if (extras === null) return { kind: "cancelled" };
1653
1642
  return { kind: "scaffold", dir, template, force, ...extras };
1654
1643
  }
@@ -1697,7 +1686,7 @@ function commented(command, note) {
1697
1686
  }
1698
1687
  function nextSteps(outcome, fileCount, name, pm, installed = false, grant = null) {
1699
1688
  const cmd = packageManagerCommands(pm);
1700
- const vendorEnv = providerById(outcome.provider)?.envVar;
1689
+ const vendorEnv = providerById(outcome.provider)?.envVar ?? (outcome.provider === "none" ? templateByName(outcome.template)?.provider?.envVar : void 0);
1701
1690
  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
1691
  const lines = [
1703
1692
  ...outcome.dir === "." ? [] : [`cd ${outcome.dir}`],
@@ -1842,7 +1831,7 @@ async function runFlow(args, opts = {}) {
1842
1831
  const result = await addNola(outcome.dir, { provider: outcome.provider });
1843
1832
  const trial = grant ? await applyTrial(outcome.dir, { apiKey: grant.apiKey, hasConfig: !result.wrote.includes("nola.config.ts") }) : { wrote: [], skipped: [] };
1844
1833
  const ide = outcome.ide === "vscode" ? await writeVscodeSetup(outcome.dir) : { wrote: [], skipped: [] };
1845
- const ag = outcome.agents.length > 0 ? await writeAgentSkills(outcome.dir, outcome.agents) : { wrote: [], skipped: [] };
1834
+ const ag = outcome.agents.length > 0 ? await writeAgentSkills(outcome.dir, outcome.agents) : { wrote: [], removed: [], skipped: [] };
1846
1835
  for (const note of [...result.skipped, ...trial.skipped, ...ide.skipped, ...ag.skipped]) prompter.note(note);
1847
1836
  const trialWrote = trial.wrote.filter((f) => !result.wrote.includes(f));
1848
1837
  const written = [...result.wrote, ...result.added, ...trialWrote, ...ide.wrote, ...ag.wrote];
@@ -1931,7 +1920,6 @@ export {
1931
1920
  AGENT_IDS,
1932
1921
  SKILL_AGENTS_QUESTION,
1933
1922
  AGENT_OPTIONS,
1934
- detectAgents,
1935
1923
  defaultAgents,
1936
1924
  parseAgentsFlag,
1937
1925
  writeAgentSkills,
package/dist/flow.js CHANGED
@@ -46,11 +46,13 @@ const EDITOR_OPTIONS = [
46
46
  { value: "vscode", label: "VS Code", hint: ".vscode/ with a debug launch config + extension recommendation" },
47
47
  ];
48
48
  function templateOption(t) {
49
- return { value: t.name, label: t.source === "example" ? `example: ${t.name}` : t.name, hint: t.label };
49
+ // a pinned template names its vendor in the row itself: `example: triage-ticket (typesafe.ai)`
50
+ 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 };
50
52
  }
51
53
  /**
52
54
  * The setup step: a yes/no gate (default yes), then editor + coding agents in
53
- * one grouped checkbox list, VS Code and Claude Code preselected — so the
55
+ * one grouped checkbox list, VS Code and both skill copies preselected — so the
54
56
  * defaults are two Enters and the whole no is one. `--ide` / `--agents` each
55
57
  * fill their half — a group a flag already answered is not shown and the gate
56
58
  * narrows to the half that remains; a no leaves the asked halves at none.
@@ -133,11 +135,13 @@ function flaggedProvider(input) {
133
135
  * one Enter away, Ctrl+C cancels). The key itself is minted later, on the
134
136
  * stored session, after every question — nothing is consumed by a cancel.
135
137
  */
136
- async function resolveProvider(input, prompter) {
138
+ async function resolveProvider(input, prompter, pinned = false) {
137
139
  const flagged = flaggedProvider(input);
138
140
  if (flagged !== undefined)
139
141
  return flagged;
140
- if (!input.interactive)
142
+ // A template whose own config names its vendor (triage-ticket → typesafe())
143
+ // is not asked: "none" keeps that config, and the outro names its env var.
144
+ if (pinned || !input.interactive)
141
145
  return "none";
142
146
  const path = input.keyPath ?? { kind: "trial" };
143
147
  while (true) {
@@ -152,8 +156,8 @@ async function resolveProvider(input, prompter) {
152
156
  }
153
157
  }
154
158
  /** The provider and setup (editor + agents) questions, in that order, shared by every outcome site; null = cancelled. */
155
- async function resolveExtras(input, prompter, dir) {
156
- const provider = await resolveProvider(input, prompter);
159
+ async function resolveExtras(input, prompter, dir, pinnedProvider = false) {
160
+ const provider = await resolveProvider(input, prompter, pinnedProvider);
157
161
  if (provider === null)
158
162
  return null;
159
163
  const setup = await resolveSetup(input, prompter, dir);
@@ -263,7 +267,7 @@ export async function resolveScaffoldOptions(input, prompter) {
263
267
  template = choice;
264
268
  }
265
269
  }
266
- const extras = await resolveExtras(input, prompter, dir);
270
+ const extras = await resolveExtras(input, prompter, dir, templateByName(template)?.provider !== undefined);
267
271
  if (extras === null)
268
272
  return { kind: "cancelled" };
269
273
  return { kind: "scaffold", dir, template, force, ...extras };
@@ -320,7 +324,9 @@ function commented(command, note) {
320
324
  }
321
325
  function nextSteps(outcome, fileCount, name, pm, installed = false, grant = null) {
322
326
  const cmd = packageManagerCommands(pm);
323
- const vendorEnv = providerById(outcome.provider)?.envVar;
327
+ // 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);
324
330
  const startNote = grant
325
331
  ? grant.source === "trial"
326
332
  ? "25 free Nola runs — key in .env"
@@ -518,7 +524,9 @@ export async function runFlow(args, opts = {}) {
518
524
  : { wrote: [], skipped: [] };
519
525
  // An explicit editor choice is honored even when Nola itself was already set up.
520
526
  const ide = outcome.ide === "vscode" ? await writeVscodeSetup(outcome.dir) : { wrote: [], skipped: [] };
521
- const ag = outcome.agents.length > 0 ? await writeAgentSkills(outcome.dir, outcome.agents) : { wrote: [], skipped: [] };
527
+ const ag = outcome.agents.length > 0
528
+ ? await writeAgentSkills(outcome.dir, outcome.agents)
529
+ : { wrote: [], removed: [], skipped: [] };
522
530
  for (const note of [...result.skipped, ...trial.skipped, ...ide.skipped, ...ag.skipped])
523
531
  prompter.note(note);
524
532
  // applyTrial re-writes the config addNola just wrote — list it once.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type { AddOptions, AddResult } from "./add.js";
2
2
  export { addNola } from "./add.js";
3
3
  export type { AgentId, AgentSetupResult } from "./agents.js";
4
- export { AGENT_IDS, AGENT_OPTIONS, defaultAgents, detectAgents, parseAgentsFlag, writeAgentSkills } from "./agents.js";
4
+ export { AGENT_IDS, AGENT_OPTIONS, defaultAgents, parseAgentsFlag, writeAgentSkills } from "./agents.js";
5
5
  export type { ClientInit, KeyGrant, NolaAccountKind, NolaAuthConfig, NolaBillingSession, NolaCapabilities, NolaClaim, NolaConsoleCreatedKey, NolaConsoleMe, NolaTrialResponse, } from "./api.js";
6
6
  export { claimSession, createBillingSession, createConsoleKey, DEFAULT_API_URL, fetchCapabilities, fetchMe, isSessionRejected, NolaApiError, nolaApiUrl, requestTrial, sessionIdOf, } from "./api.js";
7
7
  export type { AuthOptions, SignInOptions, TokenSet } from "./auth.js";