autonomous-flow-daemon 1.6.0 → 1.9.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 +85 -85
- package/LICENSE +21 -21
- package/README-ko.md +282 -0
- package/README.md +282 -266
- package/mcp-config.json +10 -10
- package/package.json +4 -2
- package/src/adapters/index.ts +370 -370
- package/src/cli.ts +162 -127
- package/src/commands/benchmark.ts +187 -187
- package/src/commands/correlate.ts +180 -0
- package/src/commands/dashboard.ts +404 -0
- package/src/commands/evolution.ts +84 -1
- package/src/commands/fix.ts +158 -158
- package/src/commands/lang.ts +41 -41
- package/src/commands/plugin.ts +110 -0
- package/src/commands/restart.ts +14 -14
- package/src/commands/score.ts +276 -276
- package/src/commands/start.ts +155 -155
- package/src/commands/status.ts +157 -157
- package/src/commands/stop.ts +68 -68
- package/src/commands/suggest.ts +211 -0
- package/src/commands/sync.ts +329 -16
- package/src/constants.ts +32 -32
- package/src/core/boast.ts +280 -280
- package/src/core/config.ts +49 -49
- package/src/core/correlation-engine.ts +265 -0
- package/src/core/db.ts +145 -117
- package/src/core/discovery.ts +65 -65
- package/src/core/federation.ts +129 -0
- package/src/core/hologram/engine.ts +71 -71
- package/src/core/hologram/fallback.ts +11 -11
- package/src/core/hologram/go-extractor.ts +203 -0
- package/src/core/hologram/incremental.ts +227 -227
- package/src/core/hologram/py-extractor.ts +132 -132
- package/src/core/hologram/rust-extractor.ts +244 -0
- package/src/core/hologram/ts-extractor.ts +406 -320
- package/src/core/hologram/types.ts +27 -25
- package/src/core/hologram.ts +73 -71
- package/src/core/i18n/messages.ts +309 -309
- package/src/core/locale.ts +88 -88
- package/src/core/log-rotate.ts +33 -33
- package/src/core/log-utils.ts +38 -38
- package/src/core/lru-map.ts +61 -61
- package/src/core/notify.ts +74 -74
- package/src/core/plugin-manager.ts +225 -0
- package/src/core/rule-suggestion.ts +127 -0
- package/src/core/validator-generator.ts +224 -0
- package/src/core/workspace.ts +28 -28
- package/src/daemon/client.ts +78 -65
- package/src/daemon/event-batcher.ts +108 -108
- package/src/daemon/guards.ts +13 -13
- package/src/daemon/http-routes.ts +376 -293
- package/src/daemon/mcp-handler.ts +575 -270
- package/src/daemon/mcp-subscriptions.ts +81 -0
- package/src/daemon/mesh.ts +51 -0
- package/src/daemon/server.ts +655 -590
- package/src/daemon/types.ts +121 -100
- package/src/daemon/workspace-map.ts +104 -92
- package/src/platform.ts +60 -60
- package/src/version.ts +15 -15
- package/README.ko.md +0 -266
package/src/commands/start.ts
CHANGED
|
@@ -1,155 +1,155 @@
|
|
|
1
|
-
import { resolve } from "path";
|
|
2
|
-
import { spawn } from "child_process";
|
|
3
|
-
import { openSync, mkdirSync } from "fs";
|
|
4
|
-
import { getDaemonInfo, isDaemonAlive } from "../daemon/client";
|
|
5
|
-
import { WATCH_TARGETS, resolveWorkspacePaths } from "../constants";
|
|
6
|
-
import { detectEcosystem } from "../adapters/index";
|
|
7
|
-
import type { DetectionResult } from "../adapters/index";
|
|
8
|
-
import { detachedSpawnOptions, IS_WINDOWS } from "../platform";
|
|
9
|
-
import { rotateLogIfNeeded } from "../core/log-rotate";
|
|
10
|
-
import { getSystemLanguage } from "../core/locale";
|
|
11
|
-
import { getMessages, t } from "../core/i18n/messages";
|
|
12
|
-
import type { MessageDict } from "../core/i18n/messages";
|
|
13
|
-
import { discoverWatchTargets } from "../core/discovery";
|
|
14
|
-
|
|
15
|
-
const STARTUP_POLL_INTERVAL_MS = 100;
|
|
16
|
-
const STARTUP_POLL_MAX_MS = 3000;
|
|
17
|
-
|
|
18
|
-
interface SetupStep {
|
|
19
|
-
label: string;
|
|
20
|
-
newMsg: string;
|
|
21
|
-
okMsg: string;
|
|
22
|
-
skipMsg: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface SetupResult {
|
|
26
|
-
ecosystem: string;
|
|
27
|
-
steps: { label: string; status: "new" | "ok" | "skip" }[];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* One-Command Zero-Touch: detect ecosystem and provision all integration
|
|
32
|
-
* channels (hooks, MCP, statusLine) with idempotency.
|
|
33
|
-
*/
|
|
34
|
-
function setupEcosystem(cwd: string, msg: MessageDict): SetupResult[] {
|
|
35
|
-
const ecosystems = detectEcosystem(cwd);
|
|
36
|
-
const results: SetupResult[] = [];
|
|
37
|
-
|
|
38
|
-
for (const { adapter } of ecosystems) {
|
|
39
|
-
console.log(t(msg.SETUP_HEADER, { ecosystem: adapter.name }));
|
|
40
|
-
const steps: SetupResult["steps"] = [];
|
|
41
|
-
|
|
42
|
-
// 1. Hook injection
|
|
43
|
-
if (adapter.injectHooks) {
|
|
44
|
-
const r = adapter.injectHooks(cwd);
|
|
45
|
-
const status = r.injected ? "new" : "ok";
|
|
46
|
-
console.log(status === "new" ? msg.SETUP_HOOKS_NEW : msg.SETUP_HOOKS_OK);
|
|
47
|
-
steps.push({ label: "hooks", status });
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// 2. MCP registration
|
|
51
|
-
if (adapter.registerMcp) {
|
|
52
|
-
const r = adapter.registerMcp(cwd);
|
|
53
|
-
const status = r.registered ? "new" : "ok";
|
|
54
|
-
console.log(status === "new" ? msg.SETUP_MCP_NEW : msg.SETUP_MCP_OK);
|
|
55
|
-
steps.push({ label: "mcp", status });
|
|
56
|
-
} else {
|
|
57
|
-
console.log(msg.SETUP_MCP_SKIP);
|
|
58
|
-
steps.push({ label: "mcp", status: "skip" });
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// 3. StatusLine configuration
|
|
62
|
-
if (adapter.configureStatusLine) {
|
|
63
|
-
const r = adapter.configureStatusLine(cwd);
|
|
64
|
-
const status = r.configured ? "new" : "ok";
|
|
65
|
-
console.log(status === "new" ? msg.SETUP_STATUS_NEW : msg.SETUP_STATUS_OK);
|
|
66
|
-
steps.push({ label: "statusLine", status });
|
|
67
|
-
} else {
|
|
68
|
-
console.log(msg.SETUP_STATUS_SKIP);
|
|
69
|
-
steps.push({ label: "statusLine", status: "skip" });
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
results.push({ ecosystem: adapter.name, steps });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (ecosystems.length > 0) {
|
|
76
|
-
console.log(msg.SETUP_DONE);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return results;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export async function startCommand(options?: { mcp?: boolean }) {
|
|
83
|
-
// MCP stdio mode: run daemon in foreground with stdio transport
|
|
84
|
-
if (options?.mcp) {
|
|
85
|
-
const { main: runDaemon } = await import("../daemon/server");
|
|
86
|
-
runDaemon({ mcp: true });
|
|
87
|
-
return; // never reaches here — stdio loop blocks
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const lang = getSystemLanguage();
|
|
91
|
-
const msg = getMessages(lang);
|
|
92
|
-
|
|
93
|
-
const paths = resolveWorkspacePaths();
|
|
94
|
-
mkdirSync(paths.afdDir, { recursive: true });
|
|
95
|
-
|
|
96
|
-
// ── Idempotency: check if already running ──
|
|
97
|
-
const existing = getDaemonInfo();
|
|
98
|
-
if (existing && (await isDaemonAlive(existing))) {
|
|
99
|
-
console.log(msg.DAEMON_ALREADY_RUNNING);
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ── Spawn detached daemon with log redirection ──
|
|
104
|
-
const daemonScript = resolve(import.meta.dirname, "../daemon/server.ts");
|
|
105
|
-
const logPath = paths.logFile;
|
|
106
|
-
rotateLogIfNeeded(logPath);
|
|
107
|
-
const logFd = openSync(logPath, "a"); // append mode
|
|
108
|
-
|
|
109
|
-
// On Windows, wrap in shell for proper detach; quote path for spaces
|
|
110
|
-
const args = IS_WINDOWS
|
|
111
|
-
? ["run", `"${daemonScript}"`]
|
|
112
|
-
: ["run", daemonScript];
|
|
113
|
-
|
|
114
|
-
const child = spawn("bun", args, detachedSpawnOptions(logFd));
|
|
115
|
-
|
|
116
|
-
// Detach: allow parent to exit without killing child
|
|
117
|
-
child.unref();
|
|
118
|
-
|
|
119
|
-
// ── Poll for daemon readiness instead of fixed sleep ──
|
|
120
|
-
const info = await pollForDaemon(STARTUP_POLL_MAX_MS, STARTUP_POLL_INTERVAL_MS);
|
|
121
|
-
|
|
122
|
-
if (info) {
|
|
123
|
-
console.log(t(msg.DAEMON_STARTED, { pid: info.pid, port: info.port }));
|
|
124
|
-
|
|
125
|
-
// Smart Discovery: show what we're actually watching
|
|
126
|
-
const discovery = discoverWatchTargets(WATCH_TARGETS);
|
|
127
|
-
console.log(t(msg.DAEMON_WATCHING, { count: discovery.targets.length }));
|
|
128
|
-
console.log(`[afd] Targets: ${discovery.targets.join(", ")}`);
|
|
129
|
-
console.log(t(msg.DAEMON_LOGS, { path: logPath }));
|
|
130
|
-
|
|
131
|
-
// One-Command Zero-Touch: auto-provision all ecosystem integrations
|
|
132
|
-
setupEcosystem(process.cwd(), msg);
|
|
133
|
-
} else {
|
|
134
|
-
console.error(t(msg.DAEMON_START_FAILED, { path: logPath }));
|
|
135
|
-
process.exit(1);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Poll until daemon PID/port files appear and health check passes */
|
|
140
|
-
async function pollForDaemon(
|
|
141
|
-
maxMs: number,
|
|
142
|
-
intervalMs: number,
|
|
143
|
-
): Promise<{ pid: number; port: number } | null> {
|
|
144
|
-
const deadline = Date.now() + maxMs;
|
|
145
|
-
|
|
146
|
-
while (Date.now() < deadline) {
|
|
147
|
-
const info = getDaemonInfo();
|
|
148
|
-
if (info && (await isDaemonAlive(info))) {
|
|
149
|
-
return info;
|
|
150
|
-
}
|
|
151
|
-
await Bun.sleep(intervalMs);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return null;
|
|
155
|
-
}
|
|
1
|
+
import { resolve } from "path";
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { openSync, mkdirSync } from "fs";
|
|
4
|
+
import { getDaemonInfo, isDaemonAlive } from "../daemon/client";
|
|
5
|
+
import { WATCH_TARGETS, resolveWorkspacePaths } from "../constants";
|
|
6
|
+
import { detectEcosystem } from "../adapters/index";
|
|
7
|
+
import type { DetectionResult } from "../adapters/index";
|
|
8
|
+
import { detachedSpawnOptions, IS_WINDOWS } from "../platform";
|
|
9
|
+
import { rotateLogIfNeeded } from "../core/log-rotate";
|
|
10
|
+
import { getSystemLanguage } from "../core/locale";
|
|
11
|
+
import { getMessages, t } from "../core/i18n/messages";
|
|
12
|
+
import type { MessageDict } from "../core/i18n/messages";
|
|
13
|
+
import { discoverWatchTargets } from "../core/discovery";
|
|
14
|
+
|
|
15
|
+
const STARTUP_POLL_INTERVAL_MS = 100;
|
|
16
|
+
const STARTUP_POLL_MAX_MS = 3000;
|
|
17
|
+
|
|
18
|
+
interface SetupStep {
|
|
19
|
+
label: string;
|
|
20
|
+
newMsg: string;
|
|
21
|
+
okMsg: string;
|
|
22
|
+
skipMsg: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SetupResult {
|
|
26
|
+
ecosystem: string;
|
|
27
|
+
steps: { label: string; status: "new" | "ok" | "skip" }[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* One-Command Zero-Touch: detect ecosystem and provision all integration
|
|
32
|
+
* channels (hooks, MCP, statusLine) with idempotency.
|
|
33
|
+
*/
|
|
34
|
+
function setupEcosystem(cwd: string, msg: MessageDict): SetupResult[] {
|
|
35
|
+
const ecosystems = detectEcosystem(cwd);
|
|
36
|
+
const results: SetupResult[] = [];
|
|
37
|
+
|
|
38
|
+
for (const { adapter } of ecosystems) {
|
|
39
|
+
console.log(t(msg.SETUP_HEADER, { ecosystem: adapter.name }));
|
|
40
|
+
const steps: SetupResult["steps"] = [];
|
|
41
|
+
|
|
42
|
+
// 1. Hook injection
|
|
43
|
+
if (adapter.injectHooks) {
|
|
44
|
+
const r = adapter.injectHooks(cwd);
|
|
45
|
+
const status = r.injected ? "new" : "ok";
|
|
46
|
+
console.log(status === "new" ? msg.SETUP_HOOKS_NEW : msg.SETUP_HOOKS_OK);
|
|
47
|
+
steps.push({ label: "hooks", status });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. MCP registration
|
|
51
|
+
if (adapter.registerMcp) {
|
|
52
|
+
const r = adapter.registerMcp(cwd);
|
|
53
|
+
const status = r.registered ? "new" : "ok";
|
|
54
|
+
console.log(status === "new" ? msg.SETUP_MCP_NEW : msg.SETUP_MCP_OK);
|
|
55
|
+
steps.push({ label: "mcp", status });
|
|
56
|
+
} else {
|
|
57
|
+
console.log(msg.SETUP_MCP_SKIP);
|
|
58
|
+
steps.push({ label: "mcp", status: "skip" });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. StatusLine configuration
|
|
62
|
+
if (adapter.configureStatusLine) {
|
|
63
|
+
const r = adapter.configureStatusLine(cwd);
|
|
64
|
+
const status = r.configured ? "new" : "ok";
|
|
65
|
+
console.log(status === "new" ? msg.SETUP_STATUS_NEW : msg.SETUP_STATUS_OK);
|
|
66
|
+
steps.push({ label: "statusLine", status });
|
|
67
|
+
} else {
|
|
68
|
+
console.log(msg.SETUP_STATUS_SKIP);
|
|
69
|
+
steps.push({ label: "statusLine", status: "skip" });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
results.push({ ecosystem: adapter.name, steps });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (ecosystems.length > 0) {
|
|
76
|
+
console.log(msg.SETUP_DONE);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function startCommand(options?: { mcp?: boolean }) {
|
|
83
|
+
// MCP stdio mode: run daemon in foreground with stdio transport
|
|
84
|
+
if (options?.mcp) {
|
|
85
|
+
const { main: runDaemon } = await import("../daemon/server");
|
|
86
|
+
runDaemon({ mcp: true });
|
|
87
|
+
return; // never reaches here — stdio loop blocks
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const lang = getSystemLanguage();
|
|
91
|
+
const msg = getMessages(lang);
|
|
92
|
+
|
|
93
|
+
const paths = resolveWorkspacePaths();
|
|
94
|
+
mkdirSync(paths.afdDir, { recursive: true });
|
|
95
|
+
|
|
96
|
+
// ── Idempotency: check if already running ──
|
|
97
|
+
const existing = getDaemonInfo();
|
|
98
|
+
if (existing && (await isDaemonAlive(existing))) {
|
|
99
|
+
console.log(msg.DAEMON_ALREADY_RUNNING);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Spawn detached daemon with log redirection ──
|
|
104
|
+
const daemonScript = resolve(import.meta.dirname, "../daemon/server.ts");
|
|
105
|
+
const logPath = paths.logFile;
|
|
106
|
+
rotateLogIfNeeded(logPath);
|
|
107
|
+
const logFd = openSync(logPath, "a"); // append mode
|
|
108
|
+
|
|
109
|
+
// On Windows, wrap in shell for proper detach; quote path for spaces
|
|
110
|
+
const args = IS_WINDOWS
|
|
111
|
+
? ["run", `"${daemonScript}"`]
|
|
112
|
+
: ["run", daemonScript];
|
|
113
|
+
|
|
114
|
+
const child = spawn("bun", args, detachedSpawnOptions(logFd));
|
|
115
|
+
|
|
116
|
+
// Detach: allow parent to exit without killing child
|
|
117
|
+
child.unref();
|
|
118
|
+
|
|
119
|
+
// ── Poll for daemon readiness instead of fixed sleep ──
|
|
120
|
+
const info = await pollForDaemon(STARTUP_POLL_MAX_MS, STARTUP_POLL_INTERVAL_MS);
|
|
121
|
+
|
|
122
|
+
if (info) {
|
|
123
|
+
console.log(t(msg.DAEMON_STARTED, { pid: info.pid, port: info.port }));
|
|
124
|
+
|
|
125
|
+
// Smart Discovery: show what we're actually watching
|
|
126
|
+
const discovery = discoverWatchTargets(WATCH_TARGETS);
|
|
127
|
+
console.log(t(msg.DAEMON_WATCHING, { count: discovery.targets.length }));
|
|
128
|
+
console.log(`[afd] Targets: ${discovery.targets.join(", ")}`);
|
|
129
|
+
console.log(t(msg.DAEMON_LOGS, { path: logPath }));
|
|
130
|
+
|
|
131
|
+
// One-Command Zero-Touch: auto-provision all ecosystem integrations
|
|
132
|
+
setupEcosystem(process.cwd(), msg);
|
|
133
|
+
} else {
|
|
134
|
+
console.error(t(msg.DAEMON_START_FAILED, { path: logPath }));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Poll until daemon PID/port files appear and health check passes */
|
|
140
|
+
async function pollForDaemon(
|
|
141
|
+
maxMs: number,
|
|
142
|
+
intervalMs: number,
|
|
143
|
+
): Promise<{ pid: number; port: number } | null> {
|
|
144
|
+
const deadline = Date.now() + maxMs;
|
|
145
|
+
|
|
146
|
+
while (Date.now() < deadline) {
|
|
147
|
+
const info = getDaemonInfo();
|
|
148
|
+
if (info && (await isDaemonAlive(info))) {
|
|
149
|
+
return info;
|
|
150
|
+
}
|
|
151
|
+
await Bun.sleep(intervalMs);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return null;
|
|
155
|
+
}
|
package/src/commands/status.ts
CHANGED
|
@@ -1,157 +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
|
-
}
|
|
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
|
+
}
|