@rch4com/agent-setup 1.21.0 → 1.21.1
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/README.md +16 -0
- package/lib/args.mjs +3 -1
- package/lib/bootstrap/apply.mjs +8 -6
- package/lib/bootstrap/flow.mjs +1 -2
- package/lib/bootstrap/record.mjs +5 -2
- package/lib/catalog.mjs +14 -1
- package/lib/deps.mjs +4 -2
- package/lib/design-md/catalog.mjs +5 -9
- package/lib/tomlfile.mjs +1 -4
- package/lib/untrusted.mjs +26 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,6 +78,14 @@ show up in `status` and can be pulled in later with `update --force`.
|
|
|
78
78
|
|
|
79
79
|
## Safety
|
|
80
80
|
|
|
81
|
+
The rules below are what the **bootstrap** (`bootstrap`, `update`, `status`)
|
|
82
|
+
upholds. The optional-item picker runs third-party installers, and its two
|
|
83
|
+
`global.*` items (`global.superpowers`, `global.ponytail`) deliberately write
|
|
84
|
+
to per-user CLI configuration under your home directory — the screen marks
|
|
85
|
+
them as machine-global and `--set` never removes them. The full list of what
|
|
86
|
+
each item runs is in the
|
|
87
|
+
[usage guide](https://github.com/rch4com/Agent-Setup/blob/main/AgentSetup-README.md#what-the-installer-runs).
|
|
88
|
+
|
|
81
89
|
- Runs only inside a Git repository, and never writes outside the repository root.
|
|
82
90
|
- Never reads or modifies global configuration in your home directory.
|
|
83
91
|
- Never overwrites existing configuration files.
|
|
@@ -176,6 +184,14 @@ npx @rch4com/agent-setup bootstrap --adopt
|
|
|
176
184
|
|
|
177
185
|
## 안전 원칙
|
|
178
186
|
|
|
187
|
+
아래는 **부트스트랩**(`bootstrap`·`update`·`status`)이 지키는 규칙입니다.
|
|
188
|
+
선택 항목 화면은 제3자 설치기를 실행하며, 그중 `global.*` 두 항목
|
|
189
|
+
(`global.superpowers`, `global.ponytail`)은 의도적으로 홈 디렉터리 아래
|
|
190
|
+
CLI별 사용자 설정에 씁니다 — 화면이 머신 전역임을 표시하고 `--set`은 이
|
|
191
|
+
항목을 제거하지 않습니다. 항목마다 무엇을 실행하는지는
|
|
192
|
+
[사용 안내](https://github.com/rch4com/Agent-Setup/blob/main/AgentSetup-README.ko.md#설치기가-실행하는-것)에
|
|
193
|
+
있습니다.
|
|
194
|
+
|
|
179
195
|
- 반드시 Git 저장소 안에서만 실행되며, 저장소 루트 밖에는 쓰지 않습니다.
|
|
180
196
|
- 홈 디렉터리의 글로벌 설정을 읽거나 수정하지 않습니다.
|
|
181
197
|
- 기존 설정 파일을 덮어쓰지 않습니다.
|
package/lib/args.mjs
CHANGED
|
@@ -17,7 +17,9 @@ export const designUsage = (t) => t('usage.design')
|
|
|
17
17
|
export const updateUsage = (t) => t('usage.update')
|
|
18
18
|
export const statusUsage = (t) => t('usage.status')
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// 허용값의 단일 출처. flow.mjs(실행 시 검증)와 record.mjs(기록 읽기)가 함께 쓴다 —
|
|
21
|
+
// 셋이 따로 들고 있으면 한 곳에 값이 늘 때 나머지가 그 값을 거부한다.
|
|
22
|
+
export const SKILL_MODES = ['auto', 'link', 'copy']
|
|
21
23
|
|
|
22
24
|
// 인자 사양: 플래그 이름 → 'bool'(값 없음) | 'value'(값 하나를 소비).
|
|
23
25
|
// 미지 인자를 조용히 삼키지 않기 위한 것이다 — 오타(`--dryrun`)나 지원하지
|
package/lib/bootstrap/apply.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname } from 'node:path'
|
|
|
5
5
|
import { repoPath, repoPathStrict } from '../context.mjs'
|
|
6
6
|
import { ensureGitignoreEntries } from '../gitignore.mjs'
|
|
7
7
|
import { createT, msg } from '../i18n/index.mjs'
|
|
8
|
+
import { escapeRegExp } from '../untrusted.mjs'
|
|
8
9
|
import { hashBody, normalizeBody } from './text.mjs'
|
|
9
10
|
import { BEGIN_MARKER, END_MARKER, extractBlock, managedKey } from './record.mjs'
|
|
10
11
|
import { configureAdapter } from './adapter.mjs'
|
|
@@ -231,12 +232,13 @@ export function ensureJsonKeys(root, entries, { dryRun = false, log, t = createT
|
|
|
231
232
|
return { ok: true, action: 'warn', path: rel, message: msg('msg.readFailed') }
|
|
232
233
|
}
|
|
233
234
|
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
// 최상위 키만 보는 파서가
|
|
239
|
-
|
|
235
|
+
// 줄 머리에 `"키":` 꼴로 놓인 것만 "있음"으로 본다. 단순 문자열 포함
|
|
236
|
+
// 검사는 주석(`// see "chat.useAgentsMdFile"`)이나 다른 키의 값 안에 든
|
|
237
|
+
// 이름까지 "있음"으로 읽어, 키를 넣지 않고도 성공 로그를 냈다 — VS Code
|
|
238
|
+
// Copilot이 AGENTS.md를 영영 읽지 못하는데 화면은 성공이다. 정확히 하려면
|
|
239
|
+
// 최상위 키만 보는 파서가 필요한데 부트스트랩은 의존성을 쓸 수 없다 —
|
|
240
|
+
// 중첩 객체 안의 같은 키를 최상위로 오판하는 쪽(넣지 않음)으로만 틀린다.
|
|
241
|
+
if (new RegExp(`^\\s*${escapeRegExp(JSON.stringify(key))}\\s*:`, 'm').test(text)) {
|
|
240
242
|
log(t('log.json.keep', { path: rel, key }))
|
|
241
243
|
return { ok: true, action: 'skip', path: rel }
|
|
242
244
|
}
|
package/lib/bootstrap/flow.mjs
CHANGED
|
@@ -5,11 +5,10 @@ import {
|
|
|
5
5
|
configureAdapterSafe, ensureBlocks, ensureDirs, ensureFiles, ensureIgnore, ensureJsonKeys,
|
|
6
6
|
} from './apply.mjs'
|
|
7
7
|
import { RECORD_REL, collectManaged, emptyRecord, readRecord, writeRecord } from './record.mjs'
|
|
8
|
+
import { SKILL_MODES } from '../args.mjs'
|
|
8
9
|
import { createT, LocalizedError, toText } from '../i18n/index.mjs'
|
|
9
10
|
import { width } from '../width.mjs'
|
|
10
11
|
|
|
11
|
-
const SKILL_MODES = ['auto', 'link', 'copy']
|
|
12
|
-
|
|
13
12
|
// 오른쪽 정렬. width.mjs의 pad()는 오른쪽에 공백을 채워 왼쪽 정렬만 하므로
|
|
14
13
|
// 숫자에는 못 쓴다 — 자릿수가 9에서 10으로 늘 때 열이 흔들리지 않으려면
|
|
15
14
|
// 왼쪽에 채워야 한다. 숫자만 다루니 실제로는 문자 수와 같지만, 이 저장소의
|
package/lib/bootstrap/record.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// 부트스트랩 그래프에 속하므로 node: 내장 모듈만 쓴다.
|
|
11
11
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
12
12
|
import { dirname } from 'node:path'
|
|
13
|
+
import { SKILL_MODES } from '../args.mjs'
|
|
13
14
|
import { repoPath, repoPathStrict } from '../context.mjs'
|
|
14
15
|
import { LOCALES, LocalizedError } from '../i18n/index.mjs'
|
|
15
16
|
import { hashBody, normalizeBody } from './text.mjs'
|
|
@@ -66,12 +67,14 @@ export function readRecord(root) {
|
|
|
66
67
|
return {
|
|
67
68
|
formatVersion: parsed.formatVersion,
|
|
68
69
|
pinnedVersion: parsed.pinnedVersion ?? null,
|
|
69
|
-
skillMode: parsed.skillMode ?? 'auto',
|
|
70
70
|
// 손으로 편집된 값 때문에 도구가 죽으면 안 된다. 모르는 값은 없는 것으로 본다.
|
|
71
|
+
// skillMode도 같다 — 걸러 두지 않으면 update가 그 값을 어댑터까지 흘려보낸다.
|
|
72
|
+
skillMode: SKILL_MODES.includes(parsed.skillMode) ? parsed.skillMode : 'auto',
|
|
71
73
|
lang: LOCALES.includes(parsed.lang) ? parsed.lang : null,
|
|
72
74
|
items: Array.isArray(parsed.items) ? parsed.items : [],
|
|
73
75
|
design: Array.isArray(parsed.design) ? parsed.design : [],
|
|
74
|
-
|
|
76
|
+
// 배열도 typeof가 object다 — managed[rel] 조회가 undefined만 돌려주는 빈 껍데기가 된다.
|
|
77
|
+
managed: parsed.managed && typeof parsed.managed === 'object' && !Array.isArray(parsed.managed) ? parsed.managed : {},
|
|
75
78
|
}
|
|
76
79
|
}
|
|
77
80
|
|
package/lib/catalog.mjs
CHANGED
|
@@ -7,6 +7,9 @@ import { CLIS, CLI_IDS, MCP_CLI_IDS } from './clis.mjs'
|
|
|
7
7
|
import { isPluginEnabled, enablePlugin, disablePlugin } from './claude-plugins.mjs'
|
|
8
8
|
import { repoPath, repoPathStrict } from './context.mjs'
|
|
9
9
|
import { LocalizedError, msg } from './i18n/index.mjs'
|
|
10
|
+
// design-md가 아니라 잎 모듈에서 가져온다 — deps.test.mjs의 "의존성 없는
|
|
11
|
+
// 트리"는 design-md를 통째로 빼고 --list를 돌린다(bootstrap.isolation.test.mjs).
|
|
12
|
+
import { escapeRegExp, isSafeSegment } from './untrusted.mjs'
|
|
10
13
|
|
|
11
14
|
const ITEMS_DIR = join(dirname(fileURLToPath(import.meta.url)), 'items')
|
|
12
15
|
|
|
@@ -240,11 +243,18 @@ function findSkillDir(root, skill) {
|
|
|
240
243
|
const file = join(base, name, 'SKILL.md')
|
|
241
244
|
if (!existsSync(file)) continue
|
|
242
245
|
const head = readFileSync(file, 'utf8').slice(0, 2000)
|
|
243
|
-
if (new RegExp(`^name:\\s*['"]?${skill}['"]?\\s*$`, 'm').test(head)) return name
|
|
246
|
+
if (new RegExp(`^name:\\s*['"]?${escapeRegExp(skill)}['"]?\\s*$`, 'm').test(head)) return name
|
|
244
247
|
}
|
|
245
248
|
return null
|
|
246
249
|
}
|
|
247
250
|
|
|
251
|
+
// 잠금 파일의 이름은 상류 SKILL.md frontmatter에서 온다 — 신뢰 경계 밖이다.
|
|
252
|
+
// `npx skills remove <이름>`의 인자이자 `.agents/skills/<이름>` 경로가 되므로,
|
|
253
|
+
// 플래그로 읽히는 선행 대시와 경로 구분자·상위 이동은 어느 쪽으로도 쓸 수 없다.
|
|
254
|
+
function isSafeSkillName(name) {
|
|
255
|
+
return isSafeSegment(name) && !name.startsWith('-')
|
|
256
|
+
}
|
|
257
|
+
|
|
248
258
|
// 레지스트리가 저장소 루트에 남기는 잠금 파일. 스킬 이름마다 어느 출처에서
|
|
249
259
|
// 왔는지를 적어 두므로, 한 상류가 여러 스킬을 깔았을 때 "우리가 넣은 것"만
|
|
250
260
|
// 골라 지울 수 있는 유일한 근거다.
|
|
@@ -321,6 +331,9 @@ export function defineRegistrySkill({ id, label, source, skill, anchor = null, n
|
|
|
321
331
|
const names = skill === '*' ? lockedSkills(root, source) : [skill]
|
|
322
332
|
const targets = names.length > 0 ? names : [probe]
|
|
323
333
|
for (const name of targets) {
|
|
334
|
+
// 인자·경로로 쓸 수 없는 이름은 건너뛴다. 잠금 항목은 아래 pruneLock이
|
|
335
|
+
// 함께 지운다 — 남기면 다음 제거가 같은 자리에서 또 멈춘다.
|
|
336
|
+
if (!isSafeSkillName(name)) continue
|
|
324
337
|
// 레지스트리의 remove는 --agent universal에서 "Done!"을 찍고도 디렉터리를
|
|
325
338
|
// 남긴다(실측). 그래서 성공/실패와 무관하게 남은 디렉터리를 우리가 지운다 —
|
|
326
339
|
// 남겨 두면 detect가 계속 installed로 읽어 제거가 안 된 채 성공으로 보인다.
|
package/lib/deps.mjs
CHANGED
|
@@ -9,9 +9,11 @@ export async function withDeps(load, t) {
|
|
|
9
9
|
try {
|
|
10
10
|
return await load()
|
|
11
11
|
} catch (err) {
|
|
12
|
-
//
|
|
12
|
+
// 패키지가 정말 없을 때만 안내로 바꾼다. 로드된 모듈 안에서 난 오류는
|
|
13
13
|
// 그대로 올려보내야 한다 — 삼키면 진짜 버그가 "의존성 없음"으로 둔갑한다.
|
|
14
|
-
|
|
14
|
+
// 상대 경로 import 오타도 같은 코드로 오지만 Node는 문구로 가른다: 패키지
|
|
15
|
+
// 부재는 "Cannot find package", 파일 부재는 "Cannot find module".
|
|
16
|
+
if (err.code !== 'ERR_MODULE_NOT_FOUND' || !/Cannot find package/.test(err.message)) throw err
|
|
15
17
|
throw new LocalizedError('error.depsMissing')
|
|
16
18
|
}
|
|
17
19
|
}
|
|
@@ -5,6 +5,7 @@ import { createHash } from 'node:crypto'
|
|
|
5
5
|
import { repoPath, repoPathStrict } from '../context.mjs'
|
|
6
6
|
import { PROVIDERS, getProvider } from './providers/index.mjs'
|
|
7
7
|
import { createT, LocalizedError } from '../i18n/index.mjs'
|
|
8
|
+
import { isSafeSegment } from '../untrusted.mjs'
|
|
8
9
|
|
|
9
10
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
10
11
|
export const CATALOG_PATH = join(HERE, 'catalog.json')
|
|
@@ -84,16 +85,11 @@ export async function netFetch(url, opts = {}) {
|
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
// 경로 세그먼트
|
|
88
|
-
//
|
|
88
|
+
// 경로 세그먼트 가드는 잎 모듈(lib/untrusted.mjs)에 있다 — lib/catalog.mjs도
|
|
89
|
+
// 쓰는데 그쪽은 design-md에 정적으로 닿을 수 없다. 이 모듈의 소비자
|
|
90
|
+
// (flow·scan·refresh-bundle)가 계속 여기서 가져갈 수 있게 다시 내보낸다.
|
|
89
91
|
// (name은 원격 README 파싱이나 디렉터리 스캔에서 오므로 신뢰 경계 밖이다.)
|
|
90
|
-
export
|
|
91
|
-
const value = String(text ?? '')
|
|
92
|
-
// 선행·후행 점은 숨김 파일이자 Windows에서 다루기 어려운 이름이다('.', '..' 포함).
|
|
93
|
-
if (!value || value.startsWith('.') || value.endsWith('.')) return false
|
|
94
|
-
// eslint-disable-next-line no-control-regex
|
|
95
|
-
return !/[\\/:*?"<>|\x00-\x1f]/.test(value)
|
|
96
|
-
}
|
|
92
|
+
export { isSafeSegment }
|
|
97
93
|
|
|
98
94
|
// 라벨·카테고리·설명은 원격 README나 DESIGN.md 본문에서 온다. 목록과 TUI가
|
|
99
95
|
// 이 값을 터미널에 그대로 찍으므로 제어문자를 걷어낸다 — ANSI 이스케이프가
|
package/lib/tomlfile.mjs
CHANGED
|
@@ -2,15 +2,12 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
|
2
2
|
import { dirname } from 'node:path'
|
|
3
3
|
import { parse } from 'smol-toml'
|
|
4
4
|
import { LocalizedError } from './i18n/index.mjs'
|
|
5
|
+
import { escapeRegExp } from './untrusted.mjs'
|
|
5
6
|
|
|
6
7
|
function readText(file) {
|
|
7
8
|
return existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
function escapeRegExp(text) {
|
|
11
|
-
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
12
|
-
}
|
|
13
|
-
|
|
14
11
|
// 깨진 TOML은 읽기·쓰기 모두 거부한다. 예전에는 파싱 실패를 "섹션 없음"으로
|
|
15
12
|
// 돌려줘, 닫는 대괄호가 빠진 파일 끝에 같은 섹션이 하나 더 붙고 사용자는
|
|
16
13
|
// 파일이 깨진 것을 끝까지 몰랐다 — jsonfile.mjs의 JSON 규칙과 같다.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// 신뢰 경계 밖에서 온 문자열의 가드. 잎 모듈이다 — bootstrap/apply.mjs가
|
|
2
|
+
// 쓰므로 의존성이 들어오면 npm install 없이 도는 부트스트랩
|
|
3
|
+
// (bootstrap.isolation.test.mjs)이 깨지고, lib/catalog.mjs가 쓰므로 design-md
|
|
4
|
+
// 아래에 두면 design-md를 뺀 "의존성 없는 트리"(deps.test.mjs)에서 --list가
|
|
5
|
+
// 원시 "Cannot find module"로 죽는다.
|
|
6
|
+
//
|
|
7
|
+
// 이 저장소의 방어는 "쓰기 경로만 엄격 검사"인데, 그 규칙은 읽은 데이터가
|
|
8
|
+
// 경로·명령 인자·정규식이 되는 자리를 각각 덮지는 않는다. 그래서 그 셋을
|
|
9
|
+
// 한 곳에 모아 둔다 — 원격 README의 항목 이름, 잠금 파일의 스킬 이름,
|
|
10
|
+
// 디렉터리 스캔의 폴더 이름이 전부 여기를 지난다.
|
|
11
|
+
|
|
12
|
+
// 경로 세그먼트 하나로 안전한 이름인지 본다. 한글 등 유니코드는 허용하되
|
|
13
|
+
// 경로 구분자·상위 이동·Windows 금지 문자·제어문자는 막는다.
|
|
14
|
+
export function isSafeSegment(text) {
|
|
15
|
+
const value = String(text ?? '')
|
|
16
|
+
// 선행·후행 점은 숨김 파일이자 Windows에서 다루기 어려운 이름이다('.', '..' 포함).
|
|
17
|
+
if (!value || value.startsWith('.') || value.endsWith('.')) return false
|
|
18
|
+
// eslint-disable-next-line no-control-regex
|
|
19
|
+
return !/[\\/:*?"<>|\x00-\x1f]/.test(value)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// RegExp 본문에 넣기 전의 이스케이프. 잠금 파일의 스킬 이름처럼 상류에서 온
|
|
23
|
+
// 문자열이 `(`를 품으면 RegExp 생성 자체가 던져 제거가 중간에 죽었다.
|
|
24
|
+
export function escapeRegExp(text) {
|
|
25
|
+
return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rch4com/agent-setup",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.1",
|
|
4
4
|
"description": "Repository-scoped bootstrap and optional-item installer for using multiple coding-agent CLIs in one repository | 여러 코딩 에이전트 CLI를 한 저장소에서 함께 쓰기 위한 저장소 범위 부트스트랩과 선택 항목 설치기",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents-md",
|