@rch4com/agent-setup 1.1.0 → 1.3.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/LICENSE +21 -21
- package/README.md +197 -55
- package/install.mjs +152 -101
- package/lib/args.mjs +276 -311
- package/lib/bootstrap/adapter.mjs +96 -95
- package/lib/bootstrap/apply.mjs +380 -379
- package/lib/bootstrap/flow.mjs +73 -70
- package/lib/bootstrap/record.mjs +154 -142
- package/lib/bootstrap/templates.mjs +148 -148
- package/lib/bootstrap/text.mjs +19 -19
- package/lib/catalog.mjs +213 -145
- package/lib/claude-plugins.mjs +64 -64
- package/lib/clis.mjs +102 -102
- package/lib/context.mjs +66 -65
- package/lib/deps.mjs +5 -7
- package/lib/design-md/catalog.json +454 -454
- package/lib/design-md/catalog.mjs +266 -268
- package/lib/design-md/flow.mjs +181 -166
- package/lib/design-md/open.mjs +51 -50
- package/lib/design-md/providers/awesome-design-md.mjs +117 -114
- package/lib/design-md/providers/index.mjs +9 -9
- package/lib/design-md/scan.mjs +14 -10
- package/lib/engine.mjs +48 -44
- package/lib/gitignore.mjs +18 -18
- package/lib/i18n/catalog/en.mjs +369 -0
- package/lib/i18n/catalog/ko.mjs +359 -0
- package/lib/i18n/detect.mjs +39 -0
- package/lib/i18n/index.mjs +93 -0
- package/lib/items/mcp.codebase-memory.mjs +6 -6
- package/lib/items/mcp.graphify.mjs +8 -0
- package/lib/items/mcp.headroom.mjs +8 -0
- package/lib/items/mcp.notion.mjs +6 -6
- package/lib/items/mcp.supabase.mjs +6 -6
- package/lib/items/mcp.vercel.mjs +6 -6
- package/lib/items/plugin.bkit.mjs +7 -7
- package/lib/items/plugin.ecc.mjs +11 -0
- package/lib/items/plugin.impeccable.mjs +14 -0
- package/lib/items/plugin.mattpocock-skills.mjs +2 -2
- package/lib/items/plugin.ponytail.mjs +104 -0
- package/lib/items/plugin.superpowers.mjs +7 -7
- package/lib/items/plugin.understand-anything.mjs +10 -0
- package/lib/items/skill.caveman.mjs +7 -0
- package/lib/items/skill.gsd.mjs +25 -24
- package/lib/items/skill.gstack.mjs +40 -39
- package/lib/items/skill.karpathy.mjs +7 -0
- package/lib/items/skill.taste.mjs +9 -0
- package/lib/jsonfile.mjs +40 -40
- package/lib/status.mjs +93 -92
- package/lib/tomlfile.mjs +60 -60
- package/lib/tui/render.mjs +41 -63
- package/lib/tui/rows.mjs +278 -206
- package/lib/tui/run.mjs +64 -28
- package/lib/update.mjs +85 -85
- package/lib/width.mjs +57 -0
- package/package.json +36 -36
package/lib/bootstrap/flow.mjs
CHANGED
|
@@ -1,70 +1,73 @@
|
|
|
1
|
-
// 부트스트랩 진입점 — 순서와 보고만 담당한다.
|
|
2
|
-
// 무엇을 만들지는 manifest.mjs가, 어떻게 만들지는 apply.mjs·adapter.mjs가 안다.
|
|
3
|
-
import { MANIFEST } from './manifest.mjs'
|
|
4
|
-
import {
|
|
5
|
-
configureAdapterSafe, ensureBlocks, ensureDirs, ensureFiles, ensureIgnore, ensureJsonKeys,
|
|
6
|
-
} from './apply.mjs'
|
|
7
|
-
import { RECORD_REL, collectManaged, emptyRecord, readRecord, writeRecord } from './record.mjs'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
say('
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
...
|
|
29
|
-
...
|
|
30
|
-
...
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
say(
|
|
65
|
-
say('
|
|
66
|
-
say('
|
|
67
|
-
say(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
1
|
+
// 부트스트랩 진입점 — 순서와 보고만 담당한다.
|
|
2
|
+
// 무엇을 만들지는 manifest.mjs가, 어떻게 만들지는 apply.mjs·adapter.mjs가 안다.
|
|
3
|
+
import { MANIFEST } from './manifest.mjs'
|
|
4
|
+
import {
|
|
5
|
+
configureAdapterSafe, ensureBlocks, ensureDirs, ensureFiles, ensureIgnore, ensureJsonKeys,
|
|
6
|
+
} from './apply.mjs'
|
|
7
|
+
import { RECORD_REL, collectManaged, emptyRecord, readRecord, writeRecord } from './record.mjs'
|
|
8
|
+
import { createT, LocalizedError, toText } from '../i18n/index.mjs'
|
|
9
|
+
|
|
10
|
+
const SKILL_MODES = ['auto', 'link', 'copy']
|
|
11
|
+
|
|
12
|
+
export function runBootstrap(root, opts = {}) {
|
|
13
|
+
const { dryRun = false, skillMode = 'auto', adopt = false, log = console.log, manifest = MANIFEST, t = createT('en') } = opts
|
|
14
|
+
|
|
15
|
+
if (!SKILL_MODES.includes(skillMode)) {
|
|
16
|
+
throw new LocalizedError('error.badSkillModeRuntime', { list: SKILL_MODES.join(', '), value: skillMode })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const say = (message) => log(`[agent-setup] ${message}`)
|
|
20
|
+
const ctx = { dryRun, log: say, t }
|
|
21
|
+
|
|
22
|
+
say(t('log.repoRoot', { path: root }))
|
|
23
|
+
say(t('log.noGlobalWrites'))
|
|
24
|
+
|
|
25
|
+
// --adopt는 이미 있는 저장소를 기록 체계로 끌어오는 용도라 파일을 만들지
|
|
26
|
+
// 않는다. 설치기를 복사해 쓰던 저장소가 여기로 들어온다.
|
|
27
|
+
const results = adopt ? [] : [
|
|
28
|
+
...ensureDirs(root, manifest.dirs, ctx),
|
|
29
|
+
...ensureFiles(root, manifest.files, ctx),
|
|
30
|
+
...ensureJsonKeys(root, manifest.settings ?? [], ctx),
|
|
31
|
+
...ensureBlocks(root, manifest.blocks, ctx),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
if (!adopt) {
|
|
35
|
+
// 어댑터는 항목별로 실패를 격리한다 — 하나가 실패해도 나머지를 계속한다.
|
|
36
|
+
// update.mjs도 같은 격리를 써야 하므로 apply.mjs가 단일 출처다.
|
|
37
|
+
for (const entry of manifest.adapters) {
|
|
38
|
+
results.push(configureAdapterSafe(root, entry, { ...ctx, skillMode }))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
results.push(...ensureIgnore(root, manifest.ignore, ctx))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 기존 기록의 items·design은 보존한다 — 부트스트랩은 배선만 다루므로
|
|
45
|
+
// 사용자가 고른 설치 항목을 지울 권한이 없다. dry-run에서는 읽지 않는다:
|
|
46
|
+
// 깨진 기록이 "무엇이 바뀔지 확인"까지 막을 이유가 없다.
|
|
47
|
+
const previous = dryRun ? null : readRecord(root)
|
|
48
|
+
const record = {
|
|
49
|
+
...emptyRecord({ skillMode }),
|
|
50
|
+
// 부트스트랩은 배선만 다룬다. 사용자가 고른 언어·항목을 지울 권한이 없다.
|
|
51
|
+
lang: previous?.lang ?? null,
|
|
52
|
+
items: previous?.items ?? [],
|
|
53
|
+
design: previous?.design ?? [],
|
|
54
|
+
managed: collectManaged(root, manifest),
|
|
55
|
+
}
|
|
56
|
+
results.push(writeRecord(root, record, ctx))
|
|
57
|
+
|
|
58
|
+
const failed = results.filter((r) => !r.ok)
|
|
59
|
+
log('')
|
|
60
|
+
if (failed.length > 0) {
|
|
61
|
+
say(t('bootstrap.failures', { count: failed.length }))
|
|
62
|
+
for (const f of failed) say(` ✖ ${f.path} — ${toText(t, f.message)}`)
|
|
63
|
+
}
|
|
64
|
+
say(t('bootstrap.done'))
|
|
65
|
+
say(t('bootstrap.sharedGuide'))
|
|
66
|
+
say(t('bootstrap.sharedSkills'))
|
|
67
|
+
say(t('bootstrap.tools', { list: manifest.tools.join(', ') }))
|
|
68
|
+
say(t('bootstrap.repoOnly'))
|
|
69
|
+
say(t('bootstrap.noOverwrite'))
|
|
70
|
+
say(t('bootstrap.record', { path: RECORD_REL }))
|
|
71
|
+
|
|
72
|
+
return { results, failed, record }
|
|
73
|
+
}
|
package/lib/bootstrap/record.mjs
CHANGED
|
@@ -1,142 +1,154 @@
|
|
|
1
|
-
// 저장소에 커밋되는 설치 기록.
|
|
2
|
-
//
|
|
3
|
-
// 이 파일은 "의도"다 — 실제 상태의 근거는 여전히 스캔이다. 기록이 더하는
|
|
4
|
-
// 것은 재현성(팀원이 같은 결과를 얻는다)과 버전 고정, 그리고 "우리가 쓴
|
|
5
|
-
// 그대로인가"를 판정할 해시다.
|
|
6
|
-
//
|
|
7
|
-
// 설치기 안의 manifest.mjs(무엇을 생성할지 선언)와 이름이 겹치지 않게
|
|
8
|
-
// 저장소 쪽 파일은 '설치 기록'이라 부른다.
|
|
9
|
-
//
|
|
10
|
-
// 부트스트랩 그래프에 속하므로 node: 내장 모듈만 쓴다.
|
|
11
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
12
|
-
import { dirname } from 'node:path'
|
|
13
|
-
import { repoPath, repoPathStrict } from '../context.mjs'
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
//
|
|
105
|
-
export
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
1
|
+
// 저장소에 커밋되는 설치 기록.
|
|
2
|
+
//
|
|
3
|
+
// 이 파일은 "의도"다 — 실제 상태의 근거는 여전히 스캔이다. 기록이 더하는
|
|
4
|
+
// 것은 재현성(팀원이 같은 결과를 얻는다)과 버전 고정, 그리고 "우리가 쓴
|
|
5
|
+
// 그대로인가"를 판정할 해시다.
|
|
6
|
+
//
|
|
7
|
+
// 설치기 안의 manifest.mjs(무엇을 생성할지 선언)와 이름이 겹치지 않게
|
|
8
|
+
// 저장소 쪽 파일은 '설치 기록'이라 부른다.
|
|
9
|
+
//
|
|
10
|
+
// 부트스트랩 그래프에 속하므로 node: 내장 모듈만 쓴다.
|
|
11
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { dirname } from 'node:path'
|
|
13
|
+
import { repoPath, repoPathStrict } from '../context.mjs'
|
|
14
|
+
import { LOCALES, LocalizedError } from '../i18n/index.mjs'
|
|
15
|
+
import { hashBody, normalizeBody } from './text.mjs'
|
|
16
|
+
|
|
17
|
+
export const RECORD_REL = '.agent-kit/agent-setup.json'
|
|
18
|
+
export const FORMAT_VERSION = 1
|
|
19
|
+
|
|
20
|
+
// 발행된 패키지에도 package.json은 항상 들어간다(npm이 무조건 포함한다).
|
|
21
|
+
// createRequire 대신 URL로 읽어 의존성 0을 유지한다.
|
|
22
|
+
let cachedVersion
|
|
23
|
+
export function toolVersion() {
|
|
24
|
+
if (!cachedVersion) {
|
|
25
|
+
const url = new URL('../../package.json', import.meta.url)
|
|
26
|
+
cachedVersion = JSON.parse(readFileSync(url, 'utf8')).version
|
|
27
|
+
}
|
|
28
|
+
return cachedVersion
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function emptyRecord({ skillMode = 'auto', lang = null } = {}) {
|
|
32
|
+
return {
|
|
33
|
+
formatVersion: FORMAT_VERSION,
|
|
34
|
+
pinnedVersion: toolVersion(),
|
|
35
|
+
skillMode,
|
|
36
|
+
lang,
|
|
37
|
+
items: [],
|
|
38
|
+
design: [],
|
|
39
|
+
managed: {},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 없으면 null. 있으면 필드를 채워 돌려준다 — 손으로 편집해 필드가 빠져도
|
|
44
|
+
// 죽지 않아야 한다. 형식 버전이 다르면 추측하지 않고 던진다.
|
|
45
|
+
export function readRecord(root) {
|
|
46
|
+
const target = repoPath(root, RECORD_REL)
|
|
47
|
+
let text
|
|
48
|
+
try {
|
|
49
|
+
text = readFileSync(target, 'utf8')
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (err.code === 'ENOENT') return null
|
|
52
|
+
throw new LocalizedError('error.recordUnreadable', { path: RECORD_REL, code: err.code ?? err.message })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let parsed
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(text)
|
|
58
|
+
} catch (err) {
|
|
59
|
+
throw new LocalizedError('error.recordNotJson', { path: RECORD_REL, message: err.message })
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (parsed.formatVersion !== FORMAT_VERSION) {
|
|
63
|
+
throw new LocalizedError('error.recordVersion', { path: RECORD_REL, found: parsed.formatVersion, expected: FORMAT_VERSION })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
formatVersion: parsed.formatVersion,
|
|
68
|
+
pinnedVersion: parsed.pinnedVersion ?? null,
|
|
69
|
+
skillMode: parsed.skillMode ?? 'auto',
|
|
70
|
+
// 손으로 편집된 값 때문에 도구가 죽으면 안 된다. 모르는 값은 없는 것으로 본다.
|
|
71
|
+
lang: LOCALES.includes(parsed.lang) ? parsed.lang : null,
|
|
72
|
+
items: Array.isArray(parsed.items) ? parsed.items : [],
|
|
73
|
+
design: Array.isArray(parsed.design) ? parsed.design : [],
|
|
74
|
+
managed: parsed.managed && typeof parsed.managed === 'object' ? parsed.managed : {},
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// pinnedVersion은 여기서 실행 중 버전으로 맞춘다 — 기록을 쓰는 명령만
|
|
79
|
+
// 버전을 옮길 수 있어야 고정이 거짓말하지 않는다.
|
|
80
|
+
//
|
|
81
|
+
// log를 주면 t도 반드시 줘야 한다. 영어 리터럴 폴백을 두면 t를 빠뜨린
|
|
82
|
+
// 호출부가 한국어 출력 한가운데에 영어 한 줄을 흘리면서도 아무 신호를 내지
|
|
83
|
+
// 않는다 — update.mjs가 실제로 그렇게 새어 나갔다.
|
|
84
|
+
export function writeRecord(root, record, { dryRun = false, log, t } = {}) {
|
|
85
|
+
const target = repoPathStrict(root, RECORD_REL)
|
|
86
|
+
const body = `${JSON.stringify({ ...record, pinnedVersion: toolVersion() }, null, 2)}\n`
|
|
87
|
+
log?.(t('log.record.write', { path: RECORD_REL }))
|
|
88
|
+
if (!dryRun) {
|
|
89
|
+
mkdirSync(dirname(target), { recursive: true })
|
|
90
|
+
writeFileSync(target, body, { encoding: 'utf8' })
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, action: dryRun ? 'skip' : 'write', path: RECORD_REL }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 언어만 바꾼다. 기록이 없으면 만든다 — 부트스트랩 전에 언어를 골라도
|
|
96
|
+
// 유지되어야 하기 때문이다. 나머지 필드는 있는 그대로 옮긴다.
|
|
97
|
+
export function writeLang(root, lang, { dryRun = false, log, t } = {}) {
|
|
98
|
+
const previous = readRecord(root)
|
|
99
|
+
return writeRecord(root, { ...(previous ?? emptyRecord()), lang }, { dryRun, log, t })
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 마커는 apply.mjs의 ensureBlocks(생성)와 updateBlocks(갱신), 그리고 여기의
|
|
103
|
+
// extractBlock(판정)이 함께 쓴다. 정의가 둘로 갈리면 한쪽만 고치는 회귀가
|
|
104
|
+
// 생기므로 이 모듈이 단일 출처가 되고 apply.mjs가 import한다.
|
|
105
|
+
export const BEGIN_MARKER = '<!-- agent-kit:begin -->'
|
|
106
|
+
export const END_MARKER = '<!-- agent-kit:end -->'
|
|
107
|
+
|
|
108
|
+
// 블록은 파일 전체가 아니라 마커 사이 본문만 관리 대상이다. 파일 전체 해시와
|
|
109
|
+
// 섞이지 않게 키에 접미사를 붙인다.
|
|
110
|
+
export const BLOCK_SUFFIX = '#agent-kit'
|
|
111
|
+
|
|
112
|
+
export function managedKey(rel, isBlock) {
|
|
113
|
+
return isBlock ? `${rel}${BLOCK_SUFFIX}` : rel
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 마커 사이 본문을 정규화해 돌려준다. 마커가 없거나 순서가 뒤집혀 있으면 null.
|
|
117
|
+
export function extractBlock(text) {
|
|
118
|
+
const begin = text.indexOf(BEGIN_MARKER)
|
|
119
|
+
if (begin === -1) return null
|
|
120
|
+
const end = text.indexOf(END_MARKER, begin + BEGIN_MARKER.length)
|
|
121
|
+
if (end === -1) return null
|
|
122
|
+
return normalizeBody(text.slice(begin + BEGIN_MARKER.length, end))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readOrNull(root, rel) {
|
|
126
|
+
try {
|
|
127
|
+
return readFileSync(repoPath(root, rel), 'utf8')
|
|
128
|
+
} catch {
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 채택 규칙: 현재 내용이 이 버전의 템플릿과 정규화 후 일치하는 것만 해시를
|
|
134
|
+
// 남긴다. 그러지 않으면 사용자가 이미 고쳐 둔 파일이 "우리가 쓴 그대로"로
|
|
135
|
+
// 위장되어 다음 update에 날아간다. 값이 null인 키는 update가 절대 덮어쓰지
|
|
136
|
+
// 않으며, 없는 파일도 키를 남겨 update의 생성 분기가 집어간다.
|
|
137
|
+
export function collectManaged(root, manifest) {
|
|
138
|
+
const managed = {}
|
|
139
|
+
|
|
140
|
+
for (const { path: rel, template } of manifest.files ?? []) {
|
|
141
|
+
const text = readOrNull(root, rel)
|
|
142
|
+
const wanted = hashBody(template)
|
|
143
|
+
managed[rel] = text !== null && hashBody(text) === wanted ? wanted : null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const { path: rel, block } of manifest.blocks ?? []) {
|
|
147
|
+
const text = readOrNull(root, rel)
|
|
148
|
+
const wanted = hashBody(extractBlock(block) ?? block)
|
|
149
|
+
const current = text === null ? null : extractBlock(text)
|
|
150
|
+
managed[managedKey(rel, true)] = current !== null && hashBody(current) === wanted ? wanted : null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return managed
|
|
154
|
+
}
|