poe-code 2.0.3 → 2.0.5

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 (100) hide show
  1. package/README.md +17 -30
  2. package/dist/cli/command-runner.d.ts +1 -1
  3. package/dist/cli/commands/configure.d.ts +0 -2
  4. package/dist/cli/commands/configure.js +99 -34
  5. package/dist/cli/commands/configure.js.map +1 -1
  6. package/dist/cli/commands/doctor.d.ts +4 -0
  7. package/dist/cli/commands/doctor.js +73 -0
  8. package/dist/cli/commands/doctor.js.map +1 -0
  9. package/dist/cli/commands/remove.d.ts +1 -0
  10. package/dist/cli/commands/remove.js +41 -28
  11. package/dist/cli/commands/remove.js.map +1 -1
  12. package/dist/cli/commands/shared.d.ts +9 -12
  13. package/dist/cli/commands/shared.js +26 -24
  14. package/dist/cli/commands/shared.js.map +1 -1
  15. package/dist/cli/commands/spawn.d.ts +1 -0
  16. package/dist/cli/commands/spawn.js +14 -5
  17. package/dist/cli/commands/spawn.js.map +1 -1
  18. package/dist/cli/commands/test.d.ts +4 -0
  19. package/dist/cli/commands/test.js +36 -0
  20. package/dist/cli/commands/test.js.map +1 -0
  21. package/dist/cli/constants.d.ts +12 -9
  22. package/dist/cli/constants.js +15 -9
  23. package/dist/cli/constants.js.map +1 -1
  24. package/dist/cli/container.d.ts +3 -3
  25. package/dist/cli/context.d.ts +4 -13
  26. package/dist/cli/context.js +19 -149
  27. package/dist/cli/context.js.map +1 -1
  28. package/dist/cli/error-logger.js +1 -1
  29. package/dist/cli/error-logger.js.map +1 -1
  30. package/dist/cli/errors.d.ts +0 -10
  31. package/dist/cli/errors.js +0 -12
  32. package/dist/cli/errors.js.map +1 -1
  33. package/dist/cli/logger.js +9 -11
  34. package/dist/cli/logger.js.map +1 -1
  35. package/dist/cli/options.d.ts +13 -4
  36. package/dist/cli/options.js +12 -11
  37. package/dist/cli/options.js.map +1 -1
  38. package/dist/cli/program.js +5 -2
  39. package/dist/cli/program.js.map +1 -1
  40. package/dist/cli/prompts.d.ts +14 -4
  41. package/dist/cli/prompts.js +12 -24
  42. package/dist/cli/prompts.js.map +1 -1
  43. package/dist/cli/service-registry.d.ts +37 -15
  44. package/dist/cli/service-registry.js.map +1 -1
  45. package/dist/cli/telemetry.d.ts +1 -1
  46. package/dist/cli/ui/service-menu.d.ts +2 -2
  47. package/dist/cli/ui/theme.js +6 -6
  48. package/dist/cli/ui/theme.js.map +1 -1
  49. package/dist/providers/claude-code.d.ts +10 -46
  50. package/dist/providers/claude-code.js +94 -224
  51. package/dist/providers/claude-code.js.map +1 -1
  52. package/dist/providers/codex.d.ts +14 -40
  53. package/dist/providers/codex.js +74 -177
  54. package/dist/providers/codex.js.map +1 -1
  55. package/dist/providers/create-provider.d.ts +26 -0
  56. package/dist/providers/create-provider.js +120 -0
  57. package/dist/providers/create-provider.js.map +1 -0
  58. package/dist/providers/index.d.ts +2 -2
  59. package/dist/providers/index.js +4 -10
  60. package/dist/providers/index.js.map +1 -1
  61. package/dist/providers/opencode.d.ts +3 -43
  62. package/dist/providers/opencode.js +112 -173
  63. package/dist/providers/opencode.js.map +1 -1
  64. package/dist/providers/provider-helpers.d.ts +10 -0
  65. package/dist/providers/provider-helpers.js +62 -0
  66. package/dist/providers/provider-helpers.js.map +1 -0
  67. package/dist/providers/versioned-provider.d.ts +3 -0
  68. package/dist/providers/versioned-provider.js +9 -0
  69. package/dist/providers/versioned-provider.js.map +1 -0
  70. package/dist/services/credentials.d.ts +14 -0
  71. package/dist/services/credentials.js +123 -22
  72. package/dist/services/credentials.js.map +1 -1
  73. package/dist/services/model-strategy.d.ts +1 -1
  74. package/dist/services/model-strategy.js +20 -15
  75. package/dist/services/model-strategy.js.map +1 -1
  76. package/dist/services/mutation-events.d.ts +4 -0
  77. package/dist/services/mutation-events.js +56 -0
  78. package/dist/services/mutation-events.js.map +1 -0
  79. package/dist/services/service-install.d.ts +3 -4
  80. package/dist/services/service-install.js +30 -12
  81. package/dist/services/service-install.js.map +1 -1
  82. package/dist/services/service-manifest.d.ts +49 -21
  83. package/dist/services/service-manifest.js +261 -64
  84. package/dist/services/service-manifest.js.map +1 -1
  85. package/dist/tools/label-generator.d.ts +2 -2
  86. package/dist/tools/label-generator.js +1 -1
  87. package/dist/tools/label-generator.js.map +1 -1
  88. package/dist/utils/binary-version.d.ts +6 -0
  89. package/dist/utils/binary-version.js +35 -0
  90. package/dist/utils/binary-version.js.map +1 -0
  91. package/dist/utils/command-checks.d.ts +39 -0
  92. package/dist/utils/command-checks.js +116 -0
  93. package/dist/utils/command-checks.js.map +1 -0
  94. package/package.json +6 -8
  95. package/dist/providers/roo-code.d.ts +0 -41
  96. package/dist/providers/roo-code.js +0 -201
  97. package/dist/providers/roo-code.js.map +0 -1
  98. package/dist/utils/prerequisites.d.ts +0 -41
  99. package/dist/utils/prerequisites.js +0 -92
  100. package/dist/utils/prerequisites.js.map +0 -1
@@ -1,9 +1,13 @@
1
+ import type { CliEnvironment } from "../cli/environment.js";
2
+ import type { CommandContext } from "../cli/context.js";
1
3
  import type { FileSystem } from "../utils/file-system.js";
2
4
  import { type JsonObject } from "../utils/json.js";
5
+ import { type TomlTable } from "../utils/toml.js";
3
6
  type ValueResolver<Options, Value> = Value | ((context: MutationContext<Options>) => Value);
4
7
  interface MutationContext<Options> {
5
8
  options: Options;
6
9
  fs: FileSystem;
10
+ env: CliEnvironment;
7
11
  }
8
12
  interface TransformResult {
9
13
  content: string | null;
@@ -11,7 +15,7 @@ interface TransformResult {
11
15
  }
12
16
  interface TransformFileMutation<Options> {
13
17
  kind: "transformFile";
14
- label?: string;
18
+ label?: ValueResolver<Options, string | undefined>;
15
19
  target: ValueResolver<Options, string>;
16
20
  transform(input: {
17
21
  content: string | null;
@@ -20,42 +24,48 @@ interface TransformFileMutation<Options> {
20
24
  }
21
25
  interface EnsureDirectoryMutation<Options> {
22
26
  kind: "ensureDirectory";
23
- label?: string;
27
+ label?: ValueResolver<Options, string | undefined>;
24
28
  path: ValueResolver<Options, string>;
25
29
  }
26
30
  interface CreateBackupMutation<Options> {
27
31
  kind: "createBackup";
28
- label?: string;
32
+ label?: ValueResolver<Options, string | undefined>;
29
33
  target: ValueResolver<Options, string>;
30
34
  timestamp?: ValueResolver<Options, (() => string) | undefined>;
31
35
  }
32
36
  interface WriteTemplateMutation<Options> {
33
37
  kind: "writeTemplate";
34
- label?: string;
38
+ label?: ValueResolver<Options, string | undefined>;
35
39
  target: ValueResolver<Options, string>;
36
40
  templateId: string;
37
41
  context?: ValueResolver<Options, JsonObject | undefined>;
38
42
  }
39
43
  interface RemoveFileMutation<Options> {
40
44
  kind: "removeFile";
41
- label?: string;
45
+ label?: ValueResolver<Options, string | undefined>;
42
46
  target: ValueResolver<Options, string>;
43
47
  whenEmpty?: boolean;
44
48
  whenContentMatches?: RegExp;
45
49
  }
46
50
  export type ServiceMutation<Options> = TransformFileMutation<Options> | EnsureDirectoryMutation<Options> | CreateBackupMutation<Options> | WriteTemplateMutation<Options> | RemoveFileMutation<Options>;
47
- export interface ServiceManifest<ConfigureOptions, RemoveOptions = ConfigureOptions> {
51
+ export interface ServiceManifestDefinition<ConfigureOptions, RemoveOptions = ConfigureOptions> {
48
52
  id: string;
49
53
  summary: string;
50
- prerequisites?: {
51
- before?: string[];
52
- after?: string[];
53
- };
54
54
  configure: ServiceMutation<ConfigureOptions>[];
55
55
  remove?: ServiceMutation<RemoveOptions>[];
56
56
  }
57
+ export interface ServiceManifest<ConfigureOptions, RemoveOptions = ConfigureOptions> {
58
+ id: string;
59
+ summary: string;
60
+ configureMutations: ServiceMutation<ConfigureOptions>[];
61
+ removeMutations?: ServiceMutation<RemoveOptions>[];
62
+ configure(context: ServiceExecutionContext<ConfigureOptions>, runOptions?: ServiceRunOptions): Promise<void>;
63
+ remove: (context: ServiceExecutionContext<RemoveOptions>, runOptions?: ServiceRunOptions) => Promise<boolean>;
64
+ }
57
65
  export interface ServiceExecutionContext<Options> {
58
66
  fs: FileSystem;
67
+ env: CliEnvironment;
68
+ command: CommandContext;
59
69
  options: Options;
60
70
  }
61
71
  export interface MutationLogDetails {
@@ -71,52 +81,70 @@ export interface ServiceMutationOutcome {
71
81
  detail?: MutationDetail;
72
82
  }
73
83
  export type MutationEffect = "none" | "mkdir" | "copy" | "write" | "delete";
74
- export interface ServiceMutationHooks {
84
+ export interface ServiceMutationObservers {
75
85
  onStart?(details: MutationLogDetails): void;
76
86
  onComplete?(details: MutationLogDetails, outcome: ServiceMutationOutcome): void;
77
87
  onError?(details: MutationLogDetails, error: unknown): void;
78
88
  }
79
89
  export declare function ensureDirectory<Options>(config: {
80
90
  path: ValueResolver<Options, string>;
81
- label?: string;
91
+ label?: ValueResolver<Options, string | undefined>;
82
92
  }): ServiceMutation<Options>;
83
93
  export declare function createBackupMutation<Options>(config: {
84
94
  target: ValueResolver<Options, string>;
85
95
  timestamp?: ValueResolver<Options, (() => string) | undefined>;
86
- label?: string;
96
+ label?: ValueResolver<Options, string | undefined>;
87
97
  }): ServiceMutation<Options>;
88
98
  export declare function writeTemplateMutation<Options>(config: {
89
99
  target: ValueResolver<Options, string>;
90
100
  templateId: string;
91
101
  context?: ValueResolver<Options, JsonObject | undefined>;
92
- label?: string;
102
+ label?: ValueResolver<Options, string | undefined>;
93
103
  }): ServiceMutation<Options>;
94
104
  export declare function jsonMergeMutation<Options>(config: {
95
105
  target: ValueResolver<Options, string>;
96
106
  value: ValueResolver<Options, JsonObject>;
97
- label?: string;
107
+ label?: ValueResolver<Options, string | undefined>;
98
108
  }): ServiceMutation<Options>;
99
109
  export declare function jsonPruneMutation<Options>(config: {
100
110
  target: ValueResolver<Options, string>;
101
111
  shape: ValueResolver<Options, JsonObject>;
102
- label?: string;
112
+ label?: ValueResolver<Options, string | undefined>;
113
+ }): ServiceMutation<Options>;
114
+ export declare function tomlMergeMutation<Options>(config: {
115
+ target: ValueResolver<Options, string>;
116
+ value: ValueResolver<Options, TomlTable>;
117
+ label?: ValueResolver<Options, string | undefined>;
118
+ }): ServiceMutation<Options>;
119
+ export declare function tomlPruneMutation<Options>(config: {
120
+ target: ValueResolver<Options, string>;
121
+ prune: (document: TomlTable, context: MutationContext<Options>) => {
122
+ changed: boolean;
123
+ result: TomlTable | null;
124
+ };
125
+ label?: ValueResolver<Options, string | undefined>;
126
+ }): ServiceMutation<Options>;
127
+ export declare function tomlTemplateMergeMutation<Options>(config: {
128
+ target: ValueResolver<Options, string>;
129
+ templateId: string;
130
+ context?: ValueResolver<Options, JsonObject | undefined>;
131
+ label?: ValueResolver<Options, string | undefined>;
103
132
  }): ServiceMutation<Options>;
104
133
  export declare function removePatternMutation<Options>(config: {
105
134
  target: ValueResolver<Options, string>;
106
135
  pattern: RegExp;
107
136
  replacement?: string | ((match: string, context: MutationContext<Options>) => string);
108
- label?: string;
137
+ label?: ValueResolver<Options, string | undefined>;
109
138
  }): ServiceMutation<Options>;
110
139
  export declare function removeFileMutation<Options>(config: {
111
140
  target: ValueResolver<Options, string>;
112
141
  whenEmpty?: boolean;
113
142
  whenContentMatches?: RegExp;
114
- label?: string;
143
+ label?: ValueResolver<Options, string | undefined>;
115
144
  }): ServiceMutation<Options>;
116
- export declare function runServiceConfigure<ConfigureOptions, RemoveOptions = ConfigureOptions>(manifest: ServiceManifest<ConfigureOptions, RemoveOptions>, context: ServiceExecutionContext<ConfigureOptions>, runOptions?: ServiceRunOptions): Promise<void>;
117
- export declare function runServiceRemove<ConfigureOptions, RemoveOptions = ConfigureOptions>(manifest: ServiceManifest<ConfigureOptions, RemoveOptions>, context: ServiceExecutionContext<RemoveOptions>, runOptions?: ServiceRunOptions): Promise<boolean>;
145
+ export declare function createServiceManifest<ConfigureOptions, RemoveOptions = ConfigureOptions>(definition: ServiceManifestDefinition<ConfigureOptions, RemoveOptions>): ServiceManifest<ConfigureOptions, RemoveOptions>;
118
146
  export interface ServiceRunOptions {
119
- hooks?: ServiceMutationHooks;
147
+ observers?: ServiceMutationObservers;
120
148
  }
121
149
  export type ServiceMutationKind = ServiceMutation<unknown>["kind"];
122
150
  export {};
@@ -1,6 +1,8 @@
1
+ import path from "node:path";
1
2
  import { createBackup } from "../utils/backup.js";
2
3
  import { renderTemplate } from "../utils/templates.js";
3
4
  import { deepMergeJson, isJsonObject, pruneJsonByShape } from "../utils/json.js";
5
+ import { parseTomlDocument, serializeTomlDocument, mergeTomlTables } from "../utils/toml.js";
4
6
  export function ensureDirectory(config) {
5
7
  return {
6
8
  kind: "ensureDirectory",
@@ -31,7 +33,13 @@ export function jsonMergeMutation(config) {
31
33
  target: config.target,
32
34
  label: config.label,
33
35
  async transform({ content, context }) {
34
- const current = parseJson(content);
36
+ const rawTarget = resolveValue(config.target, context);
37
+ const targetPath = expandHomeShortcut(rawTarget, context.env);
38
+ const current = await parseJsonWithRecovery({
39
+ content,
40
+ fs: context.fs,
41
+ targetPath
42
+ });
35
43
  const desired = resolveValue(config.value, context);
36
44
  const merged = deepMergeJson(current, desired);
37
45
  const serialized = serializeJson(merged);
@@ -68,6 +76,89 @@ export function jsonPruneMutation(config) {
68
76
  }
69
77
  };
70
78
  }
79
+ export function tomlMergeMutation(config) {
80
+ return {
81
+ kind: "transformFile",
82
+ target: config.target,
83
+ label: config.label,
84
+ async transform({ content, context }) {
85
+ const rawTarget = resolveValue(config.target, context);
86
+ const targetPath = expandHomeShortcut(rawTarget, context.env);
87
+ const current = await parseTomlWithRecovery({
88
+ content,
89
+ fs: context.fs,
90
+ targetPath
91
+ });
92
+ const desired = resolveValue(config.value, context);
93
+ const merged = mergeTomlTables(current, desired);
94
+ const serialized = serializeTomlDocument(merged);
95
+ const previous = content ?? "";
96
+ return {
97
+ content: serialized,
98
+ changed: serialized !== previous
99
+ };
100
+ }
101
+ };
102
+ }
103
+ export function tomlPruneMutation(config) {
104
+ return {
105
+ kind: "transformFile",
106
+ target: config.target,
107
+ label: config.label,
108
+ async transform({ content, context }) {
109
+ if (content == null) {
110
+ return { content: null, changed: false };
111
+ }
112
+ let document;
113
+ try {
114
+ document = parseTomlDocument(content);
115
+ }
116
+ catch {
117
+ return { content, changed: false };
118
+ }
119
+ const outcome = config.prune(document, context);
120
+ if (!outcome.changed) {
121
+ return { content, changed: false };
122
+ }
123
+ if (!outcome.result || Object.keys(outcome.result).length === 0) {
124
+ return { content: null, changed: true };
125
+ }
126
+ const serialized = serializeTomlDocument(outcome.result);
127
+ return {
128
+ content: serialized,
129
+ changed: serialized !== content
130
+ };
131
+ }
132
+ };
133
+ }
134
+ export function tomlTemplateMergeMutation(config) {
135
+ return {
136
+ kind: "transformFile",
137
+ target: config.target,
138
+ label: config.label,
139
+ async transform({ content, context }) {
140
+ const rawTarget = resolveValue(config.target, context);
141
+ const targetPath = expandHomeShortcut(rawTarget, context.env);
142
+ const current = await parseTomlWithRecovery({
143
+ content,
144
+ fs: context.fs,
145
+ targetPath
146
+ });
147
+ const templateContext = config.context
148
+ ? resolveValue(config.context, context)
149
+ : undefined;
150
+ const rendered = await renderTemplate(config.templateId, templateContext ?? {});
151
+ const templateDocument = parseTomlDocument(rendered);
152
+ const merged = mergeTomlTables(current, templateDocument);
153
+ const serialized = serializeTomlDocument(merged);
154
+ const previous = content ?? "";
155
+ return {
156
+ content: serialized,
157
+ changed: serialized !== previous
158
+ };
159
+ }
160
+ };
161
+ }
71
162
  export function removePatternMutation(config) {
72
163
  return {
73
164
  kind: "transformFile",
@@ -102,145 +193,165 @@ export function removeFileMutation(config) {
102
193
  label: config.label
103
194
  };
104
195
  }
105
- export async function runServiceConfigure(manifest, context, runOptions) {
106
- await runMutations(manifest.configure, context, {
107
- trackChanges: false,
108
- hooks: runOptions?.hooks,
109
- manifestId: manifest.id
110
- });
111
- }
112
- export async function runServiceRemove(manifest, context, runOptions) {
113
- return runMutations(manifest.remove ?? [], context, {
114
- trackChanges: true,
115
- hooks: runOptions?.hooks,
116
- manifestId: manifest.id
117
- });
196
+ export function createServiceManifest(definition) {
197
+ const configureMutations = definition.configure;
198
+ const removeMutations = definition.remove;
199
+ return {
200
+ id: definition.id,
201
+ summary: definition.summary,
202
+ configureMutations,
203
+ removeMutations,
204
+ async configure(context, runOptions) {
205
+ await runMutations(configureMutations, context, {
206
+ trackChanges: false,
207
+ observers: runOptions?.observers,
208
+ manifestId: definition.id
209
+ });
210
+ },
211
+ async remove(context, runOptions) {
212
+ if (!removeMutations) {
213
+ return false;
214
+ }
215
+ return runMutations(removeMutations, context, {
216
+ trackChanges: true,
217
+ observers: runOptions?.observers,
218
+ manifestId: definition.id
219
+ });
220
+ }
221
+ };
118
222
  }
119
223
  async function runMutations(mutations, context, options) {
120
224
  let touched = false;
121
225
  for (const mutation of mutations) {
122
- const changed = await applyMutation(mutation, context, options.manifestId, options.hooks);
226
+ const changed = await applyMutation(mutation, context, options.manifestId, options.observers);
123
227
  if (options.trackChanges && changed) {
124
228
  touched = true;
125
229
  }
126
230
  }
127
231
  return touched;
128
232
  }
129
- async function applyMutation(mutation, context, manifestId, hooks) {
233
+ async function applyMutation(mutation, context, manifestId, observers) {
130
234
  switch (mutation.kind) {
131
235
  case "ensureDirectory": {
132
- const targetPath = resolveValue(mutation.path, mutationContext(context));
133
- const details = createMutationDetails(mutation, manifestId, targetPath);
134
- hooks?.onStart?.(details);
236
+ const targetPath = resolvePath(mutation.path, context);
237
+ const details = createMutationDetails(mutation, manifestId, targetPath, context);
238
+ observers?.onStart?.(details);
135
239
  try {
136
240
  const existed = await pathExists(context.fs, targetPath);
137
241
  await context.fs.mkdir(targetPath, { recursive: true });
138
- hooks?.onComplete?.(details, {
242
+ observers?.onComplete?.(details, {
139
243
  changed: !existed,
140
244
  effect: "mkdir",
141
245
  detail: existed ? "noop" : "create"
142
246
  });
247
+ flushCommandDryRun(context);
143
248
  return !existed;
144
249
  }
145
250
  catch (error) {
146
- hooks?.onError?.(details, error);
251
+ observers?.onError?.(details, error);
147
252
  throw error;
148
253
  }
149
254
  }
150
255
  case "createBackup": {
151
- const targetPath = resolveValue(mutation.target, mutationContext(context));
256
+ const targetPath = resolvePath(mutation.target, context);
152
257
  const timestamp = mutation.timestamp
153
258
  ? resolveValue(mutation.timestamp, mutationContext(context))
154
259
  : undefined;
155
- const details = createMutationDetails(mutation, manifestId, targetPath);
156
- hooks?.onStart?.(details);
260
+ const details = createMutationDetails(mutation, manifestId, targetPath, context);
261
+ observers?.onStart?.(details);
157
262
  try {
158
263
  const backupPath = await createBackup(context.fs, targetPath, timestamp);
159
- hooks?.onComplete?.(details, {
264
+ observers?.onComplete?.(details, {
160
265
  changed: backupPath != null,
161
266
  effect: backupPath ? "copy" : "none",
162
267
  detail: backupPath ? "backup" : "noop"
163
268
  });
269
+ flushCommandDryRun(context);
164
270
  }
165
271
  catch (error) {
166
- hooks?.onError?.(details, error);
272
+ observers?.onError?.(details, error);
167
273
  throw error;
168
274
  }
169
275
  return false;
170
276
  }
171
277
  case "writeTemplate": {
172
- const targetPath = resolveValue(mutation.target, mutationContext(context));
278
+ const targetPath = resolvePath(mutation.target, context);
173
279
  const renderContext = mutation.context
174
280
  ? resolveValue(mutation.context, mutationContext(context))
175
281
  : undefined;
176
282
  const rendered = await renderTemplate(mutation.templateId, renderContext ?? {});
177
- const details = createMutationDetails(mutation, manifestId, targetPath);
178
- hooks?.onStart?.(details);
283
+ const details = createMutationDetails(mutation, manifestId, targetPath, context);
284
+ observers?.onStart?.(details);
179
285
  try {
180
286
  const existed = await pathExists(context.fs, targetPath);
181
287
  await context.fs.writeFile(targetPath, rendered, { encoding: "utf8" });
182
- hooks?.onComplete?.(details, {
288
+ observers?.onComplete?.(details, {
183
289
  changed: true,
184
290
  effect: "write",
185
291
  detail: existed ? "update" : "create"
186
292
  });
293
+ flushCommandDryRun(context);
187
294
  }
188
295
  catch (error) {
189
- hooks?.onError?.(details, error);
296
+ observers?.onError?.(details, error);
190
297
  throw error;
191
298
  }
192
299
  return true;
193
300
  }
194
301
  case "removeFile": {
195
- const targetPath = resolveValue(mutation.target, mutationContext(context));
196
- const details = createMutationDetails(mutation, manifestId, targetPath);
197
- hooks?.onStart?.(details);
302
+ const targetPath = resolvePath(mutation.target, context);
303
+ const details = createMutationDetails(mutation, manifestId, targetPath, context);
304
+ observers?.onStart?.(details);
198
305
  try {
199
306
  const raw = await context.fs.readFile(targetPath, "utf8");
200
307
  const trimmed = raw.trim();
201
308
  if (mutation.whenContentMatches &&
202
309
  !mutation.whenContentMatches.test(trimmed)) {
203
- hooks?.onComplete?.(details, {
310
+ observers?.onComplete?.(details, {
204
311
  changed: false,
205
312
  effect: "none",
206
313
  detail: "noop"
207
314
  });
315
+ flushCommandDryRun(context);
208
316
  return false;
209
317
  }
210
318
  if (mutation.whenEmpty && trimmed.length > 0) {
211
- hooks?.onComplete?.(details, {
319
+ observers?.onComplete?.(details, {
212
320
  changed: false,
213
321
  effect: "none",
214
322
  detail: "noop"
215
323
  });
324
+ flushCommandDryRun(context);
216
325
  return false;
217
326
  }
218
327
  await context.fs.unlink(targetPath);
219
- hooks?.onComplete?.(details, {
328
+ observers?.onComplete?.(details, {
220
329
  changed: true,
221
330
  effect: "delete",
222
331
  detail: "delete"
223
332
  });
333
+ flushCommandDryRun(context);
224
334
  return true;
225
335
  }
226
336
  catch (error) {
227
337
  if (isNotFound(error)) {
228
- hooks?.onComplete?.(details, {
338
+ observers?.onComplete?.(details, {
229
339
  changed: false,
230
340
  effect: "none",
231
341
  detail: "noop"
232
342
  });
343
+ flushCommandDryRun(context);
233
344
  return false;
234
345
  }
235
- hooks?.onError?.(details, error);
346
+ observers?.onError?.(details, error);
236
347
  throw error;
237
348
  }
238
349
  }
239
350
  case "transformFile": {
240
- const targetPath = resolveValue(mutation.target, mutationContext(context));
351
+ const targetPath = resolvePath(mutation.target, context);
241
352
  const current = await readFileIfExists(context.fs, targetPath);
242
- const details = createMutationDetails(mutation, manifestId, targetPath);
243
- hooks?.onStart?.(details);
353
+ const details = createMutationDetails(mutation, manifestId, targetPath, context);
354
+ observers?.onStart?.(details);
244
355
  try {
245
356
  const result = await mutation.transform({
246
357
  content: current,
@@ -252,11 +363,12 @@ async function applyMutation(mutation, context, manifestId, hooks) {
252
363
  previousContent: current,
253
364
  result
254
365
  });
255
- hooks?.onComplete?.(details, transformOutcome);
366
+ observers?.onComplete?.(details, transformOutcome);
367
+ flushCommandDryRun(context);
256
368
  return transformOutcome.changed;
257
369
  }
258
370
  catch (error) {
259
- hooks?.onError?.(details, error);
371
+ observers?.onError?.(details, error);
260
372
  throw error;
261
373
  }
262
374
  }
@@ -269,40 +381,81 @@ async function applyMutation(mutation, context, manifestId, hooks) {
269
381
  function mutationContext(context) {
270
382
  return {
271
383
  fs: context.fs,
272
- options: context.options
384
+ options: context.options,
385
+ env: context.env
273
386
  };
274
387
  }
275
- function createMutationDetails(mutation, manifestId, targetPath) {
276
- const subject = (() => {
277
- switch (mutation.kind) {
278
- case "ensureDirectory":
279
- return mutation.label ?? "Ensure directory";
280
- case "createBackup":
281
- return mutation.label ?? "Create backup";
282
- case "writeTemplate":
283
- return (mutation.label ??
284
- `Write template ${mutation.templateId}`);
285
- case "removeFile":
286
- return mutation.label ?? "Remove file";
287
- case "transformFile":
288
- return mutation.label ?? "Transform file";
289
- default:
290
- return "Operation";
291
- }
292
- })();
388
+ function createMutationDetails(mutation, manifestId, targetPath, context) {
389
+ const customLabel = mutation.label != null
390
+ ? resolveValue(mutation.label, mutationContext(context))
391
+ : undefined;
392
+ const label = customLabel ?? describeMutationOperation(mutation.kind, targetPath);
293
393
  return {
294
394
  manifestId,
295
395
  kind: mutation.kind,
296
- label: subject,
396
+ label,
297
397
  targetPath
298
398
  };
299
399
  }
400
+ function describeMutationOperation(kind, targetPath) {
401
+ const displayPath = targetPath ?? "target";
402
+ switch (kind) {
403
+ case "ensureDirectory":
404
+ return `Ensure directory ${displayPath}`;
405
+ case "createBackup":
406
+ return `Create backup ${displayPath}`;
407
+ case "writeTemplate":
408
+ return `Write file ${displayPath}`;
409
+ case "removeFile":
410
+ return `Remove file ${displayPath}`;
411
+ case "transformFile":
412
+ return `Transform file ${displayPath}`;
413
+ default:
414
+ return "Operation";
415
+ }
416
+ }
300
417
  function resolveValue(resolver, context) {
301
418
  if (typeof resolver === "function") {
302
419
  return resolver(context);
303
420
  }
304
421
  return resolver;
305
422
  }
423
+ function resolvePath(resolver, context) {
424
+ const raw = resolveValue(resolver, mutationContext(context));
425
+ return expandHomeShortcut(raw, context.env);
426
+ }
427
+ function flushCommandDryRun(context) {
428
+ context.command.flushDryRun({ emitIfEmpty: false });
429
+ }
430
+ function expandHomeShortcut(targetPath, env) {
431
+ if (!targetPath?.startsWith("~")) {
432
+ return targetPath;
433
+ }
434
+ if (targetPath.startsWith("~./")) {
435
+ targetPath = `~/.${targetPath.slice(3)}`;
436
+ }
437
+ const homeDir = env?.homeDir ?? process.env.HOME ?? process.env.USERPROFILE;
438
+ if (!homeDir) {
439
+ return targetPath;
440
+ }
441
+ let remainder = targetPath.slice(1);
442
+ if (remainder.startsWith("/")) {
443
+ remainder = remainder.slice(1);
444
+ }
445
+ else if (remainder.startsWith("\\")) {
446
+ remainder = remainder.slice(1);
447
+ }
448
+ else if (remainder.startsWith(".")) {
449
+ remainder = remainder.slice(1);
450
+ if (remainder.startsWith("/")) {
451
+ remainder = remainder.slice(1);
452
+ }
453
+ else if (remainder.startsWith("\\")) {
454
+ remainder = remainder.slice(1);
455
+ }
456
+ }
457
+ return remainder.length === 0 ? homeDir : path.join(homeDir, remainder);
458
+ }
306
459
  function parseJson(content) {
307
460
  if (content == null) {
308
461
  return {};
@@ -313,6 +466,50 @@ function parseJson(content) {
313
466
  }
314
467
  return parsed;
315
468
  }
469
+ async function parseJsonWithRecovery(input) {
470
+ try {
471
+ return parseJson(input.content);
472
+ }
473
+ catch {
474
+ await backupInvalidJsonDocument(input);
475
+ return {};
476
+ }
477
+ }
478
+ async function parseTomlWithRecovery(input) {
479
+ if (input.content == null) {
480
+ return {};
481
+ }
482
+ try {
483
+ return parseTomlDocument(input.content);
484
+ }
485
+ catch {
486
+ await backupInvalidTomlDocument(input);
487
+ return {};
488
+ }
489
+ }
490
+ async function backupInvalidJsonDocument(input) {
491
+ if (input.content == null) {
492
+ return;
493
+ }
494
+ const backupPath = createInvalidDocumentBackupPath(input.targetPath);
495
+ await input.fs.writeFile(backupPath, input.content, { encoding: "utf8" });
496
+ }
497
+ async function backupInvalidTomlDocument(input) {
498
+ if (input.content == null) {
499
+ return;
500
+ }
501
+ const backupPath = createInvalidDocumentBackupPath(input.targetPath);
502
+ await input.fs.writeFile(backupPath, input.content, { encoding: "utf8" });
503
+ }
504
+ function createInvalidDocumentBackupPath(targetPath) {
505
+ return `${targetPath}.invalid-${createTimestamp()}.json`;
506
+ }
507
+ function createTimestamp() {
508
+ return new Date()
509
+ .toISOString()
510
+ .replaceAll(":", "-")
511
+ .replaceAll(".", "-");
512
+ }
316
513
  function serializeJson(value) {
317
514
  return `${JSON.stringify(value, null, 2)}\n`;
318
515
  }