@rch4com/agent-setup 1.21.0 → 1.22.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/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/design-md/providers/oppadu.mjs +128 -0
- package/lib/i18n/catalog/en.mjs +2 -0
- package/lib/i18n/catalog/ko.mjs +2 -0
- package/lib/items/skill.officecli.mjs +21 -0
- 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 이스케이프가
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// 오빠두엑셀 "디자인 시스템 갤러리" 프로바이더.
|
|
2
|
+
// https://www.oppadu.com/tools/design-systems-site/
|
|
3
|
+
//
|
|
4
|
+
// ⚠️ 아직 PROVIDERS(providers/index.mjs)에 등록하지 않는다. 사이트는 마크다운
|
|
5
|
+
// 다운로드·복사·원본 보기를 로그인 회원 전용으로 두고 바닥글이 "All rights
|
|
6
|
+
// reserved"다 — 재배포 허가(info@oppadu.com)가 오기 전에는 번들도, 카탈로그
|
|
7
|
+
// 항목도, 등록도 하지 않는다. design-md.oppadu.test.mjs가 이 상태를 못박는다.
|
|
8
|
+
// 허가가 오면: index.mjs에 등록 → `design --sync=catalog` → `npm run refresh-bundle`
|
|
9
|
+
// → cache/oppadu/LICENSE.md 고지 → README의 design.md 절 갱신 → 발행.
|
|
10
|
+
//
|
|
11
|
+
// 사이트 구조(2026-09-06 실측, schema_version 3):
|
|
12
|
+
// 카탈로그 data/brands-index.json — 브랜드 242개의 slug·이름·지역·한 줄 설명.
|
|
13
|
+
// 갤러리 그리드가 그대로 읽는 공개 파일이다.
|
|
14
|
+
// 파일 data/detail/<slug>.json — raw_markdown(라이트)·raw_markdown_dark.
|
|
15
|
+
// 사이트의 "가이드 다운로드"는 라이트 모드에서 raw_markdown의
|
|
16
|
+
// `[data-theme="dark"]` 규칙을 걷어낸 것을 <slug>-light.md로 저장한다
|
|
17
|
+
// (app.js currentDownloadMd·stripDarkBlock) — DESIGN.md도 그 판을 쓴다.
|
|
18
|
+
// 미리보기 brand/<slug>.html — 로그인 없이 보이는 공개 페이지.
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
20
|
+
import { join, dirname } from 'node:path'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
import { LocalizedError } from '../../i18n/index.mjs'
|
|
23
|
+
import { isSafeSegment } from '../../untrusted.mjs'
|
|
24
|
+
|
|
25
|
+
const BUNDLE_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'cache')
|
|
26
|
+
|
|
27
|
+
const BASE = 'https://www.oppadu.com/tools/design-systems-site'
|
|
28
|
+
const INDEX_URL = `${BASE}/data/brands-index.json`
|
|
29
|
+
|
|
30
|
+
// 카테고리는 사이트의 전체 목록 페이지(brand/)가 쓰는 지역 묶음과 같다 —
|
|
31
|
+
// 한국 48·아시아 14·해외 180. 색인의 `western`을 그 페이지는 "해외"로 적는다.
|
|
32
|
+
export const REGION_LABELS = { korea: '한국', asia: '아시아', western: '해외' }
|
|
33
|
+
|
|
34
|
+
// 사이트의 displayName과 같은 규칙 — 한국 브랜드만 괄호에 한글 이름을 붙인다.
|
|
35
|
+
function displayName(b) {
|
|
36
|
+
return b.region === 'korea' && b.brand_ko ? `${b.brand} (${b.brand_ko})` : String(b.brand ?? b.slug)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// brands-index.json → 카탈로그 엔트리. slug가 곧 이름이자 설치 경로 세그먼트다.
|
|
40
|
+
export function parseIndex(index) {
|
|
41
|
+
const brands = Array.isArray(index?.brands) ? index.brands : []
|
|
42
|
+
return brands
|
|
43
|
+
.filter((b) => typeof b?.slug === 'string' && isSafeSegment(b.slug))
|
|
44
|
+
.map((b) => ({
|
|
45
|
+
name: b.slug,
|
|
46
|
+
label: displayName(b),
|
|
47
|
+
category: REGION_LABELS[b.region] ?? String(b.region ?? ''),
|
|
48
|
+
description: String(b.signature_keyword ?? ''),
|
|
49
|
+
}))
|
|
50
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// app.js의 stripDarkBlock을 그대로 옮겼다 — 사이트의 라이트 다운로드와 글자
|
|
54
|
+
// 단위로 같은 결과를 내야 한다. `[data-theme="dark"] { … }` 규칙을 중괄호
|
|
55
|
+
// 짝으로 찾아 제 줄까지 통째로 지운다(CSS 규칙 안에 중첩 중괄호는 없다).
|
|
56
|
+
export function stripDarkBlock(md) {
|
|
57
|
+
if (!md) return md
|
|
58
|
+
let out = md
|
|
59
|
+
let idx
|
|
60
|
+
while ((idx = out.indexOf('[data-theme="dark"]')) !== -1) {
|
|
61
|
+
const open = out.indexOf('{', idx)
|
|
62
|
+
if (open === -1) break
|
|
63
|
+
let depth = 1
|
|
64
|
+
let i = open + 1
|
|
65
|
+
for (; i < out.length && depth > 0; i++) {
|
|
66
|
+
if (out[i] === '{') depth++
|
|
67
|
+
else if (out[i] === '}') depth--
|
|
68
|
+
}
|
|
69
|
+
let start = idx
|
|
70
|
+
while (start > 0 && (out[start - 1] === ' ' || out[start - 1] === '\t')) start--
|
|
71
|
+
if (start > 0 && out[start - 1] === '\n') start--
|
|
72
|
+
out = out.slice(0, start) + out.slice(i)
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// app.js의 currentDownloadMd(mode='light')와 같다. 라이트·다크 겸용 브랜드는
|
|
78
|
+
// 다크 규칙을 걷어낸 라이트 판, 단일 모드 브랜드는 원문 그대로.
|
|
79
|
+
export function toDesignMd(detail) {
|
|
80
|
+
const md = detail?.raw_markdown
|
|
81
|
+
if (typeof md !== 'string' || md.length === 0) return null
|
|
82
|
+
const modes = Array.isArray(detail.theme_modes) ? detail.theme_modes : []
|
|
83
|
+
const dual = modes.includes('light') && modes.includes('dark')
|
|
84
|
+
const hasDark = dual && typeof detail.raw_markdown_dark === 'string' && detail.raw_markdown_dark.length > 0
|
|
85
|
+
return hasDark ? stripDarkBlock(md) : md
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const oppadu = {
|
|
89
|
+
id: 'oppadu',
|
|
90
|
+
label: 'Design Systems Gallery (오빠두엑셀)',
|
|
91
|
+
files: ['DESIGN.md'],
|
|
92
|
+
// 재배포 허가 전이다 — refresh-bundle과 번들 검사가 이 값을 보고 건너뛴다.
|
|
93
|
+
redistributable: false,
|
|
94
|
+
|
|
95
|
+
fileUrl(name) {
|
|
96
|
+
return `${BASE}/data/detail/${encodeURIComponent(name)}.json`
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
webUrl(name) {
|
|
100
|
+
return `${BASE}/brand/${encodeURIComponent(name)}.html`
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
bundlePath(name, file = 'DESIGN.md') {
|
|
104
|
+
return join(BUNDLE_DIR, this.id, name, file)
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
bundledText(name, file = 'DESIGN.md') {
|
|
108
|
+
if (/[\\/]/.test(name)) return null
|
|
109
|
+
const p = this.bundlePath(name, file)
|
|
110
|
+
return existsSync(p) ? readFileSync(p, 'utf8') : null
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// 상세 JSON 하나가 곧 파일이다. 응답이 없거나 원문 필드가 비면 null —
|
|
114
|
+
// defineDesignMd가 그것을 "내려받지 못했다"로 보고한다.
|
|
115
|
+
async fetchFile(fetchImpl, name) {
|
|
116
|
+
const res = await fetchImpl(this.fileUrl(name))
|
|
117
|
+
if (!res.ok) return null
|
|
118
|
+
return toDesignMd(await res.json())
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
async fetchCatalog(fetchImpl) {
|
|
122
|
+
const res = await fetchImpl(INDEX_URL)
|
|
123
|
+
if (!res.ok) throw new LocalizedError('error.indexFetch', { url: INDEX_URL, status: res.status })
|
|
124
|
+
return parseIndex(await res.json())
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default oppadu
|
package/lib/i18n/catalog/en.mjs
CHANGED
|
@@ -289,6 +289,7 @@ export default {
|
|
|
289
289
|
'item.skill.karpathy.note': 'behavioral guidelines that curb overbuilding and vague success criteria',
|
|
290
290
|
'item.skill.prompt-master.note': 'a skill that writes and refines prompts for AI tools. Activates only when explicitly asked to write or fix a prompt. Harness-neutral body shared by all 11 CLIs',
|
|
291
291
|
'item.skill.strix.note': 'copies 9 security-testing skills (pentesting, OWASP Top 10, vulnerability discovery and fix flows) into the shared .agents/skills. The skills drive the Strix agent, so the strix binary is required on PATH — self-hosted needs pipx install strix-agent plus Docker and STRIX_LLM/LLM_API_KEY, managed needs strix cloud login. Use only against targets you are authorized to test',
|
|
292
|
+
'item.skill.officecli.note': 'copies a skill for reading, editing and creating Word, Excel and PowerPoint documents (.docx, .xlsx, .pptx) into the shared .agents/skills. The skill drives the officecli binary, so officecli is required on PATH — npm install -g @officecli/officecli or the upstream install.sh/install.ps1. No Office installation needed',
|
|
292
293
|
'item.skill.gstack.note': 'repo-local clone + setup (needs bash; Git Bash on Windows). Runtime state (~/.gstack) may be created globally. In a bootstrapped repository .claude/skills is a junction, so the files land in the shared .agents/skills — codex and opencode scan recursively and see the individual skills too',
|
|
293
294
|
'item.skill.gsd.note': 'npx @opengsd/gsd-core, installed per project — wires five runtimes in one run: claude, codex, opencode, copilot, kilo (.claude/commands, .codex/skills, .opencode/skills, .github/skills, .kilo/skills). Each runtime writes roughly 700 files. gemini was dropped upstream and kimi still blocks project installs',
|
|
294
295
|
// 커밋 템플릿은 CLI 배선이 아니라 저장소 규약이다 — note가 "무엇이 놓이고
|
|
@@ -489,4 +490,5 @@ export default {
|
|
|
489
490
|
'error.ambiguousDesignId': 'ambiguous name \'{token}\' — pick a provider: {options}',
|
|
490
491
|
'error.designDownload': '{provider}/{name}: DESIGN.md download failed',
|
|
491
492
|
'error.readmeFetch': 'README fetch failed: HTTP {status}',
|
|
493
|
+
'error.indexFetch': 'index fetch failed: HTTP {status} ({url})',
|
|
492
494
|
}
|
package/lib/i18n/catalog/ko.mjs
CHANGED
|
@@ -282,6 +282,7 @@ export default {
|
|
|
282
282
|
'item.skill.karpathy.note': '과설계와 모호한 완료 기준을 줄이는 행동 지침',
|
|
283
283
|
'item.skill.prompt-master.note': 'AI 도구용 프롬프트를 만들고 다듬는 스킬. 프롬프트 작성·수정을 명시적으로 요청할 때만 활성화됩니다. 본문이 하네스 중립이라 11개 CLI가 함께 씁니다',
|
|
284
284
|
'item.skill.strix.note': '보안 테스트 스킬 9종(침투 테스트·OWASP Top 10·취약점 탐지와 수정 흐름)을 공유 .agents/skills에 복사합니다. 스킬이 Strix 에이전트를 구동하므로 PATH에 strix 바이너리가 필요합니다 — 자체 호스팅은 pipx install strix-agent에 Docker와 STRIX_LLM·LLM_API_KEY, 관리형은 strix cloud login. 승인된 대상에만 쓰세요',
|
|
285
|
+
'item.skill.officecli.note': 'Word·Excel·PowerPoint(.docx·.xlsx·.pptx) 문서를 읽고 고치고 만드는 스킬을 공유 .agents/skills에 복사합니다. 스킬이 officecli 바이너리를 구동하므로 PATH에 officecli가 필요합니다 — npm install -g @officecli/officecli 또는 상류 install.sh·install.ps1. Office 설치는 필요 없습니다',
|
|
285
286
|
'item.skill.gstack.note': '저장소 로컬 clone + setup (bash 필요, Windows는 Git Bash). 런타임 상태(~/.gstack)는 전역에 생길 수 있음. 부트스트랩 저장소에서는 .claude/skills가 Junction이라 실물이 공유 .agents/skills에 앉습니다 — codex·opencode는 재귀 스캔이라 개별 스킬까지 함께 봅니다',
|
|
286
287
|
'item.skill.gsd.note': 'npx @opengsd/gsd-core 프로젝트 로컬 설치 — claude·codex·opencode·copilot·kilo 다섯 런타임을 한 번에 배선합니다(.claude/commands, .codex/skills, .opencode/skills, .github/skills, .kilo/skills). 런타임마다 700개 안팎의 파일이 생깁니다. gemini는 상류가 제거했고 kimi는 프로젝트 설치를 아직 막습니다',
|
|
287
288
|
// 커밋 템플릿은 CLI 배선이 아니라 저장소 규약이다 — en.mjs 쪽 주석 참고.
|
|
@@ -474,4 +475,5 @@ export default {
|
|
|
474
475
|
'error.ambiguousDesignId': '중복된 이름 \'{token}\' — 제공자를 지정하세요: {options}',
|
|
475
476
|
'error.designDownload': '{provider}/{name}: DESIGN.md 다운로드 실패',
|
|
476
477
|
'error.readmeFetch': 'README 가져오기 실패: HTTP {status}',
|
|
478
|
+
'error.indexFetch': '색인 가져오기 실패: HTTP {status} ({url})',
|
|
477
479
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineRegistrySkill } from '../catalog.mjs'
|
|
2
|
+
// 상류는 루트 SKILL.md(공개 URL이 서빙하는 판)와 skills/officecli/SKILL.md
|
|
3
|
+
// (레지스트리가 발견하는 spec 준수 판)를 바이트 동일하게 유지한다(상류
|
|
4
|
+
// skill-parity 워크플로). 저장소 루트를 source로 주면 레지스트리가 루트
|
|
5
|
+
// SKILL.md를 스킬로 보고 **저장소 전체**(src·assets·워크플로, 94MB·1,208
|
|
6
|
+
// 파일)를 .agents/skills/officecli에 복사한다 — 2026-09-15 실측. 그래서
|
|
7
|
+
// skills/officecli 하위 경로를 준다: SKILL.md 하나(28K)만 들어온다.
|
|
8
|
+
// 잠금 파일의 source는 하위 경로 없이 iOfficeAI/OfficeCLI로 기록되는데,
|
|
9
|
+
// 단일 스킬 항목은 잠금이 아니라 이름으로 지우므로 영향이 없다.
|
|
10
|
+
//
|
|
11
|
+
// 스킬 본문은 officecli 바이너리를 구동하는 지침이라 바이너리가 사실상
|
|
12
|
+
// 전제다 — note가 알린다. 같은 폴더의 전문 스킬 10종(officecli-docx·pptx·
|
|
13
|
+
// xlsx, pitch-deck, morph-ppt 등)은 넣지 않는다 — 상류가 에이전트에 안내하는
|
|
14
|
+
// 공식 경로는 이 하나다(README "For AI Agents").
|
|
15
|
+
export default defineRegistrySkill({
|
|
16
|
+
id: 'skill.officecli', label: 'OfficeCLI', group: '__service',
|
|
17
|
+
source: 'https://github.com/iOfficeAI/OfficeCLI/tree/main/skills/officecli',
|
|
18
|
+
skill: 'officecli',
|
|
19
|
+
note: 'item.skill.officecli.note',
|
|
20
|
+
verified: '2026-09-15',
|
|
21
|
+
})
|
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.
|
|
3
|
+
"version": "1.22.0",
|
|
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",
|