@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 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
- const SKILL_MODES = ['auto', 'link', 'copy']
20
+ // 허용값의 단일 출처. flow.mjs(실행 시 검증)와 record.mjs(기록 읽기)가 함께 쓴다 —
21
+ // 셋이 따로 들고 있으면 한 곳에 값이 늘 때 나머지가 그 값을 거부한다.
22
+ export const SKILL_MODES = ['auto', 'link', 'copy']
21
23
 
22
24
  // 인자 사양: 플래그 이름 → 'bool'(값 없음) | 'value'(값 하나를 소비).
23
25
  // 미지 인자를 조용히 삼키지 않기 위한 것이다 — 오타(`--dryrun`)나 지원하지
@@ -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
- if (text.includes(JSON.stringify(key))) {
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
  }
@@ -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
  // 왼쪽에 채워야 한다. 숫자만 다루니 실제로는 문자 수와 같지만, 이 저장소의
@@ -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
- managed: parsed.managed && typeof parsed.managed === 'object' ? parsed.managed : {},
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
- if (err.code !== 'ERR_MODULE_NOT_FOUND') throw err
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
- // 경로 구분자·상위 이동·Windows 금지 문자·제어문자는 막는다.
88
+ // 경로 세그먼트 가드는 잎 모듈(lib/untrusted.mjs)에 있다 — lib/catalog.mjs도
89
+ // 쓰는데 그쪽은 design-md에 정적으로 닿을 수 없다. 이 모듈의 소비자
90
+ // (flow·scan·refresh-bundle)가 계속 여기서 가져갈 수 있게 다시 내보낸다.
89
91
  // (name은 원격 README 파싱이나 디렉터리 스캔에서 오므로 신뢰 경계 밖이다.)
90
- export function isSafeSegment(text) {
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.0",
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",