@actiondock/core 2.0.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.
Files changed (43) hide show
  1. package/README.md +50 -0
  2. package/package.json +51 -0
  3. package/src/build/builder.ts +205 -0
  4. package/src/build/index.ts +2 -0
  5. package/src/build/templates.ts +59 -0
  6. package/src/doctor/doctor.ts +332 -0
  7. package/src/doctor/index.ts +2 -0
  8. package/src/doctor/types.ts +25 -0
  9. package/src/export/index.ts +2 -0
  10. package/src/export/skill.ts +349 -0
  11. package/src/export/templates.ts +258 -0
  12. package/src/filter/index.ts +1 -0
  13. package/src/filter/intent.ts +154 -0
  14. package/src/index.ts +13 -0
  15. package/src/profile/client.ts +302 -0
  16. package/src/profile/index.ts +3 -0
  17. package/src/profile/manager.ts +341 -0
  18. package/src/profile/types.ts +71 -0
  19. package/src/project/index.ts +3 -0
  20. package/src/project/init.ts +194 -0
  21. package/src/project/loader.ts +382 -0
  22. package/src/project/types.ts +62 -0
  23. package/src/registry/index.ts +2 -0
  24. package/src/registry/registry.ts +703 -0
  25. package/src/registry/types.ts +127 -0
  26. package/src/runtime/context.ts +232 -0
  27. package/src/runtime/env.ts +172 -0
  28. package/src/runtime/execution-manager.ts +74 -0
  29. package/src/runtime/index.ts +5 -0
  30. package/src/runtime/runner.ts +368 -0
  31. package/src/runtime/standalone.ts +429 -0
  32. package/src/schema/validator.ts +61 -0
  33. package/src/server/body.ts +112 -0
  34. package/src/server/index.ts +6 -0
  35. package/src/server/runtime-registry.ts +80 -0
  36. package/src/server/security.ts +115 -0
  37. package/src/server/server.ts +572 -0
  38. package/src/server/types.ts +42 -0
  39. package/src/storage/index.ts +64 -0
  40. package/src/storage/mask.ts +34 -0
  41. package/src/storage/sqlite.ts +578 -0
  42. package/src/storage/types.ts +111 -0
  43. package/src/utils/index.ts +60 -0
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @actiondock/core
2
+
3
+ The core engine and domain kernel of ActionDock 2.0.
4
+
5
+ [![Bun](https://img.shields.io/badge/Bun-%3E%3D1.2-black?logo=bun)](https://bun.sh/)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue?logo=typescript)](https://www.typescriptlang.org/)
7
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
8
+
9
+ `@actiondock/core` provides the project loader, runtime execution engine (`ActionRunner`), native SQLite persistence, standalone binary compiler, remote HTTP server/client, and Agent Skill exporter.
10
+
11
+ > **Role & Usage Context**:
12
+ > - **Authoring Actions**: Use [`@actiondock/sdk`](../sdk) for defining actions, testing with in-memory harness, and zero-dependency action packages.
13
+ > - **CLI Toolchain**: Use [`@actiondock/cli`](../cli) (`ac`) for command-line workflows.
14
+ > - **Engine & Embedding**: Use `@actiondock/core` when you need programmatic access to the ActionRunner engine, project loader, or custom server integrations.
15
+ >
16
+ > **Runtime requirement**: [Bun](https://bun.sh/) >= 1.2.0 is required (`@actiondock/core` leverages native `bun:sqlite` and Bun runtime APIs).
17
+
18
+ ---
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ bun add @actiondock/core
24
+ ```
25
+
26
+ ---
27
+
28
+ ## Key Modules
29
+
30
+ - **Project Loader** (`loadProjectConfig`, `loadActions`, `loadPlaybooks`, `initProject`): Discovers and parses Action Packages.
31
+ - **Runtime Execution** (`ActionRunner`, `createActionContext`, `createStandaloneRuntime`): Manages the full action execution lifecycle, schema validation, cycle detection, timeouts, and JSON envelopes.
32
+ - **Storage** (`SqliteRuntimeStorage`, `createStorage`, `createGlobalStorage`): Native SQLite KV store with namespaces, TTL, and WAL mode.
33
+ - **Standalone Builder** (`buildProject`): Uses Bun's native bundler to compile actions into a single standalone binary.
34
+ - **Skill Exporter** (`exportSkill`): Generates self-contained Agent Skills with Playbook SOPs.
35
+ - **Profile & Remote Runner** (`ProfileManager`, `ActionServer`, `executeRemoteAction`): Multi-cloud remote execution with token authentication.
36
+
37
+ ---
38
+
39
+ ## 📖 Documentation
40
+
41
+ - [Runtime Architecture](https://github.com/team4u/actiondock/blob/main/docs/architecture/runtime.md)
42
+ - [Storage & Persistence Guide](https://github.com/team4u/actiondock/blob/main/docs/guides/storage.md)
43
+ - [Standalone Binary Build](https://github.com/team4u/actiondock/blob/main/docs/guides/standalone-build.md)
44
+ - [HTTP Server & Remote Dispatch](https://github.com/team4u/actiondock/blob/main/docs/guides/http-server.md)
45
+
46
+ ---
47
+
48
+ ## License
49
+
50
+ [Apache-2.0](LICENSE) © team4u
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@actiondock/core",
3
+ "version": "2.0.0",
4
+ "description": "ActionDock Core Engine - Project loader, runtime execution, SQLite storage, standalone builder, and skill exporter",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "module": "./src/index.ts",
8
+ "types": "./src/index.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./src/index.ts",
12
+ "types": "./src/index.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "src",
17
+ "README.md"
18
+ ],
19
+ "engines": {
20
+ "bun": ">=1.2.0"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public",
24
+ "registry": "https://registry.npmjs.org/"
25
+ },
26
+ "scripts": {
27
+ "test": "bun test"
28
+ },
29
+ "dependencies": {
30
+ "@actiondock/sdk": "^2.0.0",
31
+ "ajv": "^8.17.1",
32
+ "ajv-formats": "^3.0.1",
33
+ "yaml": "^2.7.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/bun": "latest",
37
+ "typescript": "^5.7.0"
38
+ },
39
+ "keywords": ["actiondock", "core", "runtime", "storage", "builder"],
40
+ "author": "team4u",
41
+ "license": "Apache-2.0",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/team4u/actiondock.git",
45
+ "directory": "packages/core"
46
+ },
47
+ "homepage": "https://github.com/team4u/actiondock#readme",
48
+ "bugs": {
49
+ "url": "https://github.com/team4u/actiondock/issues"
50
+ }
51
+ }
@@ -0,0 +1,205 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { loadActionFileMap, loadActions, loadProjectConfig } from "../project/loader";
5
+ import type { ProjectConfig } from "../project/types";
6
+ import { getPackageSlug } from "../utils";
7
+ import { type ActionImport, generateStandaloneEntrypoint } from "./templates";
8
+
9
+ /**
10
+ * 独立二进制可执行文件构建选项。
11
+ */
12
+ export interface BuildOptions {
13
+ /** 目标项目根目录 */
14
+ projectRoot: string;
15
+ /** 目标架构(如 "bun-linux-x64", "bun-darwin-arm64", "bun-windows-x64" 等) */
16
+ target?: string;
17
+ /** 输出可执行文件的目标路径(默认输出到 dist/ 目录) */
18
+ outfile?: string;
19
+ /** 是否开启代码压缩混淆(默认 true) */
20
+ minify?: boolean;
21
+ /** 是否编译为 V8/JavaScriptCore 字节码(默认 false) */
22
+ bytecode?: boolean;
23
+ /** 显式挑选打包的 Action ID 清单(用于按需子集打包) */
24
+ actions?: string[];
25
+ }
26
+
27
+ /**
28
+ * 独立二进制构建完成后的元数据结果对象。
29
+ */
30
+ export interface BuildResult {
31
+ /** 所属 Package ID */
32
+ packageId: string;
33
+ /** 打包的项目版本号 */
34
+ version: string;
35
+ /** 编译的目标平台架构 */
36
+ target: string;
37
+ /** 生成的独立二进制可执行文件绝对路径 */
38
+ executablePath: string;
39
+ /** 生成的 sidecar 元数据 JSON 文件绝对路径 */
40
+ metadataPath: string;
41
+ /** 打包内置的 Action ID 列表 */
42
+ actions: string[];
43
+ }
44
+
45
+ /**
46
+ * 调用 Bun 原生编译引擎(Bun.build --compile)将 Action Package 打包为零外部依赖的独立二进制可执行文件。
47
+ *
48
+ * 构建过程:
49
+ * 1. 动态生成 Standalone 入口点代码(包含 StandaloneRuntime 与 Action 注册)。
50
+ * 2. 生成 sidecar metadata 文件(.actiondock-meta.json),包含 Package 元数据与 sha256 校验和。
51
+ * 3. 执行 Bun.build({ compile: true, target, minify, bytecode })。
52
+ *
53
+ * @param options 构建参数
54
+ * @returns 构建产物结果元数据
55
+ */
56
+ export async function buildProject(options: BuildOptions): Promise<BuildResult> {
57
+ const root = resolve(options.projectRoot);
58
+ const config = loadProjectConfig(root);
59
+ const actionsMap = await loadActions(root, config.actionsDir);
60
+
61
+ if (actionsMap.size === 0) {
62
+ throw new Error(`No valid actions found in ${join(root, config.actionsDir || "actions")}`);
63
+ }
64
+
65
+ if (options.actions && options.actions.length > 0) {
66
+ const requestedActions = new Set(options.actions);
67
+ for (const reqId of requestedActions) {
68
+ if (!actionsMap.has(reqId)) {
69
+ throw new Error(`Action '${reqId}' requested in build options not found in project`);
70
+ }
71
+ }
72
+ for (const id of Array.from(actionsMap.keys())) {
73
+ if (!requestedActions.has(id)) {
74
+ actionsMap.delete(id);
75
+ }
76
+ }
77
+ }
78
+
79
+ // Action imports list
80
+ const actionFileMap = await loadActionFileMap(root, config.actionsDir);
81
+ const actionImports: ActionImport[] = [];
82
+
83
+ for (const [id, entry] of actionFileMap.entries()) {
84
+ if (actionsMap.has(id)) {
85
+ actionImports.push({
86
+ id,
87
+ filePath: entry.filePath,
88
+ });
89
+ }
90
+ }
91
+
92
+ if (actionImports.length === 0) {
93
+ throw new Error("Could not map any action files for build");
94
+ }
95
+
96
+ // Create build dir
97
+ const buildDir = join(root, ".actiondock", ".build");
98
+ mkdirSync(buildDir, { recursive: true });
99
+
100
+ const entryCode = generateStandaloneEntrypoint(
101
+ config.id,
102
+ config.version,
103
+ config.description,
104
+ actionImports,
105
+ config.config
106
+ );
107
+ const entryPath = join(buildDir, "entry.ts");
108
+ writeFileSync(entryPath, entryCode, "utf-8");
109
+
110
+ // Determine target and outfile
111
+ const target = options.target || "bun";
112
+ const binaryName = getPackageSlug(config.id);
113
+
114
+ const defaultOutfile = join(root, "dist", binaryName);
115
+ const outfile = resolve(options.outfile || defaultOutfile);
116
+
117
+ mkdirSync(dirname(outfile), { recursive: true });
118
+
119
+ // Run bun build --compile --bytecode --minify
120
+ const buildArgs = [
121
+ "bun",
122
+ "build",
123
+ entryPath,
124
+ "--compile",
125
+ "--outfile",
126
+ outfile,
127
+ ];
128
+
129
+ if (options.bytecode !== false) {
130
+ buildArgs.push("--bytecode");
131
+ }
132
+
133
+ if (options.minify !== false) {
134
+ buildArgs.push("--minify");
135
+ }
136
+
137
+ if (options.target && options.target !== "bun" && options.target !== "host") {
138
+ // e.g. bun-linux-x64 or linux-x64
139
+ const formattedTarget = options.target.startsWith("bun-")
140
+ ? options.target
141
+ : `bun-${options.target}`;
142
+ buildArgs.push(`--target=${formattedTarget}`);
143
+ }
144
+
145
+ const proc = Bun.spawnSync(buildArgs, {
146
+ cwd: root,
147
+ stdout: "pipe",
148
+ stderr: "pipe",
149
+ });
150
+
151
+ if (proc.exitCode !== 0) {
152
+ const errText = proc.stderr.toString() || proc.stdout.toString();
153
+ throw new Error(`Bun compile failed (exit code ${proc.exitCode}):\n${errText}`);
154
+ }
155
+
156
+ // Compile artifact resolution (on Windows bun compile automatically appends .exe)
157
+ let artifactPath = outfile;
158
+ if (!existsSync(artifactPath)) {
159
+ const withExe = outfile + ".exe";
160
+ if (existsSync(withExe)) {
161
+ artifactPath = withExe;
162
+ }
163
+ }
164
+
165
+ // Calculate build hash
166
+ const binaryBuffer = readFileSync(artifactPath);
167
+ const buildHash = createHash("sha256").update(binaryBuffer).digest("hex").slice(0, 16);
168
+
169
+ // Calculate lockHash
170
+ let lockHash = "none";
171
+ const lockFiles = ["bun.lock", "bun.lockb", "package.json"];
172
+ for (const lf of lockFiles) {
173
+ const p = join(root, lf);
174
+ if (existsSync(p)) {
175
+ lockHash = createHash("sha256").update(readFileSync(p)).digest("hex").slice(0, 16);
176
+ break;
177
+ }
178
+ }
179
+
180
+ // Generate artifact.json metadata
181
+ const metadata = {
182
+ packageId: config.id,
183
+ name: config.name,
184
+ version: config.version,
185
+ description: config.description,
186
+ target: options.target || "host",
187
+ actions: actionImports.map((a) => a.id),
188
+ bunVersion: Bun.version,
189
+ lockHash,
190
+ buildHash,
191
+ createdAt: new Date().toISOString(),
192
+ };
193
+
194
+ const metadataPath = join(dirname(artifactPath), "artifact.json");
195
+ writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + "\n", "utf-8");
196
+
197
+ return {
198
+ packageId: config.id,
199
+ version: config.version,
200
+ target: options.target || "host",
201
+ executablePath: artifactPath,
202
+ metadataPath,
203
+ actions: metadata.actions,
204
+ };
205
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./builder";
2
+ export * from "./templates";
@@ -0,0 +1,59 @@
1
+ import { resolve } from "node:path";
2
+
3
+ /**
4
+ * 构建时 Action 导入映射。
5
+ */
6
+ export interface ActionImport {
7
+ /** Action 唯一标识符 */
8
+ id: string;
9
+ /** Action 源码文件物理路径 */
10
+ filePath: string;
11
+ }
12
+
13
+ /**
14
+ * 动态生成 Standalone 独立可执行文件的 TypeScript 入口文件源码。
15
+ *
16
+ * @param packageId 所属 Package ID
17
+ * @param version 版本号
18
+ * @param description 描述
19
+ * @param actions 待打包 Action 列表
20
+ * @param configDefs 声明的配置定义字典
21
+ */
22
+ export function generateStandaloneEntrypoint(
23
+ packageId: string,
24
+ version: string,
25
+ description: string | undefined,
26
+ actions: ActionImport[],
27
+ configDefs?: Record<string, unknown>
28
+ ): string {
29
+ // Resolve path to standalone runtime inside @actiondock/cli
30
+ const standaloneRuntimePath = resolve(__dirname, "../runtime/standalone");
31
+
32
+ const imports = actions
33
+ .map((a, idx) => `import action_${idx} from ${JSON.stringify(a.filePath)};`)
34
+ .join("\n");
35
+
36
+ const actionArray = actions
37
+ .map((_, idx) => `action_${idx}`)
38
+ .join(",\n ");
39
+
40
+ return `// AUTO-GENERATED ENTRYPOINT BY ACTIONDOCK BUILDER. DO NOT EDIT.
41
+ import { createStandaloneRuntime } from ${JSON.stringify(standaloneRuntimePath)};
42
+ ${imports}
43
+
44
+ const app = createStandaloneRuntime({
45
+ packageId: ${JSON.stringify(packageId)},
46
+ version: ${JSON.stringify(version)},
47
+ description: ${JSON.stringify(description || "")},
48
+ config: ${JSON.stringify(configDefs || {})},
49
+ actions: [
50
+ ${actionArray}
51
+ ],
52
+ });
53
+
54
+ app.run(process.argv.slice(2)).catch((err: unknown) => {
55
+ console.error(err);
56
+ process.exit(1);
57
+ });
58
+ `;
59
+ }
@@ -0,0 +1,332 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { findProjectRoot, loadActions, loadPlaybooks, loadProjectConfig } from "../project/loader";
4
+ import { getRegistryStatus } from "../registry/registry";
5
+ import { createGlobalStorage, createStorage } from "../storage";
6
+ import { getActionDockHome } from "../utils";
7
+ import type { DoctorCheckItem, DoctorReport } from "./types";
8
+
9
+ function compareSemver(v1: string, v2: string): number {
10
+ const p1 = v1.replace(/^v/, "").split(".").map(Number);
11
+ const p2 = v2.replace(/^v/, "").split(".").map(Number);
12
+ for (let i = 0; i < Math.max(p1.length, p2.length); i++) {
13
+ const num1 = p1[i] || 0;
14
+ const num2 = p2[i] || 0;
15
+ if (num1 > num2) return 1;
16
+ if (num1 < num2) return -1;
17
+ }
18
+ return 0;
19
+ }
20
+
21
+ export async function runDoctorChecks(options?: {
22
+ cwd?: string;
23
+ packageIdOrPath?: string;
24
+ customHome?: string;
25
+ }): Promise<DoctorReport> {
26
+ const cwd = options?.cwd || process.cwd();
27
+ const checks: DoctorCheckItem[] = [];
28
+
29
+ // 1. Check Bun Runtime
30
+ const bunVersion = (typeof Bun !== "undefined" && Bun.version) || (process.versions as any).bun;
31
+ if (bunVersion) {
32
+ const isGte12 = compareSemver(bunVersion, "1.2.0") >= 0;
33
+ if (isGte12) {
34
+ checks.push({
35
+ id: "runtime.bun",
36
+ category: "runtime",
37
+ name: "Bun Runtime",
38
+ status: "ok",
39
+ message: `v${bunVersion} (>= 1.2.0 required)`,
40
+ });
41
+ } else {
42
+ checks.push({
43
+ id: "runtime.bun",
44
+ category: "runtime",
45
+ name: "Bun Runtime",
46
+ status: "error",
47
+ message: `v${bunVersion} is too old (>= 1.2.0 required)`,
48
+ fix: "Run 'bun upgrade' to update Bun",
49
+ });
50
+ }
51
+ } else {
52
+ checks.push({
53
+ id: "runtime.bun",
54
+ category: "runtime",
55
+ name: "Bun Runtime",
56
+ status: "error",
57
+ message: "Bun runtime not detected",
58
+ fix: "Install Bun via 'npm install bun -g'",
59
+ });
60
+ }
61
+
62
+ // 2. Check CLI in PATH
63
+ let acPath: string | null = null;
64
+ try {
65
+ acPath = typeof Bun !== "undefined" && Bun.which ? Bun.which("ac") : null;
66
+ } catch {
67
+ // ignore
68
+ }
69
+
70
+ if (acPath) {
71
+ checks.push({
72
+ id: "runtime.cli",
73
+ category: "runtime",
74
+ name: "CLI Executable",
75
+ status: "ok",
76
+ message: `Found 'ac' in PATH at ${acPath}`,
77
+ });
78
+ } else {
79
+ checks.push({
80
+ id: "runtime.cli",
81
+ category: "runtime",
82
+ name: "CLI Executable",
83
+ status: "warn",
84
+ message: "'ac' command not found in PATH",
85
+ fix: "Run 'bun add -g @actiondock/cli' or in SDK workspace run 'cd packages/cli && bun link'",
86
+ });
87
+ }
88
+
89
+ // 3. Check Global Storage
90
+ const globalHome = getActionDockHome(options?.customHome);
91
+ try {
92
+ const globalStorage = createGlobalStorage(options?.customHome);
93
+ await globalStorage.setConfig("_doctor_probe_", "ok");
94
+ await globalStorage.deleteConfig("_doctor_probe_");
95
+ globalStorage.close();
96
+
97
+ checks.push({
98
+ id: "storage.global",
99
+ category: "storage",
100
+ name: "Global Storage",
101
+ status: "ok",
102
+ message: `Database writable at ${join(globalHome, ".actiondock", "global.db")}`,
103
+ });
104
+ } catch (err: any) {
105
+ checks.push({
106
+ id: "storage.global",
107
+ category: "storage",
108
+ name: "Global Storage",
109
+ status: "error",
110
+ message: `Failed to write global database: ${err.message}`,
111
+ fix: `Check write permissions for directory '${join(globalHome, ".actiondock")}'`,
112
+ });
113
+ }
114
+
115
+ // 4. Check Global Registry Health
116
+ try {
117
+ const regStatus = getRegistryStatus(options?.customHome);
118
+ if (regStatus.staleCount > 0) {
119
+ checks.push({
120
+ id: "registry.global",
121
+ category: "registry",
122
+ name: "Global Registry",
123
+ status: "warn",
124
+ message: `${regStatus.totalPackagesCount} package(s), ${regStatus.workspaces.length} workspace(s), but ${regStatus.staleCount} stale path(s) detected`,
125
+ fix: "Run 'ac unlink --prune' to clean up stale entries from registry",
126
+ });
127
+ } else {
128
+ checks.push({
129
+ id: "registry.global",
130
+ category: "registry",
131
+ name: "Global Registry",
132
+ status: "ok",
133
+ message: `${regStatus.totalPackagesCount} linked package(s), ${regStatus.workspaces.length} workspace(s) (0 stale)`,
134
+ });
135
+ }
136
+ } catch (err: any) {
137
+ checks.push({
138
+ id: "registry.global",
139
+ category: "registry",
140
+ name: "Global Registry",
141
+ status: "error",
142
+ message: `Failed to read registry: ${err.message}`,
143
+ });
144
+ }
145
+
146
+ // 5. Check Project Context
147
+ let projectRoot: string | null = null;
148
+ if (options?.packageIdOrPath) {
149
+ projectRoot = findProjectRoot(options.packageIdOrPath);
150
+ } else {
151
+ projectRoot = findProjectRoot(cwd);
152
+ }
153
+
154
+ let packageId: string | undefined;
155
+
156
+ if (projectRoot) {
157
+ try {
158
+ const config = loadProjectConfig(projectRoot);
159
+ packageId = config.id;
160
+
161
+ // Project Config Check
162
+ checks.push({
163
+ id: "project.config",
164
+ category: "project",
165
+ name: "Project Configuration",
166
+ status: "ok",
167
+ message: `Valid (${config.id} v${config.version})`,
168
+ });
169
+
170
+ // SDK Resolution Check
171
+ const hasSdkInNodeModules =
172
+ existsSync(join(projectRoot, "node_modules", "@actiondock", "sdk")) ||
173
+ existsSync(join(projectRoot, "node_modules", "@actiondock", "sdk", "package.json"));
174
+
175
+ if (hasSdkInNodeModules) {
176
+ checks.push({
177
+ id: "project.sdk",
178
+ category: "project",
179
+ name: "SDK Dependency",
180
+ status: "ok",
181
+ message: "Resolved @actiondock/sdk in node_modules",
182
+ });
183
+ } else {
184
+ checks.push({
185
+ id: "project.sdk",
186
+ category: "project",
187
+ name: "SDK Dependency",
188
+ status: "warn",
189
+ message: "@actiondock/sdk not found in project node_modules",
190
+ fix: "Run 'bun link @actiondock/sdk' or 'bun install' in project directory",
191
+ });
192
+ }
193
+
194
+ // Project Runtime Database Check
195
+ try {
196
+ const projectStorage = createStorage(config.id, { projectRoot });
197
+ await projectStorage.setConfig("_doctor_probe_", "ok");
198
+ await projectStorage.deleteConfig("_doctor_probe_");
199
+ projectStorage.close();
200
+
201
+ checks.push({
202
+ id: "project.storage",
203
+ category: "project",
204
+ name: "Project Database",
205
+ status: "ok",
206
+ message: `Database writable at ${join(projectRoot, ".actiondock", "runtime.db")}`,
207
+ });
208
+ } catch (err: any) {
209
+ checks.push({
210
+ id: "project.storage",
211
+ category: "project",
212
+ name: "Project Database",
213
+ status: "error",
214
+ message: `Failed to write project runtime database: ${err.message}`,
215
+ fix: `Check write permissions for '${join(projectRoot, ".actiondock")}'`,
216
+ });
217
+ }
218
+
219
+ // Actions Check
220
+ try {
221
+ const actions = await loadActions(projectRoot, config.actionsDir);
222
+ if (actions.size === 0) {
223
+ checks.push({
224
+ id: "project.actions",
225
+ category: "project",
226
+ name: "Actions",
227
+ status: "warn",
228
+ message: `No actions found in '${config.actionsDir || "actions"}'`,
229
+ fix: "Run 'ac action create <id>' to create your first action",
230
+ });
231
+ } else {
232
+ checks.push({
233
+ id: "project.actions",
234
+ category: "project",
235
+ name: "Actions",
236
+ status: "ok",
237
+ message: `${actions.size} action(s) valid and loaded`,
238
+ });
239
+ }
240
+ } catch (err: any) {
241
+ checks.push({
242
+ id: "project.actions",
243
+ category: "project",
244
+ name: "Actions",
245
+ status: "error",
246
+ message: `Failed to load actions: ${err.message}`,
247
+ });
248
+ }
249
+
250
+ // Playbooks Check
251
+ try {
252
+ const playbooks = loadPlaybooks(projectRoot, config.playbooksDir);
253
+ checks.push({
254
+ id: "project.playbooks",
255
+ category: "project",
256
+ name: "Playbooks",
257
+ status: "ok",
258
+ message: `${playbooks.size} playbook(s) valid`,
259
+ });
260
+ } catch (err: any) {
261
+ checks.push({
262
+ id: "project.playbooks",
263
+ category: "project",
264
+ name: "Playbooks",
265
+ status: "warn",
266
+ message: `Playbooks issue: ${err.message}`,
267
+ });
268
+ }
269
+
270
+ // Config readiness check
271
+ if (config.config && Object.keys(config.config).length > 0) {
272
+ const missingKeys: string[] = [];
273
+ const projectStorage = createStorage(config.id, { projectRoot });
274
+ for (const [key, def] of Object.entries(config.config)) {
275
+ const inStorage = await projectStorage.getConfig(key);
276
+ const envNames = Array.isArray(def.env) ? def.env : def.env ? [def.env] : [key];
277
+ const inEnv = envNames.some((e) => process.env[e] !== undefined);
278
+ const isRequired = (def as any).required || (def.default === undefined && def.secret);
279
+ if ((isRequired || def.default === undefined) && inStorage === undefined && !inEnv) {
280
+ missingKeys.push(key);
281
+ }
282
+ }
283
+ projectStorage.close();
284
+
285
+ if (missingKeys.length > 0) {
286
+ checks.push({
287
+ id: "project.config_readiness",
288
+ category: "project",
289
+ name: "Config Readiness",
290
+ status: "warn",
291
+ message: `Required config item(s) missing: ${missingKeys.join(", ")}`,
292
+ fix: `Run 'ac config set <KEY> <VALUE>' to configure missing keys`,
293
+ });
294
+ } else {
295
+ checks.push({
296
+ id: "project.config_readiness",
297
+ category: "project",
298
+ name: "Config Readiness",
299
+ status: "ok",
300
+ message: "All declared configuration dependencies satisfied",
301
+ });
302
+ }
303
+ }
304
+ } catch (err: any) {
305
+ checks.push({
306
+ id: "project.config",
307
+ category: "project",
308
+ name: "Project Configuration",
309
+ status: "error",
310
+ message: `Invalid actiondock.json: ${err.message}`,
311
+ });
312
+ }
313
+ }
314
+
315
+ const okCount = checks.filter((c) => c.status === "ok").length;
316
+ const warnCount = checks.filter((c) => c.status === "warn").length;
317
+ const errorCount = checks.filter((c) => c.status === "error").length;
318
+
319
+ return {
320
+ ok: errorCount === 0,
321
+ hasProject: !!projectRoot,
322
+ projectRoot: projectRoot || undefined,
323
+ packageId,
324
+ summary: {
325
+ total: checks.length,
326
+ ok: okCount,
327
+ warn: warnCount,
328
+ error: errorCount,
329
+ },
330
+ checks,
331
+ };
332
+ }