tunestra 0.1.0 → 0.2.0

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.
package/README.md CHANGED
@@ -9,8 +9,8 @@
9
9
  > Tune your AI agents to play as one orchestra.
10
10
 
11
11
  > [!WARNING]
12
- > **Early version.** v0.1 supports Claude Code; Codex comes in v0.2. The formats may still
13
- > change before v1.0.
12
+ > **Early version.** tunestra supports Claude Code and Codex. The formats may still change
13
+ > before v1.0.
14
14
 
15
15
  tunestra gives Claude Code, Codex and other AI coding agents **one shared source** of rules,
16
16
  skills, commands and MCP servers, and generates each agent's native configuration from it.
@@ -33,7 +33,7 @@ specific one wins.
33
33
 
34
34
  ```sh
35
35
  npx tunestra init --extends github:<organisation_name>/<presets_repo>/team#v1
36
- npx tunestra setup # generates AGENTS.md, CLAUDE.md, .claude/, .agents/, .mcp.json
36
+ npx tunestra setup # generates AGENTS.md and the configuration of every agent
37
37
  ```
38
38
 
39
39
  `init` creates `tunestra.yaml`:
@@ -41,7 +41,7 @@ npx tunestra setup # generates AGENTS.md, CLAUDE.md, .claude/, .agents/, .mcp
41
41
  ```yaml
42
42
  extends:
43
43
  - github:<organisation_name>/<presets_repo>/team#v1
44
- agents: [claude]
44
+ agents: [claude] # add codex: [claude, codex]
45
45
  ```
46
46
 
47
47
  Commit `tunestra.yaml`, `.tunestra/`, `AGENTS.md` and `CLAUDE.md`. Everything else that
@@ -97,7 +97,7 @@ as a preset.
97
97
  name: my-service # optional, defaults to the directory name
98
98
  extends:
99
99
  - ../presets/team
100
- agents: [claude] # v0.1 supports claude
100
+ agents: [claude, codex] # claude, codex; defaults to [claude]
101
101
  exclude: {}
102
102
  overrides: {}
103
103
  ```
@@ -116,7 +116,7 @@ repository with a relative path, like `../base`. A relative path cannot leave th
116
116
  Git is not needed: repositories are downloaded as archives over HTTPS.
117
117
 
118
118
  For a private repository, set the `TUNESTRA_GIT_TOKEN` environment variable to a token with
119
- read access. Lockfiles and npm sources are planned for v0.3.
119
+ read access. Lockfiles and npm sources are planned for v0.4.
120
120
 
121
121
  ## Inheritance
122
122
 
@@ -153,20 +153,42 @@ are merged by key (the specific value wins); `command`, `args` and `url` are rep
153
153
 
154
154
  All problems are reported at once, in the order of the files, so you can fix them in one pass.
155
155
 
156
- ## What gets generated (Claude Code)
156
+ ## What gets generated
157
+
158
+ For every agent:
157
159
 
158
160
  | Path | Content | Committed |
159
161
  |---|---|---|
160
162
  | `AGENTS.md` | Index: hierarchy, rules, skills, commands, MCP servers | yes |
161
- | `CLAUDE.md` | `@AGENTS.md` | yes |
162
163
  | `.agents/rules/<NN>-<level>-<key>.md` | Rule texts, ordered from specific to general | no |
163
164
  | `.agents/skills/<key>/` | Skills | no |
165
+ | `.tunestra/state/` | What tunestra generated last time | no |
166
+ | `.gitignore` | A block between `# >>> tunestra` and `# <<< tunestra` | yes |
167
+
168
+ ### Claude Code
169
+
170
+ | Path | Content | Committed |
171
+ |---|---|---|
172
+ | `CLAUDE.md` | `@AGENTS.md` | yes |
164
173
  | `.claude/rules/tunestra/<NN>-<level>-<key>.md` | Copies of the rules; Claude Code loads them in every session | no |
165
174
  | `.claude/skills/<key>` | Symlinks to `.agents/skills/<key>` | no |
166
175
  | `.claude/commands/<key>.md` | Commands | no |
167
176
  | `.mcp.json` | MCP servers | no |
168
- | `.tunestra/state/` | What tunestra generated last time | no |
169
- | `.gitignore` | A block between `# >>> tunestra` and `# <<< tunestra` | yes |
177
+
178
+ ### Codex
179
+
180
+ Codex reads `AGENTS.md` and `.agents/skills/` by itself. It does not follow the links in
181
+ `AGENTS.md`, so tunestra gives it the rule texts in its config.
182
+
183
+ | Path | Content | Committed |
184
+ |---|---|---|
185
+ | `.codex/config.toml` | `developer_instructions` with every rule, from specific to general, and `[mcp_servers]` | no |
186
+ | `.agents/skills/<command>/` | Each command as a skill that runs only when you call it: `$release-notes` | no |
187
+
188
+ Codex reads `.codex/config.toml` only in a trusted project: trust the project when Codex asks.
189
+ Codex has no `type` for MCP servers and does not support SSE: an SSE server is skipped with a
190
+ note, and `headers` become `http_headers`. tunestra owns the whole `.codex/config.toml`; keep
191
+ your personal Codex settings in `~/.codex/config.toml`.
170
192
 
171
193
  `<NN>` orders rules from specific to general: `10` project, `20` team, `30` domain, `40` base.
172
194
  Level instructions get the key `instructions`, and the preset name when it differs from the
@@ -180,7 +202,9 @@ Every generated rule starts with its origin:
180
202
  > Level: team, preset: team. Overrides: base.
181
203
  ```
182
204
 
183
- Skills and commands are copied as is: their frontmatter must stay on the first line.
205
+ Skills and commands are copied as is: their frontmatter must stay on the first line. For
206
+ Codex, a command gets only `name` and `description` in its frontmatter; other fields, like
207
+ `argument-hint`, are for Claude Code.
184
208
 
185
209
  ## Running setup again
186
210
 
@@ -214,14 +238,15 @@ and tells you where to move their text.
214
238
 
215
239
  | Release | Scope |
216
240
  |---|---|
217
- | v0.1 | Inheritance for Claude Code (this document) |
218
- | v0.2 | Codex |
219
- | v0.3 | Spaces, npm sources, lockfile |
220
- | v0.4 | `required` / `locked` policies, CI check |
221
- | v0.5 | Hooks |
222
- | v0.6 | LLM gateway, permissions |
223
- | v0.7 | Local plugins |
224
- | v0.8 | Isolated remote plugins |
241
+ | v0.1 | Inheritance for Claude Code |
242
+ | v0.2 | Codex (this document) |
243
+ | v0.3 | Subagents, a personal `local` level, `env` and permissions |
244
+ | v0.4 | Spaces, npm sources, lockfile |
245
+ | v0.5 | `required` / `locked` policies, CI check |
246
+ | v0.6 | Hooks |
247
+ | v0.7 | LLM gateway |
248
+ | v0.8 | Local plugins |
249
+ | v0.9 | Isolated remote plugins |
225
250
 
226
251
  ## License
227
252
 
package/dist/cli.mjs CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import { t as VERSION } from "./src-DSbrKtiC.mjs";
2
+ import { t as VERSION } from "./src-jfoAZbqo.mjs";
3
3
  import { Command } from "@commander-js/extra-typings";
4
4
  import pc from "picocolors";
5
5
  import { copyFile, lstat, mkdir, readFile, readdir, readlink, rename, rm, rmdir, stat, symlink, unlink, writeFile } from "node:fs/promises";
6
6
  import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
7
7
  import { glob } from "tinyglobby";
8
8
  import { prettifyError, z } from "zod";
9
- import { parse } from "yaml";
9
+ import { parse, stringify } from "yaml";
10
10
  import { downloadTemplate } from "giget";
11
+ import { stringify as stringify$1 } from "smol-toml";
11
12
  import { createHash } from "node:crypto";
12
13
  //#region src/shared/constants/files.ts
13
14
  /** Preset manifest, in the root of every preset. */
@@ -32,6 +33,11 @@ const OutputKind = {
32
33
  /** First line of every generated markdown file; tunestra recognizes its files by it. */
33
34
  const GENERATED_MARKER = "<!-- Generated by tunestra";
34
35
  const GENERATED_NOTICE = `${GENERATED_MARKER}. Do not edit: change the sources and run \`tunestra setup\`. -->`;
36
+ /** The same marker for generated TOML and YAML files, where a comment starts with `#`. */
37
+ const GENERATED_HASH_MARKER = "# Generated by tunestra";
38
+ const GENERATED_HASH_NOTICE = `${GENERATED_HASH_MARKER}. Do not edit: change the sources and run \`tunestra setup\`.`;
39
+ /** Every marker that tunestra puts on the first line of a generated file. */
40
+ const GENERATED_MARKERS = [GENERATED_MARKER, GENERATED_HASH_MARKER];
35
41
  /** Shared output that any agent can read. */
36
42
  const AGENTS_DIR = ".agents";
37
43
  /** Rules for every agent, ordered from specific to general by their file names. */
@@ -143,10 +149,22 @@ async function readFileIfExists(path) {
143
149
  throw error;
144
150
  }
145
151
  }
152
+ /** Removes a directory only when it has nothing inside; a missing directory is fine too. */
153
+ async function removeDirIfEmpty(dir) {
154
+ try {
155
+ if ((await readdir(dir)).length === 0) await rmdir(dir);
156
+ } catch (error) {
157
+ if (isMissingPathError(error)) return;
158
+ throw error;
159
+ }
160
+ }
146
161
  //#endregion
147
162
  //#region src/shared/constants/agents.ts
148
163
  /** Agents that tunestra generates configuration for. */
149
- const Agent = { Claude: "claude" };
164
+ const Agent = {
165
+ Claude: "claude",
166
+ Codex: "codex"
167
+ };
150
168
  //#endregion
151
169
  //#region src/init/render-config.ts
152
170
  const EXTENDS_EXAMPLE = "# - github:<owner>/<repo>/<path>#<ref>";
@@ -437,13 +455,14 @@ async function loadCommands(dir) {
437
455
  async function loadCommand(dir, source) {
438
456
  const file = join(dir, source);
439
457
  const content = await readFile(file, "utf8");
440
- const { data } = parseFrontmatter(content, file);
458
+ const { data, body } = parseFrontmatter(content, file);
441
459
  const frontmatter = validate(data, commandFrontmatterSchema, `frontmatter in ${file}`);
442
460
  return {
443
461
  key: basename(source, ".md"),
444
462
  source,
445
463
  description: frontmatter.description,
446
- content
464
+ content,
465
+ body
447
466
  };
448
467
  }
449
468
  //#endregion
@@ -932,7 +951,8 @@ function planClaude(context) {
932
951
  `${COMMANDS_DIR}/`,
933
952
  MCP_FILE
934
953
  ],
935
- protectedPaths: [CLAUDE_FILE]
954
+ protectedPaths: [CLAUDE_FILE],
955
+ notes: []
936
956
  };
937
957
  }
938
958
  function copyRule(ruleFile) {
@@ -952,9 +972,147 @@ function linkSkill(projectDir, skill) {
952
972
  };
953
973
  }
954
974
  //#endregion
975
+ //#region src/agents/codex/command-skill.ts
976
+ /** Codex starts the skill only when people call it, like a command. */
977
+ const OPENAI_YAML = [
978
+ GENERATED_HASH_NOTICE,
979
+ "policy:",
980
+ " allow_implicit_invocation: false",
981
+ ""
982
+ ].join("\n");
983
+ /**
984
+ * Codex has no project commands, so a command becomes a skill with the same name that runs
985
+ * only on `$<name>`. The skill gets `name` and `description`; other frontmatter of the
986
+ * command belongs to Claude Code.
987
+ */
988
+ function planCommandSkill(projectDir, command) {
989
+ const { key, description, body } = command.artifact;
990
+ const dir = `${AGENTS_SKILLS_DIR}/${key}`;
991
+ const source = describeArtifactSource(projectDir, command.origin, command.artifact.source);
992
+ const frontmatter = {
993
+ name: key,
994
+ description: description ?? `The /${key} command`
995
+ };
996
+ const skill = `---\n${stringify(frontmatter)}---\n${body}`;
997
+ const skillPath = `${dir}/SKILL.md`;
998
+ const policyPath = `${dir}/agents/openai.yaml`;
999
+ return [createFileOutput(skillPath, skill, source), createFileOutput(policyPath, OPENAI_YAML, source)];
1000
+ }
1001
+ //#endregion
1002
+ //#region src/agents/codex/config-toml.ts
1003
+ /** Starts and ends a multi-line literal string, where TOML escapes nothing. */
1004
+ const LITERAL_DELIMITER = "'''";
1005
+ /**
1006
+ * Control characters that a TOML literal string cannot hold. Tab and line feed are allowed;
1007
+ * a carriage return is in the list, because only `\r\n` would be valid.
1008
+ *
1009
+ * Matches: "\u0000", "\r", "\u001B" (escape), "\u007F" (delete)
1010
+ * Does not match: "\t", "\n", "é", "→"
1011
+ */
1012
+ const CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000B-\u001F\u007F]/;
1013
+ /**
1014
+ * `.codex/config.toml`: the rules as `developer_instructions`, which Codex adds to every
1015
+ * session, and the MCP servers. The rules are a literal string, so that people can read them.
1016
+ */
1017
+ function renderConfigToml(instructions, servers) {
1018
+ const parts = [GENERATED_HASH_NOTICE];
1019
+ if (typeof instructions === "string") {
1020
+ const line = renderInstructions$1(instructions);
1021
+ parts.push(line);
1022
+ }
1023
+ if (servers.length > 0) {
1024
+ const entries = servers.map((server) => {
1025
+ const config = convertMcpConfig(server.artifact.config);
1026
+ return [server.artifact.key, config];
1027
+ });
1028
+ const content = { mcp_servers: Object.fromEntries(entries) };
1029
+ const tables = stringify$1(content).trimEnd();
1030
+ parts.push(tables);
1031
+ }
1032
+ return `${parts.join("\n\n")}\n`;
1033
+ }
1034
+ function renderInstructions$1(text) {
1035
+ const hasDelimiter = text.includes(LITERAL_DELIMITER);
1036
+ const hasControlCharacter = CONTROL_CHARACTER_PATTERN.test(text);
1037
+ if (hasDelimiter || hasControlCharacter) return stringify$1({ developer_instructions: text });
1038
+ return `developer_instructions = ${LITERAL_DELIMITER}\n${text}${LITERAL_DELIMITER}`;
1039
+ }
1040
+ /** Codex picks the transport by `command` or `url` and has no `type`. */
1041
+ function convertMcpConfig(config) {
1042
+ const { type: _type, headers, ...server } = config;
1043
+ if (headers === void 0) return server;
1044
+ return {
1045
+ ...server,
1046
+ http_headers: headers
1047
+ };
1048
+ }
1049
+ //#endregion
1050
+ //#region src/agents/codex/codex-adapter.ts
1051
+ const CONFIG_FILE = ".codex/config.toml";
1052
+ const CONFIG_SOURCE = "the rules/ and mcp/ folders of tunestra.yaml and its presets";
1053
+ const INSTRUCTIONS_INTRO = "Rules of this project, ordered from specific to general. When two rules conflict, follow the one that comes first.";
1054
+ const TRUST_NOTE = "Codex reads rules and MCP servers from .codex/config.toml only in a trusted project: trust the project when Codex asks.";
1055
+ /**
1056
+ * Codex reads AGENTS.md and the skills in `.agents/skills` by itself. Rules go to
1057
+ * `developer_instructions` in `.codex/config.toml`, because Codex does not follow the links
1058
+ * in AGENTS.md; MCP servers go to the same file. Commands become skills.
1059
+ */
1060
+ const codexAdapter = {
1061
+ agent: Agent.Codex,
1062
+ plan: planCodex
1063
+ };
1064
+ function planCodex(context) {
1065
+ const { resolution, ruleFiles } = context;
1066
+ const projectDir = resolution.chain.project.dir;
1067
+ const notes = [];
1068
+ const outputs = [];
1069
+ const commandSkills = resolution.commands.filter((command) => {
1070
+ if (skillName.safeParse(command.artifact.key).success) return true;
1071
+ notes.push(`Codex skips the command "${command.artifact.key}": a skill name needs lowercase letters, digits and single hyphens.`);
1072
+ return false;
1073
+ }).flatMap((command) => planCommandSkill(projectDir, command));
1074
+ outputs.push(...commandSkills);
1075
+ const configFile = planConfigFile(ruleFiles, resolution.mcp.filter((server) => {
1076
+ if (server.artifact.config.type === "sse") {
1077
+ notes.push(`Codex skips the MCP server "${server.artifact.key}": Codex does not support SSE.`);
1078
+ return false;
1079
+ }
1080
+ return true;
1081
+ }));
1082
+ if (configFile === void 0) return {
1083
+ outputs,
1084
+ ignoredPaths: [],
1085
+ protectedPaths: [],
1086
+ notes
1087
+ };
1088
+ outputs.push(configFile);
1089
+ notes.push(TRUST_NOTE);
1090
+ return {
1091
+ outputs,
1092
+ ignoredPaths: [CONFIG_FILE],
1093
+ protectedPaths: [],
1094
+ notes
1095
+ };
1096
+ }
1097
+ function planConfigFile(ruleFiles, servers) {
1098
+ const hasRules = ruleFiles.length > 0;
1099
+ const hasServers = servers.length > 0;
1100
+ if (hasRules === false && hasServers === false) return;
1101
+ const content = renderConfigToml(hasRules ? renderInstructions(ruleFiles) : void 0, servers);
1102
+ return createFileOutput(CONFIG_FILE, content, CONFIG_SOURCE);
1103
+ }
1104
+ /** The rule files as they are, with the header that names the source of each. */
1105
+ function renderInstructions(ruleFiles) {
1106
+ const rules = ruleFiles.map((file) => file.content.trimEnd());
1107
+ return `${[INSTRUCTIONS_INTRO, ...rules].join("\n\n")}\n`;
1108
+ }
1109
+ //#endregion
955
1110
  //#region src/agents/adapters.ts
956
- /** Every supported agent; Codex comes in v0.2. */
957
- const AGENT_ADAPTERS = { [Agent.Claude]: claudeAdapter };
1111
+ /** Every supported agent. */
1112
+ const AGENT_ADAPTERS = {
1113
+ [Agent.Claude]: claudeAdapter,
1114
+ [Agent.Codex]: codexAdapter
1115
+ };
958
1116
  //#endregion
959
1117
  //#region src/shared/utils/markdown.ts
960
1118
  /**
@@ -1040,7 +1198,7 @@ function renderLevelLine(rule) {
1040
1198
  //#endregion
1041
1199
  //#region src/generate/index-sections.ts
1042
1200
  const HIERARCHY_INTRO = "Instructions are ordered from specific to general. When two instructions conflict, follow the one from the higher level in this list.";
1043
- const RULES_INTRO = "Always follow these rules. Claude Code loads them automatically; other agents should read them before starting work.";
1201
+ const RULES_INTRO = "Always follow these rules. Claude Code and Codex load them automatically; other agents should read them before starting work.";
1044
1202
  const SKILLS_INTRO = "Use a skill when the task matches its description.";
1045
1203
  const CHANGING_SECTION = [
1046
1204
  "## Changing this setup",
@@ -1102,8 +1260,8 @@ function renderSkills(skills) {
1102
1260
  }));
1103
1261
  return renderSection("Skills", SKILLS_INTRO, table);
1104
1262
  }
1105
- function renderCommands(commands) {
1106
- return renderSection("Commands", void 0, renderTable([
1263
+ function renderCommands(commands, agents) {
1264
+ const table = renderTable([
1107
1265
  "Command",
1108
1266
  "Level",
1109
1267
  "Description"
@@ -1115,7 +1273,18 @@ function renderCommands(commands) {
1115
1273
  origin,
1116
1274
  description
1117
1275
  ];
1118
- })));
1276
+ }));
1277
+ const hints = agents.map(describeCommandCall).filter(isString);
1278
+ if (hints.length > 0) return renderSection("Commands", hints.join(" "), table);
1279
+ return renderSection("Commands", void 0, table);
1280
+ }
1281
+ /** How to call a command in the agent, when it is not the usual `/<name>`. */
1282
+ function describeCommandCall(agent) {
1283
+ switch (agent) {
1284
+ case Agent.Claude: return;
1285
+ case Agent.Codex: return "In Codex, a command is a skill with the same name: call it as `$<name>`.";
1286
+ default: throw new TunestraError(`Unknown agent: ${agent}`);
1287
+ }
1119
1288
  }
1120
1289
  function renderMcpServers(servers) {
1121
1290
  return renderSection("MCP servers", void 0, renderTable([
@@ -1180,7 +1349,7 @@ function renderAgentsIndex(resolution) {
1180
1349
  sections.push(skills);
1181
1350
  }
1182
1351
  if (hasCommands) {
1183
- const commands = renderCommands(resolution.commands);
1352
+ const commands = renderCommands(resolution.commands, resolution.chain.project.agents);
1184
1353
  sections.push(commands);
1185
1354
  }
1186
1355
  if (hasMcpServers) {
@@ -1209,6 +1378,7 @@ function planSetup(resolution) {
1209
1378
  ];
1210
1379
  const ignoredPaths = [`${AGENTS_DIR}/`];
1211
1380
  const protectedPaths = [INSTRUCTIONS_FILE];
1381
+ const notes = [];
1212
1382
  const context = {
1213
1383
  resolution,
1214
1384
  ruleFiles
@@ -1218,6 +1388,7 @@ function planSetup(resolution) {
1218
1388
  outputs.push(...agentPlan.outputs);
1219
1389
  ignoredPaths.push(...agentPlan.ignoredPaths);
1220
1390
  protectedPaths.push(...agentPlan.protectedPaths);
1391
+ notes.push(...agentPlan.notes);
1221
1392
  }
1222
1393
  ignoredPaths.push(`${STATE_DIR}/`);
1223
1394
  const problems = findPathConflicts(outputs);
@@ -1225,7 +1396,8 @@ function planSetup(resolution) {
1225
1396
  return {
1226
1397
  outputs,
1227
1398
  ignoredPaths,
1228
- protectedPaths
1399
+ protectedPaths,
1400
+ notes
1229
1401
  };
1230
1402
  }
1231
1403
  function planRuleFile(projectDir, rule) {
@@ -1567,11 +1739,16 @@ async function inspectPath(path) {
1567
1739
  return {
1568
1740
  type,
1569
1741
  hash: hashContent(content),
1570
- isMarked: content.subarray(0, 26).toString("utf8") === GENERATED_MARKER
1742
+ isMarked: hasGeneratedMarker(content)
1571
1743
  };
1572
1744
  }
1573
1745
  return { type };
1574
1746
  }
1747
+ function hasGeneratedMarker(content) {
1748
+ return GENERATED_MARKERS.some((marker) => {
1749
+ return content.subarray(0, marker.length).toString("utf8") === marker;
1750
+ });
1751
+ }
1575
1752
  /** A file matches a record by its hash, a symlink by its target. */
1576
1753
  function matchesRecord(entry, record) {
1577
1754
  if (entry.type === PathType.File && "hash" in record) return entry.hash === record.hash;
@@ -1719,6 +1896,7 @@ async function runSetup(projectDir, options) {
1719
1896
  const sync = await planSync(chain.project.dir, plan, previous);
1720
1897
  const report = {
1721
1898
  resolution,
1899
+ plan,
1722
1900
  sync
1723
1901
  };
1724
1902
  if (options.dryRun) return report;
@@ -1763,26 +1941,30 @@ function measureColumn(rows, column) {
1763
1941
  //#region src/cli/commands/setup.ts
1764
1942
  async function describeSetup(projectDir, options) {
1765
1943
  const report = await runSetup(projectDir, options);
1766
- const summary = describeSync(report.sync, options);
1944
+ const summary = describeSync(report.plan, report.sync, options);
1767
1945
  if (options.verbose) return `${describeResolution(report.resolution)}
1768
1946
 
1769
1947
  ${summary}`;
1770
1948
  return summary;
1771
1949
  }
1772
- function describeSync(sync, options) {
1950
+ function describeSync(plan, sync, options) {
1773
1951
  const warningIcon = pc.yellow("⚠");
1774
1952
  const lines = sync.warnings.map((warning) => `${warningIcon} ${warning}`);
1953
+ const noteIcon = pc.cyan("ℹ");
1954
+ const notes = plan.notes.map((note) => `${noteIcon} ${note}`);
1775
1955
  const writeCount = sync.outputs.length;
1776
1956
  const removeCount = sync.removals.length;
1777
1957
  if (options.dryRun) {
1778
1958
  const writes = sync.outputs.map((output) => ` write ${output.path}`);
1779
1959
  const removals = sync.removals.map((path) => ` remove ${path}`);
1780
- lines.push(...writes, ...removals, "", "Nothing was written (--dry-run).");
1960
+ lines.push(...writes, ...removals, ...notes, "", "Nothing was written (--dry-run).");
1781
1961
  return lines.join("\n");
1782
1962
  }
1783
1963
  const check = pc.green("✔");
1964
+ const committedFiles = plan.protectedPaths.join(" and ");
1784
1965
  lines.push(`${check} Generated ${writeCount} files, removed ${removeCount} stale files.`);
1785
- lines.push("Commit AGENTS.md and CLAUDE.md; the rest is in .gitignore.");
1966
+ lines.push(...notes);
1967
+ lines.push(`Commit ${committedFiles}; the rest is in .gitignore.`);
1786
1968
  return lines.join("\n");
1787
1969
  }
1788
1970
  //#endregion
@@ -1815,12 +1997,14 @@ async function runUninstall(projectDir, options) {
1815
1997
  warnings
1816
1998
  };
1817
1999
  const stateDir = join(projectDir, STATE_DIR);
2000
+ const tunestraDir = join(projectDir, PROJECT_DIR);
1818
2001
  await removeOutputs(projectDir, removals);
1819
2002
  await removeGitignoreBlockFile(projectDir);
1820
2003
  await rm(stateDir, {
1821
2004
  recursive: true,
1822
2005
  force: true
1823
2006
  });
2007
+ await removeDirIfEmpty(tunestraDir);
1824
2008
  return {
1825
2009
  removals,
1826
2010
  warnings
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- export { t as VERSION } from "./src-DSbrKtiC.mjs";
1
+ export { t as VERSION } from "./src-jfoAZbqo.mjs";
@@ -1,5 +1,5 @@
1
1
  //#endregion
2
2
  //#region src/index.ts
3
- const VERSION = "0.1.0";
3
+ const VERSION = "0.2.0";
4
4
  //#endregion
5
5
  export { VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tunestra",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Tune your AI agents to play as one orchestra: one source of rules, skills, commands and MCP servers for Claude Code, Codex and more.",
5
5
  "keywords": [
6
6
  "ai",
@@ -44,7 +44,13 @@
44
44
  "test:watch": "vitest",
45
45
  "coverage": "vitest run --coverage",
46
46
  "check": "npm run typecheck && npm run lint && npm run test && npm run build",
47
- "prepublishOnly": "npm run check"
47
+ "prepublishOnly": "npm run check",
48
+ "changeset": "changeset",
49
+ "version-packages": "changeset version && npm install --package-lock-only",
50
+ "release": "changeset publish"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
48
54
  },
49
55
  "engines": {
50
56
  "node": ">=22.18"
@@ -54,12 +60,14 @@
54
60
  "commander": "^15.0.0",
55
61
  "giget": "^3.3.1",
56
62
  "picocolors": "^1.1.1",
63
+ "smol-toml": "^1.9.1",
57
64
  "tinyglobby": "^0.2.17",
58
65
  "yaml": "^2.9.1",
59
66
  "zod": "^4.6.7"
60
67
  },
61
68
  "devDependencies": {
62
69
  "@biomejs/biome": "^2.5.15",
70
+ "@changesets/cli": "^3.0.3",
63
71
  "@types/node": "^22.20.5",
64
72
  "@vitest/coverage-v8": "^5.0.5",
65
73
  "tsdown": "^0.23.1",