gigarag-copilot 0.1.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.
Files changed (56) hide show
  1. package/README.md +31 -0
  2. package/cli/auth/credentials.js +106 -0
  3. package/cli/auth/oauth.js +203 -0
  4. package/cli/auth/page.js +68 -0
  5. package/cli/bin.js +8 -0
  6. package/cli/cli.js +90 -0
  7. package/cli/clients/commands.js +160 -0
  8. package/cli/clients/connect.js +217 -0
  9. package/cli/clients/inspect.js +74 -0
  10. package/cli/clients/json.js +135 -0
  11. package/cli/clients/launcher.js +71 -0
  12. package/cli/clients/registry.js +40 -0
  13. package/cli/clients/toml.js +169 -0
  14. package/cli/clients/tomlarray.js +121 -0
  15. package/cli/clients/yaml.js +146 -0
  16. package/cli/clients.json +1226 -0
  17. package/cli/commands/authHeader.js +22 -0
  18. package/cli/commands/connect.js +285 -0
  19. package/cli/commands/indexSync.js +46 -0
  20. package/cli/commands/login.js +129 -0
  21. package/cli/commands/mcp.js +22 -0
  22. package/cli/commands/record.js +72 -0
  23. package/cli/commands/repo.js +48 -0
  24. package/cli/commands/scan.js +72 -0
  25. package/cli/commands/status.js +115 -0
  26. package/cli/config.js +69 -0
  27. package/cli/connect.js +8 -0
  28. package/cli/constants.js +24 -0
  29. package/cli/hooks.js +151 -0
  30. package/cli/index.js +3 -0
  31. package/cli/mcp/bridge.js +123 -0
  32. package/cli/mcp/client.js +156 -0
  33. package/cli/mcp/session.js +79 -0
  34. package/cli/package.json +5 -0
  35. package/cli/paths.js +34 -0
  36. package/cli/prompts.generated.js +44 -0
  37. package/cli/prompts.js +48 -0
  38. package/cli/scan/chunk.js +43 -0
  39. package/cli/scan/ignore.js +117 -0
  40. package/cli/scan/repo.js +99 -0
  41. package/cli/scan/scan.js +262 -0
  42. package/cli/scan.js +5 -0
  43. package/cli/sdk.js +130 -0
  44. package/cli/secrets.js +192 -0
  45. package/cli/secureUrl.js +18 -0
  46. package/cli/state.js +210 -0
  47. package/cli/ui.js +66 -0
  48. package/mcp.json +8 -0
  49. package/package.json +20 -0
  50. package/plugin.json +10 -0
  51. package/scripts/run.mjs +64 -0
  52. package/skills/gigadocs/SKILL.md +21 -0
  53. package/skills/gigaindex/SKILL.md +74 -0
  54. package/skills/gigarecall/SKILL.md +17 -0
  55. package/skills/gigasave/SKILL.md +26 -0
  56. package/skills/gigasync/SKILL.md +76 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Just enough TOML to add, replace and remove one table without touching the rest
3
+ * of the file. It does not parse values. It finds table headers and the keys that
4
+ * could define our table another way, which is all a safe edit needs, and leaves
5
+ * every other byte exactly as it was.
6
+ *
7
+ * It refuses, and says why, when our table is written in a form it cannot edit:
8
+ * an inline table (`gigarag = { ... }`), a dotted key (`mcp_servers.gigarag.command
9
+ * = ...`), or a parent written as an inline table. Adding a `[header]` beside any of
10
+ * those would define the same key twice, which is invalid TOML, and the client would
11
+ * then fail to load its whole config.
12
+ */
13
+ /** The dotted parts of a key such as `a."b c".d`, respecting quotes. Undefined if it does not parse. */
14
+ export function splitDotted(src) {
15
+ const parts = [];
16
+ let i = 0;
17
+ while (i < src.length) {
18
+ while (src[i] === ' ' || src[i] === '\t')
19
+ i++;
20
+ if (i >= src.length)
21
+ break;
22
+ if (src[i] === '"' || src[i] === "'") {
23
+ const quote = src[i];
24
+ const end = src.indexOf(quote, i + 1);
25
+ if (end < 0)
26
+ return undefined;
27
+ parts.push(src.slice(i + 1, end));
28
+ i = end + 1;
29
+ }
30
+ else {
31
+ const start = i;
32
+ while (i < src.length && src[i] !== '.' && src[i] !== ' ' && src[i] !== '\t')
33
+ i++;
34
+ parts.push(src.slice(start, i));
35
+ }
36
+ while (src[i] === ' ' || src[i] === '\t')
37
+ i++;
38
+ if (src[i] === '.')
39
+ i++;
40
+ else if (i < src.length)
41
+ return undefined;
42
+ }
43
+ return parts;
44
+ }
45
+ /** The dotted key parts of a `[header]` line, or undefined if the line is not one. */
46
+ export function headerParts(line) {
47
+ const m = /^\s*\[([^\[\]]+)\]\s*(?:#.*)?$/.exec(line);
48
+ return m ? splitDotted(m[1]) : undefined;
49
+ }
50
+ const isHeaderLine = (line) => /^\s*\[/.test(line);
51
+ const isBlankOrComment = (line) => /^\s*(#.*)?$/.test(line);
52
+ const startsWith = (parts, prefix) => parts.length >= prefix.length && prefix.every((p, i) => parts[i] === p);
53
+ const same = (a, b) => a.length === b.length && a.every((p, i) => p === b[i]);
54
+ function eolOf(text) {
55
+ return text.includes('\r\n') ? '\r\n' : '\n';
56
+ }
57
+ function render(value) {
58
+ if (Array.isArray(value))
59
+ return `[${value.map(v => JSON.stringify(v)).join(', ')}]`;
60
+ return typeof value === 'string' ? JSON.stringify(value) : String(value);
61
+ }
62
+ /** A reason our table cannot be edited safely, or undefined when it can. */
63
+ export function unsafeForm(lines, path) {
64
+ let current = [];
65
+ for (const line of lines) {
66
+ if (isHeaderLine(line)) {
67
+ const parts = headerParts(line);
68
+ // An array-of-tables header, or one we could not read, is somebody else's structure.
69
+ current = parts ?? ['\u0000'];
70
+ continue;
71
+ }
72
+ if (isBlankOrComment(line))
73
+ continue;
74
+ const m = /^\s*([^=#]+?)\s*=\s*(.*)$/.exec(line);
75
+ if (!m)
76
+ continue;
77
+ const key = splitDotted(m[1].trim());
78
+ if (!key)
79
+ continue;
80
+ // Only keys in a table above ours can define ours by another route. Inside our own header they are just its keys.
81
+ const above = current.length < path.length && startsWith(path, current);
82
+ if (!above)
83
+ continue;
84
+ const full = [...current, ...key];
85
+ if (startsWith(full, path))
86
+ return `${path.join('.')} is already defined as an inline table or a dotted key`;
87
+ if (full.length < path.length && startsWith(path, full) && /^\{/.test(m[2].trim())) {
88
+ return `${full.join('.')} is written as an inline table`;
89
+ }
90
+ }
91
+ return undefined;
92
+ }
93
+ /**
94
+ * Where our table lives: its own block and any `[path.sub]` blocks, as line ranges. A block ends at
95
+ * its last line of content, so a comment or blank line before the next header stays with what follows.
96
+ */
97
+ function ranges(lines, path) {
98
+ const found = [];
99
+ for (let i = 0; i < lines.length; i++) {
100
+ if (!isHeaderLine(lines[i]))
101
+ continue;
102
+ const parts = headerParts(lines[i]);
103
+ if (!parts || !startsWith(parts, path))
104
+ continue;
105
+ let end = i + 1;
106
+ while (end < lines.length && !isHeaderLine(lines[end]))
107
+ end++;
108
+ while (end > i + 1 && isBlankOrComment(lines[end - 1]))
109
+ end--;
110
+ found.push({ start: i, end });
111
+ i = end - 1;
112
+ }
113
+ return found;
114
+ }
115
+ function block(path, values) {
116
+ const header = `[${path.map(p => (/^[A-Za-z0-9_-]+$/.test(p) ? p : JSON.stringify(p))).join('.')}]`;
117
+ return [header, ...Object.entries(values).map(([k, v]) => `${k} = ${render(v)}`)];
118
+ }
119
+ function withoutRanges(lines, rs) {
120
+ const drop = new Set();
121
+ for (const r of rs)
122
+ for (let i = r.start; i < r.end; i++)
123
+ drop.add(i);
124
+ return lines.filter((_, i) => !drop.has(i));
125
+ }
126
+ /** Drops `[path]` and every `[path.sub]` block. */
127
+ export function removeTable(text, path) {
128
+ const eol = eolOf(text);
129
+ const lines = text.split(/\r?\n/);
130
+ const bad = unsafeForm(lines, path);
131
+ if (bad)
132
+ return { ok: false, reason: bad };
133
+ const rs = ranges(lines, path);
134
+ if (rs.length === 0)
135
+ return { ok: true, text, existed: false, changed: false };
136
+ const kept = withoutRanges(lines, rs);
137
+ // Removing a block can leave two blank lines where one block used to be. Keep one.
138
+ for (let i = kept.length - 1; i > 0; i--)
139
+ if (kept[i] === '' && kept[i - 1] === '' && i < kept.length - 1)
140
+ kept.splice(i, 1);
141
+ while (kept.length > 1 && kept[kept.length - 1] === '' && kept[kept.length - 2] === '')
142
+ kept.pop();
143
+ return { ok: true, text: kept.join(eol), existed: true, changed: true };
144
+ }
145
+ /**
146
+ * Replaces `[path]` where it is, or appends it. Replacing in place means running it twice changes
147
+ * nothing, and an entry in the middle of a file does not migrate to the end on every run.
148
+ */
149
+ export function setTable(text, path, values) {
150
+ const eol = eolOf(text);
151
+ const lines = text.split(/\r?\n/);
152
+ const bad = unsafeForm(lines, path);
153
+ if (bad)
154
+ return { ok: false, reason: bad };
155
+ const fresh = block(path, values);
156
+ const rs = ranges(lines, path);
157
+ if (rs.length === 0) {
158
+ const trimmed = text.replace(/\s+$/, '');
159
+ const next = trimmed === '' ? `${fresh.join(eol)}${eol}` : `${trimmed}${eol}${eol}${fresh.join(eol)}${eol}`;
160
+ return { ok: true, text: next, existed: false, changed: next !== text };
161
+ }
162
+ const first = rs[0];
163
+ const out = [...lines.slice(0, first.start), ...fresh, ...lines.slice(first.end)];
164
+ // Any later `[path.sub]` block belonged to the entry being replaced.
165
+ const shift = fresh.length - (first.end - first.start);
166
+ const rest = rs.slice(1).map(r => ({ start: r.start + shift, end: r.end + shift }));
167
+ const next = withoutRanges(out, rest).join(eol);
168
+ return { ok: true, text: next, existed: true, changed: next !== text };
169
+ }
@@ -0,0 +1,121 @@
1
+ import { headerParts, splitDotted } from './toml.js';
2
+ /**
3
+ * Editing one entry of a TOML array of tables, such as Mistral Vibe's
4
+ *
5
+ * [[mcp_servers]]
6
+ * name = "gigarag"
7
+ * transport = "stdio"
8
+ *
9
+ * The entry is found by its `name` line, so the other `[[mcp_servers]]` blocks belong to somebody
10
+ * else and stay exactly as they are. A `[mcp_servers.env]` table after our block is a sub-table of
11
+ * that entry and travels with it. The comment or blank line before the next block does not.
12
+ */
13
+ const ARRAY_HEADER = /^\s*\[\[\s*([^\[\]]+?)\s*\]\]\s*(?:#.*)?$/;
14
+ const isHeader = (line) => /^\s*\[/.test(line);
15
+ const isBlankOrComment = (line) => /^\s*(#.*)?$/.test(line);
16
+ const same = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
17
+ const startsWith = (a, p) => a.length >= p.length && p.every((x, i) => a[i] === x);
18
+ function eolOf(text) {
19
+ return text.includes('\r\n') ? '\r\n' : '\n';
20
+ }
21
+ function render(value) {
22
+ if (Array.isArray(value))
23
+ return `[${value.map(v => JSON.stringify(v)).join(', ')}]`;
24
+ return typeof value === 'string' ? JSON.stringify(value) : String(value);
25
+ }
26
+ /** Every `[[path]]` element as a line range, sub-tables included, and whether it carries our name. */
27
+ function elements(lines, path, name) {
28
+ const out = [];
29
+ const nameLine = new RegExp(`^\\s*name\\s*=\\s*["']${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["']\\s*(#.*)?$`);
30
+ for (let i = 0; i < lines.length; i++) {
31
+ const m = ARRAY_HEADER.exec(lines[i]);
32
+ const parts = m ? splitDotted(m[1]) : undefined;
33
+ if (!parts || !same(parts, path))
34
+ continue;
35
+ let end = i + 1;
36
+ while (end < lines.length) {
37
+ const line = lines[end];
38
+ if (!isHeader(line)) {
39
+ end++;
40
+ continue;
41
+ }
42
+ // A sub-table of this element continues it. Anything else starts something new.
43
+ const sub = headerParts(line);
44
+ if (sub && sub.length > path.length && startsWith(sub, path))
45
+ end++;
46
+ else
47
+ break;
48
+ }
49
+ while (end > i + 1 && isBlankOrComment(lines[end - 1]))
50
+ end--;
51
+ out.push({ start: i, end, ours: lines.slice(i + 1, end).some(l => nameLine.test(l)) });
52
+ i = end - 1;
53
+ }
54
+ return out;
55
+ }
56
+ /** A reason this file cannot be edited safely, or undefined. */
57
+ function unsafe(lines, path) {
58
+ const first = path[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
59
+ if (lines.some(l => new RegExp(`^\\s*${first}\\s*=`).test(l)))
60
+ return `${path.join('.')} is written as an inline array`;
61
+ return undefined;
62
+ }
63
+ export function removeArrayEntry(text, path, name) {
64
+ const eol = eolOf(text);
65
+ const lines = text.split(/\r?\n/);
66
+ const bad = unsafe(lines, path);
67
+ if (bad)
68
+ return { ok: false, reason: bad };
69
+ const mine = elements(lines, path, name).filter(e => e.ours);
70
+ if (mine.length === 0)
71
+ return { ok: true, text, existed: false, changed: false };
72
+ const drop = new Set();
73
+ for (const e of mine) {
74
+ for (let i = e.start; i < e.end; i++)
75
+ drop.add(i);
76
+ // The blank line that separated this element from the one before it goes with it.
77
+ if (e.start > 0 && lines[e.start - 1].trim() === '')
78
+ drop.add(e.start - 1);
79
+ }
80
+ const kept = lines.filter((_, i) => !drop.has(i));
81
+ while (kept.length > 1 && kept[kept.length - 1] === '' && kept[kept.length - 2] === '')
82
+ kept.pop();
83
+ return { ok: true, text: kept.join(eol), existed: true, changed: true };
84
+ }
85
+ export function setArrayEntry(text, path, name, values) {
86
+ const eol = eolOf(text);
87
+ const lines = text.split(/\r?\n/);
88
+ const bad = unsafe(lines, path);
89
+ if (bad)
90
+ return { ok: false, reason: bad };
91
+ const fresh = [`[[${path.join('.')}]]`, ...Object.entries({ name, ...values }).map(([k, v]) => `${k} = ${render(v)}`)];
92
+ const mine = elements(lines, path, name).filter(e => e.ours);
93
+ if (mine.length === 0) {
94
+ const trimmed = text.replace(/\s+$/, '');
95
+ const next = trimmed === '' ? `${fresh.join(eol)}${eol}` : `${trimmed}${eol}${eol}${fresh.join(eol)}${eol}`;
96
+ return { ok: true, text: next, existed: false, changed: next !== text };
97
+ }
98
+ // Replace the first copy where it stands, and drop any duplicates.
99
+ const first = mine[0];
100
+ const out = [...lines.slice(0, first.start), ...fresh, ...lines.slice(first.end)];
101
+ const shift = fresh.length - (first.end - first.start);
102
+ const drop = new Set();
103
+ for (const e of mine.slice(1))
104
+ for (let i = e.start + shift; i < e.end + shift; i++)
105
+ drop.add(i);
106
+ const next = out.filter((_, i) => !drop.has(i)).join(eol);
107
+ return { ok: true, text: next, existed: true, changed: next !== text };
108
+ }
109
+ /** The command of our entry, or undefined. Used by `status` to spot a launcher that has moved. */
110
+ export function arrayEntryCommand(text, path, name) {
111
+ const lines = text.split(/\r?\n/);
112
+ const e = elements(lines, path, name).find(x => x.ours);
113
+ if (!e)
114
+ return undefined;
115
+ for (const l of lines.slice(e.start + 1, e.end)) {
116
+ const m = /^\s*command\s*=\s*("(?:[^"\\]|\\.)*")/.exec(l);
117
+ if (m)
118
+ return JSON.parse(m[1]);
119
+ }
120
+ return undefined;
121
+ }
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Just enough YAML to add, replace and remove one entry under a top-level mapping,
3
+ * such as Goose's `extensions:`, without touching any other byte of the file. Like
4
+ * the TOML helper it finds structure by indentation and never parses values, so
5
+ * comments and anchors elsewhere survive.
6
+ *
7
+ * It refuses, and says why, what it cannot edit safely: tabs, a mapping written in
8
+ * flow style (`extensions: {}`), a sequence written at the same indent as its key,
9
+ * and an entry of ours that is already written inline (`gigarag: {cmd: x}`).
10
+ */
11
+ const indentOf = (line) => /^ */.exec(line)[0].length;
12
+ const isBlankOrComment = (line) => /^\s*(#.*)?$/.test(line);
13
+ const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
14
+ function eolOf(text) {
15
+ return text.includes('\r\n') ? '\r\n' : '\n';
16
+ }
17
+ function scalar(v) {
18
+ return typeof v === 'string' ? JSON.stringify(v) : String(v);
19
+ }
20
+ function flow(v) {
21
+ if (Array.isArray(v))
22
+ return `[${v.map(flow).join(', ')}]`;
23
+ if (typeof v === 'object') {
24
+ const entries = Object.entries(v);
25
+ return entries.length === 0 ? '{}' : `{ ${entries.map(([k, x]) => `${k}: ${flow(x)}`).join(', ')} }`;
26
+ }
27
+ return scalar(v);
28
+ }
29
+ /** The lines of `name:` and its mapping, indented `indent` spaces, with `step` spaces per level. */
30
+ export function renderEntry(name, entry, indent, step = 2) {
31
+ const pad = ' '.repeat(indent);
32
+ const lines = [`${pad}${name}:`];
33
+ for (const [k, v] of Object.entries(entry)) {
34
+ // Arrays and empty maps stay on one line, which is how these files are written by hand.
35
+ if (typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length > 0) {
36
+ lines.push(...renderEntry(k, v, indent + step, step));
37
+ }
38
+ else {
39
+ lines.push(`${' '.repeat(indent + step)}${k}: ${flow(v)}`);
40
+ }
41
+ }
42
+ return lines;
43
+ }
44
+ /** A byte order mark would hide the first key from every pattern below, so it is set aside and put back. */
45
+ function splitBom(text) {
46
+ return text.charCodeAt(0) === 0xfeff ? { bom: String.fromCharCode(0xfeff), body: text.slice(1) } : { bom: '', body: text };
47
+ }
48
+ function locate(lines, key) {
49
+ const re = new RegExp(`^${escape(key)}:\\s*(.*?)\\s*$`);
50
+ const keyLine = lines.findIndex(l => re.test(l));
51
+ if (keyLine < 0)
52
+ return undefined;
53
+ const rest = re.exec(lines[keyLine])[1];
54
+ if (rest !== '' && !rest.startsWith('#'))
55
+ return 'flow';
56
+ let end = keyLine + 1;
57
+ let childIndent = 0;
58
+ for (let i = keyLine + 1; i < lines.length; i++) {
59
+ const line = lines[i];
60
+ if (isBlankOrComment(line))
61
+ continue;
62
+ // A sequence may sit at the same indent as its key, which is not a mapping we can add an entry to.
63
+ if (indentOf(line) === 0) {
64
+ if (/^-(\s|$)/.test(line) && childIndent === 0)
65
+ return 'sequence';
66
+ break;
67
+ }
68
+ if (childIndent === 0)
69
+ childIndent = indentOf(line);
70
+ end = i + 1;
71
+ }
72
+ return { keyLine, end, childIndent: childIndent || 2 };
73
+ }
74
+ const unsafeText = (body) => (/^\s*\t/m.test(body) ? 'it is indented with tabs, which YAML does not allow, so it is not safe to edit' : undefined);
75
+ /** Finds our entry's line range inside the mapping, or a reason it is written in a way we will not edit. */
76
+ function findEntry(lines, where, name) {
77
+ const n = escape(name);
78
+ const block = new RegExp(`^ {${where.childIndent}}${n}:\\s*(#.*)?$`);
79
+ const inline = new RegExp(`^ {${where.childIndent}}${n}:\\s*\\S`);
80
+ for (let i = where.keyLine + 1; i < where.end; i++) {
81
+ if (inline.test(lines[i]) && !block.test(lines[i]))
82
+ return { reason: `${name} is already written inline, which gigarag will not edit` };
83
+ }
84
+ const start = lines.findIndex((l, i) => i > where.keyLine && i < where.end && block.test(l));
85
+ if (start < 0)
86
+ return undefined;
87
+ let stop = start + 1;
88
+ while (stop < where.end && (isBlankOrComment(lines[stop]) || indentOf(lines[stop]) > where.childIndent))
89
+ stop++;
90
+ // Trailing blank or comment lines belong to whatever comes next, not to this entry.
91
+ while (stop > start + 1 && isBlankOrComment(lines[stop - 1]))
92
+ stop--;
93
+ return { start, stop };
94
+ }
95
+ /** Removes `name` from under `key`. */
96
+ export function removeYamlEntry(text, key, name) {
97
+ const { bom, body } = splitBom(text);
98
+ const tabs = unsafeText(body);
99
+ if (tabs)
100
+ return { ok: false, reason: tabs };
101
+ const eol = eolOf(body);
102
+ const lines = body.split(/\r?\n/);
103
+ const where = locate(lines, key);
104
+ if (where === undefined)
105
+ return { ok: true, text, existed: false, changed: false };
106
+ if (where === 'flow')
107
+ return { ok: false, reason: `"${key}" is written in flow style, which gigarag will not edit` };
108
+ if (where === 'sequence')
109
+ return { ok: false, reason: `"${key}" is a sequence, not a mapping` };
110
+ const found = findEntry(lines, where, name);
111
+ if (!found)
112
+ return { ok: true, text, existed: false, changed: false };
113
+ if ('reason' in found)
114
+ return { ok: false, reason: found.reason };
115
+ return { ok: true, text: bom + [...lines.slice(0, found.start), ...lines.slice(found.stop)].join(eol), existed: true, changed: true };
116
+ }
117
+ /** Replaces `name` under `key` where it stands, or adds it at the end of the mapping. */
118
+ export function setYamlEntry(text, key, name, entry) {
119
+ const { bom, body } = splitBom(text);
120
+ const tabs = unsafeText(body);
121
+ if (tabs)
122
+ return { ok: false, reason: tabs };
123
+ const eol = eolOf(body);
124
+ const lines = body.split(/\r?\n/);
125
+ const where = locate(lines, key);
126
+ if (where === undefined) {
127
+ const trimmed = body.replace(/\s+$/, '');
128
+ const fresh = [`${key}:`, ...renderEntry(name, entry, 2)];
129
+ const out = trimmed === '' ? [...fresh, ''] : [...trimmed.split(/\r?\n/), '', ...fresh, ''];
130
+ const next = bom + out.join(eol);
131
+ return { ok: true, text: next, existed: false, changed: next !== text };
132
+ }
133
+ if (where === 'flow')
134
+ return { ok: false, reason: `"${key}" is written in flow style, which gigarag will not edit` };
135
+ if (where === 'sequence')
136
+ return { ok: false, reason: `"${key}" is a sequence, not a mapping` };
137
+ const found = findEntry(lines, where, name);
138
+ if (found && 'reason' in found)
139
+ return { ok: false, reason: found.reason };
140
+ const fresh = renderEntry(name, entry, where.childIndent);
141
+ const out = found
142
+ ? [...lines.slice(0, found.start), ...fresh, ...lines.slice(found.stop)]
143
+ : [...lines.slice(0, where.end), ...fresh, ...lines.slice(where.end)];
144
+ const next = bom + out.join(eol);
145
+ return { ok: true, text: next, existed: found !== undefined, changed: next !== text };
146
+ }