pai-zero 0.13.0 → 0.13.2
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 +2050 -1869
- package/dist/bin/pai.js.map +1 -1
- package/dist/cli/index.js +2050 -1869
- 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,2070 +3605,2200 @@ 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
|
-
await walk(srcPath, destDir);
|
|
3763
|
-
return result;
|
|
3764
3748
|
}
|
|
3765
|
-
|
|
3766
|
-
"
|
|
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"() {
|
|
3767
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 -->";
|
|
3768
3828
|
}
|
|
3769
3829
|
});
|
|
3770
3830
|
|
|
3771
|
-
// src/
|
|
3772
|
-
var
|
|
3773
|
-
__export(
|
|
3774
|
-
|
|
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
|
|
3775
3843
|
});
|
|
3776
3844
|
import path6 from "path";
|
|
3777
|
-
import
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
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
|
+
});
|
|
3791
3866
|
}
|
|
3792
|
-
|
|
3793
|
-
if (
|
|
3794
|
-
|
|
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
|
+
});
|
|
3795
3887
|
} else {
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
error(`\uC54C \uC218 \uC5C6\uB294 \uB808\uC2DC\uD53C: ${recipeKey}`);
|
|
3805
|
-
hint(`\uC0AC\uC6A9 \uAC00\uB2A5: ${listRecipeKeys().join(", ")}`);
|
|
3806
|
-
process.exitCode = 1;
|
|
3807
|
-
return;
|
|
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
|
+
});
|
|
3808
3896
|
}
|
|
3809
|
-
keys = [recipeKey.toLowerCase()];
|
|
3810
3897
|
}
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
const
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
repo: recipe.source.repo,
|
|
3821
|
-
ref: recipe.source.ref,
|
|
3822
|
-
srcPath: recipe.source.path,
|
|
3823
|
-
destDir: targetDir,
|
|
3824
|
-
overwrite: options.overwrite ?? false
|
|
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"
|
|
3825
3907
|
});
|
|
3826
|
-
});
|
|
3827
|
-
if (result.errors.length > 0) {
|
|
3828
|
-
error(`\uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${result.errors[0].error}`);
|
|
3829
|
-
for (const e of result.errors.slice(1)) {
|
|
3830
|
-
console.log(chalk4.gray(` ${e.path}: ${e.error}`));
|
|
3831
|
-
}
|
|
3832
|
-
continue;
|
|
3833
|
-
}
|
|
3834
|
-
if (result.written.length > 0) {
|
|
3835
|
-
success(`${result.written.length}\uAC1C \uD30C\uC77C \uC800\uC7A5`);
|
|
3836
|
-
for (const f of result.written) {
|
|
3837
|
-
console.log(chalk4.gray(` ${path6.relative(cwd, f)}`));
|
|
3838
|
-
}
|
|
3839
|
-
}
|
|
3840
|
-
if (result.skipped.length > 0) {
|
|
3841
|
-
info(`${result.skipped.length}\uAC1C \uD30C\uC77C \uAC74\uB108\uB700 (\uC774\uBBF8 \uC874\uC7AC \u2014 \uB36E\uC5B4\uC4F0\uAE30: --overwrite)`);
|
|
3842
3908
|
}
|
|
3843
|
-
await appendEnvKeys(cwd, recipe);
|
|
3844
|
-
installed.push(key);
|
|
3845
3909
|
}
|
|
3846
|
-
|
|
3847
|
-
|
|
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
|
+
});
|
|
3848
3917
|
}
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
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
|
+
});
|
|
3861
3937
|
}
|
|
3862
3938
|
}
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
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
|
+
});
|
|
3868
3946
|
}
|
|
3869
|
-
|
|
3947
|
+
return makeResult("validation.entry", violations);
|
|
3870
3948
|
}
|
|
3871
|
-
async function
|
|
3872
|
-
|
|
3873
|
-
const
|
|
3874
|
-
|
|
3875
|
-
if (
|
|
3876
|
-
|
|
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
|
+
}
|
|
3877
3973
|
}
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
if (
|
|
3887
|
-
|
|
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++;
|
|
3888
3995
|
}
|
|
3889
|
-
|
|
3890
|
-
await fs12.appendFile(envPath, lines.join("\n") + "\n");
|
|
3996
|
+
return count;
|
|
3891
3997
|
}
|
|
3892
|
-
|
|
3893
|
-
const
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
const
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
"## \uD2B8\uB9AC\uAC70",
|
|
3911
|
-
"",
|
|
3912
|
-
"- " + triggers,
|
|
3913
|
-
"",
|
|
3914
|
-
"## \uC124\uCE58\uB41C \uB808\uC2DC\uD53C",
|
|
3915
|
-
"",
|
|
3916
|
-
...recipes.map((r) => [
|
|
3917
|
-
`### ${r.label}`,
|
|
3918
|
-
"",
|
|
3919
|
-
`- \uACBD\uB85C: \`${r.target}/\``,
|
|
3920
|
-
`- \uC8FC\uC694 \uBB38\uC11C: \`${r.target}/guideline.md\``,
|
|
3921
|
-
`- \uD658\uACBD\uBCC0\uC218: ${r.envKeys.map((e) => "`" + e.key + "`").join(", ")}`,
|
|
3922
|
-
""
|
|
3923
|
-
].join("\n")),
|
|
3924
|
-
"",
|
|
3925
|
-
"## \uC791\uC5C5 \uC21C\uC11C",
|
|
3926
|
-
"",
|
|
3927
|
-
"1. \uC0AC\uC6A9\uC790\uC758 \uC694\uCCAD\uC774 \uC704 \uD2B8\uB9AC\uAC70 \uC911 \uD558\uB098\uC5D0 \uD574\uB2F9\uD558\uB294\uC9C0 \uD310\uB2E8",
|
|
3928
|
-
"2. \uD574\uB2F9 \uB808\uC2DC\uD53C\uC758 `guideline.md`\uB97C Read \uB3C4\uAD6C\uB85C \uC77D\uAE30",
|
|
3929
|
-
"3. \uC0D8\uD50C \uCF54\uB4DC(`*.html`, `*.ts` \uB4F1)\uAC00 \uC788\uC73C\uBA74 \uD568\uAED8 \uD655\uC778",
|
|
3930
|
-
"4. `.env.local`\uC5D0 \uAD00\uB828 \uD658\uACBD\uBCC0\uC218\uAC00 \uCC44\uC6CC\uC838 \uC788\uB294\uC9C0 \uD655\uC778",
|
|
3931
|
-
"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
|
-
""
|
|
3933
|
-
].join("\n");
|
|
3934
|
-
await fs12.writeFile(skillPath, body);
|
|
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;
|
|
3935
4016
|
}
|
|
3936
|
-
async function
|
|
3937
|
-
const
|
|
3938
|
-
|
|
3939
|
-
const
|
|
3940
|
-
const
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
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
|
+
}
|
|
3951
4077
|
}
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
const
|
|
3956
|
-
let
|
|
3957
|
-
|
|
3958
|
-
|
|
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++;
|
|
3959
4096
|
}
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
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
|
+
}
|
|
3970
4120
|
}
|
|
3971
|
-
await
|
|
3972
|
-
|
|
4121
|
+
await walk(srcDir);
|
|
4122
|
+
return count;
|
|
3973
4123
|
}
|
|
3974
|
-
var
|
|
3975
|
-
|
|
4124
|
+
var EVAL_GATE_WINDOW_MS, STAGE_GATES;
|
|
4125
|
+
var init_gates = __esm({
|
|
4126
|
+
"src/core/gates.ts"() {
|
|
3976
4127
|
"use strict";
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
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
|
+
};
|
|
3982
4139
|
}
|
|
3983
4140
|
});
|
|
3984
4141
|
|
|
3985
|
-
// src/stages/environment/
|
|
3986
|
-
var
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
"use strict";
|
|
3990
|
-
init_analyzer();
|
|
3991
|
-
init_interviewer();
|
|
3992
|
-
init_generator();
|
|
3993
|
-
init_installer();
|
|
3994
|
-
init_registry();
|
|
3995
|
-
init_claude_commands();
|
|
3996
|
-
init_detector();
|
|
3997
|
-
init_config();
|
|
3998
|
-
init_progress();
|
|
3999
|
-
init_ui();
|
|
4000
|
-
init_analyzer();
|
|
4001
|
-
init_interviewer();
|
|
4002
|
-
init_doctor();
|
|
4003
|
-
environmentStage = {
|
|
4004
|
-
name: "environment",
|
|
4005
|
-
canSkip(input) {
|
|
4006
|
-
return input.config.plugins.length > 0;
|
|
4007
|
-
},
|
|
4008
|
-
async run(input) {
|
|
4009
|
-
const start = Date.now();
|
|
4010
|
-
const artifacts = [];
|
|
4011
|
-
const errors = [];
|
|
4012
|
-
try {
|
|
4013
|
-
console.log("");
|
|
4014
|
-
const analysis = await withSpinner("\uD504\uB85C\uC81D\uD2B8 \uBD84\uC11D \uC911...", async () => {
|
|
4015
|
-
const result = await analyzeProject(input.cwd);
|
|
4016
|
-
await sleep(500);
|
|
4017
|
-
return result;
|
|
4018
|
-
});
|
|
4019
|
-
console.log("");
|
|
4020
|
-
if (analysis.stack.languages.length > 0) {
|
|
4021
|
-
success(`\uC2A4\uD0DD: ${analysis.stack.languages.join(", ")}`);
|
|
4022
|
-
}
|
|
4023
|
-
if (analysis.stack.frameworks.length > 0) {
|
|
4024
|
-
success(`\uD504\uB808\uC784\uC6CC\uD06C: ${analysis.stack.frameworks.join(", ")}`);
|
|
4025
|
-
}
|
|
4026
|
-
success(`Git: ${analysis.git.isGitRepo ? `${analysis.git.repoName ?? "local"}` : "\uBBF8\uCD08\uAE30\uD654"}`);
|
|
4027
|
-
const interview = await runInterview(analysis, input.cwd, input.config.projectName);
|
|
4028
|
-
console.log("");
|
|
4029
|
-
const configFiles = [
|
|
4030
|
-
"CLAUDE.md",
|
|
4031
|
-
".claude/settings.json",
|
|
4032
|
-
"docs/vibe-coding/01-intent.md",
|
|
4033
|
-
"docs/vibe-coding/02-requirements.md",
|
|
4034
|
-
"docs/vibe-coding/03-research.md",
|
|
4035
|
-
"docs/vibe-coding/04-plan.md",
|
|
4036
|
-
"docs/vibe-coding/05-implement.md"
|
|
4037
|
-
];
|
|
4038
|
-
const generated = await withFileSpinner("\uC124\uC815 \uD30C\uC77C \uC0DD\uC131 \uC911...", configFiles, async () => {
|
|
4039
|
-
const files = await generateFiles(input.cwd, analysis, interview);
|
|
4040
|
-
await sleep(400);
|
|
4041
|
-
return files;
|
|
4042
|
-
});
|
|
4043
|
-
artifacts.push(...generated);
|
|
4044
|
-
const basePlugins = ["github", "openspec", "roboco"];
|
|
4045
|
-
const pluginKeys = [.../* @__PURE__ */ new Set([...basePlugins, ...interview.extraTools])];
|
|
4046
|
-
const provCtx = {
|
|
4047
|
-
cwd: input.cwd,
|
|
4048
|
-
projectName: interview.projectName,
|
|
4049
|
-
mode: interview.mode,
|
|
4050
|
-
envEntries: {
|
|
4051
|
-
PAI_PROJECT_NAME: interview.projectName,
|
|
4052
|
-
PAI_MODE: interview.mode
|
|
4053
|
-
},
|
|
4054
|
-
analysis,
|
|
4055
|
-
mcp: interview.mcp
|
|
4056
|
-
};
|
|
4057
|
-
if (interview.authMethods.includes("custom")) {
|
|
4058
|
-
provCtx.envEntries["OAUTH_CLIENT_ID"] = interview.customAuth?.clientId || "YOUR_CLIENT_ID_HERE";
|
|
4059
|
-
provCtx.envEntries["OAUTH_CLIENT_SECRET"] = interview.customAuth?.clientSecret || "YOUR_CLIENT_SECRET_HERE";
|
|
4060
|
-
provCtx.envEntries["OAUTH_REDIRECT_URI"] = "http://localhost:3000/auth/callback";
|
|
4061
|
-
}
|
|
4062
|
-
const pluginFileNames = [
|
|
4063
|
-
".gitignore",
|
|
4064
|
-
"src/",
|
|
4065
|
-
"docs/",
|
|
4066
|
-
"tests/",
|
|
4067
|
-
"public/",
|
|
4068
|
-
"docs/openspec.md",
|
|
4069
|
-
".pai/roboco.json",
|
|
4070
|
-
...interview.extraTools.includes("omc") ? [".pai/omc.md", ".omc/"] : [],
|
|
4071
|
-
...interview.extraTools.includes("vercel") ? ["vercel.json"] : [],
|
|
4072
|
-
...interview.extraTools.includes("supabase") ? ["supabase/config.toml"] : [],
|
|
4073
|
-
...interview.extraTools.includes("gstack") ? [".pai/gstack.json"] : [],
|
|
4074
|
-
...interview.extraTools.includes("harness") ? [".pai/harness.json"] : [],
|
|
4075
|
-
...interview.extraTools.includes("mcp") ? ["mcp-server/", ".mcp.json"] : [],
|
|
4076
|
-
".env.local"
|
|
4077
|
-
];
|
|
4078
|
-
console.log("");
|
|
4079
|
-
await withFileSpinner("\uD504\uB85C\uC81D\uD2B8 \uAD6C\uC870 \uC124\uCE58 \uC911...", pluginFileNames, async () => {
|
|
4080
|
-
await runProvisioners(pluginKeys, provCtx);
|
|
4081
|
-
await sleep(500);
|
|
4082
|
-
});
|
|
4083
|
-
const cmdFiles = [
|
|
4084
|
-
"SKILL.md",
|
|
4085
|
-
"info.md",
|
|
4086
|
-
"init.md",
|
|
4087
|
-
"status.md",
|
|
4088
|
-
"doctor.md",
|
|
4089
|
-
"design.md",
|
|
4090
|
-
"validate.md",
|
|
4091
|
-
"evaluate.md",
|
|
4092
|
-
"install.md"
|
|
4093
|
-
];
|
|
4094
|
-
console.log("");
|
|
4095
|
-
await withFileSpinner("\uC2AC\uB798\uC2DC \uCEE4\uB9E8\uB4DC \uC124\uCE58 \uC911...", cmdFiles, async () => {
|
|
4096
|
-
await provisionClaudeCommands(input.cwd);
|
|
4097
|
-
await sleep(400);
|
|
4098
|
-
});
|
|
4099
|
-
console.log("");
|
|
4100
|
-
const installResults = await installTools(interview.tools, input.cwd);
|
|
4101
|
-
printInstallReport(installResults, interview.tools);
|
|
4102
|
-
if (interview.recipes && interview.recipes.length > 0) {
|
|
4103
|
-
console.log("");
|
|
4104
|
-
const { fetchCommand: fetchCommand2 } = await Promise.resolve().then(() => (init_fetch_cmd(), fetch_cmd_exports));
|
|
4105
|
-
for (const recipeKey of interview.recipes) {
|
|
4106
|
-
try {
|
|
4107
|
-
await fetchCommand2(input.cwd, recipeKey, {});
|
|
4108
|
-
} catch (err) {
|
|
4109
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4110
|
-
warn(`\uB808\uC2DC\uD53C '${recipeKey}' \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328 \u2014 ${msg}`);
|
|
4111
|
-
hint(`\uB098\uC911\uC5D0 \uC218\uB3D9: pai fetch ${recipeKey}`);
|
|
4112
|
-
}
|
|
4113
|
-
}
|
|
4114
|
-
}
|
|
4115
|
-
console.log("");
|
|
4116
|
-
await withSpinner("\uC124\uC815 \uC800\uC7A5 \uC911...", async () => {
|
|
4117
|
-
const config = createDefaultConfig(interview.projectName, interview.mode);
|
|
4118
|
-
config.plugins = pluginKeys;
|
|
4119
|
-
config.edges = deriveEdges(pluginKeys);
|
|
4120
|
-
if (interview.mcp) config.mcp = interview.mcp;
|
|
4121
|
-
await saveConfig(input.cwd, config);
|
|
4122
|
-
await sleep(300);
|
|
4123
|
-
});
|
|
4124
|
-
return {
|
|
4125
|
-
stage: "environment",
|
|
4126
|
-
status: "success",
|
|
4127
|
-
data: { analysis, interview, installResults },
|
|
4128
|
-
artifacts,
|
|
4129
|
-
duration: Date.now() - start,
|
|
4130
|
-
errors
|
|
4131
|
-
};
|
|
4132
|
-
} catch (err) {
|
|
4133
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4134
|
-
errors.push({ code: "ENV_SETUP_FAILED", message: msg, recoverable: true });
|
|
4135
|
-
return {
|
|
4136
|
-
stage: "environment",
|
|
4137
|
-
status: "failed",
|
|
4138
|
-
data: {},
|
|
4139
|
-
artifacts,
|
|
4140
|
-
duration: Date.now() - start,
|
|
4141
|
-
errors
|
|
4142
|
-
};
|
|
4143
|
-
}
|
|
4144
|
-
}
|
|
4145
|
-
};
|
|
4146
|
-
}
|
|
4147
|
-
});
|
|
4148
|
-
|
|
4149
|
-
// src/stages/evaluation/prompts/analyze.ts
|
|
4150
|
-
var analyze_exports = {};
|
|
4151
|
-
__export(analyze_exports, {
|
|
4152
|
-
buildAnalysisPrompt: () => buildAnalysisPrompt
|
|
4142
|
+
// src/stages/environment/doctor.ts
|
|
4143
|
+
var doctor_exports = {};
|
|
4144
|
+
__export(doctor_exports, {
|
|
4145
|
+
runDoctor: () => runDoctor
|
|
4153
4146
|
});
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
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
|
+
}
|
|
4188
4219
|
}
|
|
4189
|
-
|
|
4190
|
-
"
|
|
4220
|
+
}
|
|
4221
|
+
console.log("");
|
|
4222
|
+
info(`${passed}/${checks.length} \uD56D\uBAA9 \uD1B5\uACFC`);
|
|
4223
|
+
if (passed < checks.length) {
|
|
4224
|
+
process.exitCode = 1;
|
|
4225
|
+
}
|
|
4191
4226
|
}
|
|
4192
|
-
|
|
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
|
+
}
|
|
4193
4235
|
}
|
|
4194
|
-
var
|
|
4195
|
-
"src/stages/
|
|
4236
|
+
var init_doctor = __esm({
|
|
4237
|
+
"src/stages/environment/doctor.ts"() {
|
|
4196
4238
|
"use strict";
|
|
4239
|
+
init_ui();
|
|
4197
4240
|
}
|
|
4198
4241
|
});
|
|
4199
4242
|
|
|
4200
|
-
// src/
|
|
4201
|
-
|
|
4202
|
-
__export(analyzer_exports2, {
|
|
4203
|
-
analyzeRepository: () => analyzeRepository
|
|
4204
|
-
});
|
|
4205
|
-
import { join as join7 } from "path";
|
|
4243
|
+
// src/utils/github-fetch.ts
|
|
4244
|
+
import path7 from "path";
|
|
4206
4245
|
import fs13 from "fs-extra";
|
|
4207
|
-
async function
|
|
4246
|
+
async function httpGet(url, timeoutMs, accept) {
|
|
4247
|
+
const controller = new AbortController();
|
|
4248
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4208
4249
|
try {
|
|
4209
|
-
return await
|
|
4210
|
-
|
|
4211
|
-
|
|
4250
|
+
return await fetch(url, {
|
|
4251
|
+
signal: controller.signal,
|
|
4252
|
+
headers: { "Accept": accept, "User-Agent": "pai-zero" }
|
|
4253
|
+
});
|
|
4254
|
+
} finally {
|
|
4255
|
+
clearTimeout(timer);
|
|
4212
4256
|
}
|
|
4213
4257
|
}
|
|
4214
|
-
async function
|
|
4215
|
-
const
|
|
4216
|
-
const
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
for await (const message of query({
|
|
4220
|
-
prompt,
|
|
4221
|
-
options: {
|
|
4222
|
-
maxTurns: 200,
|
|
4223
|
-
systemPrompt: "You are a vibe coding readiness analyzer. Analyze the repository and output JSON.",
|
|
4224
|
-
allowedTools: ["Read", "Glob", "Grep"],
|
|
4225
|
-
permissionMode: "plan"
|
|
4226
|
-
}
|
|
4227
|
-
})) {
|
|
4228
|
-
if ("result" in message) {
|
|
4229
|
-
resultText = message.result;
|
|
4230
|
-
}
|
|
4231
|
-
}
|
|
4232
|
-
return parseJsonFromText(resultText);
|
|
4233
|
-
}
|
|
4234
|
-
function parseJsonFromText(text) {
|
|
4235
|
-
const jsonMatch = text.match(/```json\s*([\s\S]*?)```/);
|
|
4236
|
-
if (jsonMatch?.[1]) {
|
|
4237
|
-
return JSON.parse(jsonMatch[1]);
|
|
4238
|
-
}
|
|
4239
|
-
return JSON.parse(text);
|
|
4240
|
-
}
|
|
4241
|
-
async function staticAnalysis(repoPath) {
|
|
4242
|
-
const categories = [];
|
|
4243
|
-
categories.push(await checkTestCoverage(repoPath));
|
|
4244
|
-
categories.push(await checkCiCd(repoPath));
|
|
4245
|
-
categories.push(await checkHooks(repoPath));
|
|
4246
|
-
categories.push(await checkRepoStructure(repoPath));
|
|
4247
|
-
categories.push(await checkDocumentation(repoPath));
|
|
4248
|
-
categories.push(await checkHarnessEngineering(repoPath));
|
|
4249
|
-
return {
|
|
4250
|
-
categories,
|
|
4251
|
-
summary: `\uC815\uC801 \uBD84\uC11D \uC644\uB8CC. ${categories.filter((c2) => c2.score >= 60).length}/6 \uCE74\uD14C\uACE0\uB9AC \uD1B5\uACFC.`
|
|
4252
|
-
};
|
|
4253
|
-
}
|
|
4254
|
-
async function checkTestCoverage(repoPath) {
|
|
4255
|
-
const findings = [];
|
|
4256
|
-
let score = 0;
|
|
4257
|
-
const testConfigs = [
|
|
4258
|
-
"jest.config.ts",
|
|
4259
|
-
"jest.config.js",
|
|
4260
|
-
"vitest.config.ts",
|
|
4261
|
-
"vitest.config.js",
|
|
4262
|
-
"pytest.ini",
|
|
4263
|
-
"phpunit.xml",
|
|
4264
|
-
".nycrc"
|
|
4265
|
-
];
|
|
4266
|
-
for (const f of testConfigs) {
|
|
4267
|
-
const found = await fs13.pathExists(join7(repoPath, f));
|
|
4268
|
-
findings.push({ item: f, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
4269
|
-
if (found) score += 20;
|
|
4270
|
-
}
|
|
4271
|
-
const testDirs = ["tests", "test", "__tests__", "spec"];
|
|
4272
|
-
let hasTestDir = false;
|
|
4273
|
-
for (const d of testDirs) {
|
|
4274
|
-
if (await fs13.pathExists(join7(repoPath, d))) {
|
|
4275
|
-
findings.push({ item: d, found: true, details: "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" });
|
|
4276
|
-
hasTestDir = true;
|
|
4277
|
-
score += 30;
|
|
4278
|
-
break;
|
|
4279
|
-
}
|
|
4280
|
-
}
|
|
4281
|
-
if (!hasTestDir) {
|
|
4282
|
-
findings.push({ item: "test directory", found: false, details: "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC5C6\uC74C" });
|
|
4283
|
-
}
|
|
4284
|
-
score = Math.min(100, score);
|
|
4285
|
-
const recommendations = score < 60 ? [{ severity: "critical", message: "\uD14C\uC2A4\uD2B8 \uD658\uACBD \uBBF8\uAD6C\uC131", action: "\uD14C\uC2A4\uD2B8 \uD504\uB808\uC784\uC6CC\uD06C\uC640 \uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC\uB97C \uCD94\uAC00\uD558\uC138\uC694" }] : [];
|
|
4286
|
-
return { name: "\uD14C\uC2A4\uD2B8 \uCEE4\uBC84\uB9AC\uC9C0", tier: "must", score, recommendations, rawFindings: findings };
|
|
4287
|
-
}
|
|
4288
|
-
async function checkCiCd(repoPath) {
|
|
4289
|
-
const findings = [];
|
|
4290
|
-
let score = 0;
|
|
4291
|
-
const ciConfigs = [
|
|
4292
|
-
{ path: ".github/workflows", label: "GitHub Actions" },
|
|
4293
|
-
{ path: ".gitlab-ci.yml", label: "GitLab CI" },
|
|
4294
|
-
{ path: "Jenkinsfile", label: "Jenkins" },
|
|
4295
|
-
{ path: ".circleci", label: "CircleCI" }
|
|
4296
|
-
];
|
|
4297
|
-
for (const { path: path10, label } of ciConfigs) {
|
|
4298
|
-
const found = await fs13.pathExists(join7(repoPath, path10));
|
|
4299
|
-
findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
4300
|
-
if (found) score += 40;
|
|
4301
|
-
}
|
|
4302
|
-
score = Math.min(100, score);
|
|
4303
|
-
const recommendations = score === 0 ? [{ severity: "critical", message: "CI/CD \uBBF8\uAD6C\uC131", action: "GitHub Actions \uB610\uB294 \uB2E4\uB978 CI \uD30C\uC774\uD504\uB77C\uC778\uC744 \uC124\uC815\uD558\uC138\uC694" }] : [];
|
|
4304
|
-
return { name: "CI/CD", tier: "must", score, recommendations, rawFindings: findings };
|
|
4305
|
-
}
|
|
4306
|
-
async function checkHooks(repoPath) {
|
|
4307
|
-
const findings = [];
|
|
4308
|
-
let score = 0;
|
|
4309
|
-
const hookConfigs = [
|
|
4310
|
-
{ path: ".husky", label: "Husky" },
|
|
4311
|
-
{ path: ".lintstagedrc", label: "lint-staged" },
|
|
4312
|
-
{ path: ".lintstagedrc.json", label: "lint-staged (json)" },
|
|
4313
|
-
{ path: "commitlint.config.js", label: "commitlint" },
|
|
4314
|
-
{ path: ".claude/settings.json", label: "Claude Code settings" }
|
|
4315
|
-
];
|
|
4316
|
-
for (const { path: path10, label } of hookConfigs) {
|
|
4317
|
-
const found = await fs13.pathExists(join7(repoPath, path10));
|
|
4318
|
-
findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
4319
|
-
if (found) score += 20;
|
|
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})`);
|
|
4320
4263
|
}
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
}
|
|
4325
|
-
async function checkRepoStructure(repoPath) {
|
|
4326
|
-
const findings = [];
|
|
4327
|
-
let score = 0;
|
|
4328
|
-
const structureChecks = [
|
|
4329
|
-
{ path: "src", label: "src/ \uB514\uB809\uD1A0\uB9AC" },
|
|
4330
|
-
{ path: "package.json", label: "\uC758\uC874\uC131 \uAD00\uB9AC" },
|
|
4331
|
-
{ path: ".env.example", label: "\uD658\uACBD\uBCC0\uC218 \uC608\uC2DC" },
|
|
4332
|
-
{ path: ".gitignore", label: ".gitignore" }
|
|
4333
|
-
];
|
|
4334
|
-
for (const { path: path10, label } of structureChecks) {
|
|
4335
|
-
const found = await fs13.pathExists(join7(repoPath, path10));
|
|
4336
|
-
findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
4337
|
-
if (found) score += 25;
|
|
4264
|
+
const data = await res.json();
|
|
4265
|
+
if (!Array.isArray(data)) {
|
|
4266
|
+
throw new Error(`Expected directory, got single entry at ${dirPath}`);
|
|
4338
4267
|
}
|
|
4339
|
-
|
|
4340
|
-
return { name: "\uB9AC\uD3EC\uC9C0\uD1A0\uB9AC \uAD6C\uC870", tier: "nice", score, recommendations: [], rawFindings: findings };
|
|
4268
|
+
return data;
|
|
4341
4269
|
}
|
|
4342
|
-
async function
|
|
4343
|
-
const
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
{ path: "README.md", label: "README.md", points: 30 },
|
|
4347
|
-
{ path: "CONTRIBUTING.md", label: "CONTRIBUTING.md", points: 20 },
|
|
4348
|
-
{ path: "docs", label: "docs/ \uB514\uB809\uD1A0\uB9AC", points: 25 },
|
|
4349
|
-
{ path: "docs/openspec.md", label: "OpenSpec PRD", points: 25 }
|
|
4350
|
-
];
|
|
4351
|
-
for (const { path: path10, label, points } of docChecks) {
|
|
4352
|
-
const found = await fs13.pathExists(join7(repoPath, path10));
|
|
4353
|
-
findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
4354
|
-
if (found) score += points;
|
|
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})`);
|
|
4355
4274
|
}
|
|
4356
|
-
|
|
4357
|
-
|
|
4275
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
4276
|
+
await fs13.ensureDir(path7.dirname(destPath));
|
|
4277
|
+
await fs13.writeFile(destPath, buf);
|
|
4358
4278
|
}
|
|
4359
|
-
async function
|
|
4360
|
-
const
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
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
|
+
}
|
|
4374
4321
|
}
|
|
4375
|
-
|
|
4376
|
-
return
|
|
4322
|
+
await walk(srcPath, destDir);
|
|
4323
|
+
return result;
|
|
4377
4324
|
}
|
|
4378
|
-
var
|
|
4379
|
-
"src/
|
|
4380
|
-
"use strict";
|
|
4381
|
-
}
|
|
4382
|
-
});
|
|
4383
|
-
|
|
4384
|
-
// src/core/types/stage.ts
|
|
4385
|
-
var STAGE_ORDER;
|
|
4386
|
-
var init_stage = __esm({
|
|
4387
|
-
"src/core/types/stage.ts"() {
|
|
4325
|
+
var init_github_fetch = __esm({
|
|
4326
|
+
"src/utils/github-fetch.ts"() {
|
|
4388
4327
|
"use strict";
|
|
4389
|
-
STAGE_ORDER = [
|
|
4390
|
-
"environment",
|
|
4391
|
-
"design",
|
|
4392
|
-
"execution",
|
|
4393
|
-
"validation",
|
|
4394
|
-
"evaluation"
|
|
4395
|
-
];
|
|
4396
4328
|
}
|
|
4397
4329
|
});
|
|
4398
4330
|
|
|
4399
|
-
// src/
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
if (score >= 70) return "C";
|
|
4404
|
-
if (score >= 50) return "D";
|
|
4405
|
-
return "F";
|
|
4406
|
-
}
|
|
4407
|
-
var DEFAULT_CATEGORY_WEIGHTS;
|
|
4408
|
-
var init_scoring = __esm({
|
|
4409
|
-
"src/core/types/scoring.ts"() {
|
|
4410
|
-
"use strict";
|
|
4411
|
-
DEFAULT_CATEGORY_WEIGHTS = [
|
|
4412
|
-
{ name: "\uD14C\uC2A4\uD2B8 \uCEE4\uBC84\uB9AC\uC9C0", tier: "must", weight: 0.2 },
|
|
4413
|
-
{ name: "CI/CD", tier: "must", weight: 0.2 },
|
|
4414
|
-
{ name: "\uD6C5 \uAE30\uBC18 \uAC80\uC99D", tier: "must", weight: 0.2 },
|
|
4415
|
-
{ name: "\uB9AC\uD3EC\uC9C0\uD1A0\uB9AC \uAD6C\uC870", tier: "nice", weight: 0.133 },
|
|
4416
|
-
{ name: "\uBB38\uC11C\uD654 \uC218\uC900", tier: "nice", weight: 0.133 },
|
|
4417
|
-
{ name: "\uD558\uB124\uC2A4 \uC5D4\uC9C0\uB2C8\uC5B4\uB9C1", tier: "nice", weight: 0.134 }
|
|
4418
|
-
];
|
|
4419
|
-
}
|
|
4331
|
+
// src/cli/commands/fetch.cmd.ts
|
|
4332
|
+
var fetch_cmd_exports = {};
|
|
4333
|
+
__export(fetch_cmd_exports, {
|
|
4334
|
+
fetchCommand: () => fetchCommand
|
|
4420
4335
|
});
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4336
|
+
import path8 from "path";
|
|
4337
|
+
import fs14 from "fs-extra";
|
|
4338
|
+
import chalk4 from "chalk";
|
|
4339
|
+
async function fetchCommand(cwd, recipeKey, options) {
|
|
4340
|
+
if (options.list) {
|
|
4341
|
+
section("\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uB808\uC2DC\uD53C");
|
|
4342
|
+
for (const key of listRecipeKeys()) {
|
|
4343
|
+
const r = RECIPES[key];
|
|
4344
|
+
console.log(` ${colors.accent(key.padEnd(14))} ${r.label}`);
|
|
4345
|
+
console.log(` ${colors.dim(" " + r.description)}`);
|
|
4346
|
+
console.log(` ${colors.dim(" \uC18C\uC2A4: github.com/" + r.source.repo + "/tree/" + r.source.ref + "/" + r.source.path)}`);
|
|
4347
|
+
console.log("");
|
|
4348
|
+
}
|
|
4349
|
+
hint("\uC0AC\uC6A9: pai fetch <key> \uB610\uB294 pai fetch --all");
|
|
4350
|
+
return;
|
|
4428
4351
|
}
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
});
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
let totalGrade = gradeFromScore(totalScore);
|
|
4448
|
-
const { penaltyApplied, penaltyReason } = checkPenalty(categories);
|
|
4449
|
-
if (penaltyApplied && gradeRank(totalGrade) > gradeRank("C")) {
|
|
4450
|
-
totalGrade = "C";
|
|
4451
|
-
}
|
|
4452
|
-
return {
|
|
4453
|
-
categories,
|
|
4454
|
-
totalScore,
|
|
4455
|
-
totalGrade,
|
|
4456
|
-
summary: llmOutput.summary,
|
|
4457
|
-
penaltyApplied,
|
|
4458
|
-
penaltyReason
|
|
4459
|
-
};
|
|
4460
|
-
}
|
|
4461
|
-
function computeWeightedAverage(categories, weights) {
|
|
4462
|
-
let weightedSum = 0;
|
|
4463
|
-
let totalWeight = 0;
|
|
4464
|
-
for (const cat of categories) {
|
|
4465
|
-
const config = weights.find((w) => w.name === cat.name);
|
|
4466
|
-
const weight = config?.weight ?? (cat.tier === "must" ? 0.2 : 0.133);
|
|
4467
|
-
weightedSum += cat.score * weight;
|
|
4468
|
-
totalWeight += weight;
|
|
4352
|
+
let keys;
|
|
4353
|
+
if (options.all) {
|
|
4354
|
+
keys = listRecipeKeys();
|
|
4355
|
+
} else {
|
|
4356
|
+
if (!recipeKey) {
|
|
4357
|
+
error("\uB808\uC2DC\uD53C \uD0A4\uB97C \uC9C0\uC815\uD558\uC138\uC694.");
|
|
4358
|
+
hint(`\uC0AC\uC6A9 \uAC00\uB2A5: ${listRecipeKeys().join(", ")}`);
|
|
4359
|
+
hint("\uBAA9\uB85D: pai fetch --list");
|
|
4360
|
+
process.exitCode = 1;
|
|
4361
|
+
return;
|
|
4362
|
+
}
|
|
4363
|
+
if (!getRecipe(recipeKey)) {
|
|
4364
|
+
error(`\uC54C \uC218 \uC5C6\uB294 \uB808\uC2DC\uD53C: ${recipeKey}`);
|
|
4365
|
+
hint(`\uC0AC\uC6A9 \uAC00\uB2A5: ${listRecipeKeys().join(", ")}`);
|
|
4366
|
+
process.exitCode = 1;
|
|
4367
|
+
return;
|
|
4368
|
+
}
|
|
4369
|
+
keys = [recipeKey.toLowerCase()];
|
|
4469
4370
|
}
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
(
|
|
4476
|
-
);
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4371
|
+
section("\uB808\uC2DC\uD53C \uB2E4\uC6B4\uB85C\uB4DC");
|
|
4372
|
+
const installed = [];
|
|
4373
|
+
for (const key of keys) {
|
|
4374
|
+
const recipe = RECIPES[key];
|
|
4375
|
+
const targetDir = path8.join(cwd, recipe.target);
|
|
4376
|
+
console.log("");
|
|
4377
|
+
console.log(` ${colors.accent(key)} \u2014 ${recipe.label}`);
|
|
4378
|
+
const result = await withSpinner(`\uB2E4\uC6B4\uB85C\uB4DC \uC911...`, async () => {
|
|
4379
|
+
return fetchGithubDir({
|
|
4380
|
+
repo: recipe.source.repo,
|
|
4381
|
+
ref: recipe.source.ref,
|
|
4382
|
+
srcPath: recipe.source.path,
|
|
4383
|
+
destDir: targetDir,
|
|
4384
|
+
overwrite: options.overwrite ?? false
|
|
4385
|
+
});
|
|
4386
|
+
});
|
|
4387
|
+
if (result.errors.length > 0) {
|
|
4388
|
+
error(`\uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${result.errors[0].error}`);
|
|
4389
|
+
for (const e of result.errors.slice(1)) {
|
|
4390
|
+
console.log(chalk4.gray(` ${e.path}: ${e.error}`));
|
|
4391
|
+
}
|
|
4392
|
+
continue;
|
|
4393
|
+
}
|
|
4394
|
+
if (result.written.length > 0) {
|
|
4395
|
+
success(`${result.written.length}\uAC1C \uD30C\uC77C \uC800\uC7A5`);
|
|
4396
|
+
for (const f of result.written) {
|
|
4397
|
+
console.log(chalk4.gray(` ${path8.relative(cwd, f)}`));
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
if (result.skipped.length > 0) {
|
|
4401
|
+
info(`${result.skipped.length}\uAC1C \uD30C\uC77C \uAC74\uB108\uB700 (\uC774\uBBF8 \uC874\uC7AC \u2014 \uB36E\uC5B4\uC4F0\uAE30: --overwrite)`);
|
|
4402
|
+
}
|
|
4403
|
+
await appendEnvKeys(cwd, recipe);
|
|
4404
|
+
installed.push(key);
|
|
4483
4405
|
}
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
function gradeRank(grade) {
|
|
4487
|
-
const ranks = { A: 4, B: 3, C: 2, D: 1, F: 0 };
|
|
4488
|
-
return ranks[grade];
|
|
4489
|
-
}
|
|
4490
|
-
var init_scorer = __esm({
|
|
4491
|
-
"src/stages/evaluation/scorer.ts"() {
|
|
4492
|
-
"use strict";
|
|
4493
|
-
init_types();
|
|
4406
|
+
if (installed.length === 0) {
|
|
4407
|
+
return;
|
|
4494
4408
|
}
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
// src/stages/evaluation/reporter.ts
|
|
4498
|
-
var reporter_exports = {};
|
|
4499
|
-
__export(reporter_exports, {
|
|
4500
|
-
buildDetailedReport: () => buildDetailedReport,
|
|
4501
|
-
buildMarkdownReport: () => buildMarkdownReport,
|
|
4502
|
-
printReport: () => printReport,
|
|
4503
|
-
printVerboseFindings: () => printVerboseFindings
|
|
4504
|
-
});
|
|
4505
|
-
import chalk5 from "chalk";
|
|
4506
|
-
function printReport(result) {
|
|
4409
|
+
await upsertRecipesSkill(cwd, installed);
|
|
4410
|
+
await upsertClaudeMdBlock(cwd, installed);
|
|
4507
4411
|
console.log("");
|
|
4508
|
-
|
|
4509
|
-
console.log(chalk5.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
4412
|
+
success(`\uB808\uC2DC\uD53C ${installed.length}\uAC1C \uC124\uCE58 \uC644\uB8CC: ${installed.join(", ")}`);
|
|
4510
4413
|
console.log("");
|
|
4511
|
-
|
|
4512
|
-
console.log(
|
|
4513
|
-
|
|
4514
|
-
|
|
4414
|
+
console.log(colors.accent(" \uB2E4\uC74C \uB2E8\uACC4"));
|
|
4415
|
+
console.log(colors.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
4416
|
+
console.log(` ${colors.success("1.")} ${colors.dim(".env.local \uD30C\uC77C\uC744 \uC5F4\uC5B4 \uD658\uACBD\uBCC0\uC218 \uAC12\uC744 \uCC44\uC6B0\uC138\uC694")}`);
|
|
4417
|
+
for (const key of installed) {
|
|
4418
|
+
const recipe = RECIPES[key];
|
|
4419
|
+
for (const ek of recipe.envKeys) {
|
|
4420
|
+
console.log(` ${chalk4.cyan(ek.key.padEnd(22))} ${colors.dim(ek.hint ?? "")}`);
|
|
4421
|
+
}
|
|
4515
4422
|
}
|
|
4516
4423
|
console.log("");
|
|
4517
|
-
console.log(
|
|
4518
|
-
for (const
|
|
4519
|
-
const
|
|
4520
|
-
|
|
4521
|
-
const bar = renderBar(cat.score);
|
|
4522
|
-
console.log(` ${tierLabel} ${cat.name.padEnd(16)} ${bar} ${color(`${cat.score}`).padStart(12)} ${color(cat.grade)}`);
|
|
4424
|
+
console.log(` ${colors.success("2.")} ${colors.dim("Claude Code\uC5D0\uC11C \uAD00\uB828 \uAE30\uB2A5 \uAD6C\uD604 \uC2DC \uC790\uB3D9\uC73C\uB85C \uB808\uC2DC\uD53C \uBB38\uC11C\uB97C \uCC38\uC870\uD569\uB2C8\uB2E4")}`);
|
|
4425
|
+
for (const key of installed) {
|
|
4426
|
+
const recipe = RECIPES[key];
|
|
4427
|
+
console.log(` ${chalk4.cyan(recipe.target + "/")}`);
|
|
4523
4428
|
}
|
|
4524
4429
|
console.log("");
|
|
4525
|
-
console.log(chalk5.gray(` ${result.summary}`));
|
|
4526
|
-
console.log("");
|
|
4527
|
-
}
|
|
4528
|
-
function printVerboseFindings(result) {
|
|
4529
|
-
for (const cat of result.categories) {
|
|
4530
|
-
console.log("");
|
|
4531
|
-
console.log(chalk5.bold(` ${cat.name} (${cat.grade}, ${cat.score}\uC810)`));
|
|
4532
|
-
for (const f of cat.rawFindings) {
|
|
4533
|
-
const icon = f.found ? chalk5.green("\u2713") : chalk5.red("\u2717");
|
|
4534
|
-
console.log(` ${icon} ${f.item} \u2014 ${f.details}`);
|
|
4535
|
-
}
|
|
4536
|
-
for (const r of cat.recommendations) {
|
|
4537
|
-
const severity = r.severity === "critical" ? chalk5.red("!") : r.severity === "warning" ? chalk5.yellow("!") : chalk5.gray("i");
|
|
4538
|
-
console.log(` ${severity} ${r.message}`);
|
|
4539
|
-
console.log(chalk5.gray(` \u2192 ${r.action}`));
|
|
4540
|
-
}
|
|
4541
|
-
}
|
|
4542
4430
|
}
|
|
4543
|
-
function
|
|
4544
|
-
|
|
4545
|
-
const
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
}
|
|
4550
|
-
function buildMarkdownReport(result) {
|
|
4551
|
-
const lines = [
|
|
4552
|
-
"# PAI Evaluation Report",
|
|
4553
|
-
"",
|
|
4554
|
-
`> \uC0DD\uC131\uC77C\uC2DC: ${(/* @__PURE__ */ new Date()).toLocaleString("ko-KR")}`,
|
|
4555
|
-
"",
|
|
4556
|
-
`## \uC885\uD569 \uC810\uC218: ${result.totalScore}/100 \u2014 \uB4F1\uAE09 ${result.totalGrade}`,
|
|
4557
|
-
""
|
|
4558
|
-
];
|
|
4559
|
-
if (result.penaltyApplied) {
|
|
4560
|
-
lines.push(`> \u26A0 ${result.penaltyReason}`);
|
|
4561
|
-
lines.push("");
|
|
4562
|
-
}
|
|
4563
|
-
lines.push("## \uCE74\uD14C\uACE0\uB9AC\uBCC4 \uC0C1\uC138");
|
|
4564
|
-
lines.push("");
|
|
4565
|
-
lines.push("| \uCE74\uD14C\uACE0\uB9AC | \uBD84\uB958 | \uC810\uC218 | \uB4F1\uAE09 |");
|
|
4566
|
-
lines.push("|---------|------|------|------|");
|
|
4567
|
-
for (const cat of result.categories) {
|
|
4568
|
-
const tier = cat.tier === "must" ? "\uD544\uC218" : "\uC120\uD0DD";
|
|
4569
|
-
lines.push(`| ${cat.name} | ${tier} | ${cat.score} | ${cat.grade} |`);
|
|
4431
|
+
async function appendEnvKeys(cwd, recipe) {
|
|
4432
|
+
if (recipe.envKeys.length === 0) return;
|
|
4433
|
+
const envPath = path8.join(cwd, ".env.local");
|
|
4434
|
+
let content = "";
|
|
4435
|
+
if (await fs14.pathExists(envPath)) {
|
|
4436
|
+
content = await fs14.readFile(envPath, "utf8");
|
|
4570
4437
|
}
|
|
4438
|
+
const missingKeys = recipe.envKeys.filter((ek) => !content.includes(`${ek.key}=`));
|
|
4439
|
+
if (missingKeys.length === 0) return;
|
|
4440
|
+
const lines = [];
|
|
4441
|
+
if (content.length > 0 && !content.endsWith("\n")) lines.push("");
|
|
4571
4442
|
lines.push("");
|
|
4572
|
-
lines.push(
|
|
4573
|
-
lines.push(
|
|
4574
|
-
const
|
|
4575
|
-
(
|
|
4576
|
-
|
|
4577
|
-
if (allRecs.length === 0) {
|
|
4578
|
-
lines.push("\uBAA8\uB4E0 \uD56D\uBAA9\uC774 \uC591\uD638\uD569\uB2C8\uB2E4!");
|
|
4579
|
-
} else {
|
|
4580
|
-
for (const r of allRecs) {
|
|
4581
|
-
const icon = r.severity === "critical" ? "\u{1F534}" : r.severity === "warning" ? "\u{1F7E1}" : "\u2139\uFE0F";
|
|
4582
|
-
lines.push(`- ${icon} **${r.category}**: ${r.message}`);
|
|
4583
|
-
lines.push(` - ${r.action}`);
|
|
4584
|
-
}
|
|
4443
|
+
lines.push(`# \u2500\u2500 ${recipe.label} \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`);
|
|
4444
|
+
lines.push(`# \uAC00\uC774\uB4DC: ${recipe.target}/guideline.md`);
|
|
4445
|
+
for (const ek of missingKeys) {
|
|
4446
|
+
if (ek.hint) lines.push(`# ${ek.hint}`);
|
|
4447
|
+
lines.push(`${ek.key}=${ek.default ?? ""}`);
|
|
4585
4448
|
}
|
|
4586
|
-
|
|
4587
|
-
lines.
|
|
4588
|
-
lines.push("");
|
|
4589
|
-
lines.push(result.summary);
|
|
4590
|
-
lines.push("");
|
|
4591
|
-
lines.push("---");
|
|
4592
|
-
lines.push("*Generated by PAI (Plugin-based AI)*");
|
|
4593
|
-
return lines.join("\n") + "\n";
|
|
4449
|
+
await fs14.ensureFile(envPath);
|
|
4450
|
+
await fs14.appendFile(envPath, lines.join("\n") + "\n");
|
|
4594
4451
|
}
|
|
4595
|
-
function
|
|
4596
|
-
const
|
|
4597
|
-
|
|
4598
|
-
const
|
|
4599
|
-
const
|
|
4600
|
-
|
|
4452
|
+
async function upsertRecipesSkill(cwd, installedKeys) {
|
|
4453
|
+
const skillDir = path8.join(cwd, ".claude", "skills", "recipes");
|
|
4454
|
+
await fs14.ensureDir(skillDir);
|
|
4455
|
+
const skillPath = path8.join(skillDir, "SKILL.md");
|
|
4456
|
+
const recipes = installedKeys.map((k) => RECIPES[k]);
|
|
4457
|
+
const triggers = recipes.map((r) => r.skillDescription ?? `${r.label} \uAD00\uB828 \uAE30\uB2A5 \uAD6C\uD604 \uC2DC ${r.target}/ \uCC38\uC870`).join("\n- ");
|
|
4458
|
+
const body = [
|
|
4459
|
+
"---",
|
|
4460
|
+
"name: recipes",
|
|
4461
|
+
`description: "\uC0AC\uB0B4 \uC2DC\uC2A4\uD15C \uC5F0\uB3D9 \uB808\uC2DC\uD53C \u2014 ${recipes.map((r) => r.label).join(", ")}"`,
|
|
4462
|
+
"---",
|
|
4601
4463
|
"",
|
|
4602
|
-
|
|
4603
|
-
`> \uBD84\uC11D \uB300\uC0C1: ${projectName}`,
|
|
4464
|
+
"# \uC0AC\uB0B4 \uC2DC\uC2A4\uD15C \uC5F0\uB3D9 \uB808\uC2DC\uD53C",
|
|
4604
4465
|
"",
|
|
4605
|
-
"
|
|
4466
|
+
"\uC774 \uD504\uB85C\uC81D\uD2B8\uC5D0\uB294 \uB2E4\uC74C \uC0AC\uB0B4 \uC5F0\uB3D9 \uB808\uC2DC\uD53C\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.",
|
|
4467
|
+
"\uAD00\uB828 \uC694\uCCAD\uC744 \uBC1B\uC73C\uBA74 \uD574\uB2F9 \uACBD\uB85C\uC758 `guideline.md`\uB97C **\uBC18\uB4DC\uC2DC \uBA3C\uC800 \uC77D\uACE0**",
|
|
4468
|
+
"\uC9C0\uCE68\uC5D0 \uB9DE\uAC8C \uAD6C\uD604\uD558\uC138\uC694.",
|
|
4606
4469
|
"",
|
|
4607
|
-
"## \
|
|
4470
|
+
"## \uD2B8\uB9AC\uAC70",
|
|
4608
4471
|
"",
|
|
4609
|
-
|
|
4610
|
-
""
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
lines.push(
|
|
4639
|
-
lines.push("
|
|
4472
|
+
"- " + triggers,
|
|
4473
|
+
"",
|
|
4474
|
+
"## \uC124\uCE58\uB41C \uB808\uC2DC\uD53C",
|
|
4475
|
+
"",
|
|
4476
|
+
...recipes.map((r) => [
|
|
4477
|
+
`### ${r.label}`,
|
|
4478
|
+
"",
|
|
4479
|
+
`- \uACBD\uB85C: \`${r.target}/\``,
|
|
4480
|
+
`- \uC8FC\uC694 \uBB38\uC11C: \`${r.target}/guideline.md\``,
|
|
4481
|
+
`- \uD658\uACBD\uBCC0\uC218: ${r.envKeys.map((e) => "`" + e.key + "`").join(", ")}`,
|
|
4482
|
+
""
|
|
4483
|
+
].join("\n")),
|
|
4484
|
+
"",
|
|
4485
|
+
"## \uC791\uC5C5 \uC21C\uC11C",
|
|
4486
|
+
"",
|
|
4487
|
+
"1. \uC0AC\uC6A9\uC790\uC758 \uC694\uCCAD\uC774 \uC704 \uD2B8\uB9AC\uAC70 \uC911 \uD558\uB098\uC5D0 \uD574\uB2F9\uD558\uB294\uC9C0 \uD310\uB2E8",
|
|
4488
|
+
"2. \uD574\uB2F9 \uB808\uC2DC\uD53C\uC758 `guideline.md`\uB97C Read \uB3C4\uAD6C\uB85C \uC77D\uAE30",
|
|
4489
|
+
"3. \uC0D8\uD50C \uCF54\uB4DC(`*.html`, `*.ts` \uB4F1)\uAC00 \uC788\uC73C\uBA74 \uD568\uAED8 \uD655\uC778",
|
|
4490
|
+
"4. `.env.local`\uC5D0 \uAD00\uB828 \uD658\uACBD\uBCC0\uC218\uAC00 \uCC44\uC6CC\uC838 \uC788\uB294\uC9C0 \uD655\uC778",
|
|
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",
|
|
4492
|
+
""
|
|
4493
|
+
].join("\n");
|
|
4494
|
+
await fs14.writeFile(skillPath, body);
|
|
4495
|
+
}
|
|
4496
|
+
async function upsertClaudeMdBlock(cwd, installedKeys) {
|
|
4497
|
+
const claudeMdPath = path8.join(cwd, "CLAUDE.md");
|
|
4498
|
+
const BLOCK_START = "<!-- pai:recipes:start -->";
|
|
4499
|
+
const BLOCK_END = "<!-- pai:recipes:end -->";
|
|
4500
|
+
const lines = [];
|
|
4501
|
+
lines.push(BLOCK_START);
|
|
4502
|
+
lines.push("## \uC0AC\uB0B4 \uC2DC\uC2A4\uD15C \uC5F0\uB3D9");
|
|
4640
4503
|
lines.push("");
|
|
4641
|
-
lines.push("
|
|
4504
|
+
lines.push("\uB2E4\uC74C \uB808\uC2DC\uD53C\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4. \uAD00\uB828 \uAE30\uB2A5 \uAD6C\uD604 \uC2DC **\uBC18\uB4DC\uC2DC** \uD574\uB2F9 \uACBD\uB85C\uC758");
|
|
4505
|
+
lines.push("`guideline.md`\uB97C \uBA3C\uC800 \uC77D\uACE0 \uC9C0\uCE68\uC744 \uB530\uB974\uC138\uC694:");
|
|
4642
4506
|
lines.push("");
|
|
4643
|
-
for (const
|
|
4644
|
-
const
|
|
4645
|
-
lines.push(
|
|
4646
|
-
lines.push("");
|
|
4647
|
-
if (cat.rawFindings.length > 0) {
|
|
4648
|
-
lines.push("**\uC810\uAC80 \uACB0\uACFC:**");
|
|
4649
|
-
lines.push("");
|
|
4650
|
-
for (const f of cat.rawFindings) {
|
|
4651
|
-
const icon = f.found ? "\u2705" : "\u274C";
|
|
4652
|
-
lines.push(`- ${icon} ${f.item} \u2014 ${f.details}`);
|
|
4653
|
-
}
|
|
4654
|
-
lines.push("");
|
|
4655
|
-
}
|
|
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("");
|
|
4507
|
+
for (const key of installedKeys) {
|
|
4508
|
+
const r = RECIPES[key];
|
|
4509
|
+
lines.push(`- **${r.label}** \u2014 \`${r.target}/\``);
|
|
4510
|
+
lines.push(` \uD658\uACBD\uBCC0\uC218: ${r.envKeys.map((e) => "`" + e.key + "`").join(", ")}`);
|
|
4678
4511
|
}
|
|
4679
|
-
lines.push("## \uAC1C\uC120 \uB85C\uB4DC\uB9F5");
|
|
4680
4512
|
lines.push("");
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
const
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
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("");
|
|
4513
|
+
lines.push("\uD658\uACBD\uBCC0\uC218 \uAC12\uC740 `.env.local`\uC5D0 \uCC44\uC6CC\uC838 \uC788\uC5B4\uC57C \uD569\uB2C8\uB2E4.");
|
|
4514
|
+
lines.push(BLOCK_END);
|
|
4515
|
+
const block = lines.join("\n");
|
|
4516
|
+
let content = "";
|
|
4517
|
+
if (await fs14.pathExists(claudeMdPath)) {
|
|
4518
|
+
content = await fs14.readFile(claudeMdPath, "utf8");
|
|
4713
4519
|
}
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
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.");
|
|
4520
|
+
const startIdx = content.indexOf(BLOCK_START);
|
|
4521
|
+
const endIdx = content.indexOf(BLOCK_END);
|
|
4522
|
+
if (startIdx >= 0 && endIdx > startIdx) {
|
|
4523
|
+
const before = content.slice(0, startIdx);
|
|
4524
|
+
const after = content.slice(endIdx + BLOCK_END.length);
|
|
4525
|
+
content = before + block + after;
|
|
4725
4526
|
} else {
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
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;
|
|
4527
|
+
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
|
4528
|
+
if (content.length > 0) content += "\n";
|
|
4529
|
+
content += block + "\n";
|
|
4802
4530
|
}
|
|
4803
|
-
await fs14.
|
|
4804
|
-
|
|
4805
|
-
return false;
|
|
4531
|
+
await fs14.ensureFile(claudeMdPath);
|
|
4532
|
+
await fs14.writeFile(claudeMdPath, content);
|
|
4806
4533
|
}
|
|
4807
|
-
var
|
|
4808
|
-
|
|
4809
|
-
"src/utils/shell-cd.ts"() {
|
|
4534
|
+
var init_fetch_cmd = __esm({
|
|
4535
|
+
"src/cli/commands/fetch.cmd.ts"() {
|
|
4810
4536
|
"use strict";
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
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
|
-
`;
|
|
4537
|
+
init_ui();
|
|
4538
|
+
init_logger();
|
|
4539
|
+
init_recipes();
|
|
4540
|
+
init_github_fetch();
|
|
4541
|
+
init_progress();
|
|
4852
4542
|
}
|
|
4853
4543
|
});
|
|
4854
4544
|
|
|
4855
|
-
// src/
|
|
4856
|
-
var
|
|
4857
|
-
|
|
4858
|
-
|
|
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"() {
|
|
4545
|
+
// src/stages/environment/index.ts
|
|
4546
|
+
var environmentStage;
|
|
4547
|
+
var init_environment = __esm({
|
|
4548
|
+
"src/stages/environment/index.ts"() {
|
|
4952
4549
|
"use strict";
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4550
|
+
init_analyzer();
|
|
4551
|
+
init_interviewer();
|
|
4552
|
+
init_generator();
|
|
4553
|
+
init_installer();
|
|
4554
|
+
init_registry();
|
|
4555
|
+
init_claude_commands();
|
|
4556
|
+
init_detector();
|
|
4557
|
+
init_config();
|
|
4558
|
+
init_progress();
|
|
4559
|
+
init_ui();
|
|
4560
|
+
init_analyzer();
|
|
4561
|
+
init_interviewer();
|
|
4562
|
+
init_doctor();
|
|
4563
|
+
environmentStage = {
|
|
4564
|
+
name: "environment",
|
|
4565
|
+
canSkip(input) {
|
|
4566
|
+
return input.config.plugins.length > 0;
|
|
4567
|
+
},
|
|
4568
|
+
async run(input) {
|
|
4569
|
+
const start = Date.now();
|
|
4570
|
+
const artifacts = [];
|
|
4571
|
+
const errors = [];
|
|
4572
|
+
try {
|
|
4573
|
+
console.log("");
|
|
4574
|
+
const analysis = await withSpinner("\uD504\uB85C\uC81D\uD2B8 \uBD84\uC11D \uC911...", async () => {
|
|
4575
|
+
const result = await analyzeProject(input.cwd);
|
|
4576
|
+
await sleep(500);
|
|
4577
|
+
return result;
|
|
4578
|
+
});
|
|
4579
|
+
console.log("");
|
|
4580
|
+
if (analysis.stack.languages.length > 0) {
|
|
4581
|
+
success(`\uC2A4\uD0DD: ${analysis.stack.languages.join(", ")}`);
|
|
4582
|
+
}
|
|
4583
|
+
if (analysis.stack.frameworks.length > 0) {
|
|
4584
|
+
success(`\uD504\uB808\uC784\uC6CC\uD06C: ${analysis.stack.frameworks.join(", ")}`);
|
|
4585
|
+
}
|
|
4586
|
+
success(`Git: ${analysis.git.isGitRepo ? `${analysis.git.repoName ?? "local"}` : "\uBBF8\uCD08\uAE30\uD654"}`);
|
|
4587
|
+
const interview = await runInterview(analysis, input.cwd, input.config.projectName);
|
|
4588
|
+
console.log("");
|
|
4589
|
+
const configFiles = [
|
|
4590
|
+
"CLAUDE.md",
|
|
4591
|
+
".claude/settings.json",
|
|
4592
|
+
"docs/vibe-coding/01-intent.md",
|
|
4593
|
+
"docs/vibe-coding/02-requirements.md",
|
|
4594
|
+
"docs/vibe-coding/03-research.md",
|
|
4595
|
+
"docs/vibe-coding/04-plan.md",
|
|
4596
|
+
"docs/vibe-coding/05-implement.md"
|
|
4597
|
+
];
|
|
4598
|
+
const generated = await withFileSpinner("\uC124\uC815 \uD30C\uC77C \uC0DD\uC131 \uC911...", configFiles, async () => {
|
|
4599
|
+
const files = await generateFiles(input.cwd, analysis, interview);
|
|
4600
|
+
await sleep(400);
|
|
4601
|
+
return files;
|
|
4602
|
+
});
|
|
4603
|
+
artifacts.push(...generated);
|
|
4604
|
+
const basePlugins = ["github", "openspec", "roboco"];
|
|
4605
|
+
const pluginKeys = [.../* @__PURE__ */ new Set([...basePlugins, ...interview.extraTools])];
|
|
4606
|
+
const provCtx = {
|
|
4607
|
+
cwd: input.cwd,
|
|
4608
|
+
projectName: interview.projectName,
|
|
4609
|
+
mode: interview.mode,
|
|
4610
|
+
envEntries: {
|
|
4611
|
+
PAI_PROJECT_NAME: interview.projectName,
|
|
4612
|
+
PAI_MODE: interview.mode
|
|
4613
|
+
},
|
|
4614
|
+
analysis,
|
|
4615
|
+
mcp: interview.mcp
|
|
4616
|
+
};
|
|
4617
|
+
if (interview.authMethods.includes("custom")) {
|
|
4618
|
+
provCtx.envEntries["OAUTH_CLIENT_ID"] = interview.customAuth?.clientId || "YOUR_CLIENT_ID_HERE";
|
|
4619
|
+
provCtx.envEntries["OAUTH_CLIENT_SECRET"] = interview.customAuth?.clientSecret || "YOUR_CLIENT_SECRET_HERE";
|
|
4620
|
+
provCtx.envEntries["OAUTH_REDIRECT_URI"] = "http://localhost:3000/auth/callback";
|
|
4621
|
+
}
|
|
4622
|
+
const pluginFileNames = [
|
|
4623
|
+
".gitignore",
|
|
4624
|
+
"src/",
|
|
4625
|
+
"docs/",
|
|
4626
|
+
"tests/",
|
|
4627
|
+
"public/",
|
|
4628
|
+
"docs/openspec.md",
|
|
4629
|
+
".pai/roboco.json",
|
|
4630
|
+
...interview.extraTools.includes("omc") ? [".pai/omc.md", ".omc/"] : [],
|
|
4631
|
+
...interview.extraTools.includes("vercel") ? ["vercel.json"] : [],
|
|
4632
|
+
...interview.extraTools.includes("supabase") ? ["supabase/config.toml"] : [],
|
|
4633
|
+
...interview.extraTools.includes("gstack") ? [".pai/gstack.json"] : [],
|
|
4634
|
+
...interview.extraTools.includes("harness") ? [".pai/harness.json"] : [],
|
|
4635
|
+
...interview.extraTools.includes("mcp") ? ["mcp-server/", ".mcp.json"] : [],
|
|
4636
|
+
".env.local"
|
|
4637
|
+
];
|
|
4638
|
+
console.log("");
|
|
4639
|
+
await withFileSpinner("\uD504\uB85C\uC81D\uD2B8 \uAD6C\uC870 \uC124\uCE58 \uC911...", pluginFileNames, async () => {
|
|
4640
|
+
await runProvisioners(pluginKeys, provCtx);
|
|
4641
|
+
await sleep(500);
|
|
4642
|
+
});
|
|
4643
|
+
const cmdFiles = [
|
|
4644
|
+
"SKILL.md",
|
|
4645
|
+
"info.md",
|
|
4646
|
+
"init.md",
|
|
4647
|
+
"status.md",
|
|
4648
|
+
"doctor.md",
|
|
4649
|
+
"design.md",
|
|
4650
|
+
"validate.md",
|
|
4651
|
+
"evaluate.md",
|
|
4652
|
+
"install.md"
|
|
4653
|
+
];
|
|
4654
|
+
console.log("");
|
|
4655
|
+
await withFileSpinner("\uC2AC\uB798\uC2DC \uCEE4\uB9E8\uB4DC \uC124\uCE58 \uC911...", cmdFiles, async () => {
|
|
4656
|
+
await provisionClaudeCommands(input.cwd);
|
|
4657
|
+
await sleep(400);
|
|
4658
|
+
});
|
|
4659
|
+
console.log("");
|
|
4660
|
+
const installResults = await installTools(interview.tools, input.cwd);
|
|
4661
|
+
printInstallReport(installResults, interview.tools);
|
|
4662
|
+
if (interview.recipes && interview.recipes.length > 0) {
|
|
4663
|
+
console.log("");
|
|
4664
|
+
const { fetchCommand: fetchCommand2 } = await Promise.resolve().then(() => (init_fetch_cmd(), fetch_cmd_exports));
|
|
4665
|
+
for (const recipeKey of interview.recipes) {
|
|
4666
|
+
try {
|
|
4667
|
+
await fetchCommand2(input.cwd, recipeKey, {});
|
|
4668
|
+
} catch (err) {
|
|
4669
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4670
|
+
warn(`\uB808\uC2DC\uD53C '${recipeKey}' \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328 \u2014 ${msg}`);
|
|
4671
|
+
hint(`\uB098\uC911\uC5D0 \uC218\uB3D9: pai fetch ${recipeKey}`);
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
console.log("");
|
|
4676
|
+
await withSpinner("\uC124\uC815 \uC800\uC7A5 \uC911...", async () => {
|
|
4677
|
+
const config = createDefaultConfig(interview.projectName, interview.mode);
|
|
4678
|
+
config.plugins = pluginKeys;
|
|
4679
|
+
config.edges = deriveEdges(pluginKeys);
|
|
4680
|
+
if (interview.mcp) config.mcp = interview.mcp;
|
|
4681
|
+
await saveConfig(input.cwd, config);
|
|
4682
|
+
await sleep(300);
|
|
4683
|
+
});
|
|
4684
|
+
return {
|
|
4685
|
+
stage: "environment",
|
|
4686
|
+
status: "success",
|
|
4687
|
+
data: { analysis, interview, installResults },
|
|
4688
|
+
artifacts,
|
|
4689
|
+
duration: Date.now() - start,
|
|
4690
|
+
errors
|
|
4691
|
+
};
|
|
4692
|
+
} catch (err) {
|
|
4693
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4694
|
+
errors.push({ code: "ENV_SETUP_FAILED", message: msg, recoverable: true });
|
|
4695
|
+
return {
|
|
4696
|
+
stage: "environment",
|
|
4697
|
+
status: "failed",
|
|
4698
|
+
data: {},
|
|
4699
|
+
artifacts,
|
|
4700
|
+
duration: Date.now() - start,
|
|
4701
|
+
errors
|
|
4702
|
+
};
|
|
4703
|
+
}
|
|
4962
4704
|
}
|
|
4963
|
-
backupPath;
|
|
4964
4705
|
};
|
|
4965
4706
|
}
|
|
4966
4707
|
});
|
|
4967
4708
|
|
|
4968
|
-
// src/stages/
|
|
4969
|
-
import {
|
|
4970
|
-
import
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
const
|
|
4974
|
-
|
|
4975
|
-
|
|
4709
|
+
// src/stages/validation/runner.ts
|
|
4710
|
+
import { join as join7 } from "path";
|
|
4711
|
+
import fs15 from "fs-extra";
|
|
4712
|
+
async function runTests(cwd) {
|
|
4713
|
+
const start = Date.now();
|
|
4714
|
+
const gstackPath = join7(cwd, ".pai", "gstack.json");
|
|
4715
|
+
let runner = "npm test";
|
|
4716
|
+
if (await fs15.pathExists(gstackPath)) {
|
|
4976
4717
|
try {
|
|
4977
|
-
const
|
|
4978
|
-
|
|
4718
|
+
const config = await fs15.readJson(gstackPath);
|
|
4719
|
+
if (config.testRunner === "vitest") runner = "npx vitest run";
|
|
4720
|
+
else if (config.testRunner === "jest") runner = "npx jest";
|
|
4721
|
+
else if (config.testRunner === "mocha") runner = "npx mocha";
|
|
4979
4722
|
} catch {
|
|
4980
|
-
hash.update(`${file}:missing`);
|
|
4981
4723
|
}
|
|
4982
4724
|
}
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4725
|
+
const pkgPath = join7(cwd, "package.json");
|
|
4726
|
+
if (await fs15.pathExists(pkgPath)) {
|
|
4727
|
+
try {
|
|
4728
|
+
const pkg5 = await fs15.readJson(pkgPath);
|
|
4729
|
+
if (!pkg5.scripts?.test || pkg5.scripts.test.includes("no test specified")) {
|
|
4730
|
+
return {
|
|
4731
|
+
runner,
|
|
4732
|
+
passed: false,
|
|
4733
|
+
output: "\uD14C\uC2A4\uD2B8 \uC2A4\uD06C\uB9BD\uD2B8\uAC00 \uC815\uC758\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. package.json\uC758 scripts.test\uB97C \uC124\uC815\uD558\uC138\uC694.",
|
|
4734
|
+
duration: Date.now() - start
|
|
4735
|
+
};
|
|
4736
|
+
}
|
|
4737
|
+
} catch {
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4740
|
+
try {
|
|
4741
|
+
const { execa } = await import("execa");
|
|
4742
|
+
const { stdout, stderr } = await execa("npm", ["test"], {
|
|
4743
|
+
cwd,
|
|
4744
|
+
timeout: 12e4,
|
|
4745
|
+
env: { ...process.env, CI: "true" }
|
|
4746
|
+
});
|
|
4747
|
+
return {
|
|
4748
|
+
runner,
|
|
4749
|
+
passed: true,
|
|
4750
|
+
output: stdout || stderr,
|
|
4751
|
+
duration: Date.now() - start
|
|
4752
|
+
};
|
|
4753
|
+
} catch (err) {
|
|
4754
|
+
const output = err instanceof Error ? err.message : String(err);
|
|
4755
|
+
return {
|
|
4756
|
+
runner,
|
|
4757
|
+
passed: false,
|
|
4758
|
+
output,
|
|
4759
|
+
duration: Date.now() - start
|
|
4760
|
+
};
|
|
4761
|
+
}
|
|
4987
4762
|
}
|
|
4988
|
-
|
|
4763
|
+
var init_runner = __esm({
|
|
4764
|
+
"src/stages/validation/runner.ts"() {
|
|
4765
|
+
"use strict";
|
|
4766
|
+
}
|
|
4767
|
+
});
|
|
4768
|
+
|
|
4769
|
+
// src/stages/validation/harness.ts
|
|
4770
|
+
import { join as join8 } from "path";
|
|
4771
|
+
import fs16 from "fs-extra";
|
|
4772
|
+
async function runHarnessCheck(cwd) {
|
|
4773
|
+
const harnessPath = join8(cwd, ".pai", "harness.json");
|
|
4774
|
+
if (!await fs16.pathExists(harnessPath)) {
|
|
4775
|
+
return { enabled: false, specFile: null, rules: [], checks: [] };
|
|
4776
|
+
}
|
|
4777
|
+
let config;
|
|
4989
4778
|
try {
|
|
4990
|
-
|
|
4991
|
-
return JSON.parse(data);
|
|
4779
|
+
config = await fs16.readJson(harnessPath);
|
|
4992
4780
|
} catch {
|
|
4993
|
-
return {
|
|
4781
|
+
return { enabled: false, specFile: null, rules: [], checks: [] };
|
|
4994
4782
|
}
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
const
|
|
4998
|
-
if (
|
|
4999
|
-
|
|
4783
|
+
const specFile = config.specFile ?? "docs/openspec.md";
|
|
4784
|
+
const rules = config.rules ?? [];
|
|
4785
|
+
const checks = [];
|
|
4786
|
+
if (rules.includes("spec-implementation-match")) {
|
|
4787
|
+
const specExists = await fs16.pathExists(join8(cwd, specFile));
|
|
4788
|
+
const srcExists = await fs16.pathExists(join8(cwd, "src"));
|
|
4789
|
+
checks.push({
|
|
4790
|
+
rule: "spec-implementation-match",
|
|
4791
|
+
passed: specExists && srcExists,
|
|
4792
|
+
detail: specExists && srcExists ? "\uC124\uACC4 \uBB38\uC11C\uC640 \uC18C\uC2A4 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" : `${!specExists ? specFile + " \uC5C6\uC74C" : ""} ${!srcExists ? "src/ \uC5C6\uC74C" : ""}`.trim()
|
|
4793
|
+
});
|
|
5000
4794
|
}
|
|
5001
|
-
|
|
4795
|
+
if (rules.includes("api-contract-test")) {
|
|
4796
|
+
const testDir = await fs16.pathExists(join8(cwd, "tests"));
|
|
4797
|
+
const testDir2 = await fs16.pathExists(join8(cwd, "test"));
|
|
4798
|
+
checks.push({
|
|
4799
|
+
rule: "api-contract-test",
|
|
4800
|
+
passed: testDir || testDir2,
|
|
4801
|
+
detail: testDir || testDir2 ? "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" : "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC(tests/ \uB610\uB294 test/) \uC5C6\uC74C"
|
|
4802
|
+
});
|
|
4803
|
+
}
|
|
4804
|
+
return { enabled: true, specFile, rules, checks };
|
|
5002
4805
|
}
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
4806
|
+
var init_harness = __esm({
|
|
4807
|
+
"src/stages/validation/harness.ts"() {
|
|
4808
|
+
"use strict";
|
|
4809
|
+
}
|
|
4810
|
+
});
|
|
4811
|
+
|
|
4812
|
+
// src/cli/commands/validate.cmd.ts
|
|
4813
|
+
var validate_cmd_exports = {};
|
|
4814
|
+
__export(validate_cmd_exports, {
|
|
4815
|
+
handleRobocoCliError: () => handleRobocoError,
|
|
4816
|
+
validateCommand: () => validateCommand
|
|
4817
|
+
});
|
|
4818
|
+
async function validateCommand(cwd, options = {}) {
|
|
4819
|
+
try {
|
|
4820
|
+
await withRoboco(
|
|
4821
|
+
cwd,
|
|
4822
|
+
"validation",
|
|
4823
|
+
"pai test",
|
|
4824
|
+
{ skipGates: options.skipGates, force: options.force, gate: STAGE_GATES.validation },
|
|
4825
|
+
async () => {
|
|
4826
|
+
section("\uD14C\uC2A4\uD2B8 \uC2E4\uD589");
|
|
4827
|
+
const testResult = await runTests(cwd);
|
|
4828
|
+
if (testResult.passed) {
|
|
4829
|
+
success(`\uD14C\uC2A4\uD2B8 \uD1B5\uACFC (${testResult.runner}, ${testResult.duration}ms)`);
|
|
4830
|
+
} else {
|
|
4831
|
+
error("\uD14C\uC2A4\uD2B8 \uC2E4\uD328");
|
|
4832
|
+
info(testResult.output.slice(0, 300));
|
|
4833
|
+
}
|
|
4834
|
+
section("\uD558\uB124\uC2A4 \uAC80\uC99D");
|
|
4835
|
+
const harness = await runHarnessCheck(cwd);
|
|
4836
|
+
if (!harness.enabled) {
|
|
4837
|
+
info("Harness \uC124\uC815 \uC5C6\uC74C \u2014 \uAC74\uB108\uB700");
|
|
4838
|
+
info("\uC124\uC815 \uCD94\uAC00: `pai add` \uC5D0\uC11C Harness Engineering \uC120\uD0DD");
|
|
4839
|
+
} else {
|
|
4840
|
+
for (const check of harness.checks) {
|
|
4841
|
+
if (check.passed) {
|
|
4842
|
+
success(`${check.rule}: ${check.detail}`);
|
|
4843
|
+
} else {
|
|
4844
|
+
warn(`${check.rule}: ${check.detail}`);
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
}
|
|
4848
|
+
const allPassed = testResult.passed && harness.checks.every((c2) => c2.passed);
|
|
4849
|
+
console.log("");
|
|
4850
|
+
if (allPassed) {
|
|
4851
|
+
success("\uAC80\uC99D \uD1B5\uACFC!");
|
|
4852
|
+
} else if (!testResult.passed) {
|
|
4853
|
+
error("\uAC80\uC99D \uC2E4\uD328 \u2014 \uD14C\uC2A4\uD2B8\uB97C \uC218\uC815\uD558\uC138\uC694.");
|
|
4854
|
+
throw new Error("tests-failed");
|
|
4855
|
+
} else {
|
|
4856
|
+
warn("\uBD80\uBD84 \uD1B5\uACFC \u2014 \uD558\uB124\uC2A4 \uAC80\uC99D \uD56D\uBAA9\uC744 \uD655\uC778\uD558\uC138\uC694.");
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
);
|
|
4860
|
+
await syncHandoff(cwd).catch(() => {
|
|
4861
|
+
});
|
|
4862
|
+
} catch (err) {
|
|
4863
|
+
handleRobocoError(err);
|
|
4864
|
+
}
|
|
5010
4865
|
}
|
|
5011
|
-
function
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
4866
|
+
function handleRobocoError(err) {
|
|
4867
|
+
if (err instanceof RobocoLockError) {
|
|
4868
|
+
error(err.message);
|
|
4869
|
+
process.exitCode = 1;
|
|
4870
|
+
return;
|
|
4871
|
+
}
|
|
4872
|
+
if (err instanceof GateFailedError) {
|
|
4873
|
+
error(`\uB2E8\uACC4 \uC9C4\uC785 \uC2E4\uD328 (${err.result.name})`);
|
|
4874
|
+
for (const v of err.result.violations) {
|
|
4875
|
+
warn(` \xB7 ${v.message}${v.location ? ` [${v.location}]` : ""}`);
|
|
4876
|
+
}
|
|
4877
|
+
hint("\uC6B0\uD68C \uC635\uC158: --skip-gates (\uACBD\uACE0\uB9CC) / --force (\uAC8C\uC774\uD2B8 \uBB34\uC2DC)");
|
|
4878
|
+
process.exitCode = 1;
|
|
4879
|
+
return;
|
|
4880
|
+
}
|
|
4881
|
+
if (err instanceof Error) {
|
|
4882
|
+
if (err.message === "tests-failed") {
|
|
4883
|
+
process.exitCode = 1;
|
|
4884
|
+
return;
|
|
5018
4885
|
}
|
|
4886
|
+
error(err.message);
|
|
4887
|
+
process.exitCode = 1;
|
|
4888
|
+
return;
|
|
5019
4889
|
}
|
|
5020
|
-
|
|
5021
|
-
|
|
4890
|
+
throw err;
|
|
4891
|
+
}
|
|
4892
|
+
var init_validate_cmd = __esm({
|
|
4893
|
+
"src/cli/commands/validate.cmd.ts"() {
|
|
4894
|
+
"use strict";
|
|
4895
|
+
init_ui();
|
|
4896
|
+
init_runner();
|
|
4897
|
+
init_harness();
|
|
4898
|
+
init_roboco();
|
|
4899
|
+
init_gates();
|
|
4900
|
+
}
|
|
4901
|
+
});
|
|
4902
|
+
|
|
4903
|
+
// src/stages/evaluation/prompts/analyze.ts
|
|
4904
|
+
var analyze_exports = {};
|
|
4905
|
+
__export(analyze_exports, {
|
|
4906
|
+
buildAnalysisPrompt: () => buildAnalysisPrompt
|
|
4907
|
+
});
|
|
4908
|
+
function buildAnalysisPrompt(repoPath) {
|
|
4909
|
+
return `Analyze the repository at "${repoPath}" for vibe coding readiness.
|
|
4910
|
+
|
|
4911
|
+
Use only Read, Glob, and Grep tools. Complete in ~20 tool calls.
|
|
4912
|
+
Focus on config files, not source code.
|
|
4913
|
+
|
|
4914
|
+
Evaluate these 6 categories:
|
|
4915
|
+
|
|
4916
|
+
### Must-Have (60% weight)
|
|
4917
|
+
1. **\uD14C\uC2A4\uD2B8 \uCEE4\uBC84\uB9AC\uC9C0**: test framework config, test dirs, coverage thresholds
|
|
4918
|
+
- 0=none, 20=config only, 40=minimal files, 60=moderate, 80=good, 100=comprehensive
|
|
4919
|
+
2. **CI/CD**: pipeline configs, quality gates, multi-stage
|
|
4920
|
+
- 0=none, 20=exists but minimal, 40=basic, 60=tests+lint, 80=tests+lint+build, 100=comprehensive
|
|
4921
|
+
3. **\uD6C5 \uAE30\uBC18 \uAC80\uC99D**: git hooks, lint-staged, AI agent hooks (.claude/settings.json)
|
|
4922
|
+
- 0=none, 20=framework only, 40=single hook, 60=lint+format, 80=lint+test+format, 100=comprehensive+AI
|
|
4923
|
+
|
|
4924
|
+
### Nice-to-Have (40% weight)
|
|
4925
|
+
4. **\uB9AC\uD3EC\uC9C0\uD1A0\uB9AC \uAD6C\uC870**: dir organization, deps management, env config
|
|
4926
|
+
- 0=flat/chaotic, 20=minimal, 40=basic, 60=reasonable, 80=well-organized, 100=exemplary
|
|
4927
|
+
5. **\uBB38\uC11C\uD654 \uC218\uC900**: README, CONTRIBUTING, API docs, architecture docs
|
|
4928
|
+
- 0=none, 20=minimal, 40=basic, 60=good README, 80=README+API, 100=comprehensive
|
|
4929
|
+
6. **\uD558\uB124\uC2A4 \uC5D4\uC9C0\uB2C8\uC5B4\uB9C1**: CLAUDE.md, AGENTS.md, .claude/ config, skills, commands
|
|
4930
|
+
- 0=none, 20=basic, 40=one AI config, 60=context+config, 80=context+safety+skills, 100=comprehensive
|
|
4931
|
+
|
|
4932
|
+
Output ONLY a JSON block:
|
|
4933
|
+
\`\`\`json
|
|
4934
|
+
{
|
|
4935
|
+
"categories": [
|
|
4936
|
+
{
|
|
4937
|
+
"name": "\uCE74\uD14C\uACE0\uB9AC\uBA85 (Korean)",
|
|
4938
|
+
"tier": "must" | "nice",
|
|
4939
|
+
"score": 0-100,
|
|
4940
|
+
"recommendations": [{ "severity": "critical"|"warning"|"info", "message": "...", "action": "..." }],
|
|
4941
|
+
"rawFindings": [{ "item": "filename or concept", "found": true|false, "details": "..." }]
|
|
4942
|
+
}
|
|
4943
|
+
],
|
|
4944
|
+
"summary": "2-3 sentence Korean summary"
|
|
5022
4945
|
}
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
4946
|
+
\`\`\``;
|
|
4947
|
+
}
|
|
4948
|
+
var init_analyze = __esm({
|
|
4949
|
+
"src/stages/evaluation/prompts/analyze.ts"() {
|
|
5026
4950
|
"use strict";
|
|
5027
|
-
CACHE_DIR = ".pai";
|
|
5028
|
-
CACHE_FILE = "cache/evaluation.json";
|
|
5029
|
-
CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
5030
|
-
FILES_TO_HASH = [
|
|
5031
|
-
"package.json",
|
|
5032
|
-
"pyproject.toml",
|
|
5033
|
-
"go.mod",
|
|
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
|
-
];
|
|
5048
4951
|
}
|
|
5049
4952
|
});
|
|
5050
4953
|
|
|
5051
|
-
// src/
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
async function
|
|
5059
|
-
const p = robocoPath(cwd);
|
|
5060
|
-
if (!await fs16.pathExists(p)) return freshState();
|
|
4954
|
+
// src/stages/evaluation/analyzer.ts
|
|
4955
|
+
var analyzer_exports2 = {};
|
|
4956
|
+
__export(analyzer_exports2, {
|
|
4957
|
+
analyzeRepository: () => analyzeRepository
|
|
4958
|
+
});
|
|
4959
|
+
import { join as join9 } from "path";
|
|
4960
|
+
import fs17 from "fs-extra";
|
|
4961
|
+
async function analyzeRepository(repoPath) {
|
|
5061
4962
|
try {
|
|
5062
|
-
|
|
5063
|
-
if (raw?.version !== "2.0") return freshState();
|
|
5064
|
-
return raw;
|
|
4963
|
+
return await aiAnalysis(repoPath);
|
|
5065
4964
|
} catch {
|
|
5066
|
-
return
|
|
4965
|
+
return staticAnalysis(repoPath);
|
|
5067
4966
|
}
|
|
5068
4967
|
}
|
|
5069
|
-
async function
|
|
5070
|
-
const
|
|
5071
|
-
await
|
|
5072
|
-
|
|
4968
|
+
async function aiAnalysis(repoPath) {
|
|
4969
|
+
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
4970
|
+
const { buildAnalysisPrompt: buildAnalysisPrompt2 } = await Promise.resolve().then(() => (init_analyze(), analyze_exports));
|
|
4971
|
+
const prompt = buildAnalysisPrompt2(repoPath);
|
|
4972
|
+
let resultText = "";
|
|
4973
|
+
for await (const message of query({
|
|
4974
|
+
prompt,
|
|
4975
|
+
options: {
|
|
4976
|
+
maxTurns: 200,
|
|
4977
|
+
systemPrompt: "You are a vibe coding readiness analyzer. Analyze the repository and output JSON.",
|
|
4978
|
+
allowedTools: ["Read", "Glob", "Grep"],
|
|
4979
|
+
permissionMode: "plan"
|
|
4980
|
+
}
|
|
4981
|
+
})) {
|
|
4982
|
+
if ("result" in message) {
|
|
4983
|
+
resultText = message.result;
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
4986
|
+
return parseJsonFromText(resultText);
|
|
5073
4987
|
}
|
|
5074
|
-
function
|
|
4988
|
+
function parseJsonFromText(text) {
|
|
4989
|
+
const jsonMatch = text.match(/```json\s*([\s\S]*?)```/);
|
|
4990
|
+
if (jsonMatch?.[1]) {
|
|
4991
|
+
return JSON.parse(jsonMatch[1]);
|
|
4992
|
+
}
|
|
4993
|
+
return JSON.parse(text);
|
|
4994
|
+
}
|
|
4995
|
+
async function staticAnalysis(repoPath) {
|
|
4996
|
+
const categories = [];
|
|
4997
|
+
categories.push(await checkTestCoverage(repoPath));
|
|
4998
|
+
categories.push(await checkCiCd(repoPath));
|
|
4999
|
+
categories.push(await checkHooks(repoPath));
|
|
5000
|
+
categories.push(await checkRepoStructure(repoPath));
|
|
5001
|
+
categories.push(await checkDocumentation(repoPath));
|
|
5002
|
+
categories.push(await checkHarnessEngineering(repoPath));
|
|
5075
5003
|
return {
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
stageHistory: [],
|
|
5079
|
-
lock: null,
|
|
5080
|
-
gates: {}
|
|
5004
|
+
categories,
|
|
5005
|
+
summary: `\uC815\uC801 \uBD84\uC11D \uC644\uB8CC. ${categories.filter((c2) => c2.score >= 60).length}/6 \uCE74\uD14C\uACE0\uB9AC \uD1B5\uACFC.`
|
|
5081
5006
|
};
|
|
5082
5007
|
}
|
|
5083
|
-
function
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5008
|
+
async function checkTestCoverage(repoPath) {
|
|
5009
|
+
const findings = [];
|
|
5010
|
+
let score = 0;
|
|
5011
|
+
const testConfigs = [
|
|
5012
|
+
"jest.config.ts",
|
|
5013
|
+
"jest.config.js",
|
|
5014
|
+
"vitest.config.ts",
|
|
5015
|
+
"vitest.config.js",
|
|
5016
|
+
"pytest.ini",
|
|
5017
|
+
"phpunit.xml",
|
|
5018
|
+
".nycrc"
|
|
5019
|
+
];
|
|
5020
|
+
for (const f of testConfigs) {
|
|
5021
|
+
const found = await fs17.pathExists(join9(repoPath, f));
|
|
5022
|
+
findings.push({ item: f, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
5023
|
+
if (found) score += 20;
|
|
5089
5024
|
}
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
if (state.lock) {
|
|
5099
|
-
if (!isLockStale(state.lock)) {
|
|
5100
|
-
throw new RobocoLockError(formatLockError(state.lock), state.lock);
|
|
5025
|
+
const testDirs = ["tests", "test", "__tests__", "spec"];
|
|
5026
|
+
let hasTestDir = false;
|
|
5027
|
+
for (const d of testDirs) {
|
|
5028
|
+
if (await fs17.pathExists(join9(repoPath, d))) {
|
|
5029
|
+
findings.push({ item: d, found: true, details: "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" });
|
|
5030
|
+
hasTestDir = true;
|
|
5031
|
+
score += 30;
|
|
5032
|
+
break;
|
|
5101
5033
|
}
|
|
5102
5034
|
}
|
|
5103
|
-
|
|
5104
|
-
|
|
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;
|
|
5115
|
-
}
|
|
5116
|
-
async function releaseLock(cwd) {
|
|
5117
|
-
const state = await loadRobocoState(cwd);
|
|
5118
|
-
if (state.lock && state.lock.pid === process.pid) {
|
|
5119
|
-
state.lock = null;
|
|
5120
|
-
await saveRobocoState(cwd, state);
|
|
5035
|
+
if (!hasTestDir) {
|
|
5036
|
+
findings.push({ item: "test directory", found: false, details: "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC5C6\uC74C" });
|
|
5121
5037
|
}
|
|
5038
|
+
score = Math.min(100, score);
|
|
5039
|
+
const recommendations = score < 60 ? [{ severity: "critical", message: "\uD14C\uC2A4\uD2B8 \uD658\uACBD \uBBF8\uAD6C\uC131", action: "\uD14C\uC2A4\uD2B8 \uD504\uB808\uC784\uC6CC\uD06C\uC640 \uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC\uB97C \uCD94\uAC00\uD558\uC138\uC694" }] : [];
|
|
5040
|
+
return { name: "\uD14C\uC2A4\uD2B8 \uCEE4\uBC84\uB9AC\uC9C0", tier: "must", score, recommendations, rawFindings: findings };
|
|
5122
5041
|
}
|
|
5123
|
-
function
|
|
5124
|
-
const
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
"
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
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);
|
|
5144
|
-
}
|
|
5145
|
-
async function markStageEnd(cwd, stage, success2, reason) {
|
|
5146
|
-
const state = await loadRobocoState(cwd);
|
|
5147
|
-
for (let i = state.stageHistory.length - 1; i >= 0; i--) {
|
|
5148
|
-
const entry = state.stageHistory[i];
|
|
5149
|
-
if (entry.stage === stage && entry.status === "in_progress") {
|
|
5150
|
-
entry.status = success2 ? "done" : "failed";
|
|
5151
|
-
entry.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5152
|
-
if (reason) entry.reason = reason;
|
|
5153
|
-
break;
|
|
5154
|
-
}
|
|
5042
|
+
async function checkCiCd(repoPath) {
|
|
5043
|
+
const findings = [];
|
|
5044
|
+
let score = 0;
|
|
5045
|
+
const ciConfigs = [
|
|
5046
|
+
{ path: ".github/workflows", label: "GitHub Actions" },
|
|
5047
|
+
{ path: ".gitlab-ci.yml", label: "GitLab CI" },
|
|
5048
|
+
{ path: "Jenkinsfile", label: "Jenkins" },
|
|
5049
|
+
{ path: ".circleci", label: "CircleCI" }
|
|
5050
|
+
];
|
|
5051
|
+
for (const { path: path10, label } of ciConfigs) {
|
|
5052
|
+
const found = await fs17.pathExists(join9(repoPath, path10));
|
|
5053
|
+
findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
5054
|
+
if (found) score += 40;
|
|
5155
5055
|
}
|
|
5156
|
-
|
|
5056
|
+
score = Math.min(100, score);
|
|
5057
|
+
const recommendations = score === 0 ? [{ severity: "critical", message: "CI/CD \uBBF8\uAD6C\uC131", action: "GitHub Actions \uB610\uB294 \uB2E4\uB978 CI \uD30C\uC774\uD504\uB77C\uC778\uC744 \uC124\uC815\uD558\uC138\uC694" }] : [];
|
|
5058
|
+
return { name: "CI/CD", tier: "must", score, recommendations, rawFindings: findings };
|
|
5157
5059
|
}
|
|
5158
|
-
async function
|
|
5159
|
-
const
|
|
5160
|
-
|
|
5161
|
-
|
|
5060
|
+
async function checkHooks(repoPath) {
|
|
5061
|
+
const findings = [];
|
|
5062
|
+
let score = 0;
|
|
5063
|
+
const hookConfigs = [
|
|
5064
|
+
{ path: ".husky", label: "Husky" },
|
|
5065
|
+
{ path: ".lintstagedrc", label: "lint-staged" },
|
|
5066
|
+
{ path: ".lintstagedrc.json", label: "lint-staged (json)" },
|
|
5067
|
+
{ path: "commitlint.config.js", label: "commitlint" },
|
|
5068
|
+
{ path: ".claude/settings.json", label: "Claude Code settings" }
|
|
5069
|
+
];
|
|
5070
|
+
for (const { path: path10, label } of hookConfigs) {
|
|
5071
|
+
const found = await fs17.pathExists(join9(repoPath, path10));
|
|
5072
|
+
findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
5073
|
+
if (found) score += 20;
|
|
5074
|
+
}
|
|
5075
|
+
score = Math.min(100, score);
|
|
5076
|
+
const recommendations = score < 40 ? [{ severity: "warning", message: "\uD6C5 \uAE30\uBC18 \uAC80\uC99D \uBD80\uC871", action: "Husky + lint-staged\uB97C \uC124\uC815\uD558\uC5EC \uCEE4\uBC0B \uC804 \uAC80\uC99D\uC744 \uCD94\uAC00\uD558\uC138\uC694" }] : [];
|
|
5077
|
+
return { name: "\uD6C5 \uAE30\uBC18 \uAC80\uC99D", tier: "must", score, recommendations, rawFindings: findings };
|
|
5162
5078
|
}
|
|
5163
|
-
async function
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
await markStageStart(cwd, stage, command);
|
|
5177
|
-
const value = await fn();
|
|
5178
|
-
await markStageEnd(cwd, stage, true);
|
|
5179
|
-
return value;
|
|
5180
|
-
} catch (err) {
|
|
5181
|
-
if (!(err instanceof GateFailedError)) {
|
|
5182
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
5183
|
-
await markStageEnd(cwd, stage, false, msg).catch(() => {
|
|
5184
|
-
});
|
|
5185
|
-
}
|
|
5186
|
-
throw err;
|
|
5187
|
-
} finally {
|
|
5188
|
-
await releaseLock(cwd).catch(() => {
|
|
5189
|
-
});
|
|
5079
|
+
async function checkRepoStructure(repoPath) {
|
|
5080
|
+
const findings = [];
|
|
5081
|
+
let score = 0;
|
|
5082
|
+
const structureChecks = [
|
|
5083
|
+
{ path: "src", label: "src/ \uB514\uB809\uD1A0\uB9AC" },
|
|
5084
|
+
{ path: "package.json", label: "\uC758\uC874\uC131 \uAD00\uB9AC" },
|
|
5085
|
+
{ path: ".env.example", label: "\uD658\uACBD\uBCC0\uC218 \uC608\uC2DC" },
|
|
5086
|
+
{ path: ".gitignore", label: ".gitignore" }
|
|
5087
|
+
];
|
|
5088
|
+
for (const { path: path10, label } of structureChecks) {
|
|
5089
|
+
const found = await fs17.pathExists(join9(repoPath, path10));
|
|
5090
|
+
findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
5091
|
+
if (found) score += 25;
|
|
5190
5092
|
}
|
|
5093
|
+
score = Math.min(100, score);
|
|
5094
|
+
return { name: "\uB9AC\uD3EC\uC9C0\uD1A0\uB9AC \uAD6C\uC870", tier: "nice", score, recommendations: [], rawFindings: findings };
|
|
5191
5095
|
}
|
|
5192
|
-
async function
|
|
5193
|
-
const
|
|
5194
|
-
|
|
5195
|
-
const
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
next = content + sep + block + "\n";
|
|
5096
|
+
async function checkDocumentation(repoPath) {
|
|
5097
|
+
const findings = [];
|
|
5098
|
+
let score = 0;
|
|
5099
|
+
const docChecks = [
|
|
5100
|
+
{ path: "README.md", label: "README.md", points: 30 },
|
|
5101
|
+
{ path: "CONTRIBUTING.md", label: "CONTRIBUTING.md", points: 20 },
|
|
5102
|
+
{ path: "docs", label: "docs/ \uB514\uB809\uD1A0\uB9AC", points: 25 },
|
|
5103
|
+
{ path: "docs/openspec.md", label: "OpenSpec PRD", points: 25 }
|
|
5104
|
+
];
|
|
5105
|
+
for (const { path: path10, label, points } of docChecks) {
|
|
5106
|
+
const found = await fs17.pathExists(join9(repoPath, path10));
|
|
5107
|
+
findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
5108
|
+
if (found) score += points;
|
|
5206
5109
|
}
|
|
5207
|
-
|
|
5110
|
+
score = Math.min(100, score);
|
|
5111
|
+
return { name: "\uBB38\uC11C\uD654 \uC218\uC900", tier: "nice", score, recommendations: [], rawFindings: findings };
|
|
5208
5112
|
}
|
|
5209
|
-
function
|
|
5210
|
-
const
|
|
5211
|
-
|
|
5212
|
-
const
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
}
|
|
5219
|
-
|
|
5113
|
+
async function checkHarnessEngineering(repoPath) {
|
|
5114
|
+
const findings = [];
|
|
5115
|
+
let score = 0;
|
|
5116
|
+
const harnessChecks = [
|
|
5117
|
+
{ path: "CLAUDE.md", label: "CLAUDE.md", points: 25 },
|
|
5118
|
+
{ path: "AGENTS.md", label: "AGENTS.md", points: 15 },
|
|
5119
|
+
{ path: ".claude/settings.json", label: ".claude/settings.json", points: 20 },
|
|
5120
|
+
{ path: ".claude/skills", label: ".claude/skills/", points: 20 },
|
|
5121
|
+
{ path: ".claude/commands", label: ".claude/commands/", points: 10 },
|
|
5122
|
+
{ path: ".pai/config.json", label: "PAI config", points: 10 }
|
|
5123
|
+
];
|
|
5124
|
+
for (const { path: path10, label, points } of harnessChecks) {
|
|
5125
|
+
const found = await fs17.pathExists(join9(repoPath, path10));
|
|
5126
|
+
findings.push({ item: label, found, details: found ? "\uC874\uC7AC" : "\uC5C6\uC74C" });
|
|
5127
|
+
if (found) score += points;
|
|
5220
5128
|
}
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5129
|
+
score = Math.min(100, score);
|
|
5130
|
+
return { name: "\uD558\uB124\uC2A4 \uC5D4\uC9C0\uB2C8\uC5B4\uB9C1", tier: "nice", score, recommendations: [], rawFindings: findings };
|
|
5131
|
+
}
|
|
5132
|
+
var init_analyzer2 = __esm({
|
|
5133
|
+
"src/stages/evaluation/analyzer.ts"() {
|
|
5134
|
+
"use strict";
|
|
5227
5135
|
}
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5136
|
+
});
|
|
5137
|
+
|
|
5138
|
+
// src/core/types/stage.ts
|
|
5139
|
+
var STAGE_ORDER;
|
|
5140
|
+
var init_stage = __esm({
|
|
5141
|
+
"src/core/types/stage.ts"() {
|
|
5142
|
+
"use strict";
|
|
5143
|
+
STAGE_ORDER = [
|
|
5144
|
+
"environment",
|
|
5145
|
+
"design",
|
|
5146
|
+
"execution",
|
|
5147
|
+
"validation",
|
|
5148
|
+
"evaluation"
|
|
5149
|
+
];
|
|
5234
5150
|
}
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5151
|
+
});
|
|
5152
|
+
|
|
5153
|
+
// src/core/types/scoring.ts
|
|
5154
|
+
function gradeFromScore(score) {
|
|
5155
|
+
if (score >= 90) return "A";
|
|
5156
|
+
if (score >= 80) return "B";
|
|
5157
|
+
if (score >= 70) return "C";
|
|
5158
|
+
if (score >= 50) return "D";
|
|
5159
|
+
return "F";
|
|
5238
5160
|
}
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5161
|
+
var DEFAULT_CATEGORY_WEIGHTS;
|
|
5162
|
+
var init_scoring = __esm({
|
|
5163
|
+
"src/core/types/scoring.ts"() {
|
|
5164
|
+
"use strict";
|
|
5165
|
+
DEFAULT_CATEGORY_WEIGHTS = [
|
|
5166
|
+
{ name: "\uD14C\uC2A4\uD2B8 \uCEE4\uBC84\uB9AC\uC9C0", tier: "must", weight: 0.2 },
|
|
5167
|
+
{ name: "CI/CD", tier: "must", weight: 0.2 },
|
|
5168
|
+
{ name: "\uD6C5 \uAE30\uBC18 \uAC80\uC99D", tier: "must", weight: 0.2 },
|
|
5169
|
+
{ name: "\uB9AC\uD3EC\uC9C0\uD1A0\uB9AC \uAD6C\uC870", tier: "nice", weight: 0.133 },
|
|
5170
|
+
{ name: "\uBB38\uC11C\uD654 \uC218\uC900", tier: "nice", weight: 0.133 },
|
|
5171
|
+
{ name: "\uD558\uB124\uC2A4 \uC5D4\uC9C0\uB2C8\uC5B4\uB9C1", tier: "nice", weight: 0.134 }
|
|
5172
|
+
];
|
|
5245
5173
|
}
|
|
5246
|
-
}
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5174
|
+
});
|
|
5175
|
+
|
|
5176
|
+
// src/core/types/index.ts
|
|
5177
|
+
var init_types = __esm({
|
|
5178
|
+
"src/core/types/index.ts"() {
|
|
5250
5179
|
"use strict";
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
RobocoLockError = class extends Error {
|
|
5254
|
-
constructor(message, holder) {
|
|
5255
|
-
super(message);
|
|
5256
|
-
this.holder = holder;
|
|
5257
|
-
this.name = "RobocoLockError";
|
|
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";
|
|
5266
|
-
}
|
|
5267
|
-
result;
|
|
5268
|
-
};
|
|
5269
|
-
HANDOFF_START = "<!-- roboco:start -->";
|
|
5270
|
-
HANDOFF_END = "<!-- roboco:end -->";
|
|
5180
|
+
init_stage();
|
|
5181
|
+
init_scoring();
|
|
5271
5182
|
}
|
|
5272
5183
|
});
|
|
5273
5184
|
|
|
5274
|
-
// src/
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5185
|
+
// src/stages/evaluation/scorer.ts
|
|
5186
|
+
var scorer_exports = {};
|
|
5187
|
+
__export(scorer_exports, {
|
|
5188
|
+
computeResult: () => computeResult
|
|
5189
|
+
});
|
|
5190
|
+
function computeResult(llmOutput, customWeights) {
|
|
5191
|
+
const weights = customWeights ?? DEFAULT_CATEGORY_WEIGHTS;
|
|
5192
|
+
const categories = llmOutput.categories.map((cat) => ({
|
|
5193
|
+
name: cat.name,
|
|
5194
|
+
tier: cat.tier,
|
|
5195
|
+
score: Math.round(Math.max(0, Math.min(100, cat.score))),
|
|
5196
|
+
grade: gradeFromScore(Math.max(0, Math.min(100, cat.score))),
|
|
5197
|
+
recommendations: cat.recommendations,
|
|
5198
|
+
rawFindings: cat.rawFindings
|
|
5199
|
+
}));
|
|
5200
|
+
const totalScore = computeWeightedAverage(categories, weights);
|
|
5201
|
+
let totalGrade = gradeFromScore(totalScore);
|
|
5202
|
+
const { penaltyApplied, penaltyReason } = checkPenalty(categories);
|
|
5203
|
+
if (penaltyApplied && gradeRank(totalGrade) > gradeRank("C")) {
|
|
5204
|
+
totalGrade = "C";
|
|
5205
|
+
}
|
|
5281
5206
|
return {
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5207
|
+
categories,
|
|
5208
|
+
totalScore,
|
|
5209
|
+
totalGrade,
|
|
5210
|
+
summary: llmOutput.summary,
|
|
5211
|
+
penaltyApplied,
|
|
5212
|
+
penaltyReason
|
|
5286
5213
|
};
|
|
5287
5214
|
}
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
});
|
|
5215
|
+
function computeWeightedAverage(categories, weights) {
|
|
5216
|
+
let weightedSum = 0;
|
|
5217
|
+
let totalWeight = 0;
|
|
5218
|
+
for (const cat of categories) {
|
|
5219
|
+
const config = weights.find((w) => w.name === cat.name);
|
|
5220
|
+
const weight = config?.weight ?? (cat.tier === "must" ? 0.2 : 0.133);
|
|
5221
|
+
weightedSum += cat.score * weight;
|
|
5222
|
+
totalWeight += weight;
|
|
5297
5223
|
}
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5224
|
+
if (totalWeight === 0) return 0;
|
|
5225
|
+
return Math.round(weightedSum / totalWeight * 100) / 100;
|
|
5226
|
+
}
|
|
5227
|
+
function checkPenalty(categories) {
|
|
5228
|
+
const failedMust = categories.filter(
|
|
5229
|
+
(c2) => c2.tier === "must" && c2.grade === "F"
|
|
5230
|
+
);
|
|
5231
|
+
if (failedMust.length > 0) {
|
|
5232
|
+
const names = failedMust.map((c2) => c2.name).join(", ");
|
|
5233
|
+
return {
|
|
5234
|
+
penaltyApplied: true,
|
|
5235
|
+
penaltyReason: `\uD544\uC218 \uCE74\uD14C\uACE0\uB9AC F \uB4F1\uAE09: ${names} \u2192 \uC804\uCCB4 \uB4F1\uAE09 \uCD5C\uB300 C\uB85C \uC81C\uD55C`
|
|
5236
|
+
};
|
|
5237
|
+
}
|
|
5238
|
+
return { penaltyApplied: false };
|
|
5239
|
+
}
|
|
5240
|
+
function gradeRank(grade) {
|
|
5241
|
+
const ranks = { A: 4, B: 3, C: 2, D: 1, F: 0 };
|
|
5242
|
+
return ranks[grade];
|
|
5243
|
+
}
|
|
5244
|
+
var init_scorer = __esm({
|
|
5245
|
+
"src/stages/evaluation/scorer.ts"() {
|
|
5246
|
+
"use strict";
|
|
5247
|
+
init_types();
|
|
5248
|
+
}
|
|
5249
|
+
});
|
|
5250
|
+
|
|
5251
|
+
// src/stages/evaluation/reporter.ts
|
|
5252
|
+
var reporter_exports = {};
|
|
5253
|
+
__export(reporter_exports, {
|
|
5254
|
+
buildDetailedReport: () => buildDetailedReport,
|
|
5255
|
+
buildMarkdownReport: () => buildMarkdownReport,
|
|
5256
|
+
printReport: () => printReport,
|
|
5257
|
+
printVerboseFindings: () => printVerboseFindings
|
|
5258
|
+
});
|
|
5259
|
+
import chalk5 from "chalk";
|
|
5260
|
+
function printReport(result) {
|
|
5261
|
+
console.log("");
|
|
5262
|
+
console.log(chalk5.hex("#7B93DB")(" PAI Evaluation Report"));
|
|
5263
|
+
console.log(chalk5.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
5264
|
+
console.log("");
|
|
5265
|
+
const gradeColor = GRADE_COLORS[result.totalGrade];
|
|
5266
|
+
console.log(` \uC885\uD569 \uC810\uC218: ${gradeColor(String(result.totalScore))} / 100 \uB4F1\uAE09: ${gradeColor(result.totalGrade)}`);
|
|
5267
|
+
if (result.penaltyApplied) {
|
|
5268
|
+
console.log(chalk5.red(` \u26A0 ${result.penaltyReason}`));
|
|
5269
|
+
}
|
|
5270
|
+
console.log("");
|
|
5271
|
+
console.log(chalk5.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
5272
|
+
for (const cat of result.categories) {
|
|
5273
|
+
const color = GRADE_COLORS[cat.grade];
|
|
5274
|
+
const tierLabel = cat.tier === "must" ? chalk5.hex("#E06C75")("[\uD544\uC218]") : chalk5.gray("[\uC120\uD0DD]");
|
|
5275
|
+
const bar = renderBar(cat.score);
|
|
5276
|
+
console.log(` ${tierLabel} ${cat.name.padEnd(16)} ${bar} ${color(`${cat.score}`).padStart(12)} ${color(cat.grade)}`);
|
|
5305
5277
|
}
|
|
5306
|
-
|
|
5278
|
+
console.log("");
|
|
5279
|
+
console.log(chalk5.gray(` ${result.summary}`));
|
|
5280
|
+
console.log("");
|
|
5307
5281
|
}
|
|
5308
|
-
|
|
5309
|
-
const
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
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
|
-
});
|
|
5282
|
+
function printVerboseFindings(result) {
|
|
5283
|
+
for (const cat of result.categories) {
|
|
5284
|
+
console.log("");
|
|
5285
|
+
console.log(chalk5.bold(` ${cat.name} (${cat.grade}, ${cat.score}\uC810)`));
|
|
5286
|
+
for (const f of cat.rawFindings) {
|
|
5287
|
+
const icon = f.found ? chalk5.green("\u2713") : chalk5.red("\u2717");
|
|
5288
|
+
console.log(` ${icon} ${f.item} \u2014 ${f.details}`);
|
|
5327
5289
|
}
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
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
|
-
});
|
|
5290
|
+
for (const r of cat.recommendations) {
|
|
5291
|
+
const severity = r.severity === "critical" ? chalk5.red("!") : r.severity === "warning" ? chalk5.yellow("!") : chalk5.gray("i");
|
|
5292
|
+
console.log(` ${severity} ${r.message}`);
|
|
5293
|
+
console.log(chalk5.gray(` \u2192 ${r.action}`));
|
|
5339
5294
|
}
|
|
5340
5295
|
}
|
|
5341
|
-
return makeResult("execution.entry", violations);
|
|
5342
5296
|
}
|
|
5343
|
-
|
|
5344
|
-
const
|
|
5345
|
-
const
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5297
|
+
function renderBar(score) {
|
|
5298
|
+
const width = 20;
|
|
5299
|
+
const filled = Math.round(score / 100 * width);
|
|
5300
|
+
const empty = width - filled;
|
|
5301
|
+
const color = score >= 80 ? chalk5.hex("#6BCB77") : score >= 60 ? chalk5.hex("#E2B340") : chalk5.hex("#E06C75");
|
|
5302
|
+
return color("\u2588".repeat(filled)) + chalk5.gray("\u2591".repeat(empty));
|
|
5303
|
+
}
|
|
5304
|
+
function buildMarkdownReport(result) {
|
|
5305
|
+
const lines = [
|
|
5306
|
+
"# PAI Evaluation Report",
|
|
5307
|
+
"",
|
|
5308
|
+
`> \uC0DD\uC131\uC77C\uC2DC: ${(/* @__PURE__ */ new Date()).toLocaleString("ko-KR")}`,
|
|
5309
|
+
"",
|
|
5310
|
+
`## \uC885\uD569 \uC810\uC218: ${result.totalScore}/100 \u2014 \uB4F1\uAE09 ${result.totalGrade}`,
|
|
5311
|
+
""
|
|
5312
|
+
];
|
|
5313
|
+
if (result.penaltyApplied) {
|
|
5314
|
+
lines.push(`> \u26A0 ${result.penaltyReason}`);
|
|
5315
|
+
lines.push("");
|
|
5316
|
+
}
|
|
5317
|
+
lines.push("## \uCE74\uD14C\uACE0\uB9AC\uBCC4 \uC0C1\uC138");
|
|
5318
|
+
lines.push("");
|
|
5319
|
+
lines.push("| \uCE74\uD14C\uACE0\uB9AC | \uBD84\uB958 | \uC810\uC218 | \uB4F1\uAE09 |");
|
|
5320
|
+
lines.push("|---------|------|------|------|");
|
|
5321
|
+
for (const cat of result.categories) {
|
|
5322
|
+
const tier = cat.tier === "must" ? "\uD544\uC218" : "\uC120\uD0DD";
|
|
5323
|
+
lines.push(`| ${cat.name} | ${tier} | ${cat.score} | ${cat.grade} |`);
|
|
5324
|
+
}
|
|
5325
|
+
lines.push("");
|
|
5326
|
+
lines.push("## \uAD8C\uACE0\uC0AC\uD56D");
|
|
5327
|
+
lines.push("");
|
|
5328
|
+
const allRecs = result.categories.flatMap(
|
|
5329
|
+
(c2) => c2.recommendations.map((r) => ({ ...r, category: c2.name }))
|
|
5330
|
+
);
|
|
5331
|
+
if (allRecs.length === 0) {
|
|
5332
|
+
lines.push("\uBAA8\uB4E0 \uD56D\uBAA9\uC774 \uC591\uD638\uD569\uB2C8\uB2E4!");
|
|
5352
5333
|
} else {
|
|
5353
|
-
const
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
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
|
-
});
|
|
5334
|
+
for (const r of allRecs) {
|
|
5335
|
+
const icon = r.severity === "critical" ? "\u{1F534}" : r.severity === "warning" ? "\u{1F7E1}" : "\u2139\uFE0F";
|
|
5336
|
+
lines.push(`- ${icon} **${r.category}**: ${r.message}`);
|
|
5337
|
+
lines.push(` - ${r.action}`);
|
|
5360
5338
|
}
|
|
5361
5339
|
}
|
|
5362
|
-
|
|
5340
|
+
lines.push("");
|
|
5341
|
+
lines.push(`## \uC694\uC57D`);
|
|
5342
|
+
lines.push("");
|
|
5343
|
+
lines.push(result.summary);
|
|
5344
|
+
lines.push("");
|
|
5345
|
+
lines.push("---");
|
|
5346
|
+
lines.push("*Generated by PAI (Plugin-based AI)*");
|
|
5347
|
+
return lines.join("\n") + "\n";
|
|
5363
5348
|
}
|
|
5364
|
-
|
|
5365
|
-
const
|
|
5366
|
-
const
|
|
5367
|
-
const
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
}
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5349
|
+
function buildDetailedReport(result, projectName) {
|
|
5350
|
+
const now2 = (/* @__PURE__ */ new Date()).toLocaleString("ko-KR");
|
|
5351
|
+
const mustCats = result.categories.filter((c2) => c2.tier === "must");
|
|
5352
|
+
const niceCats = result.categories.filter((c2) => c2.tier === "nice");
|
|
5353
|
+
const lines = [
|
|
5354
|
+
`# ${projectName} \uBC14\uC774\uBE0C \uCF54\uB529 \uC900\uBE44\uB3C4 \uBD84\uC11D \uB9AC\uD3EC\uD2B8`,
|
|
5355
|
+
"",
|
|
5356
|
+
`> \uC2A4\uCE94 \uC77C\uC2DC: ${now2}`,
|
|
5357
|
+
`> \uBD84\uC11D \uB300\uC0C1: ${projectName}`,
|
|
5358
|
+
"",
|
|
5359
|
+
"---",
|
|
5360
|
+
"",
|
|
5361
|
+
"## \uC694\uC57D",
|
|
5362
|
+
"",
|
|
5363
|
+
`${projectName}\uC758 \uBC14\uC774\uBE0C \uCF54\uB529 \uC900\uBE44\uB3C4 \uC885\uD569 \uC810\uC218\uB294 **${result.totalScore}/100 (${result.totalGrade}\uB4F1\uAE09)**\uC785\uB2C8\uB2E4.`,
|
|
5364
|
+
""
|
|
5365
|
+
];
|
|
5366
|
+
if (result.penaltyApplied) {
|
|
5367
|
+
lines.push(`> \u26A0 ${result.penaltyReason}`);
|
|
5368
|
+
lines.push("");
|
|
5369
|
+
}
|
|
5370
|
+
if (result.totalGrade === "A") {
|
|
5371
|
+
lines.push("AI \uC9C0\uC6D0 \uCF54\uB529 \uC6CC\uD06C\uD50C\uB85C\uC6B0\uB97C \uD65C\uC6A9\uD560 \uC900\uBE44\uAC00 \uC798 \uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.");
|
|
5372
|
+
} else if (result.totalGrade === "B") {
|
|
5373
|
+
lines.push("\uB300\uBD80\uBD84 \uC900\uBE44\uB418\uC5B4 \uC788\uC73C\uBA70, \uC77C\uBD80 \uAC1C\uC120\uC73C\uB85C \uCD5C\uC801\uC758 \uBC14\uC774\uBE0C \uCF54\uB529 \uD658\uACBD\uC744 \uAD6C\uCD95\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
|
|
5374
|
+
} else if (result.totalGrade === "C") {
|
|
5375
|
+
lines.push("\uAE30\uBCF8\uC801\uC778 \uAD6C\uC870\uB294 \uAC16\uCD94\uACE0 \uC788\uC73C\uB098, \uBC14\uC774\uBE0C \uCF54\uB529\uC758 \uC7A0\uC7AC\uB825\uC744 \uCDA9\uBD84\uD788 \uD65C\uC6A9\uD558\uB824\uBA74 \uAC1C\uC120\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.");
|
|
5376
|
+
} else if (result.totalGrade === "D") {
|
|
5377
|
+
lines.push("AI \uC9C0\uC6D0 \uCF54\uB529 \uC6CC\uD06C\uD50C\uB85C\uC6B0\uB97C \uBCF8\uACA9\uC801\uC73C\uB85C \uD65C\uC6A9\uD558\uAE30\uC5D0\uB294 \uC0C1\uB2F9\uD55C \uAC1C\uC120\uC774 \uD544\uC694\uD55C \uC0C1\uD0DC\uC785\uB2C8\uB2E4.");
|
|
5378
5378
|
} else {
|
|
5379
|
-
|
|
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
|
-
}
|
|
5379
|
+
lines.push("\uBC14\uC774\uBE0C \uCF54\uB529 \uD658\uACBD\uC774 \uAC70\uC758 \uAD6C\uC131\uB418\uC9C0 \uC54A\uC740 \uC0C1\uD0DC\uB85C, \uAE30\uBCF8 \uC124\uC815\uBD80\uD130 \uC2DC\uC791\uD574\uC57C \uD569\uB2C8\uB2E4.");
|
|
5388
5380
|
}
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5381
|
+
lines.push("");
|
|
5382
|
+
lines.push("---");
|
|
5383
|
+
lines.push("");
|
|
5384
|
+
lines.push("## \uD56D\uBAA9\uBCC4 \uD604\uD669");
|
|
5385
|
+
lines.push("");
|
|
5386
|
+
lines.push("| \uD56D\uBAA9 | \uBD84\uB958 | \uC810\uC218 | \uB4F1\uAE09 | \uAC00\uC911\uCE58 |");
|
|
5387
|
+
lines.push("|------|------|------|------|--------|");
|
|
5388
|
+
for (const cat of result.categories) {
|
|
5389
|
+
const tier = cat.tier === "must" ? "**\uD544\uC218**" : "\uC120\uD0DD";
|
|
5390
|
+
lines.push(`| ${cat.name} | ${tier} | ${cat.score}/100 | ${cat.grade} | ${cat.tier === "must" ? "20%" : "13.3%"} |`);
|
|
5391
|
+
}
|
|
5392
|
+
lines.push("");
|
|
5393
|
+
lines.push("---");
|
|
5394
|
+
lines.push("");
|
|
5395
|
+
lines.push("## \uD56D\uBAA9\uBCC4 \uC0C1\uC138 \uBD84\uC11D");
|
|
5396
|
+
lines.push("");
|
|
5397
|
+
for (const cat of result.categories) {
|
|
5398
|
+
const tierLabel = cat.tier === "must" ? "\uD544\uC218" : "\uC120\uD0DD";
|
|
5399
|
+
lines.push(`### ${cat.name} (${tierLabel}, \uAC00\uC911\uCE58 ${cat.tier === "must" ? "20%" : "13.3%"}) \u2014 ${cat.grade}\uB4F1\uAE09`);
|
|
5400
|
+
lines.push("");
|
|
5401
|
+
if (cat.rawFindings.length > 0) {
|
|
5402
|
+
lines.push("**\uC810\uAC80 \uACB0\uACFC:**");
|
|
5403
|
+
lines.push("");
|
|
5404
|
+
for (const f of cat.rawFindings) {
|
|
5405
|
+
const icon = f.found ? "\u2705" : "\u274C";
|
|
5406
|
+
lines.push(`- ${icon} ${f.item} \u2014 ${f.details}`);
|
|
5407
|
+
}
|
|
5408
|
+
lines.push("");
|
|
5400
5409
|
}
|
|
5401
|
-
if (
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5410
|
+
if (cat.grade === "A" || cat.grade === "B") {
|
|
5411
|
+
lines.push(`**\uD3C9\uAC00:** \uC591\uD638\uD55C \uC0C1\uD0DC\uC785\uB2C8\uB2E4.`);
|
|
5412
|
+
} else if (cat.grade === "C") {
|
|
5413
|
+
lines.push(`**\uD3C9\uAC00:** \uAE30\uBCF8\uC740 \uAC16\uCD94\uACE0 \uC788\uC73C\uB098 \uCD94\uAC00 \uAC1C\uC120\uC774 \uAD8C\uC7A5\uB429\uB2C8\uB2E4.`);
|
|
5414
|
+
} else if (cat.grade === "D") {
|
|
5415
|
+
lines.push(`**\uD3C9\uAC00:** \uAC1C\uC120\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.`);
|
|
5416
|
+
} else {
|
|
5417
|
+
lines.push(`**\uD3C9\uAC00:** \uC2DC\uAE09\uD55C \uAC1C\uC120\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.`);
|
|
5405
5418
|
}
|
|
5406
|
-
|
|
5407
|
-
if (
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
}
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
let inDomainSection = false;
|
|
5417
|
-
for (const rawLine of lines) {
|
|
5418
|
-
const line = rawLine.trim();
|
|
5419
|
-
if (/^##\s*도메인 객체|^##\s*Domain Objects/i.test(line)) {
|
|
5420
|
-
inDomainSection = true;
|
|
5421
|
-
continue;
|
|
5419
|
+
lines.push("");
|
|
5420
|
+
if (cat.recommendations.length > 0) {
|
|
5421
|
+
lines.push("**\uAD8C\uACE0\uC0AC\uD56D:**");
|
|
5422
|
+
lines.push("");
|
|
5423
|
+
for (const r of cat.recommendations) {
|
|
5424
|
+
const icon = r.severity === "critical" ? "\u{1F534}" : r.severity === "warning" ? "\u{1F7E1}" : "\u2139\uFE0F";
|
|
5425
|
+
lines.push(`- ${icon} ${r.message}`);
|
|
5426
|
+
lines.push(` - \u2192 ${r.action}`);
|
|
5427
|
+
}
|
|
5428
|
+
lines.push("");
|
|
5422
5429
|
}
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5430
|
+
lines.push("---");
|
|
5431
|
+
lines.push("");
|
|
5432
|
+
}
|
|
5433
|
+
lines.push("## \uAC1C\uC120 \uB85C\uB4DC\uB9F5");
|
|
5434
|
+
lines.push("");
|
|
5435
|
+
const critical = result.categories.filter((c2) => c2.grade === "F");
|
|
5436
|
+
const warning = result.categories.filter((c2) => c2.grade === "D");
|
|
5437
|
+
const ok = result.categories.filter((c2) => c2.grade === "C" || c2.grade === "B" || c2.grade === "A");
|
|
5438
|
+
if (critical.length > 0) {
|
|
5439
|
+
lines.push("### \uC989\uC2DC \uAC1C\uC120 \uD544\uC694 (F\uB4F1\uAE09)");
|
|
5440
|
+
lines.push("");
|
|
5441
|
+
lines.push("| \uD56D\uBAA9 | \uD604\uC7AC \uC810\uC218 | \uBAA9\uD45C | \uC870\uCE58 |");
|
|
5442
|
+
lines.push("|------|----------|------|------|");
|
|
5443
|
+
for (const c2 of critical) {
|
|
5444
|
+
const action = c2.recommendations[0]?.action ?? "\uC124\uC815 \uCD94\uAC00 \uD544\uC694";
|
|
5445
|
+
lines.push(`| ${c2.name} | ${c2.score}\uC810 (F) | 50\uC810+ (D) | ${action} |`);
|
|
5427
5446
|
}
|
|
5428
|
-
|
|
5447
|
+
lines.push("");
|
|
5429
5448
|
}
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
entries = await fs17.readdir(dir);
|
|
5439
|
-
} catch {
|
|
5440
|
-
return;
|
|
5449
|
+
if (warning.length > 0) {
|
|
5450
|
+
lines.push("### \uB2E8\uAE30 \uAC1C\uC120 \uAD8C\uC7A5 (D\uB4F1\uAE09)");
|
|
5451
|
+
lines.push("");
|
|
5452
|
+
lines.push("| \uD56D\uBAA9 | \uD604\uC7AC \uC810\uC218 | \uBAA9\uD45C | \uC870\uCE58 |");
|
|
5453
|
+
lines.push("|------|----------|------|------|");
|
|
5454
|
+
for (const c2 of warning) {
|
|
5455
|
+
const action = c2.recommendations[0]?.action ?? "\uCD94\uAC00 \uC124\uC815 \uAD8C\uC7A5";
|
|
5456
|
+
lines.push(`| ${c2.name} | ${c2.score}\uC810 (D) | 70\uC810+ (C) | ${action} |`);
|
|
5441
5457
|
}
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
} else if (exts.has(path9.extname(name))) {
|
|
5450
|
-
count++;
|
|
5451
|
-
}
|
|
5458
|
+
lines.push("");
|
|
5459
|
+
}
|
|
5460
|
+
if (ok.length > 0) {
|
|
5461
|
+
lines.push("### \uC591\uD638 (C\uB4F1\uAE09 \uC774\uC0C1)");
|
|
5462
|
+
lines.push("");
|
|
5463
|
+
for (const c2 of ok) {
|
|
5464
|
+
lines.push(`- \u2705 ${c2.name}: ${c2.score}\uC810 (${c2.grade})`);
|
|
5452
5465
|
}
|
|
5466
|
+
lines.push("");
|
|
5453
5467
|
}
|
|
5454
|
-
|
|
5455
|
-
|
|
5468
|
+
lines.push("---");
|
|
5469
|
+
lines.push("");
|
|
5470
|
+
lines.push("## \uACB0\uB860");
|
|
5471
|
+
lines.push("");
|
|
5472
|
+
lines.push(result.summary);
|
|
5473
|
+
lines.push("");
|
|
5474
|
+
if (result.totalGrade === "F" || result.totalGrade === "D") {
|
|
5475
|
+
const immediateActions = critical.length + warning.length;
|
|
5476
|
+
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.`);
|
|
5477
|
+
} else if (result.totalGrade === "C") {
|
|
5478
|
+
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.");
|
|
5479
|
+
} else {
|
|
5480
|
+
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.");
|
|
5481
|
+
}
|
|
5482
|
+
lines.push("");
|
|
5483
|
+
lines.push("---");
|
|
5484
|
+
lines.push("");
|
|
5485
|
+
lines.push("*PAI Zero (Plugin AI for ProjectZero) + Claude Code\uB85C \uC0DD\uC131\uB428*");
|
|
5486
|
+
return lines.join("\n") + "\n";
|
|
5456
5487
|
}
|
|
5457
|
-
var
|
|
5458
|
-
var
|
|
5459
|
-
"src/
|
|
5488
|
+
var GRADE_COLORS;
|
|
5489
|
+
var init_reporter = __esm({
|
|
5490
|
+
"src/stages/evaluation/reporter.ts"() {
|
|
5460
5491
|
"use strict";
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
execution: executionEntryGate,
|
|
5468
|
-
validation: validationEntryGate,
|
|
5469
|
-
evaluation: evaluationEntryGate
|
|
5492
|
+
GRADE_COLORS = {
|
|
5493
|
+
A: chalk5.hex("#6BCB77"),
|
|
5494
|
+
B: chalk5.hex("#7B93DB"),
|
|
5495
|
+
C: chalk5.hex("#E2B340"),
|
|
5496
|
+
D: chalk5.hex("#E06C75"),
|
|
5497
|
+
F: chalk5.hex("#CC4444")
|
|
5470
5498
|
};
|
|
5471
5499
|
}
|
|
5472
5500
|
});
|
|
5473
5501
|
|
|
5474
|
-
// src/
|
|
5502
|
+
// src/utils/shell-cd.ts
|
|
5503
|
+
var shell_cd_exports = {};
|
|
5504
|
+
__export(shell_cd_exports, {
|
|
5505
|
+
installShellHelper: () => installShellHelper,
|
|
5506
|
+
requestCdAfter: () => requestCdAfter
|
|
5507
|
+
});
|
|
5475
5508
|
import { join as join10 } from "path";
|
|
5509
|
+
import { homedir as homedir2 } from "os";
|
|
5476
5510
|
import fs18 from "fs-extra";
|
|
5477
|
-
async function
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5511
|
+
async function requestCdAfter(targetDir) {
|
|
5512
|
+
await fs18.ensureDir(PAI_DIR);
|
|
5513
|
+
await fs18.writeFile(CD_FILE, targetDir);
|
|
5514
|
+
}
|
|
5515
|
+
async function installShellHelper() {
|
|
5516
|
+
await fs18.ensureDir(PAI_DIR);
|
|
5517
|
+
if (isWindows) {
|
|
5518
|
+
return installPowerShellHelper();
|
|
5519
|
+
}
|
|
5520
|
+
return installBashHelper();
|
|
5521
|
+
}
|
|
5522
|
+
async function installBashHelper() {
|
|
5523
|
+
await fs18.writeFile(HELPER_FILE_SH, BASH_HELPER);
|
|
5524
|
+
const rcFile = getShellRcPath();
|
|
5525
|
+
const sourceLine = 'source "$HOME/.pai/shell-helper.sh"';
|
|
5526
|
+
if (await fs18.pathExists(rcFile)) {
|
|
5527
|
+
const content = await fs18.readFile(rcFile, "utf8");
|
|
5528
|
+
if (content.includes("shell-helper.sh")) {
|
|
5529
|
+
return true;
|
|
5488
5530
|
}
|
|
5531
|
+
await fs18.appendFile(rcFile, `
|
|
5532
|
+
# PAI \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9
|
|
5533
|
+
${sourceLine}
|
|
5534
|
+
`);
|
|
5535
|
+
return false;
|
|
5489
5536
|
}
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5537
|
+
await fs18.writeFile(rcFile, `${sourceLine}
|
|
5538
|
+
`);
|
|
5539
|
+
return false;
|
|
5540
|
+
}
|
|
5541
|
+
async function installPowerShellHelper() {
|
|
5542
|
+
await fs18.writeFile(HELPER_FILE_PS1, POWERSHELL_HELPER);
|
|
5543
|
+
const rcFile = getShellRcPath();
|
|
5544
|
+
const sourceLine = '. "$env:USERPROFILE\\.pai\\shell-helper.ps1"';
|
|
5545
|
+
await fs18.ensureDir(join10(rcFile, ".."));
|
|
5546
|
+
if (await fs18.pathExists(rcFile)) {
|
|
5547
|
+
const content = await fs18.readFile(rcFile, "utf8");
|
|
5548
|
+
if (content.includes("shell-helper.ps1")) {
|
|
5549
|
+
return true;
|
|
5550
|
+
}
|
|
5551
|
+
await fs18.appendFile(rcFile, `
|
|
5552
|
+
# PAI \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9
|
|
5553
|
+
${sourceLine}
|
|
5554
|
+
`);
|
|
5555
|
+
return false;
|
|
5556
|
+
}
|
|
5557
|
+
await fs18.writeFile(rcFile, `${sourceLine}
|
|
5558
|
+
`);
|
|
5559
|
+
return false;
|
|
5560
|
+
}
|
|
5561
|
+
var PAI_DIR, CD_FILE, HELPER_FILE_SH, HELPER_FILE_PS1, BASH_HELPER, POWERSHELL_HELPER;
|
|
5562
|
+
var init_shell_cd = __esm({
|
|
5563
|
+
"src/utils/shell-cd.ts"() {
|
|
5564
|
+
"use strict";
|
|
5565
|
+
init_platform();
|
|
5566
|
+
PAI_DIR = join10(homedir2(), ".pai");
|
|
5567
|
+
CD_FILE = join10(PAI_DIR, ".cd-after");
|
|
5568
|
+
HELPER_FILE_SH = join10(PAI_DIR, "shell-helper.sh");
|
|
5569
|
+
HELPER_FILE_PS1 = join10(PAI_DIR, "shell-helper.ps1");
|
|
5570
|
+
BASH_HELPER = `# PAI shell helper \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9 \uC9C0\uC6D0
|
|
5571
|
+
pai() {
|
|
5572
|
+
local cd_target="$HOME/.pai/.cd-after"
|
|
5573
|
+
rm -f "$cd_target"
|
|
5574
|
+
PAI_CD_AFTER="$cd_target" command npx pai-zero "$@"
|
|
5575
|
+
local exit_code=$?
|
|
5576
|
+
if [ -f "$cd_target" ]; then
|
|
5577
|
+
local dir
|
|
5578
|
+
dir="$(cat "$cd_target")"
|
|
5579
|
+
rm -f "$cd_target"
|
|
5580
|
+
if [ -n "$dir" ] && [ -d "$dir" ]; then
|
|
5581
|
+
cd "$dir" || true
|
|
5582
|
+
echo " \u2192 cd $dir"
|
|
5583
|
+
fi
|
|
5584
|
+
fi
|
|
5585
|
+
return $exit_code
|
|
5586
|
+
}
|
|
5587
|
+
`;
|
|
5588
|
+
POWERSHELL_HELPER = `# PAI shell helper \u2014 \uC790\uB3D9 \uB514\uB809\uD1A0\uB9AC \uC774\uB3D9 \uC9C0\uC6D0
|
|
5589
|
+
function pai {
|
|
5590
|
+
$cdTarget = Join-Path $env:USERPROFILE '.pai\\.cd-after'
|
|
5591
|
+
Remove-Item $cdTarget -ErrorAction SilentlyContinue
|
|
5592
|
+
$env:PAI_CD_AFTER = $cdTarget
|
|
5593
|
+
& npx pai-zero @args
|
|
5594
|
+
$exitCode = $LASTEXITCODE
|
|
5595
|
+
if (Test-Path $cdTarget) {
|
|
5596
|
+
$dir = Get-Content $cdTarget -Raw
|
|
5597
|
+
Remove-Item $cdTarget -ErrorAction SilentlyContinue
|
|
5598
|
+
if ($dir -and (Test-Path $dir)) {
|
|
5599
|
+
Set-Location $dir
|
|
5600
|
+
Write-Host " -> cd $dir"
|
|
5503
5601
|
}
|
|
5504
5602
|
}
|
|
5603
|
+
return $exitCode
|
|
5604
|
+
}
|
|
5605
|
+
`;
|
|
5606
|
+
}
|
|
5607
|
+
});
|
|
5608
|
+
|
|
5609
|
+
// src/utils/claude-settings.ts
|
|
5610
|
+
var claude_settings_exports = {};
|
|
5611
|
+
__export(claude_settings_exports, {
|
|
5612
|
+
ClaudeSettingsError: () => ClaudeSettingsError,
|
|
5613
|
+
buildSkeleton: () => buildSkeleton,
|
|
5614
|
+
enableOmcPlugin: () => enableOmcPlugin,
|
|
5615
|
+
getClaudeSettingsPath: () => getClaudeSettingsPath,
|
|
5616
|
+
isAlreadyEnabled: () => isAlreadyEnabled,
|
|
5617
|
+
mergeOmcIntoSettings: () => mergeOmcIntoSettings
|
|
5618
|
+
});
|
|
5619
|
+
import os3 from "os";
|
|
5620
|
+
import path9 from "path";
|
|
5621
|
+
import fs19 from "fs-extra";
|
|
5622
|
+
function getClaudeSettingsPath(homeDir = os3.homedir()) {
|
|
5623
|
+
return path9.join(homeDir, ".claude", "settings.json");
|
|
5624
|
+
}
|
|
5625
|
+
function parseJsonWithBom(raw) {
|
|
5626
|
+
const stripped = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
|
|
5627
|
+
return JSON.parse(stripped);
|
|
5628
|
+
}
|
|
5629
|
+
function timestampSuffix() {
|
|
5630
|
+
return sanitizeFilenameForWindows((/* @__PURE__ */ new Date()).toISOString());
|
|
5631
|
+
}
|
|
5632
|
+
async function enableOmcPlugin(options = {}) {
|
|
5633
|
+
const marketplaceId = options.marketplaceId ?? DEFAULT_MARKETPLACE_ID;
|
|
5634
|
+
const marketplaceUrl = options.marketplaceUrl ?? DEFAULT_MARKETPLACE_URL;
|
|
5635
|
+
const pluginId = options.pluginId ?? DEFAULT_PLUGIN_ID;
|
|
5636
|
+
const wantBackup = options.backup ?? true;
|
|
5637
|
+
const settingsPath = getClaudeSettingsPath();
|
|
5638
|
+
await fs19.ensureDir(path9.dirname(settingsPath));
|
|
5639
|
+
if (!await fs19.pathExists(settingsPath)) {
|
|
5640
|
+
const skeleton = buildSkeleton(marketplaceId, marketplaceUrl, pluginId);
|
|
5641
|
+
await fs19.writeFile(settingsPath, JSON.stringify(skeleton, null, 2) + "\n", "utf8");
|
|
5642
|
+
return { action: "created", settingsPath };
|
|
5643
|
+
}
|
|
5644
|
+
const raw = await fs19.readFile(settingsPath, "utf8");
|
|
5645
|
+
let parsed;
|
|
5505
5646
|
try {
|
|
5506
|
-
const
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
});
|
|
5512
|
-
return {
|
|
5513
|
-
runner,
|
|
5514
|
-
passed: true,
|
|
5515
|
-
output: stdout || stderr,
|
|
5516
|
-
duration: Date.now() - start
|
|
5517
|
-
};
|
|
5647
|
+
const value = parseJsonWithBom(raw);
|
|
5648
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5649
|
+
throw new Error("settings.json is not a JSON object");
|
|
5650
|
+
}
|
|
5651
|
+
parsed = value;
|
|
5518
5652
|
} catch (err) {
|
|
5519
|
-
const
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5653
|
+
const backupPath2 = `${settingsPath}.backup-${timestampSuffix()}`;
|
|
5654
|
+
await fs19.copy(settingsPath, backupPath2);
|
|
5655
|
+
throw new ClaudeSettingsError(
|
|
5656
|
+
`settings.json \uD30C\uC2F1 \uC2E4\uD328: ${err.message}. \uBC31\uC5C5: ${backupPath2}`,
|
|
5657
|
+
backupPath2
|
|
5658
|
+
);
|
|
5659
|
+
}
|
|
5660
|
+
if (isAlreadyEnabled(parsed, marketplaceId, pluginId)) {
|
|
5661
|
+
return { action: "already-enabled", settingsPath };
|
|
5662
|
+
}
|
|
5663
|
+
let backupPath;
|
|
5664
|
+
if (wantBackup) {
|
|
5665
|
+
backupPath = `${settingsPath}.backup-${timestampSuffix()}`;
|
|
5666
|
+
await fs19.copy(settingsPath, backupPath);
|
|
5526
5667
|
}
|
|
5668
|
+
const merged = mergeOmcIntoSettings(parsed, marketplaceId, marketplaceUrl, pluginId);
|
|
5669
|
+
await fs19.writeFile(settingsPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
5670
|
+
return { action: "added", settingsPath, backupPath };
|
|
5671
|
+
}
|
|
5672
|
+
function buildSkeleton(marketplaceId, marketplaceUrl, pluginId) {
|
|
5673
|
+
return {
|
|
5674
|
+
extraKnownMarketplaces: {
|
|
5675
|
+
[marketplaceId]: {
|
|
5676
|
+
source: { source: "git", url: marketplaceUrl }
|
|
5677
|
+
}
|
|
5678
|
+
},
|
|
5679
|
+
enabledPlugins: {
|
|
5680
|
+
[pluginId]: true
|
|
5681
|
+
}
|
|
5682
|
+
};
|
|
5527
5683
|
}
|
|
5528
|
-
|
|
5529
|
-
"
|
|
5684
|
+
function isAlreadyEnabled(settings, marketplaceId, pluginId) {
|
|
5685
|
+
const markets = settings["extraKnownMarketplaces"];
|
|
5686
|
+
const enabled = settings["enabledPlugins"];
|
|
5687
|
+
const hasMarket = typeof markets === "object" && markets !== null && marketplaceId in markets;
|
|
5688
|
+
const hasPlugin = typeof enabled === "object" && enabled !== null && enabled[pluginId] === true;
|
|
5689
|
+
return hasMarket && hasPlugin;
|
|
5690
|
+
}
|
|
5691
|
+
function mergeOmcIntoSettings(settings, marketplaceId, marketplaceUrl, pluginId) {
|
|
5692
|
+
const next = { ...settings };
|
|
5693
|
+
const existingMarkets = typeof next["extraKnownMarketplaces"] === "object" && next["extraKnownMarketplaces"] !== null ? { ...next["extraKnownMarketplaces"] } : {};
|
|
5694
|
+
existingMarkets[marketplaceId] = {
|
|
5695
|
+
source: { source: "git", url: marketplaceUrl }
|
|
5696
|
+
};
|
|
5697
|
+
next["extraKnownMarketplaces"] = existingMarkets;
|
|
5698
|
+
const existingPlugins = typeof next["enabledPlugins"] === "object" && next["enabledPlugins"] !== null ? { ...next["enabledPlugins"] } : {};
|
|
5699
|
+
existingPlugins[pluginId] = true;
|
|
5700
|
+
next["enabledPlugins"] = existingPlugins;
|
|
5701
|
+
return next;
|
|
5702
|
+
}
|
|
5703
|
+
var DEFAULT_MARKETPLACE_ID, DEFAULT_MARKETPLACE_URL, DEFAULT_PLUGIN_ID, ClaudeSettingsError;
|
|
5704
|
+
var init_claude_settings = __esm({
|
|
5705
|
+
"src/utils/claude-settings.ts"() {
|
|
5530
5706
|
"use strict";
|
|
5707
|
+
init_platform();
|
|
5708
|
+
DEFAULT_MARKETPLACE_ID = "omc";
|
|
5709
|
+
DEFAULT_MARKETPLACE_URL = "https://github.com/SoInKyu/oh-my-claudecode.git";
|
|
5710
|
+
DEFAULT_PLUGIN_ID = "oh-my-claudecode@omc";
|
|
5711
|
+
ClaudeSettingsError = class extends Error {
|
|
5712
|
+
constructor(message, backupPath) {
|
|
5713
|
+
super(message);
|
|
5714
|
+
this.backupPath = backupPath;
|
|
5715
|
+
this.name = "ClaudeSettingsError";
|
|
5716
|
+
}
|
|
5717
|
+
backupPath;
|
|
5718
|
+
};
|
|
5531
5719
|
}
|
|
5532
5720
|
});
|
|
5533
5721
|
|
|
5534
|
-
// src/stages/
|
|
5722
|
+
// src/stages/evaluation/cache.ts
|
|
5723
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
5535
5724
|
import { join as join11 } from "path";
|
|
5536
|
-
import
|
|
5537
|
-
|
|
5538
|
-
const
|
|
5539
|
-
|
|
5540
|
-
|
|
5725
|
+
import { createHash } from "crypto";
|
|
5726
|
+
function computeRepoHash(repoPath) {
|
|
5727
|
+
const hash = createHash("sha256");
|
|
5728
|
+
for (const file of FILES_TO_HASH) {
|
|
5729
|
+
const fullPath = join11(repoPath, file);
|
|
5730
|
+
try {
|
|
5731
|
+
const content = readFileSync(fullPath);
|
|
5732
|
+
hash.update(`${file}:${content.length}`);
|
|
5733
|
+
} catch {
|
|
5734
|
+
hash.update(`${file}:missing`);
|
|
5735
|
+
}
|
|
5541
5736
|
}
|
|
5542
|
-
|
|
5737
|
+
return hash.digest("hex").slice(0, 16);
|
|
5738
|
+
}
|
|
5739
|
+
function getCachePath(repoPath) {
|
|
5740
|
+
return join11(repoPath, CACHE_DIR, CACHE_FILE);
|
|
5741
|
+
}
|
|
5742
|
+
function loadCache(repoPath) {
|
|
5543
5743
|
try {
|
|
5544
|
-
|
|
5744
|
+
const data = readFileSync(getCachePath(repoPath), "utf-8");
|
|
5745
|
+
return JSON.parse(data);
|
|
5545
5746
|
} catch {
|
|
5546
|
-
return {
|
|
5547
|
-
}
|
|
5548
|
-
const specFile = config.specFile ?? "docs/openspec.md";
|
|
5549
|
-
const rules = config.rules ?? [];
|
|
5550
|
-
const checks = [];
|
|
5551
|
-
if (rules.includes("spec-implementation-match")) {
|
|
5552
|
-
const specExists = await fs19.pathExists(join11(cwd, specFile));
|
|
5553
|
-
const srcExists = await fs19.pathExists(join11(cwd, "src"));
|
|
5554
|
-
checks.push({
|
|
5555
|
-
rule: "spec-implementation-match",
|
|
5556
|
-
passed: specExists && srcExists,
|
|
5557
|
-
detail: specExists && srcExists ? "\uC124\uACC4 \uBB38\uC11C\uC640 \uC18C\uC2A4 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" : `${!specExists ? specFile + " \uC5C6\uC74C" : ""} ${!srcExists ? "src/ \uC5C6\uC74C" : ""}`.trim()
|
|
5558
|
-
});
|
|
5559
|
-
}
|
|
5560
|
-
if (rules.includes("api-contract-test")) {
|
|
5561
|
-
const testDir = await fs19.pathExists(join11(cwd, "tests"));
|
|
5562
|
-
const testDir2 = await fs19.pathExists(join11(cwd, "test"));
|
|
5563
|
-
checks.push({
|
|
5564
|
-
rule: "api-contract-test",
|
|
5565
|
-
passed: testDir || testDir2,
|
|
5566
|
-
detail: testDir || testDir2 ? "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC" : "\uD14C\uC2A4\uD2B8 \uB514\uB809\uD1A0\uB9AC(tests/ \uB610\uB294 test/) \uC5C6\uC74C"
|
|
5567
|
-
});
|
|
5747
|
+
return { version: 1, entries: {} };
|
|
5568
5748
|
}
|
|
5569
|
-
return { enabled: true, specFile, rules, checks };
|
|
5570
5749
|
}
|
|
5571
|
-
|
|
5572
|
-
"
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
});
|
|
5576
|
-
|
|
5577
|
-
// src/cli/commands/validate.cmd.ts
|
|
5578
|
-
var validate_cmd_exports = {};
|
|
5579
|
-
__export(validate_cmd_exports, {
|
|
5580
|
-
handleRobocoCliError: () => handleRobocoError,
|
|
5581
|
-
validateCommand: () => validateCommand
|
|
5582
|
-
});
|
|
5583
|
-
async function validateCommand(cwd, options = {}) {
|
|
5584
|
-
try {
|
|
5585
|
-
await withRoboco(
|
|
5586
|
-
cwd,
|
|
5587
|
-
"validation",
|
|
5588
|
-
"pai test",
|
|
5589
|
-
{ skipGates: options.skipGates, force: options.force, gate: STAGE_GATES.validation },
|
|
5590
|
-
async () => {
|
|
5591
|
-
section("\uD14C\uC2A4\uD2B8 \uC2E4\uD589");
|
|
5592
|
-
const testResult = await runTests(cwd);
|
|
5593
|
-
if (testResult.passed) {
|
|
5594
|
-
success(`\uD14C\uC2A4\uD2B8 \uD1B5\uACFC (${testResult.runner}, ${testResult.duration}ms)`);
|
|
5595
|
-
} else {
|
|
5596
|
-
error("\uD14C\uC2A4\uD2B8 \uC2E4\uD328");
|
|
5597
|
-
info(testResult.output.slice(0, 300));
|
|
5598
|
-
}
|
|
5599
|
-
section("\uD558\uB124\uC2A4 \uAC80\uC99D");
|
|
5600
|
-
const harness = await runHarnessCheck(cwd);
|
|
5601
|
-
if (!harness.enabled) {
|
|
5602
|
-
info("Harness \uC124\uC815 \uC5C6\uC74C \u2014 \uAC74\uB108\uB700");
|
|
5603
|
-
info("\uC124\uC815 \uCD94\uAC00: `pai add` \uC5D0\uC11C Harness Engineering \uC120\uD0DD");
|
|
5604
|
-
} else {
|
|
5605
|
-
for (const check of harness.checks) {
|
|
5606
|
-
if (check.passed) {
|
|
5607
|
-
success(`${check.rule}: ${check.detail}`);
|
|
5608
|
-
} else {
|
|
5609
|
-
warn(`${check.rule}: ${check.detail}`);
|
|
5610
|
-
}
|
|
5611
|
-
}
|
|
5612
|
-
}
|
|
5613
|
-
const allPassed = testResult.passed && harness.checks.every((c2) => c2.passed);
|
|
5614
|
-
console.log("");
|
|
5615
|
-
if (allPassed) {
|
|
5616
|
-
success("\uAC80\uC99D \uD1B5\uACFC!");
|
|
5617
|
-
} else if (!testResult.passed) {
|
|
5618
|
-
error("\uAC80\uC99D \uC2E4\uD328 \u2014 \uD14C\uC2A4\uD2B8\uB97C \uC218\uC815\uD558\uC138\uC694.");
|
|
5619
|
-
throw new Error("tests-failed");
|
|
5620
|
-
} else {
|
|
5621
|
-
warn("\uBD80\uBD84 \uD1B5\uACFC \u2014 \uD558\uB124\uC2A4 \uAC80\uC99D \uD56D\uBAA9\uC744 \uD655\uC778\uD558\uC138\uC694.");
|
|
5622
|
-
}
|
|
5623
|
-
}
|
|
5624
|
-
);
|
|
5625
|
-
await syncHandoff(cwd).catch(() => {
|
|
5626
|
-
});
|
|
5627
|
-
} catch (err) {
|
|
5628
|
-
handleRobocoError(err);
|
|
5750
|
+
function saveCache(repoPath, store) {
|
|
5751
|
+
const cacheDir = join11(repoPath, CACHE_DIR, "cache");
|
|
5752
|
+
if (!existsSync(cacheDir)) {
|
|
5753
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
5629
5754
|
}
|
|
5755
|
+
writeFileSync(getCachePath(repoPath), JSON.stringify(store, null, 2));
|
|
5630
5756
|
}
|
|
5631
|
-
function
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
if (err instanceof Error) {
|
|
5647
|
-
if (err.message === "tests-failed") {
|
|
5648
|
-
process.exitCode = 1;
|
|
5649
|
-
return;
|
|
5757
|
+
function getCachedResult(repoPath) {
|
|
5758
|
+
const store = loadCache(repoPath);
|
|
5759
|
+
const repoHash = computeRepoHash(repoPath);
|
|
5760
|
+
const entry = store.entries[repoHash];
|
|
5761
|
+
if (!entry) return null;
|
|
5762
|
+
if (Date.now() - entry.timestamp > CACHE_TTL_MS) return null;
|
|
5763
|
+
return entry.llmOutput;
|
|
5764
|
+
}
|
|
5765
|
+
function setCachedResult(repoPath, llmOutput) {
|
|
5766
|
+
const store = loadCache(repoPath);
|
|
5767
|
+
const repoHash = computeRepoHash(repoPath);
|
|
5768
|
+
const now2 = Date.now();
|
|
5769
|
+
for (const [key, entry] of Object.entries(store.entries)) {
|
|
5770
|
+
if (now2 - entry.timestamp > CACHE_TTL_MS) {
|
|
5771
|
+
delete store.entries[key];
|
|
5650
5772
|
}
|
|
5651
|
-
error(err.message);
|
|
5652
|
-
process.exitCode = 1;
|
|
5653
|
-
return;
|
|
5654
5773
|
}
|
|
5655
|
-
|
|
5774
|
+
store.entries[repoHash] = { repoPath, repoHash, timestamp: now2, llmOutput };
|
|
5775
|
+
saveCache(repoPath, store);
|
|
5656
5776
|
}
|
|
5657
|
-
var
|
|
5658
|
-
|
|
5777
|
+
var CACHE_DIR, CACHE_FILE, CACHE_TTL_MS, FILES_TO_HASH;
|
|
5778
|
+
var init_cache = __esm({
|
|
5779
|
+
"src/stages/evaluation/cache.ts"() {
|
|
5659
5780
|
"use strict";
|
|
5660
|
-
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5781
|
+
CACHE_DIR = ".pai";
|
|
5782
|
+
CACHE_FILE = "cache/evaluation.json";
|
|
5783
|
+
CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
5784
|
+
FILES_TO_HASH = [
|
|
5785
|
+
"package.json",
|
|
5786
|
+
"pyproject.toml",
|
|
5787
|
+
"go.mod",
|
|
5788
|
+
"Cargo.toml",
|
|
5789
|
+
"tsconfig.json",
|
|
5790
|
+
"jest.config.ts",
|
|
5791
|
+
"vitest.config.ts",
|
|
5792
|
+
".github/workflows",
|
|
5793
|
+
".gitlab-ci.yml",
|
|
5794
|
+
".husky",
|
|
5795
|
+
".lintstagedrc",
|
|
5796
|
+
"CLAUDE.md",
|
|
5797
|
+
"AGENTS.md",
|
|
5798
|
+
".cursorrules",
|
|
5799
|
+
"README.md",
|
|
5800
|
+
"CONTRIBUTING.md"
|
|
5801
|
+
];
|
|
5665
5802
|
}
|
|
5666
5803
|
});
|
|
5667
5804
|
|
|
@@ -6097,7 +6234,24 @@ async function setupInDirectory(projectDir, projectName) {
|
|
|
6097
6234
|
config,
|
|
6098
6235
|
previousResults: /* @__PURE__ */ new Map()
|
|
6099
6236
|
};
|
|
6100
|
-
|
|
6237
|
+
let stageResult;
|
|
6238
|
+
try {
|
|
6239
|
+
await withRoboco(
|
|
6240
|
+
projectDir,
|
|
6241
|
+
"environment",
|
|
6242
|
+
"pai init",
|
|
6243
|
+
{},
|
|
6244
|
+
async () => {
|
|
6245
|
+
stageResult = await environmentStage.run(input);
|
|
6246
|
+
}
|
|
6247
|
+
);
|
|
6248
|
+
await syncHandoff(projectDir).catch(() => {
|
|
6249
|
+
});
|
|
6250
|
+
} catch (err) {
|
|
6251
|
+
handleRobocoError(err);
|
|
6252
|
+
return;
|
|
6253
|
+
}
|
|
6254
|
+
const result = stageResult;
|
|
6101
6255
|
if (result.status === "success") {
|
|
6102
6256
|
try {
|
|
6103
6257
|
const { requestCdAfter: requestCdAfter2 } = await Promise.resolve().then(() => (init_shell_cd(), shell_cd_exports));
|
|
@@ -6520,6 +6674,8 @@ var init_init_cmd = __esm({
|
|
|
6520
6674
|
init_environment();
|
|
6521
6675
|
init_config();
|
|
6522
6676
|
init_detector();
|
|
6677
|
+
init_roboco();
|
|
6678
|
+
init_validate_cmd();
|
|
6523
6679
|
init_ui();
|
|
6524
6680
|
init_logger();
|
|
6525
6681
|
}
|
|
@@ -7123,6 +7279,29 @@ async function runPipeline(cwd, config, options = {}) {
|
|
|
7123
7279
|
config,
|
|
7124
7280
|
previousResults: ctx.getResults()
|
|
7125
7281
|
};
|
|
7282
|
+
if (!options.force) {
|
|
7283
|
+
const gateResult = await STAGE_GATES[stageName](cwd).catch(() => null);
|
|
7284
|
+
if (gateResult) {
|
|
7285
|
+
await saveGateResult(cwd, `${stageName}.entry`, gateResult).catch(() => {
|
|
7286
|
+
});
|
|
7287
|
+
if (!gateResult.passed && !options.skipGates) {
|
|
7288
|
+
error(`${stageName} \uAC8C\uC774\uD2B8 \uC2E4\uD328 \u2014 \uD30C\uC774\uD504\uB77C\uC778 \uC911\uB2E8`);
|
|
7289
|
+
for (const v of gateResult.violations) {
|
|
7290
|
+
warn(` \xB7 ${v.message}${v.location ? ` [${v.location}]` : ""}`);
|
|
7291
|
+
}
|
|
7292
|
+
hint("\uC6B0\uD68C: pai run --skip-gates (\uACBD\uACE0) / --force (\uBB34\uC2DC)");
|
|
7293
|
+
await markStageEnd(cwd, stageName, false, `\uAC8C\uC774\uD2B8 \uC2E4\uD328: ${gateResult.name}`).catch(() => {
|
|
7294
|
+
});
|
|
7295
|
+
shouldStop = true;
|
|
7296
|
+
break;
|
|
7297
|
+
}
|
|
7298
|
+
if (!gateResult.passed && options.skipGates) {
|
|
7299
|
+
for (const v of gateResult.violations) {
|
|
7300
|
+
warn(` [\uAC8C\uC774\uD2B8 \uACBD\uACE0] ${v.message}`);
|
|
7301
|
+
}
|
|
7302
|
+
}
|
|
7303
|
+
}
|
|
7304
|
+
}
|
|
7126
7305
|
if (stage.canSkip(input)) {
|
|
7127
7306
|
info(`${stageName} \u2014 \uAC74\uB108\uB700 (\uC774\uBBF8 \uC644\uB8CC)`);
|
|
7128
7307
|
ctx.setResult(stageName, {
|
|
@@ -7135,8 +7314,14 @@ async function runPipeline(cwd, config, options = {}) {
|
|
|
7135
7314
|
});
|
|
7136
7315
|
continue;
|
|
7137
7316
|
}
|
|
7317
|
+
await markStageStart(cwd, stageName, "pai run").catch(() => {
|
|
7318
|
+
});
|
|
7138
7319
|
const result = await stage.run(input);
|
|
7139
7320
|
ctx.setResult(stageName, result);
|
|
7321
|
+
const stageSuccess = result.status !== "failed";
|
|
7322
|
+
const failReason = result.errors[0]?.message;
|
|
7323
|
+
await markStageEnd(cwd, stageName, stageSuccess, failReason).catch(() => {
|
|
7324
|
+
});
|
|
7140
7325
|
switch (result.status) {
|
|
7141
7326
|
case "success":
|
|
7142
7327
|
success(`${stageName} \uC644\uB8CC (${result.duration}ms)`);
|
|
@@ -7184,6 +7369,8 @@ var init_pipeline = __esm({
|
|
|
7184
7369
|
init_execution();
|
|
7185
7370
|
init_validation();
|
|
7186
7371
|
init_evaluation();
|
|
7372
|
+
init_roboco();
|
|
7373
|
+
init_gates();
|
|
7187
7374
|
init_ui();
|
|
7188
7375
|
STAGES = {
|
|
7189
7376
|
environment: environmentStage,
|
|
@@ -7203,23 +7390,17 @@ __export(pipeline_cmd_exports, {
|
|
|
7203
7390
|
async function pipelineCommand(cwd, options) {
|
|
7204
7391
|
printBanner();
|
|
7205
7392
|
try {
|
|
7206
|
-
await
|
|
7207
|
-
|
|
7208
|
-
"
|
|
7209
|
-
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7213
|
-
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
const pipelineOpts = {};
|
|
7218
|
-
if (options.from) pipelineOpts.from = options.from;
|
|
7219
|
-
if (options.only) pipelineOpts.only = options.only.split(",").map((s) => s.trim());
|
|
7220
|
-
await runPipeline(cwd, config, pipelineOpts);
|
|
7221
|
-
}
|
|
7222
|
-
);
|
|
7393
|
+
await acquireLock(cwd, "environment", "pai run", PIPELINE_TTL_MS);
|
|
7394
|
+
try {
|
|
7395
|
+
const config = await loadConfig(cwd) ?? createDefaultConfig("my-project", "prototype");
|
|
7396
|
+
const pipelineOpts = { skipGates: options.skipGates, force: options.force };
|
|
7397
|
+
if (options.from) pipelineOpts.from = options.from;
|
|
7398
|
+
if (options.only) pipelineOpts.only = options.only.split(",").map((s) => s.trim());
|
|
7399
|
+
await runPipeline(cwd, config, pipelineOpts);
|
|
7400
|
+
} finally {
|
|
7401
|
+
await releaseLock(cwd).catch(() => {
|
|
7402
|
+
});
|
|
7403
|
+
}
|
|
7223
7404
|
await syncHandoff(cwd).catch(() => {
|
|
7224
7405
|
});
|
|
7225
7406
|
} catch (err) {
|