@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 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
- Claude Code · Codex 스킬 디렉터리를 직접 읽는다 → 심볼릭 링크만 걸면
79
- ~/.claude/skills/<이름>/SKILL.md
80
-
81
- Gemini · Cursor · 외 스킬 개념이 없다 → 공용 위치에 두고
82
- ~/.ai-skills/qualisoft/ 지시문 파일에 목록을 적는다
83
- + GEMINI.md / AGENTS.md
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
- 설치는 **심볼릭 링크**입니다. 저장소의 `skills/`를 고치면 모든 도구에 즉시 반영됩니다.
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: join(HOME, ".ai-skills", "qualisoft"),
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: join(HOME, ".ai-skills", "qualisoft"),
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: join(HOME, ".ai-skills", "qualisoft"),
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
- return realpathSync(path) === realpathSync(src);
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
- return false;
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
- if (!useCopy && isOurs(dest, src)) return "이미 연결됨";
110
- // 남이 놓은 실디렉터리는 지우지 않는다. 사용자의 수정이 들어 있을 수 있다.
111
- if (st.isDirectory() && !st.isSymbolicLink() && !force) {
112
- return "건너뜀 기존 디렉터리가 있습니다 (--force 덮어씀)";
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
- cpSync(src, dest, { recursive: true });
118
- return "복사";
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
- symlinkSync(src, dest, "dir");
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
- console.log("\n" + t.label + " → " + t.dir + (t.verified ? "" : " (경로 미확인)"));
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
- mkdirSync(t.dir, { recursive: true });
179
- for (const s of skills) {
180
- const how = place(join(SKILLS_DIR, s.name), join(t.dir, s.name), useCopy, force);
181
- if (how.startsWith("건너뜀")) skipped.push(join(t.dir, s.name));
182
- console.log(" · " + s.name.padEnd(20) + how);
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, t.dir);
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
- if (!lstatSync(dest, { throwIfNoEntry: false })) return "-".padEnd(8);
246
- // link = 이 패키지가 놓은 것, other = 다른 내용이 이미 있음
247
- return (isOurs(dest, join(SKILLS_DIR, s.name)) ? "link" : "other").padEnd(8);
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
- const found = TARGETS.filter((t) =>
286
- t.mode === "native"
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.0.0",
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": {