autonomous-flow-daemon 1.0.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.ko.md +142 -125
- package/README.md +119 -134
- package/package.json +11 -5
- package/src/adapters/index.ts +247 -35
- package/src/cli.ts +79 -1
- package/src/commands/benchmark.ts +187 -0
- package/src/commands/diagnose.ts +56 -14
- package/src/commands/doctor.ts +243 -0
- package/src/commands/evolution.ts +107 -0
- package/src/commands/fix.ts +22 -2
- package/src/commands/hooks.ts +136 -0
- package/src/commands/lang.ts +41 -0
- package/src/commands/mcp.ts +129 -0
- package/src/commands/restart.ts +14 -0
- package/src/commands/score.ts +192 -64
- package/src/commands/start.ts +137 -37
- package/src/commands/stats.ts +103 -0
- package/src/commands/status.ts +157 -0
- package/src/commands/stop.ts +42 -9
- package/src/commands/sync.ts +253 -20
- package/src/commands/vaccine.ts +177 -0
- package/src/constants.ts +26 -1
- package/src/core/boast.ts +280 -0
- package/src/core/config.ts +49 -0
- package/src/core/db.ts +74 -3
- package/src/core/discovery.ts +65 -0
- package/src/core/evolution.ts +215 -0
- package/src/core/hologram/engine.ts +71 -0
- package/src/core/hologram/fallback.ts +11 -0
- package/src/core/hologram/incremental.ts +227 -0
- package/src/core/hologram/py-extractor.ts +132 -0
- package/src/core/hologram/ts-extractor.ts +320 -0
- package/src/core/hologram/types.ts +25 -0
- package/src/core/hologram.ts +64 -236
- package/src/core/hook-manager.ts +259 -0
- package/src/core/i18n/messages.ts +309 -0
- package/src/core/immune.ts +8 -123
- package/src/core/locale.ts +88 -0
- package/src/core/log-rotate.ts +33 -0
- package/src/core/log-utils.ts +38 -0
- package/src/core/lru-map.ts +61 -0
- package/src/core/notify.ts +53 -14
- package/src/core/rule-engine.ts +287 -0
- package/src/core/semantic-diff.ts +432 -0
- package/src/core/telemetry.ts +94 -0
- package/src/core/vaccine-registry.ts +212 -0
- package/src/core/workspace.ts +28 -0
- package/src/core/yaml-minimal.ts +176 -0
- package/src/daemon/client.ts +34 -6
- package/src/daemon/event-batcher.ts +108 -0
- package/src/daemon/guards.ts +13 -0
- package/src/daemon/http-routes.ts +293 -0
- package/src/daemon/mcp-handler.ts +270 -0
- package/src/daemon/server.ts +492 -273
- package/src/daemon/types.ts +100 -0
- package/src/daemon/workspace-map.ts +92 -0
- package/src/platform.ts +60 -0
- package/src/version.ts +15 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `afd stats` — Feature usage telemetry dashboard (developer-only).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { queryTelemetry, type TelemetrySummary } from "../core/telemetry";
|
|
6
|
+
|
|
7
|
+
const C = {
|
|
8
|
+
reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
|
|
9
|
+
red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m",
|
|
10
|
+
blue: "\x1b[34m", magenta: "\x1b[35m", cyan: "\x1b[36m", white: "\x1b[37m",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function bar(count: number, max: number, width = 20): string {
|
|
14
|
+
const ratio = max > 0 ? Math.min(count / max, 1) : 0;
|
|
15
|
+
const filled = Math.round(ratio * width);
|
|
16
|
+
return `${C.green}${"█".repeat(filled)}${C.dim}${"░".repeat(width - filled)}${C.reset}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sortedEntries(obj: Record<string, number>): [string, number][] {
|
|
20
|
+
return Object.entries(obj).sort((a, b) => b[1] - a[1]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function renderRankedList(data: Record<string, number>, barWidth = 16): string[] {
|
|
24
|
+
const entries = sortedEntries(data);
|
|
25
|
+
if (entries.length === 0) return [` ${C.dim}(no data)${C.reset}`];
|
|
26
|
+
const maxVal = entries[0][1];
|
|
27
|
+
const maxNameLen = Math.max(...entries.map(([n]) => n.length), 8);
|
|
28
|
+
return entries.map(([name, count]) => {
|
|
29
|
+
const pad = " ".repeat(Math.max(1, maxNameLen - name.length + 2));
|
|
30
|
+
return ` ${C.white}${name}${C.reset}${pad}${bar(count, maxVal, barWidth)} ${count}`;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderSection(title: string): string {
|
|
35
|
+
return `\n${C.bold}${C.cyan}${title}${C.reset}\n${"─".repeat(50)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function statsCommand(opts: { days?: string }) {
|
|
39
|
+
const days = parseInt(opts.days ?? "7", 10) || 7;
|
|
40
|
+
const data: TelemetrySummary = queryTelemetry(days);
|
|
41
|
+
|
|
42
|
+
const out: string[] = [];
|
|
43
|
+
|
|
44
|
+
out.push(`${C.bold}📊 Feature Usage Telemetry${C.reset} ${C.dim}(last ${days} days, ${data.totalEvents} events total)${C.reset}`);
|
|
45
|
+
|
|
46
|
+
// CLI Commands
|
|
47
|
+
out.push(renderSection("CLI Commands"));
|
|
48
|
+
out.push(...renderRankedList(data.cli));
|
|
49
|
+
|
|
50
|
+
// MCP Tools
|
|
51
|
+
out.push(renderSection("MCP Tools"));
|
|
52
|
+
out.push(...renderRankedList(data.mcp));
|
|
53
|
+
|
|
54
|
+
// S.E.A.M Cycle
|
|
55
|
+
out.push(renderSection("S.E.A.M Cycle"));
|
|
56
|
+
const seamEntries = sortedEntries(data.seam.counts);
|
|
57
|
+
if (seamEntries.length === 0) {
|
|
58
|
+
out.push(` ${C.dim}(no data)${C.reset}`);
|
|
59
|
+
} else {
|
|
60
|
+
const maxSeam = seamEntries[0][1];
|
|
61
|
+
const maxNameLen = Math.max(...seamEntries.map(([n]) => n.length), 8);
|
|
62
|
+
for (const [action, count] of seamEntries) {
|
|
63
|
+
const pad = " ".repeat(Math.max(1, maxNameLen - action.length + 2));
|
|
64
|
+
const avg = data.seam.avgDurationMs[action];
|
|
65
|
+
const avgStr = avg != null ? `${C.dim}avg ${avg}ms${C.reset}` : "";
|
|
66
|
+
out.push(` ${C.white}${action}${C.reset}${pad}${bar(count, maxSeam, 12)} ${count} ${avgStr}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Immune Activity + Accuracy
|
|
71
|
+
out.push(renderSection("Immune Activity"));
|
|
72
|
+
const hits = data.immune["heal_hit"] ?? 0;
|
|
73
|
+
const falsePos = data.immune["heal_false_positive"] ?? 0;
|
|
74
|
+
const passes = data.immune["heal_pass"] ?? 0;
|
|
75
|
+
const suppressions = data.immune["suppression"] ?? 0;
|
|
76
|
+
const totalJudgments = hits + falsePos + passes;
|
|
77
|
+
const accuracy = totalJudgments > 0 ? Math.round((hits + passes) / totalJudgments * 100) : null;
|
|
78
|
+
const precisionLabel = (hits + falsePos) > 0 ? `${Math.round(hits / (hits + falsePos) * 100)}%` : "—";
|
|
79
|
+
|
|
80
|
+
out.push(` ${C.white}Hits${C.reset} ${hits} ${C.dim}(corruption detected & restored)${C.reset}`);
|
|
81
|
+
out.push(` ${C.white}Passes${C.reset} ${passes} ${C.dim}(immune file changed, valid)${C.reset}`);
|
|
82
|
+
out.push(` ${C.white}False +${C.reset} ${falsePos} ${C.dim}(restored but user overrode)${C.reset}`);
|
|
83
|
+
out.push(` ${C.white}Suppress${C.reset} ${suppressions} ${C.dim}(mass event skip)${C.reset}`);
|
|
84
|
+
out.push("");
|
|
85
|
+
out.push(` ${C.bold}Accuracy${C.reset} ${accuracy != null ? `${C.green}${accuracy}%${C.reset}` : `${C.dim}—${C.reset}`} ${C.dim}(correct judgments / total)${C.reset}`);
|
|
86
|
+
out.push(` ${C.bold}Precision${C.reset} ${(hits + falsePos) > 0 ? `${C.green}${precisionLabel}${C.reset}` : `${C.dim}—${C.reset}`} ${C.dim}(true hits / all blocks)${C.reset}`);
|
|
87
|
+
|
|
88
|
+
// Validators
|
|
89
|
+
if (Object.keys(data.validator).length > 0) {
|
|
90
|
+
out.push(renderSection("Validator Triggers"));
|
|
91
|
+
out.push(...renderRankedList(data.validator));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Dead features warning
|
|
95
|
+
const allCli = ["start", "stop", "restart", "status", "score", "fix", "sync", "doctor", "diagnose", "vaccine", "evolution", "mcp", "lang", "stats"];
|
|
96
|
+
const unusedCli = allCli.filter(cmd => !(cmd in data.cli));
|
|
97
|
+
if (unusedCli.length > 0 && Object.keys(data.cli).length > 0) {
|
|
98
|
+
out.push(renderSection("Unused CLI Commands"));
|
|
99
|
+
out.push(` ${C.yellow}${unusedCli.join(", ")}${C.reset}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(out.join("\n"));
|
|
103
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { getDaemonInfo, isDaemonAlive, daemonRequest } from "../daemon/client";
|
|
4
|
+
import { resolveWorkspacePaths } from "../constants";
|
|
5
|
+
import { getSystemLanguage } from "../core/locale";
|
|
6
|
+
|
|
7
|
+
const C = {
|
|
8
|
+
reset: "\x1b[0m",
|
|
9
|
+
bold: "\x1b[1m",
|
|
10
|
+
dim: "\x1b[2m",
|
|
11
|
+
red: "\x1b[31m",
|
|
12
|
+
green: "\x1b[32m",
|
|
13
|
+
yellow: "\x1b[33m",
|
|
14
|
+
cyan: "\x1b[36m",
|
|
15
|
+
white: "\x1b[37m",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const ko = getSystemLanguage() === "ko";
|
|
19
|
+
|
|
20
|
+
interface HealthData {
|
|
21
|
+
status: string;
|
|
22
|
+
pid: number;
|
|
23
|
+
workspace: string;
|
|
24
|
+
port: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ScoreData {
|
|
28
|
+
uptime: number;
|
|
29
|
+
immune: { antibodies: number; autoHealed: number };
|
|
30
|
+
ecosystem: { primary: string };
|
|
31
|
+
dynamicImmune?: { activeValidators: number };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function checkHooksInjected(): boolean {
|
|
35
|
+
const hooksPath = join(resolveWorkspacePaths().root, ".claude/hooks.json");
|
|
36
|
+
if (!existsSync(hooksPath)) return false;
|
|
37
|
+
try {
|
|
38
|
+
const content = readFileSync(hooksPath, "utf-8");
|
|
39
|
+
return content.includes("afd-auto-heal");
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function checkMcpRegistered(): boolean {
|
|
46
|
+
const mcpPath = join(resolveWorkspacePaths().root, ".mcp.json");
|
|
47
|
+
if (!existsSync(mcpPath)) return false;
|
|
48
|
+
try {
|
|
49
|
+
const content = readFileSync(mcpPath, "utf-8");
|
|
50
|
+
return content.includes('"afd"');
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getQuarantinedFiles(): string[] {
|
|
57
|
+
const paths = resolveWorkspacePaths();
|
|
58
|
+
if (!existsSync(paths.quarantineDir)) return [];
|
|
59
|
+
try {
|
|
60
|
+
return readdirSync(paths.quarantineDir).sort().reverse();
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatUptime(seconds: number): string {
|
|
67
|
+
if (seconds < 60) return `${seconds}s`;
|
|
68
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
69
|
+
const h = Math.floor(seconds / 3600);
|
|
70
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
71
|
+
return `${h}h ${m}m`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function indicator(ok: boolean): string {
|
|
75
|
+
return ok ? `${C.green}●${C.reset}` : `${C.red}●${C.reset}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function statusCommand() {
|
|
79
|
+
const out: string[] = [];
|
|
80
|
+
|
|
81
|
+
out.push("");
|
|
82
|
+
out.push(`${C.bold}afd status${C.reset}`);
|
|
83
|
+
out.push("");
|
|
84
|
+
|
|
85
|
+
// ── 1. Daemon ──
|
|
86
|
+
const info = getDaemonInfo();
|
|
87
|
+
if (!info || !(await isDaemonAlive(info))) {
|
|
88
|
+
out.push(` ${C.red}●${C.reset} ${C.bold}Daemon${C.reset} ${C.red}STOPPED${C.reset}`);
|
|
89
|
+
out.push("");
|
|
90
|
+
out.push(` ${C.dim}${ko ? "→ afd start 를 실행하세요" : "→ Run afd start to activate"}${C.reset}`);
|
|
91
|
+
out.push("");
|
|
92
|
+
console.log(out.join("\n"));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fetch live data
|
|
97
|
+
let score: ScoreData | null = null;
|
|
98
|
+
try {
|
|
99
|
+
score = await daemonRequest<ScoreData>("/score");
|
|
100
|
+
} catch { /* use fallback */ }
|
|
101
|
+
|
|
102
|
+
const uptime = score ? formatUptime(score.uptime) : "?";
|
|
103
|
+
const ecosystem = score?.ecosystem.primary ?? "Unknown";
|
|
104
|
+
|
|
105
|
+
out.push(` ${C.green}●${C.reset} ${C.bold}Daemon${C.reset} ${C.green}ACTIVE${C.reset} ${C.dim}(pid=${info.pid} port=${info.port})${C.reset}`);
|
|
106
|
+
out.push(` ${C.dim} Uptime${C.reset} ${uptime} ${C.dim}|${C.reset} ${ecosystem}`);
|
|
107
|
+
out.push("");
|
|
108
|
+
|
|
109
|
+
// ── 2. Connections ──
|
|
110
|
+
out.push(` ${C.bold}${ko ? "연결 상태" : "Connections"}${C.reset}`);
|
|
111
|
+
|
|
112
|
+
const hooksOk = checkHooksInjected();
|
|
113
|
+
out.push(` ${indicator(hooksOk)} Hook ${hooksOk ? `${C.green}INJECTED${C.reset}` : `${C.red}MISSING${C.reset} ${C.dim}(afd start로 주입)${C.reset}`}`);
|
|
114
|
+
|
|
115
|
+
const mcpOk = checkMcpRegistered();
|
|
116
|
+
out.push(` ${indicator(mcpOk)} MCP ${mcpOk ? `${C.green}REGISTERED${C.reset}` : `${C.yellow}NOT SET${C.reset}`}`);
|
|
117
|
+
|
|
118
|
+
out.push("");
|
|
119
|
+
|
|
120
|
+
// ── 3. Defenses ──
|
|
121
|
+
out.push(` ${C.bold}${ko ? "방어막" : "Defenses"}${C.reset}`);
|
|
122
|
+
|
|
123
|
+
const antibodies = score?.immune.antibodies ?? 0;
|
|
124
|
+
const healed = score?.immune.autoHealed ?? 0;
|
|
125
|
+
const validators = score?.dynamicImmune?.activeValidators ?? 0;
|
|
126
|
+
|
|
127
|
+
out.push(` ${indicator(antibodies > 0)} ${ko ? "항체" : "Antibodies"} ${C.bold}${antibodies}${C.reset} ${ko ? "활성" : "active"}${healed > 0 ? ` ${C.dim}(${healed}${ko ? "회 치유" : " healed"})${C.reset}` : ""}`);
|
|
128
|
+
out.push(` ${indicator(validators > 0)} ${ko ? "검증기" : "Validators"} ${validators > 0 ? `${C.bold}${validators}${C.reset} ${ko ? "로드됨" : "loaded"}` : `${C.dim}${ko ? "없음" : "none"}${C.reset} ${C.dim}(.afd/validators/)${C.reset}`}`);
|
|
129
|
+
|
|
130
|
+
out.push("");
|
|
131
|
+
|
|
132
|
+
// ── 4. Quarantine ──
|
|
133
|
+
const quarantined = getQuarantinedFiles();
|
|
134
|
+
|
|
135
|
+
if (quarantined.length > 0) {
|
|
136
|
+
out.push(` ${C.yellow}⚠${C.reset} ${C.bold}${C.yellow}${ko ? "격리 구역" : "Quarantine"}${C.reset} ${C.dim}(${quarantined.length} ${ko ? "파일" : "file"}${quarantined.length !== 1 ? "s" : ""})${C.reset}`);
|
|
137
|
+
|
|
138
|
+
const show = quarantined.slice(0, 5);
|
|
139
|
+
for (const file of show) {
|
|
140
|
+
out.push(` ${C.dim}${file}${C.reset}`);
|
|
141
|
+
}
|
|
142
|
+
if (quarantined.length > 5) {
|
|
143
|
+
out.push(` ${C.dim}... +${quarantined.length - 5} more${C.reset}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
out.push("");
|
|
147
|
+
out.push(` ${C.dim}💡 ${ko
|
|
148
|
+
? "격리된 파일에서 코드를 구출하거나 불필요하면 삭제하세요."
|
|
149
|
+
: "Rescue code from quarantined files or delete if unneeded."}${C.reset}`);
|
|
150
|
+
out.push(` ${C.dim} ${ko ? "경로" : "Path"}: .afd/quarantine/${C.reset}`);
|
|
151
|
+
} else {
|
|
152
|
+
out.push(` ${C.green}●${C.reset} ${ko ? "격리 구역" : "Quarantine"} ${C.dim}${ko ? "비어있음 — 이상 없음" : "empty — all clear"}${C.reset}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
out.push("");
|
|
156
|
+
console.log(out.join("\n"));
|
|
157
|
+
}
|
package/src/commands/stop.ts
CHANGED
|
@@ -1,35 +1,68 @@
|
|
|
1
1
|
import { getDaemonInfo, isDaemonAlive, daemonRequest } from "../daemon/client";
|
|
2
2
|
import { unlinkSync } from "fs";
|
|
3
|
-
import {
|
|
3
|
+
import { resolveWorkspacePaths } from "../constants";
|
|
4
|
+
import { formatShiftSummary } from "../core/boast";
|
|
5
|
+
import type { ShiftSummary } from "../core/boast";
|
|
6
|
+
import { getSystemLanguage } from "../core/locale";
|
|
7
|
+
import { getMessages, t } from "../core/i18n/messages";
|
|
8
|
+
import { detectEcosystem } from "../adapters/index";
|
|
4
9
|
|
|
5
10
|
function cleanupFiles() {
|
|
6
|
-
|
|
7
|
-
try { unlinkSync(
|
|
11
|
+
const paths = resolveWorkspacePaths();
|
|
12
|
+
try { unlinkSync(paths.pidFile); } catch {}
|
|
13
|
+
try { unlinkSync(paths.portFile); } catch {}
|
|
8
14
|
}
|
|
9
15
|
|
|
10
|
-
export async function stopCommand() {
|
|
16
|
+
export async function stopCommand(options?: { clean?: boolean }) {
|
|
17
|
+
const lang = getSystemLanguage();
|
|
18
|
+
const msg = getMessages(lang);
|
|
11
19
|
const info = getDaemonInfo();
|
|
20
|
+
|
|
12
21
|
if (!info) {
|
|
13
|
-
console.log(
|
|
22
|
+
console.log(msg.DAEMON_NOT_RUNNING);
|
|
14
23
|
return;
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
if (await isDaemonAlive(info)) {
|
|
27
|
+
// Fetch shift summary before stopping
|
|
28
|
+
try {
|
|
29
|
+
const summary = await daemonRequest<ShiftSummary>("/shift-summary");
|
|
30
|
+
console.log(formatShiftSummary(summary, lang));
|
|
31
|
+
} catch {
|
|
32
|
+
// Non-fatal: summary is a nicety, not a requirement
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
try {
|
|
19
36
|
await daemonRequest("/stop");
|
|
20
|
-
console.log(
|
|
37
|
+
console.log(t(msg.DAEMON_STOPPED, { pid: info.pid }));
|
|
21
38
|
} catch {
|
|
22
|
-
// Force kill if graceful stop fails
|
|
23
39
|
try {
|
|
24
40
|
process.kill(info.pid, "SIGTERM");
|
|
25
|
-
console.log(
|
|
41
|
+
console.log(t(msg.DAEMON_KILLED, { pid: info.pid }));
|
|
26
42
|
} catch {
|
|
27
43
|
console.log("[afd] Daemon process already gone.");
|
|
28
44
|
}
|
|
29
45
|
}
|
|
30
46
|
} else {
|
|
31
|
-
console.log(
|
|
47
|
+
console.log(msg.DAEMON_NOT_RESPONDING);
|
|
32
48
|
}
|
|
33
49
|
|
|
34
50
|
cleanupFiles();
|
|
51
|
+
|
|
52
|
+
// --clean: remove injected hooks and MCP registration
|
|
53
|
+
if (options?.clean) {
|
|
54
|
+
const cwd = process.cwd();
|
|
55
|
+
const ecosystems = detectEcosystem(cwd);
|
|
56
|
+
for (const { adapter } of ecosystems) {
|
|
57
|
+
if (adapter.removeHooks) {
|
|
58
|
+
const r = adapter.removeHooks(cwd);
|
|
59
|
+
if (r.removed) console.log(`[afd] ${r.message}`);
|
|
60
|
+
}
|
|
61
|
+
if (adapter.unregisterMcp) {
|
|
62
|
+
const r = adapter.unregisterMcp(cwd);
|
|
63
|
+
if (r.removed) console.log(`[afd] ${r.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
console.log("[afd] Clean stop complete. All afd integrations removed.");
|
|
67
|
+
}
|
|
35
68
|
}
|
package/src/commands/sync.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { readFileSync } from "fs";
|
|
2
|
-
import { resolve } from "path";
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
2
|
+
import { resolve, join } from "path";
|
|
3
3
|
import { daemonRequest } from "../daemon/client";
|
|
4
4
|
import { AFD_DIR } from "../constants";
|
|
5
|
+
import { getSystemLanguage } from "../core/locale";
|
|
5
6
|
|
|
6
7
|
interface SyncResponse {
|
|
7
8
|
status: string;
|
|
@@ -9,7 +10,95 @@ interface SyncResponse {
|
|
|
9
10
|
count: number;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
interface VaccinePayload {
|
|
14
|
+
version: string;
|
|
15
|
+
generatedAt: string;
|
|
16
|
+
ecosystem: string;
|
|
17
|
+
antibodyCount: number;
|
|
18
|
+
antibodies: VaccineAntibody[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface VaccineAntibody {
|
|
22
|
+
id: string;
|
|
23
|
+
patternType: string;
|
|
24
|
+
fileTarget: string;
|
|
25
|
+
patches: { op: string; path: string; value?: string }[];
|
|
26
|
+
learnedAt: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SyncOptions {
|
|
30
|
+
push?: boolean;
|
|
31
|
+
pull?: boolean;
|
|
32
|
+
remote?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const msgs = {
|
|
36
|
+
en: {
|
|
37
|
+
title: "afd sync — Vaccine Network",
|
|
38
|
+
ecosystem: "Ecosystem",
|
|
39
|
+
antibodies: "Antibodies",
|
|
40
|
+
generated: "Generated",
|
|
41
|
+
payload: "Payload",
|
|
42
|
+
noAntibodies: "No antibodies to export. Run `afd fix` first.",
|
|
43
|
+
exported: "Vaccine payload generated.",
|
|
44
|
+
pushTitle: "afd sync --push",
|
|
45
|
+
pushSuccess: "Pushed to team vaccine store",
|
|
46
|
+
pushCreated: "Created team vaccine store",
|
|
47
|
+
pullTitle: "afd sync --pull",
|
|
48
|
+
pullSuccess: "Pulled from team vaccine store",
|
|
49
|
+
pullMerged: "merged",
|
|
50
|
+
pullNew: "new",
|
|
51
|
+
pullSkipped: "skipped (already known)",
|
|
52
|
+
pullNoStore: "No team vaccine store found.",
|
|
53
|
+
pullHint: "Run `afd sync --push` first to create the shared store.",
|
|
54
|
+
learnedVia: "Learned via pull",
|
|
55
|
+
ready: "antibody(ies) ready for global federation.",
|
|
56
|
+
},
|
|
57
|
+
ko: {
|
|
58
|
+
title: "afd sync — 백신 네트워크",
|
|
59
|
+
ecosystem: "에코시스템",
|
|
60
|
+
antibodies: "항체",
|
|
61
|
+
generated: "생성일",
|
|
62
|
+
payload: "페이로드",
|
|
63
|
+
noAntibodies: "내보낼 항체가 없습니다. `afd fix`를 먼저 실행하세요.",
|
|
64
|
+
exported: "백신 페이로드 생성 완료.",
|
|
65
|
+
pushTitle: "afd sync --push",
|
|
66
|
+
pushSuccess: "팀 백신 저장소에 푸시 완료",
|
|
67
|
+
pushCreated: "팀 백신 저장소 생성",
|
|
68
|
+
pullTitle: "afd sync --pull",
|
|
69
|
+
pullSuccess: "팀 백신 저장소에서 풀 완료",
|
|
70
|
+
pullMerged: "병합됨",
|
|
71
|
+
pullNew: "신규",
|
|
72
|
+
pullSkipped: "건너뜀 (이미 존재)",
|
|
73
|
+
pullNoStore: "팀 백신 저장소를 찾을 수 없습니다.",
|
|
74
|
+
pullHint: "`afd sync --push`를 먼저 실행하여 공유 저장소를 생성하세요.",
|
|
75
|
+
learnedVia: "풀로 학습됨",
|
|
76
|
+
ready: "개 항체가 글로벌 페더레이션 준비 완료.",
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const TEAM_STORE_DIR = join(AFD_DIR, "team-vaccines");
|
|
81
|
+
const TEAM_PAYLOAD_FILE = join(TEAM_STORE_DIR, "shared-vaccine-payload.json");
|
|
82
|
+
|
|
83
|
+
export async function syncCommand(opts: SyncOptions = {}) {
|
|
84
|
+
const lang = getSystemLanguage();
|
|
85
|
+
const m = msgs[lang];
|
|
86
|
+
|
|
87
|
+
if (opts.push) {
|
|
88
|
+
await syncPush(m);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (opts.pull) {
|
|
93
|
+
await syncPull(m);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Default: export local payload (original behavior)
|
|
98
|
+
await syncExport(m);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function syncExport(m: typeof msgs.en) {
|
|
13
102
|
let result: SyncResponse;
|
|
14
103
|
try {
|
|
15
104
|
result = await daemonRequest<SyncResponse>("/sync");
|
|
@@ -20,31 +109,175 @@ export async function syncCommand() {
|
|
|
20
109
|
}
|
|
21
110
|
|
|
22
111
|
if (result.count === 0) {
|
|
23
|
-
console.log(
|
|
112
|
+
console.log(`[afd sync] ${m.noAntibodies}`);
|
|
24
113
|
return;
|
|
25
114
|
}
|
|
26
115
|
|
|
27
|
-
// Read the generated payload for display
|
|
28
116
|
const payloadPath = resolve(AFD_DIR, "global-vaccine-payload.json");
|
|
29
117
|
const payload = JSON.parse(readFileSync(payloadPath, "utf-8"));
|
|
118
|
+
renderPayloadBox(payload, m);
|
|
119
|
+
console.log(`\n[afd sync] ${m.exported} ${result.count} ${m.ready}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function syncPush(m: typeof msgs.en) {
|
|
123
|
+
// First, export latest payload
|
|
124
|
+
let result: SyncResponse;
|
|
125
|
+
try {
|
|
126
|
+
result = await daemonRequest<SyncResponse>("/sync");
|
|
127
|
+
} catch (err: unknown) {
|
|
128
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
129
|
+
console.error(`[afd sync] ${msg}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (result.count === 0) {
|
|
134
|
+
console.log(`[afd sync] ${m.noAntibodies}`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Copy to team store
|
|
139
|
+
const localPayloadPath = resolve(AFD_DIR, "global-vaccine-payload.json");
|
|
140
|
+
const localPayload = readFileSync(localPayloadPath, "utf-8");
|
|
141
|
+
|
|
142
|
+
mkdirSync(TEAM_STORE_DIR, { recursive: true });
|
|
143
|
+
|
|
144
|
+
// Merge with existing team payload if present
|
|
145
|
+
let teamPayload: VaccinePayload;
|
|
146
|
+
if (existsSync(TEAM_PAYLOAD_FILE)) {
|
|
147
|
+
try {
|
|
148
|
+
teamPayload = JSON.parse(readFileSync(TEAM_PAYLOAD_FILE, "utf-8"));
|
|
149
|
+
} catch {
|
|
150
|
+
teamPayload = JSON.parse(localPayload);
|
|
151
|
+
}
|
|
152
|
+
// Merge: add new antibodies, update existing ones
|
|
153
|
+
const newPayload = JSON.parse(localPayload) as VaccinePayload;
|
|
154
|
+
const existingIds = new Set(teamPayload.antibodies.map(a => a.id));
|
|
155
|
+
for (const ab of newPayload.antibodies) {
|
|
156
|
+
if (existingIds.has(ab.id)) {
|
|
157
|
+
// Update existing
|
|
158
|
+
const idx = teamPayload.antibodies.findIndex(a => a.id === ab.id);
|
|
159
|
+
if (idx >= 0) teamPayload.antibodies[idx] = ab;
|
|
160
|
+
} else {
|
|
161
|
+
teamPayload.antibodies.push(ab);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
teamPayload.antibodyCount = teamPayload.antibodies.length;
|
|
165
|
+
teamPayload.generatedAt = new Date().toISOString();
|
|
166
|
+
} else {
|
|
167
|
+
teamPayload = JSON.parse(localPayload);
|
|
168
|
+
console.log(`[afd sync] ${m.pushCreated}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
writeFileSync(TEAM_PAYLOAD_FILE, JSON.stringify(teamPayload, null, 2), "utf-8");
|
|
172
|
+
|
|
173
|
+
const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
|
|
174
|
+
const W = 50;
|
|
175
|
+
const line = (l: string, r: string) => `${l}${BOX.h.repeat(W)}${r}`;
|
|
176
|
+
const row = (s: string) => `${BOX.v} ${s}${" ".repeat(Math.max(0, W - 2 - s.length))}${BOX.v}`;
|
|
177
|
+
|
|
178
|
+
console.log(line(BOX.tl, BOX.tr));
|
|
179
|
+
console.log(row(`📤 ${m.pushTitle}`));
|
|
180
|
+
console.log(line(BOX.ml, BOX.mr));
|
|
181
|
+
console.log(row(`${m.antibodies}: ${teamPayload.antibodyCount}`));
|
|
182
|
+
console.log(row(`${m.payload}: ${TEAM_PAYLOAD_FILE}`));
|
|
183
|
+
console.log(line(BOX.ml, BOX.mr));
|
|
184
|
+
console.log(row(`✅ ${m.pushSuccess}`));
|
|
185
|
+
console.log(line(BOX.bl, BOX.br));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function syncPull(m: typeof msgs.en) {
|
|
189
|
+
if (!existsSync(TEAM_PAYLOAD_FILE)) {
|
|
190
|
+
console.log(`[afd sync] ${m.pullNoStore}`);
|
|
191
|
+
console.log(`[afd sync] ${m.pullHint}`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let teamPayload: VaccinePayload;
|
|
196
|
+
try {
|
|
197
|
+
teamPayload = JSON.parse(readFileSync(TEAM_PAYLOAD_FILE, "utf-8"));
|
|
198
|
+
} catch {
|
|
199
|
+
console.error("[afd sync] Failed to parse team vaccine payload.");
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Learn each antibody into the daemon
|
|
204
|
+
let newCount = 0;
|
|
205
|
+
let skippedCount = 0;
|
|
206
|
+
const results: { id: string; status: string }[] = [];
|
|
207
|
+
|
|
208
|
+
for (const ab of teamPayload.antibodies) {
|
|
209
|
+
try {
|
|
210
|
+
// Try to learn via daemon API
|
|
211
|
+
const res = await fetch(`http://127.0.0.1:${getDaemonPort()}/antibodies/learn`, {
|
|
212
|
+
method: "POST",
|
|
213
|
+
headers: { "Content-Type": "application/json" },
|
|
214
|
+
body: JSON.stringify({
|
|
215
|
+
id: ab.id,
|
|
216
|
+
patternType: ab.patternType,
|
|
217
|
+
fileTarget: ab.fileTarget,
|
|
218
|
+
patches: ab.patches,
|
|
219
|
+
}),
|
|
220
|
+
signal: AbortSignal.timeout(2000),
|
|
221
|
+
});
|
|
222
|
+
if (res.ok) {
|
|
223
|
+
results.push({ id: ab.id, status: m.pullNew });
|
|
224
|
+
newCount++;
|
|
225
|
+
} else {
|
|
226
|
+
results.push({ id: ab.id, status: m.pullSkipped });
|
|
227
|
+
skippedCount++;
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
results.push({ id: ab.id, status: m.pullSkipped });
|
|
231
|
+
skippedCount++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
|
|
236
|
+
const W = 50;
|
|
237
|
+
const line = (l: string, r: string) => `${l}${BOX.h.repeat(W)}${r}`;
|
|
238
|
+
const row = (s: string) => `${BOX.v} ${s}${" ".repeat(Math.max(0, W - 2 - s.length))}${BOX.v}`;
|
|
239
|
+
|
|
240
|
+
console.log(line(BOX.tl, BOX.tr));
|
|
241
|
+
console.log(row(`📥 ${m.pullTitle}`));
|
|
242
|
+
console.log(line(BOX.ml, BOX.mr));
|
|
243
|
+
|
|
244
|
+
for (const r of results) {
|
|
245
|
+
const icon = r.status === m.pullNew ? "✅" : "⏭️";
|
|
246
|
+
console.log(row(`${icon} ${r.id}: ${r.status}`));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(line(BOX.ml, BOX.mr));
|
|
250
|
+
console.log(row(`✅ ${m.pullSuccess}: ${newCount} ${m.pullMerged}`));
|
|
251
|
+
console.log(line(BOX.bl, BOX.br));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getDaemonPort(): number {
|
|
255
|
+
const { getDaemonInfo } = require("../daemon/client");
|
|
256
|
+
const info = getDaemonInfo();
|
|
257
|
+
if (!info) throw new Error("Daemon not running");
|
|
258
|
+
return info.port;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function renderPayloadBox(payload: VaccinePayload, m: typeof msgs.en) {
|
|
262
|
+
const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
|
|
263
|
+
const W = 48;
|
|
264
|
+
const line = (l: string, r: string) => `${l}${BOX.h.repeat(W)}${r}`;
|
|
265
|
+
const row = (s: string) => `${BOX.v} ${s}${" ".repeat(Math.max(0, W - 2 - s.length))}${BOX.v}`;
|
|
30
266
|
|
|
31
|
-
|
|
32
|
-
console.log(
|
|
33
|
-
console.log(
|
|
34
|
-
console.log(
|
|
35
|
-
console.log(
|
|
36
|
-
console.log(
|
|
37
|
-
console.log(
|
|
38
|
-
console.log(`\u251C${box}\u2524`);
|
|
267
|
+
console.log(line(BOX.tl, BOX.tr));
|
|
268
|
+
console.log(row(`${m.title}`));
|
|
269
|
+
console.log(line(BOX.ml, BOX.mr));
|
|
270
|
+
console.log(row(`${m.ecosystem} : ${payload.ecosystem}`));
|
|
271
|
+
console.log(row(`${m.antibodies} : ${payload.antibodyCount}`));
|
|
272
|
+
console.log(row(`${m.generated} : ${payload.generatedAt.substring(0, 19)}`));
|
|
273
|
+
console.log(line(BOX.ml, BOX.mr));
|
|
39
274
|
|
|
40
275
|
for (const ab of payload.antibodies) {
|
|
41
|
-
const patches = ab.patches.map(
|
|
42
|
-
console.log(
|
|
276
|
+
const patches = ab.patches.map(p => `${p.op} ${p.path}`).join(", ");
|
|
277
|
+
console.log(row(`[${ab.id}] ${ab.patternType.padEnd(18)} ${patches.substring(0, 14)}`));
|
|
43
278
|
}
|
|
44
279
|
|
|
45
|
-
console.log(
|
|
46
|
-
console.log(
|
|
47
|
-
console.log(
|
|
48
|
-
console.log();
|
|
49
|
-
console.log(`[afd sync] Vaccine payload generated. ${result.count} antibody(ies) ready for global federation.`);
|
|
280
|
+
console.log(line(BOX.ml, BOX.mr));
|
|
281
|
+
console.log(row(`${m.payload}: .afd/global-vaccine-payload.json`));
|
|
282
|
+
console.log(line(BOX.bl, BOX.br));
|
|
50
283
|
}
|