reborn-jobsearch 1.0.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 ADDED
@@ -0,0 +1,89 @@
1
+ # reborn-jobsearch
2
+
3
+ **ai-job-search 를 받기 전에, 이 컴퓨터가 되는지 재 줍니다.**
4
+
5
+ ```
6
+ npx --yes reborn-jobsearch --check # 재기만 한다 (안 깐다)
7
+ npx --yes reborn-jobsearch # 안내 스킬을 깐다
8
+ ```
9
+
10
+ > `--yes` 를 빼지 마세요. 클로드 코드 안에서 실행하면 설치 여부를 묻는 자리에서 조용히 멈춥니다.
11
+
12
+ ---
13
+
14
+ ## 무엇에 관한 것인가
15
+
16
+ [`MadsLorentzen/ai-job-search`](https://github.com/MadsLorentzen/ai-job-search) 는
17
+ 클로드 코드 위에 얹은 **AI 구직 지원 프레임워크**입니다. MIT · 별 40.6k · 포크 13.8k
18
+ (2026-09-04 확인). 명령 셋으로 돕니다.
19
+
20
+ | 명령 | 하는 일 |
21
+ |---|---|
22
+ | `/setup` | 문답과 문서 분석으로 내 프로필을 채운다 |
23
+ | `/scrape` | 채용 포털을 훑어 적합도 순으로 보여 준다 |
24
+ | `/apply` | 평가 → 맞춤 이력서·자기소개서 초안 → 리뷰어 검수 → 수정 → PDF |
25
+
26
+ **우리가 만든 저장소가 아닙니다.** Anthropic 과도 무관한 독립 오픈소스이고,
27
+ 저장소 스스로 그렇게 밝혀 두었습니다. 이 패키지는 그 저장소를 **재배포하지 않습니다** —
28
+ 코드를 담지도, 대신 클론하지도 않습니다.
29
+
30
+ ## 그럼 이 패키지는 뭘 하나
31
+
32
+ **받기 전에 막히는 자리를 먼저 말해 줍니다.**
33
+
34
+ `git clone` 은 1분이면 끝납니다. 그다음이 안 넘어갑니다 —
35
+ 결과물인 **이력서는 `lualatex`, 자기소개서는 `xelatex`** 로 굽고,
36
+ 포털 CLI 는 **Bun**, 연봉 도구는 **파이썬 3.10+** 입니다.
37
+
38
+ `--check` 는 말로 「준비물이 있습니다」 하지 않고,
39
+ **지금 이 컴퓨터에서 다섯 개를 직접 실행해 판까지 읽습니다.**
40
+
41
+ ```
42
+ Claude Code 있음 — 2.1.260 (Claude Code)
43
+ Bun 없음 — winget install Oven-sh.Bun
44
+ Python 3.10+ 있음 — Python 3.12.10
45
+ LaTeX (lualatex) 없음 — 윈도우 MiKTeX · 맥 MacTeX · 리눅스 texlive-full
46
+ LaTeX (xelatex) 없음 — 위와 같은 배포판에 함께 들어 있다
47
+
48
+ 2/5 준비돼 있습니다.
49
+ ```
50
+
51
+ 못 재면 **「못 쟀다」고 합니다.** 「있다」고 하지 않습니다.
52
+
53
+ ## 저장소가 말하지 않는 것 넷
54
+
55
+ 1. **받는 게 아니라 굽는 게 관문이다** — LaTeX 배포판이 몇 기가입니다
56
+ 2. **채용 사이트가 덴마크 것이다** — 한국에서 쓰려면 `job_scraper/` 를 갈아 끼워야 합니다
57
+ (README 도 country-agnostic 이라고 적어 뒀지만, 갈아 끼우는 일은 남아 있습니다)
58
+ 3. **★fork 를 공개로 두면 내 이력서가 공개된다** — GitHub 에서 fork 는 기본이 공개입니다.
59
+ 비공개로 쓰려면 fork 가 아니라 내려받아 새 비공개 저장소로 올리세요
60
+ 4. **쓰는 만큼 돈이 든다** — 저장소는 무료지만 모델 호출은 아닙니다
61
+
62
+ ## 명령
63
+
64
+ | | |
65
+ |---|---|
66
+ | `npx --yes reborn-jobsearch` | 안내 스킬을 홈(`~/.claude/skills/`)에 깐다 |
67
+ | `npx --yes reborn-jobsearch --check` | 안 깔고 이 컴퓨터만 잰다 |
68
+ | `npx --yes reborn-jobsearch --local` | 현재 폴더 `.claude/skills/` 에 깐다 |
69
+ | `npx --yes reborn-jobsearch --dry-run` | 무엇을 할지 보여 주기만 한다 |
70
+ | `npx --yes reborn-jobsearch --self-test` | 자기시험 (네트워크 안 씀) |
71
+
72
+ ## 하지 않는 일
73
+
74
+ - 저장소를 대신 깔아 주지 않습니다
75
+ - 취업을 보장하지 않습니다
76
+ - 한국 채용 사이트를 붙여 주지 않습니다
77
+ - LaTeX 없이 PDF 를 뽑아 주지 않습니다
78
+
79
+ ## 이런 안내가 더 필요하시면
80
+
81
+ 같은 방식으로 48종 골라 담아 두었습니다 — 리본랩스 스킬팩(8,900원).
82
+
83
+ https://rebornlabs.kr/skillpack?utm_source=installer&utm_medium=cli&utm_campaign=jobsearch
84
+
85
+ 목록과 직접 까는 명령까지 그 페이지에 그대로 적어 두었습니다. 안 사셔도 됩니다.
86
+
87
+ ---
88
+
89
+ MIT · REBORN LABS · 문의 <https://mobility.rebornlabs.kr/cs>
package/cli.mjs ADDED
@@ -0,0 +1,331 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ai-job-search 안내 설치기 — **깔기 전에 이 컴퓨터가 되는지 재 주고**, 안내 스킬을 깐다.
4
+ *
5
+ * npx --yes reborn-jobsearch 안내 스킬을 깐다 (이게 전부다)
6
+ * npx --yes reborn-jobsearch --check ★깔지 않고 **이 컴퓨터를 실제로 잰다**
7
+ * npx --yes reborn-jobsearch --dry-run 무엇을 할지 보여주기만 한다
8
+ * npx --yes reborn-jobsearch --local 홈이 아니라 현재 폴더에 깐다
9
+ * npx --yes reborn-jobsearch --self-test 자기시험 (네트워크 안 씀)
10
+ *
11
+ * ★★우리가 만든 저장소가 아니다.
12
+ * ai-job-search 는 MadsLorentzen 의 MIT 오픈소스이고, Anthropic 과도 무관한 독립 프로젝트다.
13
+ * 우리는 **재배포하지 않는다** — 코드를 담지도, 대신 클론하지도 않는다. 안내만 한다.
14
+ *
15
+ * ★★--check 가 이 설치기의 존재 이유다
16
+ * 저장소는 「fork 하고 프로필만 채우면 된다」로 읽힌다. 그런데 결과물인 이력서·자기소개서는
17
+ * **LaTeX 로 굽는다**(이력서 lualatex · 자기소개서 xelatex). 포털 CLI 는 **Bun** 으로 돌고,
18
+ * 연봉 도구는 **파이썬 3.10+** 다. 받는 것이 아니라 **굽는 것**이 관문이다.
19
+ * 그래서 말로 「준비물이 있습니다」 하지 않고, **지금 이 컴퓨터에서 다섯 개를 직접 재 준다.**
20
+ * 못 재면 「못 쟀다」고 한다 — 「있다」고 하지 않는다.
21
+ */
22
+ import { existsSync, statSync, readdirSync, readFileSync, mkdirSync, cpSync } from "node:fs";
23
+ import { homedir, platform } from "node:os";
24
+ import { dirname, join } from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+ import { execFileSync } from "node:child_process";
27
+ import { TAIL, 링크, 마지막 } from "./tail.mjs";
28
+
29
+ const HOME = homedir();
30
+ const 여기 = dirname(fileURLToPath(import.meta.url));
31
+ const 인자 = process.argv.slice(2);
32
+ const 확인만 = 인자.includes("--check");
33
+ const 미리보기 = 인자.includes("--dry-run");
34
+ const 여기에 = 인자.includes("--local");
35
+ const 자기시험모드 = 인자.includes("--self-test");
36
+
37
+ const c = { g: "\x1b[32m", r: "\x1b[31m", y: "\x1b[33m", d: "\x1b[90m", cy: "\x1b[36m", b: "\x1b[1m", 0: "\x1b[0m" };
38
+ const 굵게 = (t) => String(t).replace(/\*\*(.+?)\*\*/g, `${c[0]}${c.b}$1${c[0]}${c.d}`);
39
+ const 콤마 = (n) => Number(n).toLocaleString("ko-KR");
40
+
41
+ const 퍼널 = "jobsearch";
42
+ const 스킬이름 = "ai-job-search-guide";
43
+ const 원본 = join(여기, "skill");
44
+ const 대상뿌리 = 여기에 ? join(process.cwd(), ".claude", "skills") : join(HOME, ".claude", "skills");
45
+ const 대상 = join(대상뿌리, 스킬이름);
46
+
47
+ /* ══ 원장 ═══════════════════════════════════════════════════════ */
48
+
49
+ function 원장읽기() {
50
+ const p = join(원본, "jobsearch.json");
51
+ if (!existsSync(p)) return { 됨: false, 이유: `원장이 없다 — ${p}` };
52
+ try { return { 됨: true, d: JSON.parse(readFileSync(p, "utf8")) }; }
53
+ catch (e) { return { 됨: false, 이유: `원장을 못 읽었다 — ${e.message}` }; }
54
+ }
55
+
56
+ /** ★조건 없이 단정하는 말 [[kmong-no-unbounded-guarantee]] */
57
+ const 금칙 = [/완전\s*무료/, /무제한/, /평생/, /100\s*%\s*보장/, /누구나\s*바로/, /무조건/,
58
+ /반드시\s*합격/, /취업\s*보장/, /전액\s*환불/];
59
+ const 금칙면제 = /(하지\s*않는다|말하지\s*마라|쓰지\s*않는다|아니다|틀리다|금칙|안\s*된다|하지\s*마라|말하지\s*않는다|은\s*아니다|되지\s*않는다|마라|않는다)/;
60
+ /** ★검사기가 자기 인용을 막지 않게 낫표 안은 벗기고 잰다 [[forbidden-word-checker-blocks-its-own-quotes]] */
61
+ const 인용벗기기 = (s) => String(s).replace(/「[^」]*」/g, "○");
62
+
63
+ function 금칙검사(텍스트, 어디) {
64
+ const 문제 = [];
65
+ for (const 줄 of String(텍스트).split("\n")) {
66
+ const 벗긴 = 인용벗기기(줄);
67
+ if (금칙면제.test(벗긴)) continue;
68
+ for (const re of 금칙) if (re.test(벗긴)) 문제.push(`${어디}: 조건 없이 단정하는 말 — ${줄.trim().slice(0, 60)}`);
69
+ }
70
+ return 문제;
71
+ }
72
+
73
+ function 원장검사(d) {
74
+ const 문제 = [];
75
+ if (!d?.저장소?.주소) 문제.push("원장에 저장소 주소가 없다");
76
+ if (!/^https:\/\/github\.com\//.test(String(d?.저장소?.주소 || ""))) 문제.push("저장소 주소가 GitHub 이 아니다");
77
+ if (d?.저장소?.라이선스 !== "MIT") 문제.push("라이선스 칸이 MIT 가 아니다 — 확인 없이 안내하지 않는다");
78
+ if (!String(d?.저장소?.["★면책"] || "").includes("우리가 만든 것이 아니다"))
79
+ 문제.push("★면책이 사라졌다 — 남의 저장소를 크게 세우면 귀속을 반드시 박는다");
80
+ const 관문 = Array.isArray(d?.관문) ? d.관문 : [];
81
+ if (관문.length !== Number(d?.실측?.필요도구수))
82
+ 문제.push(`관문 ${관문.length}개인데 원장은 필요도구수 ${d?.실측?.필요도구수} 라고 한다 — 둘이 어긋난다`);
83
+ for (const g of 관문) {
84
+ if (!g.재는법) 문제.push(`관문 ${g.키} 에 재는법이 없다 — 못 재면 있다고 말하게 된다`);
85
+ if (!g.없으면) 문제.push(`관문 ${g.키} 에 「없으면」이 없다 — 막힌 사람을 그냥 두는 것이다`);
86
+ if (!g.출처) 문제.push(`관문 ${g.키} 에 출처가 없다 — 지어낸 준비물과 구분이 안 된다`);
87
+ }
88
+ if (!Array.isArray(d?.["★막히는자리"]) || d["★막히는자리"].length < 3)
89
+ 문제.push("★막히는자리가 셋보다 적다 — 이 킷이 파는 것이 그것이다");
90
+ for (const m of d?.["★막히는자리"] || []) if (!m.근거) 문제.push(`막히는자리 「${m.제목}」 에 근거가 없다`);
91
+ 문제.push(...금칙검사(JSON.stringify(d, null, 1), "원장"));
92
+ return 문제;
93
+ }
94
+
95
+ /* ══ 이 컴퓨터를 잰다 ═══════════════════════════════════════════ */
96
+
97
+ function 실행파일있나(cmd) {
98
+ const 찾기 = platform() === "win32" ? "where" : "which";
99
+ try { execFileSync(찾기, [cmd], { stdio: "pipe", timeout: 4000 }); return true; }
100
+ catch { return false; }
101
+ }
102
+
103
+ /** 판 문자열을 실제로 받아 온다. 못 받으면 null — 「있다」고 단정하지 않는다. */
104
+ function 판재기(cmd, 인자들) {
105
+ try {
106
+ const out = execFileSync(cmd, 인자들, { stdio: "pipe", timeout: 12000, encoding: "utf8" });
107
+ const 첫줄 = String(out).split("\n").find((l) => l.trim()) || "";
108
+ return 첫줄.trim().slice(0, 80);
109
+ } catch { return null; }
110
+ }
111
+
112
+ function 판수(s) {
113
+ const m = String(s || "").match(/(\d+)\.(\d+)(?:\.(\d+))?/);
114
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3] || 0)] : null;
115
+ }
116
+
117
+ function 하한충족(판, 하한) {
118
+ const a = 판수(판); const b = 판수(하한);
119
+ if (!a || !b) return null;
120
+ if (a[0] !== b[0]) return a[0] > b[0];
121
+ if (a[1] !== b[1]) return a[1] > b[1];
122
+ return a[2] >= b[2];
123
+ }
124
+
125
+ /** 관문 다섯을 **직접 실행해서** 잰다. */
126
+ function 관문재기(D) {
127
+ const 결과 = [];
128
+ for (const g of D.관문) {
129
+ const 후보 = g.키 === "python" ? ["python", "python3", "py"] : [g.키];
130
+ let 찾음 = null; let 판 = null;
131
+ for (const cmd of 후보) {
132
+ if (!실행파일있나(cmd)) continue;
133
+ 찾음 = cmd;
134
+ 판 = 판재기(cmd, ["--version"]);
135
+ if (판) break;
136
+ }
137
+ const 행 = { 키: g.키, 이름: g.이름, 있나: Boolean(찾음), 명령: 찾음, 판, 없으면: g.없으면, 왜: g.왜 };
138
+ if (찾음 && g.하한) {
139
+ 행.하한 = g.하한;
140
+ 행.하한충족 = 하한충족(판, g.하한);
141
+ }
142
+ 결과.push(행);
143
+ }
144
+ return 결과;
145
+ }
146
+
147
+ function 이미깔렸나() {
148
+ const 자리 = [];
149
+ for (const 뿌리 of [join(HOME, ".claude", "skills"), join(process.cwd(), ".claude", "skills")]) {
150
+ const p = join(뿌리, 스킬이름);
151
+ if (existsSync(p) && statSync(p).isDirectory()) 자리.push({ 어디: p, 개수: readdirSync(p).length });
152
+ }
153
+ return 자리;
154
+ }
155
+
156
+ /* ══ 보고 ═══════════════════════════════════════════════════════ */
157
+
158
+ function 환경보고(D, 잰것) {
159
+ const 됨 = 잰것.filter((x) => x.있나 && x.하한충족 !== false).length;
160
+ console.log(`\n${c.b}이 컴퓨터를 지금 재 봤습니다${c[0]} ${c.d}(ai-job-search 를 굽는 데 필요한 다섯 가지)${c[0]}\n`);
161
+
162
+ for (const x of 잰것) {
163
+ let 표; let 꼬리말 = "";
164
+ if (!x.있나) { 표 = `${c.r}없음${c[0]}`; 꼬리말 = `${c.d} — ${x.없으면}${c[0]}`; }
165
+ else if (x.하한충족 === false) { 표 = `${c.y}낮음${c[0]}`; 꼬리말 = `${c.d} — ${x.판} (${x.하한} 이상이 필요합니다)${c[0]}`; }
166
+ else if (!x.판) { 표 = `${c.y}못 쟀음${c[0]}`; 꼬리말 = `${c.d} — 실행은 되는데 판을 못 읽었습니다${c[0]}`; }
167
+ else { 표 = `${c.g}있음${c[0]}`; 꼬리말 = `${c.d} — ${x.판}${c[0]}`; }
168
+ console.log(` ${String(x.이름).padEnd(18)} ${표}${꼬리말}`);
169
+ }
170
+
171
+ console.log(`\n ${c.b}${됨}/${잰것.length}${c[0]} ${c.d}준비돼 있습니다.${c[0]}`);
172
+ if (됨 < 잰것.length) {
173
+ console.log(` ${c.d}★없는 것을 깔기 전에는 저장소를 받아도 ${c[0]}${c.b}결과물이 안 나옵니다${c[0]}${c.d} — 굽는 자리에서 멈춥니다.${c[0]}`);
174
+ }
175
+
176
+ console.log(`\n${c.b}저장소가 말하지 않는 것${c[0]}`);
177
+ for (const m of D["★막히는자리"]) {
178
+ console.log(` ${c.b}· ${m.제목}${c[0]}`);
179
+ console.log(` ${c.d}${굵게(m.무엇)}${c[0]}`);
180
+ console.log(` ${c.d}근거 — ${m.근거}${c[0]}`);
181
+ }
182
+
183
+ const R = D.저장소;
184
+ console.log(`\n${c.b}저장소${c[0]} ${c.d}${R.주인}/${R.이름} · ${R.라이선스} · 별 ${콤마(R.별)} · 포크 ${콤마(R.포크)} (${R.확인일} 확인)${c[0]}`);
185
+ console.log(` ${c.cy}${R.주소}${c[0]}`);
186
+ console.log(` ${c.d}${R["★면책"]}${c[0]}\n`);
187
+ }
188
+
189
+ function 꼬리(깔았나) {
190
+ if (!깔았나) return;
191
+ const t = TAIL[퍼널];
192
+ console.log();
193
+ for (const 줄 of t.이음) console.log(`${c.d}${굵게(줄)}${c[0]}`);
194
+ console.log(`\n ${c.b}${t.제목}${c[0]} ${c.d}— ${t.한줄}${c[0]}`);
195
+ console.log(` ${c.cy}${링크(퍼널)}${c[0]}`);
196
+ console.log(`${c.d}${마지막}${c[0]}\n`);
197
+ }
198
+
199
+ /* ══ 심기 ═══════════════════════════════════════════════════════ */
200
+
201
+ function 심기() {
202
+ mkdirSync(대상뿌리, { recursive: true });
203
+ cpSync(원본, 대상, { recursive: true });
204
+ for (const f of ["SKILL.md", "jobsearch.json"]) {
205
+ const p = join(대상, f);
206
+ if (!existsSync(p)) throw new Error(`심었는데 ${f} 이 없다 — ${p}`);
207
+ if (readFileSync(p, "utf8").length < 200) throw new Error(`${f} 이 너무 짧다 — 복사가 끊긴 것 같다`);
208
+ }
209
+ const 원장 = JSON.parse(readFileSync(join(대상, "jobsearch.json"), "utf8"));
210
+ if (!원장?.저장소?.주소) throw new Error("심은 원장에 저장소 주소가 없다 — 복사가 깨졌다");
211
+ if (!Array.isArray(원장?.관문) || 원장.관문.length < 5) throw new Error("심은 원장에 관문이 모자란다 — 복사가 깨졌다");
212
+ return 대상;
213
+ }
214
+
215
+ /* ══ 자기시험 — ★양방향 ═══════════════════════════════════════ */
216
+
217
+ function 자기시험() {
218
+ const 나쁨 = [];
219
+ const r = 원장읽기();
220
+ if (!r.됨) { console.log(`${c.r}✗ ${r.이유}${c[0]}`); process.exit(1); }
221
+ const D = r.d;
222
+
223
+ const 문제 = 원장검사(D);
224
+ if (문제.length) 나쁨.push(...문제.map((m) => `오탐 또는 진짜 문제: ${m}`));
225
+
226
+ const 흉내 = () => JSON.parse(JSON.stringify(D));
227
+
228
+ /* 미탐 ① 귀속을 지우면 잡아야 한다 — 이 킷의 경계선이다 */
229
+ { const x = 흉내(); x.저장소["★면책"] = "우리 도구입니다";
230
+ if (!원장검사(x).length) 나쁨.push("미탐: ★면책을 지웠는데 안 잡힌다"); }
231
+
232
+ /* 미탐 ② 관문에서 재는법을 빼면 잡아야 한다 */
233
+ { const x = 흉내(); delete x.관문[0].재는법;
234
+ if (!원장검사(x).length) 나쁨.push("미탐: 관문의 재는법을 지웠는데 안 잡힌다"); }
235
+
236
+ /* 미탐 ③ 관문 개수와 필요도구수가 어긋나면 잡아야 한다 */
237
+ { const x = 흉내(); x.관문.pop();
238
+ if (!원장검사(x).length) 나쁨.push("미탐: 관문 하나를 빼도 개수 어긋남이 안 잡힌다"); }
239
+
240
+ /* 미탐 ④ 막히는자리의 근거를 빼면 잡아야 한다 */
241
+ { const x = 흉내(); delete x["★막히는자리"][0].근거;
242
+ if (!원장검사(x).length) 나쁨.push("미탐: 막히는자리의 근거를 지웠는데 안 잡힌다"); }
243
+
244
+ /* 미탐 ⑤ 조건 없이 단정하는 말이 들어가면 잡아야 한다 */
245
+ { const x = 흉내(); x["★막히는자리"][0].무엇 = "이 스킬만 깔면 취업 보장";
246
+ if (!원장검사(x).length) 나쁨.push("미탐: 취업 보장 문구가 안 잡힌다"); }
247
+
248
+ /* 미탐 ⑥ 라이선스를 바꾸면 잡아야 한다 */
249
+ { const x = 흉내(); x.저장소.라이선스 = "unknown";
250
+ if (!원장검사(x).length) 나쁨.push("미탐: 라이선스가 MIT 가 아닌데 안 잡힌다"); }
251
+
252
+ /* 오탐 ① 원장 그대로는 통과해야 한다 — 위에서 이미 쟀다 */
253
+
254
+ /* 오탐 ② 「취업을 보장한다」를 **하지 않는 말 목록**으로 적은 것은 잡으면 안 된다 */
255
+ { const x = 흉내(); x.안하는말.push("취업 보장 같은 말은 하지 않는다");
256
+ if (원장검사(x).length > 문제.length) 나쁨.push("오탐: 금칙어를 부정문으로 적었는데 잡힌다"); }
257
+
258
+ /* 관문 재기가 실제로 도는지 — 결과가 다섯 줄이어야 한다 */
259
+ const 잰것 = 관문재기(D);
260
+ if (잰것.length !== D.관문.length) 나쁨.push(`관문재기가 ${잰것.length}줄을 냈다 — ${D.관문.length}줄이어야 한다`);
261
+ for (const x of 잰것) if (x.있나 && x.판 === null && x.키 !== "python")
262
+ console.log(`${c.d} (참고) ${x.이름} 은 있는데 판을 못 읽었다${c[0]}`);
263
+
264
+ /* 심는 원본이 실제로 있나 */
265
+ for (const f of ["SKILL.md", "jobsearch.json"]) {
266
+ const p = join(원본, f);
267
+ if (!existsSync(p)) 나쁨.push(`심을 원본에 ${f} 이 없다`);
268
+ }
269
+
270
+ if (나쁨.length) {
271
+ console.log(`\n${c.r}✗ 자기시험 ${나쁨.length}건${c[0]}`);
272
+ for (const m of 나쁨) console.log(` ${c.r}· ${m}${c[0]}`);
273
+ process.exit(1);
274
+ }
275
+ console.log(`\n${c.g}✓ 자기시험 통과${c[0]} ${c.d}— 원장 검사 양방향 · 관문 ${잰것.length}줄 실측 · 심을 원본 확인${c[0]}\n`);
276
+ }
277
+
278
+ /* ══ 본체 ═══════════════════════════════════════════════════════ */
279
+
280
+ async function 본체() {
281
+ if (자기시험모드) return 자기시험();
282
+
283
+ const r = 원장읽기();
284
+ if (!r.됨) { console.log(`${c.r}✗ ${r.이유}${c[0]}`); process.exit(1); }
285
+ const D = r.d;
286
+
287
+ const 문제 = 원장검사(D);
288
+ if (문제.length) {
289
+ console.log(`${c.r}✗ 원장이 규격을 벗어났다 — 안내를 내보내지 않는다${c[0]}`);
290
+ for (const m of 문제) console.log(` ${c.r}· ${m}${c[0]}`);
291
+ process.exit(1);
292
+ }
293
+
294
+ const 잰것 = 관문재기(D);
295
+ 환경보고(D, 잰것);
296
+
297
+ if (확인만) {
298
+ console.log(`${c.d}재기만 했습니다. 안내 스킬을 깔려면 ${c[0]}${c.b}npx --yes reborn-jobsearch${c[0]}${c.d} 를 그대로 실행하세요.${c[0]}\n`);
299
+ return;
300
+ }
301
+
302
+ const 이미 = 이미깔렸나();
303
+ if (이미.length) {
304
+ console.log(`${c.y}이미 깔려 있습니다${c[0]} ${c.d}— ${이미.map((x) => x.어디).join(" · ")}${c[0]}`);
305
+ console.log(`${c.d}덮어씁니다.${c[0]}`);
306
+ }
307
+
308
+ if (미리보기) {
309
+ console.log(`${c.d}--dry-run — 실제로는 안 깝니다.${c[0]}`);
310
+ console.log(` 깔 자리 ${대상}`);
311
+ console.log(` 깔 파일 SKILL.md · jobsearch.json`);
312
+ console.log(` ${c.d}저장소 코드는 담지 않습니다. 안내 스킬만 깝니다.${c[0]}\n`);
313
+ return;
314
+ }
315
+
316
+ let 깔았나 = false;
317
+ try {
318
+ const 자리 = 심기();
319
+ 깔았나 = true;
320
+ console.log(`${c.g}✓ 안내 스킬을 깔았습니다${c[0]} ${c.d}— ${자리}${c[0]}`);
321
+ console.log(`${c.d}클로드 코드를 다시 켜면 ${c[0]}${c.b}${스킬이름}${c[0]}${c.d} 로 잡힙니다.${c[0]}`);
322
+ console.log(`${c.d}「구직 저장소 깔아 볼까」 같은 말에도 붙습니다.${c[0]}`);
323
+ } catch (e) {
324
+ console.log(`${c.r}✗ 못 깔았습니다 — ${e.message}${c[0]}`);
325
+ process.exitCode = 1;
326
+ }
327
+
328
+ 꼬리(깔았나);
329
+ }
330
+
331
+ 본체().catch((e) => { console.log(`${c.r}✗ ${e?.message || e}${c[0]}`); process.exit(1); });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "reborn-jobsearch",
3
+ "version": "1.0.0",
4
+ "description": "ai-job-search(MadsLorentzen · MIT · 별 40.6k) — 클로드 코드 위에 얹은 AI 구직 지원 프레임워크를 받기 전에 무엇이 막는지 알려 주는 안내 스킬입니다. --check 가 이 컴퓨터에서 다섯 가지(Claude Code · Bun · Python 3.10+ · lualatex · xelatex)를 실제로 재 줍니다. 결과물인 이력서·자기소개서는 LaTeX 로 굽기 때문에, 막히는 자리는 받는 것이 아니라 굽는 것입니다. 저장소 코드는 담지 않습니다.",
5
+ "keywords": [
6
+ "claude",
7
+ "claude-code",
8
+ "job-search",
9
+ "job-application",
10
+ "resume",
11
+ "cv",
12
+ "cover-letter",
13
+ "ai-agents",
14
+ "latex",
15
+ "korean"
16
+ ],
17
+ "homepage": "https://rebornlabs.kr/jobsearch?utm_source=installer&utm_medium=cli&utm_campaign=jobsearch",
18
+ "bugs": {
19
+ "url": "https://mobility.rebornlabs.kr/cs"
20
+ },
21
+ "license": "MIT",
22
+ "author": "REBORN LABS",
23
+ "type": "module",
24
+ "bin": {
25
+ "reborn-jobsearch": "cli.mjs"
26
+ },
27
+ "files": [
28
+ "cli.mjs",
29
+ "tail.mjs",
30
+ "skill/",
31
+ "README.md",
32
+ "안내스킬.md"
33
+ ],
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "scripts": {
38
+ "test": "node cli.mjs --self-test",
39
+ "check": "node cli.mjs --check"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ }
44
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: ai-job-search-guide
3
+ description: ai-job-search — 클로드 코드 위에 얹은 AI 구직 지원 프레임워크(MadsLorentzen · MIT · 별 40.6k)를 **받기 전에** 무엇이 막는지 알려 준다. 「fork 하고 프로필만 채우면 된다」로 읽히지만 결과물인 이력서·자기소개서는 LaTeX 로 굽고(이력서 lualatex · 자기소개서 xelatex), 포털 CLI 는 Bun 으로 돌고, 연봉 도구는 파이썬 3.10+ 다. 기본으로 붙은 채용 사이트는 덴마크 것이라 한국에서 쓰려면 갈아 끼워야 한다. 그리고 fork 를 공개로 두면 내 이력서가 그대로 공개된다. "ai-job-search", "AI 구직", "이력서 자동", "자기소개서 AI", "채용공고 스크래핑", "클로드 코드로 취업", "구직 자동화", "CV 자동 생성", "job search 저장소", "이 저장소 내 컴퓨터에서 돌아갈까" 같은 이야기가 나올 때 쓴다.
4
+ ---
5
+
6
+ # ai-job-search — 받기 전에 알아야 하는 것
7
+
8
+ ## 0. 먼저 — 이건 우리 것이 아니다
9
+
10
+ **`MadsLorentzen/ai-job-search`** · MIT · 별 40.6k · 포크 13.8k (2026-09-04 확인)
11
+ <https://github.com/MadsLorentzen/ai-job-search>
12
+
13
+ 독립 오픈소스이며 **Anthropic 과 무관하다** — 저장소 스스로 그렇게 밝혀 두었다.
14
+ 이 스킬은 그 저장소를 **재배포하지 않는다.** 코드를 담지도, 대신 클론하지도 않는다.
15
+ 하는 일은 하나다 — **받기 전에 무엇이 막는지 먼저 말해 주는 것.**
16
+
17
+ ## 1. 이 저장소가 실제로 하는 일
18
+
19
+ `/setup` → `/scrape` → `/apply` 세 명령이 전부다.
20
+
21
+ | 명령 | 하는 일 |
22
+ |---|---|
23
+ | `/setup` | 문서 분석·이력서 가져오기·문답으로 내 프로필 파일을 채운다 |
24
+ | `/scrape` | 여러 채용 포털을 훑어 중복을 걷고 **적합도 순**으로 보여 준다 |
25
+ | `/apply` | 적합도 평가 → 맞춤 이력서·자기소개서 초안 → **리뷰어 에이전트 검수** → 수정 → PDF 컴파일 → ATS 텍스트층 확인 |
26
+
27
+ ★핵심은 세 번째다. 초안을 쓰는 데서 끝나지 않고 **다른 에이전트가 그 초안을 깐다.**
28
+ 드래프터-리뷰어 구조가 이 저장소의 설계이고, 그래서 결과물이 한 번에 나오지 않는다.
29
+
30
+ ## 2. ★진짜 관문은 받는 게 아니라 굽는 것이다
31
+
32
+ 가장 많이 틀리는 지점이다. `git clone` 은 1분이면 끝난다. 그다음이 안 넘어간다.
33
+
34
+ | 필요한 것 | 왜 | 없으면 어디서 멈추나 |
35
+ |---|---|---|
36
+ | **Claude Code** | 슬래시 명령으로 돈다 | `/setup` 부터 안 열린다 |
37
+ | **Bun** | 포털 CLI 가 TypeScript 다 | `/scrape` 가 안 돈다 |
38
+ | **Python 3.10+** | 연봉 조회 도구 | 연봉 벤치마킹만 빠진다 |
39
+ | **lualatex** | ★**이력서 PDF** 를 굽는다 | 평가·초안까지 되고 **PDF 가 안 나온다** |
40
+ | **xelatex** | ★**자기소개서** 를 굽는다 | 이력서는 되는데 자기소개서만 안 나온다 |
41
+
42
+ ★`lualatex` 와 `xelatex` 는 **다른 엔진**이다. 둘 다 있어야 한다.
43
+ 윈도우면 **MiKTeX**, 맥이면 **MacTeX**, 리눅스면 `texlive-full` 이다. 몇 기가짜리 설치다.
44
+
45
+ 지금 이 컴퓨터가 되는지 재려면:
46
+
47
+ ```
48
+ npx --yes reborn-jobsearch --check
49
+ ```
50
+
51
+ 다섯 개를 실제로 실행해 판까지 읽고, 없는 것에는 깔 명령을 붙여 준다.
52
+ **「있습니다」라고 단정하지 않는다** — 못 재면 「못 쟀다」고 한다.
53
+
54
+ ## 3. ★채용 사이트가 덴마크 것이다
55
+
56
+ 기본으로 붙어 있는 포털은 **Jobindex · Jobnet · Akademikernes Jobbank** — 전부 덴마크다.
57
+
58
+ README 스스로 이렇게 적어 두었다: *language- and country-agnostic … the pattern is designed
59
+ to be swapped for your local job boards.* 즉 **갈아 끼우게 설계돼 있다.**
60
+ 그렇지만 **갈아 끼우는 일 자체는 남아 있다.** `job_scraper/` 를 손봐야 한다.
61
+
62
+ 한국에서 쓸 때 사실만 말하면 이렇다 — **한국 채용 사이트는 붙어 있지 않다.**
63
+ 붙이려면 스크레이퍼를 새로 쓰거나, `/apply <공고 URL>` 만 쓰고 `/scrape` 는 건너뛴다.
64
+ ★공고 하나 주소를 직접 넣는 길이 있으므로, 포털 연결 없이도 나머지 전부를 쓸 수 있다.
65
+ 처음 써 볼 때는 이 길이 훨씬 짧다.
66
+
67
+ ★채용 사이트를 긁을 때는 그 사이트의 이용약관과 `robots.txt` 를 먼저 본다.
68
+ 막아 둔 곳을 자동으로 긁으면 계정이 잠긴다.
69
+
70
+ ## 4. ★★fork 를 공개로 두면 내 이력서가 공개된다
71
+
72
+ 이 저장소의 방식은 「Fork it and own it」이고, 채워 넣는 것은 **이름·연락처·경력·주소**다.
73
+ 그런데 **GitHub 에서 fork 는 기본이 공개**다. 프로필을 채우고 그대로 push 하면
74
+ 그 파일이 검색되는 자리에 놓인다.
75
+
76
+ 비공개로 쓰려면 **fork 하지 말고** 이렇게 한다:
77
+
78
+ ```
79
+ git clone https://github.com/MadsLorentzen/ai-job-search.git
80
+ cd ai-job-search
81
+ rm -rf .git && git init # 남의 이력을 안 들고 온다
82
+ # GitHub 에서 private 저장소를 새로 만들고 그걸 origin 으로 건다
83
+ ```
84
+
85
+ ★이미 fork 해서 개인정보를 올렸다면, 저장소를 지워도 **fork 네트워크에 남을 수 있다.**
86
+ 지우기 전에 GitHub 지원에 캐시 삭제를 요청해야 하는 경우가 있다.
87
+
88
+ ## 5. 값이 드는 자리
89
+
90
+ - 클로드 코드는 **Anthropic API 키 또는 Claude Pro/Team 구독** 위에서 돈다.
91
+ - 공고 하나에 **평가 → 초안 → 검수 → 수정**이 붙는다. 호출이 한 번으로 안 끝난다.
92
+ - 공고를 여러 개 돌리면 그만큼 곱해진다. 처음에는 **한 건만** 돌려 보고 값을 재라.
93
+
94
+ ★저장소 자체는 무료다(MIT). 드는 값은 **모델을 부르는 값**이다. 둘은 다르다.
95
+
96
+ ## 6. 처음 쓸 때 권하는 순서
97
+
98
+ 1. `npx --yes reborn-jobsearch --check` — 다섯 개 중 뭐가 없는지 먼저 본다
99
+ 2. 없는 것을 깐다. **LaTeX 부터** 깐다(제일 오래 걸린다)
100
+ 3. fork 대신 **비공개로 clone** 한다 (§4)
101
+ 4. `/setup` 으로 프로필을 채운다
102
+ 5. `/scrape` 를 건너뛰고 **공고 주소 하나로 `/apply`** 부터 해 본다
103
+ 6. 나온 이력서 PDF 를 **직접 읽어 본다** — 지어낸 경력이 없는지 확인한다
104
+
105
+ ★★여섯 번째가 제일 중요하다. 모델이 쓴 이력서에는 **없던 성과가 붙을 수 있다.**
106
+ 제출하는 것은 나이고, 책임도 나에게 온다. 초안이지 완성본이 아니다.
107
+
108
+ ## 7. 이 스킬이 하지 않는 일
109
+
110
+ - 저장소를 대신 깔아 주지 않는다 (재배포하지 않는다)
111
+ - 취업을 보장하지 않는다
112
+ - 한국 채용 사이트를 붙여 주지 않는다
113
+ - LaTeX 없이 PDF 를 뽑아 주지 않는다
114
+
115
+ 수치와 인용은 전부 `jobsearch.json` 에 출처와 함께 들어 있다.
116
+ 값이 바뀌면 그 파일을 고치고, 이 문서를 고치지 않는다.
@@ -0,0 +1,105 @@
1
+ {
2
+ "_doc": "ai-job-search 안내 스킬 원장. 화면·대본·설치기·착지에 나가는 수치는 전부 여기서 온다. 지어내지 않는다.",
3
+ "_출처": "https://github.com/MadsLorentzen/ai-job-search (저장소 첫 화면 + README + SETUP.md) — 2026-09-04 확인",
4
+
5
+ "저장소": {
6
+ "이름": "ai-job-search",
7
+ "주인": "MadsLorentzen",
8
+ "주소": "https://github.com/MadsLorentzen/ai-job-search",
9
+ "라이선스": "MIT",
10
+ "별": 40600,
11
+ "별표기": "40.6k",
12
+ "포크": 13800,
13
+ "포크표기": "13.8k",
14
+ "한줄": "The job search that runs on your machine.",
15
+ "About": "AI job application framework built on Claude Code: evaluate postings, tailor CVs, write cover letters, prep interviews. Fork it and own it.",
16
+ "확인일": "2026-09-04",
17
+ "★면책": "우리가 만든 것이 아니다. Anthropic 과도 무관한 독립 오픈소스이며, 저장소 스스로 그렇게 밝히고 있다. 우리는 이 저장소를 재배포하지 않는다 — 안내만 한다."
18
+ },
19
+
20
+ "실측": {
21
+ "명령수": 3,
22
+ "명령": [
23
+ { "이름": "/setup", "하는일": "문서 분석·이력서 가져오기·문답으로 내 프로필 파일을 채운다" },
24
+ { "이름": "/scrape", "하는일": "여러 채용 포털을 훑어 중복을 걷고 적합도 순으로 보여 준다" },
25
+ { "이름": "/apply", "하는일": "적합도 평가 → 맞춤 이력서·자기소개서 초안 → 리뷰어 검수 → 수정 → PDF 컴파일 → ATS 텍스트층 확인" }
26
+ ],
27
+ "최상위폴더수": 13,
28
+ "최상위폴더": [".agents/skills", ".claude", ".github", "assets/mascot", "company_research", "cover_letters", "cv", "documents", "job_scraper", "templates", "tests", "tools", "upskill"],
29
+ "필요도구수": 5
30
+ },
31
+
32
+ "관문": [
33
+ {
34
+ "키": "claude",
35
+ "이름": "Claude Code",
36
+ "왜": "이 저장소는 클로드 코드의 슬래시 명령으로 돈다. 없으면 /setup 부터 안 열린다",
37
+ "재는법": "claude --version",
38
+ "없으면": "npm install -g @anthropic-ai/claude-code",
39
+ "출처": "SETUP.md — Anthropic API 키 또는 Claude Pro/Team 구독이 함께 필요하다"
40
+ },
41
+ {
42
+ "키": "bun",
43
+ "이름": "Bun",
44
+ "왜": "채용 포털을 훑는 CLI 가 TypeScript 라 Bun 으로 돈다. 없으면 /scrape 가 안 돈다",
45
+ "재는법": "bun --version",
46
+ "없으면": "winget install Oven-sh.Bun (맥·리눅스: curl -fsSL https://bun.sh/install | bash)",
47
+ "출처": "SETUP.md — The job portal CLIs are written in TypeScript and run with Bun"
48
+ },
49
+ {
50
+ "키": "python",
51
+ "이름": "Python 3.10+",
52
+ "왜": "연봉 조회 도구가 파이썬이다",
53
+ "재는법": "python --version / python3 --version",
54
+ "없으면": "https://www.python.org/downloads 에서 3.10 이상",
55
+ "출처": "SETUP.md — Python 3.10+ is required for the salary lookup tool",
56
+ "하한": "3.10"
57
+ },
58
+ {
59
+ "키": "lualatex",
60
+ "이름": "LaTeX (lualatex)",
61
+ "왜": "★이력서 PDF 를 lualatex 로 굽는다. 이게 이 저장소의 진짜 관문이다",
62
+ "재는법": "lualatex --version",
63
+ "없으면": "윈도우 MiKTeX · 맥 MacTeX · 리눅스 texlive-full",
64
+ "출처": "SETUP.md — CV compilation uses lualatex"
65
+ },
66
+ {
67
+ "키": "xelatex",
68
+ "이름": "LaTeX (xelatex)",
69
+ "왜": "자기소개서는 xelatex 로 굽는다. lualatex 와 다른 엔진이라 둘 다 있어야 한다",
70
+ "재는법": "xelatex --version",
71
+ "없으면": "위와 같은 LaTeX 배포판에 함께 들어 있다",
72
+ "출처": "SETUP.md — Cover letter compilation uses xelatex"
73
+ }
74
+ ],
75
+
76
+ "★막히는자리": [
77
+ {
78
+ "제목": "받는 게 아니라 굽는 게 관문이다",
79
+ "무엇": "README 는 「fork 하고 프로필만 채우면 된다」로 읽힌다. 그런데 결과물인 이력서·자기소개서는 LaTeX 로 굽는다 — 이력서는 lualatex, 자기소개서는 xelatex. 윈도우면 MiKTeX 를 따로 깔아야 하고 그게 몇 기가다.",
80
+ "근거": "SETUP.md 의 LaTeX Distribution 항목"
81
+ },
82
+ {
83
+ "제목": "채용 사이트가 덴마크 것이다",
84
+ "무엇": "기본으로 붙어 있는 포털은 Jobindex · Jobnet · Akademikernes Jobbank 다. README 스스로 「language- and country-agnostic 이고 내 지역 잡보드로 갈아 끼우게 설계했다」고 적어 뒀지만, 갈아 끼우는 일은 남아 있다. 한국에서 쓰려면 job_scraper 를 손봐야 한다.",
85
+ "근거": "README 의 구조 문단 — The job portal search skills are built for the Danish market"
86
+ },
87
+ {
88
+ "제목": "★fork 를 공개로 두면 내 이력서가 공개된다",
89
+ "무엇": "이 저장소의 방식은 「fork 해서 내 것으로 만든다」이고, 채워 넣는 것은 이름·연락처·경력 같은 개인정보다. GitHub 에서 fork 는 기본이 공개다. 프로필을 채우고 그대로 push 하면 그게 검색된다. 비공개로 쓰려면 fork 가 아니라 내려받아 새 비공개 저장소로 올려야 한다.",
90
+ "근거": "저장소 방식(Fork it and own it) + GitHub fork 의 기본 공개 설정"
91
+ },
92
+ {
93
+ "제목": "쓰는 만큼 돈이 든다",
94
+ "무엇": "클로드 코드가 API 키 또는 Claude Pro/Team 구독 위에서 돈다. 공고 하나에 평가·초안·검수·수정이 붙으므로 호출이 한 번으로 끝나지 않는다.",
95
+ "근거": "SETUP.md — You will need an Anthropic API key or a Claude Pro/Team subscription"
96
+ }
97
+ ],
98
+
99
+ "안하는말": [
100
+ "이 스킬이 저장소를 대신 깔아 준다",
101
+ "취업을 보장한다",
102
+ "한국 채용 사이트가 이미 붙어 있다",
103
+ "LaTeX 없이도 PDF 가 나온다"
104
+ ]
105
+ }
package/tail.mjs ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 「끝난 뒤 한 칸」 문안 — 정본은 `skillpack/tail_copy.mjs` 다.
3
+ * 사본이 여기 있는 이유는 하나뿐이다: npm 패키지는 폴더 밖을 import 할 수 없다.
4
+ * 고칠 일이 생기면 정본을 먼저 고치고 여기로 옮긴다. 반대로 하지 마라.
5
+ *
6
+ * 규율: ① 끝난 뒤 딱 한 칸 · ② 못 깔았으면 안 띄운다 · ③ 파일을 안 남긴다
7
+ * ④ 고지 블록 금지 · ⑤ 첫 줄은 방금 한 일에서 이어져야 한다
8
+ * ★클로드킷·자비스 풀스택은 여기서 말하지 않는다 — 스킬팩 구매 이후다(2026-08-24 전략).
9
+ */
10
+ export const 랜딩 = "https://rebornlabs.kr/skillpack";
11
+ export const 링크 = (퍼널) => `${랜딩}?utm_source=installer&utm_medium=cli&utm_campaign=${퍼널}`;
12
+
13
+ export const TAIL = {
14
+ jobsearch: {
15
+ 이음: [
16
+ "방금 한 일은 **이 컴퓨터가 되는지 실제로 재 본 것**입니다 — 다섯 가지 중 무엇이 있고 무엇이 없는지.",
17
+ "저장소는 공개돼 있습니다. 막히는 자리는 **받는 것이 아니라 굽는 것**입니다.",
18
+ ],
19
+ 제목: "리본랩스 스킬팩",
20
+ 한줄: "같은 방식으로 골라 담은 48종을 한 줄로. 8,900원",
21
+ },
22
+ };
23
+
24
+ export const 마지막 = "목록과 직접 까는 명령까지 그 페이지에 그대로 적어 두었습니다. 안 사셔도 됩니다.";
@@ -0,0 +1,63 @@
1
+ # 안내스킬 — `ai-job-search-guide`
2
+
3
+ 이 패키지가 `~/.claude/skills/ai-job-search-guide/` 에 심는 것.
4
+
5
+ | 파일 | 무엇 |
6
+ |---|---|
7
+ | `SKILL.md` | 클로드가 읽는 안내. **답하는 순서**가 여기 있다 |
8
+ | `jobsearch.json` | **원장.** 모든 사실의 주인. 여기 없는 숫자·명령·준비물은 말하지 않는다 |
9
+
10
+ ★심는 것은 이 폴더 하나뿐이다. **공유 안내스킬에는 손대지 않는다.**
11
+
12
+ ## ★★★이 킷의 유일한 금기 — 남의 저장소를 재배포하지 않는다
13
+
14
+ 소개하는 것은 **남의 저작물**이다. MIT 라 퍼 나르는 것 자체는 허용되지만,
15
+ 퍼 나르는 순간 우리가 **그 코드의 배포자**가 된다 — 판이 바뀌어도 우리 사본은 안 바뀌고,
16
+ 문제가 생기면 우리 것으로 읽힌다.
17
+
18
+ 그래서 선을 하나 그었다:
19
+
20
+ | 한다 | 안 한다 |
21
+ |---|---|
22
+ | 저장소 **주소**를 들고 다닌다 | 저장소 **코드**를 패키지에 담는다 |
23
+ | 쓰는 사람 컴퓨터에서 **준비물을 잰다** | 대신 `git clone` 해 준다 |
24
+ | 필요한 줄을 **출처와 함께** 인용한다 | README 를 통째로 옮겨 붙인다 |
25
+
26
+ ★`cli.mjs --self-test` 가 원장의 **★면책**과 **관문의 출처**를 실제로 본다 —
27
+ 면책을 지우거나 관문에서 출처를 빼면 **exit 1** 이다.
28
+ 사람이 기억해야 하는 절차는 반드시 언젠가 빠진다.
29
+
30
+ ## 왜 원장을 따로 두나
31
+
32
+ 사실을 `SKILL.md` 본문에 적으면 두 곳이 서로 다른 말을 하기 시작한다.
33
+ 별 수·포크 수·명령 이름·준비물 다섯·확인일은 **한 곳에서만** 나온다 [[one-metric-one-owner]].
34
+
35
+ ★특히 **준비물 다섯**이 그렇다. 릴스 자막·착지 페이지·DM·`--check` 가
36
+ 전부 원장의 `관문[]` 을 읽어 간다. 손으로 옮겨 적으면 어느 한 곳이 조용히 어긋난다.
37
+
38
+ ## ★이 킷의 존재 이유는 두 문장이다
39
+
40
+ 저장소의 첫인상은 **「fork 하고 프로필만 채우면 된다」**다. 별이 40.6k 인 이유이기도 하다.
41
+ 그런데 결과물인 이력서·자기소개서는 **LaTeX 로 굽는다.**
42
+
43
+ **받는 건 1분이고, 굽는 자리에서 멈춘다.**
44
+ 그래서 이 킷은 대신 깔아 주지 않고 **재 준다** — 다섯 개 중 무엇이 없는지, 그리고
45
+ 없는 것을 어떻게 까는지. 말로 「준비물이 있습니다」 하는 것과 **실제로 재 주는 것**은 다르다.
46
+
47
+ ## 두 번째 이유 — 개인정보
48
+
49
+ 이건 다른 안내에서 거의 안 나온다. 이 저장소는 **내 이력서를 저장소 안에 넣는 방식**이고,
50
+ GitHub 에서 **fork 는 기본이 공개**다. 프로필을 채우고 push 하면 이름·연락처·경력이
51
+ 검색되는 자리에 놓인다.
52
+
53
+ `SKILL.md` §4 가 fork 대신 **비공개 clone** 하는 명령을 그대로 준다.
54
+ 이건 「알아 두면 좋은 것」이 아니라 **되돌리기 어려운 것**이라 앞에 뒀다.
55
+
56
+ ## 안 하는 말
57
+
58
+ 원장의 `안하는말` 에 네 줄로 박아 뒀고, `--self-test` 가 금칙어를 양방향으로 시험한다.
59
+
60
+ - 이 스킬이 저장소를 대신 깔아 준다
61
+ - 취업을 보장한다
62
+ - 한국 채용 사이트가 이미 붙어 있다
63
+ - LaTeX 없이도 PDF 가 나온다