pai-zero 0.13.0 → 0.13.1
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/dist/bin/pai.js +1065 -928
- package/dist/bin/pai.js.map +1 -1
- package/dist/cli/index.js +1065 -928
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/pai.js
CHANGED
|
@@ -54,6 +54,13 @@ var init_logger = __esm({
|
|
|
54
54
|
});
|
|
55
55
|
|
|
56
56
|
// src/core/config.ts
|
|
57
|
+
var config_exports = {};
|
|
58
|
+
__export(config_exports, {
|
|
59
|
+
createDefaultConfig: () => createDefaultConfig,
|
|
60
|
+
loadConfig: () => loadConfig,
|
|
61
|
+
normalizeMode: () => normalizeMode,
|
|
62
|
+
saveConfig: () => saveConfig
|
|
63
|
+
});
|
|
57
64
|
import path from "path";
|
|
58
65
|
import { createRequire } from "module";
|
|
59
66
|
import fs from "fs-extra";
|
|
@@ -3598,169 +3605,722 @@ var init_detector = __esm({
|
|
|
3598
3605
|
}
|
|
3599
3606
|
});
|
|
3600
3607
|
|
|
3601
|
-
// src/
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
runDoctor: () => runDoctor
|
|
3605
|
-
});
|
|
3606
|
-
import { join as join6 } from "path";
|
|
3607
|
-
import { homedir } from "os";
|
|
3608
|
+
// src/core/roboco.ts
|
|
3609
|
+
import os2 from "os";
|
|
3610
|
+
import path5 from "path";
|
|
3608
3611
|
import fs10 from "fs-extra";
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
const
|
|
3614
|
-
|
|
3615
|
-
label: "Node.js \uBC84\uC804",
|
|
3616
|
-
ok: nodeMajor >= 20,
|
|
3617
|
-
detail: `${nodeVersion} ${nodeMajor >= 20 ? "(>=20 OK)" : "(\uC5C5\uADF8\uB808\uC774\uB4DC \uD544\uC694)"}`,
|
|
3618
|
-
fix: nodeMajor < 20 ? "Node.js 20 \uC774\uC0C1\uC73C\uB85C \uC5C5\uADF8\uB808\uC774\uB4DC\uD558\uC138\uC694" : void 0
|
|
3619
|
-
});
|
|
3620
|
-
const claudeCheck = await checkCommand("claude", ["--version"]);
|
|
3621
|
-
checks.push({
|
|
3622
|
-
label: "Claude Code CLI",
|
|
3623
|
-
ok: claudeCheck.ok,
|
|
3624
|
-
detail: claudeCheck.detail,
|
|
3625
|
-
fix: claudeCheck.ok ? void 0 : "npm install -g @anthropic-ai/claude-code"
|
|
3626
|
-
});
|
|
3627
|
-
const globalConfigPath = join6(homedir(), ".pai", "config.json");
|
|
3628
|
-
const hasGlobalConfig = await fs10.pathExists(globalConfigPath);
|
|
3629
|
-
checks.push({
|
|
3630
|
-
label: "\uAE00\uB85C\uBC8C \uC124\uC815",
|
|
3631
|
-
ok: true,
|
|
3632
|
-
detail: hasGlobalConfig ? globalConfigPath : "\uBBF8\uC124\uC815 (\uAE30\uBCF8\uAC12 \uC0AC\uC6A9)"
|
|
3633
|
-
});
|
|
3634
|
-
let hasSdk = false;
|
|
3612
|
+
function robocoPath(cwd) {
|
|
3613
|
+
return path5.join(cwd, ROBOCO_PATH);
|
|
3614
|
+
}
|
|
3615
|
+
async function loadRobocoState(cwd) {
|
|
3616
|
+
const p = robocoPath(cwd);
|
|
3617
|
+
if (!await fs10.pathExists(p)) return freshState();
|
|
3635
3618
|
try {
|
|
3636
|
-
await
|
|
3637
|
-
|
|
3619
|
+
const raw = await fs10.readJson(p);
|
|
3620
|
+
if (raw?.version !== "2.0") return freshState();
|
|
3621
|
+
return raw;
|
|
3638
3622
|
} catch {
|
|
3639
|
-
|
|
3640
|
-
checks.push({
|
|
3641
|
-
label: "Agent SDK",
|
|
3642
|
-
ok: true,
|
|
3643
|
-
// optional이므로 항상 OK
|
|
3644
|
-
detail: hasSdk ? "\uC124\uCE58\uB428 (AI \uAE30\uB2A5 \uD65C\uC131\uD654)" : "\uBBF8\uC124\uCE58 (\uC815\uC801 \uBD84\uC11D \uBAA8\uB4DC)"
|
|
3645
|
-
});
|
|
3646
|
-
console.log("");
|
|
3647
|
-
let passed = 0;
|
|
3648
|
-
for (const check of checks) {
|
|
3649
|
-
const icon = check.ok ? "\u2713" : "\u2717";
|
|
3650
|
-
const pad = " ".repeat(Math.max(1, 20 - check.label.length));
|
|
3651
|
-
if (check.ok) {
|
|
3652
|
-
success(`${icon} ${check.label}${pad}${check.detail}`);
|
|
3653
|
-
passed++;
|
|
3654
|
-
} else {
|
|
3655
|
-
error(`${icon} ${check.label}${pad}${check.detail}`);
|
|
3656
|
-
if (check.fix) {
|
|
3657
|
-
info(` \u2192 ${check.fix}`);
|
|
3658
|
-
}
|
|
3659
|
-
}
|
|
3660
|
-
}
|
|
3661
|
-
console.log("");
|
|
3662
|
-
info(`${passed}/${checks.length} \uD56D\uBAA9 \uD1B5\uACFC`);
|
|
3663
|
-
if (passed < checks.length) {
|
|
3664
|
-
process.exitCode = 1;
|
|
3623
|
+
return freshState();
|
|
3665
3624
|
}
|
|
3666
3625
|
}
|
|
3667
|
-
async function
|
|
3626
|
+
async function saveRobocoState(cwd, state) {
|
|
3627
|
+
const p = robocoPath(cwd);
|
|
3628
|
+
await fs10.ensureDir(path5.dirname(p));
|
|
3629
|
+
await fs10.writeJson(p, state, { spaces: 2 });
|
|
3630
|
+
}
|
|
3631
|
+
function freshState() {
|
|
3632
|
+
return {
|
|
3633
|
+
version: "2.0",
|
|
3634
|
+
currentStage: "environment",
|
|
3635
|
+
stageHistory: [],
|
|
3636
|
+
lock: null,
|
|
3637
|
+
gates: {}
|
|
3638
|
+
};
|
|
3639
|
+
}
|
|
3640
|
+
function isProcessAlive(pid) {
|
|
3668
3641
|
try {
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
return { ok: true, detail: stdout.trim().split("\n")[0] ?? "ok" };
|
|
3642
|
+
process.kill(pid, 0);
|
|
3643
|
+
return true;
|
|
3672
3644
|
} catch {
|
|
3673
|
-
return
|
|
3645
|
+
return false;
|
|
3674
3646
|
}
|
|
3675
3647
|
}
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
}
|
|
3681
|
-
});
|
|
3682
|
-
|
|
3683
|
-
// src/utils/github-fetch.ts
|
|
3684
|
-
import path5 from "path";
|
|
3685
|
-
import fs11 from "fs-extra";
|
|
3686
|
-
async function httpGet(url, timeoutMs, accept) {
|
|
3687
|
-
const controller = new AbortController();
|
|
3688
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
3689
|
-
try {
|
|
3690
|
-
return await fetch(url, {
|
|
3691
|
-
signal: controller.signal,
|
|
3692
|
-
headers: { "Accept": accept, "User-Agent": "pai-zero" }
|
|
3693
|
-
});
|
|
3694
|
-
} finally {
|
|
3695
|
-
clearTimeout(timer);
|
|
3696
|
-
}
|
|
3648
|
+
function isLockStale(lock, now2 = /* @__PURE__ */ new Date()) {
|
|
3649
|
+
if (!isProcessAlive(lock.pid)) return true;
|
|
3650
|
+
if (new Date(lock.expiresAt).getTime() < now2.getTime()) return true;
|
|
3651
|
+
return false;
|
|
3697
3652
|
}
|
|
3698
|
-
async function
|
|
3699
|
-
const
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
const data = await res.json();
|
|
3705
|
-
if (!Array.isArray(data)) {
|
|
3706
|
-
throw new Error(`Expected directory, got single entry at ${dirPath}`);
|
|
3653
|
+
async function acquireLock(cwd, stage, command, ttlMs = DEFAULT_TTL_MS) {
|
|
3654
|
+
const state = await loadRobocoState(cwd);
|
|
3655
|
+
if (state.lock) {
|
|
3656
|
+
if (!isLockStale(state.lock)) {
|
|
3657
|
+
throw new RobocoLockError(formatLockError(state.lock), state.lock);
|
|
3658
|
+
}
|
|
3707
3659
|
}
|
|
3708
|
-
|
|
3660
|
+
const now2 = /* @__PURE__ */ new Date();
|
|
3661
|
+
const newLock = {
|
|
3662
|
+
pid: process.pid,
|
|
3663
|
+
stage,
|
|
3664
|
+
command,
|
|
3665
|
+
acquiredAt: now2.toISOString(),
|
|
3666
|
+
expiresAt: new Date(now2.getTime() + ttlMs).toISOString(),
|
|
3667
|
+
host: os2.hostname()
|
|
3668
|
+
};
|
|
3669
|
+
state.lock = newLock;
|
|
3670
|
+
await saveRobocoState(cwd, state);
|
|
3671
|
+
return state;
|
|
3709
3672
|
}
|
|
3710
|
-
async function
|
|
3711
|
-
const
|
|
3712
|
-
if (
|
|
3713
|
-
|
|
3673
|
+
async function releaseLock(cwd) {
|
|
3674
|
+
const state = await loadRobocoState(cwd);
|
|
3675
|
+
if (state.lock && state.lock.pid === process.pid) {
|
|
3676
|
+
state.lock = null;
|
|
3677
|
+
await saveRobocoState(cwd, state);
|
|
3714
3678
|
}
|
|
3715
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
3716
|
-
await fs11.ensureDir(path5.dirname(destPath));
|
|
3717
|
-
await fs11.writeFile(destPath, buf);
|
|
3718
3679
|
}
|
|
3719
|
-
|
|
3720
|
-
const
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3680
|
+
function formatLockError(lock) {
|
|
3681
|
+
const started = new Date(lock.acquiredAt);
|
|
3682
|
+
const mins = Math.round((Date.now() - started.getTime()) / 6e4);
|
|
3683
|
+
return [
|
|
3684
|
+
"\uB2E4\uB978 PAI \uBA85\uB839\uC774 \uC9C4\uD589 \uC911\uC785\uB2C8\uB2E4",
|
|
3685
|
+
` stage: ${lock.stage}`,
|
|
3686
|
+
` command: ${lock.command}`,
|
|
3687
|
+
` \uC2DC\uC791: ${mins}\uBD84 \uC804 (PID ${lock.pid}${lock.host ? ", host " + lock.host : ""})`,
|
|
3688
|
+
" \uB300\uAE30\uD558\uC9C0 \uC54A\uACE0 \uC989\uC2DC \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uC644\uB8CC \uD6C4 \uC7AC\uC2DC\uB3C4\uD558\uC138\uC694."
|
|
3689
|
+
].join("\n");
|
|
3690
|
+
}
|
|
3691
|
+
async function markStageStart(cwd, stage, command) {
|
|
3692
|
+
const state = await loadRobocoState(cwd);
|
|
3693
|
+
state.currentStage = stage;
|
|
3694
|
+
state.stageHistory.push({
|
|
3695
|
+
stage,
|
|
3696
|
+
status: "in_progress",
|
|
3697
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3698
|
+
by: command
|
|
3699
|
+
});
|
|
3700
|
+
await saveRobocoState(cwd, state);
|
|
3701
|
+
}
|
|
3702
|
+
async function markStageEnd(cwd, stage, success2, reason) {
|
|
3703
|
+
const state = await loadRobocoState(cwd);
|
|
3704
|
+
for (let i = state.stageHistory.length - 1; i >= 0; i--) {
|
|
3705
|
+
const entry = state.stageHistory[i];
|
|
3706
|
+
if (entry.stage === stage && entry.status === "in_progress") {
|
|
3707
|
+
entry.status = success2 ? "done" : "failed";
|
|
3708
|
+
entry.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3709
|
+
if (reason) entry.reason = reason;
|
|
3710
|
+
break;
|
|
3737
3711
|
}
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
result.errors.push({ path: entry.path, error: msg });
|
|
3758
|
-
}
|
|
3712
|
+
}
|
|
3713
|
+
await saveRobocoState(cwd, state);
|
|
3714
|
+
}
|
|
3715
|
+
async function saveGateResult(cwd, key, result) {
|
|
3716
|
+
const state = await loadRobocoState(cwd);
|
|
3717
|
+
state.gates[key] = result;
|
|
3718
|
+
await saveRobocoState(cwd, state);
|
|
3719
|
+
}
|
|
3720
|
+
async function withRoboco(cwd, stage, command, options, fn) {
|
|
3721
|
+
await acquireLock(cwd, stage, command, options.ttlMs);
|
|
3722
|
+
try {
|
|
3723
|
+
if (options.gate && !options.force) {
|
|
3724
|
+
const result = await options.gate(cwd);
|
|
3725
|
+
await saveGateResult(cwd, `${stage}.entry`, result);
|
|
3726
|
+
if (!result.passed && !options.skipGates) {
|
|
3727
|
+
await markStageEnd(cwd, stage, false, `\uAC8C\uC774\uD2B8 \uC2E4\uD328: ${result.name}`);
|
|
3728
|
+
throw new GateFailedError(result);
|
|
3729
|
+
}
|
|
3730
|
+
if (!result.passed && options.skipGates) {
|
|
3759
3731
|
}
|
|
3760
3732
|
}
|
|
3733
|
+
await markStageStart(cwd, stage, command);
|
|
3734
|
+
const value = await fn();
|
|
3735
|
+
await markStageEnd(cwd, stage, true);
|
|
3736
|
+
return value;
|
|
3737
|
+
} catch (err) {
|
|
3738
|
+
if (!(err instanceof GateFailedError)) {
|
|
3739
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3740
|
+
await markStageEnd(cwd, stage, false, msg).catch(() => {
|
|
3741
|
+
});
|
|
3742
|
+
}
|
|
3743
|
+
throw err;
|
|
3744
|
+
} finally {
|
|
3745
|
+
await releaseLock(cwd).catch(() => {
|
|
3746
|
+
});
|
|
3761
3747
|
}
|
|
3762
|
-
|
|
3763
|
-
|
|
3748
|
+
}
|
|
3749
|
+
async function syncHandoff(cwd) {
|
|
3750
|
+
const handoffPath = path5.join(cwd, "handoff.md");
|
|
3751
|
+
if (!await fs10.pathExists(handoffPath)) return;
|
|
3752
|
+
const state = await loadRobocoState(cwd);
|
|
3753
|
+
const block = buildHandoffBlock(state);
|
|
3754
|
+
const content = await fs10.readFile(handoffPath, "utf8");
|
|
3755
|
+
const startIdx = content.indexOf(HANDOFF_START);
|
|
3756
|
+
const endIdx = content.indexOf(HANDOFF_END);
|
|
3757
|
+
let next;
|
|
3758
|
+
if (startIdx >= 0 && endIdx > startIdx) {
|
|
3759
|
+
next = content.slice(0, startIdx) + block + content.slice(endIdx + HANDOFF_END.length);
|
|
3760
|
+
} else {
|
|
3761
|
+
const sep = content.endsWith("\n") ? "\n" : "\n\n";
|
|
3762
|
+
next = content + sep + block + "\n";
|
|
3763
|
+
}
|
|
3764
|
+
await fs10.writeFile(handoffPath, next);
|
|
3765
|
+
}
|
|
3766
|
+
function buildHandoffBlock(state) {
|
|
3767
|
+
const lines = [HANDOFF_START, "## \uD30C\uC774\uD504\uB77C\uC778 \uC9C4\uD589 \uC0C1\uD0DC (roboco \uC790\uB3D9 \uAD00\uB9AC)", ""];
|
|
3768
|
+
const done = state.stageHistory.filter((h) => h.status === "done");
|
|
3769
|
+
const inProgress = state.stageHistory.filter((h) => h.status === "in_progress");
|
|
3770
|
+
const failed = state.stageHistory.filter((h) => h.status === "failed");
|
|
3771
|
+
if (inProgress.length > 0) {
|
|
3772
|
+
lines.push("### \uC9C4\uD589 \uC911");
|
|
3773
|
+
for (const h of inProgress) {
|
|
3774
|
+
lines.push(`- [ ] ${h.stage} (${h.by}, \uC2DC\uC791 ${fmtDate(h.startedAt)})`);
|
|
3775
|
+
}
|
|
3776
|
+
lines.push("");
|
|
3777
|
+
}
|
|
3778
|
+
if (failed.length > 0) {
|
|
3779
|
+
lines.push("### \uC2E4\uD328");
|
|
3780
|
+
for (const h of failed) {
|
|
3781
|
+
lines.push(`- [x] ${h.stage} \u2014 ${h.reason ?? "\uC6D0\uC778 \uBD88\uBA85"} (${fmtDate(h.startedAt)})`);
|
|
3782
|
+
}
|
|
3783
|
+
lines.push("");
|
|
3784
|
+
}
|
|
3785
|
+
if (done.length > 0) {
|
|
3786
|
+
lines.push("### \uC644\uB8CC");
|
|
3787
|
+
for (const h of done) {
|
|
3788
|
+
lines.push(`- [x] ${h.stage} (${h.by}, ${fmtDate(h.endedAt ?? h.startedAt)})`);
|
|
3789
|
+
}
|
|
3790
|
+
lines.push("");
|
|
3791
|
+
}
|
|
3792
|
+
lines.push(`\uD604\uC7AC \uB2E8\uACC4: **${state.currentStage}**`);
|
|
3793
|
+
lines.push(HANDOFF_END);
|
|
3794
|
+
return lines.join("\n");
|
|
3795
|
+
}
|
|
3796
|
+
function fmtDate(iso) {
|
|
3797
|
+
try {
|
|
3798
|
+
const d = new Date(iso);
|
|
3799
|
+
return d.toISOString().slice(0, 16).replace("T", " ");
|
|
3800
|
+
} catch {
|
|
3801
|
+
return iso;
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
var ROBOCO_PATH, DEFAULT_TTL_MS, RobocoLockError, GateFailedError, HANDOFF_START, HANDOFF_END;
|
|
3805
|
+
var init_roboco = __esm({
|
|
3806
|
+
"src/core/roboco.ts"() {
|
|
3807
|
+
"use strict";
|
|
3808
|
+
ROBOCO_PATH = path5.join(".pai", "roboco.json");
|
|
3809
|
+
DEFAULT_TTL_MS = 10 * 60 * 1e3;
|
|
3810
|
+
RobocoLockError = class extends Error {
|
|
3811
|
+
constructor(message, holder) {
|
|
3812
|
+
super(message);
|
|
3813
|
+
this.holder = holder;
|
|
3814
|
+
this.name = "RobocoLockError";
|
|
3815
|
+
}
|
|
3816
|
+
holder;
|
|
3817
|
+
};
|
|
3818
|
+
GateFailedError = class extends Error {
|
|
3819
|
+
constructor(result) {
|
|
3820
|
+
super(`\uAC8C\uC774\uD2B8 \uC2E4\uD328 (${result.name}): ${result.violations.length}\uAC74 \uC704\uBC18`);
|
|
3821
|
+
this.result = result;
|
|
3822
|
+
this.name = "GateFailedError";
|
|
3823
|
+
}
|
|
3824
|
+
result;
|
|
3825
|
+
};
|
|
3826
|
+
HANDOFF_START = "<!-- roboco:start -->";
|
|
3827
|
+
HANDOFF_END = "<!-- roboco:end -->";
|
|
3828
|
+
}
|
|
3829
|
+
});
|
|
3830
|
+
|
|
3831
|
+
// src/core/gates.ts
|
|
3832
|
+
var gates_exports = {};
|
|
3833
|
+
__export(gates_exports, {
|
|
3834
|
+
STAGE_GATES: () => STAGE_GATES,
|
|
3835
|
+
countBusinessRules: () => countBusinessRules,
|
|
3836
|
+
countDomainObjects: () => countDomainObjects,
|
|
3837
|
+
countEndpoints: () => countEndpoints,
|
|
3838
|
+
designEntryGate: () => designEntryGate,
|
|
3839
|
+
evaluationEntryGate: () => evaluationEntryGate,
|
|
3840
|
+
executionEntryGate: () => executionEntryGate,
|
|
3841
|
+
validateEdges: () => validateEdges,
|
|
3842
|
+
validationEntryGate: () => validationEntryGate
|
|
3843
|
+
});
|
|
3844
|
+
import path6 from "path";
|
|
3845
|
+
import fs11 from "fs-extra";
|
|
3846
|
+
function now() {
|
|
3847
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3848
|
+
}
|
|
3849
|
+
function makeResult(name, violations) {
|
|
3850
|
+
return {
|
|
3851
|
+
name,
|
|
3852
|
+
passed: violations.length === 0,
|
|
3853
|
+
checkedAt: now(),
|
|
3854
|
+
violations
|
|
3855
|
+
};
|
|
3856
|
+
}
|
|
3857
|
+
async function designEntryGate(cwd) {
|
|
3858
|
+
const violations = [];
|
|
3859
|
+
const claudeMd = path6.join(cwd, "CLAUDE.md");
|
|
3860
|
+
if (!await fs11.pathExists(claudeMd)) {
|
|
3861
|
+
violations.push({
|
|
3862
|
+
rule: "claude-md-exists",
|
|
3863
|
+
message: "CLAUDE.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 `pai init`\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
|
|
3864
|
+
location: "CLAUDE.md"
|
|
3865
|
+
});
|
|
3866
|
+
}
|
|
3867
|
+
const configJson = path6.join(cwd, ".pai", "config.json");
|
|
3868
|
+
if (!await fs11.pathExists(configJson)) {
|
|
3869
|
+
violations.push({
|
|
3870
|
+
rule: "pai-config-exists",
|
|
3871
|
+
message: "PAI \uC124\uC815\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. `pai init`\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
|
|
3872
|
+
location: ".pai/config.json"
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
return makeResult("design.entry", violations);
|
|
3876
|
+
}
|
|
3877
|
+
async function executionEntryGate(cwd) {
|
|
3878
|
+
const violations = [];
|
|
3879
|
+
const openspec = path6.join(cwd, "docs", "openspec.md");
|
|
3880
|
+
const hasOpenspec = await fs11.pathExists(openspec);
|
|
3881
|
+
if (!hasOpenspec) {
|
|
3882
|
+
violations.push({
|
|
3883
|
+
rule: "openspec-exists",
|
|
3884
|
+
message: "docs/openspec.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
3885
|
+
location: "docs/openspec.md"
|
|
3886
|
+
});
|
|
3887
|
+
} else {
|
|
3888
|
+
const content = await fs11.readFile(openspec, "utf8");
|
|
3889
|
+
const endpointCount = countEndpoints(content);
|
|
3890
|
+
if (endpointCount < 1) {
|
|
3891
|
+
violations.push({
|
|
3892
|
+
rule: "openspec-endpoints-min",
|
|
3893
|
+
message: `openspec.md\uC5D0 \uC815\uC758\uB41C API \uC5D4\uB4DC\uD3EC\uC778\uD2B8\uAC00 0\uAC1C\uC785\uB2C8\uB2E4. \uCD5C\uC18C 1\uAC1C \uD544\uC694. /pai design \uC2E4\uD589 \uAD8C\uC7A5.`,
|
|
3894
|
+
location: "docs/openspec.md \xA7 5. API \uC5D4\uB4DC\uD3EC\uC778\uD2B8"
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
const omc = path6.join(cwd, ".pai", "omc.md");
|
|
3899
|
+
if (await fs11.pathExists(omc)) {
|
|
3900
|
+
const content = await fs11.readFile(omc, "utf8");
|
|
3901
|
+
const domainCount = countDomainObjects(content);
|
|
3902
|
+
if (domainCount < 1) {
|
|
3903
|
+
violations.push({
|
|
3904
|
+
rule: "omc-domain-min",
|
|
3905
|
+
message: "omc.md\uC5D0 \uB3C4\uBA54\uC778 \uAC1D\uCCB4\uAC00 0\uAC1C\uC785\uB2C8\uB2E4. \uCD5C\uC18C 1\uAC1C \uD544\uC694.",
|
|
3906
|
+
location: ".pai/omc.md \xA7 \uB3C4\uBA54\uC778 \uAC1D\uCCB4"
|
|
3907
|
+
});
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
const edgeViolations = await validateEdges(cwd);
|
|
3911
|
+
for (const ev of edgeViolations) {
|
|
3912
|
+
violations.push({
|
|
3913
|
+
rule: `edge-${ev.edge.from}-${ev.edge.to}`,
|
|
3914
|
+
message: ev.message,
|
|
3915
|
+
location: ev.edge.path
|
|
3916
|
+
});
|
|
3917
|
+
}
|
|
3918
|
+
return makeResult("execution.entry", violations);
|
|
3919
|
+
}
|
|
3920
|
+
async function validationEntryGate(cwd) {
|
|
3921
|
+
const violations = [];
|
|
3922
|
+
const srcDir = path6.join(cwd, "src");
|
|
3923
|
+
if (!await fs11.pathExists(srcDir)) {
|
|
3924
|
+
violations.push({
|
|
3925
|
+
rule: "src-dir-exists",
|
|
3926
|
+
message: "src/ \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
3927
|
+
location: "src/"
|
|
3928
|
+
});
|
|
3929
|
+
} else {
|
|
3930
|
+
const codeFileCount = await countCodeFiles(srcDir);
|
|
3931
|
+
if (codeFileCount === 0) {
|
|
3932
|
+
violations.push({
|
|
3933
|
+
rule: "code-files-min",
|
|
3934
|
+
message: "src/ \uB0B4\uBD80\uC5D0 \uCF54\uB4DC \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uCD5C\uC18C 1\uAC1C \uD544\uC694.",
|
|
3935
|
+
location: "src/"
|
|
3936
|
+
});
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
const edgeViolations = await validateEdges(cwd);
|
|
3940
|
+
for (const ev of edgeViolations) {
|
|
3941
|
+
violations.push({
|
|
3942
|
+
rule: `edge-${ev.edge.from}-${ev.edge.to}`,
|
|
3943
|
+
message: ev.message,
|
|
3944
|
+
location: ev.edge.path
|
|
3945
|
+
});
|
|
3946
|
+
}
|
|
3947
|
+
return makeResult("validation.entry", violations);
|
|
3948
|
+
}
|
|
3949
|
+
async function evaluationEntryGate(cwd) {
|
|
3950
|
+
const violations = [];
|
|
3951
|
+
const state = await loadRobocoState(cwd);
|
|
3952
|
+
const recentValidation = [...state.stageHistory].reverse().find((h) => h.stage === "validation");
|
|
3953
|
+
if (!recentValidation) {
|
|
3954
|
+
violations.push({
|
|
3955
|
+
rule: "validation-executed",
|
|
3956
|
+
message: "validation \uB2E8\uACC4\uAC00 \uD55C \uBC88\uB3C4 \uC2E4\uD589\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 `pai test`\uB97C \uC2E4\uD589\uD558\uC138\uC694."
|
|
3957
|
+
});
|
|
3958
|
+
} else if (recentValidation.status !== "done") {
|
|
3959
|
+
violations.push({
|
|
3960
|
+
rule: "validation-passed",
|
|
3961
|
+
message: `\uCD5C\uADFC validation\uC774 ${recentValidation.status} \uC0C1\uD0DC\uC785\uB2C8\uB2E4. \uD14C\uC2A4\uD2B8\uB97C \uC131\uACF5\uC2DC\uD0A8 \uB4A4 \uC2DC\uB3C4\uD558\uC138\uC694.`
|
|
3962
|
+
});
|
|
3963
|
+
} else {
|
|
3964
|
+
const endedAt = recentValidation.endedAt ?? recentValidation.startedAt;
|
|
3965
|
+
const age = Date.now() - new Date(endedAt).getTime();
|
|
3966
|
+
if (age > EVAL_GATE_WINDOW_MS) {
|
|
3967
|
+
const mins = Math.round(age / 6e4);
|
|
3968
|
+
violations.push({
|
|
3969
|
+
rule: "validation-recent",
|
|
3970
|
+
message: `\uCD5C\uADFC validation \uD1B5\uACFC\uAC00 ${mins}\uBD84 \uC804\uC785\uB2C8\uB2E4 (1\uC2DC\uAC04 \uC774\uB0B4\uC5EC\uC57C \uD568). --force\uB85C \uC6B0\uD68C \uAC00\uB2A5.`
|
|
3971
|
+
});
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
return makeResult("evaluation.entry", violations);
|
|
3975
|
+
}
|
|
3976
|
+
function countEndpoints(openspecContent) {
|
|
3977
|
+
const lines = openspecContent.split("\n");
|
|
3978
|
+
let inTable = false;
|
|
3979
|
+
let count = 0;
|
|
3980
|
+
for (const rawLine of lines) {
|
|
3981
|
+
const line = rawLine.trim();
|
|
3982
|
+
if (/^##\s*5\.|^##\s*API|API 엔드포인트/i.test(line)) {
|
|
3983
|
+
inTable = true;
|
|
3984
|
+
continue;
|
|
3985
|
+
}
|
|
3986
|
+
if (!inTable) continue;
|
|
3987
|
+
if (/^##\s/.test(line) && !/5\.|API/i.test(line)) {
|
|
3988
|
+
inTable = false;
|
|
3989
|
+
continue;
|
|
3990
|
+
}
|
|
3991
|
+
if (/^\|\s*Method/i.test(line)) continue;
|
|
3992
|
+
if (/^\|\s*[-:]+\s*\|/.test(line)) continue;
|
|
3993
|
+
const m = line.match(/^\|\s*(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\|\s*(\/\S+)\s*\|/i);
|
|
3994
|
+
if (m) count++;
|
|
3995
|
+
}
|
|
3996
|
+
return count;
|
|
3997
|
+
}
|
|
3998
|
+
function countDomainObjects(omcContent) {
|
|
3999
|
+
const lines = omcContent.split("\n");
|
|
4000
|
+
let count = 0;
|
|
4001
|
+
let inDomainSection = false;
|
|
4002
|
+
for (const rawLine of lines) {
|
|
4003
|
+
const line = rawLine.trim();
|
|
4004
|
+
if (/^##\s*도메인 객체|^##\s*Domain Objects/i.test(line)) {
|
|
4005
|
+
inDomainSection = true;
|
|
4006
|
+
continue;
|
|
4007
|
+
}
|
|
4008
|
+
if (!inDomainSection) continue;
|
|
4009
|
+
if (/^##\s/.test(line) && !/도메인|Domain/i.test(line)) {
|
|
4010
|
+
inDomainSection = false;
|
|
4011
|
+
continue;
|
|
4012
|
+
}
|
|
4013
|
+
if (/^###\s+\w/.test(line)) count++;
|
|
4014
|
+
}
|
|
4015
|
+
return count;
|
|
4016
|
+
}
|
|
4017
|
+
async function validateEdges(cwd) {
|
|
4018
|
+
const config = await loadConfig(cwd);
|
|
4019
|
+
if (!config?.edges || config.edges.length === 0) return [];
|
|
4020
|
+
const violations = [];
|
|
4021
|
+
for (const edge of config.edges) {
|
|
4022
|
+
if (edge.path) {
|
|
4023
|
+
const fullPath = path6.join(cwd, edge.path);
|
|
4024
|
+
if (!await fs11.pathExists(fullPath)) {
|
|
4025
|
+
violations.push({
|
|
4026
|
+
edge,
|
|
4027
|
+
message: `${edge.from}\u2192${edge.to} \uACC4\uC57D \uD30C\uC77C \uC5C6\uC74C: ${edge.path}`
|
|
4028
|
+
});
|
|
4029
|
+
continue;
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
switch (edge.via) {
|
|
4033
|
+
case "specFile":
|
|
4034
|
+
case "endpoints": {
|
|
4035
|
+
const specPath = edge.path ? path6.join(cwd, edge.path) : path6.join(cwd, "docs", "openspec.md");
|
|
4036
|
+
if (await fs11.pathExists(specPath)) {
|
|
4037
|
+
const content = await fs11.readFile(specPath, "utf8");
|
|
4038
|
+
const count = countEndpoints(content);
|
|
4039
|
+
if (count === 0) {
|
|
4040
|
+
violations.push({
|
|
4041
|
+
edge,
|
|
4042
|
+
message: `${edge.from}\u2192${edge.to}: openspec.md\uC5D0 \uC815\uC758\uB41C \uC5D4\uB4DC\uD3EC\uC778\uD2B8\uAC00 0\uAC1C (\uCD5C\uC18C 1\uAC1C \uD544\uC694)`
|
|
4043
|
+
});
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
break;
|
|
4047
|
+
}
|
|
4048
|
+
case "domain": {
|
|
4049
|
+
const omcPath = edge.path ? path6.join(cwd, edge.path) : path6.join(cwd, ".pai", "omc.md");
|
|
4050
|
+
if (await fs11.pathExists(omcPath)) {
|
|
4051
|
+
const content = await fs11.readFile(omcPath, "utf8");
|
|
4052
|
+
const count = countDomainObjects(content);
|
|
4053
|
+
if (count === 0) {
|
|
4054
|
+
violations.push({
|
|
4055
|
+
edge,
|
|
4056
|
+
message: `${edge.from}\u2192${edge.to}: omc.md\uC5D0 \uB3C4\uBA54\uC778 \uAC1D\uCCB4\uAC00 0\uAC1C (\uCD5C\uC18C 1\uAC1C \uD544\uC694)`
|
|
4057
|
+
});
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
break;
|
|
4061
|
+
}
|
|
4062
|
+
case "businessRules": {
|
|
4063
|
+
const omcPath = edge.path ? path6.join(cwd, edge.path) : path6.join(cwd, ".pai", "omc.md");
|
|
4064
|
+
if (await fs11.pathExists(omcPath)) {
|
|
4065
|
+
const content = await fs11.readFile(omcPath, "utf8");
|
|
4066
|
+
const ruleCount = countBusinessRules(content);
|
|
4067
|
+
if (ruleCount === 0) {
|
|
4068
|
+
violations.push({
|
|
4069
|
+
edge,
|
|
4070
|
+
message: `${edge.from}\u2192${edge.to}: omc.md\uC5D0 \uBE44\uC988\uB2C8\uC2A4 \uADDC\uCE59\uC774 0\uAC1C \u2014 harness \uAC80\uC99D \uB300\uC0C1 \uC5C6\uC74C`
|
|
4071
|
+
});
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
break;
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
}
|
|
4078
|
+
return violations;
|
|
4079
|
+
}
|
|
4080
|
+
function countBusinessRules(omcContent) {
|
|
4081
|
+
const lines = omcContent.split("\n");
|
|
4082
|
+
let inSection = false;
|
|
4083
|
+
let count = 0;
|
|
4084
|
+
for (const rawLine of lines) {
|
|
4085
|
+
const line = rawLine.trim();
|
|
4086
|
+
if (/^##\s*비즈니스 규칙|^##\s*Business Rules/i.test(line)) {
|
|
4087
|
+
inSection = true;
|
|
4088
|
+
continue;
|
|
4089
|
+
}
|
|
4090
|
+
if (!inSection) continue;
|
|
4091
|
+
if (/^##\s/.test(line) && !/비즈니스|Business/i.test(line)) {
|
|
4092
|
+
inSection = false;
|
|
4093
|
+
continue;
|
|
4094
|
+
}
|
|
4095
|
+
if (/^-\s+\*\*/.test(line) || /^-\s+[^\s]/.test(line)) count++;
|
|
4096
|
+
}
|
|
4097
|
+
return count;
|
|
4098
|
+
}
|
|
4099
|
+
async function countCodeFiles(srcDir) {
|
|
4100
|
+
const exts = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java", ".kt"]);
|
|
4101
|
+
let count = 0;
|
|
4102
|
+
async function walk(dir) {
|
|
4103
|
+
let entries;
|
|
4104
|
+
try {
|
|
4105
|
+
entries = await fs11.readdir(dir);
|
|
4106
|
+
} catch {
|
|
4107
|
+
return;
|
|
4108
|
+
}
|
|
4109
|
+
for (const name of entries) {
|
|
4110
|
+
if (name.startsWith(".") || name === "node_modules") continue;
|
|
4111
|
+
const full = path6.join(dir, name);
|
|
4112
|
+
const stat = await fs11.stat(full).catch(() => null);
|
|
4113
|
+
if (!stat) continue;
|
|
4114
|
+
if (stat.isDirectory()) {
|
|
4115
|
+
await walk(full);
|
|
4116
|
+
} else if (exts.has(path6.extname(name))) {
|
|
4117
|
+
count++;
|
|
4118
|
+
}
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
await walk(srcDir);
|
|
4122
|
+
return count;
|
|
4123
|
+
}
|
|
4124
|
+
var EVAL_GATE_WINDOW_MS, STAGE_GATES;
|
|
4125
|
+
var init_gates = __esm({
|
|
4126
|
+
"src/core/gates.ts"() {
|
|
4127
|
+
"use strict";
|
|
4128
|
+
init_roboco();
|
|
4129
|
+
init_config();
|
|
4130
|
+
EVAL_GATE_WINDOW_MS = 60 * 60 * 1e3;
|
|
4131
|
+
STAGE_GATES = {
|
|
4132
|
+
environment: async () => makeResult("environment.entry", []),
|
|
4133
|
+
// 항상 통과
|
|
4134
|
+
design: designEntryGate,
|
|
4135
|
+
execution: executionEntryGate,
|
|
4136
|
+
validation: validationEntryGate,
|
|
4137
|
+
evaluation: evaluationEntryGate
|
|
4138
|
+
};
|
|
4139
|
+
}
|
|
4140
|
+
});
|
|
4141
|
+
|
|
4142
|
+
// src/stages/environment/doctor.ts
|
|
4143
|
+
var doctor_exports = {};
|
|
4144
|
+
__export(doctor_exports, {
|
|
4145
|
+
runDoctor: () => runDoctor
|
|
4146
|
+
});
|
|
4147
|
+
import { join as join6 } from "path";
|
|
4148
|
+
import { homedir } from "os";
|
|
4149
|
+
import fs12 from "fs-extra";
|
|
4150
|
+
async function runDoctor() {
|
|
4151
|
+
section("PAI Doctor \u2014 \uD658\uACBD \uC9C4\uB2E8");
|
|
4152
|
+
const checks = [];
|
|
4153
|
+
const nodeVersion = process.version;
|
|
4154
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0], 10);
|
|
4155
|
+
checks.push({
|
|
4156
|
+
label: "Node.js \uBC84\uC804",
|
|
4157
|
+
ok: nodeMajor >= 20,
|
|
4158
|
+
detail: `${nodeVersion} ${nodeMajor >= 20 ? "(>=20 OK)" : "(\uC5C5\uADF8\uB808\uC774\uB4DC \uD544\uC694)"}`,
|
|
4159
|
+
fix: nodeMajor < 20 ? "Node.js 20 \uC774\uC0C1\uC73C\uB85C \uC5C5\uADF8\uB808\uC774\uB4DC\uD558\uC138\uC694" : void 0
|
|
4160
|
+
});
|
|
4161
|
+
const claudeCheck = await checkCommand("claude", ["--version"]);
|
|
4162
|
+
checks.push({
|
|
4163
|
+
label: "Claude Code CLI",
|
|
4164
|
+
ok: claudeCheck.ok,
|
|
4165
|
+
detail: claudeCheck.detail,
|
|
4166
|
+
fix: claudeCheck.ok ? void 0 : "npm install -g @anthropic-ai/claude-code"
|
|
4167
|
+
});
|
|
4168
|
+
const globalConfigPath = join6(homedir(), ".pai", "config.json");
|
|
4169
|
+
const hasGlobalConfig = await fs12.pathExists(globalConfigPath);
|
|
4170
|
+
checks.push({
|
|
4171
|
+
label: "\uAE00\uB85C\uBC8C \uC124\uC815",
|
|
4172
|
+
ok: true,
|
|
4173
|
+
detail: hasGlobalConfig ? globalConfigPath : "\uBBF8\uC124\uC815 (\uAE30\uBCF8\uAC12 \uC0AC\uC6A9)"
|
|
4174
|
+
});
|
|
4175
|
+
let hasSdk = false;
|
|
4176
|
+
try {
|
|
4177
|
+
await import("@anthropic-ai/claude-agent-sdk");
|
|
4178
|
+
hasSdk = true;
|
|
4179
|
+
} catch {
|
|
4180
|
+
}
|
|
4181
|
+
checks.push({
|
|
4182
|
+
label: "Agent SDK",
|
|
4183
|
+
ok: true,
|
|
4184
|
+
// optional이므로 항상 OK
|
|
4185
|
+
detail: hasSdk ? "\uC124\uCE58\uB428 (AI \uAE30\uB2A5 \uD65C\uC131\uD654)" : "\uBBF8\uC124\uCE58 (\uC815\uC801 \uBD84\uC11D \uBAA8\uB4DC)"
|
|
4186
|
+
});
|
|
4187
|
+
const { validateEdges: validateEdges2 } = await Promise.resolve().then(() => (init_gates(), gates_exports));
|
|
4188
|
+
const edgeViolations = await validateEdges2(process.cwd());
|
|
4189
|
+
if (edgeViolations.length > 0) {
|
|
4190
|
+
checks.push({
|
|
4191
|
+
label: "\uACC4\uC57D \uBB34\uACB0\uC131 (edges)",
|
|
4192
|
+
ok: false,
|
|
4193
|
+
detail: `${edgeViolations.length}\uAC74 \uC704\uBC18`,
|
|
4194
|
+
fix: edgeViolations.map((v) => v.message).join("; ")
|
|
4195
|
+
});
|
|
4196
|
+
} else {
|
|
4197
|
+
const { loadConfig: loadCfg } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
4198
|
+
const cfg = await loadCfg(process.cwd());
|
|
4199
|
+
const edgeCount = cfg?.edges?.length ?? 0;
|
|
4200
|
+
checks.push({
|
|
4201
|
+
label: "\uACC4\uC57D \uBB34\uACB0\uC131 (edges)",
|
|
4202
|
+
ok: true,
|
|
4203
|
+
detail: edgeCount > 0 ? `${edgeCount}\uAC1C \uC5E3\uC9C0 \uBAA8\uB450 \uC720\uD6A8` : "\uC5E3\uC9C0 \uC5C6\uC74C (\uC815\uC0C1)"
|
|
4204
|
+
});
|
|
4205
|
+
}
|
|
4206
|
+
console.log("");
|
|
4207
|
+
let passed = 0;
|
|
4208
|
+
for (const check of checks) {
|
|
4209
|
+
const icon = check.ok ? "\u2713" : "\u2717";
|
|
4210
|
+
const pad = " ".repeat(Math.max(1, 20 - check.label.length));
|
|
4211
|
+
if (check.ok) {
|
|
4212
|
+
success(`${icon} ${check.label}${pad}${check.detail}`);
|
|
4213
|
+
passed++;
|
|
4214
|
+
} else {
|
|
4215
|
+
error(`${icon} ${check.label}${pad}${check.detail}`);
|
|
4216
|
+
if (check.fix) {
|
|
4217
|
+
info(` \u2192 ${check.fix}`);
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
console.log("");
|
|
4222
|
+
info(`${passed}/${checks.length} \uD56D\uBAA9 \uD1B5\uACFC`);
|
|
4223
|
+
if (passed < checks.length) {
|
|
4224
|
+
process.exitCode = 1;
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
async function checkCommand(cmd, args) {
|
|
4228
|
+
try {
|
|
4229
|
+
const { execa } = await import("execa");
|
|
4230
|
+
const { stdout } = await execa(cmd, args, { timeout: 1e4 });
|
|
4231
|
+
return { ok: true, detail: stdout.trim().split("\n")[0] ?? "ok" };
|
|
4232
|
+
} catch {
|
|
4233
|
+
return { ok: false, detail: "not found" };
|
|
4234
|
+
}
|
|
4235
|
+
}
|
|
4236
|
+
var init_doctor = __esm({
|
|
4237
|
+
"src/stages/environment/doctor.ts"() {
|
|
4238
|
+
"use strict";
|
|
4239
|
+
init_ui();
|
|
4240
|
+
}
|
|
4241
|
+
});
|
|
4242
|
+
|
|
4243
|
+
// src/utils/github-fetch.ts
|
|
4244
|
+
import path7 from "path";
|
|
4245
|
+
import fs13 from "fs-extra";
|
|
4246
|
+
async function httpGet(url, timeoutMs, accept) {
|
|
4247
|
+
const controller = new AbortController();
|
|
4248
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4249
|
+
try {
|
|
4250
|
+
return await fetch(url, {
|
|
4251
|
+
signal: controller.signal,
|
|
4252
|
+
headers: { "Accept": accept, "User-Agent": "pai-zero" }
|
|
4253
|
+
});
|
|
4254
|
+
} finally {
|
|
4255
|
+
clearTimeout(timer);
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
async function listDir(repo, ref, dirPath, timeoutMs) {
|
|
4259
|
+
const url = `https://api.github.com/repos/${repo}/contents/${encodeURI(dirPath)}?ref=${encodeURIComponent(ref)}`;
|
|
4260
|
+
const res = await httpGet(url, timeoutMs, "application/vnd.github+json");
|
|
4261
|
+
if (!res.ok) {
|
|
4262
|
+
throw new Error(`GitHub API ${res.status} ${res.statusText} (${url})`);
|
|
4263
|
+
}
|
|
4264
|
+
const data = await res.json();
|
|
4265
|
+
if (!Array.isArray(data)) {
|
|
4266
|
+
throw new Error(`Expected directory, got single entry at ${dirPath}`);
|
|
4267
|
+
}
|
|
4268
|
+
return data;
|
|
4269
|
+
}
|
|
4270
|
+
async function downloadFile(downloadUrl, destPath, timeoutMs) {
|
|
4271
|
+
const res = await httpGet(downloadUrl, timeoutMs, "*/*");
|
|
4272
|
+
if (!res.ok) {
|
|
4273
|
+
throw new Error(`Download failed ${res.status} ${res.statusText} (${downloadUrl})`);
|
|
4274
|
+
}
|
|
4275
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
4276
|
+
await fs13.ensureDir(path7.dirname(destPath));
|
|
4277
|
+
await fs13.writeFile(destPath, buf);
|
|
4278
|
+
}
|
|
4279
|
+
async function fetchGithubDir(opts) {
|
|
4280
|
+
const {
|
|
4281
|
+
repo,
|
|
4282
|
+
ref = "main",
|
|
4283
|
+
srcPath,
|
|
4284
|
+
destDir,
|
|
4285
|
+
overwrite = false,
|
|
4286
|
+
timeoutMs = 1e4
|
|
4287
|
+
} = opts;
|
|
4288
|
+
const result = { written: [], skipped: [], errors: [] };
|
|
4289
|
+
async function walk(currentSrc, currentDest) {
|
|
4290
|
+
let entries;
|
|
4291
|
+
try {
|
|
4292
|
+
entries = await listDir(repo, ref, currentSrc, timeoutMs);
|
|
4293
|
+
} catch (err) {
|
|
4294
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4295
|
+
result.errors.push({ path: currentSrc, error: msg });
|
|
4296
|
+
return;
|
|
4297
|
+
}
|
|
4298
|
+
for (const entry of entries) {
|
|
4299
|
+
const relName = entry.name;
|
|
4300
|
+
if (entry.type === "dir") {
|
|
4301
|
+
await walk(`${currentSrc}/${relName}`, path7.join(currentDest, relName));
|
|
4302
|
+
} else if (entry.type === "file") {
|
|
4303
|
+
const destFile = path7.join(currentDest, relName);
|
|
4304
|
+
if (!overwrite && await fs13.pathExists(destFile)) {
|
|
4305
|
+
result.skipped.push(destFile);
|
|
4306
|
+
continue;
|
|
4307
|
+
}
|
|
4308
|
+
if (!entry.download_url) {
|
|
4309
|
+
result.errors.push({ path: entry.path, error: "no download_url" });
|
|
4310
|
+
continue;
|
|
4311
|
+
}
|
|
4312
|
+
try {
|
|
4313
|
+
await downloadFile(entry.download_url, destFile, timeoutMs);
|
|
4314
|
+
result.written.push(destFile);
|
|
4315
|
+
} catch (err) {
|
|
4316
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4317
|
+
result.errors.push({ path: entry.path, error: msg });
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
await walk(srcPath, destDir);
|
|
4323
|
+
return result;
|
|
3764
4324
|
}
|
|
3765
4325
|
var init_github_fetch = __esm({
|
|
3766
4326
|
"src/utils/github-fetch.ts"() {
|
|
@@ -3773,8 +4333,8 @@ var fetch_cmd_exports = {};
|
|
|
3773
4333
|
__export(fetch_cmd_exports, {
|
|
3774
4334
|
fetchCommand: () => fetchCommand
|
|
3775
4335
|
});
|
|
3776
|
-
import
|
|
3777
|
-
import
|
|
4336
|
+
import path8 from "path";
|
|
4337
|
+
import fs14 from "fs-extra";
|
|
3778
4338
|
import chalk4 from "chalk";
|
|
3779
4339
|
async function fetchCommand(cwd, recipeKey, options) {
|
|
3780
4340
|
if (options.list) {
|
|
@@ -3812,7 +4372,7 @@ async function fetchCommand(cwd, recipeKey, options) {
|
|
|
3812
4372
|
const installed = [];
|
|
3813
4373
|
for (const key of keys) {
|
|
3814
4374
|
const recipe = RECIPES[key];
|
|
3815
|
-
const targetDir =
|
|
4375
|
+
const targetDir = path8.join(cwd, recipe.target);
|
|
3816
4376
|
console.log("");
|
|
3817
4377
|
console.log(` ${colors.accent(key)} \u2014 ${recipe.label}`);
|
|
3818
4378
|
const result = await withSpinner(`\uB2E4\uC6B4\uB85C\uB4DC \uC911...`, async () => {
|
|
@@ -3834,7 +4394,7 @@ async function fetchCommand(cwd, recipeKey, options) {
|
|
|
3834
4394
|
if (result.written.length > 0) {
|
|
3835
4395
|
success(`${result.written.length}\uAC1C \uD30C\uC77C \uC800\uC7A5`);
|
|
3836
4396
|
for (const f of result.written) {
|
|
3837
|
-
console.log(chalk4.gray(` ${
|
|
4397
|
+
console.log(chalk4.gray(` ${path8.relative(cwd, f)}`));
|
|
3838
4398
|
}
|
|
3839
4399
|
}
|
|
3840
4400
|
if (result.skipped.length > 0) {
|
|
@@ -3870,10 +4430,10 @@ async function fetchCommand(cwd, recipeKey, options) {
|
|
|
3870
4430
|
}
|
|
3871
4431
|
async function appendEnvKeys(cwd, recipe) {
|
|
3872
4432
|
if (recipe.envKeys.length === 0) return;
|
|
3873
|
-
const envPath =
|
|
4433
|
+
const envPath = path8.join(cwd, ".env.local");
|
|
3874
4434
|
let content = "";
|
|
3875
|
-
if (await
|
|
3876
|
-
content = await
|
|
4435
|
+
if (await fs14.pathExists(envPath)) {
|
|
4436
|
+
content = await fs14.readFile(envPath, "utf8");
|
|
3877
4437
|
}
|
|
3878
4438
|
const missingKeys = recipe.envKeys.filter((ek) => !content.includes(`${ek.key}=`));
|
|
3879
4439
|
if (missingKeys.length === 0) return;
|
|
@@ -3886,13 +4446,13 @@ async function appendEnvKeys(cwd, recipe) {
|
|
|
3886
4446
|
if (ek.hint) lines.push(`# ${ek.hint}`);
|
|
3887
4447
|
lines.push(`${ek.key}=${ek.default ?? ""}`);
|
|
3888
4448
|
}
|
|
3889
|
-
await
|
|
3890
|
-
await
|
|
4449
|
+
await fs14.ensureFile(envPath);
|
|
4450
|
+
await fs14.appendFile(envPath, lines.join("\n") + "\n");
|
|
3891
4451
|
}
|
|
3892
4452
|
async function upsertRecipesSkill(cwd, installedKeys) {
|
|
3893
|
-
const skillDir =
|
|
3894
|
-
await
|
|
3895
|
-
const skillPath =
|
|
4453
|
+
const skillDir = path8.join(cwd, ".claude", "skills", "recipes");
|
|
4454
|
+
await fs14.ensureDir(skillDir);
|
|
4455
|
+
const skillPath = path8.join(skillDir, "SKILL.md");
|
|
3896
4456
|
const recipes = installedKeys.map((k) => RECIPES[k]);
|
|
3897
4457
|
const triggers = recipes.map((r) => r.skillDescription ?? `${r.label} \uAD00\uB828 \uAE30\uB2A5 \uAD6C\uD604 \uC2DC ${r.target}/ \uCC38\uC870`).join("\n- ");
|
|
3898
4458
|
const body = [
|
|
@@ -3931,10 +4491,10 @@ async function upsertRecipesSkill(cwd, installedKeys) {
|
|
|
3931
4491
|
"5. \uAC00\uC774\uB4DC \uC21C\uC11C\uC5D0 \uB530\uB77C \uAD6C\uD604 \u2014 \uB808\uC2DC\uD53C \uADDC\uCE59\uC744 \uC808\uB300 \uC6B0\uD68C\uD558\uC9C0 \uB9D0 \uAC83",
|
|
3932
4492
|
""
|
|
3933
4493
|
].join("\n");
|
|
3934
|
-
await
|
|
4494
|
+
await fs14.writeFile(skillPath, body);
|
|
3935
4495
|
}
|
|
3936
4496
|
async function upsertClaudeMdBlock(cwd, installedKeys) {
|
|
3937
|
-
const claudeMdPath =
|
|
4497
|
+
const claudeMdPath = path8.join(cwd, "CLAUDE.md");
|
|
3938
4498
|
const BLOCK_START = "<!-- pai:recipes:start -->";
|
|
3939
4499
|
const BLOCK_END = "<!-- pai:recipes:end -->";
|
|
3940
4500
|
const lines = [];
|
|
@@ -3954,8 +4514,8 @@ async function upsertClaudeMdBlock(cwd, installedKeys) {
|
|
|
3954
4514
|
lines.push(BLOCK_END);
|
|
3955
4515
|
const block = lines.join("\n");
|
|
3956
4516
|
let content = "";
|
|
3957
|
-
if (await
|
|
3958
|
-
content = await
|
|
4517
|
+
if (await fs14.pathExists(claudeMdPath)) {
|
|
4518
|
+
content = await fs14.readFile(claudeMdPath, "utf8");
|
|
3959
4519
|
}
|
|
3960
4520
|
const startIdx = content.indexOf(BLOCK_START);
|
|
3961
4521
|
const endIdx = content.indexOf(BLOCK_END);
|
|
@@ -3968,8 +4528,8 @@ async function upsertClaudeMdBlock(cwd, installedKeys) {
|
|
|
3968
4528
|
if (content.length > 0) content += "\n";
|
|
3969
4529
|
content += block + "\n";
|
|
3970
4530
|
}
|
|
3971
|
-
await
|
|
3972
|
-
await
|
|
4531
|
+
await fs14.ensureFile(claudeMdPath);
|
|
4532
|
+
await fs14.writeFile(claudeMdPath, content);
|
|
3973
4533
|
}
|
|
3974
4534
|
var init_fetch_cmd = __esm({
|
|
3975
4535
|
"src/cli/commands/fetch.cmd.ts"() {
|
|
@@ -4203,7 +4763,7 @@ __export(analyzer_exports2, {
|
|
|
4203
4763
|
analyzeRepository: () => analyzeRepository
|
|
4204
4764
|
});
|
|
4205
4765
|
import { join as join7 } from "path";
|
|
4206
|
-
import
|
|
4766
|
+
import fs15 from "fs-extra";
|
|
4207
4767
|
async function analyzeRepository(repoPath) {
|
|
4208
4768
|
try {
|
|
4209
4769
|
return await aiAnalysis(repoPath);
|
|
@@ -4264,14 +4824,14 @@ async function checkTestCoverage(repoPath) {
|
|
|
4264
4824
|
".nycrc"
|
|
4265
4825
|
];
|
|
4266
4826
|
for (const f of testConfigs) {
|
|
4267
|
-
const found = await
|
|
4827
|
+
const found = await fs15.pathExists(join7(repoPath, f));
|
|
4268
4828
|
findings.push({ item: f, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
4269
4829
|
if (found) score += 20;
|
|
4270
4830
|
}
|
|
4271
4831
|
const testDirs = ["tests", "test", "__tests__", "spec"];
|
|
4272
4832
|
let hasTestDir = false;
|
|
4273
4833
|
for (const d of testDirs) {
|
|
4274
|
-
if (await
|
|
4834
|
+
if (await fs15.pathExists(join7(repoPath, d))) {
|
|
4275
4835
|
findings.push({ item: d, found: true, details: "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" });
|
|
4276
4836
|
hasTestDir = true;
|
|
4277
4837
|
score += 30;
|
|
@@ -4295,7 +4855,7 @@ async function checkCiCd(repoPath) {
|
|
|
4295
4855
|
{ path: ".circleci", label: "CircleCI" }
|
|
4296
4856
|
];
|
|
4297
4857
|
for (const { path: path10, label } of ciConfigs) {
|
|
4298
|
-
const found = await
|
|
4858
|
+
const found = await fs15.pathExists(join7(repoPath, path10));
|
|
4299
4859
|
findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
4300
4860
|
if (found) score += 40;
|
|
4301
4861
|
}
|
|
@@ -4314,7 +4874,7 @@ async function checkHooks(repoPath) {
|
|
|
4314
4874
|
{ path: ".claude/settings.json", label: "Claude Code settings" }
|
|
4315
4875
|
];
|
|
4316
4876
|
for (const { path: path10, label } of hookConfigs) {
|
|
4317
|
-
const found = await
|
|
4877
|
+
const found = await fs15.pathExists(join7(repoPath, path10));
|
|
4318
4878
|
findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
4319
4879
|
if (found) score += 20;
|
|
4320
4880
|
}
|
|
@@ -4332,7 +4892,7 @@ async function checkRepoStructure(repoPath) {
|
|
|
4332
4892
|
{ path: ".gitignore", label: ".gitignore" }
|
|
4333
4893
|
];
|
|
4334
4894
|
for (const { path: path10, label } of structureChecks) {
|
|
4335
|
-
const found = await
|
|
4895
|
+
const found = await fs15.pathExists(join7(repoPath, path10));
|
|
4336
4896
|
findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
4337
4897
|
if (found) score += 25;
|
|
4338
4898
|
}
|
|
@@ -4349,7 +4909,7 @@ async function checkDocumentation(repoPath) {
|
|
|
4349
4909
|
{ path: "docs/openspec.md", label: "OpenSpec PRD", points: 25 }
|
|
4350
4910
|
];
|
|
4351
4911
|
for (const { path: path10, label, points } of docChecks) {
|
|
4352
|
-
const found = await
|
|
4912
|
+
const found = await fs15.pathExists(join7(repoPath, path10));
|
|
4353
4913
|
findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
4354
4914
|
if (found) score += points;
|
|
4355
4915
|
}
|
|
@@ -4368,7 +4928,7 @@ async function checkHarnessEngineering(repoPath) {
|
|
|
4368
4928
|
{ path: ".pai/config.json", label: "PAI config", points: 10 }
|
|
4369
4929
|
];
|
|
4370
4930
|
for (const { path: path10, label, points } of harnessChecks) {
|
|
4371
|
-
const found = await
|
|
4931
|
+
const found = await fs15.pathExists(join7(repoPath, path10));
|
|
4372
4932
|
findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
4373
4933
|
if (found) score += points;
|
|
4374
4934
|
}
|
|
@@ -4653,821 +5213,398 @@ function buildDetailedReport(result, projectName) {
|
|
|
4653
5213
|
}
|
|
4654
5214
|
lines.push("");
|
|
4655
5215
|
}
|
|
4656
|
-
if (cat.grade === "A" || cat.grade === "B") {
|
|
4657
|
-
lines.push(`**\uD3C9\uAC00:** \uC591\uD638\uD55C \uC0C1\uD0DC\uC785\uB2C8\uB2E4.`);
|
|
4658
|
-
} else if (cat.grade === "C") {
|
|
4659
|
-
lines.push(`**\uD3C9\uAC00:** \uAE30\uBCF8\uC740 \uAC16\uCD94\uACE0 \uC788\uC73C\uB098 \uCD94\uAC00 \uAC1C\uC120\uC774 \uAD8C\uC7A5\uB429\uB2C8\uB2E4.`);
|
|
4660
|
-
} else if (cat.grade === "D") {
|
|
4661
|
-
lines.push(`**\uD3C9\uAC00:** \uAC1C\uC120\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.`);
|
|
4662
|
-
} else {
|
|
4663
|
-
lines.push(`**\uD3C9\uAC00:** \uC2DC\uAE09\uD55C \uAC1C\uC120\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.`);
|
|
4664
|
-
}
|
|
4665
|
-
lines.push("");
|
|
4666
|
-
if (cat.recommendations.length > 0) {
|
|
4667
|
-
lines.push("**\uAD8C\uACE0\uC0AC\uD56D:**");
|
|
4668
|
-
lines.push("");
|
|
4669
|
-
for (const r of cat.recommendations) {
|
|
4670
|
-
const icon = r.severity === "critical" ? "\u{1F534}" : r.severity === "warning" ? "\u{1F7E1}" : "\u2139\uFE0F";
|
|
4671
|
-
lines.push(`- ${icon} ${r.message}`);
|
|
4672
|
-
lines.push(` - \u2192 ${r.action}`);
|
|
4673
|
-
}
|
|
4674
|
-
lines.push("");
|
|
4675
|
-
}
|
|
4676
|
-
lines.push("---");
|
|
4677
|
-
lines.push("");
|
|
4678
|
-
}
|
|
4679
|
-
lines.push("## \uAC1C\uC120 \uB85C\uB4DC\uB9F5");
|
|
4680
|
-
lines.push("");
|
|
4681
|
-
const critical = result.categories.filter((c2) => c2.grade === "F");
|
|
4682
|
-
const warning = result.categories.filter((c2) => c2.grade === "D");
|
|
4683
|
-
const ok = result.categories.filter((c2) => c2.grade === "C" || c2.grade === "B" || c2.grade === "A");
|
|
4684
|
-
if (critical.length > 0) {
|
|
4685
|
-
lines.push("### \uC989\uC2DC \uAC1C\uC120 \uD544\uC694 (F\uB4F1\uAE09)");
|
|
4686
|
-
lines.push("");
|
|
4687
|
-
lines.push("| \uD56D\uBAA9 | \uD604\uC7AC \uC810\uC218 | \uBAA9\uD45C | \uC870\uCE58 |");
|
|
4688
|
-
lines.push("|------|----------|------|------|");
|
|
4689
|
-
for (const c2 of critical) {
|
|
4690
|
-
const action = c2.recommendations[0]?.action ?? "\uC124\uC815 \uCD94\uAC00 \uD544\uC694";
|
|
4691
|
-
lines.push(`| ${c2.name} | ${c2.score}\uC810 (F) | 50\uC810+ (D) | ${action} |`);
|
|
4692
|
-
}
|
|
4693
|
-
lines.push("");
|
|
4694
|
-
}
|
|
4695
|
-
if (warning.length > 0) {
|
|
4696
|
-
lines.push("### \uB2E8\uAE30 \uAC1C\uC120 \uAD8C\uC7A5 (D\uB4F1\uAE09)");
|
|
4697
|
-
lines.push("");
|
|
4698
|
-
lines.push("| \uD56D\uBAA9 | \uD604\uC7AC \uC810\uC218 | \uBAA9\uD45C | \uC870\uCE58 |");
|
|
4699
|
-
lines.push("|------|----------|------|------|");
|
|
4700
|
-
for (const c2 of warning) {
|
|
4701
|
-
const action = c2.recommendations[0]?.action ?? "\uCD94\uAC00 \uC124\uC815 \uAD8C\uC7A5";
|
|
4702
|
-
lines.push(`| ${c2.name} | ${c2.score}\uC810 (D) | 70\uC810+ (C) | ${action} |`);
|
|
4703
|
-
}
|
|
4704
|
-
lines.push("");
|
|
4705
|
-
}
|
|
4706
|
-
if (ok.length > 0) {
|
|
4707
|
-
lines.push("### \uC591\uD638 (C\uB4F1\uAE09 \uC774\uC0C1)");
|
|
4708
|
-
lines.push("");
|
|
4709
|
-
for (const c2 of ok) {
|
|
4710
|
-
lines.push(`- \u2705 ${c2.name}: ${c2.score}\uC810 (${c2.grade})`);
|
|
4711
|
-
}
|
|
4712
|
-
lines.push("");
|
|
4713
|
-
}
|
|
4714
|
-
lines.push("---");
|
|
4715
|
-
lines.push("");
|
|
4716
|
-
lines.push("## \uACB0\uB860");
|
|
4717
|
-
lines.push("");
|
|
4718
|
-
lines.push(result.summary);
|
|
4719
|
-
lines.push("");
|
|
4720
|
-
if (result.totalGrade === "F" || result.totalGrade === "D") {
|
|
4721
|
-
const immediateActions = critical.length + warning.length;
|
|
4722
|
-
lines.push(`\uC989\uC2DC \uAC1C\uC120 \uAC00\uB2A5\uD55C ${immediateActions}\uAC1C \uD56D\uBAA9\uC744 \uC644\uB8CC\uD558\uBA74 \uC810\uC218\uB97C \uD06C\uAC8C \uB04C\uC5B4\uC62C\uB9B4 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`);
|
|
4723
|
-
} else if (result.totalGrade === "C") {
|
|
4724
|
-
lines.push("\uAE30\uBCF8 \uAD6C\uC870\uAC00 \uC798 \uAC16\uCD94\uC5B4\uC838 \uC788\uC73C\uBA70, \uB2E8\uAE30 \uAC1C\uC120 \uD56D\uBAA9\uC744 \uB9C8\uBB34\uB9AC\uD558\uBA74 B\uB4F1\uAE09 \uB2EC\uC131\uC774 \uAC00\uB2A5\uD569\uB2C8\uB2E4.");
|
|
4725
|
-
} else {
|
|
4726
|
-
lines.push("\uBC14\uC774\uBE0C \uCF54\uB529 \uD658\uACBD\uC774 \uC798 \uAD6C\uC131\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uC9C0\uC18D\uC801\uC778 \uD488\uC9C8 \uAD00\uB9AC\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.");
|
|
4727
|
-
}
|
|
4728
|
-
lines.push("");
|
|
4729
|
-
lines.push("---");
|
|
4730
|
-
lines.push("");
|
|
4731
|
-
lines.push("*PAI Zero (Plugin AI for ProjectZero) + Claude Code\uB85C \uC0DD\uC131\uB428*");
|
|
4732
|
-
return lines.join("\n") + "\n";
|
|
4733
|
-
}
|
|
4734
|
-
var GRADE_COLORS;
|
|
4735
|
-
var init_reporter = __esm({
|
|
4736
|
-
"src/stages/evaluation/reporter.ts"() {
|
|
4737
|
-
"use strict";
|
|
4738
|
-
GRADE_COLORS = {
|
|
4739
|
-
A: chalk5.hex("#6BCB77"),
|
|
4740
|
-
B: chalk5.hex("#7B93DB"),
|
|
4741
|
-
C: chalk5.hex("#E2B340"),
|
|
4742
|
-
D: chalk5.hex("#E06C75"),
|
|
4743
|
-
F: chalk5.hex("#CC4444")
|
|
4744
|
-
};
|
|
4745
|
-
}
|
|
4746
|
-
});
|
|
4747
|
-
|
|
4748
|
-
// src/utils/shell-cd.ts
|
|
4749
|
-
var shell_cd_exports = {};
|
|
4750
|
-
__export(shell_cd_exports, {
|
|
4751
|
-
installShellHelper: () => installShellHelper,
|
|
4752
|
-
requestCdAfter: () => requestCdAfter
|
|
4753
|
-
});
|
|
4754
|
-
import { join as join8 } from "path";
|
|
4755
|
-
import { homedir as homedir2 } from "os";
|
|
4756
|
-
import fs14 from "fs-extra";
|
|
4757
|
-
async function requestCdAfter(targetDir) {
|
|
4758
|
-
await fs14.ensureDir(PAI_DIR);
|
|
4759
|
-
await fs14.writeFile(CD_FILE, targetDir);
|
|
4760
|
-
}
|
|
4761
|
-
async function installShellHelper() {
|
|
4762
|
-
await fs14.ensureDir(PAI_DIR);
|
|
4763
|
-
if (isWindows) {
|
|
4764
|
-
return installPowerShellHelper();
|
|
4765
|
-
}
|
|
4766
|
-
return installBashHelper();
|
|
4767
|
-
}
|
|
4768
|
-
async function installBashHelper() {
|
|
4769
|
-
await fs14.writeFile(HELPER_FILE_SH, BASH_HELPER);
|
|
4770
|
-
const rcFile = getShellRcPath();
|
|
4771
|
-
const sourceLine = 'source "$HOME/.pai/shell-helper.sh"';
|
|
4772
|
-
if (await fs14.pathExists(rcFile)) {
|
|
4773
|
-
const content = await fs14.readFile(rcFile, "utf8");
|
|
4774
|
-
if (content.includes("shell-helper.sh")) {
|
|
4775
|
-
return true;
|
|
4776
|
-
}
|
|
4777
|
-
await fs14.appendFile(rcFile, `
|
|
4778
|
-
# PAI \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9
|
|
4779
|
-
${sourceLine}
|
|
4780
|
-
`);
|
|
4781
|
-
return false;
|
|
4782
|
-
}
|
|
4783
|
-
await fs14.writeFile(rcFile, `${sourceLine}
|
|
4784
|
-
`);
|
|
4785
|
-
return false;
|
|
4786
|
-
}
|
|
4787
|
-
async function installPowerShellHelper() {
|
|
4788
|
-
await fs14.writeFile(HELPER_FILE_PS1, POWERSHELL_HELPER);
|
|
4789
|
-
const rcFile = getShellRcPath();
|
|
4790
|
-
const sourceLine = '. "$env:USERPROFILE\\.pai\\shell-helper.ps1"';
|
|
4791
|
-
await fs14.ensureDir(join8(rcFile, ".."));
|
|
4792
|
-
if (await fs14.pathExists(rcFile)) {
|
|
4793
|
-
const content = await fs14.readFile(rcFile, "utf8");
|
|
4794
|
-
if (content.includes("shell-helper.ps1")) {
|
|
4795
|
-
return true;
|
|
4796
|
-
}
|
|
4797
|
-
await fs14.appendFile(rcFile, `
|
|
4798
|
-
# PAI \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9
|
|
4799
|
-
${sourceLine}
|
|
4800
|
-
`);
|
|
4801
|
-
return false;
|
|
4802
|
-
}
|
|
4803
|
-
await fs14.writeFile(rcFile, `${sourceLine}
|
|
4804
|
-
`);
|
|
4805
|
-
return false;
|
|
4806
|
-
}
|
|
4807
|
-
var PAI_DIR, CD_FILE, HELPER_FILE_SH, HELPER_FILE_PS1, BASH_HELPER, POWERSHELL_HELPER;
|
|
4808
|
-
var init_shell_cd = __esm({
|
|
4809
|
-
"src/utils/shell-cd.ts"() {
|
|
4810
|
-
"use strict";
|
|
4811
|
-
init_platform();
|
|
4812
|
-
PAI_DIR = join8(homedir2(), ".pai");
|
|
4813
|
-
CD_FILE = join8(PAI_DIR, ".cd-after");
|
|
4814
|
-
HELPER_FILE_SH = join8(PAI_DIR, "shell-helper.sh");
|
|
4815
|
-
HELPER_FILE_PS1 = join8(PAI_DIR, "shell-helper.ps1");
|
|
4816
|
-
BASH_HELPER = `# PAI shell helper \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9 \uC9C0\uC6D0
|
|
4817
|
-
pai() {
|
|
4818
|
-
local cd_target="$HOME/.pai/.cd-after"
|
|
4819
|
-
rm -f "$cd_target"
|
|
4820
|
-
PAI_CD_AFTER="$cd_target" command npx pai-zero "$@"
|
|
4821
|
-
local exit_code=$?
|
|
4822
|
-
if [ -f "$cd_target" ]; then
|
|
4823
|
-
local dir
|
|
4824
|
-
dir="$(cat "$cd_target")"
|
|
4825
|
-
rm -f "$cd_target"
|
|
4826
|
-
if [ -n "$dir" ] && [ -d "$dir" ]; then
|
|
4827
|
-
cd "$dir" || true
|
|
4828
|
-
echo " \u2192 cd $dir"
|
|
4829
|
-
fi
|
|
4830
|
-
fi
|
|
4831
|
-
return $exit_code
|
|
4832
|
-
}
|
|
4833
|
-
`;
|
|
4834
|
-
POWERSHELL_HELPER = `# PAI shell helper \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9 \uC9C0\uC6D0
|
|
4835
|
-
function pai {
|
|
4836
|
-
$cdTarget = Join-Path $env:USERPROFILE '.pai\\.cd-after'
|
|
4837
|
-
Remove-Item $cdTarget -ErrorAction SilentlyContinue
|
|
4838
|
-
$env:PAI_CD_AFTER = $cdTarget
|
|
4839
|
-
& npx pai-zero @args
|
|
4840
|
-
$exitCode = $LASTEXITCODE
|
|
4841
|
-
if (Test-Path $cdTarget) {
|
|
4842
|
-
$dir = Get-Content $cdTarget -Raw
|
|
4843
|
-
Remove-Item $cdTarget -ErrorAction SilentlyContinue
|
|
4844
|
-
if ($dir -and (Test-Path $dir)) {
|
|
4845
|
-
Set-Location $dir
|
|
4846
|
-
Write-Host " -> cd $dir"
|
|
4847
|
-
}
|
|
4848
|
-
}
|
|
4849
|
-
return $exitCode
|
|
4850
|
-
}
|
|
4851
|
-
`;
|
|
4852
|
-
}
|
|
4853
|
-
});
|
|
4854
|
-
|
|
4855
|
-
// src/utils/claude-settings.ts
|
|
4856
|
-
var claude_settings_exports = {};
|
|
4857
|
-
__export(claude_settings_exports, {
|
|
4858
|
-
ClaudeSettingsError: () => ClaudeSettingsError,
|
|
4859
|
-
buildSkeleton: () => buildSkeleton,
|
|
4860
|
-
enableOmcPlugin: () => enableOmcPlugin,
|
|
4861
|
-
getClaudeSettingsPath: () => getClaudeSettingsPath,
|
|
4862
|
-
isAlreadyEnabled: () => isAlreadyEnabled,
|
|
4863
|
-
mergeOmcIntoSettings: () => mergeOmcIntoSettings
|
|
4864
|
-
});
|
|
4865
|
-
import os2 from "os";
|
|
4866
|
-
import path7 from "path";
|
|
4867
|
-
import fs15 from "fs-extra";
|
|
4868
|
-
function getClaudeSettingsPath(homeDir = os2.homedir()) {
|
|
4869
|
-
return path7.join(homeDir, ".claude", "settings.json");
|
|
4870
|
-
}
|
|
4871
|
-
function parseJsonWithBom(raw) {
|
|
4872
|
-
const stripped = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
|
|
4873
|
-
return JSON.parse(stripped);
|
|
4874
|
-
}
|
|
4875
|
-
function timestampSuffix() {
|
|
4876
|
-
return sanitizeFilenameForWindows((/* @__PURE__ */ new Date()).toISOString());
|
|
4877
|
-
}
|
|
4878
|
-
async function enableOmcPlugin(options = {}) {
|
|
4879
|
-
const marketplaceId = options.marketplaceId ?? DEFAULT_MARKETPLACE_ID;
|
|
4880
|
-
const marketplaceUrl = options.marketplaceUrl ?? DEFAULT_MARKETPLACE_URL;
|
|
4881
|
-
const pluginId = options.pluginId ?? DEFAULT_PLUGIN_ID;
|
|
4882
|
-
const wantBackup = options.backup ?? true;
|
|
4883
|
-
const settingsPath = getClaudeSettingsPath();
|
|
4884
|
-
await fs15.ensureDir(path7.dirname(settingsPath));
|
|
4885
|
-
if (!await fs15.pathExists(settingsPath)) {
|
|
4886
|
-
const skeleton = buildSkeleton(marketplaceId, marketplaceUrl, pluginId);
|
|
4887
|
-
await fs15.writeFile(settingsPath, JSON.stringify(skeleton, null, 2) + "\n", "utf8");
|
|
4888
|
-
return { action: "created", settingsPath };
|
|
4889
|
-
}
|
|
4890
|
-
const raw = await fs15.readFile(settingsPath, "utf8");
|
|
4891
|
-
let parsed;
|
|
4892
|
-
try {
|
|
4893
|
-
const value = parseJsonWithBom(raw);
|
|
4894
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4895
|
-
throw new Error("settings.json is not a JSON object");
|
|
4896
|
-
}
|
|
4897
|
-
parsed = value;
|
|
4898
|
-
} catch (err) {
|
|
4899
|
-
const backupPath2 = `${settingsPath}.backup-${timestampSuffix()}`;
|
|
4900
|
-
await fs15.copy(settingsPath, backupPath2);
|
|
4901
|
-
throw new ClaudeSettingsError(
|
|
4902
|
-
`settings.json \uD30C\uC2F1 \uC2E4\uD328: ${err.message}. \uBC31\uC5C5: ${backupPath2}`,
|
|
4903
|
-
backupPath2
|
|
4904
|
-
);
|
|
4905
|
-
}
|
|
4906
|
-
if (isAlreadyEnabled(parsed, marketplaceId, pluginId)) {
|
|
4907
|
-
return { action: "already-enabled", settingsPath };
|
|
4908
|
-
}
|
|
4909
|
-
let backupPath;
|
|
4910
|
-
if (wantBackup) {
|
|
4911
|
-
backupPath = `${settingsPath}.backup-${timestampSuffix()}`;
|
|
4912
|
-
await fs15.copy(settingsPath, backupPath);
|
|
4913
|
-
}
|
|
4914
|
-
const merged = mergeOmcIntoSettings(parsed, marketplaceId, marketplaceUrl, pluginId);
|
|
4915
|
-
await fs15.writeFile(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
4916
|
-
return { action: "added", settingsPath, backupPath };
|
|
4917
|
-
}
|
|
4918
|
-
function buildSkeleton(marketplaceId, marketplaceUrl, pluginId) {
|
|
4919
|
-
return {
|
|
4920
|
-
extraKnownMarketplaces: {
|
|
4921
|
-
[marketplaceId]: {
|
|
4922
|
-
source: { source: "git", url: marketplaceUrl }
|
|
4923
|
-
}
|
|
4924
|
-
},
|
|
4925
|
-
enabledPlugins: {
|
|
4926
|
-
[pluginId]: true
|
|
4927
|
-
}
|
|
4928
|
-
};
|
|
4929
|
-
}
|
|
4930
|
-
function isAlreadyEnabled(settings, marketplaceId, pluginId) {
|
|
4931
|
-
const markets = settings["extraKnownMarketplaces"];
|
|
4932
|
-
const enabled = settings["enabledPlugins"];
|
|
4933
|
-
const hasMarket = typeof markets === "object" && markets !== null && marketplaceId in markets;
|
|
4934
|
-
const hasPlugin = typeof enabled === "object" && enabled !== null && enabled[pluginId] === true;
|
|
4935
|
-
return hasMarket && hasPlugin;
|
|
4936
|
-
}
|
|
4937
|
-
function mergeOmcIntoSettings(settings, marketplaceId, marketplaceUrl, pluginId) {
|
|
4938
|
-
const next = { ...settings };
|
|
4939
|
-
const existingMarkets = typeof next["extraKnownMarketplaces"] === "object" && next["extraKnownMarketplaces"] !== null ? { ...next["extraKnownMarketplaces"] } : {};
|
|
4940
|
-
existingMarkets[marketplaceId] = {
|
|
4941
|
-
source: { source: "git", url: marketplaceUrl }
|
|
4942
|
-
};
|
|
4943
|
-
next["extraKnownMarketplaces"] = existingMarkets;
|
|
4944
|
-
const existingPlugins = typeof next["enabledPlugins"] === "object" && next["enabledPlugins"] !== null ? { ...next["enabledPlugins"] } : {};
|
|
4945
|
-
existingPlugins[pluginId] = true;
|
|
4946
|
-
next["enabledPlugins"] = existingPlugins;
|
|
4947
|
-
return next;
|
|
4948
|
-
}
|
|
4949
|
-
var DEFAULT_MARKETPLACE_ID, DEFAULT_MARKETPLACE_URL, DEFAULT_PLUGIN_ID, ClaudeSettingsError;
|
|
4950
|
-
var init_claude_settings = __esm({
|
|
4951
|
-
"src/utils/claude-settings.ts"() {
|
|
4952
|
-
"use strict";
|
|
4953
|
-
init_platform();
|
|
4954
|
-
DEFAULT_MARKETPLACE_ID = "omc";
|
|
4955
|
-
DEFAULT_MARKETPLACE_URL = "https://github.com/SoInKyu/oh-my-claudecode.git";
|
|
4956
|
-
DEFAULT_PLUGIN_ID = "oh-my-claudecode@omc";
|
|
4957
|
-
ClaudeSettingsError = class extends Error {
|
|
4958
|
-
constructor(message, backupPath) {
|
|
4959
|
-
super(message);
|
|
4960
|
-
this.backupPath = backupPath;
|
|
4961
|
-
this.name = "ClaudeSettingsError";
|
|
5216
|
+
if (cat.grade === "A" || cat.grade === "B") {
|
|
5217
|
+
lines.push(`**\uD3C9\uAC00:** \uC591\uD638\uD55C \uC0C1\uD0DC\uC785\uB2C8\uB2E4.`);
|
|
5218
|
+
} else if (cat.grade === "C") {
|
|
5219
|
+
lines.push(`**\uD3C9\uAC00:** \uAE30\uBCF8\uC740 \uAC16\uCD94\uACE0 \uC788\uC73C\uB098 \uCD94\uAC00 \uAC1C\uC120\uC774 \uAD8C\uC7A5\uB429\uB2C8\uB2E4.`);
|
|
5220
|
+
} else if (cat.grade === "D") {
|
|
5221
|
+
lines.push(`**\uD3C9\uAC00:** \uAC1C\uC120\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.`);
|
|
5222
|
+
} else {
|
|
5223
|
+
lines.push(`**\uD3C9\uAC00:** \uC2DC\uAE09\uD55C \uAC1C\uC120\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.`);
|
|
5224
|
+
}
|
|
5225
|
+
lines.push("");
|
|
5226
|
+
if (cat.recommendations.length > 0) {
|
|
5227
|
+
lines.push("**\uAD8C\uACE0\uC0AC\uD56D:**");
|
|
5228
|
+
lines.push("");
|
|
5229
|
+
for (const r of cat.recommendations) {
|
|
5230
|
+
const icon = r.severity === "critical" ? "\u{1F534}" : r.severity === "warning" ? "\u{1F7E1}" : "\u2139\uFE0F";
|
|
5231
|
+
lines.push(`- ${icon} ${r.message}`);
|
|
5232
|
+
lines.push(` - \u2192 ${r.action}`);
|
|
4962
5233
|
}
|
|
4963
|
-
|
|
4964
|
-
};
|
|
4965
|
-
}
|
|
4966
|
-
});
|
|
4967
|
-
|
|
4968
|
-
// src/stages/evaluation/cache.ts
|
|
4969
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
4970
|
-
import { join as join9 } from "path";
|
|
4971
|
-
import { createHash } from "crypto";
|
|
4972
|
-
function computeRepoHash(repoPath) {
|
|
4973
|
-
const hash = createHash("sha256");
|
|
4974
|
-
for (const file of FILES_TO_HASH) {
|
|
4975
|
-
const fullPath = join9(repoPath, file);
|
|
4976
|
-
try {
|
|
4977
|
-
const content = readFileSync(fullPath);
|
|
4978
|
-
hash.update(`${file}:${content.length}`);
|
|
4979
|
-
} catch {
|
|
4980
|
-
hash.update(`${file}:missing`);
|
|
5234
|
+
lines.push("");
|
|
4981
5235
|
}
|
|
5236
|
+
lines.push("---");
|
|
5237
|
+
lines.push("");
|
|
4982
5238
|
}
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
5239
|
+
lines.push("## \uAC1C\uC120 \uB85C\uB4DC\uB9F5");
|
|
5240
|
+
lines.push("");
|
|
5241
|
+
const critical = result.categories.filter((c2) => c2.grade === "F");
|
|
5242
|
+
const warning = result.categories.filter((c2) => c2.grade === "D");
|
|
5243
|
+
const ok = result.categories.filter((c2) => c2.grade === "C" || c2.grade === "B" || c2.grade === "A");
|
|
5244
|
+
if (critical.length > 0) {
|
|
5245
|
+
lines.push("### \uC989\uC2DC \uAC1C\uC120 \uD544\uC694 (F\uB4F1\uAE09)");
|
|
5246
|
+
lines.push("");
|
|
5247
|
+
lines.push("| \uD56D\uBAA9 | \uD604\uC7AC \uC810\uC218 | \uBAA9\uD45C | \uC870\uCE58 |");
|
|
5248
|
+
lines.push("|------|----------|------|------|");
|
|
5249
|
+
for (const c2 of critical) {
|
|
5250
|
+
const action = c2.recommendations[0]?.action ?? "\uC124\uC815 \uCD94\uAC00 \uD544\uC694";
|
|
5251
|
+
lines.push(`| ${c2.name} | ${c2.score}\uC810 (F) | 50\uC810+ (D) | ${action} |`);
|
|
5252
|
+
}
|
|
5253
|
+
lines.push("");
|
|
4994
5254
|
}
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5255
|
+
if (warning.length > 0) {
|
|
5256
|
+
lines.push("### \uB2E8\uAE30 \uAC1C\uC120 \uAD8C\uC7A5 (D\uB4F1\uAE09)");
|
|
5257
|
+
lines.push("");
|
|
5258
|
+
lines.push("| \uD56D\uBAA9 | \uD604\uC7AC \uC810\uC218 | \uBAA9\uD45C | \uC870\uCE58 |");
|
|
5259
|
+
lines.push("|------|----------|------|------|");
|
|
5260
|
+
for (const c2 of warning) {
|
|
5261
|
+
const action = c2.recommendations[0]?.action ?? "\uCD94\uAC00 \uC124\uC815 \uAD8C\uC7A5";
|
|
5262
|
+
lines.push(`| ${c2.name} | ${c2.score}\uC810 (D) | 70\uC810+ (C) | ${action} |`);
|
|
5263
|
+
}
|
|
5264
|
+
lines.push("");
|
|
5000
5265
|
}
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
const entry = store.entries[repoHash];
|
|
5007
|
-
if (!entry) return null;
|
|
5008
|
-
if (Date.now() - entry.timestamp > CACHE_TTL_MS) return null;
|
|
5009
|
-
return entry.llmOutput;
|
|
5010
|
-
}
|
|
5011
|
-
function setCachedResult(repoPath, llmOutput) {
|
|
5012
|
-
const store = loadCache(repoPath);
|
|
5013
|
-
const repoHash = computeRepoHash(repoPath);
|
|
5014
|
-
const now2 = Date.now();
|
|
5015
|
-
for (const [key, entry] of Object.entries(store.entries)) {
|
|
5016
|
-
if (now2 - entry.timestamp > CACHE_TTL_MS) {
|
|
5017
|
-
delete store.entries[key];
|
|
5266
|
+
if (ok.length > 0) {
|
|
5267
|
+
lines.push("### \uC591\uD638 (C\uB4F1\uAE09 \uC774\uC0C1)");
|
|
5268
|
+
lines.push("");
|
|
5269
|
+
for (const c2 of ok) {
|
|
5270
|
+
lines.push(`- \u2705 ${c2.name}: ${c2.score}\uC810 (${c2.grade})`);
|
|
5018
5271
|
}
|
|
5272
|
+
lines.push("");
|
|
5019
5273
|
}
|
|
5020
|
-
|
|
5021
|
-
|
|
5274
|
+
lines.push("---");
|
|
5275
|
+
lines.push("");
|
|
5276
|
+
lines.push("## \uACB0\uB860");
|
|
5277
|
+
lines.push("");
|
|
5278
|
+
lines.push(result.summary);
|
|
5279
|
+
lines.push("");
|
|
5280
|
+
if (result.totalGrade === "F" || result.totalGrade === "D") {
|
|
5281
|
+
const immediateActions = critical.length + warning.length;
|
|
5282
|
+
lines.push(`\uC989\uC2DC \uAC1C\uC120 \uAC00\uB2A5\uD55C ${immediateActions}\uAC1C \uD56D\uBAA9\uC744 \uC644\uB8CC\uD558\uBA74 \uC810\uC218\uB97C \uD06C\uAC8C \uB04C\uC5B4\uC62C\uB9B4 \uC218 \uC788\uC2B5\uB2C8\uB2E4.`);
|
|
5283
|
+
} else if (result.totalGrade === "C") {
|
|
5284
|
+
lines.push("\uAE30\uBCF8 \uAD6C\uC870\uAC00 \uC798 \uAC16\uCD94\uC5B4\uC838 \uC788\uC73C\uBA70, \uB2E8\uAE30 \uAC1C\uC120 \uD56D\uBAA9\uC744 \uB9C8\uBB34\uB9AC\uD558\uBA74 B\uB4F1\uAE09 \uB2EC\uC131\uC774 \uAC00\uB2A5\uD569\uB2C8\uB2E4.");
|
|
5285
|
+
} else {
|
|
5286
|
+
lines.push("\uBC14\uC774\uBE0C \uCF54\uB529 \uD658\uACBD\uC774 \uC798 \uAD6C\uC131\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uC9C0\uC18D\uC801\uC778 \uD488\uC9C8 \uAD00\uB9AC\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4.");
|
|
5287
|
+
}
|
|
5288
|
+
lines.push("");
|
|
5289
|
+
lines.push("---");
|
|
5290
|
+
lines.push("");
|
|
5291
|
+
lines.push("*PAI Zero (Plugin AI for ProjectZero) + Claude Code\uB85C \uC0DD\uC131\uB428*");
|
|
5292
|
+
return lines.join("\n") + "\n";
|
|
5022
5293
|
}
|
|
5023
|
-
var
|
|
5024
|
-
var
|
|
5025
|
-
"src/stages/evaluation/
|
|
5294
|
+
var GRADE_COLORS;
|
|
5295
|
+
var init_reporter = __esm({
|
|
5296
|
+
"src/stages/evaluation/reporter.ts"() {
|
|
5026
5297
|
"use strict";
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
"Cargo.toml",
|
|
5035
|
-
"tsconfig.json",
|
|
5036
|
-
"jest.config.ts",
|
|
5037
|
-
"vitest.config.ts",
|
|
5038
|
-
".github/workflows",
|
|
5039
|
-
".gitlab-ci.yml",
|
|
5040
|
-
".husky",
|
|
5041
|
-
".lintstagedrc",
|
|
5042
|
-
"CLAUDE.md",
|
|
5043
|
-
"AGENTS.md",
|
|
5044
|
-
".cursorrules",
|
|
5045
|
-
"README.md",
|
|
5046
|
-
"CONTRIBUTING.md"
|
|
5047
|
-
];
|
|
5298
|
+
GRADE_COLORS = {
|
|
5299
|
+
A: chalk5.hex("#6BCB77"),
|
|
5300
|
+
B: chalk5.hex("#7B93DB"),
|
|
5301
|
+
C: chalk5.hex("#E2B340"),
|
|
5302
|
+
D: chalk5.hex("#E06C75"),
|
|
5303
|
+
F: chalk5.hex("#CC4444")
|
|
5304
|
+
};
|
|
5048
5305
|
}
|
|
5049
5306
|
});
|
|
5050
5307
|
|
|
5051
|
-
// src/
|
|
5052
|
-
|
|
5053
|
-
|
|
5308
|
+
// src/utils/shell-cd.ts
|
|
5309
|
+
var shell_cd_exports = {};
|
|
5310
|
+
__export(shell_cd_exports, {
|
|
5311
|
+
installShellHelper: () => installShellHelper,
|
|
5312
|
+
requestCdAfter: () => requestCdAfter
|
|
5313
|
+
});
|
|
5314
|
+
import { join as join8 } from "path";
|
|
5315
|
+
import { homedir as homedir2 } from "os";
|
|
5054
5316
|
import fs16 from "fs-extra";
|
|
5055
|
-
function
|
|
5056
|
-
|
|
5317
|
+
async function requestCdAfter(targetDir) {
|
|
5318
|
+
await fs16.ensureDir(PAI_DIR);
|
|
5319
|
+
await fs16.writeFile(CD_FILE, targetDir);
|
|
5057
5320
|
}
|
|
5058
|
-
async function
|
|
5059
|
-
|
|
5060
|
-
if (
|
|
5061
|
-
|
|
5062
|
-
const raw = await fs16.readJson(p);
|
|
5063
|
-
if (raw?.version !== "2.0") return freshState();
|
|
5064
|
-
return raw;
|
|
5065
|
-
} catch {
|
|
5066
|
-
return freshState();
|
|
5321
|
+
async function installShellHelper() {
|
|
5322
|
+
await fs16.ensureDir(PAI_DIR);
|
|
5323
|
+
if (isWindows) {
|
|
5324
|
+
return installPowerShellHelper();
|
|
5067
5325
|
}
|
|
5326
|
+
return installBashHelper();
|
|
5068
5327
|
}
|
|
5069
|
-
async function
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
}
|
|
5083
|
-
function isProcessAlive(pid) {
|
|
5084
|
-
try {
|
|
5085
|
-
process.kill(pid, 0);
|
|
5086
|
-
return true;
|
|
5087
|
-
} catch {
|
|
5328
|
+
async function installBashHelper() {
|
|
5329
|
+
await fs16.writeFile(HELPER_FILE_SH, BASH_HELPER);
|
|
5330
|
+
const rcFile = getShellRcPath();
|
|
5331
|
+
const sourceLine = 'source "$HOME/.pai/shell-helper.sh"';
|
|
5332
|
+
if (await fs16.pathExists(rcFile)) {
|
|
5333
|
+
const content = await fs16.readFile(rcFile, "utf8");
|
|
5334
|
+
if (content.includes("shell-helper.sh")) {
|
|
5335
|
+
return true;
|
|
5336
|
+
}
|
|
5337
|
+
await fs16.appendFile(rcFile, `
|
|
5338
|
+
# PAI \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9
|
|
5339
|
+
${sourceLine}
|
|
5340
|
+
`);
|
|
5088
5341
|
return false;
|
|
5089
5342
|
}
|
|
5343
|
+
await fs16.writeFile(rcFile, `${sourceLine}
|
|
5344
|
+
`);
|
|
5345
|
+
return false;
|
|
5090
5346
|
}
|
|
5091
|
-
function
|
|
5092
|
-
|
|
5093
|
-
|
|
5347
|
+
async function installPowerShellHelper() {
|
|
5348
|
+
await fs16.writeFile(HELPER_FILE_PS1, POWERSHELL_HELPER);
|
|
5349
|
+
const rcFile = getShellRcPath();
|
|
5350
|
+
const sourceLine = '. "$env:USERPROFILE\\.pai\\shell-helper.ps1"';
|
|
5351
|
+
await fs16.ensureDir(join8(rcFile, ".."));
|
|
5352
|
+
if (await fs16.pathExists(rcFile)) {
|
|
5353
|
+
const content = await fs16.readFile(rcFile, "utf8");
|
|
5354
|
+
if (content.includes("shell-helper.ps1")) {
|
|
5355
|
+
return true;
|
|
5356
|
+
}
|
|
5357
|
+
await fs16.appendFile(rcFile, `
|
|
5358
|
+
# PAI \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9
|
|
5359
|
+
${sourceLine}
|
|
5360
|
+
`);
|
|
5361
|
+
return false;
|
|
5362
|
+
}
|
|
5363
|
+
await fs16.writeFile(rcFile, `${sourceLine}
|
|
5364
|
+
`);
|
|
5094
5365
|
return false;
|
|
5095
5366
|
}
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5367
|
+
var PAI_DIR, CD_FILE, HELPER_FILE_SH, HELPER_FILE_PS1, BASH_HELPER, POWERSHELL_HELPER;
|
|
5368
|
+
var init_shell_cd = __esm({
|
|
5369
|
+
"src/utils/shell-cd.ts"() {
|
|
5370
|
+
"use strict";
|
|
5371
|
+
init_platform();
|
|
5372
|
+
PAI_DIR = join8(homedir2(), ".pai");
|
|
5373
|
+
CD_FILE = join8(PAI_DIR, ".cd-after");
|
|
5374
|
+
HELPER_FILE_SH = join8(PAI_DIR, "shell-helper.sh");
|
|
5375
|
+
HELPER_FILE_PS1 = join8(PAI_DIR, "shell-helper.ps1");
|
|
5376
|
+
BASH_HELPER = `# PAI shell helper \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9 \uC9C0\uC6D0
|
|
5377
|
+
pai() {
|
|
5378
|
+
local cd_target="$HOME/.pai/.cd-after"
|
|
5379
|
+
rm -f "$cd_target"
|
|
5380
|
+
PAI_CD_AFTER="$cd_target" command npx pai-zero "$@"
|
|
5381
|
+
local exit_code=$?
|
|
5382
|
+
if [ -f "$cd_target" ]; then
|
|
5383
|
+
local dir
|
|
5384
|
+
dir="$(cat "$cd_target")"
|
|
5385
|
+
rm -f "$cd_target"
|
|
5386
|
+
if [ -n "$dir" ] && [ -d "$dir" ]; then
|
|
5387
|
+
cd "$dir" || true
|
|
5388
|
+
echo " \u2192 cd $dir"
|
|
5389
|
+
fi
|
|
5390
|
+
fi
|
|
5391
|
+
return $exit_code
|
|
5392
|
+
}
|
|
5393
|
+
`;
|
|
5394
|
+
POWERSHELL_HELPER = `# PAI shell helper \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9 \uC9C0\uC6D0
|
|
5395
|
+
function pai {
|
|
5396
|
+
$cdTarget = Join-Path $env:USERPROFILE '.pai\\.cd-after'
|
|
5397
|
+
Remove-Item $cdTarget -ErrorAction SilentlyContinue
|
|
5398
|
+
$env:PAI_CD_AFTER = $cdTarget
|
|
5399
|
+
& npx pai-zero @args
|
|
5400
|
+
$exitCode = $LASTEXITCODE
|
|
5401
|
+
if (Test-Path $cdTarget) {
|
|
5402
|
+
$dir = Get-Content $cdTarget -Raw
|
|
5403
|
+
Remove-Item $cdTarget -ErrorAction SilentlyContinue
|
|
5404
|
+
if ($dir -and (Test-Path $dir)) {
|
|
5405
|
+
Set-Location $dir
|
|
5406
|
+
Write-Host " -> cd $dir"
|
|
5101
5407
|
}
|
|
5102
5408
|
}
|
|
5103
|
-
|
|
5104
|
-
const newLock = {
|
|
5105
|
-
pid: process.pid,
|
|
5106
|
-
stage,
|
|
5107
|
-
command,
|
|
5108
|
-
acquiredAt: now2.toISOString(),
|
|
5109
|
-
expiresAt: new Date(now2.getTime() + ttlMs).toISOString(),
|
|
5110
|
-
host: os3.hostname()
|
|
5111
|
-
};
|
|
5112
|
-
state.lock = newLock;
|
|
5113
|
-
await saveRobocoState(cwd, state);
|
|
5114
|
-
return state;
|
|
5409
|
+
return $exitCode
|
|
5115
5410
|
}
|
|
5116
|
-
|
|
5117
|
-
const state = await loadRobocoState(cwd);
|
|
5118
|
-
if (state.lock && state.lock.pid === process.pid) {
|
|
5119
|
-
state.lock = null;
|
|
5120
|
-
await saveRobocoState(cwd, state);
|
|
5411
|
+
`;
|
|
5121
5412
|
}
|
|
5413
|
+
});
|
|
5414
|
+
|
|
5415
|
+
// src/utils/claude-settings.ts
|
|
5416
|
+
var claude_settings_exports = {};
|
|
5417
|
+
__export(claude_settings_exports, {
|
|
5418
|
+
ClaudeSettingsError: () => ClaudeSettingsError,
|
|
5419
|
+
buildSkeleton: () => buildSkeleton,
|
|
5420
|
+
enableOmcPlugin: () => enableOmcPlugin,
|
|
5421
|
+
getClaudeSettingsPath: () => getClaudeSettingsPath,
|
|
5422
|
+
isAlreadyEnabled: () => isAlreadyEnabled,
|
|
5423
|
+
mergeOmcIntoSettings: () => mergeOmcIntoSettings
|
|
5424
|
+
});
|
|
5425
|
+
import os3 from "os";
|
|
5426
|
+
import path9 from "path";
|
|
5427
|
+
import fs17 from "fs-extra";
|
|
5428
|
+
function getClaudeSettingsPath(homeDir = os3.homedir()) {
|
|
5429
|
+
return path9.join(homeDir, ".claude", "settings.json");
|
|
5122
5430
|
}
|
|
5123
|
-
function
|
|
5124
|
-
const
|
|
5125
|
-
|
|
5126
|
-
return [
|
|
5127
|
-
"\uB2E4\uB978 PAI \uBA85\uB839\uC774 \uC9C4\uD589 \uC911\uC785\uB2C8\uB2E4",
|
|
5128
|
-
` stage: ${lock.stage}`,
|
|
5129
|
-
` command: ${lock.command}`,
|
|
5130
|
-
` \uC2DC\uC791: ${mins}\uBD84 \uC804 (PID ${lock.pid}${lock.host ? ", host " + lock.host : ""})`,
|
|
5131
|
-
" \uB300\uAE30\uD558\uC9C0 \uC54A\uACE0 \uC989\uC2DC \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. \uC644\uB8CC \uD6C4 \uC7AC\uC2DC\uB3C4\uD558\uC138\uC694."
|
|
5132
|
-
].join("\n");
|
|
5431
|
+
function parseJsonWithBom(raw) {
|
|
5432
|
+
const stripped = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
|
|
5433
|
+
return JSON.parse(stripped);
|
|
5133
5434
|
}
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
state.currentStage = stage;
|
|
5137
|
-
state.stageHistory.push({
|
|
5138
|
-
stage,
|
|
5139
|
-
status: "in_progress",
|
|
5140
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5141
|
-
by: command
|
|
5142
|
-
});
|
|
5143
|
-
await saveRobocoState(cwd, state);
|
|
5435
|
+
function timestampSuffix() {
|
|
5436
|
+
return sanitizeFilenameForWindows((/* @__PURE__ */ new Date()).toISOString());
|
|
5144
5437
|
}
|
|
5145
|
-
async function
|
|
5146
|
-
const
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5438
|
+
async function enableOmcPlugin(options = {}) {
|
|
5439
|
+
const marketplaceId = options.marketplaceId ?? DEFAULT_MARKETPLACE_ID;
|
|
5440
|
+
const marketplaceUrl = options.marketplaceUrl ?? DEFAULT_MARKETPLACE_URL;
|
|
5441
|
+
const pluginId = options.pluginId ?? DEFAULT_PLUGIN_ID;
|
|
5442
|
+
const wantBackup = options.backup ?? true;
|
|
5443
|
+
const settingsPath = getClaudeSettingsPath();
|
|
5444
|
+
await fs17.ensureDir(path9.dirname(settingsPath));
|
|
5445
|
+
if (!await fs17.pathExists(settingsPath)) {
|
|
5446
|
+
const skeleton = buildSkeleton(marketplaceId, marketplaceUrl, pluginId);
|
|
5447
|
+
await fs17.writeFile(settingsPath, JSON.stringify(skeleton, null, 2) + "\n", "utf8");
|
|
5448
|
+
return { action: "created", settingsPath };
|
|
5155
5449
|
}
|
|
5156
|
-
await
|
|
5157
|
-
|
|
5158
|
-
async function saveGateResult(cwd, key, result) {
|
|
5159
|
-
const state = await loadRobocoState(cwd);
|
|
5160
|
-
state.gates[key] = result;
|
|
5161
|
-
await saveRobocoState(cwd, state);
|
|
5162
|
-
}
|
|
5163
|
-
async function withRoboco(cwd, stage, command, options, fn) {
|
|
5164
|
-
await acquireLock(cwd, stage, command, options.ttlMs);
|
|
5450
|
+
const raw = await fs17.readFile(settingsPath, "utf8");
|
|
5451
|
+
let parsed;
|
|
5165
5452
|
try {
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
if (!result.passed && !options.skipGates) {
|
|
5170
|
-
await markStageEnd(cwd, stage, false, `\uAC8C\uC774\uD2B8 \uC2E4\uD328: ${result.name}`);
|
|
5171
|
-
throw new GateFailedError(result);
|
|
5172
|
-
}
|
|
5173
|
-
if (!result.passed && options.skipGates) {
|
|
5174
|
-
}
|
|
5453
|
+
const value = parseJsonWithBom(raw);
|
|
5454
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5455
|
+
throw new Error("settings.json is not a JSON object");
|
|
5175
5456
|
}
|
|
5176
|
-
|
|
5177
|
-
const value = await fn();
|
|
5178
|
-
await markStageEnd(cwd, stage, true);
|
|
5179
|
-
return value;
|
|
5457
|
+
parsed = value;
|
|
5180
5458
|
} catch (err) {
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
}
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
} finally {
|
|
5188
|
-
await releaseLock(cwd).catch(() => {
|
|
5189
|
-
});
|
|
5190
|
-
}
|
|
5191
|
-
}
|
|
5192
|
-
async function syncHandoff(cwd) {
|
|
5193
|
-
const handoffPath = path8.join(cwd, "handoff.md");
|
|
5194
|
-
if (!await fs16.pathExists(handoffPath)) return;
|
|
5195
|
-
const state = await loadRobocoState(cwd);
|
|
5196
|
-
const block = buildHandoffBlock(state);
|
|
5197
|
-
const content = await fs16.readFile(handoffPath, "utf8");
|
|
5198
|
-
const startIdx = content.indexOf(HANDOFF_START);
|
|
5199
|
-
const endIdx = content.indexOf(HANDOFF_END);
|
|
5200
|
-
let next;
|
|
5201
|
-
if (startIdx >= 0 && endIdx > startIdx) {
|
|
5202
|
-
next = content.slice(0, startIdx) + block + content.slice(endIdx + HANDOFF_END.length);
|
|
5203
|
-
} else {
|
|
5204
|
-
const sep = content.endsWith("\n") ? "\n" : "\n\n";
|
|
5205
|
-
next = content + sep + block + "\n";
|
|
5459
|
+
const backupPath2 = `${settingsPath}.backup-${timestampSuffix()}`;
|
|
5460
|
+
await fs17.copy(settingsPath, backupPath2);
|
|
5461
|
+
throw new ClaudeSettingsError(
|
|
5462
|
+
`settings.json \uD30C\uC2F1 \uC2E4\uD328: ${err.message}. \uBC31\uC5C5: ${backupPath2}`,
|
|
5463
|
+
backupPath2
|
|
5464
|
+
);
|
|
5206
5465
|
}
|
|
5207
|
-
|
|
5208
|
-
}
|
|
5209
|
-
function buildHandoffBlock(state) {
|
|
5210
|
-
const lines = [HANDOFF_START, "## \uD30C\uC774\uD504\uB77C\uC778 \uC9C4\uD589 \uC0C1\uD0DC (roboco \uC790\uB3D9 \uAD00\uB9AC)", ""];
|
|
5211
|
-
const done = state.stageHistory.filter((h) => h.status === "done");
|
|
5212
|
-
const inProgress = state.stageHistory.filter((h) => h.status === "in_progress");
|
|
5213
|
-
const failed = state.stageHistory.filter((h) => h.status === "failed");
|
|
5214
|
-
if (inProgress.length > 0) {
|
|
5215
|
-
lines.push("### \uC9C4\uD589 \uC911");
|
|
5216
|
-
for (const h of inProgress) {
|
|
5217
|
-
lines.push(`- [ ] ${h.stage} (${h.by}, \uC2DC\uC791 ${fmtDate(h.startedAt)})`);
|
|
5218
|
-
}
|
|
5219
|
-
lines.push("");
|
|
5466
|
+
if (isAlreadyEnabled(parsed, marketplaceId, pluginId)) {
|
|
5467
|
+
return { action: "already-enabled", settingsPath };
|
|
5220
5468
|
}
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
}
|
|
5226
|
-
lines.push("");
|
|
5469
|
+
let backupPath;
|
|
5470
|
+
if (wantBackup) {
|
|
5471
|
+
backupPath = `${settingsPath}.backup-${timestampSuffix()}`;
|
|
5472
|
+
await fs17.copy(settingsPath, backupPath);
|
|
5227
5473
|
}
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5474
|
+
const merged = mergeOmcIntoSettings(parsed, marketplaceId, marketplaceUrl, pluginId);
|
|
5475
|
+
await fs17.writeFile(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
5476
|
+
return { action: "added", settingsPath, backupPath };
|
|
5477
|
+
}
|
|
5478
|
+
function buildSkeleton(marketplaceId, marketplaceUrl, pluginId) {
|
|
5479
|
+
return {
|
|
5480
|
+
extraKnownMarketplaces: {
|
|
5481
|
+
[marketplaceId]: {
|
|
5482
|
+
source: { source: "git", url: marketplaceUrl }
|
|
5483
|
+
}
|
|
5484
|
+
},
|
|
5485
|
+
enabledPlugins: {
|
|
5486
|
+
[pluginId]: true
|
|
5232
5487
|
}
|
|
5233
|
-
|
|
5234
|
-
}
|
|
5235
|
-
lines.push(`\uD604\uC7AC \uB2E8\uACC4: **${state.currentStage}**`);
|
|
5236
|
-
lines.push(HANDOFF_END);
|
|
5237
|
-
return lines.join("\n");
|
|
5488
|
+
};
|
|
5238
5489
|
}
|
|
5239
|
-
function
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
}
|
|
5490
|
+
function isAlreadyEnabled(settings, marketplaceId, pluginId) {
|
|
5491
|
+
const markets = settings["extraKnownMarketplaces"];
|
|
5492
|
+
const enabled = settings["enabledPlugins"];
|
|
5493
|
+
const hasMarket = typeof markets === "object" && markets !== null && marketplaceId in markets;
|
|
5494
|
+
const hasPlugin = typeof enabled === "object" && enabled !== null && enabled[pluginId] === true;
|
|
5495
|
+
return hasMarket && hasPlugin;
|
|
5246
5496
|
}
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
"
|
|
5497
|
+
function mergeOmcIntoSettings(settings, marketplaceId, marketplaceUrl, pluginId) {
|
|
5498
|
+
const next = { ...settings };
|
|
5499
|
+
const existingMarkets = typeof next["extraKnownMarketplaces"] === "object" && next["extraKnownMarketplaces"] !== null ? { ...next["extraKnownMarketplaces"] } : {};
|
|
5500
|
+
existingMarkets[marketplaceId] = {
|
|
5501
|
+
source: { source: "git", url: marketplaceUrl }
|
|
5502
|
+
};
|
|
5503
|
+
next["extraKnownMarketplaces"] = existingMarkets;
|
|
5504
|
+
const existingPlugins = typeof next["enabledPlugins"] === "object" && next["enabledPlugins"] !== null ? { ...next["enabledPlugins"] } : {};
|
|
5505
|
+
existingPlugins[pluginId] = true;
|
|
5506
|
+
next["enabledPlugins"] = existingPlugins;
|
|
5507
|
+
return next;
|
|
5508
|
+
}
|
|
5509
|
+
var DEFAULT_MARKETPLACE_ID, DEFAULT_MARKETPLACE_URL, DEFAULT_PLUGIN_ID, ClaudeSettingsError;
|
|
5510
|
+
var init_claude_settings = __esm({
|
|
5511
|
+
"src/utils/claude-settings.ts"() {
|
|
5250
5512
|
"use strict";
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5513
|
+
init_platform();
|
|
5514
|
+
DEFAULT_MARKETPLACE_ID = "omc";
|
|
5515
|
+
DEFAULT_MARKETPLACE_URL = "https://github.com/SoInKyu/oh-my-claudecode.git";
|
|
5516
|
+
DEFAULT_PLUGIN_ID = "oh-my-claudecode@omc";
|
|
5517
|
+
ClaudeSettingsError = class extends Error {
|
|
5518
|
+
constructor(message, backupPath) {
|
|
5255
5519
|
super(message);
|
|
5256
|
-
this.
|
|
5257
|
-
this.name = "
|
|
5258
|
-
}
|
|
5259
|
-
holder;
|
|
5260
|
-
};
|
|
5261
|
-
GateFailedError = class extends Error {
|
|
5262
|
-
constructor(result) {
|
|
5263
|
-
super(`\uAC8C\uC774\uD2B8 \uC2E4\uD328 (${result.name}): ${result.violations.length}\uAC74 \uC704\uBC18`);
|
|
5264
|
-
this.result = result;
|
|
5265
|
-
this.name = "GateFailedError";
|
|
5520
|
+
this.backupPath = backupPath;
|
|
5521
|
+
this.name = "ClaudeSettingsError";
|
|
5266
5522
|
}
|
|
5267
|
-
|
|
5523
|
+
backupPath;
|
|
5268
5524
|
};
|
|
5269
|
-
HANDOFF_START = "<!-- roboco:start -->";
|
|
5270
|
-
HANDOFF_END = "<!-- roboco:end -->";
|
|
5271
5525
|
}
|
|
5272
5526
|
});
|
|
5273
5527
|
|
|
5274
|
-
// src/
|
|
5275
|
-
import
|
|
5276
|
-
import
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
}
|
|
5288
|
-
async function designEntryGate(cwd) {
|
|
5289
|
-
const violations = [];
|
|
5290
|
-
const claudeMd = path9.join(cwd, "CLAUDE.md");
|
|
5291
|
-
if (!await fs17.pathExists(claudeMd)) {
|
|
5292
|
-
violations.push({
|
|
5293
|
-
rule: "claude-md-exists",
|
|
5294
|
-
message: "CLAUDE.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 `pai init`\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
|
|
5295
|
-
location: "CLAUDE.md"
|
|
5296
|
-
});
|
|
5297
|
-
}
|
|
5298
|
-
const configJson = path9.join(cwd, ".pai", "config.json");
|
|
5299
|
-
if (!await fs17.pathExists(configJson)) {
|
|
5300
|
-
violations.push({
|
|
5301
|
-
rule: "pai-config-exists",
|
|
5302
|
-
message: "PAI \uC124\uC815\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. `pai init`\uC744 \uC2E4\uD589\uD558\uC138\uC694.",
|
|
5303
|
-
location: ".pai/config.json"
|
|
5304
|
-
});
|
|
5305
|
-
}
|
|
5306
|
-
return makeResult("design.entry", violations);
|
|
5307
|
-
}
|
|
5308
|
-
async function executionEntryGate(cwd) {
|
|
5309
|
-
const violations = [];
|
|
5310
|
-
const openspec = path9.join(cwd, "docs", "openspec.md");
|
|
5311
|
-
const hasOpenspec = await fs17.pathExists(openspec);
|
|
5312
|
-
if (!hasOpenspec) {
|
|
5313
|
-
violations.push({
|
|
5314
|
-
rule: "openspec-exists",
|
|
5315
|
-
message: "docs/openspec.md\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
5316
|
-
location: "docs/openspec.md"
|
|
5317
|
-
});
|
|
5318
|
-
} else {
|
|
5319
|
-
const content = await fs17.readFile(openspec, "utf8");
|
|
5320
|
-
const endpointCount = countEndpoints(content);
|
|
5321
|
-
if (endpointCount < 1) {
|
|
5322
|
-
violations.push({
|
|
5323
|
-
rule: "openspec-endpoints-min",
|
|
5324
|
-
message: `openspec.md\uC5D0 \uC815\uC758\uB41C API \uC5D4\uB4DC\uD3EC\uC778\uD2B8\uAC00 0\uAC1C\uC785\uB2C8\uB2E4. \uCD5C\uC18C 1\uAC1C \uD544\uC694 (\uD604\uC7AC: 0). /pai design \uC2E4\uD589 \uAD8C\uC7A5.`,
|
|
5325
|
-
location: "docs/openspec.md \xA7 5. API \uC5D4\uB4DC\uD3EC\uC778\uD2B8"
|
|
5326
|
-
});
|
|
5327
|
-
}
|
|
5328
|
-
}
|
|
5329
|
-
const omc = path9.join(cwd, ".pai", "omc.md");
|
|
5330
|
-
if (await fs17.pathExists(omc)) {
|
|
5331
|
-
const content = await fs17.readFile(omc, "utf8");
|
|
5332
|
-
const domainCount = countDomainObjects(content);
|
|
5333
|
-
if (domainCount < 1) {
|
|
5334
|
-
violations.push({
|
|
5335
|
-
rule: "omc-domain-min",
|
|
5336
|
-
message: "omc.md\uC5D0 \uB3C4\uBA54\uC778 \uAC1D\uCCB4\uAC00 0\uAC1C\uC785\uB2C8\uB2E4. \uCD5C\uC18C 1\uAC1C \uD544\uC694.",
|
|
5337
|
-
location: ".pai/omc.md \xA7 \uB3C4\uBA54\uC778 \uAC1D\uCCB4"
|
|
5338
|
-
});
|
|
5528
|
+
// src/stages/evaluation/cache.ts
|
|
5529
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
5530
|
+
import { join as join9 } from "path";
|
|
5531
|
+
import { createHash } from "crypto";
|
|
5532
|
+
function computeRepoHash(repoPath) {
|
|
5533
|
+
const hash = createHash("sha256");
|
|
5534
|
+
for (const file of FILES_TO_HASH) {
|
|
5535
|
+
const fullPath = join9(repoPath, file);
|
|
5536
|
+
try {
|
|
5537
|
+
const content = readFileSync(fullPath);
|
|
5538
|
+
hash.update(`${file}:${content.length}`);
|
|
5539
|
+
} catch {
|
|
5540
|
+
hash.update(`${file}:missing`);
|
|
5339
5541
|
}
|
|
5340
5542
|
}
|
|
5341
|
-
return
|
|
5543
|
+
return hash.digest("hex").slice(0, 16);
|
|
5342
5544
|
}
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
const srcDir = path9.join(cwd, "src");
|
|
5346
|
-
if (!await fs17.pathExists(srcDir)) {
|
|
5347
|
-
violations.push({
|
|
5348
|
-
rule: "src-dir-exists",
|
|
5349
|
-
message: "src/ \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.",
|
|
5350
|
-
location: "src/"
|
|
5351
|
-
});
|
|
5352
|
-
} else {
|
|
5353
|
-
const codeFileCount = await countCodeFiles(srcDir);
|
|
5354
|
-
if (codeFileCount === 0) {
|
|
5355
|
-
violations.push({
|
|
5356
|
-
rule: "code-files-min",
|
|
5357
|
-
message: "src/ \uB0B4\uBD80\uC5D0 \uCF54\uB4DC \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. \uCD5C\uC18C 1\uAC1C \uD544\uC694.",
|
|
5358
|
-
location: "src/"
|
|
5359
|
-
});
|
|
5360
|
-
}
|
|
5361
|
-
}
|
|
5362
|
-
return makeResult("validation.entry", violations);
|
|
5545
|
+
function getCachePath(repoPath) {
|
|
5546
|
+
return join9(repoPath, CACHE_DIR, CACHE_FILE);
|
|
5363
5547
|
}
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
rule: "validation-executed",
|
|
5371
|
-
message: "validation \uB2E8\uACC4\uAC00 \uD55C \uBC88\uB3C4 \uC2E4\uD589\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 `pai test`\uB97C \uC2E4\uD589\uD558\uC138\uC694."
|
|
5372
|
-
});
|
|
5373
|
-
} else if (recentValidation.status !== "done") {
|
|
5374
|
-
violations.push({
|
|
5375
|
-
rule: "validation-passed",
|
|
5376
|
-
message: `\uCD5C\uADFC validation\uC774 ${recentValidation.status} \uC0C1\uD0DC\uC785\uB2C8\uB2E4. \uD14C\uC2A4\uD2B8\uB97C \uC131\uACF5\uC2DC\uD0A8 \uB4A4 \uC2DC\uB3C4\uD558\uC138\uC694.`
|
|
5377
|
-
});
|
|
5378
|
-
} else {
|
|
5379
|
-
const endedAt = recentValidation.endedAt ?? recentValidation.startedAt;
|
|
5380
|
-
const age = Date.now() - new Date(endedAt).getTime();
|
|
5381
|
-
if (age > EVAL_GATE_WINDOW_MS) {
|
|
5382
|
-
const mins = Math.round(age / 6e4);
|
|
5383
|
-
violations.push({
|
|
5384
|
-
rule: "validation-recent",
|
|
5385
|
-
message: `\uCD5C\uADFC validation \uD1B5\uACFC\uAC00 ${mins}\uBD84 \uC804\uC785\uB2C8\uB2E4 (1\uC2DC\uAC04 \uC774\uB0B4\uC5EC\uC57C \uD568). --force\uB85C \uC6B0\uD68C \uAC00\uB2A5.`
|
|
5386
|
-
});
|
|
5387
|
-
}
|
|
5548
|
+
function loadCache(repoPath) {
|
|
5549
|
+
try {
|
|
5550
|
+
const data = readFileSync(getCachePath(repoPath), "utf-8");
|
|
5551
|
+
return JSON.parse(data);
|
|
5552
|
+
} catch {
|
|
5553
|
+
return { version: 1, entries: {} };
|
|
5388
5554
|
}
|
|
5389
|
-
return makeResult("evaluation.entry", violations);
|
|
5390
5555
|
}
|
|
5391
|
-
function
|
|
5392
|
-
const
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
for (const rawLine of lines) {
|
|
5396
|
-
const line = rawLine.trim();
|
|
5397
|
-
if (/^##\s*5\.|^##\s*API|API 엔드포인트/i.test(line)) {
|
|
5398
|
-
inTable = true;
|
|
5399
|
-
continue;
|
|
5400
|
-
}
|
|
5401
|
-
if (!inTable) continue;
|
|
5402
|
-
if (/^##\s/.test(line) && !/5\.|API/i.test(line)) {
|
|
5403
|
-
inTable = false;
|
|
5404
|
-
continue;
|
|
5405
|
-
}
|
|
5406
|
-
if (/^\|\s*Method/i.test(line)) continue;
|
|
5407
|
-
if (/^\|\s*[-:]+\s*\|/.test(line)) continue;
|
|
5408
|
-
const m = line.match(/^\|\s*(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\|\s*(\/\S+)\s*\|/i);
|
|
5409
|
-
if (m) count++;
|
|
5556
|
+
function saveCache(repoPath, store) {
|
|
5557
|
+
const cacheDir = join9(repoPath, CACHE_DIR, "cache");
|
|
5558
|
+
if (!existsSync(cacheDir)) {
|
|
5559
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
5410
5560
|
}
|
|
5411
|
-
|
|
5561
|
+
writeFileSync(getCachePath(repoPath), JSON.stringify(store, null, 2));
|
|
5412
5562
|
}
|
|
5413
|
-
function
|
|
5414
|
-
const
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
inDomainSection = true;
|
|
5421
|
-
continue;
|
|
5422
|
-
}
|
|
5423
|
-
if (!inDomainSection) continue;
|
|
5424
|
-
if (/^##\s/.test(line) && !/도메인|Domain/i.test(line)) {
|
|
5425
|
-
inDomainSection = false;
|
|
5426
|
-
continue;
|
|
5427
|
-
}
|
|
5428
|
-
if (/^###\s+\w/.test(line)) count++;
|
|
5429
|
-
}
|
|
5430
|
-
return count;
|
|
5563
|
+
function getCachedResult(repoPath) {
|
|
5564
|
+
const store = loadCache(repoPath);
|
|
5565
|
+
const repoHash = computeRepoHash(repoPath);
|
|
5566
|
+
const entry = store.entries[repoHash];
|
|
5567
|
+
if (!entry) return null;
|
|
5568
|
+
if (Date.now() - entry.timestamp > CACHE_TTL_MS) return null;
|
|
5569
|
+
return entry.llmOutput;
|
|
5431
5570
|
}
|
|
5432
|
-
|
|
5433
|
-
const
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
} catch {
|
|
5440
|
-
return;
|
|
5441
|
-
}
|
|
5442
|
-
for (const name of entries) {
|
|
5443
|
-
if (name.startsWith(".") || name === "node_modules") continue;
|
|
5444
|
-
const full = path9.join(dir, name);
|
|
5445
|
-
const stat = await fs17.stat(full).catch(() => null);
|
|
5446
|
-
if (!stat) continue;
|
|
5447
|
-
if (stat.isDirectory()) {
|
|
5448
|
-
await walk(full);
|
|
5449
|
-
} else if (exts.has(path9.extname(name))) {
|
|
5450
|
-
count++;
|
|
5451
|
-
}
|
|
5571
|
+
function setCachedResult(repoPath, llmOutput) {
|
|
5572
|
+
const store = loadCache(repoPath);
|
|
5573
|
+
const repoHash = computeRepoHash(repoPath);
|
|
5574
|
+
const now2 = Date.now();
|
|
5575
|
+
for (const [key, entry] of Object.entries(store.entries)) {
|
|
5576
|
+
if (now2 - entry.timestamp > CACHE_TTL_MS) {
|
|
5577
|
+
delete store.entries[key];
|
|
5452
5578
|
}
|
|
5453
5579
|
}
|
|
5454
|
-
|
|
5455
|
-
|
|
5580
|
+
store.entries[repoHash] = { repoPath, repoHash, timestamp: now2, llmOutput };
|
|
5581
|
+
saveCache(repoPath, store);
|
|
5456
5582
|
}
|
|
5457
|
-
var
|
|
5458
|
-
var
|
|
5459
|
-
"src/
|
|
5583
|
+
var CACHE_DIR, CACHE_FILE, CACHE_TTL_MS, FILES_TO_HASH;
|
|
5584
|
+
var init_cache = __esm({
|
|
5585
|
+
"src/stages/evaluation/cache.ts"() {
|
|
5460
5586
|
"use strict";
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5587
|
+
CACHE_DIR = ".pai";
|
|
5588
|
+
CACHE_FILE = "cache/evaluation.json";
|
|
5589
|
+
CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
5590
|
+
FILES_TO_HASH = [
|
|
5591
|
+
"package.json",
|
|
5592
|
+
"pyproject.toml",
|
|
5593
|
+
"go.mod",
|
|
5594
|
+
"Cargo.toml",
|
|
5595
|
+
"tsconfig.json",
|
|
5596
|
+
"jest.config.ts",
|
|
5597
|
+
"vitest.config.ts",
|
|
5598
|
+
".github/workflows",
|
|
5599
|
+
".gitlab-ci.yml",
|
|
5600
|
+
".husky",
|
|
5601
|
+
".lintstagedrc",
|
|
5602
|
+
"CLAUDE.md",
|
|
5603
|
+
"AGENTS.md",
|
|
5604
|
+
".cursorrules",
|
|
5605
|
+
"README.md",
|
|
5606
|
+
"CONTRIBUTING.md"
|
|
5607
|
+
];
|
|
5471
5608
|
}
|
|
5472
5609
|
});
|
|
5473
5610
|
|