toolcraft 0.0.5 → 0.0.7

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 (149) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +77 -59
  4. package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.d.ts +2 -0
  5. package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.js +15 -0
  6. package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.d.ts +2 -0
  7. package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.js +13 -0
  8. package/node_modules/@poe-code/agent-defs/dist/agents/codex.d.ts +2 -0
  9. package/node_modules/@poe-code/agent-defs/dist/agents/codex.js +14 -0
  10. package/node_modules/@poe-code/agent-defs/dist/agents/goose.d.ts +2 -0
  11. package/node_modules/@poe-code/agent-defs/dist/agents/goose.js +14 -0
  12. package/node_modules/@poe-code/agent-defs/dist/agents/index.d.ts +7 -0
  13. package/node_modules/@poe-code/agent-defs/dist/agents/index.js +7 -0
  14. package/node_modules/@poe-code/agent-defs/dist/agents/kimi.d.ts +2 -0
  15. package/node_modules/@poe-code/agent-defs/dist/agents/kimi.js +15 -0
  16. package/node_modules/@poe-code/agent-defs/dist/agents/opencode.d.ts +2 -0
  17. package/node_modules/@poe-code/agent-defs/dist/agents/opencode.js +14 -0
  18. package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.d.ts +2 -0
  19. package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.js +13 -0
  20. package/node_modules/@poe-code/agent-defs/dist/index.d.ts +5 -0
  21. package/node_modules/@poe-code/agent-defs/dist/index.js +3 -0
  22. package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +3 -0
  23. package/node_modules/@poe-code/agent-defs/dist/registry.js +26 -0
  24. package/node_modules/@poe-code/agent-defs/dist/specifier.d.ts +7 -0
  25. package/node_modules/@poe-code/agent-defs/dist/specifier.js +27 -0
  26. package/node_modules/@poe-code/agent-defs/dist/types.d.ts +16 -0
  27. package/node_modules/@poe-code/agent-defs/dist/types.js +1 -0
  28. package/node_modules/@poe-code/agent-defs/package.json +20 -0
  29. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.d.ts +5 -0
  30. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +552 -0
  31. package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.d.ts +17 -0
  32. package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +58 -0
  33. package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.d.ts +7 -0
  34. package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.js +46 -0
  35. package/node_modules/@poe-code/config-mutations/dist/formats/index.d.ts +13 -0
  36. package/node_modules/@poe-code/config-mutations/dist/formats/index.js +49 -0
  37. package/node_modules/@poe-code/config-mutations/dist/formats/json.d.ts +31 -0
  38. package/node_modules/@poe-code/config-mutations/dist/formats/json.js +140 -0
  39. package/node_modules/@poe-code/config-mutations/dist/formats/toml.d.ts +2 -0
  40. package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +72 -0
  41. package/node_modules/@poe-code/config-mutations/dist/formats/yaml.d.ts +2 -0
  42. package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +73 -0
  43. package/node_modules/@poe-code/config-mutations/dist/fs-utils.d.ts +18 -0
  44. package/node_modules/@poe-code/config-mutations/dist/fs-utils.js +45 -0
  45. package/node_modules/@poe-code/config-mutations/dist/index.d.ts +8 -0
  46. package/node_modules/@poe-code/config-mutations/dist/index.js +8 -0
  47. package/node_modules/@poe-code/config-mutations/dist/mutations/config-mutation.d.ts +47 -0
  48. package/node_modules/@poe-code/config-mutations/dist/mutations/config-mutation.js +34 -0
  49. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +52 -0
  50. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +46 -0
  51. package/node_modules/@poe-code/config-mutations/dist/mutations/template-mutation.d.ts +40 -0
  52. package/node_modules/@poe-code/config-mutations/dist/mutations/template-mutation.js +32 -0
  53. package/node_modules/@poe-code/config-mutations/dist/template/render.d.ts +7 -0
  54. package/node_modules/@poe-code/config-mutations/dist/template/render.js +28 -0
  55. package/node_modules/@poe-code/config-mutations/dist/testing/format-utils.d.ts +7 -0
  56. package/node_modules/@poe-code/config-mutations/dist/testing/format-utils.js +21 -0
  57. package/node_modules/@poe-code/config-mutations/dist/testing/index.d.ts +3 -0
  58. package/node_modules/@poe-code/config-mutations/dist/testing/index.js +2 -0
  59. package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.d.ts +25 -0
  60. package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +170 -0
  61. package/node_modules/@poe-code/config-mutations/dist/types.d.ts +156 -0
  62. package/node_modules/@poe-code/config-mutations/dist/types.js +6 -0
  63. package/node_modules/@poe-code/config-mutations/package.json +33 -0
  64. package/node_modules/@poe-code/file-lock/README.md +52 -0
  65. package/node_modules/@poe-code/file-lock/dist/index.d.ts +1 -0
  66. package/node_modules/@poe-code/file-lock/dist/index.js +1 -0
  67. package/node_modules/@poe-code/file-lock/dist/lock.d.ts +27 -0
  68. package/node_modules/@poe-code/file-lock/dist/lock.js +203 -0
  69. package/node_modules/@poe-code/file-lock/package.json +23 -0
  70. package/node_modules/auth-store/README.md +47 -0
  71. package/node_modules/auth-store/dist/create-secret-store.d.ts +2 -0
  72. package/node_modules/auth-store/dist/create-secret-store.js +35 -0
  73. package/node_modules/auth-store/dist/encrypted-file-store.d.ts +39 -0
  74. package/node_modules/auth-store/dist/encrypted-file-store.js +156 -0
  75. package/node_modules/auth-store/dist/index.d.ts +7 -0
  76. package/node_modules/auth-store/dist/index.js +4 -0
  77. package/node_modules/auth-store/dist/keychain-store.d.ts +22 -0
  78. package/node_modules/auth-store/dist/keychain-store.js +111 -0
  79. package/node_modules/auth-store/dist/provider-store.d.ts +10 -0
  80. package/node_modules/auth-store/dist/provider-store.js +28 -0
  81. package/node_modules/auth-store/dist/types.d.ts +20 -0
  82. package/node_modules/auth-store/dist/types.js +1 -0
  83. package/node_modules/auth-store/package.json +25 -0
  84. package/node_modules/mcp-oauth/README.md +31 -0
  85. package/node_modules/mcp-oauth/dist/client/auth-store-session-store.d.ts +14 -0
  86. package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +97 -0
  87. package/node_modules/mcp-oauth/dist/client/authorization-state.d.ts +8 -0
  88. package/node_modules/mcp-oauth/dist/client/authorization-state.js +34 -0
  89. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.d.ts +3 -0
  90. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +491 -0
  91. package/node_modules/mcp-oauth/dist/client/loopback-authorization.d.ts +20 -0
  92. package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +169 -0
  93. package/node_modules/mcp-oauth/dist/client/pkce.d.ts +2 -0
  94. package/node_modules/mcp-oauth/dist/client/pkce.js +7 -0
  95. package/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +40 -0
  96. package/node_modules/mcp-oauth/dist/client/token-endpoint.js +143 -0
  97. package/node_modules/mcp-oauth/dist/client/types.d.ts +113 -0
  98. package/node_modules/mcp-oauth/dist/client/types.js +1 -0
  99. package/node_modules/mcp-oauth/dist/index.d.ts +10 -0
  100. package/node_modules/mcp-oauth/dist/index.js +7 -0
  101. package/node_modules/mcp-oauth/dist/resource-indicator.d.ts +1 -0
  102. package/node_modules/mcp-oauth/dist/resource-indicator.js +11 -0
  103. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.d.ts +27 -0
  104. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +259 -0
  105. package/node_modules/mcp-oauth/dist/types.compile-check.d.ts +1 -0
  106. package/node_modules/mcp-oauth/dist/types.compile-check.js +22 -0
  107. package/node_modules/mcp-oauth/package.json +31 -0
  108. package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +4 -0
  109. package/node_modules/tiny-mcp-client/dist/index.d.ts +2 -0
  110. package/node_modules/tiny-mcp-client/dist/index.js +1 -0
  111. package/node_modules/tiny-mcp-client/dist/internal.d.ts +547 -0
  112. package/node_modules/tiny-mcp-client/dist/internal.js +2404 -0
  113. package/node_modules/tiny-mcp-client/dist/jsonrpc-types.compile-check.d.ts +1 -0
  114. package/node_modules/tiny-mcp-client/dist/jsonrpc-types.compile-check.js +37 -0
  115. package/node_modules/tiny-mcp-client/dist/mcp-lifecycle-types.compile-check.d.ts +1 -0
  116. package/node_modules/tiny-mcp-client/dist/mcp-lifecycle-types.compile-check.js +50 -0
  117. package/node_modules/tiny-mcp-client/dist/mcp-prompt-types.compile-check.d.ts +1 -0
  118. package/node_modules/tiny-mcp-client/dist/mcp-prompt-types.compile-check.js +50 -0
  119. package/node_modules/tiny-mcp-client/dist/mcp-resource-types.compile-check.d.ts +1 -0
  120. package/node_modules/tiny-mcp-client/dist/mcp-resource-types.compile-check.js +51 -0
  121. package/node_modules/tiny-mcp-client/dist/mcp-tool-types.compile-check.d.ts +1 -0
  122. package/node_modules/tiny-mcp-client/dist/mcp-tool-types.compile-check.js +89 -0
  123. package/node_modules/tiny-mcp-client/dist/mcp-transport-types.compile-check.d.ts +1 -0
  124. package/node_modules/tiny-mcp-client/dist/mcp-transport-types.compile-check.js +56 -0
  125. package/node_modules/tiny-mcp-client/dist/mcp-utility-types.compile-check.d.ts +1 -0
  126. package/node_modules/tiny-mcp-client/dist/mcp-utility-types.compile-check.js +145 -0
  127. package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +24 -0
  128. package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +385 -0
  129. package/node_modules/tiny-mcp-client/package.json +22 -0
  130. package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +823 -0
  131. package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +882 -0
  132. package/node_modules/tiny-mcp-client/src/index.ts +94 -0
  133. package/node_modules/tiny-mcp-client/src/internal.ts +3566 -0
  134. package/node_modules/tiny-mcp-client/src/jsonrpc-types.compile-check.ts +66 -0
  135. package/node_modules/tiny-mcp-client/src/mcp-client-http-transport.integration.test.ts +222 -0
  136. package/node_modules/tiny-mcp-client/src/mcp-client-sdk.test.ts +1294 -0
  137. package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +143 -0
  138. package/node_modules/tiny-mcp-client/src/mcp-lifecycle-types.compile-check.ts +65 -0
  139. package/node_modules/tiny-mcp-client/src/mcp-prompt-types.compile-check.ts +66 -0
  140. package/node_modules/tiny-mcp-client/src/mcp-resource-types.compile-check.ts +70 -0
  141. package/node_modules/tiny-mcp-client/src/mcp-tool-types.compile-check.ts +117 -0
  142. package/node_modules/tiny-mcp-client/src/mcp-transport-types.compile-check.ts +75 -0
  143. package/node_modules/tiny-mcp-client/src/mcp-utility-types.compile-check.ts +181 -0
  144. package/node_modules/tiny-mcp-client/src/mock-servers.test.ts +980 -0
  145. package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +583 -0
  146. package/node_modules/tiny-mcp-client/src/transports.test.ts +8139 -0
  147. package/node_modules/tiny-mcp-client/src/utilities.test.ts +372 -0
  148. package/node_modules/tiny-mcp-client/tsconfig.json +11 -0
  149. package/package.json +24 -11
@@ -0,0 +1,552 @@
1
+ import Mustache from "mustache";
2
+ import { getConfigFormat, detectFormat } from "../formats/index.js";
3
+ import { resolvePath } from "./path-utils.js";
4
+ import { isNotFound, readFileIfExists, pathExists, createTimestamp } from "../fs-utils.js";
5
+ // ============================================================================
6
+ // Helper Functions
7
+ // ============================================================================
8
+ function resolveValue(resolver, options) {
9
+ if (typeof resolver === "function") {
10
+ return resolver(options);
11
+ }
12
+ return resolver;
13
+ }
14
+ function createInvalidDocumentBackupPath(targetPath) {
15
+ const ext = targetPath.includes(".") ? targetPath.split(".").pop() : "bak";
16
+ return `${targetPath}.invalid-${createTimestamp()}.${ext}`;
17
+ }
18
+ async function backupInvalidDocument(fs, targetPath, content) {
19
+ const backupPath = createInvalidDocumentBackupPath(targetPath);
20
+ await fs.writeFile(backupPath, content, { encoding: "utf8" });
21
+ }
22
+ function describeMutation(kind, targetPath) {
23
+ const displayPath = targetPath ?? "target";
24
+ switch (kind) {
25
+ case "ensureDirectory":
26
+ return `Create ${displayPath}`;
27
+ case "removeDirectory":
28
+ return `Remove directory ${displayPath}`;
29
+ case "backup":
30
+ return `Backup ${displayPath}`;
31
+ case "templateWrite":
32
+ return `Write ${displayPath}`;
33
+ case "chmod":
34
+ return `Set permissions on ${displayPath}`;
35
+ case "removeFile":
36
+ return `Remove ${displayPath}`;
37
+ case "configMerge":
38
+ case "configPrune":
39
+ case "configTransform":
40
+ case "templateMergeToml":
41
+ case "templateMergeJson":
42
+ return `Update ${displayPath}`;
43
+ default:
44
+ return "Operation";
45
+ }
46
+ }
47
+ function pruneKeysByPrefix(table, prefix) {
48
+ const result = {};
49
+ for (const [key, value] of Object.entries(table)) {
50
+ if (!key.startsWith(prefix)) {
51
+ result[key] = value;
52
+ }
53
+ }
54
+ return result;
55
+ }
56
+ function isConfigObject(value) {
57
+ return typeof value === "object" && value !== null && !Array.isArray(value);
58
+ }
59
+ function mergeWithPruneByPrefix(base, patch, pruneByPrefix) {
60
+ const result = { ...base };
61
+ const prefixMap = pruneByPrefix ?? {};
62
+ for (const [key, value] of Object.entries(patch)) {
63
+ const current = result[key];
64
+ const prefix = prefixMap[key];
65
+ if (isConfigObject(current) && isConfigObject(value)) {
66
+ if (prefix) {
67
+ const pruned = pruneKeysByPrefix(current, prefix);
68
+ result[key] = { ...pruned, ...value };
69
+ }
70
+ else {
71
+ result[key] = mergeWithPruneByPrefix(current, value, prefixMap);
72
+ }
73
+ continue;
74
+ }
75
+ result[key] = value;
76
+ }
77
+ return result;
78
+ }
79
+ // ============================================================================
80
+ // Apply Mutation
81
+ // ============================================================================
82
+ export async function applyMutation(mutation, context, options) {
83
+ switch (mutation.kind) {
84
+ case "ensureDirectory":
85
+ return applyEnsureDirectory(mutation, context, options);
86
+ case "removeDirectory":
87
+ return applyRemoveDirectory(mutation, context, options);
88
+ case "removeFile":
89
+ return applyRemoveFile(mutation, context, options);
90
+ case "chmod":
91
+ return applyChmod(mutation, context, options);
92
+ case "backup":
93
+ return applyBackup(mutation, context, options);
94
+ case "configMerge":
95
+ return applyConfigMerge(mutation, context, options);
96
+ case "configPrune":
97
+ return applyConfigPrune(mutation, context, options);
98
+ case "configTransform":
99
+ return applyConfigTransform(mutation, context, options);
100
+ case "templateWrite":
101
+ return applyTemplateWrite(mutation, context, options);
102
+ case "templateMergeToml":
103
+ return applyTemplateMerge(mutation, context, options, "toml");
104
+ case "templateMergeJson":
105
+ return applyTemplateMerge(mutation, context, options, "json");
106
+ default: {
107
+ const never = mutation;
108
+ throw new Error(`Unknown mutation kind: ${never.kind}`);
109
+ }
110
+ }
111
+ }
112
+ // ============================================================================
113
+ // File Mutation Handlers
114
+ // ============================================================================
115
+ async function applyEnsureDirectory(mutation, context, options) {
116
+ const rawPath = resolveValue(mutation.path, options);
117
+ const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
118
+ const details = {
119
+ kind: mutation.kind,
120
+ label: mutation.label ?? describeMutation(mutation.kind, targetPath),
121
+ targetPath
122
+ };
123
+ const existed = await pathExists(context.fs, targetPath);
124
+ if (!context.dryRun) {
125
+ await context.fs.mkdir(targetPath, { recursive: true });
126
+ }
127
+ return {
128
+ outcome: {
129
+ changed: !existed,
130
+ effect: "mkdir",
131
+ detail: existed ? "noop" : "create"
132
+ },
133
+ details
134
+ };
135
+ }
136
+ async function applyRemoveDirectory(mutation, context, options) {
137
+ const rawPath = resolveValue(mutation.path, options);
138
+ const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
139
+ const details = {
140
+ kind: mutation.kind,
141
+ label: mutation.label ?? describeMutation(mutation.kind, targetPath),
142
+ targetPath
143
+ };
144
+ const existed = await pathExists(context.fs, targetPath);
145
+ if (!existed) {
146
+ return {
147
+ outcome: { changed: false, effect: "none", detail: "noop" },
148
+ details
149
+ };
150
+ }
151
+ if (typeof context.fs.rm !== "function") {
152
+ return {
153
+ outcome: { changed: false, effect: "none", detail: "noop" },
154
+ details
155
+ };
156
+ }
157
+ if (mutation.force) {
158
+ if (!context.dryRun) {
159
+ await context.fs.rm(targetPath, { recursive: true, force: true });
160
+ }
161
+ return {
162
+ outcome: { changed: true, effect: "delete", detail: "delete" },
163
+ details
164
+ };
165
+ }
166
+ const entries = await context.fs.readdir(targetPath);
167
+ if (entries.length > 0) {
168
+ return {
169
+ outcome: { changed: false, effect: "none", detail: "noop" },
170
+ details
171
+ };
172
+ }
173
+ if (!context.dryRun) {
174
+ await context.fs.rm(targetPath, { recursive: true, force: true });
175
+ }
176
+ return {
177
+ outcome: { changed: true, effect: "delete", detail: "delete" },
178
+ details
179
+ };
180
+ }
181
+ async function applyRemoveFile(mutation, context, options) {
182
+ const rawPath = resolveValue(mutation.target, options);
183
+ const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
184
+ const details = {
185
+ kind: mutation.kind,
186
+ label: mutation.label ?? describeMutation(mutation.kind, targetPath),
187
+ targetPath
188
+ };
189
+ try {
190
+ const content = await context.fs.readFile(targetPath, "utf8");
191
+ const trimmed = content.trim();
192
+ // Check whenContentMatches guard
193
+ if (mutation.whenContentMatches && !mutation.whenContentMatches.test(trimmed)) {
194
+ return {
195
+ outcome: { changed: false, effect: "none", detail: "noop" },
196
+ details
197
+ };
198
+ }
199
+ // Check whenEmpty guard
200
+ if (mutation.whenEmpty && trimmed.length > 0) {
201
+ return {
202
+ outcome: { changed: false, effect: "none", detail: "noop" },
203
+ details
204
+ };
205
+ }
206
+ if (!context.dryRun) {
207
+ await context.fs.unlink(targetPath);
208
+ }
209
+ return {
210
+ outcome: { changed: true, effect: "delete", detail: "delete" },
211
+ details
212
+ };
213
+ }
214
+ catch (error) {
215
+ if (isNotFound(error)) {
216
+ return {
217
+ outcome: { changed: false, effect: "none", detail: "noop" },
218
+ details
219
+ };
220
+ }
221
+ throw error;
222
+ }
223
+ }
224
+ async function applyChmod(mutation, context, options) {
225
+ const rawPath = resolveValue(mutation.target, options);
226
+ const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
227
+ const details = {
228
+ kind: mutation.kind,
229
+ label: mutation.label ?? describeMutation(mutation.kind, targetPath),
230
+ targetPath
231
+ };
232
+ if (typeof context.fs.chmod !== "function") {
233
+ return {
234
+ outcome: { changed: false, effect: "none", detail: "noop" },
235
+ details
236
+ };
237
+ }
238
+ try {
239
+ const stat = await context.fs.stat(targetPath);
240
+ const currentMode = typeof stat.mode === "number" ? stat.mode & 0o777 : null;
241
+ if (currentMode === mutation.mode) {
242
+ return {
243
+ outcome: { changed: false, effect: "none", detail: "noop" },
244
+ details
245
+ };
246
+ }
247
+ if (!context.dryRun) {
248
+ await context.fs.chmod(targetPath, mutation.mode);
249
+ }
250
+ return {
251
+ outcome: { changed: true, effect: "chmod", detail: "update" },
252
+ details
253
+ };
254
+ }
255
+ catch (error) {
256
+ if (isNotFound(error)) {
257
+ return {
258
+ outcome: { changed: false, effect: "none", detail: "noop" },
259
+ details
260
+ };
261
+ }
262
+ throw error;
263
+ }
264
+ }
265
+ async function applyBackup(mutation, context, options) {
266
+ const rawPath = resolveValue(mutation.target, options);
267
+ const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
268
+ const details = {
269
+ kind: mutation.kind,
270
+ label: mutation.label ?? describeMutation(mutation.kind, targetPath),
271
+ targetPath
272
+ };
273
+ const content = await readFileIfExists(context.fs, targetPath);
274
+ if (content === null) {
275
+ return {
276
+ outcome: { changed: false, effect: "none", detail: "noop" },
277
+ details
278
+ };
279
+ }
280
+ if (!context.dryRun) {
281
+ const backupPath = `${targetPath}.backup-${createTimestamp()}`;
282
+ await context.fs.writeFile(backupPath, content, { encoding: "utf8" });
283
+ }
284
+ return {
285
+ outcome: { changed: true, effect: "copy", detail: "backup" },
286
+ details
287
+ };
288
+ }
289
+ // ============================================================================
290
+ // Config Mutation Handlers
291
+ // ============================================================================
292
+ async function applyConfigMerge(mutation, context, options) {
293
+ const rawPath = resolveValue(mutation.target, options);
294
+ const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
295
+ const details = {
296
+ kind: mutation.kind,
297
+ label: mutation.label ?? describeMutation(mutation.kind, targetPath),
298
+ targetPath
299
+ };
300
+ const formatName = mutation.format ?? detectFormat(rawPath);
301
+ if (!formatName) {
302
+ throw new Error(`Cannot detect config format for "${rawPath}". Provide explicit format option.`);
303
+ }
304
+ const format = getConfigFormat(formatName);
305
+ const rawContent = await readFileIfExists(context.fs, targetPath);
306
+ let current;
307
+ try {
308
+ current = rawContent === null ? {} : format.parse(rawContent);
309
+ }
310
+ catch {
311
+ // Invalid file - backup and start fresh
312
+ if (rawContent !== null) {
313
+ await backupInvalidDocument(context.fs, targetPath, rawContent);
314
+ }
315
+ current = {};
316
+ }
317
+ const value = resolveValue(mutation.value, options);
318
+ // Use mergeWithPruneByPrefix for TOML files with pruneByPrefix option
319
+ let merged;
320
+ if (mutation.pruneByPrefix) {
321
+ merged = mergeWithPruneByPrefix(current, value, mutation.pruneByPrefix);
322
+ }
323
+ else {
324
+ merged = format.merge(current, value);
325
+ }
326
+ const serialized = format.serialize(merged);
327
+ const changed = serialized !== rawContent;
328
+ if (changed && !context.dryRun) {
329
+ await context.fs.writeFile(targetPath, serialized, { encoding: "utf8" });
330
+ }
331
+ return {
332
+ outcome: {
333
+ changed,
334
+ effect: changed ? "write" : "none",
335
+ detail: changed ? (rawContent === null ? "create" : "update") : "noop"
336
+ },
337
+ details
338
+ };
339
+ }
340
+ async function applyConfigPrune(mutation, context, options) {
341
+ const rawPath = resolveValue(mutation.target, options);
342
+ const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
343
+ const details = {
344
+ kind: mutation.kind,
345
+ label: mutation.label ?? describeMutation(mutation.kind, targetPath),
346
+ targetPath
347
+ };
348
+ const rawContent = await readFileIfExists(context.fs, targetPath);
349
+ if (rawContent === null) {
350
+ return {
351
+ outcome: { changed: false, effect: "none", detail: "noop" },
352
+ details
353
+ };
354
+ }
355
+ const formatName = mutation.format ?? detectFormat(rawPath);
356
+ if (!formatName) {
357
+ throw new Error(`Cannot detect config format for "${rawPath}". Provide explicit format option.`);
358
+ }
359
+ const format = getConfigFormat(formatName);
360
+ let current;
361
+ try {
362
+ current = format.parse(rawContent);
363
+ }
364
+ catch {
365
+ // Invalid file - can't prune, leave as-is
366
+ return {
367
+ outcome: { changed: false, effect: "none", detail: "noop" },
368
+ details
369
+ };
370
+ }
371
+ // Check onlyIf guard
372
+ if (mutation.onlyIf && !mutation.onlyIf(current, options)) {
373
+ return {
374
+ outcome: { changed: false, effect: "none", detail: "noop" },
375
+ details
376
+ };
377
+ }
378
+ const shape = resolveValue(mutation.shape, options);
379
+ const { changed, result } = format.prune(current, shape);
380
+ if (!changed) {
381
+ return {
382
+ outcome: { changed: false, effect: "none", detail: "noop" },
383
+ details
384
+ };
385
+ }
386
+ // Delete file if empty
387
+ if (Object.keys(result).length === 0) {
388
+ if (!context.dryRun) {
389
+ await context.fs.unlink(targetPath);
390
+ }
391
+ return {
392
+ outcome: { changed: true, effect: "delete", detail: "delete" },
393
+ details
394
+ };
395
+ }
396
+ const serialized = format.serialize(result);
397
+ if (!context.dryRun) {
398
+ await context.fs.writeFile(targetPath, serialized, { encoding: "utf8" });
399
+ }
400
+ return {
401
+ outcome: { changed: true, effect: "write", detail: "update" },
402
+ details
403
+ };
404
+ }
405
+ async function applyConfigTransform(mutation, context, options) {
406
+ const rawPath = resolveValue(mutation.target, options);
407
+ const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
408
+ const details = {
409
+ kind: mutation.kind,
410
+ label: mutation.label ?? describeMutation(mutation.kind, targetPath),
411
+ targetPath
412
+ };
413
+ const formatName = mutation.format ?? detectFormat(rawPath);
414
+ if (!formatName) {
415
+ throw new Error(`Cannot detect config format for "${rawPath}". Provide explicit format option.`);
416
+ }
417
+ const format = getConfigFormat(formatName);
418
+ const rawContent = await readFileIfExists(context.fs, targetPath);
419
+ let current;
420
+ try {
421
+ current = rawContent === null ? {} : format.parse(rawContent);
422
+ }
423
+ catch {
424
+ if (rawContent !== null) {
425
+ await backupInvalidDocument(context.fs, targetPath, rawContent);
426
+ }
427
+ current = {};
428
+ }
429
+ const { content: transformed, changed } = mutation.transform(current, options);
430
+ if (!changed) {
431
+ return {
432
+ outcome: { changed: false, effect: "none", detail: "noop" },
433
+ details
434
+ };
435
+ }
436
+ // Delete file if null
437
+ if (transformed === null) {
438
+ if (rawContent === null) {
439
+ return {
440
+ outcome: { changed: false, effect: "none", detail: "noop" },
441
+ details
442
+ };
443
+ }
444
+ if (!context.dryRun) {
445
+ await context.fs.unlink(targetPath);
446
+ }
447
+ return {
448
+ outcome: { changed: true, effect: "delete", detail: "delete" },
449
+ details
450
+ };
451
+ }
452
+ const serialized = format.serialize(transformed);
453
+ if (!context.dryRun) {
454
+ await context.fs.writeFile(targetPath, serialized, { encoding: "utf8" });
455
+ }
456
+ return {
457
+ outcome: {
458
+ changed: true,
459
+ effect: "write",
460
+ detail: rawContent === null ? "create" : "update"
461
+ },
462
+ details
463
+ };
464
+ }
465
+ // ============================================================================
466
+ // Template Mutation Handlers
467
+ // ============================================================================
468
+ async function applyTemplateWrite(mutation, context, options) {
469
+ if (!context.templates) {
470
+ throw new Error("Template mutations require a templates loader. " +
471
+ "Provide templates function to runMutations context.");
472
+ }
473
+ const rawPath = resolveValue(mutation.target, options);
474
+ const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
475
+ const details = {
476
+ kind: mutation.kind,
477
+ label: mutation.label ?? describeMutation(mutation.kind, targetPath),
478
+ targetPath
479
+ };
480
+ const template = await context.templates(mutation.templateId);
481
+ const templateContext = mutation.context
482
+ ? resolveValue(mutation.context, options)
483
+ : {};
484
+ const rendered = Mustache.render(template, templateContext);
485
+ const existed = await pathExists(context.fs, targetPath);
486
+ if (!context.dryRun) {
487
+ await context.fs.writeFile(targetPath, rendered, { encoding: "utf8" });
488
+ }
489
+ return {
490
+ outcome: {
491
+ changed: true,
492
+ effect: "write",
493
+ detail: existed ? "update" : "create"
494
+ },
495
+ details
496
+ };
497
+ }
498
+ async function applyTemplateMerge(mutation, context, options, formatName) {
499
+ if (!context.templates) {
500
+ throw new Error("Template mutations require a templates loader. " +
501
+ "Provide templates function to runMutations context.");
502
+ }
503
+ const rawPath = resolveValue(mutation.target, options);
504
+ const targetPath = resolvePath(rawPath, context.homeDir, context.pathMapper);
505
+ const details = {
506
+ kind: mutation.kind,
507
+ label: mutation.label ?? describeMutation(mutation.kind, targetPath),
508
+ targetPath
509
+ };
510
+ const format = getConfigFormat(formatName);
511
+ // Load and render template
512
+ const template = await context.templates(mutation.templateId);
513
+ const templateContext = mutation.context
514
+ ? resolveValue(mutation.context, options)
515
+ : {};
516
+ const rendered = Mustache.render(template, templateContext);
517
+ // Parse rendered template
518
+ let templateDoc;
519
+ try {
520
+ templateDoc = format.parse(rendered);
521
+ }
522
+ catch (error) {
523
+ throw new Error(`Failed to parse rendered template "${mutation.templateId}" as ${formatName.toUpperCase()}: ${error}`, { cause: error });
524
+ }
525
+ // Read and parse existing file
526
+ const rawContent = await readFileIfExists(context.fs, targetPath);
527
+ let current;
528
+ try {
529
+ current = rawContent === null ? {} : format.parse(rawContent);
530
+ }
531
+ catch {
532
+ if (rawContent !== null) {
533
+ await backupInvalidDocument(context.fs, targetPath, rawContent);
534
+ }
535
+ current = {};
536
+ }
537
+ // Merge
538
+ const merged = format.merge(current, templateDoc);
539
+ const serialized = format.serialize(merged);
540
+ const changed = serialized !== rawContent;
541
+ if (changed && !context.dryRun) {
542
+ await context.fs.writeFile(targetPath, serialized, { encoding: "utf8" });
543
+ }
544
+ return {
545
+ outcome: {
546
+ changed,
547
+ effect: changed ? "write" : "none",
548
+ detail: changed ? (rawContent === null ? "create" : "update") : "noop"
549
+ },
550
+ details
551
+ };
552
+ }
@@ -0,0 +1,17 @@
1
+ import type { PathMapper } from "../types.js";
2
+ /**
3
+ * Expand ~ shortcut to the provided home directory.
4
+ */
5
+ export declare function expandHome(targetPath: string, homeDir: string): string;
6
+ /**
7
+ * Validate that a path is home-relative (starts with ~).
8
+ * Throws if the path is not home-relative.
9
+ */
10
+ export declare function validateHomePath(targetPath: string): void;
11
+ /**
12
+ * Resolve a path with optional path mapping for isolated configurations.
13
+ * 1. Validates the path starts with ~
14
+ * 2. Expands ~ to home directory
15
+ * 3. If pathMapper is provided, maps the directory portion and reconstructs the path
16
+ */
17
+ export declare function resolvePath(rawPath: string, homeDir: string, pathMapper?: PathMapper): string;
@@ -0,0 +1,58 @@
1
+ import path from "node:path";
2
+ /**
3
+ * Expand ~ shortcut to the provided home directory.
4
+ */
5
+ export function expandHome(targetPath, homeDir) {
6
+ if (!targetPath?.startsWith("~")) {
7
+ return targetPath;
8
+ }
9
+ // Handle ~./ -> ~/.
10
+ if (targetPath.startsWith("~./")) {
11
+ targetPath = `~/.${targetPath.slice(3)}`;
12
+ }
13
+ let remainder = targetPath.slice(1);
14
+ // Remove leading slash or backslash
15
+ if (remainder.startsWith("/") || remainder.startsWith("\\")) {
16
+ remainder = remainder.slice(1);
17
+ }
18
+ else if (remainder.startsWith(".")) {
19
+ // Handle ~/.
20
+ remainder = remainder.slice(1);
21
+ if (remainder.startsWith("/") || remainder.startsWith("\\")) {
22
+ remainder = remainder.slice(1);
23
+ }
24
+ }
25
+ return remainder.length === 0 ? homeDir : path.join(homeDir, remainder);
26
+ }
27
+ /**
28
+ * Validate that a path is home-relative (starts with ~).
29
+ * Throws if the path is not home-relative.
30
+ */
31
+ export function validateHomePath(targetPath) {
32
+ if (typeof targetPath !== "string" || targetPath.length === 0) {
33
+ throw new Error("Target path must be a non-empty string.");
34
+ }
35
+ if (!targetPath.startsWith("~")) {
36
+ throw new Error(`All target paths must be home-relative (start with ~). Received: "${targetPath}"`);
37
+ }
38
+ }
39
+ /**
40
+ * Resolve a path with optional path mapping for isolated configurations.
41
+ * 1. Validates the path starts with ~
42
+ * 2. Expands ~ to home directory
43
+ * 3. If pathMapper is provided, maps the directory portion and reconstructs the path
44
+ */
45
+ export function resolvePath(rawPath, homeDir, pathMapper) {
46
+ validateHomePath(rawPath);
47
+ const expanded = expandHome(rawPath, homeDir);
48
+ if (!pathMapper) {
49
+ return expanded;
50
+ }
51
+ // Map the directory portion
52
+ const rawDirectory = path.dirname(expanded);
53
+ const mappedDirectory = pathMapper.mapTargetDirectory({
54
+ targetDirectory: rawDirectory
55
+ });
56
+ const filename = path.basename(expanded);
57
+ return filename.length === 0 ? mappedDirectory : path.join(mappedDirectory, filename);
58
+ }
@@ -0,0 +1,7 @@
1
+ import type { Mutation, MutationContext, MutationResult, MutationOptions } from "../types.js";
2
+ /**
3
+ * Execute an array of mutations in order.
4
+ *
5
+ * All dependencies must be injected - no defaults, no globals.
6
+ */
7
+ export declare function runMutations(mutations: Mutation[], context: MutationContext, options?: MutationOptions): Promise<MutationResult>;
@@ -0,0 +1,46 @@
1
+ import { applyMutation } from "./apply-mutation.js";
2
+ /**
3
+ * Execute an array of mutations in order.
4
+ *
5
+ * All dependencies must be injected - no defaults, no globals.
6
+ */
7
+ export async function runMutations(mutations, context, options) {
8
+ const effects = [];
9
+ let anyChanged = false;
10
+ const resolverOptions = options ?? {};
11
+ for (const mutation of mutations) {
12
+ const { outcome } = await executeMutation(mutation, context, resolverOptions);
13
+ effects.push(outcome);
14
+ if (outcome.changed) {
15
+ anyChanged = true;
16
+ }
17
+ }
18
+ return {
19
+ changed: anyChanged,
20
+ effects
21
+ };
22
+ }
23
+ async function executeMutation(mutation, context, options) {
24
+ // Call onStart observer
25
+ context.observers?.onStart?.({
26
+ kind: mutation.kind,
27
+ label: mutation.label ?? mutation.kind,
28
+ targetPath: undefined // Will be resolved during apply
29
+ });
30
+ try {
31
+ const { outcome, details } = await applyMutation(mutation, context, options);
32
+ // Call onComplete observer
33
+ context.observers?.onComplete?.(details, outcome);
34
+ return { outcome, details };
35
+ }
36
+ catch (error) {
37
+ // Call onError observer
38
+ context.observers?.onError?.({
39
+ kind: mutation.kind,
40
+ label: mutation.label ?? mutation.kind,
41
+ targetPath: undefined
42
+ }, error);
43
+ // Re-throw the error
44
+ throw error;
45
+ }
46
+ }
@@ -0,0 +1,13 @@
1
+ import type { ConfigFormat } from "../types.js";
2
+ export type FormatName = "json" | "toml" | "yaml";
3
+ /**
4
+ * Get a format handler by path (auto-detect from extension) or explicit format name.
5
+ */
6
+ export declare function getConfigFormat(pathOrFormat: string): ConfigFormat;
7
+ /**
8
+ * Detect format name from a file path.
9
+ */
10
+ export declare function detectFormat(path: string): FormatName | undefined;
11
+ export { jsonFormat } from "./json.js";
12
+ export { tomlFormat } from "./toml.js";
13
+ export { yamlFormat } from "./yaml.js";