@kaydash9999/banker-plugins 0.23.3 → 0.24.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.
Files changed (76) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +39 -0
  4. package/README.md +14 -6
  5. package/codex/manifest.json +6 -2
  6. package/codex/transform-matrix.md +6 -2
  7. package/package.json +1 -1
  8. package/skills/build-testbox/SKILL.md +213 -0
  9. package/skills/build-testbox/scripts/args.mjs +156 -0
  10. package/skills/build-testbox/scripts/browser-cell.mjs +181 -0
  11. package/skills/build-testbox/scripts/browsers.mjs +164 -0
  12. package/skills/build-testbox/scripts/containers.mjs +237 -0
  13. package/skills/build-testbox/scripts/detect.mjs +166 -0
  14. package/skills/build-testbox/scripts/proc.mjs +91 -0
  15. package/skills/build-testbox/scripts/report.mjs +109 -0
  16. package/skills/build-testbox/scripts/setup.mjs +144 -0
  17. package/skills/build-testbox/scripts/testbox.mjs +148 -0
  18. package/skills/git-init/SKILL.md +152 -0
  19. package/skills/git-init/scripts/git-init.mjs +440 -0
  20. package/skills/git-init/scripts/marker-block.mjs +311 -0
  21. package/skills/git-init/scripts/payload-scan.mjs +186 -0
  22. package/skills/init-dir/SKILL.md +172 -0
  23. package/skills/init-dir/scripts/init-dir.mjs +308 -0
  24. package/skills/init-dir/scripts/marker-block.mjs +311 -0
  25. package/skills/init-dir/scripts/templates.mjs +100 -0
  26. package/skills/init-dir/templates/KB/README.md +15 -0
  27. package/skills/init-dir/templates/docs/README.md +14 -0
  28. package/skills/init-dir/templates/logs/README.md +14 -0
  29. package/skills/init-dir/templates/logs/gitignore +3 -0
  30. package/skills/init-dir/templates/mockup/README.md +7 -0
  31. package/skills/init-dir/templates/scripts/README.md +36 -0
  32. package/skills/init-dir/templates/scripts/lib/service.mjs +540 -0
  33. package/skills/init-dir/templates/scripts/restart.cmd +13 -0
  34. package/skills/init-dir/templates/scripts/restart.sh +10 -0
  35. package/skills/init-dir/templates/scripts/service.conf +5 -0
  36. package/skills/init-dir/templates/scripts/start.cmd +13 -0
  37. package/skills/init-dir/templates/scripts/start.sh +10 -0
  38. package/skills/init-dir/templates/scripts/status.cmd +13 -0
  39. package/skills/init-dir/templates/scripts/status.sh +10 -0
  40. package/skills/init-dir/templates/scripts/stop.cmd +13 -0
  41. package/skills/init-dir/templates/scripts/stop.sh +10 -0
  42. package/skills/init-dir/templates/tests/README.md +15 -0
  43. package/skills/init-dir/templates/tests/run.cmd +13 -0
  44. package/skills/init-dir/templates/tests/run.mjs +160 -0
  45. package/skills/init-dir/templates/tests/run.sh +10 -0
  46. package/skills/init-dir/templates/tests/tests.conf +7 -0
  47. package/skills/init-dir/templates/work/README.md +8 -0
  48. package/skills/progress-mon/README.md +10 -8
  49. package/skills/progress-mon/SKILL.md +53 -16
  50. package/skills/progress-mon/assets/fixture/README.md +1 -1
  51. package/skills/progress-mon/assets/fixture/adr.json +35 -0
  52. package/skills/progress-mon/assets/fixture/arch.json +1 -1
  53. package/skills/progress-mon/assets/fixture/flow.json +2 -2
  54. package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +1 -1
  55. package/skills/progress-mon/assets/fixture/variants/legacy-init/rules.json +6 -0
  56. package/skills/progress-mon/assets/fixture/variants/legacy-init/state.json +5 -0
  57. package/skills/progress-mon/references/adr.md +177 -0
  58. package/skills/progress-mon/references/authoring.md +13 -5
  59. package/skills/progress-mon/scripts/pm-adr.mjs +585 -0
  60. package/skills/progress-mon/scripts/pm-check.mjs +48 -20
  61. package/skills/progress-mon/scripts/pm-data.mjs +1 -1
  62. package/skills/progress-mon/scripts/pm-html.mjs +1 -0
  63. package/skills/progress-mon/scripts/pm-memory.mjs +113 -28
  64. package/skills/progress-mon/scripts/pm-render.mjs +6 -4
  65. package/skills/progress-mon/scripts/pm-server.mjs +2 -1
  66. package/skills/progress-mon/scripts/pm-sources.mjs +1 -0
  67. package/skills/progress-mon/scripts/progress-mon.mjs +3 -2
  68. package/skills/tune-tmux/SKILL.md +235 -0
  69. package/skills/tune-tmux/scripts/app-notes.mjs +38 -0
  70. package/skills/tune-tmux/scripts/marker-block.mjs +311 -0
  71. package/skills/tune-tmux/scripts/psmux-caps.json +71 -0
  72. package/skills/tune-tmux/scripts/tmux-config.mjs +251 -0
  73. package/skills/tune-tmux/scripts/tune-tmux.mjs +284 -0
  74. package/skills/ultra-chain/SKILL.md +52 -22
  75. package/skills/ultra-chain/references/chain-ledger.mjs +93 -11
  76. package/skills/ultra-chain/references/chain-report.mjs +13 -2
@@ -0,0 +1,144 @@
1
+ // setup: the only part of build-testbox that downloads. Without --yes it prints the plan (what, about how big) and
2
+ // exits 0. With --yes it checks each source within 15 seconds, pulls images and installs browsers from the sources
3
+ // it reached, then verifies with the same checks detect uses.
4
+ // Exit 0 everything got and verified, 1 a step failed, 4 nothing failed but a source was unreachable ("네트워크 필요").
5
+ import { homedir } from "node:os";
6
+ import { BROWSERS, DEFAULT_IMAGES, imageFor } from "./args.mjs";
7
+ import { probeBrowser } from "./browser-cell.mjs";
8
+ import { detectEngines, imageState, selectEngine } from "./containers.mjs";
9
+ import { loadPlaywright, playwrightImage } from "./detect.mjs";
10
+ import { firstLine, runProc } from "./proc.mjs";
11
+
12
+ export const NET_TIMEOUT_MS = 15000;
13
+ const SIZES = { rocky8: "약 200MB", rocky9: "약 180MB", chromium: "약 280MB(chromium 과 headless shell)", webkit: "약 100MB", playwright: "약 2GB" };
14
+
15
+ // The registry an image comes from, as the URL to check. A loopback registry is plain http (local registries are).
16
+ export function registryUrl(image) {
17
+ const first = image.split("/")[0];
18
+ const hasHost = image.includes("/") && (/[.:]/.test(first) || first === "localhost");
19
+ const host = !hasHost || first === "docker.io" ? "registry-1.docker.io" : first;
20
+ const plain = /^(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(host);
21
+ return `${plain ? "http" : "https"}://${host}/v2/`;
22
+ }
23
+
24
+ const noUser = (url) => String(url).replace(/^([A-Za-z][A-Za-z0-9+.-]*:\/\/)[^/@]*@/, "$1").replace(/\/$/, "");
25
+
26
+ export function downloadHosts(env = process.env) {
27
+ const main = noUser(env.PLAYWRIGHT_DOWNLOAD_HOST || "https://cdn.playwright.dev");
28
+ const chromium = noUser(env.PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST || main);
29
+ return [...new Set([`${chromium}/`, `${main}/`])];
30
+ }
31
+
32
+ // Any HTTP answer (401 and 404 too) means reached. No answer within the limit is unreachable.
33
+ export async function reachable(url, timeoutMs = NET_TIMEOUT_MS) {
34
+ try {
35
+ await fetch(url, { method: "HEAD", redirect: "manual", signal: AbortSignal.timeout(timeoutMs) });
36
+ return { ok: true };
37
+ } catch (e) {
38
+ const cause = e && e.cause ? e.cause.code || e.cause.message : "";
39
+ return { ok: false, detail: `${e.name === "TimeoutError" ? `${timeoutMs / 1000}초 안에 답 없음` : firstLine(e.message)}${cause ? ` (${cause})` : ""}` };
40
+ }
41
+ }
42
+
43
+ function itemsFromArgs(o, pwVersion) {
44
+ const items = (o.setupImages || []).map((os) => ({ kind: "image", key: os, image: imageFor(o, os) }));
45
+ if (o.playwrightImage) items.push({ kind: "image", key: "playwright", image: o.images.playwright || (pwVersion ? playwrightImage(pwVersion) : null) });
46
+ if (o.browsersGiven) for (const b of o.browsers) items.push({ kind: "browser", key: b });
47
+ return items;
48
+ }
49
+
50
+ // No item named: the default images missing locally and the browsers whose build is missing.
51
+ async function defaultItems(o, engine, pw) {
52
+ const items = [];
53
+ for (const os of Object.keys(DEFAULT_IMAGES)) {
54
+ const image = imageFor(o, os);
55
+ if (!engine || !(await imageState(engine, image)).present) items.push({ kind: "image", key: os, image });
56
+ }
57
+ for (const b of BROWSERS) if (!pw || (await probeBrowser(pw, b)).reason === "needs-browser") items.push({ kind: "browser", key: b });
58
+ return items;
59
+ }
60
+
61
+ export async function planSetup(o) {
62
+ const loaded = await loadPlaywright(o.fromProject, o.playwrightDir);
63
+ const engine = selectEngine(o.engine, await detectEngines());
64
+ const named = Boolean(o.setupImages || o.playwrightImage || o.browsersGiven);
65
+ const items = named ? itemsFromArgs(o, loaded.info && loaded.info.version) : await defaultItems(o, engine, loaded.pw);
66
+ return { items, engine, loaded };
67
+ }
68
+
69
+ const label = (it) => (it.kind === "image" ? `이미지 ${it.key}: ${it.image || "(Playwright 판을 모름)"}` : `브라우저 ${it.key}`);
70
+
71
+ export function renderPlan(items) {
72
+ if (!items.length) return "받을 것 없음\n";
73
+ const lines = ["받을 것(--yes 를 주면 받음):"];
74
+ for (const it of items) lines.push(`- ${label(it)}, ${SIZES[it.key] || "크기 모름"}`);
75
+ lines.push("받으려면 확인을 받은 뒤 같은 인자에 --yes 를 붙여 다시 실행");
76
+ return `${lines.join("\n")}\n`;
77
+ }
78
+
79
+ async function checkSources(items, env) {
80
+ const urls = new Set();
81
+ for (const it of items) {
82
+ if (it.kind === "image" && it.image) urls.add(registryUrl(it.image));
83
+ if (it.kind === "browser") for (const u of downloadHosts(env)) urls.add(u);
84
+ }
85
+ const results = await Promise.all([...urls].map(async (u) => [u, await reachable(u)]));
86
+ return new Map(results);
87
+ }
88
+
89
+ function sourcesOf(it, env) {
90
+ if (it.kind === "image") return it.image ? [registryUrl(it.image)] : [];
91
+ return downloadHosts(env);
92
+ }
93
+
94
+ async function getImage(it, engine) {
95
+ if (!it.image) return { status: "failed", detail: "Playwright 패키지가 없어 이미지 판을 정하지 못함(setup-playwright)" };
96
+ if (!engine) return { status: "failed", detail: "docker, podman 없음" };
97
+ const r = await runProc(engine.bin, ["pull", it.image], { timeoutMs: 60 * 60 * 1000 });
98
+ if (r.code !== 0) return { status: "failed", detail: firstLine(r.stderr) || r.error };
99
+ const v = await imageState(engine, it.image);
100
+ return v.present ? { status: "ok" } : { status: "failed", detail: `받은 뒤 확인 실패: ${v.reason}` };
101
+ }
102
+
103
+ async function getBrowser(it, loaded) {
104
+ if (!loaded.info) return { status: "failed", detail: `${loaded.error}. setup-playwright 스킬로 설치하거나 --playwright-dir 로 위치를 줌` };
105
+ // npx runs where that Playwright is: the given folder, the project, or (global) the home folder.
106
+ const cwd = loaded.info.base || homedir();
107
+ const r = await runProc("npx", ["--no-install", "playwright", "install", it.key], { cwd, timeoutMs: 60 * 60 * 1000 });
108
+ if (r.code !== 0) return { status: "failed", detail: firstLine(r.stderr) || r.error };
109
+ const again = await loadPlaywright(loaded.info.from === "project", loaded.info.from === "dir" ? loaded.info.base : null);
110
+ const p = again.pw ? await probeBrowser(again.pw, it.key) : { runnable: false, reason: "needs-playwright" };
111
+ return p.runnable ? { status: "ok" } : { status: "failed", detail: `설치 뒤 실행 확인 실패: ${p.reason}` };
112
+ }
113
+
114
+ function blockedBy(it, net, env) {
115
+ const bad = sourcesOf(it, env).filter((u) => !net.get(u).ok);
116
+ return bad.length ? bad.map((u) => `${u} ${net.get(u).detail}`).join("; ") : null;
117
+ }
118
+
119
+ // Images first in the order given, then browsers; one at a time.
120
+ export async function applySetup(plan, env = process.env) {
121
+ const net = await checkSources(plan.items, env);
122
+ const rows = [];
123
+ for (const it of plan.items) {
124
+ const blocked = blockedBy(it, net, env);
125
+ if (blocked) { rows.push({ ...it, status: "needs-network", detail: blocked }); continue; }
126
+ const got = it.kind === "image" ? await getImage(it, plan.engine) : await getBrowser(it, plan.loaded);
127
+ rows.push({ ...it, ...got });
128
+ }
129
+ return rows;
130
+ }
131
+
132
+ export function setupExitCode(rows) {
133
+ if (rows.some((r) => r.status === "failed")) return 1;
134
+ if (rows.some((r) => r.status === "needs-network")) return 4;
135
+ return 0;
136
+ }
137
+
138
+ export function renderSetup(rows, env = process.env) {
139
+ const n = (s) => rows.filter((r) => r.status === s).length;
140
+ const lines = [`setup: ok ${n("ok")}, failed ${n("failed")}, needs-network ${n("needs-network")}`];
141
+ if (n("needs-network")) lines.push(env.CODEX_SANDBOX_NETWORK_DISABLED === "1" ? "네트워크 필요: Codex 샌드박스가 네트워크를 막음(승인 뒤 다시 실행)" : "네트워크 필요: 닿지 못한 원천은 받지 않음");
142
+ for (const r of rows) lines.push(`- ${label(r)}: ${r.status}${r.detail ? ` (${r.detail})` : ""}`);
143
+ return `${lines.join("\n")}\n`;
144
+ }
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ // build-testbox: a test bed each PC builds for itself. Browser matrix (Playwright Chromium and WebKit, desktop and
3
+ // mobile emulation) and container OS (Rocky Linux; macOS only as a Tart VM on Apple Silicon) run in parallel.
4
+ // run downloads nothing; setup downloads only after --yes. Pure Node 18+, no dependency.
5
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
6
+ import { join, resolve } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { UsageError, parseArgs } from "./args.mjs";
9
+ import { runBrowserGroup } from "./browsers.mjs";
10
+ import { detectEngines, randomTag, removeAllSync, runOsGroup, selectEngine } from "./containers.mjs";
11
+ import { detect, hostInfo, loadPlaywright, renderDetect } from "./detect.mjs";
12
+ import { killAll } from "./proc.mjs";
13
+ import { buildResults, renderSummary, writeResults } from "./report.mjs";
14
+ import { applySetup, planSetup, renderPlan, renderSetup, setupExitCode } from "./setup.mjs";
15
+
16
+ const USAGE = `build-testbox: 브라우저 행렬과 컨테이너 OS 를 병렬로 돌리는 시험 환경(detect, run, setup)
17
+ usage:
18
+ node testbox.mjs detect [--json] [--engine auto|docker|podman] [--os <os,...>] [--image <os>=<이미지>] [--playwright-from-project]
19
+ [--playwright-dir <폴더>]
20
+ node testbox.mjs run [--url <loopback URL|파일>] [--browsers chromium,webkit] [--devices "<기기>,..."] [--browser-in-container]
21
+ [--os rocky8,rocky9,macos|none] [--image <os>=<이미지>] [--cmd "<명령>"] [--src <dir>]
22
+ [--engine auto|docker|podman] [--network bridge|none|host] [--jobs N] [--timeout <초>] [--out <dir>]
23
+ [--playwright-from-project] [--playwright-dir <폴더>] [--allow-remote] [--tart-vm <이름>] [--json]
24
+ node testbox.mjs setup [--images <os,...>] [--browsers <이름,...>] [--playwright-image] [--yes] [--engine ...] [--image <os>=<이미지>]
25
+ [--playwright-dir <폴더>]
26
+ detect 호스트, Playwright, 브라우저(실제 headless 실행), 엔진, 로컬 이미지, Tart. 내려받지 않음
27
+ run 브라우저 묶음(--url 이 있을 때)과 컨테이너 묶음을 같은 순간에 시작. 내려받지 않음. 결과는 <out>/results.json, summary.md
28
+ setup --yes 가 없으면 받을 것과 크기만 보임. --yes 면 원천마다 15초 확인 뒤 받고 detect 와 같은 확인
29
+ Playwright 찾는 순서: --playwright-dir, BUILD_TESTBOX_PLAYWRIGHT_DIR(둘 다 node_modules 를 담은 폴더), --playwright-from-project 의 프로젝트, 전역 npm root
30
+ 기본: --devices "Desktop Chrome,Pixel 7,Desktop Safari,iPhone 15", --os rocky8,rocky9, --cmd "cat /etc/os-release",
31
+ --jobs min(4, CPU 수), --timeout 300, --out ./.testbox/<시각>
32
+ exit(run): 0 모든 칸 pass, 1 fail 1칸 이상, 4 fail 없이 unverified 나 unsupported, 2 사용 오류, 3 내부 오류나 결과 쓰기 실패,
33
+ 129, 130, 143 SIGHUP, SIGINT, SIGTERM(만든 컨테이너를 지운 뒤)
34
+ exit(setup): 0 받고 확인됨(또는 계획만), 1 일부 실패, 4 네트워크 필요, 2 사용 오류
35
+ `;
36
+
37
+ function defaultOut() {
38
+ const stamp = new Date().toISOString().replace(/[-:]/g, "").replace("T", "-").slice(0, 15);
39
+ const dir = join(process.cwd(), ".testbox", stamp);
40
+ return existsSync(dir) ? `${dir}-${randomTag().slice(0, 4)}` : dir;
41
+ }
42
+
43
+ function cleanup(tracker, engine) {
44
+ killAll();
45
+ if (engine) removeAllSync(engine, [...tracker.containers]);
46
+ for (const dir of tracker.temps) rmSync(dir, { recursive: true, force: true });
47
+ const n = tracker.containers.size;
48
+ tracker.containers.clear();
49
+ tracker.temps.clear();
50
+ return n;
51
+ }
52
+
53
+ // 128 + the signal number, the shell's convention.
54
+ const SIGNAL_EXIT = { SIGHUP: 129, SIGINT: 130, SIGTERM: 143 };
55
+
56
+ function installSignals(tracker, engine) {
57
+ const handler = (sig) => {
58
+ const n = cleanup(tracker, engine);
59
+ process.stderr.write(`${sig}: 만든 컨테이너 ${n}개를 지우고 멈춤\n`);
60
+ process.exit(SIGNAL_EXIT[sig]);
61
+ };
62
+ for (const sig of Object.keys(SIGNAL_EXIT)) process.on(sig, handler);
63
+ return () => { for (const sig of Object.keys(SIGNAL_EXIT)) process.off(sig, handler); };
64
+ }
65
+
66
+ function notesOf(o, engines, engine) {
67
+ const notes = [];
68
+ if (!o.url) notes.push("브라우저 묶음 요청 안 됨(--url 없음)");
69
+ if (!o.os.length) notes.push("컨테이너 묶음 요청 안 됨(--os none)");
70
+ if (o.os.some((x) => x !== "macos")) {
71
+ const emu = engine && engine.name === "docker" && engines.docker.podmanEmulation ? "(podman 에뮬레이션)" : "";
72
+ notes.push(`컨테이너 엔진: ${engine ? engine.name + emu : "없음"}`);
73
+ }
74
+ return notes;
75
+ }
76
+
77
+ async function browserGroup(ctx) {
78
+ const loaded = await loadPlaywright(ctx.o.fromProject, ctx.o.playwrightDir);
79
+ return runBrowserGroup({ ...ctx, pw: loaded.pw, pwInfo: loaded.info, pwError: loaded.error });
80
+ }
81
+
82
+ async function runCommand(o) {
83
+ const startedAt = new Date().toISOString();
84
+ const out = o.out || defaultOut();
85
+ mkdirSync(out, { recursive: true });
86
+ const host = hostInfo();
87
+ const engines = await detectEngines();
88
+ const engine = selectEngine(o.engine, engines);
89
+ const tracker = { containers: new Set(), temps: new Set() };
90
+ const off = installSignals(tracker, engine);
91
+ const ctx = { o, out, engine, tracker, host, env: process.env };
92
+ try {
93
+ const [b, c] = await Promise.all([o.url ? browserGroup(ctx) : [], o.os.length ? runOsGroup(ctx) : []]);
94
+ const results = buildResults({ cells: [...b, ...c], notes: notesOf(o, engines, engine), host, startedAt, out });
95
+ writeResults(out, results);
96
+ return results;
97
+ } catch (e) {
98
+ cleanup(tracker, engine);
99
+ throw e;
100
+ } finally {
101
+ off();
102
+ }
103
+ }
104
+
105
+ async function setupCommand(o) {
106
+ const plan = await planSetup(o);
107
+ if (!o.yes) {
108
+ process.stdout.write(o.json ? `${JSON.stringify({ plan: plan.items })}\n` : renderPlan(plan.items));
109
+ return 0;
110
+ }
111
+ const rows = await applySetup(plan);
112
+ process.stdout.write(o.json ? `${JSON.stringify({ rows, exitCode: setupExitCode(rows) })}\n` : renderSetup(rows));
113
+ return setupExitCode(rows);
114
+ }
115
+
116
+ async function dispatch(o) {
117
+ if (o.command === "detect") {
118
+ const d = await detect(o);
119
+ process.stdout.write(o.json ? `${JSON.stringify(d)}\n` : renderDetect(d));
120
+ return 0;
121
+ }
122
+ if (o.command === "setup") return setupCommand(o);
123
+ const results = await runCommand(o);
124
+ process.stdout.write(o.json ? `${JSON.stringify(results)}\n` : `${renderSummary(results)}결과: ${join(results.out, "results.json")}\n`);
125
+ return results.exitCode;
126
+ }
127
+
128
+ const wantsHelp = (argv) => !argv.length || ["--help", "-h", "help"].includes(argv[0]) || argv.includes("--help");
129
+
130
+ export async function main(argv) {
131
+ if (wantsHelp(argv)) {
132
+ process.stdout.write(USAGE);
133
+ return argv.length ? 0 : 2;
134
+ }
135
+ try {
136
+ return await dispatch(parseArgs(argv));
137
+ } catch (e) {
138
+ if (e instanceof UsageError) {
139
+ process.stderr.write(`build-testbox: ${e.message}\n(--help 로 사용법)\n`);
140
+ return 2;
141
+ }
142
+ process.stderr.write(`build-testbox: 내부 오류: ${e && e.stack ? e.stack : e}\n`);
143
+ return 3;
144
+ }
145
+ }
146
+
147
+ const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
148
+ if (isMain) main(process.argv.slice(2)).then((code) => { process.exitCode = code; });
@@ -0,0 +1,152 @@
1
+ ---
2
+ name: git-init
3
+ description: "저장소가 아닌 현재 폴더에 첫 커밋을 만들어 worktree 같은 병렬 작업을 준비. 민감 정보를 빼는 .gitignore 를 쓰고, 추적될 파일을 비밀값 스캔한 뒤 git init, git add ., git commit -m \"init\" 을 실행. 걸림이 있으면 .git 을 만들지 않음. 'git-init'/'git 초기화'/'첫 커밋'/'저장소 만들기'/'worktree 준비' 시 사용."
4
+ argument-hint: "[check|run] [--branch <이름>] [--local-identity \"<이름> <<메일>>\"] [--nested] [--accept <파일:줄:종류>]... [--follow-symlink]"
5
+ ---
6
+
7
+ # git-init: 첫 커밋 준비
8
+
9
+ 저장소가 아닌 폴더를 git 저장소로 만들고 커밋 `init` 하나를 남긴다.
10
+ 그 커밋이 있어야 `git worktree add` 로 병렬 작업을 시작할 수 있다.
11
+ 답변은 한글.
12
+
13
+ 순서: `.gitignore` 작성, 추적될 파일의 이름과 내용 스캔, `git init`, `git add .`, `git commit -m "init"`.
14
+ 스캔에 걸리면 `git init` 하지 않는다.
15
+
16
+ ## 언제 쓰나
17
+
18
+ - 새 프로젝트 폴더에서 첫 커밋을 만들 때
19
+ - 에이전트 여럿이 worktree 로 나눠 일하기 전에 기준 커밋이 필요할 때
20
+
21
+ ## 쓰지 않을 때
22
+
23
+ | 상황 | 대신 |
24
+ |---|---|
25
+ | 이미 저장소인 폴더의 `.gitignore` 잡음 정리 | banker `refresh-git-ignore` |
26
+ | 이미 커밋된 비밀값 찾기, 의존성 취약점 | banker `audit-security` |
27
+ | 관리 폴더(logs, scripts, work 등) 만들기 | banker `init-dir` |
28
+
29
+ - refresh-git-ignore 와의 역할 나눔: git-init 은 저장소가 생기기 전 한 번 실행한다. refresh-git-ignore 는 이미 있는 저장소의 런타임 잡음을 정리한다(모델 절차, 스크립트 없음). 둘은 코드를 나누지 않는다
30
+
31
+ ## 자리 표시
32
+
33
+ | 자리 | 뜻 |
34
+ |---|---|
35
+ | `<S>` | 이 스킬 디렉터리 절대경로(아래 런타임 표의 "스킬 디렉터리") |
36
+ | `<root>` | 대상 폴더. 기본은 작업 폴더. 다르면 `--root <dir>` |
37
+
38
+ - 셸 변수는 도구 호출 사이에 남지 않는다. 명령마다 실제 경로를 적는다
39
+
40
+ ## 런타임 표
41
+
42
+ | 항목 | Claude Code | Codex |
43
+ |---|---|---|
44
+ | 스킬 디렉터리 | 스킬 맨 위 "Base directory for this skill:" 경로 | Codex 스킬 목록이 보이는 이 SKILL.md 경로(file)의 폴더. npm 설치는 `~/.codex/skills/banker-git-init/`(프로젝트 범위 설치면 `./.codex/skills/banker-git-init/`), 플러그인 설치(`codex plugin add`)는 `~/.codex/plugins/cache/<마켓플레이스>/banker/<판>/skills/git-init/`. 절대경로로 씀 |
45
+ | 확인 질문 도구 | `AskUserQuestion` | `request_user_input`. 없으면 번호 붙인 평문 선택지 |
46
+ | `check` | 바로 실행 | 바로 실행(임시 폴더만 씀) |
47
+ | `run` | 바로 실행 | `.git` 쓰기는 기본 샌드박스가 막을 수 있음. 명령에 `sandbox_permissions: "require_escalated"` 로 승인 요청. `--add-dir` 로 설정 폴더를 넓히지 않음 |
48
+
49
+ ## 인자
50
+
51
+ | 인자 | 뜻 | 기본 |
52
+ |---|---|---|
53
+ | `check`, `run` | `check` 는 아무것도 쓰지 않고 계획(새 `.gitignore` 블록, 추적 후보 수, 스캔 결과)만 보임 | `check` |
54
+ | `--root <dir>` | 대상 폴더 | 작업 폴더 |
55
+ | `--branch <이름>` | 첫 브랜치 | `init.defaultBranch` 설정, 없으면 `main`. 이 저장소에만 적용, 전역 설정은 바꾸지 않음 |
56
+ | `--local-identity "<이름> <<메일>>"` | 커밋 신원이 없을 때 이 저장소의 `.git/config` 에만 설정 | 없음 |
57
+ | `--nested` | 상위 폴더가 저장소여도 진행 | 꺼짐(멈춤) |
58
+ | `--accept <파일:줄:종류>` | 내용 걸림 중 사용자가 오탐으로 확인한 것. 여러 번 줄 수 있음 | 없음 |
59
+ | `--follow-symlink` | `.gitignore` 가 링크일 때 가리키는 파일에 씀 | 꺼짐(링크면 거부) |
60
+ | `--json` | JSON 한 줄 출력 | 꺼짐 |
61
+
62
+ ## 실행
63
+
64
+ 1. 계획 확인
65
+
66
+ ```bash
67
+ node "<S>/scripts/git-init.mjs" check [--root "<root>"] [--branch <이름>] [--local-identity "<이름> <<메일>>"]
68
+ ```
69
+
70
+ - 출력(브랜치, `.gitignore` 계획, 추적 후보 수, 걸림, 내용 미스캔 목록)을 사용자에게 보인다
71
+ - 내용 걸림은 `파일:줄:종류` 만 보인다. 파일을 열어 값을 답변에 옮기지 않는다
72
+ 2. 사용자 확인(런타임 표의 확인 질문 도구)
73
+ - 걸림이 없으면 "run 으로 진행" 여부만 묻는다
74
+ - 내용 걸림이 있으면 항목마다 "진짜 비밀값(파일을 고치거나 `.gitignore` 에 넣음)" 과 "오탐(넘김)" 중에서 고르게 한다. 오탐으로 고른 항목만 `--accept` 로 넘긴다. 사용자 확인 없이 `--accept` 를 붙이지 않는다
75
+ - 이름 걸림은 넘길 수 없다. 그 파일을 `.gitignore` 에 넣거나 저장소 밖으로 옮길지 묻는다
76
+ - 내용을 검사하지 않은 파일(출력의 `내용 미스캔(2MB 초과)`, `내용 미스캔(바이너리)` 목록)이 있으면 그 목록을 확인 항목으로 드러낸다. 스캔이 이 파일들의 비밀값을 보지 못했으므로 첫 커밋에 넣어도 되는지 사용자에게 확인받는다. 넣으면 안 되는 파일은 `.gitignore` 에 넣거나 저장소 밖으로 옮긴다
77
+ 3. 실행
78
+
79
+ ```bash
80
+ node "<S>/scripts/git-init.mjs" run [--root "<root>"] [같은 인자] [--accept "<파일:줄:종류>"]...
81
+ ```
82
+
83
+ 4. 결과 처리
84
+ - 커밋 해시, 브랜치, 추적 파일 수, "확인으로 넘긴 항목" 을 보고한다
85
+ - `경고:` 줄(100MB 초과 파일)과 "내용 미스캔(2MB 초과)", "내용 미스캔(바이너리)" 목록은 그대로 전한다. 큰 파일은 원격 저장소가 거부할 수 있다
86
+ - 출력이 비어 있으면 실행 실패. 성공으로 보고하지 않는다
87
+ 5. 종료 코드
88
+
89
+ | 코드 | 뜻 | 처리 |
90
+ |---|---|---|
91
+ | 0 | 커밋 완료. `check` 는 걸림 없음 | 결과 보고 |
92
+ | 1 | 스캔 걸림, `.gitignore` 쓰기 거부, git 단계 실패 | 아래 "문제 해결" |
93
+ | 2 | 사용 오류(모르는 인자, `--accept` 나 `--local-identity` 꼴, 쓸 수 없는 브랜치 이름, 루트 폴더 없음) | 인자를 고쳐 다시 실행 |
94
+ | 3 | 선행 조건 실패. 아무것도 바꾸지 않음 | 메시지를 전하고 아래 "문제 해결" |
95
+
96
+ ## run 이 하는 일
97
+
98
+ 1. 선행 조건(실패하면 종료 3, 아무것도 안 바꿈)
99
+ - `git` 이 없음, 루트가 HOME 이나 파일 시스템 루트, 환경 변수 `GIT_DIR` 이나 `GIT_WORK_TREE`
100
+ - 루트나 상위가 이미 작업 트리. 상위만 저장소면 `--nested` 일 때만 진행
101
+ - 하위 폴더에 `.git` 이 있음(넣으면 gitlink 가 됨). 그 경로를 보임
102
+ - 커밋 신원: `user.name` 과 `user.email` 설정이 둘 다 없고 `--local-identity` 도 없음. `git var` 는 호스트 이름으로 추정한 신원으로 성공할 수 있어 쓰지 않음
103
+ 2. `.gitignore`: 없으면 새로 쓴다. 있으면 기존 줄은 두고 빠진 패턴만 `# banker:git-init:start` .. `# banker:git-init:end` 블록으로 끝에 넣는다
104
+ - 패턴: `.env`, `.env.*`, `!.env.example`, `*.pem`, `*.key`, `*.p12`, `*.pfx`, `*.jks`, `*.keystore`, `id_rsa*`, `id_ecdsa*`, `id_ed25519*`, `credentials.json`, `*.tfstate`, `*.tfstate.*`, `.aws/`, `.ssh/`, `.omc/`, `.omx/`, `node_modules/`, `.npmrc`, `.netrc`, `_netrc`, `.pgpass`, `.DS_Store`, `Thumbs.db`, `.gitignore.git-init.bak`(이 스킬이 남기는 백업), `.*.init-dir-*/`(init-dir 이 강제 종료로 남긴 임시 폴더)
105
+ - 블록 밖 바이트, CRLF, BOM, 권한 비트는 그대로. 바이트가 바뀔 때만 첫 원본을 `.gitignore.git-init.bak` 에 둔다(이미 있으면 덮지 않음)
106
+ 3. 추적 후보: 임시 폴더의 빈 저장소로 `git ls-files -o --exclude-standard` 를 구한다. 루트에는 아직 `.git` 이 없다
107
+ 4. 스캔
108
+ - 이름: 키, 인증서, `.env` 류, `.npmrc`, `.netrc`, `.pgpass`, `.aws/`, `.ssh/` 아래 파일이 목록에 남았는지(사용자 `.gitignore` 의 `!` 예외 등). `--accept` 로 넘길 수 없음
109
+ - 내용: 2MB 이하 텍스트 파일(앞 8KB 에 NUL 바이트가 없으면 텍스트)을 `scripts/payload-scan.mjs` 로. 값은 출력하지 않고 `파일:줄:종류` 만
110
+ - `--accept` 와 정확히 같은 `파일:줄:종류` 만 걸림에서 뺀다. 줄이나 종류가 다르면 넘기지 못한다
111
+ - 남은 걸림이 있으면 `git init` 하지 않고 종료 1. `.gitignore` 변경은 남기고 그 사실을 보고
112
+ 5. `git init`(브랜치는 인자 표대로. git 2.28 미만은 `git symbolic-ref`), `--local-identity` 면 저장소 범위 신원
113
+ 6. `git add .` 뒤 스테이지된 이름이 스캔한 목록과 같은지 확인. 다르면 커밋하지 않고 종료 1
114
+ 7. `git commit -m "init"`. 커밋 훅을 건너뛰지 않는다. 120초 안에 끝나지 않으면 멈추고 종료 1
115
+ - 추적 후보가 0개여도 `.gitignore` 가 추적되므로 커밋은 1개 생긴다
116
+
117
+ ## 오프라인
118
+
119
+ - 네트워크를 쓰지 않는다. 원격 저장소 연결과 push 는 하지 않는다
120
+
121
+ ## 함정
122
+
123
+ - `check` 의 추적 후보는 새 패턴을 임시 저장소의 `info/exclude` 로 흉내 낸 값이다. 사용자 `.gitignore` 가 새 패턴과 부딪치면 `run` 의 수와 다를 수 있다. `run` 은 실제 `.gitignore` 를 쓴 뒤 다시 구하고 다시 스캔한다
124
+ - `includeIf "gitdir:..."` 로만 정한 신원은 저장소가 생기기 전이라 보이지 않는다. 이때는 `--local-identity` 를 쓴다
125
+ - 기존 `.gitignore` 를 바꾸면 `.gitignore.git-init.bak` 이 루트에 생긴다. 이 이름은 블록 패턴에 들어 있어 첫 커밋에 들어가지 않는다. 필요 없으면 사용자 확인을 받아 지운다
126
+ - 걸림으로 멈춘 뒤 다시 실행하면 블록을 같은 내용으로 다시 쓴다(바이트가 같으면 쓰지 않음)
127
+ - 스캔은 흔한 키 형식을 잡을 뿐 깨끗함을 증명하지 않는다. 미스캔 목록(2MB 초과, 바이너리)은 사용자가 따로 확인한다
128
+
129
+ ## 문제 해결
130
+
131
+ | 메시지 | 조치 |
132
+ |---|---|
133
+ | 커밋 신원 없음 | `--local-identity "<이름> <<메일>>"` 로 이 저장소에만 둘지, 사용자가 `git config --global` 을 직접 실행할지 묻는다. 전역 설정은 대신 바꾸지 않는다 |
134
+ | 상위 폴더가 저장소 | 그 저장소 경로를 보이고, 안에 따로 저장소를 만들지 묻는다. 동의하면 `--nested` |
135
+ | 이미 저장소 | git-init 대상이 아님. 잡음 정리는 refresh-git-ignore |
136
+ | 하위 폴더에 .git 이 있음 | 경로를 보이고, 그 폴더를 `.gitignore` 에 넣을지, 서브모듈로 할지, 옮길지 묻는다. `.git` 을 지우지 않는다 |
137
+ | 이름 걸림 | 그 파일을 `.gitignore` 에 넣거나 옮길지 묻는다. 사용자 `.gitignore` 의 `!` 예외가 원인이면 그 줄을 보인다 |
138
+ | 내용 걸림 | `파일:줄:종류` 만 보이고 오탐 여부를 묻는다. 오탐만 `--accept` |
139
+ | 심볼릭 링크라 그대로 둠 | `.gitignore` 가 링크. 링크와 대상 경로를 보이고, 대상에 쓸지 물은 뒤 `--follow-symlink` |
140
+ | 하드 링크, 다른 사용자 소유, 표지 손상, UTF-8 아님 | init-dir SKILL.md 의 같은 메시지 조치와 같음. 파일을 보이고 어떻게 할지 묻는다 |
141
+ | 스테이지된 파일이 스캔한 목록과 다름 | 스캔 뒤 파일이 바뀌었거나 템플릿의 `info/exclude` 가 다름. `.git` 이 남아 있으므로 보이고, 지우고 다시 실행할지 묻는다 |
142
+ | git commit 이 120초 안에 끝나지 않음 | 커밋 훅(`core.hooksPath`, 템플릿 훅)을 확인하도록 안내 |
143
+ | 임시 폴더가 루트 안에 있음 | `TMPDIR`(Windows 는 `TEMP`)을 루트 밖으로 바꿔 다시 실행 |
144
+
145
+ ## 출처(조사한 것)
146
+
147
+ | 주제 | 확인한 사실 | 출처 |
148
+ |---|---|---|
149
+ | 첫 브랜치 | `git init -b` 와 `init.defaultBranch` 는 git 2.28 부터 | https://git-scm.com/docs/git-init |
150
+ | 무시 규칙 우선순위 | 같은 파일 안에서는 뒤 줄이 이김, `!` 로 다시 넣음 | https://git-scm.com/docs/gitignore |
151
+ | 신원 추정 막기 | `user.useConfigOnly` 면 설정 없이 신원을 추정하지 않음 | https://git-scm.com/docs/git-config |
152
+ | 추적 후보 | `ls-files -o --exclude-standard` 는 무시되지 않은 미추적 파일, 하위 저장소는 `경로/` 한 줄 | https://git-scm.com/docs/git-ls-files, 실측(git 2.43) |