@smeltjs/core 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/bin.d.ts.map +1 -1
- package/dist/cli/bin.js +9 -4
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/doctor.d.ts +66 -0
- package/dist/cli/doctor.d.ts.map +1 -0
- package/dist/cli/doctor.js +137 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/hooks.d.ts +36 -2
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +108 -64
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +8 -14
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/installed.d.ts +56 -0
- package/dist/cli/installed.d.ts.map +1 -0
- package/dist/cli/installed.js +105 -0
- package/dist/cli/installed.js.map +1 -0
- package/dist/cli/lava.d.ts +31 -0
- package/dist/cli/lava.d.ts.map +1 -0
- package/dist/cli/lava.js +82 -0
- package/dist/cli/lava.js.map +1 -0
- package/dist/cli/setup.d.ts +87 -0
- package/dist/cli/setup.d.ts.map +1 -0
- package/dist/cli/setup.js +513 -0
- package/dist/cli/setup.js.map +1 -0
- package/dist/cli/shell.d.ts +11 -4
- package/dist/cli/shell.d.ts.map +1 -1
- package/dist/cli/shell.js.map +1 -1
- package/dist/cli/subcommands/agents.d.ts.map +1 -1
- package/dist/cli/subcommands/agents.js +2 -1
- package/dist/cli/subcommands/agents.js.map +1 -1
- package/dist/cli/subcommands/doctor.d.ts +15 -0
- package/dist/cli/subcommands/doctor.d.ts.map +1 -0
- package/dist/cli/subcommands/doctor.js +43 -0
- package/dist/cli/subcommands/doctor.js.map +1 -0
- package/dist/cli/subcommands/flags.d.ts +13 -0
- package/dist/cli/subcommands/flags.d.ts.map +1 -1
- package/dist/cli/subcommands/flags.js +27 -1
- package/dist/cli/subcommands/flags.js.map +1 -1
- package/dist/cli/subcommands/hooks.d.ts.map +1 -1
- package/dist/cli/subcommands/hooks.js +10 -2
- package/dist/cli/subcommands/hooks.js.map +1 -1
- package/dist/cli/subcommands/init.d.ts.map +1 -1
- package/dist/cli/subcommands/init.js +2 -1
- package/dist/cli/subcommands/init.js.map +1 -1
- package/dist/cli/subcommands/registry.d.ts +3 -1
- package/dist/cli/subcommands/registry.d.ts.map +1 -1
- package/dist/cli/subcommands/registry.js +6 -0
- package/dist/cli/subcommands/registry.js.map +1 -1
- package/dist/cli/subcommands/setup.d.ts +23 -0
- package/dist/cli/subcommands/setup.d.ts.map +1 -0
- package/dist/cli/subcommands/setup.js +87 -0
- package/dist/cli/subcommands/setup.js.map +1 -0
- package/dist/cli/subcommands/subcommand.d.ts +1 -1
- package/dist/cli/subcommands/subcommand.d.ts.map +1 -1
- package/dist/cli/subcommands/subcommand.js.map +1 -1
- package/dist/cli/wizard.d.ts +72 -0
- package/dist/cli/wizard.d.ts.map +1 -0
- package/dist/cli/wizard.js +86 -0
- package/dist/cli/wizard.js.map +1 -0
- package/dist/harness/claude-code.d.ts.map +1 -1
- package/dist/harness/claude-code.js +10 -0
- package/dist/harness/claude-code.js.map +1 -1
- package/dist/harness/codex.d.ts.map +1 -1
- package/dist/harness/codex.js +1 -0
- package/dist/harness/codex.js.map +1 -1
- package/dist/harness/grok.d.ts.map +1 -1
- package/dist/harness/grok.js +4 -1
- package/dist/harness/grok.js.map +1 -1
- package/dist/harness/kilocode.d.ts.map +1 -1
- package/dist/harness/kilocode.js +1 -1
- package/dist/harness/kilocode.js.map +1 -1
- package/dist/harness/opencode.d.ts.map +1 -1
- package/dist/harness/opencode.js +10 -0
- package/dist/harness/opencode.js.map +1 -1
- package/dist/harness/profile.d.ts +23 -2
- package/dist/harness/profile.d.ts.map +1 -1
- package/dist/harness/profile.js.map +1 -1
- package/dist/harness/snippet.d.ts +14 -1
- package/dist/harness/snippet.d.ts.map +1 -1
- package/dist/harness/snippet.js +18 -2
- package/dist/harness/snippet.js.map +1 -1
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/setup/recipe.d.ts +73 -0
- package/dist/setup/recipe.d.ts.map +1 -0
- package/dist/setup/recipe.js +76 -0
- package/dist/setup/recipe.js.map +1 -0
- package/dist/text/json-edit.d.ts +15 -1
- package/dist/text/json-edit.d.ts.map +1 -1
- package/dist/text/json-edit.js +76 -5
- package/dist/text/json-edit.js.map +1 -1
- package/package.json +1 -1
package/dist/text/json-edit.d.ts
CHANGED
|
@@ -52,7 +52,21 @@ export declare function jsonStyle(text: string): JsonStyle;
|
|
|
52
52
|
* array, a scalar, or something that is not JSON at all. Callers should have
|
|
53
53
|
* `JSON.parse`d first and refused; this is belt and braces, not a validator.
|
|
54
54
|
*/
|
|
55
|
-
export declare function editTopLevelProperty(text: string, key: string, value: unknown, style?: JsonStyle): string | undefined;
|
|
55
|
+
export declare function editTopLevelProperty(text: string, key: string, value: unknown, style?: JsonStyle, valueIndent?: string): string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* `editJsonProperty`: the same contract, one level deeper. Replace, insert or remove
|
|
58
|
+
* the property at `path` — e.g. `['mcpServers', 'smelt']` — where the *container* is a
|
|
59
|
+
* top-level property whose value is itself a JSON object. Everything outside the
|
|
60
|
+
* edited bytes rides through verbatim, including sibling entries inside the container.
|
|
61
|
+
*
|
|
62
|
+
* When the container key is absent and a value is given, the container is created
|
|
63
|
+
* fresh around the entry. When a removal empties the container, the container is
|
|
64
|
+
* lifted out too — a file that never carried the key comes back byte-identical after
|
|
65
|
+
* an apply → remove round trip, and one that carried other entries keeps them
|
|
66
|
+
* untouched. Returns `undefined` when the container's value is not a JSON object the
|
|
67
|
+
* scanner can walk (the caller refuses or skips, as with {@link editTopLevelProperty}).
|
|
68
|
+
*/
|
|
69
|
+
export declare function editJsonProperty(text: string, path: readonly [string, ...(readonly string[])], value: unknown, style?: JsonStyle): string | undefined;
|
|
56
70
|
/**
|
|
57
71
|
* Replace the block delimited by `start` … `end` in `existingText`, or append it.
|
|
58
72
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-edit.d.ts","sourceRoot":"","sources":["../../src/text/json-edit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,kFAAkF;AAClF,MAAM,WAAW,SAAS;IACxB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAKjD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,OAAO,EACd,KAAK,GAAE,SAA2B,GACjC,MAAM,GAAG,SAAS,CAWpB;AA+ID;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV,MAAM,CAUR;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,SAAS,CAQpB","sourcesContent":["/**\n * Byte-faithful edits to somebody else's text file.\n *\n * Two editors, one contract: **change the one thing you were asked to change and\n * leave every other byte alone.** An installer that reformats a settings file has\n * edited what it was never asked to — key order, indentation, string escapes, number\n * spellings and unknown keys are all somebody's choices, and `JSON.parse` →\n * `JSON.stringify` erases every one of them.\n *\n * - {@link editTopLevelProperty}: replace, insert or remove **one top-level property**\n * of a JSON object, in its source text. The value is rendered fresh; nothing\n * outside its bytes moves. Callers have already `JSON.parse`d the text and decided\n * what the new value is; this module holds only the tokenising.\n * - {@link upsertMarkerBlock} / {@link stripMarkerBlock}: the same idea over a\n * delimited block in a plain-text file (an instruction file, a Markdown snippet\n * between two marker lines).\n *\n * Neither knows what a harness or a hook is. `cli/hooks.ts` is the consumer today; the\n * module lives under `src/text/` rather than `cli/` because its whole interface is\n * strings in, strings out — it reads no argv, prints nothing and imports nothing from\n * the CLI, and the next byte-faithful editor (an instruction-file rewrite) wants a\n * sibling here, not a CLI import.\n */\n\n/** How a JSON file is laid out — what a rendered value must match to blend in. */\nexport interface JsonStyle {\n /** The indentation unit — the whitespace before a top-level key. */\n readonly indent: string;\n /** `'\\r\\n'` when the file uses it anywhere, `'\\n'` otherwise. */\n readonly newline: string;\n}\n\n/**\n * The layout an existing file uses: its first indented key's leading whitespace (two\n * spaces when nothing is indented) and its newline convention. Detected once, before\n * any edit, so a sequence of edits renders consistently even after an earlier one has\n * changed the first indented line.\n */\nexport function jsonStyle(text: string): JsonStyle {\n return {\n newline: text.includes('\\r\\n') ? '\\r\\n' : '\\n',\n indent: /\\n([ \\t]+)\"/.exec(text)?.[1] ?? ' ',\n };\n}\n\n/**\n * Replace, insert or remove one top-level property of the JSON object in `text`,\n * leaving every other byte verbatim.\n *\n * - `value` defined: the property's value is replaced in place when the key exists,\n * otherwise the property is appended after the last one (or into an empty object).\n * - `value === undefined`: the property is removed, with its separating comma and\n * whitespace; a key that is not there is a no-op and the text comes back unchanged.\n *\n * The rendered value is `JSON.stringify(value, null, indent)`, re-indented to sit at\n * top level. `style` defaults to {@link jsonStyle} of `text`; pass it explicitly when\n * making several edits to one file.\n *\n * Returns `undefined` when `text` is not a JSON object the scanner can walk — an\n * array, a scalar, or something that is not JSON at all. Callers should have\n * `JSON.parse`d first and refused; this is belt and braces, not a validator.\n */\nexport function editTopLevelProperty(\n text: string,\n key: string,\n value: unknown,\n style: JsonStyle = jsonStyle(text),\n): string | undefined {\n const scan = scanJsonTopLevel(text);\n if (scan === undefined) return undefined;\n const property = scan.properties.find((candidate) => candidate.key === key);\n if (value === undefined) {\n return property === undefined ? text : removeJsonProperty(text, scan, property);\n }\n const rendered = renderJsonValue(value, style.indent, style.newline);\n return property !== undefined\n ? `${text.slice(0, property.valueStart)}${rendered}${text.slice(property.valueEnd)}`\n : insertJsonProperty(text, scan, key, rendered, style.indent, style.newline);\n}\n\n/** One top-level property of a JSON object, located by offsets in its source text. */\ninterface JsonTopLevelProperty {\n readonly key: string;\n /** Offset of the key's opening quote. */\n readonly keyStart: number;\n /** Offset of the value's first byte. */\n readonly valueStart: number;\n /** Offset one past the value's last byte. */\n readonly valueEnd: number;\n}\n\ninterface JsonTopLevelScan {\n /** Offset of the root object's `{`. */\n readonly open: number;\n /** Offset of the root object's `}`. */\n readonly close: number;\n readonly properties: readonly JsonTopLevelProperty[];\n}\n\n/**\n * Locate the top-level properties of a JSON object *in its source text*, so one\n * property can be replaced, inserted or removed while every other byte of the file\n * rides through verbatim. `undefined` when the text is not an object.\n */\nfunction scanJsonTopLevel(text: string): JsonTopLevelScan | undefined {\n let i = skipJsonWhitespace(text, 0);\n if (text[i] !== '{') return undefined;\n const open = i;\n i = skipJsonWhitespace(text, i + 1);\n const properties: JsonTopLevelProperty[] = [];\n if (text[i] === '}') return { open, close: i, properties };\n for (;;) {\n if (text[i] !== '\"') return undefined;\n const keyStart = i;\n const keyEnd = skipJsonString(text, i);\n if (keyEnd === undefined) return undefined;\n const key = JSON.parse(text.slice(keyStart, keyEnd)) as string;\n i = skipJsonWhitespace(text, keyEnd);\n if (text[i] !== ':') return undefined;\n const valueStart = skipJsonWhitespace(text, i + 1);\n const valueEnd = skipJsonValue(text, valueStart);\n if (valueEnd === undefined) return undefined;\n properties.push({ key, keyStart, valueStart, valueEnd });\n i = skipJsonWhitespace(text, valueEnd);\n if (text[i] === ',') {\n i = skipJsonWhitespace(text, i + 1);\n continue;\n }\n if (text[i] === '}') return { open, close: i, properties };\n return undefined;\n }\n}\n\nfunction skipJsonWhitespace(text: string, from: number): number {\n let i = from;\n while (i < text.length && ' \\t\\r\\n'.includes(text[i]!)) i += 1;\n return i;\n}\n\n/** `from` points at `\"`; returns the offset one past the closing quote. */\nfunction skipJsonString(text: string, from: number): number | undefined {\n let i = from + 1;\n while (i < text.length) {\n if (text[i] === '\\\\') i += 2;\n else if (text[i] === '\"') return i + 1;\n else i += 1;\n }\n return undefined;\n}\n\nfunction skipJsonValue(text: string, from: number): number | undefined {\n const first = text[from];\n if (first === '\"') return skipJsonString(text, from);\n if (first === '{' || first === '[') {\n let depth = 0;\n let i = from;\n while (i < text.length) {\n const ch = text[i]!;\n if (ch === '\"') {\n const end = skipJsonString(text, i);\n if (end === undefined) return undefined;\n i = end;\n continue;\n }\n if (ch === '{' || ch === '[') depth += 1;\n else if (ch === '}' || ch === ']') {\n depth -= 1;\n if (depth === 0) return i + 1;\n }\n i += 1;\n }\n return undefined;\n }\n // number / true / false / null\n let i = from;\n while (i < text.length && !',}] \\t\\r\\n'.includes(text[i]!)) i += 1;\n return i > from ? i : undefined;\n}\n\n/** A JSON value indented for embedding at a top-level property position. */\nfunction renderJsonValue(value: unknown, indent: string, newline: string): string {\n return JSON.stringify(value, null, indent).split('\\n').join(`${newline}${indent}`);\n}\n\nfunction removeJsonProperty(\n text: string,\n scan: JsonTopLevelScan,\n property: JsonTopLevelProperty,\n): string {\n const index = scan.properties.indexOf(property);\n const next = scan.properties[index + 1];\n if (next !== undefined) {\n // Delete through the separating comma and whitespace, up to the next key.\n return text.slice(0, property.keyStart) + text.slice(next.keyStart);\n }\n const previous = scan.properties[index - 1];\n // Last (or only) property: delete the preceding comma (if any) with it.\n const from = previous !== undefined ? previous.valueEnd : scan.open + 1;\n return text.slice(0, from) + text.slice(property.valueEnd);\n}\n\nfunction insertJsonProperty(\n text: string,\n scan: JsonTopLevelScan,\n key: string,\n renderedValue: string,\n indent: string,\n newline: string,\n): string {\n const entry = `${JSON.stringify(key)}: ${renderedValue}`;\n if (scan.properties.length === 0) {\n return `${text.slice(0, scan.open + 1)}${newline}${indent}${entry}${newline}${text.slice(scan.close)}`;\n }\n const last = scan.properties[scan.properties.length - 1]!;\n return `${text.slice(0, last.valueEnd)},${newline}${indent}${entry}${text.slice(last.valueEnd)}`;\n}\n\n/* ------------------------------------------------------------------------------------\n * Marker blocks\n * ---------------------------------------------------------------------------------- */\n\n/**\n * Replace the block delimited by `start` … `end` in `existingText`, or append it.\n *\n * An absent or blank file becomes exactly `block`. A file that carries the block has\n * it replaced in place (one newline after `end` is absorbed, so a block that ends in\n * its own newline does not grow a blank line per re-run). A file without it gets the\n * block appended after exactly one blank line, whatever trailing newlines it had.\n */\nexport function upsertMarkerBlock(\n existingText: string | undefined,\n block: string,\n start: string,\n end: string,\n): string {\n if (existingText === undefined || existingText.trim() === '') return block;\n const startIndex = existingText.indexOf(start);\n const endIndex = existingText.indexOf(end);\n if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {\n const before = existingText.slice(0, startIndex);\n const after = existingText.slice(endIndex + end.length).replace(/^\\n/, '');\n return `${before}${block}${after}`;\n }\n return `${existingText.replace(/\\n*$/, '\\n\\n')}${block}`;\n}\n\n/**\n * Remove the block delimited by `start` … `end`. The text comes back unchanged when\n * the block is not there; `undefined` when nothing (or only whitespace) remains — the\n * file was entirely the block, and the caller decides whether to delete it.\n */\nexport function stripMarkerBlock(\n existingText: string,\n start: string,\n end: string,\n): string | undefined {\n const startIndex = existingText.indexOf(start);\n const endIndex = existingText.indexOf(end);\n if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) return existingText;\n const stripped =\n existingText.slice(0, startIndex).replace(/\\n+$/, '\\n') +\n existingText.slice(endIndex + end.length).replace(/^\\n+/, '');\n return stripped.trim() === '' ? undefined : stripped;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"json-edit.d.ts","sourceRoot":"","sources":["../../src/text/json-edit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,kFAAkF;AAClF,MAAM,WAAW,SAAS;IACxB,oEAAoE;IACpE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAKjD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,OAAO,EACd,KAAK,GAAE,SAA2B,EAClC,WAAW,GAAE,MAAqB,GACjC,MAAM,GAAG,SAAS,CAWpB;AAuJD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,SAAS,CAAC,MAAM,EAAE,GAAG,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC,EAC/C,KAAK,EAAE,OAAO,EACd,KAAK,GAAE,SAA2B,GACjC,MAAM,GAAG,SAAS,CAEpB;AA+DD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV,MAAM,CAUR;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,GACV,MAAM,GAAG,SAAS,CAQpB","sourcesContent":["/**\n * Byte-faithful edits to somebody else's text file.\n *\n * Two editors, one contract: **change the one thing you were asked to change and\n * leave every other byte alone.** An installer that reformats a settings file has\n * edited what it was never asked to — key order, indentation, string escapes, number\n * spellings and unknown keys are all somebody's choices, and `JSON.parse` →\n * `JSON.stringify` erases every one of them.\n *\n * - {@link editTopLevelProperty}: replace, insert or remove **one top-level property**\n * of a JSON object, in its source text. The value is rendered fresh; nothing\n * outside its bytes moves. Callers have already `JSON.parse`d the text and decided\n * what the new value is; this module holds only the tokenising.\n * - {@link upsertMarkerBlock} / {@link stripMarkerBlock}: the same idea over a\n * delimited block in a plain-text file (an instruction file, a Markdown snippet\n * between two marker lines).\n *\n * Neither knows what a harness or a hook is. `cli/hooks.ts` is the consumer today; the\n * module lives under `src/text/` rather than `cli/` because its whole interface is\n * strings in, strings out — it reads no argv, prints nothing and imports nothing from\n * the CLI, and the next byte-faithful editor (an instruction-file rewrite) wants a\n * sibling here, not a CLI import.\n */\n\n/** How a JSON file is laid out — what a rendered value must match to blend in. */\nexport interface JsonStyle {\n /** The indentation unit — the whitespace before a top-level key. */\n readonly indent: string;\n /** `'\\r\\n'` when the file uses it anywhere, `'\\n'` otherwise. */\n readonly newline: string;\n}\n\n/**\n * The layout an existing file uses: its first indented key's leading whitespace (two\n * spaces when nothing is indented) and its newline convention. Detected once, before\n * any edit, so a sequence of edits renders consistently even after an earlier one has\n * changed the first indented line.\n */\nexport function jsonStyle(text: string): JsonStyle {\n return {\n newline: text.includes('\\r\\n') ? '\\r\\n' : '\\n',\n indent: /\\n([ \\t]+)\"/.exec(text)?.[1] ?? ' ',\n };\n}\n\n/**\n * Replace, insert or remove one top-level property of the JSON object in `text`,\n * leaving every other byte verbatim.\n *\n * - `value` defined: the property's value is replaced in place when the key exists,\n * otherwise the property is appended after the last one (or into an empty object).\n * - `value === undefined`: the property is removed, with its separating comma and\n * whitespace; a key that is not there is a no-op and the text comes back unchanged.\n *\n * The rendered value is `JSON.stringify(value, null, indent)`, re-indented to sit at\n * top level. `style` defaults to {@link jsonStyle} of `text`; pass it explicitly when\n * making several edits to one file.\n *\n * Returns `undefined` when `text` is not a JSON object the scanner can walk — an\n * array, a scalar, or something that is not JSON at all. Callers should have\n * `JSON.parse`d first and refused; this is belt and braces, not a validator.\n */\nexport function editTopLevelProperty(\n text: string,\n key: string,\n value: unknown,\n style: JsonStyle = jsonStyle(text),\n valueIndent: string = style.indent,\n): string | undefined {\n const scan = scanJsonTopLevel(text);\n if (scan === undefined) return undefined;\n const property = scan.properties.find((candidate) => candidate.key === key);\n if (value === undefined) {\n return property === undefined ? text : removeJsonProperty(text, scan, property);\n }\n const rendered = renderJsonValue(value, valueIndent, style.indent, style.newline);\n return property !== undefined\n ? `${text.slice(0, property.valueStart)}${rendered}${text.slice(property.valueEnd)}`\n : insertJsonProperty(text, scan, key, rendered, style.indent, style.newline);\n}\n\n/** One top-level property of a JSON object, located by offsets in its source text. */\ninterface JsonTopLevelProperty {\n readonly key: string;\n /** Offset of the key's opening quote. */\n readonly keyStart: number;\n /** Offset of the value's first byte. */\n readonly valueStart: number;\n /** Offset one past the value's last byte. */\n readonly valueEnd: number;\n}\n\ninterface JsonTopLevelScan {\n /** Offset of the root object's `{`. */\n readonly open: number;\n /** Offset of the root object's `}`. */\n readonly close: number;\n readonly properties: readonly JsonTopLevelProperty[];\n}\n\n/**\n * Locate the top-level properties of a JSON object *in its source text*, so one\n * property can be replaced, inserted or removed while every other byte of the file\n * rides through verbatim. `undefined` when the text is not an object.\n */\nfunction scanJsonTopLevel(text: string): JsonTopLevelScan | undefined {\n let i = skipJsonWhitespace(text, 0);\n if (text[i] !== '{') return undefined;\n const open = i;\n i = skipJsonWhitespace(text, i + 1);\n const properties: JsonTopLevelProperty[] = [];\n if (text[i] === '}') return { open, close: i, properties };\n for (;;) {\n if (text[i] !== '\"') return undefined;\n const keyStart = i;\n const keyEnd = skipJsonString(text, i);\n if (keyEnd === undefined) return undefined;\n const key = JSON.parse(text.slice(keyStart, keyEnd)) as string;\n i = skipJsonWhitespace(text, keyEnd);\n if (text[i] !== ':') return undefined;\n const valueStart = skipJsonWhitespace(text, i + 1);\n const valueEnd = skipJsonValue(text, valueStart);\n if (valueEnd === undefined) return undefined;\n properties.push({ key, keyStart, valueStart, valueEnd });\n i = skipJsonWhitespace(text, valueEnd);\n if (text[i] === ',') {\n i = skipJsonWhitespace(text, i + 1);\n continue;\n }\n if (text[i] === '}') return { open, close: i, properties };\n return undefined;\n }\n}\n\nfunction skipJsonWhitespace(text: string, from: number): number {\n let i = from;\n while (i < text.length && ' \\t\\r\\n'.includes(text[i]!)) i += 1;\n return i;\n}\n\n/** `from` points at `\"`; returns the offset one past the closing quote. */\nfunction skipJsonString(text: string, from: number): number | undefined {\n let i = from + 1;\n while (i < text.length) {\n if (text[i] === '\\\\') i += 2;\n else if (text[i] === '\"') return i + 1;\n else i += 1;\n }\n return undefined;\n}\n\nfunction skipJsonValue(text: string, from: number): number | undefined {\n const first = text[from];\n if (first === '\"') return skipJsonString(text, from);\n if (first === '{' || first === '[') {\n let depth = 0;\n let i = from;\n while (i < text.length) {\n const ch = text[i]!;\n if (ch === '\"') {\n const end = skipJsonString(text, i);\n if (end === undefined) return undefined;\n i = end;\n continue;\n }\n if (ch === '{' || ch === '[') depth += 1;\n else if (ch === '}' || ch === ']') {\n depth -= 1;\n if (depth === 0) return i + 1;\n }\n i += 1;\n }\n return undefined;\n }\n // number / true / false / null\n let i = from;\n while (i < text.length && !',}] \\t\\r\\n'.includes(text[i]!)) i += 1;\n return i > from ? i : undefined;\n}\n\n/**\n * A JSON value rendered for embedding at a property position. `stringifyIndent` is\n * the unit its own members nest by; `joinIndent` is where the value sits — for a\n * top-level property they are the same, and for a member of a nested container the\n * value sits at the member's indent while its members keep nesting by the file's\n * unit, which is the difference between `command` landing one level under `smelt`\n * or two under it.\n */\nfunction renderJsonValue(\n value: unknown,\n stringifyIndent: string,\n joinIndent: string,\n newline: string,\n): string {\n return JSON.stringify(value, null, stringifyIndent).split('\\n').join(`${newline}${joinIndent}`);\n}\n\nfunction removeJsonProperty(\n text: string,\n scan: JsonTopLevelScan,\n property: JsonTopLevelProperty,\n): string {\n const index = scan.properties.indexOf(property);\n const next = scan.properties[index + 1];\n if (next !== undefined) {\n // Delete through the separating comma and whitespace, up to the next key.\n return text.slice(0, property.keyStart) + text.slice(next.keyStart);\n }\n const previous = scan.properties[index - 1];\n // Last (or only) property: delete the preceding comma (if any) with it.\n const from = previous !== undefined ? previous.valueEnd : scan.open + 1;\n return text.slice(0, from) + text.slice(property.valueEnd);\n}\n\nfunction insertJsonProperty(\n text: string,\n scan: JsonTopLevelScan,\n key: string,\n renderedValue: string,\n indent: string,\n newline: string,\n): string {\n const entry = `${JSON.stringify(key)}: ${renderedValue}`;\n if (scan.properties.length === 0) {\n return `${text.slice(0, scan.open + 1)}${newline}${indent}${entry}${newline}${text.slice(scan.close)}`;\n }\n const last = scan.properties[scan.properties.length - 1]!;\n return `${text.slice(0, last.valueEnd)},${newline}${indent}${entry}${text.slice(last.valueEnd)}`;\n}\n\n/**\n * `editJsonProperty`: the same contract, one level deeper. Replace, insert or remove\n * the property at `path` — e.g. `['mcpServers', 'smelt']` — where the *container* is a\n * top-level property whose value is itself a JSON object. Everything outside the\n * edited bytes rides through verbatim, including sibling entries inside the container.\n *\n * When the container key is absent and a value is given, the container is created\n * fresh around the entry. When a removal empties the container, the container is\n * lifted out too — a file that never carried the key comes back byte-identical after\n * an apply → remove round trip, and one that carried other entries keeps them\n * untouched. Returns `undefined` when the container's value is not a JSON object the\n * scanner can walk (the caller refuses or skips, as with {@link editTopLevelProperty}).\n */\nexport function editJsonProperty(\n text: string,\n path: readonly [string, ...(readonly string[])],\n value: unknown,\n style: JsonStyle = jsonStyle(text),\n): string | undefined {\n return editJsonPropertyAt(text, path, value, style, style.indent);\n}\n\n/** The walker: same body, `path` as a plain (runtime-checked) array. */\nfunction editJsonPropertyAt(\n text: string,\n path: readonly string[],\n value: unknown,\n style: JsonStyle,\n baseIndent: string,\n): string | undefined {\n const head = path[0]!;\n const rest = path.slice(1);\n if (rest.length === 0) return editTopLevelProperty(text, head, value, style, baseIndent);\n const scan = scanJsonTopLevel(text);\n if (scan === undefined) return undefined;\n const property = scan.properties.find((candidate) => candidate.key === head);\n if (property === undefined) {\n if (value === undefined) return text; // nothing to remove under an absent container\n // Build the fresh container from the tail of the path, then hand it to the\n // top-level editor as a plain value — one renderer, one indent story.\n const container: Record<string, unknown> = {};\n let cursor = container;\n for (const key of rest.slice(0, -1)) {\n const next: Record<string, unknown> = {};\n cursor[key] = next;\n cursor = next;\n }\n cursor[rest[rest.length - 1]!] = value;\n return editTopLevelProperty(text, head, container, style);\n }\n const inner = text.slice(property.valueStart, property.valueEnd);\n // The container's members sit one unit deeper than the file's top level — read\n // that unit off the container's own first key, so a fresh member lands beside its\n // siblings and the member's *value* keeps nesting by the file's unit (passed as\n // valueIndent), which is what one level deeper actually means.\n const memberIndent = /\\n([ \\t]+)\"/.exec(inner)?.[1] ?? style.indent + style.indent;\n const edited = editJsonPropertyAt(\n inner,\n rest,\n value,\n { ...style, indent: memberIndent },\n baseIndent,\n );\n if (edited === undefined) return undefined; // the container's value is not an object\n if (edited === inner) return text;\n if (value === undefined && removesToEmptyObject(edited)) {\n // The container is now `{}` and it only got that way because of this removal —\n // lift it out, so a file that never carried the key round-trips byte-identical.\n return editTopLevelProperty(text, head, undefined, style) ?? text;\n }\n return `${text.slice(0, property.valueStart)}${edited}${text.slice(property.valueEnd)}`;\n}\n\n/** True when `text` is exactly a JSON object with no properties. */\nfunction removesToEmptyObject(text: string): boolean {\n const scan = scanJsonTopLevel(text);\n return scan !== undefined && scan.properties.length === 0;\n}\n\n/* ------------------------------------------------------------------------------------\n * Marker blocks\n * ---------------------------------------------------------------------------------- */\n\n/**\n * Replace the block delimited by `start` … `end` in `existingText`, or append it.\n *\n * An absent or blank file becomes exactly `block`. A file that carries the block has\n * it replaced in place (one newline after `end` is absorbed, so a block that ends in\n * its own newline does not grow a blank line per re-run). A file without it gets the\n * block appended after exactly one blank line, whatever trailing newlines it had.\n */\nexport function upsertMarkerBlock(\n existingText: string | undefined,\n block: string,\n start: string,\n end: string,\n): string {\n if (existingText === undefined || existingText.trim() === '') return block;\n const startIndex = existingText.indexOf(start);\n const endIndex = existingText.indexOf(end);\n if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {\n const before = existingText.slice(0, startIndex);\n const after = existingText.slice(endIndex + end.length).replace(/^\\n/, '');\n return `${before}${block}${after}`;\n }\n return `${existingText.replace(/\\n*$/, '\\n\\n')}${block}`;\n}\n\n/**\n * Remove the block delimited by `start` … `end`. The text comes back unchanged when\n * the block is not there; `undefined` when nothing (or only whitespace) remains — the\n * file was entirely the block, and the caller decides whether to delete it.\n */\nexport function stripMarkerBlock(\n existingText: string,\n start: string,\n end: string,\n): string | undefined {\n const startIndex = existingText.indexOf(start);\n const endIndex = existingText.indexOf(end);\n if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) return existingText;\n const stripped =\n existingText.slice(0, startIndex).replace(/\\n+$/, '\\n') +\n existingText.slice(endIndex + end.length).replace(/^\\n+/, '');\n return stripped.trim() === '' ? undefined : stripped;\n}\n"]}
|
package/dist/text/json-edit.js
CHANGED
|
@@ -50,7 +50,7 @@ export function jsonStyle(text) {
|
|
|
50
50
|
* array, a scalar, or something that is not JSON at all. Callers should have
|
|
51
51
|
* `JSON.parse`d first and refused; this is belt and braces, not a validator.
|
|
52
52
|
*/
|
|
53
|
-
export function editTopLevelProperty(text, key, value, style = jsonStyle(text)) {
|
|
53
|
+
export function editTopLevelProperty(text, key, value, style = jsonStyle(text), valueIndent = style.indent) {
|
|
54
54
|
const scan = scanJsonTopLevel(text);
|
|
55
55
|
if (scan === undefined)
|
|
56
56
|
return undefined;
|
|
@@ -58,7 +58,7 @@ export function editTopLevelProperty(text, key, value, style = jsonStyle(text))
|
|
|
58
58
|
if (value === undefined) {
|
|
59
59
|
return property === undefined ? text : removeJsonProperty(text, scan, property);
|
|
60
60
|
}
|
|
61
|
-
const rendered = renderJsonValue(value, style.indent, style.newline);
|
|
61
|
+
const rendered = renderJsonValue(value, valueIndent, style.indent, style.newline);
|
|
62
62
|
return property !== undefined
|
|
63
63
|
? `${text.slice(0, property.valueStart)}${rendered}${text.slice(property.valueEnd)}`
|
|
64
64
|
: insertJsonProperty(text, scan, key, rendered, style.indent, style.newline);
|
|
@@ -155,9 +155,16 @@ function skipJsonValue(text, from) {
|
|
|
155
155
|
i += 1;
|
|
156
156
|
return i > from ? i : undefined;
|
|
157
157
|
}
|
|
158
|
-
/**
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
/**
|
|
159
|
+
* A JSON value rendered for embedding at a property position. `stringifyIndent` is
|
|
160
|
+
* the unit its own members nest by; `joinIndent` is where the value sits — for a
|
|
161
|
+
* top-level property they are the same, and for a member of a nested container the
|
|
162
|
+
* value sits at the member's indent while its members keep nesting by the file's
|
|
163
|
+
* unit, which is the difference between `command` landing one level under `smelt`
|
|
164
|
+
* or two under it.
|
|
165
|
+
*/
|
|
166
|
+
function renderJsonValue(value, stringifyIndent, joinIndent, newline) {
|
|
167
|
+
return JSON.stringify(value, null, stringifyIndent).split('\n').join(`${newline}${joinIndent}`);
|
|
161
168
|
}
|
|
162
169
|
function removeJsonProperty(text, scan, property) {
|
|
163
170
|
const index = scan.properties.indexOf(property);
|
|
@@ -179,6 +186,70 @@ function insertJsonProperty(text, scan, key, renderedValue, indent, newline) {
|
|
|
179
186
|
const last = scan.properties[scan.properties.length - 1];
|
|
180
187
|
return `${text.slice(0, last.valueEnd)},${newline}${indent}${entry}${text.slice(last.valueEnd)}`;
|
|
181
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* `editJsonProperty`: the same contract, one level deeper. Replace, insert or remove
|
|
191
|
+
* the property at `path` — e.g. `['mcpServers', 'smelt']` — where the *container* is a
|
|
192
|
+
* top-level property whose value is itself a JSON object. Everything outside the
|
|
193
|
+
* edited bytes rides through verbatim, including sibling entries inside the container.
|
|
194
|
+
*
|
|
195
|
+
* When the container key is absent and a value is given, the container is created
|
|
196
|
+
* fresh around the entry. When a removal empties the container, the container is
|
|
197
|
+
* lifted out too — a file that never carried the key comes back byte-identical after
|
|
198
|
+
* an apply → remove round trip, and one that carried other entries keeps them
|
|
199
|
+
* untouched. Returns `undefined` when the container's value is not a JSON object the
|
|
200
|
+
* scanner can walk (the caller refuses or skips, as with {@link editTopLevelProperty}).
|
|
201
|
+
*/
|
|
202
|
+
export function editJsonProperty(text, path, value, style = jsonStyle(text)) {
|
|
203
|
+
return editJsonPropertyAt(text, path, value, style, style.indent);
|
|
204
|
+
}
|
|
205
|
+
/** The walker: same body, `path` as a plain (runtime-checked) array. */
|
|
206
|
+
function editJsonPropertyAt(text, path, value, style, baseIndent) {
|
|
207
|
+
const head = path[0];
|
|
208
|
+
const rest = path.slice(1);
|
|
209
|
+
if (rest.length === 0)
|
|
210
|
+
return editTopLevelProperty(text, head, value, style, baseIndent);
|
|
211
|
+
const scan = scanJsonTopLevel(text);
|
|
212
|
+
if (scan === undefined)
|
|
213
|
+
return undefined;
|
|
214
|
+
const property = scan.properties.find((candidate) => candidate.key === head);
|
|
215
|
+
if (property === undefined) {
|
|
216
|
+
if (value === undefined)
|
|
217
|
+
return text; // nothing to remove under an absent container
|
|
218
|
+
// Build the fresh container from the tail of the path, then hand it to the
|
|
219
|
+
// top-level editor as a plain value — one renderer, one indent story.
|
|
220
|
+
const container = {};
|
|
221
|
+
let cursor = container;
|
|
222
|
+
for (const key of rest.slice(0, -1)) {
|
|
223
|
+
const next = {};
|
|
224
|
+
cursor[key] = next;
|
|
225
|
+
cursor = next;
|
|
226
|
+
}
|
|
227
|
+
cursor[rest[rest.length - 1]] = value;
|
|
228
|
+
return editTopLevelProperty(text, head, container, style);
|
|
229
|
+
}
|
|
230
|
+
const inner = text.slice(property.valueStart, property.valueEnd);
|
|
231
|
+
// The container's members sit one unit deeper than the file's top level — read
|
|
232
|
+
// that unit off the container's own first key, so a fresh member lands beside its
|
|
233
|
+
// siblings and the member's *value* keeps nesting by the file's unit (passed as
|
|
234
|
+
// valueIndent), which is what one level deeper actually means.
|
|
235
|
+
const memberIndent = /\n([ \t]+)"/.exec(inner)?.[1] ?? style.indent + style.indent;
|
|
236
|
+
const edited = editJsonPropertyAt(inner, rest, value, { ...style, indent: memberIndent }, baseIndent);
|
|
237
|
+
if (edited === undefined)
|
|
238
|
+
return undefined; // the container's value is not an object
|
|
239
|
+
if (edited === inner)
|
|
240
|
+
return text;
|
|
241
|
+
if (value === undefined && removesToEmptyObject(edited)) {
|
|
242
|
+
// The container is now `{}` and it only got that way because of this removal —
|
|
243
|
+
// lift it out, so a file that never carried the key round-trips byte-identical.
|
|
244
|
+
return editTopLevelProperty(text, head, undefined, style) ?? text;
|
|
245
|
+
}
|
|
246
|
+
return `${text.slice(0, property.valueStart)}${edited}${text.slice(property.valueEnd)}`;
|
|
247
|
+
}
|
|
248
|
+
/** True when `text` is exactly a JSON object with no properties. */
|
|
249
|
+
function removesToEmptyObject(text) {
|
|
250
|
+
const scan = scanJsonTopLevel(text);
|
|
251
|
+
return scan !== undefined && scan.properties.length === 0;
|
|
252
|
+
}
|
|
182
253
|
/* ------------------------------------------------------------------------------------
|
|
183
254
|
* Marker blocks
|
|
184
255
|
* ---------------------------------------------------------------------------------- */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json-edit.js","sourceRoot":"","sources":["../../src/text/json-edit.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Byte-faithful edits to somebody else's text file.\n *\n * Two editors, one contract: **change the one thing you were asked to change and\n * leave every other byte alone.** An installer that reformats a settings file has\n * edited what it was never asked to — key order, indentation, string escapes, number\n * spellings and unknown keys are all somebody's choices, and `JSON.parse` →\n * `JSON.stringify` erases every one of them.\n *\n * - {@link editTopLevelProperty}: replace, insert or remove **one top-level property**\n * of a JSON object, in its source text. The value is rendered fresh; nothing\n * outside its bytes moves. Callers have already `JSON.parse`d the text and decided\n * what the new value is; this module holds only the tokenising.\n * - {@link upsertMarkerBlock} / {@link stripMarkerBlock}: the same idea over a\n * delimited block in a plain-text file (an instruction file, a Markdown snippet\n * between two marker lines).\n *\n * Neither knows what a harness or a hook is. `cli/hooks.ts` is the consumer today; the\n * module lives under `src/text/` rather than `cli/` because its whole interface is\n * strings in, strings out — it reads no argv, prints nothing and imports nothing from\n * the CLI, and the next byte-faithful editor (an instruction-file rewrite) wants a\n * sibling here, not a CLI import.\n */\n\n/** How a JSON file is laid out — what a rendered value must match to blend in. */\nexport interface JsonStyle {\n /** The indentation unit — the whitespace before a top-level key. */\n readonly indent: string;\n /** `'\\r\\n'` when the file uses it anywhere, `'\\n'` otherwise. */\n readonly newline: string;\n}\n\n/**\n * The layout an existing file uses: its first indented key's leading whitespace (two\n * spaces when nothing is indented) and its newline convention. Detected once, before\n * any edit, so a sequence of edits renders consistently even after an earlier one has\n * changed the first indented line.\n */\nexport function jsonStyle(text: string): JsonStyle {\n return {\n newline: text.includes('\\r\\n') ? '\\r\\n' : '\\n',\n indent: /\\n([ \\t]+)\"/.exec(text)?.[1] ?? ' ',\n };\n}\n\n/**\n * Replace, insert or remove one top-level property of the JSON object in `text`,\n * leaving every other byte verbatim.\n *\n * - `value` defined: the property's value is replaced in place when the key exists,\n * otherwise the property is appended after the last one (or into an empty object).\n * - `value === undefined`: the property is removed, with its separating comma and\n * whitespace; a key that is not there is a no-op and the text comes back unchanged.\n *\n * The rendered value is `JSON.stringify(value, null, indent)`, re-indented to sit at\n * top level. `style` defaults to {@link jsonStyle} of `text`; pass it explicitly when\n * making several edits to one file.\n *\n * Returns `undefined` when `text` is not a JSON object the scanner can walk — an\n * array, a scalar, or something that is not JSON at all. Callers should have\n * `JSON.parse`d first and refused; this is belt and braces, not a validator.\n */\nexport function editTopLevelProperty(\n text: string,\n key: string,\n value: unknown,\n style: JsonStyle = jsonStyle(text),\n): string | undefined {\n const scan = scanJsonTopLevel(text);\n if (scan === undefined) return undefined;\n const property = scan.properties.find((candidate) => candidate.key === key);\n if (value === undefined) {\n return property === undefined ? text : removeJsonProperty(text, scan, property);\n }\n const rendered = renderJsonValue(value, style.indent, style.newline);\n return property !== undefined\n ? `${text.slice(0, property.valueStart)}${rendered}${text.slice(property.valueEnd)}`\n : insertJsonProperty(text, scan, key, rendered, style.indent, style.newline);\n}\n\n/** One top-level property of a JSON object, located by offsets in its source text. */\ninterface JsonTopLevelProperty {\n readonly key: string;\n /** Offset of the key's opening quote. */\n readonly keyStart: number;\n /** Offset of the value's first byte. */\n readonly valueStart: number;\n /** Offset one past the value's last byte. */\n readonly valueEnd: number;\n}\n\ninterface JsonTopLevelScan {\n /** Offset of the root object's `{`. */\n readonly open: number;\n /** Offset of the root object's `}`. */\n readonly close: number;\n readonly properties: readonly JsonTopLevelProperty[];\n}\n\n/**\n * Locate the top-level properties of a JSON object *in its source text*, so one\n * property can be replaced, inserted or removed while every other byte of the file\n * rides through verbatim. `undefined` when the text is not an object.\n */\nfunction scanJsonTopLevel(text: string): JsonTopLevelScan | undefined {\n let i = skipJsonWhitespace(text, 0);\n if (text[i] !== '{') return undefined;\n const open = i;\n i = skipJsonWhitespace(text, i + 1);\n const properties: JsonTopLevelProperty[] = [];\n if (text[i] === '}') return { open, close: i, properties };\n for (;;) {\n if (text[i] !== '\"') return undefined;\n const keyStart = i;\n const keyEnd = skipJsonString(text, i);\n if (keyEnd === undefined) return undefined;\n const key = JSON.parse(text.slice(keyStart, keyEnd)) as string;\n i = skipJsonWhitespace(text, keyEnd);\n if (text[i] !== ':') return undefined;\n const valueStart = skipJsonWhitespace(text, i + 1);\n const valueEnd = skipJsonValue(text, valueStart);\n if (valueEnd === undefined) return undefined;\n properties.push({ key, keyStart, valueStart, valueEnd });\n i = skipJsonWhitespace(text, valueEnd);\n if (text[i] === ',') {\n i = skipJsonWhitespace(text, i + 1);\n continue;\n }\n if (text[i] === '}') return { open, close: i, properties };\n return undefined;\n }\n}\n\nfunction skipJsonWhitespace(text: string, from: number): number {\n let i = from;\n while (i < text.length && ' \\t\\r\\n'.includes(text[i]!)) i += 1;\n return i;\n}\n\n/** `from` points at `\"`; returns the offset one past the closing quote. */\nfunction skipJsonString(text: string, from: number): number | undefined {\n let i = from + 1;\n while (i < text.length) {\n if (text[i] === '\\\\') i += 2;\n else if (text[i] === '\"') return i + 1;\n else i += 1;\n }\n return undefined;\n}\n\nfunction skipJsonValue(text: string, from: number): number | undefined {\n const first = text[from];\n if (first === '\"') return skipJsonString(text, from);\n if (first === '{' || first === '[') {\n let depth = 0;\n let i = from;\n while (i < text.length) {\n const ch = text[i]!;\n if (ch === '\"') {\n const end = skipJsonString(text, i);\n if (end === undefined) return undefined;\n i = end;\n continue;\n }\n if (ch === '{' || ch === '[') depth += 1;\n else if (ch === '}' || ch === ']') {\n depth -= 1;\n if (depth === 0) return i + 1;\n }\n i += 1;\n }\n return undefined;\n }\n // number / true / false / null\n let i = from;\n while (i < text.length && !',}] \\t\\r\\n'.includes(text[i]!)) i += 1;\n return i > from ? i : undefined;\n}\n\n/** A JSON value indented for embedding at a top-level property position. */\nfunction renderJsonValue(value: unknown, indent: string, newline: string): string {\n return JSON.stringify(value, null, indent).split('\\n').join(`${newline}${indent}`);\n}\n\nfunction removeJsonProperty(\n text: string,\n scan: JsonTopLevelScan,\n property: JsonTopLevelProperty,\n): string {\n const index = scan.properties.indexOf(property);\n const next = scan.properties[index + 1];\n if (next !== undefined) {\n // Delete through the separating comma and whitespace, up to the next key.\n return text.slice(0, property.keyStart) + text.slice(next.keyStart);\n }\n const previous = scan.properties[index - 1];\n // Last (or only) property: delete the preceding comma (if any) with it.\n const from = previous !== undefined ? previous.valueEnd : scan.open + 1;\n return text.slice(0, from) + text.slice(property.valueEnd);\n}\n\nfunction insertJsonProperty(\n text: string,\n scan: JsonTopLevelScan,\n key: string,\n renderedValue: string,\n indent: string,\n newline: string,\n): string {\n const entry = `${JSON.stringify(key)}: ${renderedValue}`;\n if (scan.properties.length === 0) {\n return `${text.slice(0, scan.open + 1)}${newline}${indent}${entry}${newline}${text.slice(scan.close)}`;\n }\n const last = scan.properties[scan.properties.length - 1]!;\n return `${text.slice(0, last.valueEnd)},${newline}${indent}${entry}${text.slice(last.valueEnd)}`;\n}\n\n/* ------------------------------------------------------------------------------------\n * Marker blocks\n * ---------------------------------------------------------------------------------- */\n\n/**\n * Replace the block delimited by `start` … `end` in `existingText`, or append it.\n *\n * An absent or blank file becomes exactly `block`. A file that carries the block has\n * it replaced in place (one newline after `end` is absorbed, so a block that ends in\n * its own newline does not grow a blank line per re-run). A file without it gets the\n * block appended after exactly one blank line, whatever trailing newlines it had.\n */\nexport function upsertMarkerBlock(\n existingText: string | undefined,\n block: string,\n start: string,\n end: string,\n): string {\n if (existingText === undefined || existingText.trim() === '') return block;\n const startIndex = existingText.indexOf(start);\n const endIndex = existingText.indexOf(end);\n if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {\n const before = existingText.slice(0, startIndex);\n const after = existingText.slice(endIndex + end.length).replace(/^\\n/, '');\n return `${before}${block}${after}`;\n }\n return `${existingText.replace(/\\n*$/, '\\n\\n')}${block}`;\n}\n\n/**\n * Remove the block delimited by `start` … `end`. The text comes back unchanged when\n * the block is not there; `undefined` when nothing (or only whitespace) remains — the\n * file was entirely the block, and the caller decides whether to delete it.\n */\nexport function stripMarkerBlock(\n existingText: string,\n start: string,\n end: string,\n): string | undefined {\n const startIndex = existingText.indexOf(start);\n const endIndex = existingText.indexOf(end);\n if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) return existingText;\n const stripped =\n existingText.slice(0, startIndex).replace(/\\n+$/, '\\n') +\n existingText.slice(endIndex + end.length).replace(/^\\n+/, '');\n return stripped.trim() === '' ? undefined : stripped;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"json-edit.js","sourceRoot":"","sources":["../../src/text/json-edit.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Byte-faithful edits to somebody else's text file.\n *\n * Two editors, one contract: **change the one thing you were asked to change and\n * leave every other byte alone.** An installer that reformats a settings file has\n * edited what it was never asked to — key order, indentation, string escapes, number\n * spellings and unknown keys are all somebody's choices, and `JSON.parse` →\n * `JSON.stringify` erases every one of them.\n *\n * - {@link editTopLevelProperty}: replace, insert or remove **one top-level property**\n * of a JSON object, in its source text. The value is rendered fresh; nothing\n * outside its bytes moves. Callers have already `JSON.parse`d the text and decided\n * what the new value is; this module holds only the tokenising.\n * - {@link upsertMarkerBlock} / {@link stripMarkerBlock}: the same idea over a\n * delimited block in a plain-text file (an instruction file, a Markdown snippet\n * between two marker lines).\n *\n * Neither knows what a harness or a hook is. `cli/hooks.ts` is the consumer today; the\n * module lives under `src/text/` rather than `cli/` because its whole interface is\n * strings in, strings out — it reads no argv, prints nothing and imports nothing from\n * the CLI, and the next byte-faithful editor (an instruction-file rewrite) wants a\n * sibling here, not a CLI import.\n */\n\n/** How a JSON file is laid out — what a rendered value must match to blend in. */\nexport interface JsonStyle {\n /** The indentation unit — the whitespace before a top-level key. */\n readonly indent: string;\n /** `'\\r\\n'` when the file uses it anywhere, `'\\n'` otherwise. */\n readonly newline: string;\n}\n\n/**\n * The layout an existing file uses: its first indented key's leading whitespace (two\n * spaces when nothing is indented) and its newline convention. Detected once, before\n * any edit, so a sequence of edits renders consistently even after an earlier one has\n * changed the first indented line.\n */\nexport function jsonStyle(text: string): JsonStyle {\n return {\n newline: text.includes('\\r\\n') ? '\\r\\n' : '\\n',\n indent: /\\n([ \\t]+)\"/.exec(text)?.[1] ?? ' ',\n };\n}\n\n/**\n * Replace, insert or remove one top-level property of the JSON object in `text`,\n * leaving every other byte verbatim.\n *\n * - `value` defined: the property's value is replaced in place when the key exists,\n * otherwise the property is appended after the last one (or into an empty object).\n * - `value === undefined`: the property is removed, with its separating comma and\n * whitespace; a key that is not there is a no-op and the text comes back unchanged.\n *\n * The rendered value is `JSON.stringify(value, null, indent)`, re-indented to sit at\n * top level. `style` defaults to {@link jsonStyle} of `text`; pass it explicitly when\n * making several edits to one file.\n *\n * Returns `undefined` when `text` is not a JSON object the scanner can walk — an\n * array, a scalar, or something that is not JSON at all. Callers should have\n * `JSON.parse`d first and refused; this is belt and braces, not a validator.\n */\nexport function editTopLevelProperty(\n text: string,\n key: string,\n value: unknown,\n style: JsonStyle = jsonStyle(text),\n valueIndent: string = style.indent,\n): string | undefined {\n const scan = scanJsonTopLevel(text);\n if (scan === undefined) return undefined;\n const property = scan.properties.find((candidate) => candidate.key === key);\n if (value === undefined) {\n return property === undefined ? text : removeJsonProperty(text, scan, property);\n }\n const rendered = renderJsonValue(value, valueIndent, style.indent, style.newline);\n return property !== undefined\n ? `${text.slice(0, property.valueStart)}${rendered}${text.slice(property.valueEnd)}`\n : insertJsonProperty(text, scan, key, rendered, style.indent, style.newline);\n}\n\n/** One top-level property of a JSON object, located by offsets in its source text. */\ninterface JsonTopLevelProperty {\n readonly key: string;\n /** Offset of the key's opening quote. */\n readonly keyStart: number;\n /** Offset of the value's first byte. */\n readonly valueStart: number;\n /** Offset one past the value's last byte. */\n readonly valueEnd: number;\n}\n\ninterface JsonTopLevelScan {\n /** Offset of the root object's `{`. */\n readonly open: number;\n /** Offset of the root object's `}`. */\n readonly close: number;\n readonly properties: readonly JsonTopLevelProperty[];\n}\n\n/**\n * Locate the top-level properties of a JSON object *in its source text*, so one\n * property can be replaced, inserted or removed while every other byte of the file\n * rides through verbatim. `undefined` when the text is not an object.\n */\nfunction scanJsonTopLevel(text: string): JsonTopLevelScan | undefined {\n let i = skipJsonWhitespace(text, 0);\n if (text[i] !== '{') return undefined;\n const open = i;\n i = skipJsonWhitespace(text, i + 1);\n const properties: JsonTopLevelProperty[] = [];\n if (text[i] === '}') return { open, close: i, properties };\n for (;;) {\n if (text[i] !== '\"') return undefined;\n const keyStart = i;\n const keyEnd = skipJsonString(text, i);\n if (keyEnd === undefined) return undefined;\n const key = JSON.parse(text.slice(keyStart, keyEnd)) as string;\n i = skipJsonWhitespace(text, keyEnd);\n if (text[i] !== ':') return undefined;\n const valueStart = skipJsonWhitespace(text, i + 1);\n const valueEnd = skipJsonValue(text, valueStart);\n if (valueEnd === undefined) return undefined;\n properties.push({ key, keyStart, valueStart, valueEnd });\n i = skipJsonWhitespace(text, valueEnd);\n if (text[i] === ',') {\n i = skipJsonWhitespace(text, i + 1);\n continue;\n }\n if (text[i] === '}') return { open, close: i, properties };\n return undefined;\n }\n}\n\nfunction skipJsonWhitespace(text: string, from: number): number {\n let i = from;\n while (i < text.length && ' \\t\\r\\n'.includes(text[i]!)) i += 1;\n return i;\n}\n\n/** `from` points at `\"`; returns the offset one past the closing quote. */\nfunction skipJsonString(text: string, from: number): number | undefined {\n let i = from + 1;\n while (i < text.length) {\n if (text[i] === '\\\\') i += 2;\n else if (text[i] === '\"') return i + 1;\n else i += 1;\n }\n return undefined;\n}\n\nfunction skipJsonValue(text: string, from: number): number | undefined {\n const first = text[from];\n if (first === '\"') return skipJsonString(text, from);\n if (first === '{' || first === '[') {\n let depth = 0;\n let i = from;\n while (i < text.length) {\n const ch = text[i]!;\n if (ch === '\"') {\n const end = skipJsonString(text, i);\n if (end === undefined) return undefined;\n i = end;\n continue;\n }\n if (ch === '{' || ch === '[') depth += 1;\n else if (ch === '}' || ch === ']') {\n depth -= 1;\n if (depth === 0) return i + 1;\n }\n i += 1;\n }\n return undefined;\n }\n // number / true / false / null\n let i = from;\n while (i < text.length && !',}] \\t\\r\\n'.includes(text[i]!)) i += 1;\n return i > from ? i : undefined;\n}\n\n/**\n * A JSON value rendered for embedding at a property position. `stringifyIndent` is\n * the unit its own members nest by; `joinIndent` is where the value sits — for a\n * top-level property they are the same, and for a member of a nested container the\n * value sits at the member's indent while its members keep nesting by the file's\n * unit, which is the difference between `command` landing one level under `smelt`\n * or two under it.\n */\nfunction renderJsonValue(\n value: unknown,\n stringifyIndent: string,\n joinIndent: string,\n newline: string,\n): string {\n return JSON.stringify(value, null, stringifyIndent).split('\\n').join(`${newline}${joinIndent}`);\n}\n\nfunction removeJsonProperty(\n text: string,\n scan: JsonTopLevelScan,\n property: JsonTopLevelProperty,\n): string {\n const index = scan.properties.indexOf(property);\n const next = scan.properties[index + 1];\n if (next !== undefined) {\n // Delete through the separating comma and whitespace, up to the next key.\n return text.slice(0, property.keyStart) + text.slice(next.keyStart);\n }\n const previous = scan.properties[index - 1];\n // Last (or only) property: delete the preceding comma (if any) with it.\n const from = previous !== undefined ? previous.valueEnd : scan.open + 1;\n return text.slice(0, from) + text.slice(property.valueEnd);\n}\n\nfunction insertJsonProperty(\n text: string,\n scan: JsonTopLevelScan,\n key: string,\n renderedValue: string,\n indent: string,\n newline: string,\n): string {\n const entry = `${JSON.stringify(key)}: ${renderedValue}`;\n if (scan.properties.length === 0) {\n return `${text.slice(0, scan.open + 1)}${newline}${indent}${entry}${newline}${text.slice(scan.close)}`;\n }\n const last = scan.properties[scan.properties.length - 1]!;\n return `${text.slice(0, last.valueEnd)},${newline}${indent}${entry}${text.slice(last.valueEnd)}`;\n}\n\n/**\n * `editJsonProperty`: the same contract, one level deeper. Replace, insert or remove\n * the property at `path` — e.g. `['mcpServers', 'smelt']` — where the *container* is a\n * top-level property whose value is itself a JSON object. Everything outside the\n * edited bytes rides through verbatim, including sibling entries inside the container.\n *\n * When the container key is absent and a value is given, the container is created\n * fresh around the entry. When a removal empties the container, the container is\n * lifted out too — a file that never carried the key comes back byte-identical after\n * an apply → remove round trip, and one that carried other entries keeps them\n * untouched. Returns `undefined` when the container's value is not a JSON object the\n * scanner can walk (the caller refuses or skips, as with {@link editTopLevelProperty}).\n */\nexport function editJsonProperty(\n text: string,\n path: readonly [string, ...(readonly string[])],\n value: unknown,\n style: JsonStyle = jsonStyle(text),\n): string | undefined {\n return editJsonPropertyAt(text, path, value, style, style.indent);\n}\n\n/** The walker: same body, `path` as a plain (runtime-checked) array. */\nfunction editJsonPropertyAt(\n text: string,\n path: readonly string[],\n value: unknown,\n style: JsonStyle,\n baseIndent: string,\n): string | undefined {\n const head = path[0]!;\n const rest = path.slice(1);\n if (rest.length === 0) return editTopLevelProperty(text, head, value, style, baseIndent);\n const scan = scanJsonTopLevel(text);\n if (scan === undefined) return undefined;\n const property = scan.properties.find((candidate) => candidate.key === head);\n if (property === undefined) {\n if (value === undefined) return text; // nothing to remove under an absent container\n // Build the fresh container from the tail of the path, then hand it to the\n // top-level editor as a plain value — one renderer, one indent story.\n const container: Record<string, unknown> = {};\n let cursor = container;\n for (const key of rest.slice(0, -1)) {\n const next: Record<string, unknown> = {};\n cursor[key] = next;\n cursor = next;\n }\n cursor[rest[rest.length - 1]!] = value;\n return editTopLevelProperty(text, head, container, style);\n }\n const inner = text.slice(property.valueStart, property.valueEnd);\n // The container's members sit one unit deeper than the file's top level — read\n // that unit off the container's own first key, so a fresh member lands beside its\n // siblings and the member's *value* keeps nesting by the file's unit (passed as\n // valueIndent), which is what one level deeper actually means.\n const memberIndent = /\\n([ \\t]+)\"/.exec(inner)?.[1] ?? style.indent + style.indent;\n const edited = editJsonPropertyAt(\n inner,\n rest,\n value,\n { ...style, indent: memberIndent },\n baseIndent,\n );\n if (edited === undefined) return undefined; // the container's value is not an object\n if (edited === inner) return text;\n if (value === undefined && removesToEmptyObject(edited)) {\n // The container is now `{}` and it only got that way because of this removal —\n // lift it out, so a file that never carried the key round-trips byte-identical.\n return editTopLevelProperty(text, head, undefined, style) ?? text;\n }\n return `${text.slice(0, property.valueStart)}${edited}${text.slice(property.valueEnd)}`;\n}\n\n/** True when `text` is exactly a JSON object with no properties. */\nfunction removesToEmptyObject(text: string): boolean {\n const scan = scanJsonTopLevel(text);\n return scan !== undefined && scan.properties.length === 0;\n}\n\n/* ------------------------------------------------------------------------------------\n * Marker blocks\n * ---------------------------------------------------------------------------------- */\n\n/**\n * Replace the block delimited by `start` … `end` in `existingText`, or append it.\n *\n * An absent or blank file becomes exactly `block`. A file that carries the block has\n * it replaced in place (one newline after `end` is absorbed, so a block that ends in\n * its own newline does not grow a blank line per re-run). A file without it gets the\n * block appended after exactly one blank line, whatever trailing newlines it had.\n */\nexport function upsertMarkerBlock(\n existingText: string | undefined,\n block: string,\n start: string,\n end: string,\n): string {\n if (existingText === undefined || existingText.trim() === '') return block;\n const startIndex = existingText.indexOf(start);\n const endIndex = existingText.indexOf(end);\n if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {\n const before = existingText.slice(0, startIndex);\n const after = existingText.slice(endIndex + end.length).replace(/^\\n/, '');\n return `${before}${block}${after}`;\n }\n return `${existingText.replace(/\\n*$/, '\\n\\n')}${block}`;\n}\n\n/**\n * Remove the block delimited by `start` … `end`. The text comes back unchanged when\n * the block is not there; `undefined` when nothing (or only whitespace) remains — the\n * file was entirely the block, and the caller decides whether to delete it.\n */\nexport function stripMarkerBlock(\n existingText: string,\n start: string,\n end: string,\n): string | undefined {\n const startIndex = existingText.indexOf(start);\n const endIndex = existingText.indexOf(end);\n if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) return existingText;\n const stripped =\n existingText.slice(0, startIndex).replace(/\\n+$/, '\\n') +\n existingText.slice(endIndex + end.length).replace(/^\\n+/, '');\n return stripped.trim() === '' ? undefined : stripped;\n}\n"]}
|