@qualisoft/ai-skills 1.0.0 → 1.1.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/CHANGELOG.md +74 -0
- package/README.md +38 -9
- package/bin/cli.mjs +171 -38
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# 변경 이력
|
|
2
|
+
|
|
3
|
+
## 1.1.0 — 2026-09-11
|
|
4
|
+
|
|
5
|
+
설치 위치와 소유 판정을 고쳤습니다. **1.0.0 사용자는 전원 재설치가 필요합니다.**
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @qualisoft/ai-skills@latest install --force
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
> 직접 고친 스킬이 있으면 `--force` 가 그 디렉터리를 지웁니다. 먼저 백업하세요.
|
|
12
|
+
|
|
13
|
+
### 고친 것
|
|
14
|
+
|
|
15
|
+
**① npx 캐시를 직접 가리키던 문제** — 모든 플랫폼 해당
|
|
16
|
+
|
|
17
|
+
`npx @qualisoft/ai-skills install` 이 만드는 링크가 npx 캐시(`_npx/<해시>/`)를
|
|
18
|
+
가리켰습니다. 그 캐시는 정리 대상이라, 비워지는 순간 스킬 전체가 죽은 링크가
|
|
19
|
+
됩니다. README 첫 줄 명령이 이 상태를 만들고 있었습니다.
|
|
20
|
+
|
|
21
|
+
이제 임시 위치(npx 캐시 · `node_modules`)에서 실행되면 **먼저
|
|
22
|
+
`~/.ai-skills/qualisoft/` 로 복사하고 도구들은 그곳을 가리킵니다.** 캐시가
|
|
23
|
+
정리돼도 스킬은 살아 있습니다.
|
|
24
|
+
|
|
25
|
+
저장소를 클론해 개발할 때는 `--direct` 로 저장소를 직접 가리킵니다 — 고친
|
|
26
|
+
내용이 즉시 반영되는 기존 동작 그대로입니다.
|
|
27
|
+
|
|
28
|
+
**② 복사 설치의 소유 판정** — Windows 해당
|
|
29
|
+
|
|
30
|
+
`isOurs()` 가 `realpath` 비교뿐이라 복사본은 항상 "남의 것"으로 판정됐습니다.
|
|
31
|
+
Windows 는 심볼릭 링크에 관리자 권한이 필요해 복사가 기본이므로, 이 때문에
|
|
32
|
+
|
|
33
|
+
- `uninstall` 이 자기 설치본조차 지우지 못하고 (전부 "건너뜀")
|
|
34
|
+
- 재설치·업그레이드마다 `--force` 가 필요하고
|
|
35
|
+
- `--force` 는 `rmSync` 하므로 직접 수정한 내용이 날아갔습니다
|
|
36
|
+
|
|
37
|
+
복사본에 `.qualisoft-ai-skills.json` 표식을 남겨 판정합니다. 이제 Windows 도
|
|
38
|
+
링크 환경과 동일하게 동작합니다.
|
|
39
|
+
|
|
40
|
+
### 곁들여 고친 것
|
|
41
|
+
|
|
42
|
+
- **죽은 링크도 소유로 인정합니다.** 그러지 않으면 캐시가 정리된 뒤
|
|
43
|
+
`uninstall` 이 자기 링크를 못 지웁니다. 1.0.0 설치본도 정리됩니다
|
|
44
|
+
- `symlink` 이 `EPERM`·`EACCES` 로 막히면 예외로 죽지 않고 **복사로
|
|
45
|
+
내려갑니다**
|
|
46
|
+
- `cursor` 자동 탐지가 `~/.cursor/rules` 를 보고 있었습니다. 그 폴더는 없을 수
|
|
47
|
+
있어 `~/.cursor` 를 봅니다
|
|
48
|
+
- `uninstall` 이 빈 정본 폴더를 남기지 않습니다
|
|
49
|
+
- `status` 가 `link` / `copy` / `other` 를 구분하고 설치된 버전을 함께
|
|
50
|
+
보여줍니다
|
|
51
|
+
|
|
52
|
+
### 검증
|
|
53
|
+
|
|
54
|
+
| 시나리오 | 결과 |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| npx 캐시에서 설치 → 캐시 전체 삭제 | 스킬 동작, `selftest` 통과 |
|
|
57
|
+
| Windows 조건(복사 + 캐시 삭제) | 동작, `uninstall` 5개 제거 |
|
|
58
|
+
| 죽은 링크 정리 | 제거 |
|
|
59
|
+
| `--direct` (개발 모드) | 저장소 직접 링크 |
|
|
60
|
+
| 링크 모드 회귀 | 연결 5 / 이미 연결 5 / 제거 5 / 잔여 0 |
|
|
61
|
+
| 사용자가 직접 만든 스킬 | 건너뜀, 내용 보존 |
|
|
62
|
+
| `--dry-run` | 아무것도 쓰지 않음 |
|
|
63
|
+
| 실제 tarball 설치 | 정본 경유 확인 |
|
|
64
|
+
|
|
65
|
+
**보고** — Windows `uninstall` 문제는 팀 내부 검토에서 제기되었습니다. 원인
|
|
66
|
+
분석(`realpath` 비교 → 복사본 판정 불가)과 해결 방향(표식 기반 판정)이 정확했고,
|
|
67
|
+
그 실마리를 따라가 ① 을 함께 찾았습니다.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 1.0.0 — 2026-09-11
|
|
72
|
+
|
|
73
|
+
최초 공개. 스킬 5종 (`project-interview` · `project-init` · `project-design` ·
|
|
74
|
+
`project-build` · `erd-visual`), 도구별 설치기, 외부 의존성 없음.
|
package/README.md
CHANGED
|
@@ -63,11 +63,24 @@ npx @qualisoft/ai-skills list # 스킬 설명 전문
|
|
|
63
63
|
npx @qualisoft/ai-skills install --dry-run # 바뀔 것만 미리 본다
|
|
64
64
|
npx @qualisoft/ai-skills install --copy # 링크 대신 복사 (Windows 기본)
|
|
65
65
|
npx @qualisoft/ai-skills install --force # 같은 이름 디렉터리를 덮어쓴다
|
|
66
|
+
npx @qualisoft/ai-skills install --direct # 저장소를 직접 가리킨다 (개발용)
|
|
66
67
|
npx @qualisoft/ai-skills uninstall
|
|
67
68
|
```
|
|
68
69
|
|
|
69
70
|
**같은 이름의 스킬이 이미 있으면 건너뜁니다.** 직접 고쳐 쓰던 스킬을 지우지 않기 위해서입니다. 바꿀 생각이면 백업하고 `--force`를 붙이세요.
|
|
70
71
|
|
|
72
|
+
### Windows
|
|
73
|
+
|
|
74
|
+
심볼릭 링크에 관리자 권한이 필요해 **복사로 자동 전환**됩니다(`--copy`가 기본). 복사본에는 `.qualisoft-ai-skills.json` 표식이 들어가고, 이걸로 소유를 판정합니다. 그래서 링크 환경과 똑같이 동작합니다 — 재설치는 `--force` 없이 갱신되고, `uninstall`도 정상 제거됩니다.
|
|
75
|
+
|
|
76
|
+
> **1.0.0으로 설치하신 분은 다시 설치해 주세요.** 그 버전은 npx 캐시를 직접 가리켜서, 캐시가 정리되면 스킬이 죽은 링크로 남습니다. 표식도 없어 `uninstall` 이 자기 설치본을 알아보지 못합니다.
|
|
77
|
+
>
|
|
78
|
+
> ```bash
|
|
79
|
+
> npx @qualisoft/ai-skills@latest install --force
|
|
80
|
+
> ```
|
|
81
|
+
>
|
|
82
|
+
> 직접 고친 내용이 있으면 `--force` 가 그 디렉터리를 지우므로 먼저 백업하세요.
|
|
83
|
+
|
|
71
84
|
---
|
|
72
85
|
|
|
73
86
|
## 어떻게 세 도구에서 다 되나
|
|
@@ -75,14 +88,19 @@ npx @qualisoft/ai-skills uninstall
|
|
|
75
88
|
스킬의 실체는 **마크다운 지시문 + Node 스크립트**입니다. 도구마다 다른 것은 실행 방식이 아니라 **스킬을 발견하는 방식**뿐입니다. 그래서 설치기가 하는 일도 그 하나입니다.
|
|
76
89
|
|
|
77
90
|
```
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
① 정본을 안정된 곳에 둔다
|
|
92
|
+
~/.ai-skills/qualisoft/<이름>/
|
|
93
|
+
npx 캐시·node_modules 는 정리 대상이라 그 자리를 직접 가리키면
|
|
94
|
+
캐시가 비워질 때 스킬이 통째로 죽는다. 그래서 먼저 여기로 복사한다.
|
|
95
|
+
|
|
96
|
+
② 도구가 보는 곳에서 정본을 가리킨다
|
|
97
|
+
Claude Code · Codex ~/.claude/skills/<이름> → 링크 (Windows 는 복사)
|
|
98
|
+
Gemini · Cursor · 그 외 정본 경로를 지시문에 적는다
|
|
99
|
+
GEMINI.md / .cursor/rules / AGENTS.md
|
|
84
100
|
```
|
|
85
101
|
|
|
102
|
+
저장소를 클론해 개발할 때는 `--direct` 를 붙입니다. 복사를 건너뛰고 저장소를 직접 가리켜서, 고친 내용이 곧바로 반영됩니다.
|
|
103
|
+
|
|
86
104
|
주입되는 블록은 표식으로 감싸여 있어(`<!-- qualisoft-ai-skills:begin -->`) **기존 지시문을 건드리지 않고** 그 부분만 갈아끼웁니다. `uninstall`은 블록만 빼고 나머지는 남깁니다.
|
|
87
105
|
|
|
88
106
|
> **경로 정확도** — `claude` · `codex` · `agents`는 실제로 확인한 경로입니다. `gemini` · `cursor`는 각 도구 문서를 근거로 넣었고 버전에 따라 다를 수 있습니다. 어긋나면 `bin/cli.mjs` 상단 `TARGETS` 한 곳만 고치면 됩니다.
|
|
@@ -193,12 +211,12 @@ Claude Code · Codex 에서는 `/project-init` 처럼 직접 호출할 수도
|
|
|
193
211
|
|
|
194
212
|
## 직접 고쳐 쓰기
|
|
195
213
|
|
|
196
|
-
|
|
214
|
+
`--direct` 로 설치하면 도구들이 저장소의 `skills/` 를 직접 가리킵니다. 고치면 즉시 반영됩니다.
|
|
197
215
|
|
|
198
216
|
```bash
|
|
199
217
|
git clone https://github.com/qualisoft-service/project-ai-skills
|
|
200
218
|
cd project-ai-skills
|
|
201
|
-
node bin/cli.mjs install --tool=all --force
|
|
219
|
+
node bin/cli.mjs install --tool=all --direct --force
|
|
202
220
|
|
|
203
221
|
# skills/project-init/modules/*.json 을 고치면 곧바로 적용된다
|
|
204
222
|
node skills/project-init/build.mjs selftest
|
|
@@ -219,7 +237,18 @@ node skills/project-init/build.mjs selftest
|
|
|
219
237
|
## 요구 사항
|
|
220
238
|
|
|
221
239
|
- **Node.js 18 이상** (`node -v`로 확인)
|
|
222
|
-
- macOS · Linux · Windows. Windows는 링크 대신 복사로 자동
|
|
240
|
+
- macOS · Linux · Windows. Windows는 링크 대신 복사로 자동 전환되며, 기능 차이는 없습니다
|
|
241
|
+
|
|
242
|
+
## 변경 이력
|
|
243
|
+
|
|
244
|
+
버전별 변경 내용은 [CHANGELOG.md](./CHANGELOG.md) 에 있습니다.
|
|
245
|
+
|
|
246
|
+
**1.0.0 을 쓰고 계시면 재설치가 필요합니다** — 그 버전은 npx 캐시를 직접
|
|
247
|
+
가리켜서, 캐시가 정리되면 스킬이 죽은 링크로 남습니다.
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
npx @qualisoft/ai-skills@latest install --force
|
|
251
|
+
```
|
|
223
252
|
|
|
224
253
|
## 저장소 · 문의
|
|
225
254
|
|
package/bin/cli.mjs
CHANGED
|
@@ -8,19 +8,42 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import {
|
|
10
10
|
existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync,
|
|
11
|
-
lstatSync, rmSync, symlinkSync, cpSync, realpathSync,
|
|
11
|
+
lstatSync, rmSync, symlinkSync, cpSync, realpathSync, readlinkSync,
|
|
12
12
|
} from "node:fs";
|
|
13
13
|
import { homedir, platform } from "node:os";
|
|
14
|
-
import { join, dirname, resolve } from "node:path";
|
|
14
|
+
import { basename, join, dirname, resolve } from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
|
|
17
17
|
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
18
18
|
const SKILLS_DIR = join(PKG_ROOT, "skills");
|
|
19
19
|
const HOME = homedir();
|
|
20
|
+
const VERSION = (() => {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8")).version;
|
|
23
|
+
} catch {
|
|
24
|
+
return "unknown";
|
|
25
|
+
}
|
|
26
|
+
})();
|
|
27
|
+
|
|
28
|
+
/* 스킬의 안정된 보관 위치.
|
|
29
|
+
*
|
|
30
|
+
* npx 는 패키지를 캐시(`_npx/<해시>/`)에 풀고, 그 캐시는 정리 대상이다.
|
|
31
|
+
* 거기를 직접 링크하면 캐시가 비워지는 순간 모든 스킬이 죽은 링크가 된다.
|
|
32
|
+
* 그래서 임시 위치에서 실행됐을 때는 먼저 여기로 복사해 두고, 도구들은
|
|
33
|
+
* 이 경로를 가리키게 한다. */
|
|
34
|
+
const STORE = join(HOME, ".ai-skills", "qualisoft");
|
|
35
|
+
|
|
36
|
+
/** 패키지가 사라질 수 있는 위치에 있는가 (npx 캐시 · node_modules). */
|
|
37
|
+
function isTransient(dir) {
|
|
38
|
+
return /[/\\](?:node_modules|_npx)[/\\]/.test(dir + "/");
|
|
39
|
+
}
|
|
20
40
|
|
|
21
41
|
/* ── 설치 대상 ──────────────────────────────────────────────────────
|
|
22
42
|
* mode: "native" — 도구가 스킬 디렉터리를 직접 읽는다. 링크만 걸면 끝.
|
|
23
|
-
* mode: "inject" — 스킬 개념이 없는 도구.
|
|
43
|
+
* mode: "inject" — 스킬 개념이 없는 도구. 정본 위치를 지시문에 적는다.
|
|
44
|
+
*
|
|
45
|
+
* dir: null 인 대상은 스킬을 따로 놓지 않고 정본(SOURCE)을 그대로 가리킨다.
|
|
46
|
+
* detect 는 자동 탐지에 쓰는 "이 도구가 설치되어 있나" 경로다.
|
|
24
47
|
*
|
|
25
48
|
* 경로가 바뀌면 여기만 고친다. verified 는 실제로 확인한 경로인지를 뜻한다. */
|
|
26
49
|
const TARGETS = [
|
|
@@ -29,6 +52,7 @@ const TARGETS = [
|
|
|
29
52
|
label: "Claude Code",
|
|
30
53
|
mode: "native",
|
|
31
54
|
dir: join(HOME, ".claude", "skills"),
|
|
55
|
+
detect: join(HOME, ".claude"),
|
|
32
56
|
verified: true,
|
|
33
57
|
},
|
|
34
58
|
{
|
|
@@ -36,30 +60,34 @@ const TARGETS = [
|
|
|
36
60
|
label: "Codex CLI",
|
|
37
61
|
mode: "native",
|
|
38
62
|
dir: join(HOME, ".codex", "skills"),
|
|
63
|
+
detect: join(HOME, ".codex"),
|
|
39
64
|
verified: true,
|
|
40
65
|
},
|
|
41
66
|
{
|
|
42
67
|
id: "gemini",
|
|
43
68
|
label: "Gemini CLI",
|
|
44
69
|
mode: "inject",
|
|
45
|
-
dir:
|
|
70
|
+
dir: null,
|
|
46
71
|
instructions: join(HOME, ".gemini", "GEMINI.md"),
|
|
72
|
+
detect: join(HOME, ".gemini"),
|
|
47
73
|
verified: false,
|
|
48
74
|
},
|
|
49
75
|
{
|
|
50
76
|
id: "cursor",
|
|
51
77
|
label: "Cursor",
|
|
52
78
|
mode: "inject",
|
|
53
|
-
dir:
|
|
79
|
+
dir: null,
|
|
54
80
|
instructions: join(HOME, ".cursor", "rules", "qualisoft-ai-skills.mdc"),
|
|
81
|
+
detect: join(HOME, ".cursor"),
|
|
55
82
|
verified: false,
|
|
56
83
|
},
|
|
57
84
|
{
|
|
58
85
|
id: "agents",
|
|
59
86
|
label: "AGENTS.md (범용)",
|
|
60
87
|
mode: "inject",
|
|
61
|
-
dir:
|
|
88
|
+
dir: null,
|
|
62
89
|
instructions: resolve("AGENTS.md"),
|
|
90
|
+
detect: null,
|
|
63
91
|
verified: true,
|
|
64
92
|
},
|
|
65
93
|
];
|
|
@@ -94,31 +122,101 @@ function allSkills() {
|
|
|
94
122
|
|
|
95
123
|
/* ── 설치 ─────────────────────────────────────────────────────── */
|
|
96
124
|
|
|
97
|
-
|
|
125
|
+
/* 복사로 설치한 스킬에 남기는 표식.
|
|
126
|
+
*
|
|
127
|
+
* 링크 설치는 realpath 비교로 소유를 알 수 있지만 복사본은 알 수 없다.
|
|
128
|
+
* Windows 는 심볼릭 링크에 권한이 필요해 복사가 기본이므로, 표식이 없으면
|
|
129
|
+
* uninstall 이 자기가 놓은 것조차 지우지 못한다. */
|
|
130
|
+
const STAMP = ".qualisoft-ai-skills.json";
|
|
131
|
+
|
|
132
|
+
function writeStamp(dest, skillName, version) {
|
|
133
|
+
writeFileSync(
|
|
134
|
+
join(dest, STAMP),
|
|
135
|
+
JSON.stringify({ package: "@qualisoft/ai-skills", version, skill: skillName,
|
|
136
|
+
mode: "copy", installedAt: new Date().toISOString() }, null, 2) + "\n",
|
|
137
|
+
"utf8",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readStamp(dest) {
|
|
142
|
+
try {
|
|
143
|
+
const j = JSON.parse(readFileSync(join(dest, STAMP), "utf8"));
|
|
144
|
+
return j.package === "@qualisoft/ai-skills" ? j : null;
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 이 경로가 우리가 놓은 것인가. 남의 디렉터리를 지우지 않기 위한 확인.
|
|
152
|
+
*
|
|
153
|
+
* 두 설치 방식을 모두 본다 — 링크는 realpath 로, 복사는 표식 파일로.
|
|
154
|
+
* 둘 중 하나라도 맞으면 우리 것이다.
|
|
155
|
+
*/
|
|
98
156
|
function isOurs(path, src) {
|
|
99
157
|
try {
|
|
100
|
-
|
|
158
|
+
if (realpathSync(path) === realpathSync(src)) return true;
|
|
159
|
+
} catch {
|
|
160
|
+
/* 링크가 깨졌거나 경로가 없다 — 아래에서 다시 본다 */
|
|
161
|
+
}
|
|
162
|
+
if (readStamp(path)) return true;
|
|
163
|
+
// 죽은 링크: 대상이 사라져 realpath 가 실패한다. 가리키던 경로로 판정한다.
|
|
164
|
+
// 이걸 놓치면 npx 캐시가 정리된 뒤 uninstall 이 자기 링크를 못 지운다.
|
|
165
|
+
try {
|
|
166
|
+
if (lstatSync(path).isSymbolicLink()) {
|
|
167
|
+
const t = readlinkSync(path);
|
|
168
|
+
return t.includes("@qualisoft/ai-skills") || t.startsWith(STORE) ||
|
|
169
|
+
t.startsWith(SKILLS_DIR);
|
|
170
|
+
}
|
|
101
171
|
} catch {
|
|
102
|
-
|
|
172
|
+
/* 판정 불가 */
|
|
103
173
|
}
|
|
174
|
+
return false;
|
|
104
175
|
}
|
|
105
176
|
|
|
106
|
-
function place(src, dest, useCopy, force) {
|
|
177
|
+
function place(src, dest, useCopy, force, version) {
|
|
107
178
|
const st = lstatSync(dest, { throwIfNoEntry: false });
|
|
108
179
|
if (st) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
180
|
+
const ours = isOurs(dest, src);
|
|
181
|
+
if (ours && !useCopy && st.isSymbolicLink()) return "이미 연결됨";
|
|
182
|
+
// 우리가 놓은 것은 힘 주지 않아도 갱신한다. 그래야 업그레이드가 된다.
|
|
183
|
+
if (!ours && st.isDirectory() && !st.isSymbolicLink() && !force) {
|
|
184
|
+
return "건너뜀 — 우리 표식이 없는 디렉터리입니다 (--force 로 덮어씀)";
|
|
113
185
|
}
|
|
114
186
|
rmSync(dest, { recursive: true, force: true });
|
|
115
187
|
}
|
|
116
|
-
if (useCopy) {
|
|
117
|
-
|
|
118
|
-
|
|
188
|
+
if (!useCopy) {
|
|
189
|
+
try {
|
|
190
|
+
symlinkSync(src, dest, "dir");
|
|
191
|
+
return "연결";
|
|
192
|
+
} catch (e) {
|
|
193
|
+
// Windows 외에도 권한·파일시스템 때문에 링크가 막힐 수 있다. 복사로 내려간다.
|
|
194
|
+
if (!["EPERM", "EACCES", "ENOSYS", "EEXIST"].includes(e.code)) throw e;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
cpSync(src, dest, { recursive: true });
|
|
198
|
+
writeStamp(dest, basename(dest), version);
|
|
199
|
+
return useCopy ? "복사" : "복사 (링크 불가)";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 도구들이 가리킬 정본 위치를 정한다.
|
|
204
|
+
*
|
|
205
|
+
* npx 캐시나 node_modules 에서 실행됐으면 그 경로는 사라질 수 있으므로
|
|
206
|
+
* STORE 로 복사해 두고 그곳을 정본으로 쓴다. 저장소를 직접 클론해
|
|
207
|
+
* 쓰는 경우(개발)에는 고친 내용이 바로 반영되도록 패키지를 그대로 쓴다.
|
|
208
|
+
*/
|
|
209
|
+
function resolveSource(skills, { direct, dryRun }) {
|
|
210
|
+
if (direct || !isTransient(PKG_ROOT)) return { dir: SKILLS_DIR, staged: false };
|
|
211
|
+
if (dryRun) return { dir: STORE, staged: true };
|
|
212
|
+
mkdirSync(STORE, { recursive: true });
|
|
213
|
+
for (const s of skills) {
|
|
214
|
+
const dest = join(STORE, s.name);
|
|
215
|
+
rmSync(dest, { recursive: true, force: true });
|
|
216
|
+
cpSync(join(SKILLS_DIR, s.name), dest, { recursive: true });
|
|
217
|
+
writeStamp(dest, s.name, VERSION);
|
|
119
218
|
}
|
|
120
|
-
|
|
121
|
-
return "연결";
|
|
219
|
+
return { dir: STORE, staged: true };
|
|
122
220
|
}
|
|
123
221
|
|
|
124
222
|
/** 지시문 파일의 표식 사이 블록만 갈아끼운다. 나머지 내용은 건드리지 않는다. */
|
|
@@ -161,7 +259,7 @@ function injectInstructions(file, skills, baseDir) {
|
|
|
161
259
|
writeFileSync(file, text, "utf8");
|
|
162
260
|
}
|
|
163
261
|
|
|
164
|
-
function install(targets, { useCopy, dryRun, force }) {
|
|
262
|
+
function install(targets, { useCopy, dryRun, force, direct }) {
|
|
165
263
|
const skills = allSkills();
|
|
166
264
|
const skipped = [];
|
|
167
265
|
if (!skills.length) {
|
|
@@ -169,20 +267,34 @@ function install(targets, { useCopy, dryRun, force }) {
|
|
|
169
267
|
process.exit(1);
|
|
170
268
|
}
|
|
171
269
|
|
|
270
|
+
const source = resolveSource(skills, { direct, dryRun });
|
|
271
|
+
if (source.staged) {
|
|
272
|
+
console.log("정본 보관: " + source.dir + " (npx·node_modules 는 사라질 수 있어 복사해 둡니다)");
|
|
273
|
+
} else {
|
|
274
|
+
console.log("정본 위치: " + source.dir + " (수정하면 즉시 반영됩니다)");
|
|
275
|
+
}
|
|
276
|
+
|
|
172
277
|
for (const t of targets) {
|
|
173
|
-
|
|
278
|
+
// dir 이 없는 대상은 정본을 그대로 가리킨다 — 같은 것을 두 번 두지 않는다.
|
|
279
|
+
const dir = t.dir || source.dir;
|
|
280
|
+
console.log("\n" + t.label + " → " + dir + (t.verified ? "" : " (경로 미확인)"));
|
|
174
281
|
if (dryRun) {
|
|
175
282
|
skills.forEach((s) => console.log(" · " + s.name + " (건너뜀 — dry-run)"));
|
|
283
|
+
if (t.mode === "inject") console.log(" 지시문: " + t.instructions);
|
|
176
284
|
continue;
|
|
177
285
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
286
|
+
if (t.dir) {
|
|
287
|
+
mkdirSync(dir, { recursive: true });
|
|
288
|
+
for (const s of skills) {
|
|
289
|
+
const how = place(join(source.dir, s.name), join(dir, s.name), useCopy, force, VERSION);
|
|
290
|
+
if (how.startsWith("건너뜀")) skipped.push(join(dir, s.name));
|
|
291
|
+
console.log(" · " + s.name.padEnd(20) + how);
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
console.log(" · 정본을 그대로 사용합니다 (" + skills.length + "개)");
|
|
183
295
|
}
|
|
184
296
|
if (t.mode === "inject") {
|
|
185
|
-
injectInstructions(t.instructions, skills,
|
|
297
|
+
injectInstructions(t.instructions, skills, dir);
|
|
186
298
|
console.log(" 지시문: " + t.instructions);
|
|
187
299
|
}
|
|
188
300
|
}
|
|
@@ -216,14 +328,14 @@ function uninstall(targets) {
|
|
|
216
328
|
for (const t of targets) {
|
|
217
329
|
console.log("\n" + t.label);
|
|
218
330
|
for (const s of skills) {
|
|
219
|
-
const dest = join(t.dir, s.name);
|
|
331
|
+
const dest = join(t.dir || STORE, s.name);
|
|
220
332
|
if (!lstatSync(dest, { throwIfNoEntry: false })) continue;
|
|
221
333
|
// 우리가 놓은 것만 지운다. 사용자가 직접 만든 디렉터리는 남긴다.
|
|
222
334
|
if (isOurs(dest, join(SKILLS_DIR, s.name))) {
|
|
223
335
|
rmSync(dest, { recursive: true, force: true });
|
|
224
336
|
console.log(" · " + s.name.padEnd(20) + "제거");
|
|
225
337
|
} else {
|
|
226
|
-
console.log(" · " + s.name.padEnd(20) + "건너뜀 —
|
|
338
|
+
console.log(" · " + s.name.padEnd(20) + "건너뜀 — 우리 표식이 없습니다 (직접 만든 스킬이거나 1.0.0 설치본)");
|
|
227
339
|
}
|
|
228
340
|
}
|
|
229
341
|
// 없는 스킬을 가리키는 지시문을 남기면 에이전트가 빈 경로를 읽으려 한다
|
|
@@ -231,6 +343,15 @@ function uninstall(targets) {
|
|
|
231
343
|
console.log(" 지시문 블록 제거: " + t.instructions);
|
|
232
344
|
}
|
|
233
345
|
}
|
|
346
|
+
// 아무 도구도 STORE 를 안 쓰면 빈 껍데기를 남기지 않는다
|
|
347
|
+
try {
|
|
348
|
+
if (existsSync(STORE) && readdirSync(STORE).length === 0) {
|
|
349
|
+
rmSync(STORE, { recursive: true, force: true });
|
|
350
|
+
console.log("\n정본 보관 폴더 제거: " + STORE);
|
|
351
|
+
}
|
|
352
|
+
} catch {
|
|
353
|
+
/* 지우지 못해도 문제는 아니다 */
|
|
354
|
+
}
|
|
234
355
|
}
|
|
235
356
|
|
|
236
357
|
function status() {
|
|
@@ -241,13 +362,26 @@ function status() {
|
|
|
241
362
|
console.log("스킬".padEnd(w) + " " + TARGETS.map((t) => t.id.padEnd(8)).join(""));
|
|
242
363
|
for (const s of skills) {
|
|
243
364
|
const cells = TARGETS.map((t) => {
|
|
244
|
-
const dest = join(t.dir, s.name);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
365
|
+
const dest = join(t.dir || STORE, s.name);
|
|
366
|
+
const st = lstatSync(dest, { throwIfNoEntry: false });
|
|
367
|
+
if (!st) return "-".padEnd(8);
|
|
368
|
+
// link/copy = 이 패키지가 놓은 것, other = 우리 표식이 없는 내용
|
|
369
|
+
if (!isOurs(dest, join(SKILLS_DIR, s.name))) return "other".padEnd(8);
|
|
370
|
+
return (st.isSymbolicLink() ? "link" : "copy").padEnd(8);
|
|
248
371
|
});
|
|
249
372
|
console.log(s.name.padEnd(w) + " " + cells.join(""));
|
|
250
373
|
}
|
|
374
|
+
console.log("\nlink = 링크 설치 · copy = 복사 설치 · other = 우리 표식이 없는 내용 · - = 없음");
|
|
375
|
+
const versions = new Set();
|
|
376
|
+
for (const t of TARGETS) {
|
|
377
|
+
for (const s of skills) {
|
|
378
|
+
const v = readStamp(join(t.dir || STORE, s.name))?.version;
|
|
379
|
+
if (v) versions.add(v);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (versions.size) {
|
|
383
|
+
console.log("복사 설치된 버전: " + [...versions].sort().join(", ") + " (패키지: " + VERSION + ")");
|
|
384
|
+
}
|
|
251
385
|
}
|
|
252
386
|
|
|
253
387
|
function list() {
|
|
@@ -282,11 +416,8 @@ function pickTargets() {
|
|
|
282
416
|
return picked;
|
|
283
417
|
}
|
|
284
418
|
// 지정이 없으면 설정 디렉터리가 실제로 있는 도구만 고른다
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
? existsSync(dirname(t.dir))
|
|
288
|
-
: t.id !== "agents" && existsSync(dirname(t.instructions)),
|
|
289
|
-
);
|
|
419
|
+
// detect 가 있는 대상만 자동 탐지한다. agents 는 명시 지정 전용이다.
|
|
420
|
+
const found = TARGETS.filter((t) => t.detect && existsSync(t.detect));
|
|
290
421
|
if (!found.length) {
|
|
291
422
|
console.error("설치된 AI 도구를 찾지 못했습니다. --tool=<id> 로 직접 지정하세요.");
|
|
292
423
|
console.error("가능한 값: " + TARGETS.map((t) => t.id).join(", ") + ", all");
|
|
@@ -301,6 +432,7 @@ switch (cmd) {
|
|
|
301
432
|
useCopy: flag("copy") || platform() === "win32",
|
|
302
433
|
dryRun: flag("dry-run"),
|
|
303
434
|
force: flag("force"),
|
|
435
|
+
direct: flag("direct"),
|
|
304
436
|
});
|
|
305
437
|
break;
|
|
306
438
|
case "uninstall":
|
|
@@ -322,6 +454,7 @@ switch (cmd) {
|
|
|
322
454
|
npx @qualisoft/ai-skills install --copy 링크 대신 복사 (Windows 기본)
|
|
323
455
|
npx @qualisoft/ai-skills install --dry-run 무엇이 바뀔지만 본다
|
|
324
456
|
npx @qualisoft/ai-skills install --force 기존 같은 이름 디렉터리를 덮어씀
|
|
457
|
+
npx @qualisoft/ai-skills install --direct 저장소를 직접 가리킨다 (개발용)
|
|
325
458
|
npx @qualisoft/ai-skills status 어디에 무엇이 설치됐는지
|
|
326
459
|
npx @qualisoft/ai-skills list 스킬 목록과 설명
|
|
327
460
|
npx @qualisoft/ai-skills uninstall
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qualisoft/ai-skills",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "기획 인터뷰부터 문서 체계·디자인 시안·구현까지 이어지는 AI 에이전트 스킬 5종. Claude Code · Codex · Gemini 등 어느 도구에서나 쓴다. 외부 의존성 없음.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-skills",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"bin/",
|
|
40
40
|
"skills/",
|
|
41
41
|
"README.md",
|
|
42
|
+
"CHANGELOG.md",
|
|
42
43
|
"LICENSE"
|
|
43
44
|
],
|
|
44
45
|
"engines": {
|