toolcraft 0.0.25 → 0.0.27
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/dist/cli.js +11 -9
- package/dist/error-report.js +109 -36
- package/dist/redaction.d.ts +4 -0
- package/dist/redaction.js +70 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +33 -9
- package/node_modules/@poe-code/config-mutations/dist/formats/json.d.ts +2 -1
- package/node_modules/@poe-code/config-mutations/dist/formats/json.js +36 -9
- package/node_modules/@poe-code/config-mutations/dist/types.d.ts +2 -0
- package/node_modules/@poe-code/design-system/dist/components/browser.js +1 -1
- package/node_modules/@poe-code/design-system/dist/explorer/actions.js +1 -1
- package/node_modules/@poe-code/design-system/dist/explorer/keymap.js +11 -1
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +64 -8
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +9 -11
- package/node_modules/@poe-code/design-system/dist/explorer/render/footer.js +18 -8
- package/node_modules/@poe-code/design-system/dist/explorer/render/header.js +11 -18
- package/node_modules/@poe-code/design-system/dist/explorer/render/index.js +2 -10
- package/node_modules/@poe-code/design-system/dist/explorer/render/list.js +32 -22
- package/node_modules/@poe-code/design-system/dist/explorer/render/modal.js +5 -9
- package/node_modules/@poe-code/design-system/dist/explorer/render/text.d.ts +12 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/text.js +81 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.js +2 -0
- package/node_modules/@poe-code/design-system/dist/prompts/index.js +3 -3
- package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +24 -3
- package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +1 -0
- package/node_modules/@poe-code/task-list/dist/backends/gh-issues-sync.js +8 -0
- package/node_modules/auth-store/dist/keychain-store.js +20 -1
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +6 -3
- package/node_modules/tiny-mcp-client/dist/internal.d.ts +2 -0
- package/node_modules/tiny-mcp-client/dist/internal.js +30 -13
- package/node_modules/tiny-mcp-client/src/internal.ts +35 -16
- package/node_modules/tiny-mcp-client/src/transports.test.ts +68 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { invokeWithHumanInLoop } from "./human-in-loop/index.js";
|
|
|
9
9
|
import { resolveMcpProxies } from "./mcp-proxy.js";
|
|
10
10
|
import { getExpectedNumberDescription, isValidNumberSchemaValue } from "./number-schema.js";
|
|
11
11
|
import { findEntrypointPackageMetadata } from "./package-metadata.js";
|
|
12
|
+
import { redactHttpBody, redactHttpHeaderValue } from "./redaction.js";
|
|
12
13
|
import { renderResult } from "./renderer.js";
|
|
13
14
|
import { renderSourceSnippet } from "./source-snippet.js";
|
|
14
15
|
import { enableSourceMaps, formatDebugStack } from "./stack-trim.js";
|
|
@@ -2586,17 +2587,18 @@ function formatGraphQLErrorEnvelopeBody(body) {
|
|
|
2586
2587
|
.join("\n\n");
|
|
2587
2588
|
}
|
|
2588
2589
|
function formatHttpErrorBody(body) {
|
|
2589
|
-
|
|
2590
|
-
|
|
2590
|
+
const redactedBody = redactHttpBody(body);
|
|
2591
|
+
if (typeof redactedBody === "string") {
|
|
2592
|
+
return redactedBody;
|
|
2591
2593
|
}
|
|
2592
|
-
if (isProblemDetailsLike(
|
|
2593
|
-
return formatProblemDetailsBody(
|
|
2594
|
+
if (isProblemDetailsLike(redactedBody)) {
|
|
2595
|
+
return formatProblemDetailsBody(redactedBody);
|
|
2594
2596
|
}
|
|
2595
|
-
if (isGraphQLErrorEnvelopeLike(
|
|
2596
|
-
return formatGraphQLErrorEnvelopeBody(
|
|
2597
|
+
if (isGraphQLErrorEnvelopeLike(redactedBody)) {
|
|
2598
|
+
return formatGraphQLErrorEnvelopeBody(redactedBody);
|
|
2597
2599
|
}
|
|
2598
|
-
const serialized = JSON.stringify(
|
|
2599
|
-
return serialized === undefined ? String(
|
|
2600
|
+
const serialized = JSON.stringify(redactedBody, null, 2);
|
|
2601
|
+
return serialized === undefined ? String(redactedBody) : serialized;
|
|
2600
2602
|
}
|
|
2601
2603
|
function indentHttpErrorBlock(value) {
|
|
2602
2604
|
return value
|
|
@@ -2605,7 +2607,7 @@ function indentHttpErrorBlock(value) {
|
|
|
2605
2607
|
.join("\n");
|
|
2606
2608
|
}
|
|
2607
2609
|
function formatHttpHeaderValue(name, value) {
|
|
2608
|
-
return name
|
|
2610
|
+
return redactHttpHeaderValue(name, value);
|
|
2609
2611
|
}
|
|
2610
2612
|
function formatHttpErrorHeaders(headers) {
|
|
2611
2613
|
return Object.entries(headers).map(([name, value]) => ` ${name}: ${formatHttpHeaderValue(name, value)}`);
|
package/dist/error-report.js
CHANGED
|
@@ -6,9 +6,9 @@ import { CommanderError } from "commander";
|
|
|
6
6
|
import { ApprovalDeclinedError } from "./human-in-loop/types.js";
|
|
7
7
|
import { findProjectRoot } from "./mcp-proxy.js";
|
|
8
8
|
import { findPackageMetadata } from "./package-metadata.js";
|
|
9
|
+
import { isSensitiveName, redactHttpBody, redactHttpHeaderValue } from "./redaction.js";
|
|
9
10
|
import { UserError } from "./user-error.js";
|
|
10
11
|
const ERROR_REPORTS_ENV = "TOOLCRAFT_ERROR_REPORTS";
|
|
11
|
-
const DEFAULT_SENSITIVE_NAMES = ["password", "token", "apikey", "secret"];
|
|
12
12
|
function isPlainObject(value) {
|
|
13
13
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
14
|
}
|
|
@@ -116,9 +116,24 @@ function redactValue(value) {
|
|
|
116
116
|
}
|
|
117
117
|
return `<set, ${value.length} chars>`;
|
|
118
118
|
}
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
function collectStringLeaves(value, output) {
|
|
120
|
+
if (typeof value === "string") {
|
|
121
|
+
if (value.length > 0) {
|
|
122
|
+
output.add(value);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(value)) {
|
|
127
|
+
for (const entry of value) {
|
|
128
|
+
collectStringLeaves(entry, output);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (isPlainObject(value)) {
|
|
133
|
+
for (const entry of Object.values(value)) {
|
|
134
|
+
collectStringLeaves(entry, output);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
122
137
|
}
|
|
123
138
|
function schemaSecretValue(schema) {
|
|
124
139
|
const unwrapped = unwrapOptional(schema);
|
|
@@ -159,6 +174,52 @@ function redactParams(params, command) {
|
|
|
159
174
|
}
|
|
160
175
|
return redactParamsValue(params, command.params, "");
|
|
161
176
|
}
|
|
177
|
+
function collectSensitiveParamValues(value, schema, name, output) {
|
|
178
|
+
if (shouldRedactParam(name, schema)) {
|
|
179
|
+
collectStringLeaves(value, output);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const unwrapped = unwrapOptional(schema);
|
|
183
|
+
if (unwrapped.kind === "object" && isPlainObject(value)) {
|
|
184
|
+
for (const [key, childValue] of Object.entries(value)) {
|
|
185
|
+
const childSchema = unwrapped.shape[key];
|
|
186
|
+
if (childSchema !== undefined) {
|
|
187
|
+
collectSensitiveParamValues(childValue, childSchema, key, output);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (unwrapped.kind === "array" && Array.isArray(value)) {
|
|
193
|
+
for (const entry of value) {
|
|
194
|
+
collectSensitiveParamValues(entry, unwrapped.item, name, output);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function createReportStringRedactor(context, env) {
|
|
199
|
+
const values = new Set();
|
|
200
|
+
for (const value of Object.values(context.secrets ?? {})) {
|
|
201
|
+
if (value !== undefined && value.length > 0) {
|
|
202
|
+
values.add(value);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for (const [name, secret] of Object.entries(context.command?.secrets ?? {})) {
|
|
206
|
+
const value = context.secrets?.[name] ?? env[secret.env];
|
|
207
|
+
if (value !== undefined && value.length > 0) {
|
|
208
|
+
values.add(value);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (context.command !== undefined) {
|
|
212
|
+
collectSensitiveParamValues(context.params, context.command.params, "", values);
|
|
213
|
+
}
|
|
214
|
+
const orderedValues = [...values].sort((left, right) => right.length - left.length);
|
|
215
|
+
return (value) => {
|
|
216
|
+
let redacted = value;
|
|
217
|
+
for (const secretValue of orderedValues) {
|
|
218
|
+
redacted = redacted.split(secretValue).join("<redacted>");
|
|
219
|
+
}
|
|
220
|
+
return redacted;
|
|
221
|
+
};
|
|
222
|
+
}
|
|
162
223
|
function commandSecretEnvNames(secrets) {
|
|
163
224
|
if (secrets === undefined) {
|
|
164
225
|
return [];
|
|
@@ -207,26 +268,36 @@ function redactArgv(argv, options) {
|
|
|
207
268
|
function stableJson(value) {
|
|
208
269
|
return JSON.stringify(value, null, 2) ?? "undefined";
|
|
209
270
|
}
|
|
210
|
-
function redactStructuredErrorField(name, value) {
|
|
211
|
-
if (typeof value === "string"
|
|
212
|
-
|
|
271
|
+
function redactStructuredErrorField(name, value, redactString) {
|
|
272
|
+
if (typeof value === "string") {
|
|
273
|
+
const redactedHeaderValue = redactHttpHeaderValue(name, value);
|
|
274
|
+
if (redactedHeaderValue !== value) {
|
|
275
|
+
return redactedHeaderValue;
|
|
276
|
+
}
|
|
277
|
+
if (isSensitiveName(name)) {
|
|
278
|
+
return "<redacted>";
|
|
279
|
+
}
|
|
280
|
+
return redactString(value);
|
|
213
281
|
}
|
|
214
282
|
if (Array.isArray(value)) {
|
|
215
|
-
return value.map((entry) => redactStructuredErrorField(name, entry));
|
|
283
|
+
return value.map((entry) => redactStructuredErrorField(name, entry, redactString));
|
|
216
284
|
}
|
|
217
285
|
if (isPlainObject(value)) {
|
|
218
|
-
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
286
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
287
|
+
key,
|
|
288
|
+
redactStructuredErrorField(key, entry, redactString)
|
|
289
|
+
]));
|
|
219
290
|
}
|
|
220
291
|
return value;
|
|
221
292
|
}
|
|
222
|
-
function ownStructuredFields(error) {
|
|
293
|
+
function ownStructuredFields(error, redactString) {
|
|
223
294
|
const fields = {};
|
|
224
295
|
for (const key of Object.keys(error)) {
|
|
225
296
|
if (key === "name" || key === "message" || key === "stack" || key === "cause") {
|
|
226
297
|
continue;
|
|
227
298
|
}
|
|
228
299
|
Object.defineProperty(fields, key, {
|
|
229
|
-
value: redactStructuredErrorField(key, error[key]),
|
|
300
|
+
value: redactStructuredErrorField(key, error[key], redactString),
|
|
230
301
|
enumerable: true,
|
|
231
302
|
configurable: true,
|
|
232
303
|
writable: true
|
|
@@ -234,46 +305,47 @@ function ownStructuredFields(error) {
|
|
|
234
305
|
}
|
|
235
306
|
return fields;
|
|
236
307
|
}
|
|
237
|
-
function formatStackChain(error) {
|
|
308
|
+
function formatStackChain(error, redactString) {
|
|
238
309
|
const lines = [];
|
|
239
310
|
let current = error;
|
|
240
311
|
let index = 0;
|
|
241
312
|
while (current !== undefined) {
|
|
242
313
|
if (current instanceof Error) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
: `Caused by: ${current.stack ?? String(current)}`);
|
|
314
|
+
const stack = current.stack ?? String(current);
|
|
315
|
+
lines.push(redactString(index === 0 ? stack : `Caused by: ${stack}`));
|
|
246
316
|
current = current.cause;
|
|
247
317
|
}
|
|
248
318
|
else {
|
|
249
|
-
|
|
319
|
+
const message = String(current);
|
|
320
|
+
lines.push(redactString(index === 0 ? message : `Caused by: ${message}`));
|
|
250
321
|
current = undefined;
|
|
251
322
|
}
|
|
252
323
|
index += 1;
|
|
253
324
|
}
|
|
254
325
|
return lines.join("\n");
|
|
255
326
|
}
|
|
256
|
-
function formatHeaderValue(name, value) {
|
|
257
|
-
return name
|
|
327
|
+
function formatHeaderValue(name, value, redactString) {
|
|
328
|
+
return redactString(redactHttpHeaderValue(name, value));
|
|
258
329
|
}
|
|
259
|
-
function formatHeaders(headers) {
|
|
330
|
+
function formatHeaders(headers, redactString) {
|
|
260
331
|
return Object.entries(headers)
|
|
261
|
-
.map(([name, value]) => `${name}: ${formatHeaderValue(name, value)}`)
|
|
332
|
+
.map(([name, value]) => `${name}: ${formatHeaderValue(name, value, redactString)}`)
|
|
262
333
|
.join("\n");
|
|
263
334
|
}
|
|
264
|
-
function formatBody(body) {
|
|
265
|
-
|
|
266
|
-
|
|
335
|
+
function formatBody(body, redactString) {
|
|
336
|
+
const redactedBody = redactHttpBody(body);
|
|
337
|
+
if (typeof redactedBody === "string") {
|
|
338
|
+
return redactString(redactedBody);
|
|
267
339
|
}
|
|
268
|
-
return stableJson(
|
|
340
|
+
return redactString(stableJson(redactedBody));
|
|
269
341
|
}
|
|
270
|
-
function formatHttpTranscript(error) {
|
|
342
|
+
function formatHttpTranscript(error, redactString) {
|
|
271
343
|
const requestLines = [
|
|
272
344
|
`${error.request.method} ${error.request.url}`,
|
|
273
|
-
formatHeaders(error.request.headers)
|
|
345
|
+
formatHeaders(error.request.headers, redactString)
|
|
274
346
|
].filter((line) => line.length > 0);
|
|
275
347
|
if (error.request.body !== undefined) {
|
|
276
|
-
requestLines.push("", formatBody(error.request.body));
|
|
348
|
+
requestLines.push("", formatBody(error.request.body, redactString));
|
|
277
349
|
}
|
|
278
350
|
return [
|
|
279
351
|
"Request:",
|
|
@@ -281,9 +353,9 @@ function formatHttpTranscript(error) {
|
|
|
281
353
|
"",
|
|
282
354
|
"Response:",
|
|
283
355
|
`${error.response.status} ${error.response.statusText}`,
|
|
284
|
-
formatHeaders(error.response.headers),
|
|
356
|
+
formatHeaders(error.response.headers, redactString),
|
|
285
357
|
"",
|
|
286
|
-
formatBody(error.response.body)
|
|
358
|
+
formatBody(error.response.body, redactString)
|
|
287
359
|
].join("\n");
|
|
288
360
|
}
|
|
289
361
|
function resolveToolcraftVersion(version) {
|
|
@@ -294,9 +366,10 @@ function resolveToolcraftVersion(version) {
|
|
|
294
366
|
function buildReport(context) {
|
|
295
367
|
const env = context.env ?? process.env;
|
|
296
368
|
const error = context.error;
|
|
369
|
+
const redactString = createReportStringRedactor(context, env);
|
|
297
370
|
const errorName = error instanceof Error ? error.name : typeof error;
|
|
298
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
299
|
-
const structuredFields = error instanceof Error ? ownStructuredFields(error) : {};
|
|
371
|
+
const errorMessage = redactString(error instanceof Error ? error.message : String(error));
|
|
372
|
+
const structuredFields = error instanceof Error ? ownStructuredFields(error, redactString) : {};
|
|
300
373
|
const secretLines = Object.entries(context.command?.secrets ?? {}).map(([name, secret]) => {
|
|
301
374
|
const value = context.secrets?.[name] ?? env[secret.env];
|
|
302
375
|
return `${secret.env}=${redactValue(value)}`;
|
|
@@ -310,7 +383,7 @@ function buildReport(context) {
|
|
|
310
383
|
`platform: ${process.platform} ${process.arch}`,
|
|
311
384
|
"",
|
|
312
385
|
"Argv",
|
|
313
|
-
stableJson(redactArgv(context.argv, { command: context.command, secrets: context.secrets })),
|
|
386
|
+
redactString(stableJson(redactArgv(context.argv, { command: context.command, secrets: context.secrets }))),
|
|
314
387
|
"",
|
|
315
388
|
"Resolved Secrets",
|
|
316
389
|
...(secretLines.length === 0 ? ["<none>"] : secretLines),
|
|
@@ -321,19 +394,19 @@ function buildReport(context) {
|
|
|
321
394
|
: context.commandPath,
|
|
322
395
|
"",
|
|
323
396
|
"Parsed Params",
|
|
324
|
-
stableJson(redactParams(context.params, context.command)),
|
|
397
|
+
redactString(stableJson(redactParams(context.params, context.command))),
|
|
325
398
|
"",
|
|
326
399
|
"Error",
|
|
327
400
|
`name: ${errorName}`,
|
|
328
401
|
`message: ${errorMessage}`,
|
|
329
402
|
"structured fields:",
|
|
330
|
-
stableJson(structuredFields),
|
|
403
|
+
redactString(stableJson(structuredFields)),
|
|
331
404
|
"",
|
|
332
405
|
"Stack",
|
|
333
|
-
formatStackChain(error)
|
|
406
|
+
formatStackChain(error, redactString)
|
|
334
407
|
];
|
|
335
408
|
if (hasHttpContext(error)) {
|
|
336
|
-
lines.push("", "HTTP Transcript", formatHttpTranscript(error));
|
|
409
|
+
lines.push("", "HTTP Transcript", formatHttpTranscript(error, redactString));
|
|
337
410
|
}
|
|
338
411
|
return `${lines.join("\n")}\n`;
|
|
339
412
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function isSensitiveName(name: string): boolean;
|
|
2
|
+
export declare function redactSecretLikeFields(value: unknown, name?: string): unknown;
|
|
3
|
+
export declare function redactHttpBody(body: unknown): unknown;
|
|
4
|
+
export declare function redactHttpHeaderValue(name: string, value: string): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const REDACTED_VALUE = "<redacted>";
|
|
2
|
+
const SENSITIVE_NAME_PARTS = ["password", "token", "apikey", "secret"];
|
|
3
|
+
const AUTHORIZATION_HEADER_NAMES = new Set(["authorization", "proxyauthorization"]);
|
|
4
|
+
const SECRET_HEADER_NAMES = new Set(["cookie", "setcookie"]);
|
|
5
|
+
function isPlainObject(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function normalizeName(name) {
|
|
9
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
10
|
+
}
|
|
11
|
+
export function isSensitiveName(name) {
|
|
12
|
+
const normalized = normalizeName(name);
|
|
13
|
+
return SENSITIVE_NAME_PARTS.some((candidate) => normalized.includes(candidate));
|
|
14
|
+
}
|
|
15
|
+
function redactSecretLikeFieldsValue(value, name, seen) {
|
|
16
|
+
if (name.length > 0 && isSensitiveName(name)) {
|
|
17
|
+
return REDACTED_VALUE;
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
if (seen.has(value)) {
|
|
21
|
+
return "[Circular]";
|
|
22
|
+
}
|
|
23
|
+
seen.add(value);
|
|
24
|
+
return value.map((entry) => redactSecretLikeFieldsValue(entry, name, seen));
|
|
25
|
+
}
|
|
26
|
+
if (isPlainObject(value)) {
|
|
27
|
+
if (seen.has(value)) {
|
|
28
|
+
return "[Circular]";
|
|
29
|
+
}
|
|
30
|
+
seen.add(value);
|
|
31
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
|
|
32
|
+
key,
|
|
33
|
+
redactSecretLikeFieldsValue(entry, key, seen)
|
|
34
|
+
]));
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
export function redactSecretLikeFields(value, name = "") {
|
|
39
|
+
return redactSecretLikeFieldsValue(value, name, new WeakSet());
|
|
40
|
+
}
|
|
41
|
+
function parseJsonObjectOrArray(value) {
|
|
42
|
+
const trimmed = value.trim();
|
|
43
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const parsed = JSON.parse(trimmed);
|
|
48
|
+
return isPlainObject(parsed) || Array.isArray(parsed) ? parsed : undefined;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function redactHttpBody(body) {
|
|
55
|
+
if (typeof body === "string") {
|
|
56
|
+
const parsed = parseJsonObjectOrArray(body);
|
|
57
|
+
return parsed === undefined ? body : redactSecretLikeFields(parsed);
|
|
58
|
+
}
|
|
59
|
+
return redactSecretLikeFields(body);
|
|
60
|
+
}
|
|
61
|
+
export function redactHttpHeaderValue(name, value) {
|
|
62
|
+
const normalized = normalizeName(name);
|
|
63
|
+
if (AUTHORIZATION_HEADER_NAMES.has(normalized)) {
|
|
64
|
+
return "Bearer ****";
|
|
65
|
+
}
|
|
66
|
+
if (SECRET_HEADER_NAMES.has(normalized) || isSensitiveName(name)) {
|
|
67
|
+
return REDACTED_VALUE;
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { renderTemplate } from "@poe-code/design-system";
|
|
3
3
|
import { getConfigFormat, detectFormat } from "../formats/index.js";
|
|
4
|
+
import { cloneConfigObject, setConfigEntry } from "../formats/object.js";
|
|
4
5
|
import { resolvePath } from "./path-utils.js";
|
|
5
6
|
import { isNotFound, readFileIfExists, pathExists, createTimestamp } from "../fs-utils.js";
|
|
6
7
|
// ============================================================================
|
|
@@ -120,7 +121,7 @@ function pruneKeysByPrefix(table, prefix) {
|
|
|
120
121
|
const result = {};
|
|
121
122
|
for (const [key, value] of Object.entries(table)) {
|
|
122
123
|
if (!key.startsWith(prefix)) {
|
|
123
|
-
result
|
|
124
|
+
setConfigEntry(result, key, value);
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
return result;
|
|
@@ -129,25 +130,44 @@ function isConfigObject(value) {
|
|
|
129
130
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
130
131
|
}
|
|
131
132
|
function mergeWithPruneByPrefix(base, patch, pruneByPrefix) {
|
|
132
|
-
const result =
|
|
133
|
+
const result = cloneConfigObject(base);
|
|
133
134
|
const prefixMap = pruneByPrefix ?? {};
|
|
134
135
|
for (const [key, value] of Object.entries(patch)) {
|
|
136
|
+
if (value === undefined) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
135
139
|
const current = result[key];
|
|
136
140
|
const prefix = prefixMap[key];
|
|
137
141
|
if (isConfigObject(current) && isConfigObject(value)) {
|
|
138
142
|
if (prefix) {
|
|
139
143
|
const pruned = pruneKeysByPrefix(current, prefix);
|
|
140
|
-
result
|
|
144
|
+
setConfigEntry(result, key, mergePrunedConfigObject(pruned, value));
|
|
141
145
|
}
|
|
142
146
|
else {
|
|
143
|
-
result
|
|
147
|
+
setConfigEntry(result, key, mergeWithPruneByPrefix(current, value, prefixMap));
|
|
144
148
|
}
|
|
145
149
|
continue;
|
|
146
150
|
}
|
|
147
|
-
result
|
|
151
|
+
setConfigEntry(result, key, value);
|
|
148
152
|
}
|
|
149
153
|
return result;
|
|
150
154
|
}
|
|
155
|
+
function mergePrunedConfigObject(base, patch) {
|
|
156
|
+
const result = cloneConfigObject(base);
|
|
157
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
158
|
+
if (value === undefined) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
setConfigEntry(result, key, value);
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
function serializeConfigUpdate(format, rawContent, current, next) {
|
|
166
|
+
if (rawContent !== null && format.serializeUpdate) {
|
|
167
|
+
return format.serializeUpdate(rawContent, current, next);
|
|
168
|
+
}
|
|
169
|
+
return format.serialize(next);
|
|
170
|
+
}
|
|
151
171
|
// ============================================================================
|
|
152
172
|
// Apply Mutation
|
|
153
173
|
// ============================================================================
|
|
@@ -480,6 +500,7 @@ async function applyConfigMerge(mutation, context, options) {
|
|
|
480
500
|
}
|
|
481
501
|
const format = getConfigFormat(formatName);
|
|
482
502
|
const rawContent = await readFileIfExists(context.fs, targetPath);
|
|
503
|
+
let preserveContent = rawContent;
|
|
483
504
|
let current;
|
|
484
505
|
try {
|
|
485
506
|
current = rawContent === null ? {} : format.parse(rawContent);
|
|
@@ -490,9 +511,10 @@ async function applyConfigMerge(mutation, context, options) {
|
|
|
490
511
|
await backupInvalidDocument(context, targetPath, rawContent);
|
|
491
512
|
}
|
|
492
513
|
current = {};
|
|
514
|
+
preserveContent = null;
|
|
493
515
|
}
|
|
494
516
|
const value = resolveValue(mutation.value, options);
|
|
495
|
-
//
|
|
517
|
+
// Keep prefix pruning on the same proto-safe object-write path as normal merges.
|
|
496
518
|
let merged;
|
|
497
519
|
if (mutation.pruneByPrefix) {
|
|
498
520
|
merged = mergeWithPruneByPrefix(current, value, mutation.pruneByPrefix);
|
|
@@ -500,7 +522,7 @@ async function applyConfigMerge(mutation, context, options) {
|
|
|
500
522
|
else {
|
|
501
523
|
merged = format.merge(current, value);
|
|
502
524
|
}
|
|
503
|
-
const serialized = format
|
|
525
|
+
const serialized = serializeConfigUpdate(format, preserveContent, current, merged);
|
|
504
526
|
const changed = serialized !== rawContent;
|
|
505
527
|
if (changed && !context.dryRun) {
|
|
506
528
|
await writeAtomically(context, targetPath, serialized);
|
|
@@ -570,7 +592,7 @@ async function applyConfigPrune(mutation, context, options) {
|
|
|
570
592
|
details
|
|
571
593
|
};
|
|
572
594
|
}
|
|
573
|
-
const serialized = format
|
|
595
|
+
const serialized = serializeConfigUpdate(format, rawContent, current, result);
|
|
574
596
|
if (!context.dryRun) {
|
|
575
597
|
await writeAtomically(context, targetPath, serialized);
|
|
576
598
|
}
|
|
@@ -593,6 +615,7 @@ async function applyConfigTransform(mutation, context, options) {
|
|
|
593
615
|
}
|
|
594
616
|
const format = getConfigFormat(formatName);
|
|
595
617
|
const rawContent = await readFileIfExists(context.fs, targetPath);
|
|
618
|
+
let preserveContent = rawContent;
|
|
596
619
|
let current;
|
|
597
620
|
try {
|
|
598
621
|
current = rawContent === null ? {} : format.parse(rawContent);
|
|
@@ -602,6 +625,7 @@ async function applyConfigTransform(mutation, context, options) {
|
|
|
602
625
|
await backupInvalidDocument(context, targetPath, rawContent);
|
|
603
626
|
}
|
|
604
627
|
current = {};
|
|
628
|
+
preserveContent = null;
|
|
605
629
|
}
|
|
606
630
|
const { content: transformed, changed } = mutation.transform(current, options);
|
|
607
631
|
if (!changed) {
|
|
@@ -626,7 +650,7 @@ async function applyConfigTransform(mutation, context, options) {
|
|
|
626
650
|
details
|
|
627
651
|
};
|
|
628
652
|
}
|
|
629
|
-
const serialized = format
|
|
653
|
+
const serialized = serializeConfigUpdate(format, preserveContent, current, transformed);
|
|
630
654
|
if (!context.dryRun) {
|
|
631
655
|
await writeAtomically(context, targetPath, serialized);
|
|
632
656
|
}
|
|
@@ -27,5 +27,6 @@ declare function mergePreservingComments(content: string, patch: ConfigObject):
|
|
|
27
27
|
* @returns The modified JSON content with comments preserved
|
|
28
28
|
*/
|
|
29
29
|
declare function removeAtPath(content: string, path: (string | number)[]): string;
|
|
30
|
-
|
|
30
|
+
declare function serializeUpdate(content: string, current: ConfigObject, next: ConfigObject): string;
|
|
31
|
+
export { detectIndent, modifyAtPath, mergePreservingComments, removeAtPath, serializeUpdate };
|
|
31
32
|
export declare const jsonFormat: ConfigFormat;
|
|
@@ -48,6 +48,9 @@ function merge(base, patch) {
|
|
|
48
48
|
}
|
|
49
49
|
return result;
|
|
50
50
|
}
|
|
51
|
+
function configValuesEqual(left, right) {
|
|
52
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
53
|
+
}
|
|
51
54
|
function prune(obj, shape) {
|
|
52
55
|
let changed = false;
|
|
53
56
|
const result = cloneConfigObject(obj);
|
|
@@ -116,14 +119,8 @@ function modifyAtPath(content, path, value) {
|
|
|
116
119
|
* @returns The modified JSON content with comments preserved
|
|
117
120
|
*/
|
|
118
121
|
function mergePreservingComments(content, patch) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (value === undefined) {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
result = modifyAtPath(result, [key], value);
|
|
125
|
-
}
|
|
126
|
-
return result;
|
|
122
|
+
const current = parse(content);
|
|
123
|
+
return serializeUpdate(content || "{}", current, merge(current, patch));
|
|
127
124
|
}
|
|
128
125
|
/**
|
|
129
126
|
* Remove a key from JSON content while preserving comments and formatting.
|
|
@@ -135,10 +132,40 @@ function mergePreservingComments(content, patch) {
|
|
|
135
132
|
function removeAtPath(content, path) {
|
|
136
133
|
return modifyAtPath(content, path, undefined);
|
|
137
134
|
}
|
|
138
|
-
|
|
135
|
+
function serializeUpdate(content, current, next) {
|
|
136
|
+
let result = content || "{}";
|
|
137
|
+
result = applyObjectUpdate(result, [], current, next);
|
|
138
|
+
if (!result.endsWith("\n")) {
|
|
139
|
+
result += "\n";
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
function applyObjectUpdate(content, path, current, next) {
|
|
144
|
+
let result = content;
|
|
145
|
+
for (const key of Object.keys(current)) {
|
|
146
|
+
if (!hasConfigEntry(next, key)) {
|
|
147
|
+
result = removeAtPath(result, [...path, key]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
for (const [key, nextValue] of Object.entries(next)) {
|
|
151
|
+
const nextPath = [...path, key];
|
|
152
|
+
const hasCurrent = hasConfigEntry(current, key);
|
|
153
|
+
const currentValue = hasCurrent ? current[key] : undefined;
|
|
154
|
+
if (hasCurrent && isConfigObject(currentValue) && isConfigObject(nextValue)) {
|
|
155
|
+
result = applyObjectUpdate(result, nextPath, currentValue, nextValue);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!hasCurrent || !configValuesEqual(currentValue, nextValue)) {
|
|
159
|
+
result = modifyAtPath(result, nextPath, nextValue);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
export { detectIndent, modifyAtPath, mergePreservingComments, removeAtPath, serializeUpdate };
|
|
139
165
|
export const jsonFormat = {
|
|
140
166
|
parse,
|
|
141
167
|
serialize,
|
|
168
|
+
serializeUpdate,
|
|
142
169
|
merge,
|
|
143
170
|
prune
|
|
144
171
|
};
|
|
@@ -35,6 +35,8 @@ export interface ConfigFormat {
|
|
|
35
35
|
parse(content: string): ConfigObject;
|
|
36
36
|
/** Serialize object to string (with consistent formatting) */
|
|
37
37
|
serialize(obj: ConfigObject): string;
|
|
38
|
+
/** Serialize an update against original content while preserving comments when possible */
|
|
39
|
+
serializeUpdate?(content: string, current: ConfigObject, next: ConfigObject): string;
|
|
38
40
|
/** Deep merge patch into base, returning new object */
|
|
39
41
|
merge(base: ConfigObject, patch: ConfigObject): ConfigObject;
|
|
40
42
|
/** Remove keys matching shape from object, returning new object */
|
|
@@ -10,7 +10,7 @@ function browserCommand(url, platform) {
|
|
|
10
10
|
return { command: "open", args: [url] };
|
|
11
11
|
}
|
|
12
12
|
if (platform === "win32") {
|
|
13
|
-
return { command: "
|
|
13
|
+
return { command: "rundll32.exe", args: ["url.dll,FileProtocolHandler", url] };
|
|
14
14
|
}
|
|
15
15
|
return { command: "xdg-open", args: [url] };
|
|
16
16
|
}
|
|
@@ -32,7 +32,7 @@ function currentDetailItem(state) {
|
|
|
32
32
|
return state.detail.items?.[state.detail.cursor];
|
|
33
33
|
}
|
|
34
34
|
function selectedRows(state, fallback) {
|
|
35
|
-
if (state.selected.size === 0) {
|
|
35
|
+
if (!state.multiSelect || state.selected.size === 0) {
|
|
36
36
|
return fallback.id === "" ? [] : [fallback];
|
|
37
37
|
}
|
|
38
38
|
return state.rows.filter((row) => state.selected.has(row.id));
|
|
@@ -45,14 +45,24 @@ const baseBuiltinCommands = [
|
|
|
45
45
|
"extendSelectionDown"
|
|
46
46
|
];
|
|
47
47
|
const reorderCommands = ["reorderUp", "reorderDown"];
|
|
48
|
+
const selectionCommands = new Set([
|
|
49
|
+
"toggleSelect",
|
|
50
|
+
"selectAll",
|
|
51
|
+
"clearSelection",
|
|
52
|
+
"extendSelectionUp",
|
|
53
|
+
"extendSelectionDown"
|
|
54
|
+
]);
|
|
48
55
|
const reservedActionIds = new Set(["quit"]);
|
|
49
56
|
export function resolveBindings(config, defaults = {}) {
|
|
50
|
-
const
|
|
57
|
+
const baseCommands = config.reorder === undefined
|
|
51
58
|
? baseBuiltinCommands
|
|
52
59
|
: [
|
|
53
60
|
...baseBuiltinCommands.filter((command) => command !== "extendSelectionUp" && command !== "extendSelectionDown"),
|
|
54
61
|
...reorderCommands
|
|
55
62
|
];
|
|
63
|
+
const commands = config.multiSelect === false
|
|
64
|
+
? baseCommands.filter((command) => !selectionCommands.has(command))
|
|
65
|
+
: baseCommands;
|
|
56
66
|
const commandBindings = new Map();
|
|
57
67
|
const flatBindings = new Map();
|
|
58
68
|
const targetKeys = new Map();
|