@actiondock/core 2.0.9 → 2.0.11-beta.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/package.json +2 -2
- package/src/build/index.ts +0 -1
- package/src/build/templates.ts +1 -1
- package/src/catalog/types.ts +1 -9
- package/src/doctor/doctor.ts +99 -38
- package/src/execution/service.ts +47 -7
- package/src/export/index.ts +0 -1
- package/src/export/templates.ts +348 -59
- package/src/index.ts +1 -0
- package/src/project/init.ts +3 -2
- package/src/project/loader.ts +46 -7
- package/src/project/manifest.ts +205 -2
- package/src/project/types.ts +49 -0
- package/src/registry/registry.ts +271 -34
- package/src/runtime/context.ts +8 -3
- package/src/runtime/index.ts +1 -0
- package/src/runtime/module-loader.ts +67 -0
- package/src/runtime/process.ts +12 -11
- package/src/runtime/runner.ts +193 -68
- package/src/server/index.ts +1 -0
- package/src/server/routes/actions.ts +262 -0
- package/src/server/routes/common.ts +126 -0
- package/src/server/routes/config.ts +147 -0
- package/src/server/routes/doctor.ts +29 -0
- package/src/server/routes/health.ts +40 -0
- package/src/server/routes/index.ts +9 -0
- package/src/server/routes/info.ts +221 -0
- package/src/server/routes/playbooks.ts +109 -0
- package/src/server/routes/runs.ts +301 -0
- package/src/server/routes/state.ts +133 -0
- package/src/server/server.ts +82 -1323
- package/src/storage/index.ts +20 -4
- package/src/storage/sqlite.ts +60 -31
- package/src/storage/types.ts +8 -0
- package/src/utils/index.ts +2 -0
- package/src/build/builder.ts +0 -218
- package/src/export/skill.ts +0 -350
package/src/storage/index.ts
CHANGED
|
@@ -55,11 +55,27 @@ export function createStorage(
|
|
|
55
55
|
* 工厂函数:创建或连接 ActionDock 全局共享数据库(~/.actiondock/global.db)。
|
|
56
56
|
* 用于跨 Package 共享的全局配置项存储。
|
|
57
57
|
*
|
|
58
|
-
* @param
|
|
58
|
+
* @param customHomeOrOptions 自定义家目录路径或配置对象(可选)
|
|
59
|
+
* @param dataDirArg 自定义数据存储目录(可选)
|
|
59
60
|
*/
|
|
60
|
-
export function createGlobalStorage(
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
export function createGlobalStorage(
|
|
62
|
+
customHomeOrOptions?: string | { customHome?: string; dataDir?: string },
|
|
63
|
+
dataDirArg?: string
|
|
64
|
+
): RuntimeStorage {
|
|
65
|
+
let customHome: string | undefined;
|
|
66
|
+
let dataDir: string | undefined;
|
|
67
|
+
|
|
68
|
+
if (typeof customHomeOrOptions === "object" && customHomeOrOptions !== null) {
|
|
69
|
+
customHome = customHomeOrOptions.customHome;
|
|
70
|
+
dataDir = customHomeOrOptions.dataDir;
|
|
71
|
+
} else {
|
|
72
|
+
customHome = customHomeOrOptions;
|
|
73
|
+
dataDir = dataDirArg;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const dbPath = dataDir
|
|
77
|
+
? join(dataDir, "global.db")
|
|
78
|
+
: join(getActionDockHome(customHome), ".actiondock", "global.db");
|
|
63
79
|
return new SqliteRuntimeStorage({ dbPath, packageId: "__global__" });
|
|
64
80
|
}
|
|
65
81
|
|
package/src/storage/sqlite.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import type { JsonValue, RuntimeError, RunRecord } from "@actiondock/sdk";
|
|
4
|
+
import { type Clock, getSystemClock } from "../runtime/clock";
|
|
4
5
|
import { createDefaultSqliteDriver } from "./driver";
|
|
5
6
|
import type {
|
|
6
7
|
RuntimeStorage,
|
|
7
8
|
SqliteDriver,
|
|
9
|
+
SqliteStatement,
|
|
8
10
|
StateEntry,
|
|
9
11
|
StorageOptions,
|
|
10
12
|
TerminalRunStatus,
|
|
@@ -17,10 +19,21 @@ import type {
|
|
|
17
19
|
export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
18
20
|
private driver: SqliteDriver;
|
|
19
21
|
private packageId: string;
|
|
22
|
+
private clock: Clock;
|
|
20
23
|
private isClosed = false;
|
|
24
|
+
private statementCache = new Map<string, SqliteStatement>();
|
|
25
|
+
|
|
26
|
+
get isOpen(): boolean {
|
|
27
|
+
return !this.isClosed;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get closed(): boolean {
|
|
31
|
+
return this.isClosed;
|
|
32
|
+
}
|
|
21
33
|
|
|
22
34
|
constructor(options: StorageOptions) {
|
|
23
35
|
this.packageId = options.packageId;
|
|
36
|
+
this.clock = options.clock ?? getSystemClock();
|
|
24
37
|
const dbPath = options.dbPath || ":memory:";
|
|
25
38
|
|
|
26
39
|
if (dbPath !== ":memory:") {
|
|
@@ -161,10 +174,22 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
161
174
|
}
|
|
162
175
|
}
|
|
163
176
|
|
|
177
|
+
/**
|
|
178
|
+
* 获取或复用预编译 SQL 语句缓存。
|
|
179
|
+
*/
|
|
180
|
+
private getStatement(sql: string): SqliteStatement {
|
|
181
|
+
let stmt = this.statementCache.get(sql);
|
|
182
|
+
if (!stmt) {
|
|
183
|
+
stmt = this.driver.prepare(sql);
|
|
184
|
+
this.statementCache.set(sql, stmt);
|
|
185
|
+
}
|
|
186
|
+
return stmt;
|
|
187
|
+
}
|
|
188
|
+
|
|
164
189
|
// --- Config 配置管理 ---
|
|
165
190
|
|
|
166
191
|
getConfig<T = unknown>(key: string): T | undefined {
|
|
167
|
-
const stmt = this.
|
|
192
|
+
const stmt = this.getStatement(
|
|
168
193
|
"SELECT value_json FROM config WHERE package_id = ? AND key = ?"
|
|
169
194
|
);
|
|
170
195
|
const row = stmt.get<{ value_json: string }>(this.packageId, key);
|
|
@@ -179,7 +204,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
179
204
|
}
|
|
180
205
|
|
|
181
206
|
listConfig(): Record<string, unknown> {
|
|
182
|
-
const stmt = this.
|
|
207
|
+
const stmt = this.getStatement(
|
|
183
208
|
"SELECT key, value_json FROM config WHERE package_id = ?"
|
|
184
209
|
);
|
|
185
210
|
const rows = stmt.all<{ key: string; value_json: string }>(this.packageId);
|
|
@@ -195,7 +220,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
195
220
|
}
|
|
196
221
|
|
|
197
222
|
setConfig(key: string, value: unknown): void {
|
|
198
|
-
const stmt = this.
|
|
223
|
+
const stmt = this.getStatement(`
|
|
199
224
|
INSERT INTO config (package_id, key, value_json, updated_at)
|
|
200
225
|
VALUES (?, ?, ?, ?)
|
|
201
226
|
ON CONFLICT(package_id, key) DO UPDATE SET
|
|
@@ -203,12 +228,12 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
203
228
|
updated_at = excluded.updated_at
|
|
204
229
|
`);
|
|
205
230
|
const valJson = JSON.stringify(value);
|
|
206
|
-
const now =
|
|
231
|
+
const now = this.clock.now().toISOString();
|
|
207
232
|
stmt.run(this.packageId, key, valJson, now);
|
|
208
233
|
}
|
|
209
234
|
|
|
210
235
|
deleteConfig(key: string): boolean {
|
|
211
|
-
const stmt = this.
|
|
236
|
+
const stmt = this.getStatement(
|
|
212
237
|
"DELETE FROM config WHERE package_id = ? AND key = ?"
|
|
213
238
|
);
|
|
214
239
|
const res = stmt.run(this.packageId, key);
|
|
@@ -218,7 +243,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
218
243
|
// --- State 状态管理 ---
|
|
219
244
|
|
|
220
245
|
async getState<T = unknown>(namespace: string, key: string): Promise<T | undefined> {
|
|
221
|
-
const stmt = this.
|
|
246
|
+
const stmt = this.getStatement(
|
|
222
247
|
"SELECT value_json, expires_at FROM state WHERE package_id = ? AND namespace = ? AND key = ?"
|
|
223
248
|
);
|
|
224
249
|
const row = stmt.get<{ value_json: string; expires_at?: string }>(
|
|
@@ -232,7 +257,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
232
257
|
|
|
233
258
|
if (row.expires_at) {
|
|
234
259
|
const expires = new Date(row.expires_at).getTime();
|
|
235
|
-
if (
|
|
260
|
+
if (this.clock.now().getTime() >= expires) {
|
|
236
261
|
this.deleteState(namespace, key).catch(() => {});
|
|
237
262
|
return undefined;
|
|
238
263
|
}
|
|
@@ -262,7 +287,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
262
287
|
const val = await this.getState<T>(ns, actualKey);
|
|
263
288
|
if (val === undefined) return undefined;
|
|
264
289
|
|
|
265
|
-
const stmt = this.
|
|
290
|
+
const stmt = this.getStatement(
|
|
266
291
|
"SELECT updated_at, expires_at FROM state WHERE package_id = ? AND namespace = ? AND key = ?"
|
|
267
292
|
);
|
|
268
293
|
const row = stmt.get<{ updated_at: string; expires_at?: string }>(
|
|
@@ -277,14 +302,14 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
277
302
|
key: actualKey,
|
|
278
303
|
fullKey: targetKey,
|
|
279
304
|
value: val,
|
|
280
|
-
updatedAt: row?.updated_at ||
|
|
305
|
+
updatedAt: row?.updated_at || this.clock.now().toISOString(),
|
|
281
306
|
expiresAt: row?.expires_at,
|
|
282
307
|
};
|
|
283
308
|
}
|
|
284
309
|
|
|
285
310
|
const val = await this.getState<T>("", actualKey);
|
|
286
311
|
if (val !== undefined) {
|
|
287
|
-
const stmt = this.
|
|
312
|
+
const stmt = this.getStatement(
|
|
288
313
|
"SELECT updated_at, expires_at FROM state WHERE package_id = ? AND namespace = ? AND key = ?"
|
|
289
314
|
);
|
|
290
315
|
const row = stmt.get<{ updated_at: string; expires_at?: string }>(
|
|
@@ -298,12 +323,12 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
298
323
|
key: actualKey,
|
|
299
324
|
fullKey: actualKey,
|
|
300
325
|
value: val,
|
|
301
|
-
updatedAt: row?.updated_at ||
|
|
326
|
+
updatedAt: row?.updated_at || this.clock.now().toISOString(),
|
|
302
327
|
expiresAt: row?.expires_at,
|
|
303
328
|
};
|
|
304
329
|
}
|
|
305
330
|
|
|
306
|
-
const stmt = this.
|
|
331
|
+
const stmt = this.getStatement(
|
|
307
332
|
"SELECT namespace, key, value_json, updated_at, expires_at FROM state WHERE package_id = ? AND key = ?"
|
|
308
333
|
);
|
|
309
334
|
const rows = stmt.all<{
|
|
@@ -314,7 +339,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
314
339
|
expires_at?: string;
|
|
315
340
|
}>(this.packageId, actualKey);
|
|
316
341
|
|
|
317
|
-
const now =
|
|
342
|
+
const now = this.clock.now().getTime();
|
|
318
343
|
for (const row of rows) {
|
|
319
344
|
if (row.expires_at && now >= new Date(row.expires_at).getTime()) {
|
|
320
345
|
continue;
|
|
@@ -345,7 +370,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
345
370
|
value: T,
|
|
346
371
|
ttl?: number
|
|
347
372
|
): Promise<void> {
|
|
348
|
-
const stmt = this.
|
|
373
|
+
const stmt = this.getStatement(`
|
|
349
374
|
INSERT INTO state (package_id, namespace, key, value_json, updated_at, expires_at)
|
|
350
375
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
351
376
|
ON CONFLICT(package_id, namespace, key) DO UPDATE SET
|
|
@@ -354,7 +379,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
354
379
|
expires_at = excluded.expires_at
|
|
355
380
|
`);
|
|
356
381
|
const valJson = JSON.stringify(value);
|
|
357
|
-
const now =
|
|
382
|
+
const now = this.clock.now();
|
|
358
383
|
const updatedAt = now.toISOString();
|
|
359
384
|
|
|
360
385
|
let expiresAt: string | null = null;
|
|
@@ -366,7 +391,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
366
391
|
}
|
|
367
392
|
|
|
368
393
|
async deleteState(namespace: string, key: string): Promise<boolean> {
|
|
369
|
-
const stmt = this.
|
|
394
|
+
const stmt = this.getStatement(
|
|
370
395
|
"DELETE FROM state WHERE package_id = ? AND namespace = ? AND key = ?"
|
|
371
396
|
);
|
|
372
397
|
const res = stmt.run(this.packageId, namespace, key);
|
|
@@ -390,7 +415,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
390
415
|
const deletedRoot = await this.deleteState("", actualKey);
|
|
391
416
|
if (deletedRoot) return true;
|
|
392
417
|
|
|
393
|
-
const stmt = this.
|
|
418
|
+
const stmt = this.getStatement(
|
|
394
419
|
"DELETE FROM state WHERE package_id = ? AND key = ?"
|
|
395
420
|
);
|
|
396
421
|
const res = stmt.run(this.packageId, actualKey);
|
|
@@ -414,7 +439,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
414
439
|
params.push(`${escapedPrefix}%`);
|
|
415
440
|
}
|
|
416
441
|
|
|
417
|
-
const stmt = this.
|
|
442
|
+
const stmt = this.getStatement(sql);
|
|
418
443
|
const res = stmt.run(...params);
|
|
419
444
|
return res.changes;
|
|
420
445
|
}
|
|
@@ -437,14 +462,14 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
437
462
|
params.push(`${escapedPrefix}%`);
|
|
438
463
|
}
|
|
439
464
|
|
|
440
|
-
const stmt = this.
|
|
465
|
+
const stmt = this.getStatement(sql);
|
|
441
466
|
const rows = stmt.all<{
|
|
442
467
|
namespace: string;
|
|
443
468
|
key: string;
|
|
444
469
|
expires_at?: string;
|
|
445
470
|
}>(...params);
|
|
446
471
|
|
|
447
|
-
const now =
|
|
472
|
+
const now = this.clock.now().getTime();
|
|
448
473
|
const result: string[] = [];
|
|
449
474
|
|
|
450
475
|
for (const row of rows) {
|
|
@@ -478,7 +503,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
478
503
|
params.push(`${escapedPrefix}%`);
|
|
479
504
|
}
|
|
480
505
|
|
|
481
|
-
const stmt = this.
|
|
506
|
+
const stmt = this.getStatement(sql);
|
|
482
507
|
const rows = stmt.all<{
|
|
483
508
|
namespace: string;
|
|
484
509
|
key: string;
|
|
@@ -487,7 +512,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
487
512
|
expires_at?: string;
|
|
488
513
|
}>(...params);
|
|
489
514
|
|
|
490
|
-
const now =
|
|
515
|
+
const now = this.clock.now().getTime();
|
|
491
516
|
const results: StateEntry[] = [];
|
|
492
517
|
|
|
493
518
|
for (const row of rows) {
|
|
@@ -519,7 +544,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
519
544
|
// --- Runs 运行记录管理 ---
|
|
520
545
|
|
|
521
546
|
createRun(record: RunRecord | any): void {
|
|
522
|
-
const stmt = this.
|
|
547
|
+
const stmt = this.getStatement(`
|
|
523
548
|
INSERT INTO runs (
|
|
524
549
|
id, root_run_id, parent_run_id, package_id, package_instance_id,
|
|
525
550
|
action_id, generation_id, owner_id, status, input_json, output_json,
|
|
@@ -567,7 +592,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
567
592
|
): void {
|
|
568
593
|
if (this.isClosed) return;
|
|
569
594
|
try {
|
|
570
|
-
const stmt = this.
|
|
595
|
+
const stmt = this.getStatement(`
|
|
571
596
|
UPDATE runs
|
|
572
597
|
SET status = ?, output_json = ?, error_json = ?, finished_at = ?
|
|
573
598
|
WHERE id = ?
|
|
@@ -576,16 +601,18 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
576
601
|
status,
|
|
577
602
|
output !== undefined ? JSON.stringify(output) : null,
|
|
578
603
|
error ? JSON.stringify(error) : null,
|
|
579
|
-
finishedAt ||
|
|
604
|
+
finishedAt || this.clock.now().toISOString(),
|
|
580
605
|
id
|
|
581
606
|
);
|
|
582
|
-
} catch {
|
|
583
|
-
|
|
607
|
+
} catch (err) {
|
|
608
|
+
if (this.isClosed) return;
|
|
609
|
+
console.warn(`[SqliteRuntimeStorage] Failed to update run "${id}":`, err);
|
|
610
|
+
throw err;
|
|
584
611
|
}
|
|
585
612
|
}
|
|
586
613
|
|
|
587
614
|
getRun(id: string): RunRecord | null {
|
|
588
|
-
const stmt = this.
|
|
615
|
+
const stmt = this.getStatement(
|
|
589
616
|
"SELECT * FROM runs WHERE id = ? AND package_id = ?"
|
|
590
617
|
);
|
|
591
618
|
const row = stmt.get<any>(id, this.packageId);
|
|
@@ -597,7 +624,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
597
624
|
const limit = options.limit || 50;
|
|
598
625
|
let rows: any[];
|
|
599
626
|
if (options.actionId) {
|
|
600
|
-
const stmt = this.
|
|
627
|
+
const stmt = this.getStatement(`
|
|
601
628
|
SELECT * FROM runs
|
|
602
629
|
WHERE package_id = ? AND action_id = ?
|
|
603
630
|
ORDER BY started_at DESC
|
|
@@ -605,7 +632,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
605
632
|
`);
|
|
606
633
|
rows = stmt.all(this.packageId, options.actionId, limit);
|
|
607
634
|
} else {
|
|
608
|
-
const stmt = this.
|
|
635
|
+
const stmt = this.getStatement(`
|
|
609
636
|
SELECT * FROM runs
|
|
610
637
|
WHERE package_id = ?
|
|
611
638
|
ORDER BY started_at DESC
|
|
@@ -627,7 +654,7 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
627
654
|
sql += " AND status = ?";
|
|
628
655
|
params.push(options.status);
|
|
629
656
|
}
|
|
630
|
-
const stmt = this.
|
|
657
|
+
const stmt = this.getStatement(sql);
|
|
631
658
|
const res = stmt.run(...params);
|
|
632
659
|
return res.changes;
|
|
633
660
|
}
|
|
@@ -675,7 +702,9 @@ export class SqliteRuntimeStorage implements RuntimeStorage {
|
|
|
675
702
|
}
|
|
676
703
|
|
|
677
704
|
close(): void {
|
|
705
|
+
if (this.isClosed) return;
|
|
678
706
|
this.isClosed = true;
|
|
707
|
+
this.statementCache.clear();
|
|
679
708
|
try {
|
|
680
709
|
this.driver.close();
|
|
681
710
|
} catch {
|
package/src/storage/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { JsonValue, RuntimeError, RunRecord } from "@actiondock/sdk";
|
|
2
|
+
import type { Clock } from "../runtime/clock";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* SQLite 基础参数值类型。
|
|
@@ -73,6 +74,8 @@ export interface StorageOptions {
|
|
|
73
74
|
packageId: string;
|
|
74
75
|
/** 显式注入的 SQLite 底层驱动 */
|
|
75
76
|
driver?: SqliteDriver;
|
|
77
|
+
/** 可选注入的时间提供器,便于与模拟时钟联动 */
|
|
78
|
+
clock?: Clock;
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
/**
|
|
@@ -89,6 +92,11 @@ export type TerminalRunStatus =
|
|
|
89
92
|
* 统一运行时存储抽象接口。
|
|
90
93
|
*/
|
|
91
94
|
export interface RuntimeStorage {
|
|
95
|
+
/** 数据库是否处于打开状态 */
|
|
96
|
+
readonly isOpen?: boolean;
|
|
97
|
+
/** 数据库是否已关闭 */
|
|
98
|
+
readonly closed?: boolean;
|
|
99
|
+
|
|
92
100
|
// --- Config 配置管理 ---
|
|
93
101
|
getConfig<T = unknown>(key: string): T | undefined;
|
|
94
102
|
listConfig(): Record<string, unknown>;
|
package/src/utils/index.ts
CHANGED
package/src/build/builder.ts
DELETED
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
5
|
-
import { loadActionFileMap, loadActions, loadProjectConfig } from "../project/loader";
|
|
6
|
-
import type { ProjectConfig } from "../project/types";
|
|
7
|
-
import { getPackageSlug } from "../utils";
|
|
8
|
-
import { type ActionImport, generateStandaloneEntrypoint } from "./templates";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 独立二进制可执行文件构建选项。
|
|
12
|
-
*/
|
|
13
|
-
export interface BuildOptions {
|
|
14
|
-
/** 目标项目根目录 */
|
|
15
|
-
projectRoot: string;
|
|
16
|
-
/** 目标架构(如 "bun-linux-x64", "bun-darwin-arm64", "bun-windows-x64" 等) */
|
|
17
|
-
target?: string;
|
|
18
|
-
/** 输出可执行文件的目标路径(默认输出到 dist/ 目录) */
|
|
19
|
-
outfile?: string;
|
|
20
|
-
/** 是否开启代码压缩混淆(默认 true) */
|
|
21
|
-
minify?: boolean;
|
|
22
|
-
/** 是否编译为 V8/JavaScriptCore 字节码(默认 true) */
|
|
23
|
-
bytecode?: boolean;
|
|
24
|
-
/** 显式挑选打包的 Action ID 清单(用于按需子集打包) */
|
|
25
|
-
actions?: string[];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* 独立二进制构建完成后的元数据结果对象。
|
|
30
|
-
*/
|
|
31
|
-
export interface BuildResult {
|
|
32
|
-
/** 所属 Package ID */
|
|
33
|
-
packageId: string;
|
|
34
|
-
/** 打包的项目版本号 */
|
|
35
|
-
version: string;
|
|
36
|
-
/** 编译的目标平台架构 */
|
|
37
|
-
target: string;
|
|
38
|
-
/** 生成的独立二进制可执行文件绝对路径 */
|
|
39
|
-
executablePath: string;
|
|
40
|
-
/** 生成的 sidecar 元数据 JSON 文件绝对路径 */
|
|
41
|
-
metadataPath: string;
|
|
42
|
-
/** 打包内置的 Action ID 列表 */
|
|
43
|
-
actions: string[];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* 调用 Bun 原生编译引擎(Bun.build --compile)将 Action Package 打包为零外部依赖的独立二进制可执行文件。
|
|
48
|
-
*
|
|
49
|
-
* 构建过程:
|
|
50
|
-
* 1. 动态生成 Standalone 入口点代码(包含 StandaloneRuntime 与 Action 注册)。
|
|
51
|
-
* 2. 生成 sidecar metadata 文件(.actiondock-meta.json),包含 Package 元数据与 sha256 校验和。
|
|
52
|
-
* 3. 执行 Bun.build({ compile: true, target, minify, bytecode })。
|
|
53
|
-
*
|
|
54
|
-
* @param options 构建参数
|
|
55
|
-
* @returns 构建产物结果元数据
|
|
56
|
-
*/
|
|
57
|
-
export async function buildProject(options: BuildOptions): Promise<BuildResult> {
|
|
58
|
-
const root = resolve(options.projectRoot);
|
|
59
|
-
const config = loadProjectConfig(root);
|
|
60
|
-
const actionsMap = await loadActions(root, config.actionsDir);
|
|
61
|
-
|
|
62
|
-
if (actionsMap.size === 0) {
|
|
63
|
-
throw new Error(`No valid actions found in ${join(root, config.actionsDir || "actions")}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (options.actions && options.actions.length > 0) {
|
|
67
|
-
const requestedActions = new Set(options.actions);
|
|
68
|
-
for (const reqId of requestedActions) {
|
|
69
|
-
if (!actionsMap.has(reqId)) {
|
|
70
|
-
throw new Error(`Action '${reqId}' requested in build options not found in project`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
for (const id of Array.from(actionsMap.keys())) {
|
|
74
|
-
if (!requestedActions.has(id)) {
|
|
75
|
-
actionsMap.delete(id);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Action imports list
|
|
81
|
-
const actionFileMap = await loadActionFileMap(root, config.actionsDir);
|
|
82
|
-
const actionImports: ActionImport[] = [];
|
|
83
|
-
|
|
84
|
-
for (const [id, entry] of actionFileMap.entries()) {
|
|
85
|
-
if (actionsMap.has(id)) {
|
|
86
|
-
actionImports.push({
|
|
87
|
-
id,
|
|
88
|
-
filePath: entry.filePath,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (actionImports.length === 0) {
|
|
94
|
-
throw new Error("Could not map any action files for build");
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Create build dir
|
|
98
|
-
const buildDir = join(root, ".actiondock", ".build");
|
|
99
|
-
mkdirSync(buildDir, { recursive: true });
|
|
100
|
-
|
|
101
|
-
const entryCode = generateStandaloneEntrypoint(
|
|
102
|
-
config.id,
|
|
103
|
-
config.version,
|
|
104
|
-
config.description,
|
|
105
|
-
actionImports,
|
|
106
|
-
config.config
|
|
107
|
-
);
|
|
108
|
-
const entryPath = join(buildDir, "entry.ts");
|
|
109
|
-
writeFileSync(entryPath, entryCode, "utf-8");
|
|
110
|
-
|
|
111
|
-
// Determine target and outfile
|
|
112
|
-
const target = options.target || "bun";
|
|
113
|
-
const binaryName = getPackageSlug(config.id);
|
|
114
|
-
|
|
115
|
-
const defaultOutfile = join(root, "dist", binaryName);
|
|
116
|
-
const outfile = resolve(options.outfile || defaultOutfile);
|
|
117
|
-
|
|
118
|
-
mkdirSync(dirname(outfile), { recursive: true });
|
|
119
|
-
|
|
120
|
-
// Run bun build --compile --bytecode --minify
|
|
121
|
-
const buildArgs = [
|
|
122
|
-
"bun",
|
|
123
|
-
"build",
|
|
124
|
-
entryPath,
|
|
125
|
-
"--compile",
|
|
126
|
-
"--outfile",
|
|
127
|
-
outfile,
|
|
128
|
-
];
|
|
129
|
-
|
|
130
|
-
if (options.bytecode !== false) {
|
|
131
|
-
buildArgs.push("--bytecode");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (options.minify !== false) {
|
|
135
|
-
buildArgs.push("--minify");
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (options.target && options.target !== "bun" && options.target !== "host") {
|
|
139
|
-
// e.g. bun-linux-x64 or linux-x64
|
|
140
|
-
const formattedTarget = options.target.startsWith("bun-")
|
|
141
|
-
? options.target
|
|
142
|
-
: `bun-${options.target}`;
|
|
143
|
-
buildArgs.push(`--target=${formattedTarget}`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const proc = spawnSync(buildArgs[0], buildArgs.slice(1), {
|
|
147
|
-
cwd: root,
|
|
148
|
-
stdio: "pipe",
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
if (proc.error) {
|
|
152
|
-
throw new Error(`Bun compile failed to spawn: ${proc.error.message}`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (proc.status !== 0) {
|
|
156
|
-
const errText = proc.stderr?.toString() || proc.stdout?.toString() || "Unknown error";
|
|
157
|
-
throw new Error(`Bun compile failed (exit code ${proc.status}):\n${errText}`);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Compile artifact resolution (on Windows bun compile automatically appends .exe)
|
|
161
|
-
let artifactPath = outfile;
|
|
162
|
-
if (!existsSync(artifactPath)) {
|
|
163
|
-
const withExe = outfile + ".exe";
|
|
164
|
-
if (existsSync(withExe)) {
|
|
165
|
-
artifactPath = withExe;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Calculate build hash
|
|
170
|
-
const binaryBuffer = readFileSync(artifactPath);
|
|
171
|
-
const buildHash = createHash("sha256").update(binaryBuffer).digest("hex").slice(0, 16);
|
|
172
|
-
|
|
173
|
-
// Calculate lockHash
|
|
174
|
-
let lockHash = "none";
|
|
175
|
-
const lockFiles = ["bun.lock", "bun.lockb", "package.json"];
|
|
176
|
-
for (const lf of lockFiles) {
|
|
177
|
-
const p = join(root, lf);
|
|
178
|
-
if (existsSync(p)) {
|
|
179
|
-
lockHash = createHash("sha256").update(readFileSync(p)).digest("hex").slice(0, 16);
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const detectedBunVersion = (typeof (globalThis as any).Bun !== "undefined" && (globalThis as any).Bun.version) || (() => {
|
|
185
|
-
try {
|
|
186
|
-
const vProc = spawnSync("bun", ["--version"], { stdio: "pipe" });
|
|
187
|
-
return vProc.stdout ? vProc.stdout.toString().trim() : "unknown";
|
|
188
|
-
} catch {
|
|
189
|
-
return "unknown";
|
|
190
|
-
}
|
|
191
|
-
})();
|
|
192
|
-
|
|
193
|
-
// Generate artifact.json metadata
|
|
194
|
-
const metadata = {
|
|
195
|
-
packageId: config.id,
|
|
196
|
-
name: config.name,
|
|
197
|
-
version: config.version,
|
|
198
|
-
description: config.description,
|
|
199
|
-
target: options.target || "host",
|
|
200
|
-
actions: actionImports.map((a) => a.id),
|
|
201
|
-
bunVersion: detectedBunVersion,
|
|
202
|
-
lockHash,
|
|
203
|
-
buildHash,
|
|
204
|
-
createdAt: new Date().toISOString(),
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
const metadataPath = join(dirname(artifactPath), "artifact.json");
|
|
208
|
-
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2) + "\n", "utf-8");
|
|
209
|
-
|
|
210
|
-
return {
|
|
211
|
-
packageId: config.id,
|
|
212
|
-
version: config.version,
|
|
213
|
-
target: options.target || "host",
|
|
214
|
-
executablePath: artifactPath,
|
|
215
|
-
metadataPath,
|
|
216
|
-
actions: metadata.actions,
|
|
217
|
-
};
|
|
218
|
-
}
|