@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +39 -0
- package/README.md +14 -6
- package/codex/manifest.json +6 -2
- package/codex/transform-matrix.md +6 -2
- package/package.json +1 -1
- package/skills/build-testbox/SKILL.md +213 -0
- package/skills/build-testbox/scripts/args.mjs +156 -0
- package/skills/build-testbox/scripts/browser-cell.mjs +181 -0
- package/skills/build-testbox/scripts/browsers.mjs +164 -0
- package/skills/build-testbox/scripts/containers.mjs +237 -0
- package/skills/build-testbox/scripts/detect.mjs +166 -0
- package/skills/build-testbox/scripts/proc.mjs +91 -0
- package/skills/build-testbox/scripts/report.mjs +109 -0
- package/skills/build-testbox/scripts/setup.mjs +144 -0
- package/skills/build-testbox/scripts/testbox.mjs +148 -0
- package/skills/git-init/SKILL.md +152 -0
- package/skills/git-init/scripts/git-init.mjs +440 -0
- package/skills/git-init/scripts/marker-block.mjs +311 -0
- package/skills/git-init/scripts/payload-scan.mjs +186 -0
- package/skills/init-dir/SKILL.md +172 -0
- package/skills/init-dir/scripts/init-dir.mjs +308 -0
- package/skills/init-dir/scripts/marker-block.mjs +311 -0
- package/skills/init-dir/scripts/templates.mjs +100 -0
- package/skills/init-dir/templates/KB/README.md +15 -0
- package/skills/init-dir/templates/docs/README.md +14 -0
- package/skills/init-dir/templates/logs/README.md +14 -0
- package/skills/init-dir/templates/logs/gitignore +3 -0
- package/skills/init-dir/templates/mockup/README.md +7 -0
- package/skills/init-dir/templates/scripts/README.md +36 -0
- package/skills/init-dir/templates/scripts/lib/service.mjs +540 -0
- package/skills/init-dir/templates/scripts/restart.cmd +13 -0
- package/skills/init-dir/templates/scripts/restart.sh +10 -0
- package/skills/init-dir/templates/scripts/service.conf +5 -0
- package/skills/init-dir/templates/scripts/start.cmd +13 -0
- package/skills/init-dir/templates/scripts/start.sh +10 -0
- package/skills/init-dir/templates/scripts/status.cmd +13 -0
- package/skills/init-dir/templates/scripts/status.sh +10 -0
- package/skills/init-dir/templates/scripts/stop.cmd +13 -0
- package/skills/init-dir/templates/scripts/stop.sh +10 -0
- package/skills/init-dir/templates/tests/README.md +15 -0
- package/skills/init-dir/templates/tests/run.cmd +13 -0
- package/skills/init-dir/templates/tests/run.mjs +160 -0
- package/skills/init-dir/templates/tests/run.sh +10 -0
- package/skills/init-dir/templates/tests/tests.conf +7 -0
- package/skills/init-dir/templates/work/README.md +8 -0
- package/skills/progress-mon/README.md +10 -8
- package/skills/progress-mon/SKILL.md +53 -16
- package/skills/progress-mon/assets/fixture/README.md +1 -1
- package/skills/progress-mon/assets/fixture/adr.json +35 -0
- package/skills/progress-mon/assets/fixture/arch.json +1 -1
- package/skills/progress-mon/assets/fixture/flow.json +2 -2
- package/skills/progress-mon/assets/fixture/variants/arch-auto/arch.json +1 -1
- package/skills/progress-mon/assets/fixture/variants/legacy-init/rules.json +6 -0
- package/skills/progress-mon/assets/fixture/variants/legacy-init/state.json +5 -0
- package/skills/progress-mon/references/adr.md +177 -0
- package/skills/progress-mon/references/authoring.md +13 -5
- package/skills/progress-mon/scripts/pm-adr.mjs +585 -0
- package/skills/progress-mon/scripts/pm-check.mjs +48 -20
- package/skills/progress-mon/scripts/pm-data.mjs +1 -1
- package/skills/progress-mon/scripts/pm-html.mjs +1 -0
- package/skills/progress-mon/scripts/pm-memory.mjs +113 -28
- package/skills/progress-mon/scripts/pm-render.mjs +6 -4
- package/skills/progress-mon/scripts/pm-server.mjs +2 -1
- package/skills/progress-mon/scripts/pm-sources.mjs +1 -0
- package/skills/progress-mon/scripts/progress-mon.mjs +3 -2
- package/skills/tune-tmux/SKILL.md +235 -0
- package/skills/tune-tmux/scripts/app-notes.mjs +38 -0
- package/skills/tune-tmux/scripts/marker-block.mjs +311 -0
- package/skills/tune-tmux/scripts/psmux-caps.json +71 -0
- package/skills/tune-tmux/scripts/tmux-config.mjs +251 -0
- package/skills/tune-tmux/scripts/tune-tmux.mjs +284 -0
- package/skills/ultra-chain/SKILL.md +52 -22
- package/skills/ultra-chain/references/chain-ledger.mjs +93 -11
- package/skills/ultra-chain/references/chain-report.mjs +13 -2
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* git-init: makes the first commit of a folder that is not a git repository yet, so worktrees and other parallel
|
|
4
|
+
* work can start from it. Writes a .gitignore that keeps secrets and local state out, scans what would be
|
|
5
|
+
* tracked for secrets, then runs `git init`, `git add .` and `git commit -m "init"`.
|
|
6
|
+
*
|
|
7
|
+
* node git-init.mjs [check|run] [--root <dir>] [--branch <name>] [--local-identity "<name> <<mail>>"] [--nested]
|
|
8
|
+
* [--accept <file:line:kind>]... [--follow-symlink] [--json]
|
|
9
|
+
*
|
|
10
|
+
* - check (the default) writes nothing: it shows the .gitignore block, the number of files that would be tracked
|
|
11
|
+
* and the scan result.
|
|
12
|
+
* - The .gitignore is written new, or the missing patterns go into a `# banker:git-init:start` .. `:end` block at
|
|
13
|
+
* its end through marker-block.mjs (links refused unless --follow-symlink, first original kept in
|
|
14
|
+
* .gitignore.git-init.bak).
|
|
15
|
+
* - The files to track are listed with an empty temp repository (`--git-dir` in the temp folder, `--work-tree`
|
|
16
|
+
* the root), so the root gets no .git before the scan passes.
|
|
17
|
+
* - Names that hold keys (.env, *.pem, .npmrc, ...) block the commit and cannot be accepted. Content hits from
|
|
18
|
+
* payload-scan.mjs are reported as file:line:kind only, never the value; --accept passes one exact
|
|
19
|
+
* file:line:kind the user confirmed as a false hit.
|
|
20
|
+
* - After `git add .` the staged names must equal the scanned list, or nothing is committed.
|
|
21
|
+
*
|
|
22
|
+
* Exit status: 0 committed (or check found nothing), 1 scan hits, a write refused or a git step failed, 2 usage
|
|
23
|
+
* error, 3 precondition (no git, root is HOME or a file system root, GIT_DIR or GIT_WORK_TREE set, already in a
|
|
24
|
+
* repository, a .git below the root, no commit identity). No network is used.
|
|
25
|
+
*/
|
|
26
|
+
import { spawnSync } from "node:child_process";
|
|
27
|
+
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { homedir, tmpdir } from "node:os";
|
|
29
|
+
import { join, parse, resolve, sep } from "node:path";
|
|
30
|
+
import { pathToFileURL } from "node:url";
|
|
31
|
+
import { HASH, commitBlock, findBlock, markers, planBlock } from "./marker-block.mjs";
|
|
32
|
+
import { scan } from "./payload-scan.mjs";
|
|
33
|
+
|
|
34
|
+
const USAGE = [
|
|
35
|
+
"git-init: 민감 정보를 빼는 .gitignore 를 쓰고 비밀값을 스캔한 뒤 git init, git add ., git commit -m \"init\" 으로 첫 커밋을 만듦(check 는 계획만)",
|
|
36
|
+
"사용법: node git-init.mjs [check|run] [--root <dir>] [--branch <이름>] [--local-identity \"<이름> <<메일>>\"] [--nested]",
|
|
37
|
+
" [--accept <파일:줄:종류>]... [--follow-symlink] [--json]",
|
|
38
|
+
"종료 코드: 0 커밋 완료(check 는 걸림 없음), 1 스캔 걸림이나 쓰기 거부나 git 단계 실패, 2 사용 오류, 3 선행 조건 실패(아무것도 안 바꿈)",
|
|
39
|
+
].join("\n");
|
|
40
|
+
|
|
41
|
+
/** The .gitignore patterns git-init makes sure of. Every name NAME_RULES blocks has one, so a file blocked by name
|
|
42
|
+
* is left out by the .gitignore and stops the commit only when a user `!` pattern brings it back. */
|
|
43
|
+
export const PATTERNS = Object.freeze([
|
|
44
|
+
".env", ".env.*", "!.env.example", "*.pem", "*.key", "*.p12", "*.pfx", "*.jks", "*.keystore", "id_rsa*", "id_ecdsa*",
|
|
45
|
+
"id_ed25519*", "credentials.json", "*.tfstate", "*.tfstate.*", ".aws/", ".ssh/", ".omc/", ".omx/", "node_modules/",
|
|
46
|
+
".npmrc", ".netrc", "_netrc", ".pgpass", ".DS_Store", "Thumbs.db", ".gitignore.git-init.bak",
|
|
47
|
+
".*.init-dir-*/",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const ID = "banker:git-init";
|
|
51
|
+
const SKILL = "git-init";
|
|
52
|
+
const BLOCK_HEAD = "# 비밀값과 로컬 상태를 커밋에서 뺌(banker git-init)";
|
|
53
|
+
const SCAN_MAX = 2 * 1024 * 1024;
|
|
54
|
+
const WARN_BYTES = 100 * 1024 * 1024;
|
|
55
|
+
const COMMIT_TIMEOUT_MS = 120000;
|
|
56
|
+
const STRIP_ENV = ["GIT_INDEX_FILE", "GIT_OBJECT_DIRECTORY", "GIT_ALTERNATE_OBJECT_DIRECTORIES", "GIT_COMMON_DIR", "GIT_NAMESPACE", "GIT_PREFIX"];
|
|
57
|
+
const FLAGS = { "--nested": "nested", "--follow-symlink": "follow", "--json": "json", "--help": "help", "-h": "help" };
|
|
58
|
+
const VALUES = { "--root": "root", "--branch": "branch", "--local-identity": "identity" };
|
|
59
|
+
const SUBCOMMANDS = new Set(["check", "run"]);
|
|
60
|
+
const ACCEPT_RE = /^(.+):([1-9]\d*):([a-z0-9-]+)$/;
|
|
61
|
+
const IDENTITY_RE = /^\s*([^<>\r\n]*[^<>\s])\s*<([^<>\s]+@[^<>\s]+)>\s*$/;
|
|
62
|
+
// File names that hold keys or credentials: blocked by name, whatever their content.
|
|
63
|
+
const NAME_RULES = [
|
|
64
|
+
/^\.env$/i, /^\.env\..+/i, /\.(?:pem|key|p12|pfx|jks|keystore)$/i, /^id_(?:rsa|ecdsa|ed25519)(?!.*\.pub$)/i,
|
|
65
|
+
/^credentials\.json$/i, /\.tfstate(?:\..+)?$/i, /^\.npmrc$/i, /^[._]netrc$/i, /^\.pgpass$/i,
|
|
66
|
+
];
|
|
67
|
+
const fail = (code, error, report) => ({ code, error, report });
|
|
68
|
+
|
|
69
|
+
function splitArg(a) {
|
|
70
|
+
const eq = a.indexOf("=");
|
|
71
|
+
return a.startsWith("--") && eq > 0 ? [a.slice(0, eq), a.slice(eq + 1)] : [a, undefined];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Stores the value of option name (inline or the next argv item); returns the index of its last item, or { error }.
|
|
75
|
+
function takeValue(o, name, inline, argv, i) {
|
|
76
|
+
const value = inline ?? argv[i + 1];
|
|
77
|
+
if (!value) return { error: `${name} 에 값이 없음` };
|
|
78
|
+
if (name === "--accept") o.accept.push(value);
|
|
79
|
+
else o[VALUES[name]] = value;
|
|
80
|
+
return inline === undefined ? i + 1 : i;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The options in argv, or { error } (exit 2). --accept may be given more than once. */
|
|
84
|
+
function parseArgs(argv) {
|
|
85
|
+
const o = { cmd: "check", root: process.cwd(), branch: null, identity: null, nested: false, accept: [], follow: false, json: false, help: false };
|
|
86
|
+
for (let i = 0; i < argv.length; i++) {
|
|
87
|
+
const [name, inline] = splitArg(argv[i]);
|
|
88
|
+
if (FLAGS[name] && inline === undefined) {
|
|
89
|
+
o[FLAGS[name]] = true;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (VALUES[name] || name === "--accept") {
|
|
93
|
+
const next = takeValue(o, name, inline, argv, i);
|
|
94
|
+
if (next.error) return next;
|
|
95
|
+
i = next;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (i === 0 && SUBCOMMANDS.has(name)) o.cmd = name;
|
|
99
|
+
else return { error: `모르는 인자: ${argv[i]}` };
|
|
100
|
+
}
|
|
101
|
+
return o;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** { name, email } from "<name> <<mail>>", or null. */
|
|
105
|
+
function parseIdentity(text) {
|
|
106
|
+
const m = IDENTITY_RE.exec(text);
|
|
107
|
+
return m ? { name: m[1], email: m[2] } : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function validate(o) {
|
|
111
|
+
const bad = o.accept.find((a) => !ACCEPT_RE.test(a));
|
|
112
|
+
if (bad) return `--accept 는 <파일>:<줄>:<종류> 꼴: ${bad}`;
|
|
113
|
+
if (o.identity !== null && !parseIdentity(o.identity)) return `--local-identity 는 "<이름> <<메일>>" 꼴: ${o.identity}`;
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function gitEnv(extra) {
|
|
118
|
+
const env = { ...process.env, ...extra };
|
|
119
|
+
for (const k of STRIP_ENV) delete env[k];
|
|
120
|
+
return env;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Runs git; never throws. ok is false when git could not start or exited non-zero.
|
|
124
|
+
function git(args, cwd, { env, timeout } = {}) {
|
|
125
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf8", env: gitEnv(env), timeout, killSignal: "SIGKILL", maxBuffer: 256 * 1024 * 1024, windowsHide: true });
|
|
126
|
+
return { ok: !r.error && r.status === 0, out: r.stdout ?? "", err: (r.stderr ?? "").trim(), error: r.error };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const errText = (r) => r.err || r.error?.message;
|
|
130
|
+
|
|
131
|
+
const real = (p) => {
|
|
132
|
+
try {
|
|
133
|
+
return realpathSync(p);
|
|
134
|
+
} catch {
|
|
135
|
+
return resolve(p);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// The root folder, or a failure: missing (2), HOME or a file system root (3).
|
|
140
|
+
function checkRoot(rootArg) {
|
|
141
|
+
const root = resolve(rootArg);
|
|
142
|
+
if (!statSync(root, { throwIfNoEntry: false })?.isDirectory()) return fail(2, `루트 폴더가 없음: ${root}`);
|
|
143
|
+
const r = real(root);
|
|
144
|
+
if (r === real(homedir())) return fail(3, `루트가 HOME 이라 거부: ${root}. 프로젝트 폴더에서 실행하거나 --root 로 지정`);
|
|
145
|
+
if (parse(r).root === r) return fail(3, `루트가 파일 시스템 루트라 거부: ${root}`);
|
|
146
|
+
return { root };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** [major, minor] of `git version x.y.z`, or null. */
|
|
150
|
+
function gitVersion(text) {
|
|
151
|
+
const m = /(\d+)\.(\d+)/.exec(text);
|
|
152
|
+
return m ? [Number(m[1]), Number(m[2])] : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Whether root or a folder above it is already a work tree: a failure (3), { parent } with --nested, or {}.
|
|
156
|
+
function existingRepo(root, nested) {
|
|
157
|
+
const dotgit = join(root, ".git");
|
|
158
|
+
if (lstatSync(dotgit, { throwIfNoEntry: false })) return fail(3, `이미 저장소: ${dotgit} 이 있음. git-init 은 저장소가 없는 폴더에서만 실행`);
|
|
159
|
+
const r = git(["rev-parse", "--show-toplevel"], root, { env: { LC_ALL: "C", LANGUAGE: "C" } });
|
|
160
|
+
if (!r.ok) return /not a git repository/i.test(r.err) ? {} : fail(3, `저장소 여부를 확인하지 못함: ${r.err || r.error?.message}`);
|
|
161
|
+
const top = r.out.trim();
|
|
162
|
+
if (real(top) === real(root)) return fail(3, `이미 저장소 안: ${top}`);
|
|
163
|
+
if (!nested) return fail(3, `상위 폴더가 저장소: ${top}. 그 안에 따로 저장소를 만들려면 --nested`);
|
|
164
|
+
return { parent: top };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// git runs and no GIT_DIR, GIT_WORK_TREE redirects it: { version } or a failure (3).
|
|
168
|
+
function gitReady() {
|
|
169
|
+
const v = git(["--version"], tmpdir());
|
|
170
|
+
if (!v.ok) return fail(3, `git 을 실행하지 못함: ${v.error?.code ?? v.err}. git 을 설치한 뒤 다시 실행`);
|
|
171
|
+
for (const k of ["GIT_DIR", "GIT_WORK_TREE"]) if (process.env[k]) return fail(3, `${k} 환경 변수가 있어 거부. 그 변수를 지운 셸에서 다시 실행`);
|
|
172
|
+
return { version: gitVersion(v.out) };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// A failure (2) when --branch is given and git does not take it as a branch name, else null.
|
|
176
|
+
function branchProblem(branch) {
|
|
177
|
+
if (branch === null) return null;
|
|
178
|
+
const b = git(["check-ref-format", "--branch", branch], tmpdir());
|
|
179
|
+
return !b.ok || b.out.trim() !== branch ? fail(2, `브랜치 이름으로 쓸 수 없음: ${branch}`) : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Everything checked before any temp folder or file is made: { root, version, parent } or a failure.
|
|
183
|
+
function preflight(o) {
|
|
184
|
+
const g = gitReady();
|
|
185
|
+
if (g.error) return g;
|
|
186
|
+
const at = checkRoot(o.root);
|
|
187
|
+
if (at.error) return at;
|
|
188
|
+
const bad = branchProblem(o.branch);
|
|
189
|
+
if (bad) return bad;
|
|
190
|
+
const repo = existingRepo(at.root, o.nested);
|
|
191
|
+
if (repo.error) return repo;
|
|
192
|
+
return { root: at.root, version: g.version, parent: repo.parent ?? null };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function identityProblem(tmp, o) {
|
|
196
|
+
if (o.identity !== null) return null;
|
|
197
|
+
const missing = ["user.name", "user.email"].filter((k) => !git(["config", "--get", k], tmp).out.trim());
|
|
198
|
+
if (missing.length === 0) return null;
|
|
199
|
+
return fail(3, `커밋 신원 없음(${missing.join(", ")} 미설정). 이 저장소에만 두려면 --local-identity "<이름> <<메일>>". 모든 저장소에 두려면 사용자가 git config --global user.name "<이름>" 과 git config --global user.email "<메일>" 실행(git-init 은 실행하지 않음)`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** The PATTERNS not already a line of text outside git-init's own block. */
|
|
203
|
+
export function missingPatterns(text) {
|
|
204
|
+
const lines = text.replace(/^\u{FEFF}/u, "").split(/(?<=\n)/);
|
|
205
|
+
const found = findBlock(lines, markers(HASH, ID));
|
|
206
|
+
const outside = found && !found.error ? [...lines.slice(0, found.first), ...lines.slice(found.last + 1)] : lines;
|
|
207
|
+
const have = new Set(outside.map((l) => l.replace(/\r?\n$/, "").trimEnd()));
|
|
208
|
+
return PATTERNS.filter((p) => !have.has(p));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// The .gitignore plan: { plan, missing } or { refuse }. A first plan with no body only reads the file.
|
|
212
|
+
function gitignorePlan(root, follow) {
|
|
213
|
+
const path = join(root, ".gitignore");
|
|
214
|
+
const opts = { comment: HASH, id: ID, skill: SKILL, followSymlink: follow };
|
|
215
|
+
const probe = planBlock(path, { ...opts, body: null });
|
|
216
|
+
if (!probe.ok) return { refuse: probe };
|
|
217
|
+
const missing = missingPatterns(probe.before === null ? "" : probe.before.toString("utf8"));
|
|
218
|
+
const plan = planBlock(path, { ...opts, body: missing.length > 0 ? [BLOCK_HEAD, ...missing].join("\n") : null });
|
|
219
|
+
return plan.ok ? { plan, missing, existed: probe.before !== null } : { refuse: plan };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// The untracked, not ignored paths under root as git sees them through the temp repository; excludes go into
|
|
223
|
+
// its info/exclude. A nested repository shows as one entry ending in "/".
|
|
224
|
+
function listCandidates(tmp, root, excludes) {
|
|
225
|
+
mkdirSync(join(tmp, ".git", "info"), { recursive: true });
|
|
226
|
+
writeFileSync(join(tmp, ".git", "info", "exclude"), excludes.map((p) => `${p}\n`).join(""));
|
|
227
|
+
const r = git(["--git-dir", join(tmp, ".git"), "--work-tree", root, "ls-files", "-o", "--exclude-standard", "-z"], root);
|
|
228
|
+
if (!r.ok) throw new Error(`추적 후보 목록을 구하지 못함: ${r.err || r.error?.message}`);
|
|
229
|
+
return r.out.split("\0").filter(Boolean).sort();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Whether the path (forward slashes, relative to the root) names a key or credential file. */
|
|
233
|
+
export function nameHit(rel) {
|
|
234
|
+
const parts = rel.split("/");
|
|
235
|
+
const base = parts[parts.length - 1];
|
|
236
|
+
if (parts.slice(0, -1).some((d) => /^\.(?:aws|ssh)$/i.test(d))) return true;
|
|
237
|
+
if (/^\.env\.example$/i.test(base)) return false;
|
|
238
|
+
return NAME_RULES.some((re) => re.test(base));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// One file: { hits } for a text file of 2MB or less, otherwise why its content was not read.
|
|
242
|
+
function inspectFile(root, rel) {
|
|
243
|
+
const st = lstatSync(join(root, rel), { throwIfNoEntry: false });
|
|
244
|
+
if (!st?.isFile()) return {};
|
|
245
|
+
const large = st.size > WARN_BYTES;
|
|
246
|
+
if (st.size > SCAN_MAX) return { unscanned: true, large };
|
|
247
|
+
const bytes = readFileSync(join(root, rel));
|
|
248
|
+
if (bytes.subarray(0, 8192).includes(0)) return { binary: true };
|
|
249
|
+
return { hits: scan(bytes.toString("utf8")).hits };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Scans the listed files: name hits, content hits (file:line:kind), accepted ones, unscanned, binary and large files. */
|
|
253
|
+
function scanFiles(root, list, accept) {
|
|
254
|
+
const r = { names: [], hits: [], accepted: [], unscanned: [], large: [], binaries: [] };
|
|
255
|
+
const wanted = new Set(accept);
|
|
256
|
+
for (const rel of list.filter((p) => !p.endsWith("/"))) {
|
|
257
|
+
if (nameHit(rel)) r.names.push(rel);
|
|
258
|
+
const f = inspectFile(root, rel);
|
|
259
|
+
if (f.large) r.large.push(rel);
|
|
260
|
+
if (f.unscanned) r.unscanned.push(rel);
|
|
261
|
+
if (f.binary) r.binaries.push(rel);
|
|
262
|
+
for (const h of f.hits ?? []) {
|
|
263
|
+
const where = `${rel}:${h.line}:${h.kind}`;
|
|
264
|
+
(wanted.has(where) ? r.accepted : r.hits).push(where);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
r.unmatched = accept.filter((a) => !r.accepted.includes(a));
|
|
268
|
+
return r;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const blocked = (s) => s.names.length > 0 || s.hits.length > 0;
|
|
272
|
+
|
|
273
|
+
function baseReport(o, pre, branch, gi) {
|
|
274
|
+
return {
|
|
275
|
+
ok: true, command: o.cmd, root: pre.root, branch, parent: pre.parent,
|
|
276
|
+
gitignore: { path: join(pre.root, ".gitignore"), status: gi.plan.changed ? (gi.existed ? "update" : "create") : "unchanged", added: gi.missing },
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function withScan(report, list, s) {
|
|
281
|
+
return { ...report, ok: !blocked(s), candidates: list.filter((p) => !p.endsWith("/")).length, nameHits: s.names, hits: s.hits, accepted: s.accepted, unmatchedAccepts: s.unmatched, unscanned: s.unscanned, large: s.large, binary: s.binaries.length, binaries: s.binaries };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function check(o, ctx) {
|
|
285
|
+
const list = [...ctx.planned];
|
|
286
|
+
if (!ctx.gi.existed && ctx.gi.plan.changed && !list.includes(".gitignore")) list.push(".gitignore");
|
|
287
|
+
const report = withScan(ctx.report, list, scanFiles(ctx.root, list, o.accept));
|
|
288
|
+
return report.ok ? { code: 0, report } : fail(1, "걸림이 있어 run 하면 git init 하지 않고 멈춤", report);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function initRepo(root, branch, version, identity) {
|
|
292
|
+
const modern = version && (version[0] > 2 || (version[0] === 2 && version[1] >= 28));
|
|
293
|
+
const steps = [modern ? ["init", "-q", "-b", branch] : ["init", "-q"]];
|
|
294
|
+
if (!modern) steps.push(["symbolic-ref", "HEAD", `refs/heads/${branch}`]);
|
|
295
|
+
const who = identity === null ? null : parseIdentity(identity);
|
|
296
|
+
if (who) steps.push(["config", "--local", "user.name", who.name], ["config", "--local", "user.email", who.email]);
|
|
297
|
+
for (const args of steps) {
|
|
298
|
+
const r = git(args, root);
|
|
299
|
+
if (!r.ok) return `git ${args[0]} 실패: ${errText(r)}`;
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// git add . and the staged names compared with the scanned list: null when they match, else why not.
|
|
305
|
+
function stage(root, list) {
|
|
306
|
+
const add = git(["add", "."], root);
|
|
307
|
+
if (!add.ok) return `git add 실패: ${errText(add)}`;
|
|
308
|
+
const d = git(["diff", "--cached", "--name-only", "--no-renames", "-z"], root);
|
|
309
|
+
if (!d.ok) return `git diff --cached 실패: ${errText(d)}`;
|
|
310
|
+
const staged = d.out.split("\0").filter(Boolean);
|
|
311
|
+
const onlyStaged = staged.filter((p) => !list.includes(p));
|
|
312
|
+
const onlyList = list.filter((p) => !staged.includes(p));
|
|
313
|
+
if (onlyStaged.length === 0 && onlyList.length === 0) return null;
|
|
314
|
+
return `스테이지된 파일이 스캔한 목록과 다름(스테이지에만: ${onlyStaged.slice(0, 20).join(", ") || "없음"} / 목록에만: ${onlyList.slice(0, 20).join(", ") || "없음"}). 커밋하지 않음`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function commitInit(root) {
|
|
318
|
+
const c = git(["commit", "-q", "-m", "init"], root, { timeout: COMMIT_TIMEOUT_MS });
|
|
319
|
+
if (c.error?.code === "ETIMEDOUT") return { error: `git commit 이 ${COMMIT_TIMEOUT_MS / 1000}초 안에 끝나지 않아 멈춤(커밋 훅 확인)` };
|
|
320
|
+
if (!c.ok) return { error: `git commit 실패: ${c.err || c.error?.message}` };
|
|
321
|
+
const head = git(["rev-parse", "HEAD"], root).out.trim();
|
|
322
|
+
const tracked = git(["ls-files", "-z"], root).out.split("\0").filter(Boolean).length;
|
|
323
|
+
return { head, tracked };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function runSteps(o, ctx) {
|
|
327
|
+
const done = commitBlock(ctx.gi.plan, SKILL);
|
|
328
|
+
const report = { ...ctx.report, gitignore: { ...ctx.report.gitignore, status: done.changed ? (done.created ? "created" : "updated") : "unchanged", backup: done.backup ?? null } };
|
|
329
|
+
const list = listCandidates(ctx.tmp, ctx.root, []);
|
|
330
|
+
const nested = list.filter((p) => p.endsWith("/"));
|
|
331
|
+
if (nested.length > 0) return fail(1, `하위 폴더에 .git 이 있음(넣으면 gitlink 가 됨): ${nested.join(", ")}. git init 하지 않음`, { ...report, ok: false });
|
|
332
|
+
const scanned = withScan(report, list, scanFiles(ctx.root, list, o.accept));
|
|
333
|
+
if (!scanned.ok) return fail(1, "걸림이 있어 git init 하지 않음. .gitignore 변경은 남김", scanned);
|
|
334
|
+
const why = initRepo(ctx.root, ctx.branch, ctx.version, o.identity) ?? stage(ctx.root, list);
|
|
335
|
+
if (why) return fail(1, `${why}. ${join(ctx.root, ".git")} 은 남아 있음`, { ...scanned, ok: false });
|
|
336
|
+
const c = commitInit(ctx.root);
|
|
337
|
+
if (c.error) return fail(1, `${c.error}. ${join(ctx.root, ".git")} 은 남아 있음`, { ...scanned, ok: false });
|
|
338
|
+
return { code: 0, report: { ...scanned, commit: c.head, tracked: c.tracked } };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Work that needs the temp repository; the caller removes it.
|
|
342
|
+
function withTemp(o, pre, tmp) {
|
|
343
|
+
const made = git(["init", "-q", tmp], tmpdir());
|
|
344
|
+
if (!made.ok) throw new Error(`임시 저장소를 만들지 못함: ${errText(made)}`);
|
|
345
|
+
if (real(tmp).startsWith(real(pre.root) + sep)) return fail(3, `임시 폴더가 루트 안에 있음: ${tmp}. TMPDIR 을 루트 밖으로 바꿔 다시 실행`);
|
|
346
|
+
const who = identityProblem(tmp, o);
|
|
347
|
+
if (who) return who;
|
|
348
|
+
const gi = gitignorePlan(pre.root, o.follow);
|
|
349
|
+
if (gi.refuse) return fail(1, `.gitignore 거부: ${gi.refuse.message}. 아무것도 바꾸지 않음`);
|
|
350
|
+
const planned = listCandidates(tmp, pre.root, gi.missing);
|
|
351
|
+
const nested = planned.filter((p) => p.endsWith("/"));
|
|
352
|
+
if (nested.length > 0) return fail(3, `하위 폴더에 .git 이 있음(넣으면 gitlink 가 됨): ${nested.join(", ")}. 아무것도 바꾸지 않음`);
|
|
353
|
+
const branch = o.branch ?? (git(["config", "--get", "init.defaultBranch"], tmp).out.trim() || "main");
|
|
354
|
+
const ctx = { root: pre.root, version: pre.version, tmp, gi, planned, branch, report: baseReport(o, pre, branch, gi) };
|
|
355
|
+
return o.cmd === "check" ? check(o, ctx) : runSteps(o, ctx);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function execute(o) {
|
|
359
|
+
const pre = preflight(o);
|
|
360
|
+
if (pre.error) return pre;
|
|
361
|
+
const tmp = mkdtempSync(join(tmpdir(), "banker-git-init-"));
|
|
362
|
+
try {
|
|
363
|
+
return withTemp(o, pre, tmp);
|
|
364
|
+
} finally {
|
|
365
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const GITIGNORE_WORDS = { create: "새로 씀(예정)", update: "끝에 표지 블록(예정)", created: "새로 씀", updated: "끝에 표지 블록 씀", unchanged: "변경 없음" };
|
|
370
|
+
|
|
371
|
+
const listLine = (label, items) => (items.length > 0 ? [`- ${label}: ${items.join(", ")}`] : []);
|
|
372
|
+
|
|
373
|
+
function scanLines(rep) {
|
|
374
|
+
return [
|
|
375
|
+
`- 추적 후보: ${rep.candidates}개`,
|
|
376
|
+
...listLine("이름 걸림(--accept 로 넘길 수 없음, .gitignore 에 넣거나 옮김)", rep.nameHits),
|
|
377
|
+
...listLine("내용 걸림(파일:줄:종류)", rep.hits),
|
|
378
|
+
...listLine("확인으로 넘긴 항목", rep.accepted),
|
|
379
|
+
...listLine("맞는 걸림이 없어 쓰이지 않은 --accept", rep.unmatchedAccepts),
|
|
380
|
+
...listLine("내용 미스캔(2MB 초과)", rep.unscanned),
|
|
381
|
+
...listLine("내용 미스캔(바이너리)", rep.binaries),
|
|
382
|
+
...listLine("경고: 100MB 초과(원격 저장소가 거부할 수 있음)", rep.large),
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function resultLines(rep, code) {
|
|
387
|
+
if (rep.commit) {
|
|
388
|
+
return [`- 커밋: ${rep.commit} init`, `- 추적 파일: ${rep.tracked}개`, "", `준비 완료: git worktree add <경로> -b <새 브랜치> 로 병렬 작업을 시작할 수 있음`, "이후 런타임 잡음 정리는 refresh-git-ignore"];
|
|
389
|
+
}
|
|
390
|
+
if (rep.command === "check") return ["", code === 0 ? "결과: 걸림 없음. run 으로 진행 가능" : "결과: 걸림이 있어 run 하면 멈춤"];
|
|
391
|
+
return [];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function textLines(rep, code) {
|
|
395
|
+
const gi = rep.gitignore;
|
|
396
|
+
return [
|
|
397
|
+
`git-init ${rep.command}${rep.command === "check" ? " (계획만, 바꾸지 않음)" : ""}: ${rep.root}`,
|
|
398
|
+
...(rep.parent ? [`- 상위 저장소 안에 따로 만듦(--nested): ${rep.parent}`] : []),
|
|
399
|
+
`- 브랜치: ${rep.branch}`,
|
|
400
|
+
`- .gitignore: ${GITIGNORE_WORDS[gi.status]}${gi.added.length > 0 && gi.status !== "unchanged" ? `, 패턴 ${gi.added.length}개: ${gi.added.join(" ")}` : ""}${gi.backup ? `, 첫 원본 ${gi.backup}` : ""}`,
|
|
401
|
+
...(rep.candidates === undefined ? [] : scanLines(rep)),
|
|
402
|
+
...resultLines(rep, code),
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// The options checked, or { error } for a usage error.
|
|
407
|
+
function options(argv) {
|
|
408
|
+
const o = parseArgs(argv);
|
|
409
|
+
if (o.error || o.help) return o;
|
|
410
|
+
const bad = validate(o);
|
|
411
|
+
return bad ? { ...o, error: bad } : o;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Runs git-init with argv; returns { code, stdout, stderr } and writes nothing to the console. */
|
|
415
|
+
export function run(argv) {
|
|
416
|
+
const o = options(argv);
|
|
417
|
+
if (o.help) return { code: 0, stdout: USAGE, stderr: "" };
|
|
418
|
+
if (o.error) return finish(o, fail(2, `${o.error}\n${USAGE}`));
|
|
419
|
+
try {
|
|
420
|
+
return finish(o, execute(o));
|
|
421
|
+
} catch (e) {
|
|
422
|
+
return finish(o, fail(1, `실패: ${e.message}`));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function finish(o, r) {
|
|
427
|
+
const stderr = r.error ? `git-init: ${r.error}` : "";
|
|
428
|
+
if (o.json) {
|
|
429
|
+
const body = r.report ? { ...r.report, ok: r.code === 0 } : { ok: false, command: o.cmd };
|
|
430
|
+
return { code: r.code, stdout: JSON.stringify({ ...body, exit: r.code, ...(r.error ? { error: r.error.split("\n")[0] } : {}) }), stderr };
|
|
431
|
+
}
|
|
432
|
+
return { code: r.code, stdout: r.report ? textLines(r.report, r.code).join("\n") : "", stderr };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(real(process.argv[1])).href) {
|
|
436
|
+
const r = run(process.argv.slice(2));
|
|
437
|
+
if (r.stdout) process.stdout.write(`${r.stdout}\n`);
|
|
438
|
+
if (r.stderr) process.stderr.write(`${r.stderr}\n`);
|
|
439
|
+
process.exitCode = r.code;
|
|
440
|
+
}
|