@actiondock/cli 2.0.12 → 2.2.1
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/README.md +41 -34
- package/bin/ad.js +28 -41
- package/dist/commands/action/create.d.ts +9 -1
- package/dist/commands/action/create.js +120 -0
- package/dist/commands/add.d.ts +7 -0
- package/dist/commands/add.js +144 -0
- package/dist/commands/build.d.ts +2 -1
- package/dist/commands/build.js +83 -0
- package/dist/commands/config/delete.d.ts +9 -0
- package/dist/commands/config/delete.js +52 -0
- package/dist/commands/config/env.d.ts +9 -0
- package/dist/commands/config/env.js +85 -0
- package/dist/commands/config/get.d.ts +9 -0
- package/dist/commands/config/get.js +120 -0
- package/dist/commands/config/index.d.ts +9 -0
- package/dist/commands/config/index.js +23 -0
- package/dist/commands/config/list.d.ts +9 -0
- package/dist/commands/config/list.js +137 -0
- package/dist/commands/config/schema.d.ts +9 -0
- package/dist/commands/config/schema.js +121 -0
- package/dist/commands/config/set.d.ts +9 -0
- package/dist/commands/config/set.js +85 -0
- package/dist/commands/describe.d.ts +9 -0
- package/dist/commands/describe.js +68 -0
- package/dist/commands/doctor.d.ts +2 -1
- package/dist/commands/doctor.js +82 -0
- package/dist/commands/export.d.ts +2 -1
- package/dist/commands/export.js +212 -0
- package/dist/commands/generate.d.ts +9 -0
- package/dist/commands/generate.js +44 -0
- package/dist/commands/index.d.ts +31 -1
- package/dist/commands/index.js +111 -0
- package/dist/commands/info.d.ts +9 -2
- package/dist/commands/info.js +210 -0
- package/dist/commands/init.js +24 -0
- package/dist/commands/link.js +71 -0
- package/dist/commands/list.d.ts +9 -0
- package/dist/commands/list.js +77 -0
- package/dist/commands/mcp.d.ts +9 -2
- package/dist/commands/mcp.js +173 -0
- package/dist/commands/new.d.ts +9 -0
- package/dist/commands/new.js +33 -0
- package/dist/commands/pack.d.ts +3 -0
- package/dist/commands/pack.js +63 -0
- package/dist/commands/playbook.d.ts +7 -3
- package/dist/commands/playbook.js +433 -0
- package/dist/commands/profile.js +253 -0
- package/dist/commands/remove.d.ts +7 -0
- package/dist/commands/remove.js +118 -0
- package/dist/commands/run.d.ts +13 -0
- package/dist/commands/run.js +146 -0
- package/dist/commands/runs.d.ts +9 -2
- package/dist/commands/runs.js +245 -0
- package/dist/commands/serve.d.ts +9 -2
- package/dist/commands/serve.js +135 -0
- package/dist/commands/state-list.d.ts +24 -0
- package/dist/commands/state-list.js +82 -0
- package/dist/commands/state.d.ts +9 -2
- package/dist/commands/state.js +310 -0
- package/dist/commands/test.js +44 -0
- package/dist/commands/validate.d.ts +9 -0
- package/dist/commands/validate.js +106 -0
- package/dist/errors.d.ts +43 -0
- package/dist/errors.js +103 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +41 -1000
- package/dist/prompt.d.ts +53 -0
- package/dist/prompt.js +192 -0
- package/dist/renderer.d.ts +197 -0
- package/dist/renderer.js +502 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/inspect.d.ts +23 -0
- package/dist/services/inspect.js +96 -0
- package/dist/standalone/index.d.ts +16 -0
- package/dist/standalone/index.js +22 -0
- package/dist/types.d.ts +132 -0
- package/dist/types.js +9 -0
- package/dist/utils/bytes.d.ts +1 -1
- package/dist/utils/bytes.js +1 -0
- package/dist/utils/duration.js +1 -0
- package/dist/utils/filter.d.ts +1 -1
- package/dist/utils/filter.js +1 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/spawn.d.ts +27 -0
- package/dist/utils/spawn.js +35 -0
- package/dist/utils/target.d.ts +45 -0
- package/dist/utils/target.js +78 -0
- package/dist/utils/utils.d.ts +38 -0
- package/dist/utils/utils.js +111 -0
- package/package.json +10 -12
- package/dist/commands/action/index.d.ts +0 -16
- package/dist/commands/action/list.d.ts +0 -5
- package/dist/commands/action/run.d.ts +0 -4
- package/dist/commands/action/show.d.ts +0 -5
- package/dist/commands/action/sync.d.ts +0 -2
- package/dist/commands/action/validate.d.ts +0 -5
- package/dist/commands/action.d.ts +0 -1
- package/dist/commands/config.d.ts +0 -2
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { findProjectRoot, formatHostForUrl, loadProjectConfig, startActionDockServer } from "@actiondock/core";
|
|
3
|
+
import { ArgumentError, ExecutionError } from "../errors.js";
|
|
4
|
+
import { writeStderr, writeStdout } from "../renderer.js";
|
|
5
|
+
import { getEffectiveOptions, parseByteSize } from "../utils/index.js";
|
|
6
|
+
/**
|
|
7
|
+
* 注册 serve HTTP 服务启动命令。
|
|
8
|
+
*
|
|
9
|
+
* @param program Commander 实例
|
|
10
|
+
* @param context 命令行上下文
|
|
11
|
+
*/
|
|
12
|
+
export function registerServeCommand(program, context) {
|
|
13
|
+
program
|
|
14
|
+
.command("serve")
|
|
15
|
+
.description("Start the ActionDock lightweight HTTP Runner server for remote execution")
|
|
16
|
+
.option("-p, --port <port>", "Port to listen on (default: 5177)", "5177")
|
|
17
|
+
.option("-H, --host <host>", "Host address to bind to (default: 127.0.0.1)", "127.0.0.1")
|
|
18
|
+
.option("-t, --token <token>", "Authentication token for securing the endpoint (or set ACTIONDOCK_TOKEN)")
|
|
19
|
+
.option("--allow-insecure-no-auth", "Allow non-loopback host binding without authentication token (INSECURE)")
|
|
20
|
+
.option("--cors-origin <origin>", "Allowed CORS origin (can be specified multiple times)", (val, prev = []) => [...prev, val], [])
|
|
21
|
+
.option("--max-body <size>", "Maximum allowed JSON request body size (e.g. 1mb, 500kb)", "1mb")
|
|
22
|
+
.option("--expose-debug-info", "Expose project root path in health and info responses")
|
|
23
|
+
.option("--no-mcp", "Disable unified MCP protocol endpoint at /mcp")
|
|
24
|
+
.option("--allow-query-token", "Allow passing authentication token via URL query parameter (?token=xxx)")
|
|
25
|
+
.option("--management", "Enable config and state management API endpoints (disabled by default for security)")
|
|
26
|
+
.option("-d, --dir <path>", "Project root directory (default: current working directory)")
|
|
27
|
+
.action(async (rawOptions, cmd) => {
|
|
28
|
+
const options = getEffectiveOptions(rawOptions, cmd);
|
|
29
|
+
const port = parseInt(options.port, 10) || 5177;
|
|
30
|
+
const host = options.host || "127.0.0.1";
|
|
31
|
+
const token = options.token || (typeof process !== "undefined" ? process.env?.ACTIONDOCK_TOKEN : undefined);
|
|
32
|
+
const allowInsecureNoAuth = Boolean(options.allowInsecureNoAuth);
|
|
33
|
+
const allowQueryToken = Boolean(options.allowQueryToken);
|
|
34
|
+
const enableManagement = Boolean(options.management);
|
|
35
|
+
const corsOrigins = options.corsOrigin && options.corsOrigin.length > 0 ? options.corsOrigin : undefined;
|
|
36
|
+
const exposeDebugInfo = Boolean(options.exposeDebugInfo);
|
|
37
|
+
let maxBodyBytes;
|
|
38
|
+
if (options.maxBody) {
|
|
39
|
+
try {
|
|
40
|
+
maxBodyBytes = parseByteSize(options.maxBody);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
throw new ArgumentError(`Invalid max-body format: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const projectRoot = options.dir
|
|
47
|
+
? resolve(options.dir)
|
|
48
|
+
: findProjectRoot(process.cwd());
|
|
49
|
+
let projectName = "Global Registry Mode";
|
|
50
|
+
if (projectRoot) {
|
|
51
|
+
try {
|
|
52
|
+
const config = loadProjectConfig(projectRoot);
|
|
53
|
+
projectName = `${config.name} (${config.id})`;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// 降级处理
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
let mcpHandler;
|
|
60
|
+
const enableMcp = options.mcp !== false;
|
|
61
|
+
if (enableMcp) {
|
|
62
|
+
// MCP 处理器创建失败时直接终止启动,避免横幅宣称不存在的端点
|
|
63
|
+
try {
|
|
64
|
+
const [{ createActionDockMcpServer }, { createMcpHandler }] = await Promise.all([
|
|
65
|
+
import("@actiondock/mcp"),
|
|
66
|
+
import("@modelcontextprotocol/server"),
|
|
67
|
+
]);
|
|
68
|
+
const handler = createMcpHandler(() => {
|
|
69
|
+
return createActionDockMcpServer({
|
|
70
|
+
projectRoot: projectRoot || undefined,
|
|
71
|
+
});
|
|
72
|
+
}, {
|
|
73
|
+
onerror: (err) => {
|
|
74
|
+
writeStderr(`[MCP HTTP Error] ${err?.message || String(err)}`, context);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
mcpHandler = async (req) => {
|
|
78
|
+
return handler.fetch(req);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
throw new ExecutionError(`Failed to initialize MCP endpoint: ${err?.message || String(err)}`, err);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
const server = await startActionDockServer({
|
|
87
|
+
port,
|
|
88
|
+
host,
|
|
89
|
+
token,
|
|
90
|
+
allowInsecureNoAuth,
|
|
91
|
+
allowQueryToken,
|
|
92
|
+
corsOrigins,
|
|
93
|
+
maxBodyBytes,
|
|
94
|
+
exposeDebugInfo,
|
|
95
|
+
enableMcp,
|
|
96
|
+
enableManagement,
|
|
97
|
+
mcpHandler,
|
|
98
|
+
projectRoot: projectRoot || undefined,
|
|
99
|
+
});
|
|
100
|
+
const displayHost = formatHostForUrl(host);
|
|
101
|
+
const actualEndpointHost = formatHostForUrl(host === "0.0.0.0" ? "127.0.0.1" : host);
|
|
102
|
+
writeStdout(`\n======================================================`, context);
|
|
103
|
+
writeStdout(` ActionDock 2.0 HTTP Runner Server`, context);
|
|
104
|
+
writeStdout(`======================================================`, context);
|
|
105
|
+
writeStdout(` * Listening on: http://${displayHost}:${server.port}`, context);
|
|
106
|
+
writeStdout(` * Project: ${projectName}`, context);
|
|
107
|
+
if (projectRoot && exposeDebugInfo) {
|
|
108
|
+
writeStdout(` * Root Path: ${projectRoot}`, context);
|
|
109
|
+
}
|
|
110
|
+
const authDesc = token
|
|
111
|
+
? (allowQueryToken ? "Bearer Token / Query Token Enabled" : "Bearer Token Enabled (Query Token Disabled)")
|
|
112
|
+
: "Disabled (Public/Local)";
|
|
113
|
+
writeStdout(` * Authentication: ${authDesc}`, context);
|
|
114
|
+
writeStdout(` * Management APIs: ${enableManagement ? "Enabled" : "Disabled (Default)"}`, context);
|
|
115
|
+
writeStdout(` * CORS Origins: ${corsOrigins ? corsOrigins.join(", ") : "Disabled (Default)"}`, context);
|
|
116
|
+
writeStdout(` * Max Body Size: ${options.maxBody || "1mb"}`, context);
|
|
117
|
+
writeStdout(` * Health Endpoint: http://${actualEndpointHost}:${server.port}/api/v1/health`, context);
|
|
118
|
+
if (enableMcp) {
|
|
119
|
+
writeStdout(` * MCP Endpoint: http://${actualEndpointHost}:${server.port}/mcp`, context);
|
|
120
|
+
}
|
|
121
|
+
writeStdout(`======================================================\n`, context);
|
|
122
|
+
writeStdout(`Server is ready to accept remote requests.`, context);
|
|
123
|
+
writeStdout(`Press Ctrl+C to terminate.\n`, context);
|
|
124
|
+
const stopSignalHandler = () => {
|
|
125
|
+
writeStdout("\nStopping ActionDock server...", context);
|
|
126
|
+
server.stop();
|
|
127
|
+
};
|
|
128
|
+
process.once("SIGINT", stopSignalHandler);
|
|
129
|
+
process.once("SIGTERM", stopSignalHandler);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
throw new ExecutionError(`Failed to start ActionDock server: ${err.message}`, err);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ActionDockTarget } from "@actiondock/core";
|
|
2
|
+
import type { CliContext } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* 状态键列表渲染参数视图。
|
|
5
|
+
*/
|
|
6
|
+
export interface StateListRenderArgs {
|
|
7
|
+
target: ActionDockTarget;
|
|
8
|
+
actionId: string;
|
|
9
|
+
prefix: string;
|
|
10
|
+
options: any;
|
|
11
|
+
effectiveIntent?: string;
|
|
12
|
+
shouldFallback: boolean;
|
|
13
|
+
context?: CliContext;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 渲染单个项目作用域的状态键列表(含 --detail JSON 明细分支)。
|
|
17
|
+
*/
|
|
18
|
+
export declare function renderProjectScopedStateList(args: StateListRenderArgs & {
|
|
19
|
+
targetRoot: string;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* 渲染全部链接包聚合的状态键列表(跳过磁盘失效与清单损坏的链接项)。
|
|
23
|
+
*/
|
|
24
|
+
export declare function renderLinkedPackagesStateList(args: StateListRenderArgs): Promise<void>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { filterWithFallbackInfo, listLinkedPackages, loadProjectConfig, } from "@actiondock/core";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { renderResult, renderStateList } from "../renderer.js";
|
|
4
|
+
/**
|
|
5
|
+
* 渲染单个项目作用域的状态键列表(含 --detail JSON 明细分支)。
|
|
6
|
+
*/
|
|
7
|
+
export async function renderProjectScopedStateList(args) {
|
|
8
|
+
const { target, targetRoot, actionId, prefix, options, effectiveIntent, shouldFallback, context } = args;
|
|
9
|
+
const projConfig = loadProjectConfig(targetRoot);
|
|
10
|
+
const allKeys = await target.listStateKeys(projConfig.id, actionId, {
|
|
11
|
+
namespace: options.namespace !== undefined ? options.namespace : null,
|
|
12
|
+
prefix,
|
|
13
|
+
});
|
|
14
|
+
if (options.detail && options.json) {
|
|
15
|
+
const entries = [];
|
|
16
|
+
for (const k of allKeys) {
|
|
17
|
+
const entry = await target.getState(projConfig.id, actionId, k, {
|
|
18
|
+
namespace: options.namespace,
|
|
19
|
+
detail: true,
|
|
20
|
+
});
|
|
21
|
+
if (entry) {
|
|
22
|
+
entries.push(entry);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const filterRes = filterWithFallbackInfo(entries, effectiveIntent, [(e) => e.fullKey, (e) => e.key, (e) => e.namespace], shouldFallback);
|
|
26
|
+
renderResult(filterRes.items, {
|
|
27
|
+
json: true,
|
|
28
|
+
envelope: options.envelope,
|
|
29
|
+
context,
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const filterRes = filterWithFallbackInfo(allKeys, effectiveIntent, [(k) => k], shouldFallback);
|
|
34
|
+
renderResult(filterRes.items, {
|
|
35
|
+
json: options.json,
|
|
36
|
+
envelope: options.envelope,
|
|
37
|
+
humanFormatter: () => renderStateList(filterRes.items, `${projConfig.name} (${projConfig.id})`, filterRes.isFallback, effectiveIntent),
|
|
38
|
+
context,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 渲染全部链接包聚合的状态键列表(跳过磁盘失效与清单损坏的链接项)。
|
|
43
|
+
*/
|
|
44
|
+
export async function renderLinkedPackagesStateList(args) {
|
|
45
|
+
const { target, actionId, prefix, options, effectiveIntent, shouldFallback, context } = args;
|
|
46
|
+
// 扫描所有链接的包状态
|
|
47
|
+
const linked = listLinkedPackages();
|
|
48
|
+
if (linked.length === 0) {
|
|
49
|
+
renderResult([], {
|
|
50
|
+
json: options.json,
|
|
51
|
+
envelope: options.envelope,
|
|
52
|
+
humanFormatter: () => "No ActionDock project in current directory, and no packages linked.",
|
|
53
|
+
context,
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const aggregatedKeys = [];
|
|
58
|
+
for (const pkg of linked) {
|
|
59
|
+
if (!existsSync(pkg.path))
|
|
60
|
+
continue;
|
|
61
|
+
try {
|
|
62
|
+
const config = loadProjectConfig(pkg.path);
|
|
63
|
+
const keys = await target.listStateKeys(config.id, actionId, {
|
|
64
|
+
namespace: options.namespace !== undefined ? options.namespace : null,
|
|
65
|
+
prefix,
|
|
66
|
+
});
|
|
67
|
+
for (const k of keys) {
|
|
68
|
+
aggregatedKeys.push(`${config.id}/${k}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// 单个链接包清单损坏或状态读取失败时跳过,继续聚合其余包
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const filterRes = filterWithFallbackInfo(aggregatedKeys, effectiveIntent, [(k) => k], shouldFallback);
|
|
76
|
+
renderResult(filterRes.items, {
|
|
77
|
+
json: options.json,
|
|
78
|
+
envelope: options.envelope,
|
|
79
|
+
humanFormatter: () => renderStateList(filterRes.items, "Linked Packages State Store", filterRes.isFallback, effectiveIntent),
|
|
80
|
+
context,
|
|
81
|
+
});
|
|
82
|
+
}
|
package/dist/commands/state.d.ts
CHANGED
|
@@ -1,2 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { CliContext } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* 注册 state 状态管理命令(get、set、delete、clear、keys、list)。
|
|
5
|
+
*
|
|
6
|
+
* @param program Commander 实例
|
|
7
|
+
* @param context 命令行上下文
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerStateCommands(program: Command, context?: CliContext): void;
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { decodeStateKey, findProjectRoot, loadProjectConfig, resolvePackageRoot, } from "@actiondock/core";
|
|
2
|
+
import { ArgumentError, ExecutionError } from "../errors.js";
|
|
3
|
+
import { renderResult, renderStateList, writeStdout } from "../renderer.js";
|
|
4
|
+
import { getEffectiveOptions, getTargetRoot, resolveIntent, withTarget } from "../utils/index.js";
|
|
5
|
+
import { renderLinkedPackagesStateList, renderProjectScopedStateList, } from "./state-list.js";
|
|
6
|
+
/**
|
|
7
|
+
* 注册 state 状态管理命令(get、set、delete、clear、keys、list)。
|
|
8
|
+
*
|
|
9
|
+
* @param program Commander 实例
|
|
10
|
+
* @param context 命令行上下文
|
|
11
|
+
*/
|
|
12
|
+
export function registerStateCommands(program, context) {
|
|
13
|
+
const stateCmd = program
|
|
14
|
+
.command("state")
|
|
15
|
+
.description("Inspect and manage Shared State store");
|
|
16
|
+
// state list / keys 共享实现(远端、本地项目、链接包聚合三分支)
|
|
17
|
+
const handleListKeys = async (prefix = "", rawOptions, cmd) => {
|
|
18
|
+
const options = getEffectiveOptions(rawOptions, cmd);
|
|
19
|
+
const effectiveIntent = resolveIntent(options.intent, prefix ? [prefix] : []);
|
|
20
|
+
const shouldFallback = options.fallback !== false;
|
|
21
|
+
await withTarget(options, context, async (target, resolved) => {
|
|
22
|
+
const actionId = options.action || "";
|
|
23
|
+
// 远端服务模式:直接列举远端包作用域键
|
|
24
|
+
if (resolved.type === "remote") {
|
|
25
|
+
const keys = await target.listStateKeys(options.package || "", actionId, {
|
|
26
|
+
namespace: options.namespace,
|
|
27
|
+
prefix,
|
|
28
|
+
});
|
|
29
|
+
renderResult(keys, {
|
|
30
|
+
json: options.json,
|
|
31
|
+
envelope: options.envelope,
|
|
32
|
+
humanFormatter: () => renderStateList(keys, `Remote Server ${resolved.serverUrl}${resolved.profileName ? ` (Profile: ${resolved.profileName})` : ""}`, false, effectiveIntent),
|
|
33
|
+
context,
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// 本地工程与链接包模式:按目标根目录决策输出范围
|
|
38
|
+
let targetRoot = null;
|
|
39
|
+
if (options.package) {
|
|
40
|
+
targetRoot = resolvePackageRoot(options.package);
|
|
41
|
+
if (!targetRoot) {
|
|
42
|
+
throw new ArgumentError(`Package '${options.package}' not found in linked packages or path`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
targetRoot = findProjectRoot();
|
|
47
|
+
}
|
|
48
|
+
if (targetRoot) {
|
|
49
|
+
await renderProjectScopedStateList({
|
|
50
|
+
target,
|
|
51
|
+
targetRoot,
|
|
52
|
+
actionId,
|
|
53
|
+
prefix,
|
|
54
|
+
options,
|
|
55
|
+
effectiveIntent,
|
|
56
|
+
shouldFallback,
|
|
57
|
+
context,
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await renderLinkedPackagesStateList({
|
|
62
|
+
target,
|
|
63
|
+
actionId,
|
|
64
|
+
prefix,
|
|
65
|
+
options,
|
|
66
|
+
effectiveIntent,
|
|
67
|
+
shouldFallback,
|
|
68
|
+
context,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
// state list
|
|
73
|
+
stateCmd
|
|
74
|
+
.command("list [prefix]")
|
|
75
|
+
.description("List state keys in current project or linked packages")
|
|
76
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
77
|
+
.option("-a, --action <id>", "Action identifier scope")
|
|
78
|
+
.option("-n, --namespace <ns>", "Filter keys under specific namespace (omit to list all namespaces)")
|
|
79
|
+
.option("-p, --profile <name>", "Query state on a remote target")
|
|
80
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
81
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
82
|
+
.option("-i, --intent <pattern>", "Regex or fuzzy intent filter; falls back to full list when no match")
|
|
83
|
+
.option("--detail", "Include metadata (ttl, expiresAt, size, updatedAt) in JSON output")
|
|
84
|
+
.option("--no-fallback", "Disable fallback to full list when no items match intent")
|
|
85
|
+
.option("--data-dir <path>", "Custom database storage directory")
|
|
86
|
+
.option("--json", "Output as JSON")
|
|
87
|
+
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
88
|
+
.action(handleListKeys);
|
|
89
|
+
// state keys 别名
|
|
90
|
+
stateCmd
|
|
91
|
+
.command("keys [prefix]")
|
|
92
|
+
.description("Alias for 'ad state list [prefix]'")
|
|
93
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
94
|
+
.option("-a, --action <id>", "Action identifier scope")
|
|
95
|
+
.option("-n, --namespace <ns>", "Filter keys under specific namespace")
|
|
96
|
+
.option("-p, --profile <name>", "Query state on a remote target")
|
|
97
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
98
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
99
|
+
.option("-i, --intent <pattern>", "Regex or fuzzy intent filter")
|
|
100
|
+
.option("--detail", "Include metadata in JSON output")
|
|
101
|
+
.option("--no-fallback", "Disable fallback")
|
|
102
|
+
.option("--data-dir <path>", "Custom database storage directory")
|
|
103
|
+
.option("--json", "Output as JSON")
|
|
104
|
+
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
105
|
+
.action(handleListKeys);
|
|
106
|
+
// state get <key>
|
|
107
|
+
stateCmd
|
|
108
|
+
.command("get <key>")
|
|
109
|
+
.description("Get state value by key (supports composite 'ns:key' and detail mode)")
|
|
110
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
111
|
+
.option("-a, --action <id>", "Action identifier scope")
|
|
112
|
+
.option("-n, --namespace <ns>", "Explicit namespace scope for key")
|
|
113
|
+
.option("-p, --profile <name>", "Query state on a remote target")
|
|
114
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
115
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
116
|
+
.option("--data-dir <path>", "Custom database storage directory")
|
|
117
|
+
.option("--json", "Output as JSON")
|
|
118
|
+
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
119
|
+
.action(async (rawKey, rawOptions, cmd) => {
|
|
120
|
+
const options = getEffectiveOptions(rawOptions, cmd);
|
|
121
|
+
if (!rawKey) {
|
|
122
|
+
throw new ArgumentError("State key is required");
|
|
123
|
+
}
|
|
124
|
+
await withTarget(options, context, async (target, resolved) => {
|
|
125
|
+
const actionId = options.action || "";
|
|
126
|
+
if (resolved.type === "remote") {
|
|
127
|
+
const decoded = decodeStateKey(rawKey);
|
|
128
|
+
const effectiveNamespace = options.namespace || (decoded.namespace || undefined);
|
|
129
|
+
const actualKey = options.namespace ? rawKey : decoded.key;
|
|
130
|
+
const entry = await target.getState(options.package || "", actionId, actualKey, {
|
|
131
|
+
namespace: effectiveNamespace,
|
|
132
|
+
detail: true,
|
|
133
|
+
});
|
|
134
|
+
if (entry === undefined || entry.value === undefined) {
|
|
135
|
+
throw new ExecutionError(`State key '${rawKey}' not found on remote server`);
|
|
136
|
+
}
|
|
137
|
+
const val = entry.value;
|
|
138
|
+
renderResult({ key: rawKey, value: val, namespace: entry.namespace || effectiveNamespace }, {
|
|
139
|
+
json: options.json,
|
|
140
|
+
envelope: options.envelope,
|
|
141
|
+
humanFormatter: () => (typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)),
|
|
142
|
+
context,
|
|
143
|
+
});
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// 本地项目模式
|
|
147
|
+
const { root, key: effectiveKey } = getTargetRoot(options.package, rawKey);
|
|
148
|
+
const projConfig = loadProjectConfig(root);
|
|
149
|
+
const entry = await target.getState(projConfig.id, actionId, effectiveKey, {
|
|
150
|
+
namespace: options.namespace,
|
|
151
|
+
detail: true,
|
|
152
|
+
});
|
|
153
|
+
if (entry === undefined || entry.value === undefined) {
|
|
154
|
+
throw new ExecutionError(`State key '${rawKey}' not found in package '${projConfig.id}'`);
|
|
155
|
+
}
|
|
156
|
+
const val = entry.value;
|
|
157
|
+
const matchedNamespace = entry.namespace;
|
|
158
|
+
renderResult({ key: rawKey, value: val, namespace: matchedNamespace }, {
|
|
159
|
+
json: options.json,
|
|
160
|
+
envelope: options.envelope,
|
|
161
|
+
humanFormatter: () => (typeof val === "object" ? JSON.stringify(val, null, 2) : String(val)),
|
|
162
|
+
context,
|
|
163
|
+
});
|
|
164
|
+
}, { localRoot: () => getTargetRoot(options.package, rawKey).root });
|
|
165
|
+
});
|
|
166
|
+
// state set <key> <value>
|
|
167
|
+
stateCmd
|
|
168
|
+
.command("set <key> <value>")
|
|
169
|
+
.description("Set state key-value (supports JSON value, composite 'ns:key', and --ttl)")
|
|
170
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
171
|
+
.option("-a, --action <id>", "Action identifier scope")
|
|
172
|
+
.option("-n, --namespace <ns>", "Explicit namespace scope for key")
|
|
173
|
+
.option("--ttl <seconds>", "Time-To-Live expiration in seconds")
|
|
174
|
+
.option("-p, --profile <name>", "Set state on a remote target")
|
|
175
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
176
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
177
|
+
.option("--data-dir <path>", "Custom database storage directory")
|
|
178
|
+
.action(async (rawKey, rawVal, rawOptions, cmd) => {
|
|
179
|
+
const options = getEffectiveOptions(rawOptions, cmd);
|
|
180
|
+
if (!rawKey || rawVal === undefined) {
|
|
181
|
+
throw new ArgumentError("Both key and value are required for state set");
|
|
182
|
+
}
|
|
183
|
+
let parsedVal = rawVal;
|
|
184
|
+
try {
|
|
185
|
+
parsedVal = JSON.parse(rawVal);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
parsedVal = rawVal;
|
|
189
|
+
}
|
|
190
|
+
const ttlSec = options.ttl ? parseInt(options.ttl, 10) : undefined;
|
|
191
|
+
if (options.ttl && (isNaN(ttlSec) || ttlSec <= 0)) {
|
|
192
|
+
throw new ArgumentError(`Invalid --ttl value: '${options.ttl}'. Must be a positive integer.`);
|
|
193
|
+
}
|
|
194
|
+
await withTarget(options, context, async (target, resolved) => {
|
|
195
|
+
const actionId = options.action || "";
|
|
196
|
+
if (resolved.type === "remote") {
|
|
197
|
+
const decoded = decodeStateKey(rawKey);
|
|
198
|
+
const effectiveNamespace = options.namespace || (decoded.namespace || undefined);
|
|
199
|
+
const actualKey = options.namespace ? rawKey : decoded.key;
|
|
200
|
+
await target.setState(options.package || "", actionId, actualKey, parsedVal, {
|
|
201
|
+
namespace: effectiveNamespace,
|
|
202
|
+
ttl: ttlSec,
|
|
203
|
+
});
|
|
204
|
+
writeStdout(`[OK] State '${rawKey}' updated on remote server`, context);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// 本地项目模式
|
|
208
|
+
const { root, key: effectiveKey } = getTargetRoot(options.package, rawKey);
|
|
209
|
+
const projConfig = loadProjectConfig(root);
|
|
210
|
+
let actualNamespace = options.namespace;
|
|
211
|
+
let finalKey = effectiveKey;
|
|
212
|
+
if (options.namespace === undefined && effectiveKey.includes(":")) {
|
|
213
|
+
const decoded = decodeStateKey(effectiveKey);
|
|
214
|
+
actualNamespace = decoded.namespace;
|
|
215
|
+
finalKey = decoded.key;
|
|
216
|
+
}
|
|
217
|
+
await target.setState(projConfig.id, actionId, finalKey, parsedVal, {
|
|
218
|
+
namespace: actualNamespace,
|
|
219
|
+
ttl: ttlSec,
|
|
220
|
+
});
|
|
221
|
+
const displayKey = actualNamespace ? `${actualNamespace}:${finalKey}` : finalKey;
|
|
222
|
+
writeStdout(`[OK] State '${displayKey}' updated in package '${projConfig.id}'`, context);
|
|
223
|
+
}, { localRoot: () => getTargetRoot(options.package, rawKey).root });
|
|
224
|
+
});
|
|
225
|
+
// state delete <key>
|
|
226
|
+
stateCmd
|
|
227
|
+
.command("delete <key>")
|
|
228
|
+
.alias("rm")
|
|
229
|
+
.description("Delete state key entry")
|
|
230
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
231
|
+
.option("-a, --action <id>", "Action identifier scope")
|
|
232
|
+
.option("-n, --namespace <ns>", "Explicit namespace scope for key")
|
|
233
|
+
.option("-p, --profile <name>", "Delete state on a remote target")
|
|
234
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
235
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
236
|
+
.option("--data-dir <path>", "Custom database storage directory")
|
|
237
|
+
.action(async (rawKey, rawOptions, cmd) => {
|
|
238
|
+
const options = getEffectiveOptions(rawOptions, cmd);
|
|
239
|
+
if (!rawKey) {
|
|
240
|
+
throw new ArgumentError("State key is required");
|
|
241
|
+
}
|
|
242
|
+
await withTarget(options, context, async (target, resolved) => {
|
|
243
|
+
const actionId = options.action || "";
|
|
244
|
+
if (resolved.type === "remote") {
|
|
245
|
+
const decoded = decodeStateKey(rawKey);
|
|
246
|
+
const effectiveNamespace = options.namespace || (decoded.namespace || undefined);
|
|
247
|
+
const actualKey = options.namespace ? rawKey : decoded.key;
|
|
248
|
+
const deleted = await target.deleteState(options.package || "", actionId, actualKey, {
|
|
249
|
+
namespace: effectiveNamespace,
|
|
250
|
+
});
|
|
251
|
+
if (!deleted) {
|
|
252
|
+
throw new ExecutionError(`State key '${rawKey}' not found on remote server`);
|
|
253
|
+
}
|
|
254
|
+
writeStdout(`[OK] State '${rawKey}' deleted on remote server`, context);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// 本地项目模式
|
|
258
|
+
const { root, key: effectiveKey } = getTargetRoot(options.package, rawKey);
|
|
259
|
+
const projConfig = loadProjectConfig(root);
|
|
260
|
+
const deleted = await target.deleteState(projConfig.id, actionId, effectiveKey, {
|
|
261
|
+
namespace: options.namespace,
|
|
262
|
+
});
|
|
263
|
+
if (!deleted) {
|
|
264
|
+
throw new ExecutionError(`State key '${rawKey}' not found in package '${projConfig.id}'`);
|
|
265
|
+
}
|
|
266
|
+
writeStdout(`[OK] State '${rawKey}' deleted from package '${projConfig.id}'`, context);
|
|
267
|
+
}, { localRoot: () => getTargetRoot(options.package, rawKey).root });
|
|
268
|
+
});
|
|
269
|
+
// state clear
|
|
270
|
+
stateCmd
|
|
271
|
+
.command("clear")
|
|
272
|
+
.alias("clean")
|
|
273
|
+
.description("Clear state entries (supports namespace cleanup or global wipe via --all)")
|
|
274
|
+
.option("-P, --package <id>", "Target package ID or path")
|
|
275
|
+
.option("--action <id>", "Action identifier scope")
|
|
276
|
+
.option("-n, --namespace <ns>", "Target namespace to clear (required unless --all is specified)")
|
|
277
|
+
.option("-a, --all", "Dangerously clear all state namespaces for the package")
|
|
278
|
+
.option("-p, --profile <name>", "Clear state on a remote target")
|
|
279
|
+
.option("-s, --server <url>", "Remote server URL")
|
|
280
|
+
.option("-t, --token <token>", "Auth token for remote server")
|
|
281
|
+
.option("--data-dir <path>", "Custom database storage directory")
|
|
282
|
+
.action(async (rawOptions, cmd) => {
|
|
283
|
+
const options = getEffectiveOptions(rawOptions, cmd);
|
|
284
|
+
if (!options.all && options.namespace === undefined) {
|
|
285
|
+
throw new ArgumentError("Must specify either -n/--namespace <name> to clear specific namespace, or --all to clear entire package state store.");
|
|
286
|
+
}
|
|
287
|
+
await withTarget(options, context, async (target, resolved) => {
|
|
288
|
+
const actionId = options.action || "";
|
|
289
|
+
if (resolved.type === "remote") {
|
|
290
|
+
const count = await target.clearState(options.package || "", actionId, {
|
|
291
|
+
namespace: options.namespace,
|
|
292
|
+
all: Boolean(options.all),
|
|
293
|
+
});
|
|
294
|
+
writeStdout(`[OK] Cleared ${count} state entry(s) on remote server`, context);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// 本地项目模式
|
|
298
|
+
const { root } = getTargetRoot(options.package);
|
|
299
|
+
const projConfig = loadProjectConfig(root);
|
|
300
|
+
const count = await target.clearState(projConfig.id, actionId, {
|
|
301
|
+
namespace: options.namespace,
|
|
302
|
+
all: Boolean(options.all),
|
|
303
|
+
});
|
|
304
|
+
const scopeDesc = options.all
|
|
305
|
+
? "all namespaces"
|
|
306
|
+
: `namespace '${options.namespace}'`;
|
|
307
|
+
writeStdout(`[OK] Cleared ${count} state entry(s) in ${scopeDesc} for package '${projConfig.id}'`, context);
|
|
308
|
+
}, { localRoot: () => getTargetRoot(options.package).root });
|
|
309
|
+
});
|
|
310
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { findProjectRoot } from "@actiondock/core";
|
|
4
|
+
import { spawnAsync } from "../utils/index.js";
|
|
5
|
+
export function registerTestCommand(program) {
|
|
6
|
+
program
|
|
7
|
+
.command("test [pattern]")
|
|
8
|
+
.description("Run project tests using configured test runner (node:test or bun test)")
|
|
9
|
+
.action(async (pattern) => {
|
|
10
|
+
const root = findProjectRoot();
|
|
11
|
+
const cwd = root || process.cwd();
|
|
12
|
+
const pkgJsonPath = join(cwd, "package.json");
|
|
13
|
+
let testCmd = "npm";
|
|
14
|
+
let testArgs = ["test"];
|
|
15
|
+
if (existsSync(pkgJsonPath)) {
|
|
16
|
+
try {
|
|
17
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
18
|
+
if (pkg.scripts?.test?.includes("bun test") && typeof globalThis.Bun !== "undefined") {
|
|
19
|
+
testCmd = "bun";
|
|
20
|
+
testArgs = ["test"];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// package.json 解析失败时回退 npm test
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (pattern) {
|
|
28
|
+
if (testCmd === "npm") {
|
|
29
|
+
testArgs.push("--", pattern);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
testArgs.push(pattern);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// stdio inherit 保持实时输出;进程异常(如命令不存在)时回退退出码 1
|
|
36
|
+
const proc = await spawnAsync(testCmd, testArgs, {
|
|
37
|
+
cwd,
|
|
38
|
+
stdio: "inherit",
|
|
39
|
+
}).catch(() => ({ status: 1, signal: null, stdout: "", stderr: "" }));
|
|
40
|
+
if (proc.status !== 0) {
|
|
41
|
+
process.exitCode = proc.status ?? 1;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { CliContext } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* 注册顶层统一 validate 命令:校验 Action 的 Schema 与方法定义完整性。
|
|
5
|
+
*
|
|
6
|
+
* @param program Commander 根程序对象
|
|
7
|
+
* @param context 命令行上下文
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerValidateCommand(program: Command, context?: CliContext): void;
|