@kaydash9999/banker-plugins 0.24.0 → 0.25.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 +49 -0
- package/README.md +5 -4
- package/bin/lib/codex-hooks.mjs +5 -1
- package/codex/hooks.json +4 -4
- package/codex/manifest.json +2 -1
- package/codex/skills/teamwork/scripts/teamwork.mjs +21 -11
- package/codex/transform-matrix.md +1 -0
- package/hooks/telemetry-count-codex.mjs +3 -2
- package/package.json +1 -1
- package/skills/anti-drm/SKILL.md +2 -2
- package/skills/anti-drm/scripts/anti-drm.mjs +8 -5
- package/skills/anti-drm/scripts/instruction-toggle.mjs +6 -3
- package/skills/build-testbox/scripts/browser-cell.mjs +10 -2
- package/skills/build-testbox/scripts/containers.mjs +8 -3
- package/skills/build-testbox/scripts/proc.mjs +32 -8
- package/skills/build-testbox/scripts/testbox.mjs +10 -2
- package/skills/git-init/scripts/git-init.mjs +53 -5
- package/skills/git-init/scripts/marker-block.mjs +144 -22
- package/skills/init-dir/SKILL.md +1 -1
- package/skills/init-dir/scripts/init-dir.mjs +29 -2
- package/skills/init-dir/scripts/marker-block.mjs +144 -22
- package/skills/init-dir/templates/scripts/README.md +4 -1
- package/skills/init-dir/templates/scripts/lib/service.mjs +140 -33
- package/skills/lineage/SKILL.md +1 -1
- package/skills/lineage/lineage.py +11 -11
- package/skills/motion-sites/SKILL.md +79 -0
- package/skills/motion-sites/scripts/motion-sites.mjs +331 -0
- package/skills/omc-patch/scripts/omc-patch.mjs +2 -1
- package/skills/payload-mon/scripts/payload-mon.mjs +2 -1
- package/skills/payload-mon/scripts/payload-size.mjs +2 -2
- package/skills/progress-mon/README.md +4 -3
- package/skills/progress-mon/SKILL.md +1 -1
- package/skills/progress-mon/references/adr.md +7 -0
- package/skills/progress-mon/scripts/pm-adr-view.mjs +101 -0
- package/skills/progress-mon/scripts/pm-adr.mjs +38 -105
- package/skills/progress-mon/scripts/pm-check.mjs +16 -7
- package/skills/progress-mon/scripts/pm-config.mjs +133 -7
- package/skills/progress-mon/scripts/pm-data.mjs +35 -29
- package/skills/progress-mon/scripts/pm-memory.mjs +14 -7
- package/skills/progress-mon/scripts/pm-render.mjs +1 -1
- package/skills/progress-mon/scripts/pm-server.mjs +3 -3
- package/skills/progress-mon/scripts/pm-sources.mjs +12 -2
- package/skills/remains/SKILL.md +30 -3
- package/skills/remains/scripts/boxes.mjs +14 -4
- package/skills/setup-bypass-permissions/scripts/bypass-permissions.mjs +2 -1
- package/skills/setup-bypass-permissions/scripts/codex-config.mjs +12 -7
- package/skills/setup-omc-hud/assets/apply-hud.mjs +2 -1
- package/skills/setup-omc-hud/assets/codex-config.mjs +12 -7
- package/skills/setup-omc-hud/scripts/claude-update-last.mjs +2 -1
- package/skills/smart-compact/scripts/codex-gate.mjs +25 -4
- package/skills/tone-compact/scripts/instruction-toggle.mjs +6 -3
- package/skills/tune-tmux/scripts/marker-block.mjs +144 -22
- package/skills/tune-tmux/scripts/tmux-config.mjs +35 -3
- package/skills/tune-tmux/scripts/tune-tmux.mjs +15 -9
- package/skills/ultra-chain/references/chain-ledger.mjs +8 -1
- package/skills/vertical-pptx/verify_a4p.mjs +11 -4
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* motion-sites: the first-run setup and the lookup behind /motion-sites, a short name for MotionSites' own
|
|
4
|
+
* motionsites-web-builder skill (https://github.com/Motionsites-AI/claude-skill). Nothing of that skill is copied
|
|
5
|
+
* here: its examples and media may not be redistributed, so the user's own Claude Code or Codex installs it from
|
|
6
|
+
* MotionSites' marketplace, as its README says.
|
|
7
|
+
*
|
|
8
|
+
* status --runtime claude|codex JSON: the plugin, its SKILL.md, a Python with Playwright, Chromium; ready or not
|
|
9
|
+
* setup --runtime claude|codex installs what is missing, in order, then answers as status
|
|
10
|
+
* path --runtime claude|codex the installed SKILL.md (exit 1 when not installed)
|
|
11
|
+
*
|
|
12
|
+
* setup runs, for Claude Code: claude plugin marketplace add Motionsites-AI/claude-skill, then claude plugin install
|
|
13
|
+
* motionsites-web-builder@motionsites; for Codex: codex plugin marketplace add Motionsites-AI/claude-skill, then
|
|
14
|
+
* codex plugin add motionsites-web-builder@motionsites. Then <python> -m pip install playwright and <python> -m
|
|
15
|
+
* playwright install chromium. Where pip refuses the system Python (an externally managed environment), Playwright
|
|
16
|
+
* goes into a virtual environment of its own under the cache folder (MOTION_SITES_HOME, else
|
|
17
|
+
* ~/.cache/banker/motion-sites, %LOCALAPPDATA%\banker\motion-sites on Windows), and status names that Python.
|
|
18
|
+
*
|
|
19
|
+
* Every step has a time limit, so with no network a step fails with its reason instead of waiting. Exit status:
|
|
20
|
+
* 0 ready (status, setup) or found (path), 1 not ready or a step failed, 2 usage.
|
|
21
|
+
*/
|
|
22
|
+
import { spawnSync } from "node:child_process";
|
|
23
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
24
|
+
import { homedir, tmpdir } from "node:os";
|
|
25
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
|
|
28
|
+
// The folder programs start in: the home folder (Windows looks for a program in the working folder first, so a
|
|
29
|
+
// program planted in the folder worked on never runs), or the temp folder when there is no home folder.
|
|
30
|
+
const runDir = () => (existsSync(homedir()) ? homedir() : tmpdir());
|
|
31
|
+
|
|
32
|
+
export const REPO = "Motionsites-AI/claude-skill";
|
|
33
|
+
export const PLUGIN = "motionsites-web-builder";
|
|
34
|
+
export const MARKET = "motionsites";
|
|
35
|
+
const ID = `${PLUGIN}@${MARKET}`;
|
|
36
|
+
const WIN = process.platform === "win32";
|
|
37
|
+
const STEP_MS = 10 * 60 * 1000; // a Chromium download can take minutes
|
|
38
|
+
const LOOK_MS = 60 * 1000;
|
|
39
|
+
|
|
40
|
+
const USAGE = `motion-sites: MotionSites 의 motionsites-web-builder 스킬 설치와 위치 확인
|
|
41
|
+
사용법: node motion-sites.mjs status|setup|path --runtime claude|codex`;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Runs a program with fixed arguments: { ok, stdout, out (stdout and stderr), why }. Data (JSON, paths) is read from
|
|
45
|
+
* stdout only, so a warning on stderr never spoils it. On Windows claude and codex are .cmd shims, which Node runs only
|
|
46
|
+
* through a shell: they go as one command line (their arguments here are fixed words). Python gets no shell.
|
|
47
|
+
*/
|
|
48
|
+
export function run(cmd, args, { timeout = LOOK_MS, env = process.env } = {}) {
|
|
49
|
+
const shim = WIN && (cmd === "claude" || cmd === "codex");
|
|
50
|
+
// From the home folder: python -c and -m put the working folder first on the import path (a pip/ or playwright/ in
|
|
51
|
+
// the project would run), and Windows looks for a program there first.
|
|
52
|
+
const opts = { cwd: runDir(), encoding: "utf8", timeout, env, windowsHide: true, maxBuffer: 64 * 1024 * 1024 };
|
|
53
|
+
const r = shim ? spawnSync([cmd, ...args].join(" "), { ...opts, shell: true }) : spawnSync(cmd, args, opts);
|
|
54
|
+
const stdout = r.stdout ?? "";
|
|
55
|
+
const out = `${stdout}${r.stderr ?? ""}`;
|
|
56
|
+
if (r.error) return { ok: false, stdout, out, why: r.error.code === "ETIMEDOUT" ? `시간 초과(${timeout / 1000}초)` : `${cmd} 실행 실패(${r.error.code})` };
|
|
57
|
+
return { ok: r.status === 0, stdout, out, why: r.status === 0 ? null : `${cmd} ${args.join(" ")} 종료 ${r.status}: ${lastLine(out)}` };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const PROXY_KEYS = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy", "ALL_PROXY", "all_proxy"];
|
|
61
|
+
const PROBE = "const s=require('net').connect({host:process.argv[1],port:443,timeout:8000});s.on('connect',()=>process.exit(0));s.on('timeout',()=>process.exit(1));s.on('error',()=>process.exit(1))";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether a web source answers on port 443 within 8 seconds: true, false, or null (not judged: behind a proxy a
|
|
65
|
+
* direct look says nothing, so the step's own time limit decides).
|
|
66
|
+
*/
|
|
67
|
+
export function reachable(host, { env = process.env, runner = run } = {}) {
|
|
68
|
+
if (PROXY_KEYS.some((k) => env[k])) return null;
|
|
69
|
+
return runner(process.execPath, ["-e", PROBE, host], { timeout: 15000 }).ok;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const hostOf = (url, fallback) => {
|
|
73
|
+
try {
|
|
74
|
+
return new URL(url).hostname || fallback;
|
|
75
|
+
} catch {
|
|
76
|
+
return fallback;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** The hosts each install step downloads from, any one answering being enough (a mirror named in the environment first). */
|
|
81
|
+
export function sources(env = process.env) {
|
|
82
|
+
return {
|
|
83
|
+
plugin: ["github.com"],
|
|
84
|
+
pip: [hostOf(env.PIP_INDEX_URL, "pypi.org")],
|
|
85
|
+
chromium: env.PLAYWRIGHT_DOWNLOAD_HOST ? [hostOf(env.PLAYWRIGHT_DOWNLOAD_HOST, "cdn.playwright.dev")] : ["cdn.playwright.dev", "playwright.download.prss.microsoft.com"],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const GIT_MIRROR = "^(https?\\..*proxy|url\\..*insteadof)$";
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether a step's tool is set to a mirror or a proxy in its own configuration, where a direct look at the public
|
|
93
|
+
* host says nothing: git (http.proxy, url.<base>.insteadOf) for the plugin, pip config (index-url) for Playwright.
|
|
94
|
+
*/
|
|
95
|
+
export function mirrorSet(kind, { py = null, runner = run } = {}) {
|
|
96
|
+
if (kind === "plugin") {
|
|
97
|
+
const r = runner("git", ["config", "--get-regexp", GIT_MIRROR]);
|
|
98
|
+
return r.ok && (r.stdout ?? "").trim() !== "";
|
|
99
|
+
}
|
|
100
|
+
if (kind === "pip" && py) {
|
|
101
|
+
const [cmd, ...pre] = py;
|
|
102
|
+
const r = runner(cmd, [...pre, "-m", "pip", "config", "list"]);
|
|
103
|
+
return r.ok && /index-url/i.test(r.stdout ?? "");
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// A failed step when none of its sources answers, or null when one answers (or it cannot be judged: a proxy, or a
|
|
109
|
+
// mirror set in the tool's own configuration).
|
|
110
|
+
function offline(step, kind, { env, runner, py = null }) {
|
|
111
|
+
const hosts = sources(env)[kind];
|
|
112
|
+
if (mirrorSet(kind, { py, runner })) return null;
|
|
113
|
+
const seen = hosts.map((h) => reachable(h, { env, runner }));
|
|
114
|
+
if (!seen.every((x) => x === false)) return null;
|
|
115
|
+
return { step, ok: false, why: `오프라인이라 설치하지 않음(${hosts.join(", ")} 에 닿지 않음)` };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const lastLine = (text) => text.trim().split(/\r?\n/).filter(Boolean).pop() ?? "";
|
|
119
|
+
|
|
120
|
+
/** The cache folder for the virtual environment. */
|
|
121
|
+
export function cacheHome(env = process.env) {
|
|
122
|
+
if (env.MOTION_SITES_HOME) return env.MOTION_SITES_HOME;
|
|
123
|
+
const base = WIN ? env.LOCALAPPDATA || join(homedir(), "AppData", "Local") : join(homedir(), ".cache");
|
|
124
|
+
return join(base, "banker", "motion-sites");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const venvPython = (env) => join(cacheHome(env), "venv", WIN ? "Scripts" : "bin", WIN ? "python.exe" : "python");
|
|
128
|
+
|
|
129
|
+
/** The plugin for Claude Code: { file (its SKILL.md, or null), disabled (installed but turned off) }. */
|
|
130
|
+
export function claudePlugin(runner = run) {
|
|
131
|
+
const none = { file: null, disabled: false };
|
|
132
|
+
const r = runner("claude", ["plugin", "list", "--json"]);
|
|
133
|
+
if (!r.ok) return none;
|
|
134
|
+
let list;
|
|
135
|
+
try {
|
|
136
|
+
list = JSON.parse(r.stdout);
|
|
137
|
+
} catch {
|
|
138
|
+
return none;
|
|
139
|
+
}
|
|
140
|
+
const p = Array.isArray(list) ? list.find((x) => x?.id === ID) : null;
|
|
141
|
+
const file = p?.installPath ? join(p.installPath, "skills", PLUGIN, "SKILL.md") : null;
|
|
142
|
+
if (!file || !existsSync(file)) return none;
|
|
143
|
+
return p.enabled === false ? { file: null, disabled: true } : { file, disabled: false };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const claudeSkill = (runner = run) => claudePlugin(runner).file;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The plugin for Codex: { file, disabled }. Installed means its table in config.toml (the body runs to the next table
|
|
150
|
+
* header, so an array value holding "[" does not cut it short) and a version in the cache; the newest version's
|
|
151
|
+
* SKILL.md is the file. enabled = false is disabled.
|
|
152
|
+
*/
|
|
153
|
+
export function codexPlugin(env = process.env) {
|
|
154
|
+
const none = { file: null, disabled: false };
|
|
155
|
+
const home = env.CODEX_HOME || join(homedir(), ".codex");
|
|
156
|
+
let toml = "";
|
|
157
|
+
try {
|
|
158
|
+
toml = readFileSync(join(home, "config.toml"), "utf8");
|
|
159
|
+
} catch {
|
|
160
|
+
return none;
|
|
161
|
+
}
|
|
162
|
+
const head = `[plugins."${ID}"]`.replace(/[.[\]"]/g, "\\$&");
|
|
163
|
+
const table = new RegExp(`^${head}[ \\t]*\\r?\\n([\\s\\S]*?)(?=^[ \\t]*\\[|(?![\\s\\S]))`, "m").exec(toml);
|
|
164
|
+
if (!table) return none;
|
|
165
|
+
const dir = join(home, "plugins", "cache", MARKET, PLUGIN);
|
|
166
|
+
let versions = [];
|
|
167
|
+
try {
|
|
168
|
+
versions = readdirSync(dir).sort(byVersion);
|
|
169
|
+
} catch {
|
|
170
|
+
return none;
|
|
171
|
+
}
|
|
172
|
+
const file = versions.map((v) => join(dir, v, "skills", PLUGIN, "SKILL.md")).reverse().find((f) => existsSync(f));
|
|
173
|
+
if (!file) return none;
|
|
174
|
+
return /^[ \t]*enabled[ \t]*=[ \t]*false\b/m.test(table[1]) ? { file: null, disabled: true } : { file, disabled: false };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const codexSkill = (env = process.env) => codexPlugin(env).file;
|
|
178
|
+
|
|
179
|
+
export function byVersion(a, b) {
|
|
180
|
+
const pa = a.split(/[.-]/).map((x) => (/^\d+$/.test(x) ? Number(x) : x));
|
|
181
|
+
const pb = b.split(/[.-]/).map((x) => (/^\d+$/.test(x) ? Number(x) : x));
|
|
182
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
183
|
+
if (pa[i] === pb[i]) continue;
|
|
184
|
+
// 2.0.0-rc1 comes before 2.0.0 (a pre-release tail), 2.0.0.1 after it
|
|
185
|
+
if (pa[i] === undefined) return typeof pb[i] === "string" ? 1 : -1;
|
|
186
|
+
if (pb[i] === undefined) return typeof pa[i] === "string" ? -1 : 1;
|
|
187
|
+
return typeof pa[i] === "number" && typeof pb[i] === "number" ? pa[i] - pb[i] : String(pa[i]).localeCompare(String(pb[i]));
|
|
188
|
+
}
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Python programs to try, in order: MOTION_SITES_PYTHON, the cache's virtual environment, then the system's. */
|
|
193
|
+
export function pythons(env = process.env) {
|
|
194
|
+
const list = [];
|
|
195
|
+
// a relative path is made absolute here, as programs start in the home folder
|
|
196
|
+
if (env.MOTION_SITES_PYTHON) list.push([isAbsolute(env.MOTION_SITES_PYTHON) ? env.MOTION_SITES_PYTHON : resolve(env.MOTION_SITES_PYTHON)]);
|
|
197
|
+
list.push([venvPython(env)]);
|
|
198
|
+
if (WIN) list.push(["py", "-3"], ["python"]);
|
|
199
|
+
else list.push(["python3"], ["python"]);
|
|
200
|
+
return list;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const CHROMIUM = "from playwright.sync_api import sync_playwright as s\np=s().start()\nprint(p.chromium.executable_path)\np.stop()";
|
|
204
|
+
|
|
205
|
+
/** What one Python has: { bin, playwright, chromium } or null when it does not run. */
|
|
206
|
+
export function lookPython(py, runner = run) {
|
|
207
|
+
const [cmd, ...pre] = py;
|
|
208
|
+
if (/[\\/]/.test(cmd) && !existsSync(cmd)) return null;
|
|
209
|
+
const v = runner(cmd, [...pre, "-c", "import sys;print(sys.executable)"]);
|
|
210
|
+
if (!v.ok) return null;
|
|
211
|
+
const bin = lastLine(v.stdout ?? "");
|
|
212
|
+
const pw = runner(cmd, [...pre, "-c", "import playwright"]).ok;
|
|
213
|
+
const exe = pw ? runner(cmd, [...pre, "-c", CHROMIUM]) : { ok: false, stdout: "" };
|
|
214
|
+
const chrome = lastLine(exe.stdout ?? "");
|
|
215
|
+
return { bin, cmd: py, playwright: pw, chromium: exe.ok && chrome !== "" && existsSync(chrome) };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The first Python with Playwright (and Chromium, when one has both), else the first that runs. */
|
|
219
|
+
export function pickPython(env = process.env, runner = run) {
|
|
220
|
+
const seen = pythons(env).map((p) => lookPython(p, runner)).filter(Boolean);
|
|
221
|
+
return seen.find((p) => p.playwright && p.chromium) ?? seen.find((p) => p.playwright) ?? seen[0] ?? null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** status: the plugin and the Python, and whether /motion-sites can hand over now. */
|
|
225
|
+
export function status(runtime, { env = process.env, runner = run } = {}) {
|
|
226
|
+
const p = runtime === "claude" ? claudePlugin(runner) : codexPlugin(env);
|
|
227
|
+
const py = pickPython(env, runner);
|
|
228
|
+
const python = py ? { bin: py.bin, playwright: py.playwright, chromium: py.chromium } : null;
|
|
229
|
+
return { runtime, plugin: ID, skillFile: p.file, disabled: p.disabled, python, ready: Boolean(p.file && py?.playwright && py?.chromium) };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The plugin steps of one runtime: marketplace add, then install. An add that fails (the marketplace may be there
|
|
234
|
+
* already) is reported only when the install fails too.
|
|
235
|
+
*/
|
|
236
|
+
function installPlugin(runtime, env, runner) {
|
|
237
|
+
const down = offline("marketplace add", "plugin", { env, runner });
|
|
238
|
+
if (down) return [down];
|
|
239
|
+
const cli = runtime === "claude" ? "claude" : "codex";
|
|
240
|
+
const add = runner(cli, ["plugin", "marketplace", "add", REPO], { timeout: STEP_MS });
|
|
241
|
+
const verb = runtime === "claude" ? "install" : "add";
|
|
242
|
+
const inst = runner(cli, ["plugin", verb, ID], { timeout: STEP_MS });
|
|
243
|
+
if (inst.ok) return [{ step: "marketplace add", ok: true }, { step: `plugin ${verb}`, ok: true }];
|
|
244
|
+
return [{ step: "marketplace add", ok: add.ok, why: add.why }, { step: `plugin ${verb}`, ok: false, why: inst.why }];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** pip install playwright with this Python, or in a virtual environment when pip refuses the system Python. */
|
|
248
|
+
function installPlaywright(py, env, runner) {
|
|
249
|
+
const down = offline("pip install playwright", "pip", { env, runner, py: py.cmd });
|
|
250
|
+
if (down) return { ok: false, why: down.why };
|
|
251
|
+
const [cmd, ...pre] = py.cmd;
|
|
252
|
+
const pip = runner(cmd, [...pre, "-m", "pip", "install", "playwright"], { timeout: STEP_MS });
|
|
253
|
+
if (pip.ok) return { ok: true, py: py.cmd };
|
|
254
|
+
if (!/externally.managed|No module named pip/i.test(pip.out)) return { ok: false, why: pip.why };
|
|
255
|
+
const venv = runner(cmd, [...pre, "-m", "venv", join(cacheHome(env), "venv")], { timeout: STEP_MS });
|
|
256
|
+
if (!venv.ok) return { ok: false, why: `${pip.why}. 가상 환경도 만들지 못함: ${venv.why}` };
|
|
257
|
+
const inVenv = runner(venvPython(env), ["-m", "pip", "install", "playwright"], { timeout: STEP_MS });
|
|
258
|
+
return inVenv.ok ? { ok: true, py: [venvPython(env)] } : { ok: false, why: inVenv.why };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** A plugin installed but turned off: Claude Code turns it on; Codex has no command for it, so it says where. */
|
|
262
|
+
function enablePlugin(runtime, runner) {
|
|
263
|
+
if (runtime === "codex") return { step: "plugin enable", ok: false, why: `Codex 설정에서 꺼져 있음: config.toml 의 [plugins."${ID}"] 를 enabled = true 로` };
|
|
264
|
+
const r = runner("claude", ["plugin", "enable", ID]);
|
|
265
|
+
return { step: "plugin enable", ok: r.ok, why: r.why };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** The Python steps: Playwright, then Chromium. */
|
|
269
|
+
function installBrowser(env, runner) {
|
|
270
|
+
let py = pickPython(env, runner);
|
|
271
|
+
if (!py) return [{ step: "python", ok: false, why: "Python 3 을 찾지 못함(python3, python, Windows 는 py)" }];
|
|
272
|
+
const steps = [];
|
|
273
|
+
let cmd = py.cmd;
|
|
274
|
+
if (!py.playwright) {
|
|
275
|
+
const pw = installPlaywright(py, env, runner);
|
|
276
|
+
steps.push({ step: "pip install playwright", ok: pw.ok, why: pw.why ?? null });
|
|
277
|
+
if (!pw.ok) return steps;
|
|
278
|
+
cmd = pw.py;
|
|
279
|
+
}
|
|
280
|
+
if (!py.chromium) {
|
|
281
|
+
const down = offline("playwright install chromium", "chromium", { env, runner });
|
|
282
|
+
if (down) return [...steps, down];
|
|
283
|
+
const [c, ...pre] = cmd;
|
|
284
|
+
const ch = runner(c, [...pre, "-m", "playwright", "install", "chromium"], { timeout: STEP_MS });
|
|
285
|
+
steps.push({ step: "playwright install chromium", ok: ch.ok, why: ch.why });
|
|
286
|
+
}
|
|
287
|
+
return steps;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** setup: the missing steps in order, stopping at the first that fails; then status. */
|
|
291
|
+
export function setup(runtime, { env = process.env, runner = run } = {}) {
|
|
292
|
+
const before = status(runtime, { env, runner });
|
|
293
|
+
const steps = [];
|
|
294
|
+
if (before.disabled) steps.push(enablePlugin(runtime, runner));
|
|
295
|
+
else if (!before.skillFile) steps.push(...installPlugin(runtime, env, runner));
|
|
296
|
+
if (steps.every((s) => s.ok) && !(before.python?.playwright && before.python?.chromium)) steps.push(...installBrowser(env, runner));
|
|
297
|
+
return { steps, ...status(runtime, { env, runner }) };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function parse(argv) {
|
|
301
|
+
const [cmd, ...rest] = argv;
|
|
302
|
+
const i = rest.indexOf("--runtime");
|
|
303
|
+
const runtime = i >= 0 ? rest[i + 1] : undefined;
|
|
304
|
+
if (!["status", "setup", "path"].includes(cmd)) return { error: USAGE };
|
|
305
|
+
if (runtime !== "claude" && runtime !== "codex") return { error: `--runtime claude|codex 가 필요함\n${USAGE}` };
|
|
306
|
+
return { cmd, runtime };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function main(argv, out = console.log, err = console.error) {
|
|
310
|
+
if (argv.includes("--help") || argv.includes("-h")) return out(USAGE), 0;
|
|
311
|
+
const o = parse(argv);
|
|
312
|
+
if (o.error) return err(o.error), 2;
|
|
313
|
+
if (o.cmd === "path") {
|
|
314
|
+
const f = o.runtime === "claude" ? claudeSkill() : codexSkill();
|
|
315
|
+
if (!f) return err(`설치되지 않음: ${ID}. setup --runtime ${o.runtime} 로 설치`), 1;
|
|
316
|
+
return out(f), 0;
|
|
317
|
+
}
|
|
318
|
+
const r = o.cmd === "setup" ? setup(o.runtime) : status(o.runtime);
|
|
319
|
+
out(JSON.stringify(r, null, 2));
|
|
320
|
+
return r.ready ? 0 : 1;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Run directly (also through a linked path: both sides as real paths).
|
|
324
|
+
const realOf = (p) => {
|
|
325
|
+
try {
|
|
326
|
+
return realpathSync(p);
|
|
327
|
+
} catch {
|
|
328
|
+
return p;
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
if (process.argv[1] && realOf(process.argv[1]) === realOf(fileURLToPath(import.meta.url))) process.exitCode = main(process.argv.slice(2));
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* OS: Windows / macOS / Linux 공통. 런처(apply.cmd, apply.sh)와 배경 문서(README.md)는
|
|
22
22
|
* 킷 배포 폴더에만 있다. 이 파일만 옮겨 쓰는 곳(예: 스킬 폴더)에는 없다.
|
|
23
23
|
*/
|
|
24
|
+
import { randomBytes } from "node:crypto";
|
|
24
25
|
import {
|
|
25
26
|
readFileSync, writeFileSync, writeSync, existsSync, copyFileSync, readdirSync,
|
|
26
27
|
unlinkSync, mkdirSync, rmSync, symlinkSync, lstatSync, realpathSync,
|
|
@@ -422,7 +423,7 @@ function switchActive(sha) {
|
|
|
422
423
|
// 줄인 비트는 chmod 가 맞춘다). 만든 뒤에 맞추면 그 사이 기본 권한(0664 등)으로 남이 읽을 수 있다.
|
|
423
424
|
function writeAtomic(file, data, mode, { follow = true } = {}) {
|
|
424
425
|
const real = follow ? realOr(file) : file;
|
|
425
|
-
const tmp = join(dirname(real), `.${basename(real)}.omc-tmp-${process.pid}`);
|
|
426
|
+
const tmp = join(dirname(real), `.${basename(real)}.omc-tmp-${process.pid}-${randomBytes(6).toString("hex")}`);
|
|
426
427
|
const m = mode ?? modeOf(real);
|
|
427
428
|
rmSync(tmp, { force: true });
|
|
428
429
|
try {
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* Exit status 1 means nothing was changed; 2 means an unexpected error stopped
|
|
43
43
|
* the command partway, so check `status`.
|
|
44
44
|
*/
|
|
45
|
+
import { randomBytes } from "node:crypto";
|
|
45
46
|
import { spawnSync } from "node:child_process";
|
|
46
47
|
import {
|
|
47
48
|
accessSync,
|
|
@@ -215,7 +216,7 @@ function linkedFolder(file) {
|
|
|
215
216
|
// either name is replaced rather than written through, and a write that fails
|
|
216
217
|
// leaves the old file as it was.
|
|
217
218
|
function writeFresh(file, data, mode) {
|
|
218
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
219
|
+
const tmp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
219
220
|
rmSync(tmp, { force: true });
|
|
220
221
|
try {
|
|
221
222
|
writeFileSync(tmp, data, { flag: "wx", mode });
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* fix sentPart and bump CACHE_VERSION, so totals cached under the old rules are
|
|
34
34
|
* rebuilt.
|
|
35
35
|
*/
|
|
36
|
-
import { createHash } from "node:crypto";
|
|
36
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
37
37
|
import {
|
|
38
38
|
closeSync,
|
|
39
39
|
fstatSync,
|
|
@@ -187,7 +187,7 @@ function saveCache(file, state) {
|
|
|
187
187
|
const dir = dirname(file);
|
|
188
188
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
189
189
|
if (!ownedDir(dir)) return; // a folder this account does not control: no cache
|
|
190
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
190
|
+
const tmp = `${file}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
191
191
|
rmSync(tmp, { force: true });
|
|
192
192
|
writeFileSync(tmp, JSON.stringify(state), { flag: "wx", mode: 0o600 });
|
|
193
193
|
renameSync(tmp, file);
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
| `scripts/pm-sources.mjs` | events.jsonl, git log, 워크플로 journal, `items.json` 읽기. 스토리와 라벨 판정 함수, 시각 해석, 읽기 캐시 |
|
|
15
15
|
| `scripts/pm-html.mjs` | escape, 색 검사, 유한 수 검사, 글 폭 추정, 줄바꿈, 공통 CSS, 탭 머리 |
|
|
16
16
|
| `scripts/pm-render.mjs` | 5탭, 단일 HTML 스냅숏(`export`), workflow 배치, 구성도 자동 배치 |
|
|
17
|
-
| `scripts/pm-adr.mjs` | ADR
|
|
17
|
+
| `scripts/pm-adr.mjs` | ADR 탭 검사: `adr.json` 두 층 스키마 검사, 참조 확인과 정리(`check --adr-cleanup`), `docs/adr/` 갈라짐 경고, 밀도, 집계, check 대조 기록(`adr-check.json`) |
|
|
18
|
+
| `scripts/pm-adr-view.mjs` | ADR 탭 렌더: 화면 본문과 check 대조 배지 |
|
|
18
19
|
| `scripts/pm-server.mjs` | `serve`, `start`, `stop`, `status`. 바인드, Host 검사, 신원 경로, 상태 파일, 포트 선택, 로그 |
|
|
19
20
|
| `scripts/pm-data.mjs` | `init`, `item`, `note`, `stage`, `story`(`items.json` 잠금과 낡은 잠금 정리) |
|
|
20
21
|
| `scripts/pm-memory.mjs` | 메모리 규칙 등록, 옛 판 규칙 줄의 제자리 교체(첫 원본 `.progress-mon.bak`), 규칙 로드 한도 측정 |
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
- 시험은 임시 HOME, 임시 프로젝트, OS 가 고른 빈 포트에서 돈다. 포트 30000, 3456, 3458 은 쓰지 않는다
|
|
42
43
|
- 시험은 PATH 의 `git` 으로 임시 저장소를 만든다. git 이 없는 PC 에서는 그 시험 10개가 실패한다(2026-10-30, PATH 에 node 와 mkfifo 만 두고 실측). 스킬 자체는 git 이 없으면 커밋 기록 없이 동작
|
|
43
44
|
- FIFO 와 `/dev/zero` 시험은 POSIX 만. Windows 에서는 이유를 밝히고 건너뛴다
|
|
44
|
-
- ADR 화면 e2e 는
|
|
45
|
+
- ADR 화면 e2e 는 Playwright Chromium 으로 `export` 한 HTML 을 연다. Playwright 는 `BUILD_TESTBOX_PLAYWRIGHT_DIR`(테스트박스 도구 폴더, `node_modules` 를 담은 폴더)이 있으면 그곳, 없으면 `npm root -g` 에서 찾는다. Playwright 가 없거나 Chromium 을 띄우지 못하면 사유를 밝히고 건너뛴다. `PM_ADR_SHOT=<png 경로>` 를 주면 스크린숏을 그 경로에 남긴다
|
|
45
46
|
|
|
46
47
|
## 개발
|
|
47
48
|
|
|
@@ -103,7 +104,7 @@ override 에 banker 블록이나 이 스킬 블록만 있으면 `init`, `status`
|
|
|
103
104
|
```json
|
|
104
105
|
{ "name": "progress-mon", "type": "skill", "target": "both",
|
|
105
106
|
"reason": "project progress web screen with five tabs (progress, requirements, workflow, architecture, ADR) on a local server; init reads the code and writes the tab data, registers a one-line update rule in project memory; item, note, stage, story keep it current; pure Node, no deps",
|
|
106
|
-
"supportingFiles": ["README.md", "references/adr.md", "references/arch.md", "references/authoring.md", "references/config.md", "references/flow.md", "references/inventory.md", "references/items.md", "references/req-spec.md", "references/reqs.md", "scripts/index.js", "scripts/pm-adr.mjs", "scripts/pm-check.mjs", "scripts/pm-config.mjs", "scripts/pm-data.mjs", "scripts/pm-html.mjs", "scripts/pm-memory.mjs", "scripts/pm-render.mjs", "scripts/pm-server.mjs", "scripts/pm-sources.mjs", "scripts/progress-mon.mjs", "assets/fixture/README.md", "assets/fixture/config.json", "assets/fixture/events.jsonl", "assets/fixture/items.json", "assets/fixture/commits.json", "assets/fixture/req-spec.json", "assets/fixture/reqs.json", "assets/fixture/flow.json", "assets/fixture/arch.json", "assets/fixture/adr.json", "assets/fixture/workflows/wf_ex1/journal.jsonl", "assets/fixture/workflows/wf_ex1/agent-a01.jsonl", "assets/fixture/workflows/wf_ex1/agent-a02.jsonl", "assets/fixture/workflows/wf_ex1/agent-a03.jsonl", "assets/fixture/workflows/wf_ex1/agent-a04.jsonl", "assets/fixture/variants/arch-auto/arch.json", "assets/fixture/variants/check-auto/arch.json", "assets/fixture/variants/check-overlap/arch.json", "assets/fixture/variants/history-manual/config.json", "assets/fixture/variants/history-manual/items.json", "assets/fixture/variants/workflow-wide/flow.json", "assets/fixture/variants/legacy-init/rules.json", "assets/fixture/variants/legacy-init/state.json"],
|
|
107
|
+
"supportingFiles": ["README.md", "references/adr.md", "references/arch.md", "references/authoring.md", "references/config.md", "references/flow.md", "references/inventory.md", "references/items.md", "references/req-spec.md", "references/reqs.md", "scripts/index.js", "scripts/pm-adr.mjs", "scripts/pm-adr-view.mjs", "scripts/pm-check.mjs", "scripts/pm-config.mjs", "scripts/pm-data.mjs", "scripts/pm-html.mjs", "scripts/pm-memory.mjs", "scripts/pm-render.mjs", "scripts/pm-server.mjs", "scripts/pm-sources.mjs", "scripts/progress-mon.mjs", "assets/fixture/README.md", "assets/fixture/config.json", "assets/fixture/events.jsonl", "assets/fixture/items.json", "assets/fixture/commits.json", "assets/fixture/req-spec.json", "assets/fixture/reqs.json", "assets/fixture/flow.json", "assets/fixture/arch.json", "assets/fixture/adr.json", "assets/fixture/workflows/wf_ex1/journal.jsonl", "assets/fixture/workflows/wf_ex1/agent-a01.jsonl", "assets/fixture/workflows/wf_ex1/agent-a02.jsonl", "assets/fixture/workflows/wf_ex1/agent-a03.jsonl", "assets/fixture/workflows/wf_ex1/agent-a04.jsonl", "assets/fixture/variants/arch-auto/arch.json", "assets/fixture/variants/check-auto/arch.json", "assets/fixture/variants/check-overlap/arch.json", "assets/fixture/variants/history-manual/config.json", "assets/fixture/variants/history-manual/items.json", "assets/fixture/variants/workflow-wide/flow.json", "assets/fixture/variants/legacy-init/rules.json", "assets/fixture/variants/legacy-init/state.json"],
|
|
107
108
|
"caveat": "needs Node 18 or later (above the package engines floor); binds 127.0.0.1 only by default, --host takes a private IPv4 with a warning; data lives in .omc/progress-mon/ (Codex .omx/progress-mon/); on Codex start, stop, status and init each ask for sandbox_permissions require_escalated, and a long-lived screen runs from the user's terminal (serve) since closing the session may stop the server" }
|
|
108
109
|
```
|
|
109
110
|
|
|
@@ -114,7 +114,7 @@ ADR 탭이 생기기 전 판으로 init 한 프로젝트는 `adr.json` 이 없
|
|
|
114
114
|
| `note` | `--key <k> [--note <t>] [--issue <t>]` | 단계(`stage:<id>`)나 스토리 행의 비고, 문제점. 빈 값은 그 칸을 지움 |
|
|
115
115
|
| `stage` | `--id <id> --status started\|done\|skipped\|failed\|stopped [--label <t>]` | 단계 상태 기록. 설정에 없는 id 는 새 단계 행 |
|
|
116
116
|
| `story` | `--id <id> (--status todo\|doing\|done \| --commit <hash>) [--name <t>]` | 스토리 상태나 완료 커밋 기록. 설정에 없는 id 는 새 스토리 행 |
|
|
117
|
-
| `check` | `[--adr-cleanup]` | 자료 파일 검사. `FAIL <파일> <json 경로> <이유>`, `WARN ...` 한 줄씩, 끝에 `check: FAIL n, WARN m` 과 밀도 요약 `density <탭> ok\|low`, 마지막 줄 `adr 근거 없음 n칸, 미검증 n, 구현 못 함 n, 참조 없는 서술 n`(늘. 참조 없는 서술은 `refs` 에 확인된 참조가 0개인 `optimization`, `scaling` 칸 수로 규칙은 WARN 이지만 0 이 아니면 화면 배지가 빨강). 밀도 미달 줄 `WARN density <탭> <항목> <값>/<하한>[: <못 채운 대상>]`. `--adr-cleanup` 은 먼저 init 정리 단계를 `adr.json` 에 적용(참조 FAIL 은 `gap`, 스키마 FAIL 이 남으면 같은 폴더의 `adr.json.rejected-<YYYYMMDD-HHMMSS>` 로 옮김. 이름이 있으면 `-2` 부터 번호)하고 `정리:` 줄(옮긴 경로 포함)을 낸 뒤
|
|
117
|
+
| `check` | `[--adr-cleanup]` | 자료 파일 검사. `FAIL <파일> <json 경로> <이유>`, `WARN ...` 한 줄씩, 끝에 `check: FAIL n, WARN m` 과 밀도 요약 `density <탭> ok\|low`, 마지막 줄 `adr 근거 없음 n칸, 미검증 n, 구현 못 함 n, 참조 없는 서술 n`(늘. 참조 없는 서술은 `refs` 에 확인된 참조가 0개인 `optimization`, `scaling` 칸 수로 규칙은 WARN 이지만 0 이 아니면 화면 배지가 빨강). 밀도 미달 줄 `WARN density <탭> <항목> <값>/<하한>[: <못 채운 대상>]`. `--adr-cleanup` 은 먼저 init 정리 단계를 `adr.json` 에 적용(참조 FAIL 은 `gap`, 스키마 FAIL 이 남으면 같은 폴더의 `adr.json.rejected-<YYYYMMDD-HHMMSS>` 로 옮김. 이름이 있으면 `-2` 부터 번호)하고 `정리:` 줄(옮긴 경로 포함)을 낸 뒤 검사. 끝에 화면 대조용 `adr-check.json`(check 집계와 `adr.json` sha256)을 쓴다(`references/adr.md`) |
|
|
118
118
|
|
|
119
119
|
- 공통 인자: `--runtime claude|codex`, `--project <dir>`, `--data <dir>`(자료 폴더 직접 지정)
|
|
120
120
|
- 종료 코드: 0 성공(이미 그 상태 포함), 1 거부나 사용법 오류나 `check` FAIL, 2 예기치 못한 오류, 3 `status` 꺼짐
|
|
@@ -66,6 +66,13 @@ init 절차의 ADR 단계와 갱신 규칙(SKILL.md)에서 에이전트가 쓰
|
|
|
66
66
|
- 화면은 빈 칸을 `근거 없음: <사유>` 로 보이고, 결정 목록에 결정별 3항목 채움 수(`n/3`)와 빈 항목을 보인다
|
|
67
67
|
- 집계 배지 `근거 없음 n칸, 미검증 n, 구현 못 함 n, 참조 없는 서술 n`: adr.json 안의 `{ "gap" }` 수(여섯 항목 칸과 참조 자리), `status: "unverified"` 모듈 수, `notImplemented` 기능 수, `refs` 에 확인된 참조가 0개인 서술 칸 수. 화면 맨 위(프로젝트 요약 위)와 `check` 의 마지막 줄 `adr 근거 없음 n칸, 미검증 n, 구현 못 함 n, 참조 없는 서술 n` 에 늘 보임. 0 이면 0(adr.json 이 없으면 check 는 모두 0). 넷 가운데 하나라도 0 이 아니면 배지는 빨강(모든 모듈이 미검증이거나 서술 칸에 참조가 없는데 초록 0칸만 보이지 않게)
|
|
68
68
|
- `check` 는 참조 확인을 마친 참조(FAIL, WARN 없음)만 센다. 화면은 파일을 읽지 않으므로 꼴이 맞고 `gap` 이 아닌 참조를 센다. 그래서 출력 파일 없는 `cmd` 하나만 적힌 칸은 화면에서 0, `check` 에서 1 로 셀 수 있다
|
|
69
|
+
- check 대조: `check` 명령은 끝에 자료 폴더의 `adr-check.json` 에 그 집계 글, 그때 `adr.json` 글(BOM 을 뗀 뒤)의 sha256, 시각(UTC)을 쓴다. `adr.json` 이 없거나 JSON 이 아니면 쓰지 않는다. 쓰지 못하면 stderr 에 `경고: adr-check.json 쓰기 실패(...)` 만 내고 검사 결과와 종료 코드는 그대로다
|
|
70
|
+
- 화면은 집계 배지 옆에 대조 배지를 보인다
|
|
71
|
+
- 기록이 없거나 꼴이 틀림: `check 기록 없음. 참조 확인은 check`
|
|
72
|
+
- sha256 이 다름: `check(<시각> UTC) 뒤 adr.json 이 바뀜. check 를 다시 실행하면 대조`
|
|
73
|
+
- sha256 과 집계 글이 같음: 초록 `check(<시각> UTC) 와 같음`
|
|
74
|
+
- 집계 글이 다름: 빨강 `check(<시각> UTC) 기준: <check 집계>. 화면은 적힌 참조로 세어 다름. 확인 못 한 참조는 check 의 참조 줄`
|
|
75
|
+
- 대조는 check 를 실행한 때의 참조 파일 기준이다. 그 뒤 참조 파일만 바뀌면(adr.json 은 그대로) 배지는 그대로이므로 다시 확인하려면 check 를 실행한다
|
|
69
76
|
|
|
70
77
|
## 참조
|
|
71
78
|
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// pm-adr-view.mjs: ADR 탭 화면 렌더. 검사, 밀도, 집계는 pm-adr.mjs.
|
|
2
|
+
// 화면은 참조를 글로만 보이고 파일을 읽거나 링크하지 않는다. 참조 확인은 check 가 하고, 그 집계는 adr-check.json 으로 대조한다.
|
|
3
|
+
import { DECISION_SLOTS, STATUS, adrTally, arr, isGap, obj, str } from './pm-adr.mjs';
|
|
4
|
+
import { esc } from './pm-html.mjs';
|
|
5
|
+
|
|
6
|
+
const gapHtml = (v) => `<span class="b miss">근거 없음: ${esc(v.gap)}</span>`;
|
|
7
|
+
|
|
8
|
+
// 참조 한 줄 글. 파일을 읽거나 링크하지 않는다.
|
|
9
|
+
export function refText(x) {
|
|
10
|
+
if (!obj(x)) return '';
|
|
11
|
+
if (isGap(x)) return gapHtml(x);
|
|
12
|
+
const note = typeof x.note === 'string' && x.note ? ` <small>${esc(x.note)}</small>` : '';
|
|
13
|
+
const line = Number.isInteger(x.line) ? `:${x.line}` : '';
|
|
14
|
+
const out = {
|
|
15
|
+
file: () => `파일 <code>${esc(x.path)}${line}</code>`,
|
|
16
|
+
test: () => `시험 <code>${esc(x.path)}</code> 의 ${esc(x.name)}`,
|
|
17
|
+
commit: () => `커밋 <code>${esc(String(x.hash || '').slice(0, 7))}</code>`,
|
|
18
|
+
cmd: () => `명령 <code>${esc(x.cmd)}</code>, 출력 ${x.output ? `<code>${esc(x.output)}</code>` : '없음'}`,
|
|
19
|
+
}[x.kind];
|
|
20
|
+
return out ? out() + note : esc(JSON.stringify(x)) + note;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const refsHtml = (list) => arr(list).map(refText).filter(Boolean).join('<br>') || '<span class="n">참조 없음</span>';
|
|
24
|
+
const table = (head, rows) => `<table><thead><tr>${head.map((h) => `<th>${esc(h)}</th>`).join('')}</tr></thead><tbody>${rows.join('') || `<tr><td colspan="${head.length}" class="n">없음</td></tr>`}</tbody></table>`;
|
|
25
|
+
const td = (h, cls = '') => `<td${cls ? ` class="${cls}"` : ''}>${h}</td>`;
|
|
26
|
+
const sec = (title, body) => `<h3>${esc(title)}</h3>${body}`;
|
|
27
|
+
// 칸이 gap 이면 근거 없음, 아니면 body(값).
|
|
28
|
+
const slotHtml = (v, body) => (isGap(v) ? `<div>${gapHtml(v)}</div>` : obj(v) || Array.isArray(v) ? body(v) : `<div>${gapHtml({ gap: '칸 없음' })}</div>`);
|
|
29
|
+
|
|
30
|
+
const evidenceHtml = (x) => (x.status === 'unverified' ? `<span class="b unv">미검증: ${esc(x.why)}</span>${arr(x.evidence).length ? `<br>${refsHtml(x.evidence)}` : ''}` : refsHtml(x.evidence));
|
|
31
|
+
|
|
32
|
+
function verificationHtml(v) {
|
|
33
|
+
return table(['id', '단위 기능', '결과', 'evidence'], arr(v).filter(obj).map((x) => `<tr>${td(esc(x.id), 'h')}${td(esc(x.module))}${td(esc(x.result || '-'))}${td(evidenceHtml(x), 'n')}</tr>`));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function flowHtml(f) {
|
|
37
|
+
const rows = [['입력', f.input], ['처리', f.logic], ['출력', f.output]].map(([k, t]) => `<tr>${td(k, 't')}${td(str(t) ? esc(t) : gapHtml({ gap: '빈 칸' }))}</tr>`);
|
|
38
|
+
return `${table(['구분', '내용'], rows)}<div class="meta">참조: ${refsHtml(f.refs)}</div>`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function proofHtml(p) {
|
|
42
|
+
const impl = arr(p.implemented).filter(obj).map((x) => `<tr>${td(esc(x.feature))}${td(esc(arr(x.modules).join(', ')), 'h')}${td(refsHtml(x.proofs), 'n')}</tr>`);
|
|
43
|
+
const not = arr(p.notImplemented).filter(obj).map((x) => `<tr>${td(esc(x.feature))}${td(esc(x.why))}</tr>`);
|
|
44
|
+
return `<div class="meta">구현된 기능과 증명 위치</div>${table(['구현된 기능', '모듈', '증명 위치'], impl)}<div class="meta">구현 못 한 기능</div>${table(['구현 못 한 기능', '사유'], not)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const filled = (d) => DECISION_SLOTS.filter(([k]) => obj(d[k]) && !isGap(d[k])).length;
|
|
48
|
+
const emptyNames = (d) => DECISION_SLOTS.filter(([k]) => !(obj(d[k]) && !isGap(d[k]))).map(([, n]) => n);
|
|
49
|
+
|
|
50
|
+
function decisionRow(d) {
|
|
51
|
+
const miss = emptyNames(d);
|
|
52
|
+
const st = STATUS[d.status] || d.status;
|
|
53
|
+
return `<tr>${td(esc(d.id), 'h')}${td(esc(d.title))}${td(esc(st))}${td(esc(d.date || '-'), 't')}${td(`${filled(d)}/3`, 'd')}${td(miss.length ? `<span class="b miss">${esc(miss.join(', '))}</span>` : '-', 'n')}</tr>`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function tradeoffsHtml(t) {
|
|
57
|
+
const gave = arr(t.gaveUp).filter(obj).map((x) => `<tr>${td(esc(x.what))}${td(esc(x.why))}</tr>`);
|
|
58
|
+
const got = arr(t.gained).filter(obj).map((x) => `<tr>${td(esc(x.what))}${td(esc(x.purpose))}</tr>`);
|
|
59
|
+
return `${table(['포기한 것', '이유'], gave)}${table(['얻은 것', '목적'], got)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function decisionHtml(d) {
|
|
63
|
+
const opts = arr(d.options).filter(obj).map((x) => `<tr>${td(esc(x.name))}${td(esc(x.pros))}${td(esc(x.cons))}</tr>`);
|
|
64
|
+
const rejected = str(d.rejected) ? `<div class="meta">버린 선택지: ${esc(d.rejected)}</div>` : '';
|
|
65
|
+
const optim = (o) => `<div>${esc(o.process)}</div><div class="meta">참조한 모듈: ${esc(arr(o.modules).join(', ') || '없음')}</div><div class="meta">참조: ${refsHtml(o.refs)}</div>`;
|
|
66
|
+
const scale = (s) => `<div>${esc(s.argument)}</div>${str(s.limits) ? `<div class="meta">한계: ${esc(s.limits)}</div>` : ''}<div class="meta">참조: ${refsHtml(s.refs)}</div>`;
|
|
67
|
+
return `<h2>${esc(d.id)} ${esc(d.title)} <small>${esc(STATUS[d.status] || d.status)}</small></h2>
|
|
68
|
+
<div class="meta">맥락: ${esc(d.context)}</div>${table(['선택지', '장점', '단점'], opts)}${rejected}
|
|
69
|
+
<div><b>결론</b>: ${esc(d.decision)}</div><div class="meta">이유: ${esc(d.why)}</div>
|
|
70
|
+
${sec('최적화 로직', slotHtml(d.optimization, optim))}${sec('확장 대응', slotHtml(d.scaling, scale))}${sec('포기와 얻은 것', slotHtml(d.tradeoffs, tradeoffsHtml))}
|
|
71
|
+
<div class="meta">근거 출처: ${refsHtml(d.sources)}</div>`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// check 대조. 화면은 적힌 참조로, check 는 확인을 마친 참조로 센다(깨진 참조가 있으면 참조 없는 서술 수가 다름).
|
|
75
|
+
// m.adrCheck: check 가 남긴 adr-check.json({ sha, tally, at }), m.adr.sha: 지금 adr.json 의 sha256. text: 화면 집계 글.
|
|
76
|
+
export function checkNote(m, text) {
|
|
77
|
+
const rec = m.adrCheck && obj(m.adrCheck.value);
|
|
78
|
+
const b = (cls, t) => `<span class="b ${cls}">${esc(t)}</span>`;
|
|
79
|
+
if (!rec || typeof rec.sha !== 'string' || typeof rec.tally !== 'string') return b('unv', 'check 기록 없음. 참조 확인은 check');
|
|
80
|
+
const at = typeof rec.at === 'string' ? rec.at.slice(0, 19).replace('T', ' ') : '';
|
|
81
|
+
const when = at ? `check(${at} UTC)` : 'check';
|
|
82
|
+
if (rec.sha !== (m.adr && m.adr.sha)) return b('unv', `${when} 뒤 adr.json 이 바뀜. check 를 다시 실행하면 대조`);
|
|
83
|
+
if (rec.tally === text) return b('done', `${when} 와 같음`);
|
|
84
|
+
return b('miss', `${when} 기준: ${rec.tally}. 화면은 적힌 참조로 세어 다름. 확인 못 한 참조는 check 의 참조 줄`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ADR 탭 본문. 자료가 없거나 깨졌으면 다른 탭처럼 안내와 스키마 문서.
|
|
88
|
+
export function adrBody(m, emptyNotice) {
|
|
89
|
+
const v = m.adr && obj(m.adr.value);
|
|
90
|
+
if (!v) return emptyNotice('references/adr.md');
|
|
91
|
+
const p = obj(v.project) || {};
|
|
92
|
+
const ds = arr(v.decisions).filter(obj);
|
|
93
|
+
const tally = adrTally(v);
|
|
94
|
+
return `<div class="meta">의사 결정 기록. 위는 프로젝트 전체(검증, 흐름, 증거), 아래는 결정마다(최적화 로직, 확장 대응, 포기와 얻은 것). 자료: <code>adr.json</code>. 참조는 위치만 보이고 이 화면은 파일을 열지 않음. 참조 확인은 check.</div>
|
|
95
|
+
<div class="meta"><span class="b ${tally.any ? 'miss' : 'done'}">${esc(tally.text)}</span> ${checkNote(m, tally.text)}</div>
|
|
96
|
+
<h2>프로젝트 요약</h2>
|
|
97
|
+
${sec('검증', slotHtml(p.verification, verificationHtml))}${sec('흐름', slotHtml(p.flow, flowHtml))}${sec('증거', slotHtml(p.proof, proofHtml))}
|
|
98
|
+
<h2>결정 목록 <small>${ds.length}건</small></h2>
|
|
99
|
+
${table(['id', '제목', '상태', '날짜', '결정별 3항목', '빈 항목'], ds.map(decisionRow))}
|
|
100
|
+
${ds.map(decisionHtml).join('\n')}`;
|
|
101
|
+
}
|