infra-kit 0.1.105 → 0.1.107
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/.eslintcache +1 -1
- package/.omc/state/agent-replay-afc6290b-40d3-4bef-b3b6-14484c034ab9.jsonl +14 -0
- package/.omc/state/agent-replay-e947a3c6-989d-4a60-91dd-6b0ddd827b2d.jsonl +3 -0
- package/.omc/state/idle-notif-cooldown.json +1 -1
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/mission-state.json +89 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/pre-tool-advisory-throttle.json +34 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/ralph-state.json +13 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/skill-active-state.json +15 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/subagent-tracking-state.json +35 -0
- package/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/ultrawork-state.json +11 -0
- package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/last-tool-error-state.json +7 -0
- package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/pre-tool-advisory-throttle.json +10 -0
- package/.omc/state/sessions/e947a3c6-989d-4a60-91dd-6b0ddd827b2d/subagent-tracking-state.json +26 -0
- package/dist/cli.js +61 -54
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +2 -2
- package/dist/mcp.js.map +2 -2
- package/package.json +1 -1
- package/src/commands/.omc/state/sessions/afc6290b-40d3-4bef-b3b6-14484c034ab9/pre-tool-advisory-throttle.json +14 -0
- package/src/commands/doctor/__tests__/agent-files.test.ts +110 -0
- package/src/commands/doctor/doctor.ts +66 -1
- package/src/commands/init/__tests__/agent-files.test.ts +147 -0
- package/src/commands/init/agent-files.ts +199 -0
- package/src/commands/init/index.ts +7 -0
- package/src/commands/init/init.ts +34 -25
- package/src/entry/cli.ts +1 -1
- package/src/integrations/cmux/open-workspace-with-layout.ts +1 -1
- package/src/lib/managed-block/__tests__/managed-block.test.ts +121 -0
- package/src/lib/managed-block/index.ts +8 -0
- package/src/lib/managed-block/managed-block.ts +145 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -3,7 +3,9 @@ import os from 'node:os'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
|
|
5
5
|
import { logger } from 'src/lib/logger'
|
|
6
|
+
import { removeManagedBlock, upsertManagedBlock } from 'src/lib/managed-block'
|
|
6
7
|
|
|
8
|
+
import { writeAgentFiles } from './agent-files'
|
|
7
9
|
import { migrateLegacyConfig } from './migrate-config'
|
|
8
10
|
|
|
9
11
|
export const MARKER_START = '# -- infra-kit:begin --'
|
|
@@ -55,16 +57,22 @@ const USER_GLOBAL_CONFIG_EXAMPLE = `// infra-kit user-global config — ~/.infra
|
|
|
55
57
|
*/
|
|
56
58
|
export const init = async (): Promise<void> => {
|
|
57
59
|
const zshrcPath = path.join(os.homedir(), '.zshrc')
|
|
58
|
-
const shellBlock = buildShellBlock()
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// Strip any prior block (current or legacy markers) anywhere in the file, then
|
|
62
|
+
// append a fresh block at end-of-file via the shared managed-block utility —
|
|
63
|
+
// the historical `removeExistingBlock` + append behavior, now centralized.
|
|
64
|
+
const existing = fs.existsSync(zshrcPath) ? removeExistingBlock(fs.readFileSync(zshrcPath, 'utf-8')) : ''
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
const updated = upsertManagedBlock({
|
|
67
|
+
content: existing,
|
|
68
|
+
body: buildShellBody(),
|
|
69
|
+
startMarker: MARKER_START,
|
|
70
|
+
endMarker: MARKER_END,
|
|
71
|
+
placement: 'append-end',
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
fs.writeFileSync(zshrcPath, updated)
|
|
66
75
|
|
|
67
|
-
fs.appendFileSync(zshrcPath, `\n${shellBlock}\n`)
|
|
68
76
|
logger.info(`Added infra-kit shell functions to ${zshrcPath}`)
|
|
69
77
|
|
|
70
78
|
// Convert any legacy infra-kit.yml config layers to JSON before seeding, so a
|
|
@@ -73,6 +81,10 @@ export const init = async (): Promise<void> => {
|
|
|
73
81
|
|
|
74
82
|
seedUserGlobalConfig()
|
|
75
83
|
|
|
84
|
+
// Best-effort, non-fatal, repo-gated: keep the agent-instruction files in sync
|
|
85
|
+
// with the CLI surface. A no-op outside an infra-kit repo.
|
|
86
|
+
await writeAgentFiles()
|
|
87
|
+
|
|
76
88
|
logger.info('Run `source ~/.zshrc` or open a new terminal to activate.')
|
|
77
89
|
}
|
|
78
90
|
|
|
@@ -125,28 +137,15 @@ const isBlockLine = (line: string): boolean => {
|
|
|
125
137
|
)
|
|
126
138
|
}
|
|
127
139
|
|
|
128
|
-
const removeBetween = (content: string, start: string, end: string): string | null => {
|
|
129
|
-
const startIdx = content.indexOf(start)
|
|
130
|
-
const endIdx = content.indexOf(end)
|
|
131
|
-
|
|
132
|
-
if (startIdx === -1 || endIdx === -1) return null
|
|
133
|
-
|
|
134
|
-
// eslint-disable-next-line sonarjs/slow-regex
|
|
135
|
-
const before = content.slice(0, startIdx).replace(/\n+$/, '')
|
|
136
|
-
const after = content.slice(endIdx + end.length).replace(/^\n+/, '')
|
|
137
|
-
|
|
138
|
-
return before + (after ? `\n${after}` : '')
|
|
139
|
-
}
|
|
140
|
-
|
|
141
140
|
const removeExistingBlock = (content: string): string => {
|
|
142
141
|
// 1. Current markers
|
|
143
|
-
const result =
|
|
142
|
+
const result = removeManagedBlock(content, MARKER_START, MARKER_END)
|
|
144
143
|
|
|
145
144
|
if (result !== null) return result
|
|
146
145
|
|
|
147
146
|
// 2. Legacy paired markers (# region / # endregion)
|
|
148
147
|
for (const [start, end] of LEGACY_PAIRED) {
|
|
149
|
-
const legacyResult =
|
|
148
|
+
const legacyResult = removeManagedBlock(content, start, end)
|
|
150
149
|
|
|
151
150
|
if (legacyResult !== null) return legacyResult
|
|
152
151
|
}
|
|
@@ -171,11 +170,14 @@ const removeExistingBlock = (content: string): string => {
|
|
|
171
170
|
return before + (remaining ? `\n${remaining}` : '')
|
|
172
171
|
}
|
|
173
172
|
|
|
174
|
-
|
|
173
|
+
/**
|
|
174
|
+
* The inner shell-function lines (no markers). Composed into the full marked
|
|
175
|
+
* block by {@link buildShellBlock} and fed to `upsertManagedBlock` by `init()`.
|
|
176
|
+
*/
|
|
177
|
+
export const buildShellBody = (): string => {
|
|
175
178
|
const runCmd = 'pnpm exec infra-kit'
|
|
176
179
|
|
|
177
180
|
return [
|
|
178
|
-
MARKER_START,
|
|
179
181
|
'zmodload zsh/stat 2>/dev/null',
|
|
180
182
|
'zmodload zsh/datetime 2>/dev/null',
|
|
181
183
|
// eslint-disable-next-line no-template-curly-in-string
|
|
@@ -223,6 +225,13 @@ export const buildShellBlock = (): string => {
|
|
|
223
225
|
'if (( _INFRA_KIT_SHELL_STARTED > 0 )); then',
|
|
224
226
|
' add-zsh-hook precmd _infra_kit_autoload',
|
|
225
227
|
'fi',
|
|
226
|
-
MARKER_END,
|
|
227
228
|
].join('\n')
|
|
228
229
|
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The full marker-delimited shell block (`MARKER_START … MARKER_END`). Kept as
|
|
233
|
+
* a single composed string so `doctor`'s exact-match freshness check stays valid.
|
|
234
|
+
*/
|
|
235
|
+
export const buildShellBlock = (): string => {
|
|
236
|
+
return `${MARKER_START}\n${buildShellBody()}\n${MARKER_END}`
|
|
237
|
+
}
|
package/src/entry/cli.ts
CHANGED
|
@@ -286,7 +286,7 @@ program
|
|
|
286
286
|
|
|
287
287
|
program
|
|
288
288
|
.command('init')
|
|
289
|
-
.description('Inject shell integration into
|
|
289
|
+
.description('Inject shell integration into .zshrc and sync repo agent-instruction files')
|
|
290
290
|
.action(async () => {
|
|
291
291
|
await init()
|
|
292
292
|
})
|
|
@@ -26,7 +26,7 @@ export const openCmuxWorkspaceWithLayout = async (args: OpenCmuxWorkspaceArgs):
|
|
|
26
26
|
await $`cmux new-split down --workspace ${workspaceRef} --surface ${leftTopRef}`
|
|
27
27
|
|
|
28
28
|
if (title) {
|
|
29
|
-
await $`cmux workspace rename --workspace ${workspaceRef} ${title}`
|
|
29
|
+
await $`cmux workspace rename --workspace ${workspaceRef} --title ${title}`
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { extractVersion, hasManagedBlock, removeManagedBlock, upsertManagedBlock } from '../managed-block'
|
|
4
|
+
|
|
5
|
+
const START = '<!-- ik:begin -->'
|
|
6
|
+
const END = '<!-- ik:end -->'
|
|
7
|
+
|
|
8
|
+
describe('hasManagedBlock', () => {
|
|
9
|
+
it('detects a well-formed block', () => {
|
|
10
|
+
expect(hasManagedBlock(`x ${START} y ${END} z`, START, END)).toBe(true)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('returns false when a marker is missing', () => {
|
|
14
|
+
expect(hasManagedBlock(`only ${START} here`, START, END)).toBe(false)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('treats reversed markers (end before start) as absent', () => {
|
|
18
|
+
expect(hasManagedBlock(`${END} middle ${START}`, START, END)).toBe(false)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('removeManagedBlock', () => {
|
|
23
|
+
it('removes the block and preserves surrounding text', () => {
|
|
24
|
+
const content = `top\n${START}\nmid\n${END}\nbot`
|
|
25
|
+
|
|
26
|
+
expect(removeManagedBlock(content, START, END)).toBe('top\nbot')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('returns null when no block is present', () => {
|
|
30
|
+
expect(removeManagedBlock('nothing here', START, END)).toBeNull()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('returns null for reversed markers (guard)', () => {
|
|
34
|
+
expect(removeManagedBlock(`${END}\nx\n${START}`, START, END)).toBeNull()
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('extractVersion', () => {
|
|
39
|
+
const PREFIX = '<!-- ik:version '
|
|
40
|
+
|
|
41
|
+
it('reads the version token after the prefix', () => {
|
|
42
|
+
expect(extractVersion('<!-- ik:version 0.1.105 -->', PREFIX)).toBe('0.1.105')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('returns null when the prefix is absent', () => {
|
|
46
|
+
expect(extractVersion('no version here', PREFIX)).toBeNull()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('upsertManagedBlock', () => {
|
|
51
|
+
it('inserts into an empty file (replace-in-place default)', () => {
|
|
52
|
+
const result = upsertManagedBlock({ content: '', body: 'hello', startMarker: START, endMarker: END })
|
|
53
|
+
|
|
54
|
+
expect(result).toBe(`${START}\nhello\n${END}\n`)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('inserts into an empty file (append-end)', () => {
|
|
58
|
+
const result = upsertManagedBlock({
|
|
59
|
+
content: '',
|
|
60
|
+
body: 'hello',
|
|
61
|
+
startMarker: START,
|
|
62
|
+
endMarker: END,
|
|
63
|
+
placement: 'append-end',
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
expect(result).toBe(`${START}\nhello\n${END}\n`)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('replaces an existing block in place, keeping surrounding content verbatim', () => {
|
|
70
|
+
const content = `# top heading\n\n${START}\nold body\n${END}\n\n# bottom heading\n`
|
|
71
|
+
const result = upsertManagedBlock({ content, body: 'new body', startMarker: START, endMarker: END })
|
|
72
|
+
|
|
73
|
+
expect(result).toBe(`# top heading\n\n${START}\nnew body\n${END}\n\n# bottom heading\n`)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('append-end relocates an existing mid-file block to end-of-file', () => {
|
|
77
|
+
const content = `intro\n\n${START}\nold\n${END}\n\nuser tail`
|
|
78
|
+
const result = upsertManagedBlock({
|
|
79
|
+
content,
|
|
80
|
+
body: 'fresh',
|
|
81
|
+
startMarker: START,
|
|
82
|
+
endMarker: END,
|
|
83
|
+
placement: 'append-end',
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
expect(result).toBe(`intro\nuser tail\n${START}\nfresh\n${END}\n`)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('append-end lands the block at end-of-file when absent', () => {
|
|
90
|
+
const content = 'existing user content'
|
|
91
|
+
const result = upsertManagedBlock({
|
|
92
|
+
content,
|
|
93
|
+
body: 'b',
|
|
94
|
+
startMarker: START,
|
|
95
|
+
endMarker: END,
|
|
96
|
+
placement: 'append-end',
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
expect(result.endsWith(`${START}\nb\n${END}\n`)).toBe(true)
|
|
100
|
+
expect(result.startsWith('existing user content')).toBe(true)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('is idempotent — re-running does not nest or duplicate blocks', () => {
|
|
104
|
+
const once = upsertManagedBlock({ content: 'pre\n', body: 'b', startMarker: START, endMarker: END })
|
|
105
|
+
const twice = upsertManagedBlock({ content: once, body: 'b', startMarker: START, endMarker: END })
|
|
106
|
+
|
|
107
|
+
expect(twice).toBe(once)
|
|
108
|
+
expect(twice.match(new RegExp(START, 'g'))?.length).toBe(1)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('preserves content above and below across an update', () => {
|
|
112
|
+
const first = upsertManagedBlock({ content: 'ABOVE\n', body: 'v1', startMarker: START, endMarker: END })
|
|
113
|
+
const withTail = `${first}BELOW\n`
|
|
114
|
+
const updated = upsertManagedBlock({ content: withTail, body: 'v2', startMarker: START, endMarker: END })
|
|
115
|
+
|
|
116
|
+
expect(updated).toContain('ABOVE')
|
|
117
|
+
expect(updated).toContain('BELOW')
|
|
118
|
+
expect(updated).toContain('v2')
|
|
119
|
+
expect(updated).not.toContain('v1')
|
|
120
|
+
})
|
|
121
|
+
})
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic "managed block" helpers — a marker-delimited region a tool owns and
|
|
3
|
+
* rewrites idempotently while never touching content outside the markers.
|
|
4
|
+
*
|
|
5
|
+
* This is the same mechanism `infra-kit init` already uses for the `~/.zshrc`
|
|
6
|
+
* shell block (`# -- infra-kit:begin -- … # -- infra-kit:end --`), lifted here so
|
|
7
|
+
* it can be reused for the repo agent-instruction files (`AGENTS.md`,
|
|
8
|
+
* `CLAUDE.md` import region, `.cursor/rules`). It mirrors the design of OMC's
|
|
9
|
+
* `<!-- OMC:START --> … <!-- OMC:END -->` CLAUDE.md installer.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Where a freshly upserted block lands relative to existing content.
|
|
14
|
+
*
|
|
15
|
+
* - `replace-in-place`: if the block already exists, rewrite it where it sits
|
|
16
|
+
* (surrounding text untouched, verbatim); if absent, append it at end-of-file.
|
|
17
|
+
* - `append-end`: strip any existing block from wherever it is, then append the
|
|
18
|
+
* fresh block at end-of-file (i.e. always relocate to the end). This preserves
|
|
19
|
+
* the historical `~/.zshrc` behavior of `removeExistingBlock` + append.
|
|
20
|
+
*/
|
|
21
|
+
export type BlockPlacement = 'replace-in-place' | 'append-end'
|
|
22
|
+
|
|
23
|
+
export interface UpsertManagedBlockArgs {
|
|
24
|
+
/** Existing file content (`''` for a new file). */
|
|
25
|
+
content: string
|
|
26
|
+
/** Inner body to place between the markers (WITHOUT the markers). */
|
|
27
|
+
body: string
|
|
28
|
+
startMarker: string
|
|
29
|
+
endMarker: string
|
|
30
|
+
/** Defaults to `replace-in-place`. */
|
|
31
|
+
placement?: BlockPlacement
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether `content` contains a well-formed `start … end` block. A reversed pair
|
|
36
|
+
* (end before start) is treated as absent — the same guard `doctor.ts` applies
|
|
37
|
+
* to the zshrc block, ported here so corrupted markers never match.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* hasManagedBlock('a<!--s-->x<!--e-->b', '<!--s-->', '<!--e-->') // => true
|
|
41
|
+
* hasManagedBlock('<!--e--><!--s-->', '<!--s-->', '<!--e-->') // => false (reversed)
|
|
42
|
+
*/
|
|
43
|
+
export const hasManagedBlock = (content: string, startMarker: string, endMarker: string): boolean => {
|
|
44
|
+
const startIdx = content.indexOf(startMarker)
|
|
45
|
+
const endIdx = content.indexOf(endMarker)
|
|
46
|
+
|
|
47
|
+
return startIdx !== -1 && endIdx !== -1 && endIdx >= startIdx
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Remove the first complete `start … end` block, preserving surrounding text.
|
|
52
|
+
* Returns `null` when no well-formed block is present (no markers, or reversed),
|
|
53
|
+
* so callers can fall through to legacy-format handling — this matches the
|
|
54
|
+
* `string | null` contract of `init.ts`'s original `removeBetween`, with the
|
|
55
|
+
* added reversed-marker guard.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* removeManagedBlock('top\n<!--s-->mid<!--e-->\nbot', '<!--s-->', '<!--e-->')
|
|
59
|
+
* // => 'top\nbot'
|
|
60
|
+
*/
|
|
61
|
+
export const removeManagedBlock = (content: string, startMarker: string, endMarker: string): string | null => {
|
|
62
|
+
const startIdx = content.indexOf(startMarker)
|
|
63
|
+
const endIdx = content.indexOf(endMarker)
|
|
64
|
+
|
|
65
|
+
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) return null
|
|
66
|
+
|
|
67
|
+
// eslint-disable-next-line sonarjs/slow-regex
|
|
68
|
+
const before = content.slice(0, startIdx).replace(/\n+$/, '')
|
|
69
|
+
const after = content.slice(endIdx + endMarker.length).replace(/^\n+/, '')
|
|
70
|
+
|
|
71
|
+
return before + (after ? `\n${after}` : '')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Read the version token that follows `versionPrefix` (e.g.
|
|
76
|
+
* `'<!-- infra-kit:version '`). Returns the token up to the next whitespace or
|
|
77
|
+
* `>`, or `null` if the prefix is absent. Mirrors OMC's `OMC:VERSION:` line.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* extractVersion('<!-- infra-kit:version 0.1.105 -->', '<!-- infra-kit:version ')
|
|
81
|
+
* // => '0.1.105'
|
|
82
|
+
*/
|
|
83
|
+
export const extractVersion = (content: string, versionPrefix: string): string | null => {
|
|
84
|
+
const idx = content.indexOf(versionPrefix)
|
|
85
|
+
|
|
86
|
+
if (idx === -1) return null
|
|
87
|
+
|
|
88
|
+
const rest = content.slice(idx + versionPrefix.length)
|
|
89
|
+
const match = rest.match(/^([^\s>]+)/)
|
|
90
|
+
|
|
91
|
+
return match ? match[1]! : null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Compose a full block string: `start\n{body}\nend`. Kept identical in shape to
|
|
96
|
+
* the historical `buildShellBlock()` output so doctor's exact-match comparison
|
|
97
|
+
* stays valid.
|
|
98
|
+
*/
|
|
99
|
+
export const buildManagedBlock = (startMarker: string, body: string, endMarker: string): string => {
|
|
100
|
+
return `${startMarker}\n${body}\n${endMarker}`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Insert or update a managed block in `content`, preserving everything outside
|
|
105
|
+
* the markers. Idempotent: re-running with the same body yields the same block
|
|
106
|
+
* and never nests duplicates.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* // fresh file
|
|
110
|
+
* upsertManagedBlock({ content: '', body: 'hi', startMarker: '<!--s-->', endMarker: '<!--e-->' })
|
|
111
|
+
* // => '<!--s-->\nhi\n<!--e-->\n'
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* // existing block replaced in place, surrounding text kept
|
|
115
|
+
* upsertManagedBlock({
|
|
116
|
+
* content: 'top\n<!--s-->\nold\n<!--e-->\nbot',
|
|
117
|
+
* body: 'new', startMarker: '<!--s-->', endMarker: '<!--e-->',
|
|
118
|
+
* })
|
|
119
|
+
* // => 'top\n<!--s-->\nnew\n<!--e-->\nbot'
|
|
120
|
+
*/
|
|
121
|
+
export const upsertManagedBlock = ({
|
|
122
|
+
content,
|
|
123
|
+
body,
|
|
124
|
+
startMarker,
|
|
125
|
+
endMarker,
|
|
126
|
+
placement = 'replace-in-place',
|
|
127
|
+
}: UpsertManagedBlockArgs): string => {
|
|
128
|
+
const block = buildManagedBlock(startMarker, body, endMarker)
|
|
129
|
+
const present = hasManagedBlock(content, startMarker, endMarker)
|
|
130
|
+
|
|
131
|
+
if (placement === 'replace-in-place' && present) {
|
|
132
|
+
const startIdx = content.indexOf(startMarker)
|
|
133
|
+
const endIdx = content.indexOf(endMarker) + endMarker.length
|
|
134
|
+
|
|
135
|
+
return content.slice(0, startIdx) + block + content.slice(endIdx)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// append-end, or replace-in-place with no existing block: drop any stale block
|
|
139
|
+
// then append the fresh one at end-of-file.
|
|
140
|
+
const stripped = present ? (removeManagedBlock(content, startMarker, endMarker) ?? content) : content
|
|
141
|
+
// eslint-disable-next-line sonarjs/slow-regex
|
|
142
|
+
const base = stripped.replace(/\n+$/, '')
|
|
143
|
+
|
|
144
|
+
return base.length > 0 ? `${base}\n${block}\n` : `${block}\n`
|
|
145
|
+
}
|