@actiondock/cli 2.4.0 → 2.5.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/README.md +1 -1
- package/dist/commands/config/delete.js +3 -6
- package/dist/commands/config/env.js +9 -7
- package/dist/commands/config/get.js +9 -7
- package/dist/commands/config/list.js +14 -8
- package/dist/commands/config/set.js +3 -6
- package/dist/commands/describe.js +4 -6
- package/dist/commands/doctor.js +9 -7
- package/dist/commands/info.js +7 -6
- package/dist/commands/list.js +4 -6
- package/dist/commands/playbook.js +15 -12
- package/dist/commands/profile.js +58 -6
- package/dist/commands/run.js +5 -6
- package/dist/commands/runs.js +10 -21
- package/dist/commands/serve.js +56 -5
- package/dist/commands/state.js +13 -31
- package/dist/utils/cert.d.ts +42 -0
- package/dist/utils/cert.js +211 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/target.d.ts +14 -1
- package/dist/utils/target.js +30 -1
- package/package.json +8 -7
package/dist/commands/runs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { filterWithFallbackInfo, findProjectRoot, listLinkedPackages, loadProjectConfig, resolvePackageRoot, resolveTarget, } from "@actiondock/core";
|
|
2
2
|
import { ArgumentError, ExecutionError } from "../errors.js";
|
|
3
3
|
import { renderResult, renderRunDetail, renderRunsList } from "../renderer.js";
|
|
4
|
-
import { getEffectiveOptions, resolveIntent, withRemoteTarget, withTarget } from "../utils/index.js";
|
|
4
|
+
import { applyTargetOptions, getEffectiveOptions, resolveIntent, withRemoteTarget, withTarget, } from "../utils/index.js";
|
|
5
5
|
/**
|
|
6
6
|
* 解析本地目标包根目录与工程配置(仅在 local 分支调用)。
|
|
7
7
|
* 显式指定包且寻址失败时抛出参数错误;工程清单损坏时降级为链接包视图。
|
|
@@ -43,16 +43,13 @@ export function registerRunsCommands(program, context) {
|
|
|
43
43
|
.command("runs")
|
|
44
44
|
.description("Inspect action execution history");
|
|
45
45
|
// runs list
|
|
46
|
-
runsCmd
|
|
46
|
+
applyTargetOptions(runsCmd
|
|
47
47
|
.command("list [patterns...]")
|
|
48
48
|
.description("List recent execution records")
|
|
49
49
|
.option("-P, --package <id>", "Target package ID or path")
|
|
50
50
|
.option("-i, --intent <pattern>", "Regex or fuzzy intent filter; falls back to full list when no match")
|
|
51
51
|
.option("-a, --action <actionId>", "Filter by action ID")
|
|
52
|
-
.option("-n, --limit <count>", "Maximum number of records to return", "20")
|
|
53
|
-
.option("-p, --profile <name>", "Query against a specific profile")
|
|
54
|
-
.option("-s, --server <url>", "Remote server URL")
|
|
55
|
-
.option("-t, --token <token>", "Auth token for remote server")
|
|
52
|
+
.option("-n, --limit <count>", "Maximum number of records to return", "20"))
|
|
56
53
|
.option("--no-fallback", "Disable fallback to full list when no items match intent")
|
|
57
54
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
58
55
|
.option("--json", "Output as JSON")
|
|
@@ -107,13 +104,10 @@ export function registerRunsCommands(program, context) {
|
|
|
107
104
|
}, { localRoot: scope.targetPackageRoot, scanLinkedPackages: true });
|
|
108
105
|
});
|
|
109
106
|
// runs show <id>
|
|
110
|
-
runsCmd
|
|
107
|
+
applyTargetOptions(runsCmd
|
|
111
108
|
.command("show <id>")
|
|
112
109
|
.description("Show details of a specific execution run")
|
|
113
|
-
.option("-
|
|
114
|
-
.option("-s, --server <url>", "Remote server URL")
|
|
115
|
-
.option("-t, --token <token>", "Auth token for remote server")
|
|
116
|
-
.option("-P, --package <id>", "Target package ID or path")
|
|
110
|
+
.option("-P, --package <id>", "Target package ID or path"))
|
|
117
111
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
118
112
|
.option("--json", "Output as JSON")
|
|
119
113
|
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
@@ -146,12 +140,9 @@ export function registerRunsCommands(program, context) {
|
|
|
146
140
|
}, { localRoot: scope.targetPackageRoot, scanLinkedPackages: true });
|
|
147
141
|
});
|
|
148
142
|
// runs cancel
|
|
149
|
-
runsCmd
|
|
143
|
+
applyTargetOptions(runsCmd
|
|
150
144
|
.command("cancel <id>")
|
|
151
|
-
.description("Cancel a running action execution on a remote server")
|
|
152
|
-
.option("-p, --profile <name>", "Execute cancel against a specific profile")
|
|
153
|
-
.option("-s, --server <url>", "Remote server URL")
|
|
154
|
-
.option("-t, --token <token>", "Auth token for remote server")
|
|
145
|
+
.description("Cancel a running action execution on a remote server"))
|
|
155
146
|
.option("-r, --reason <reason>", "Reason for cancellation")
|
|
156
147
|
.option("--json", "Output as JSON")
|
|
157
148
|
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
@@ -164,6 +155,7 @@ export function registerRunsCommands(program, context) {
|
|
|
164
155
|
profile: options.profile,
|
|
165
156
|
server: options.server,
|
|
166
157
|
token: options.token,
|
|
158
|
+
insecure: options.insecure,
|
|
167
159
|
}, context?.customHome);
|
|
168
160
|
if (resolved.type === "local") {
|
|
169
161
|
throw new ArgumentError("'ad runs cancel' is only supported for remote execution targets. Use --profile <name> or --server <url>.");
|
|
@@ -191,14 +183,11 @@ export function registerRunsCommands(program, context) {
|
|
|
191
183
|
});
|
|
192
184
|
});
|
|
193
185
|
// runs clear
|
|
194
|
-
runsCmd
|
|
186
|
+
applyTargetOptions(runsCmd
|
|
195
187
|
.command("clear")
|
|
196
188
|
.description("Clear execution run records")
|
|
197
189
|
.option("-P, --package <id>", "Target package ID or path")
|
|
198
|
-
.option("-a, --action <actionId>", "Filter by action ID")
|
|
199
|
-
.option("-p, --profile <name>", "Target profile")
|
|
200
|
-
.option("-s, --server <url>", "Remote server URL")
|
|
201
|
-
.option("-t, --token <token>", "Auth token for remote server")
|
|
190
|
+
.option("-a, --action <actionId>", "Filter by action ID"))
|
|
202
191
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
203
192
|
.option("--json", "Output as JSON")
|
|
204
193
|
.option("--envelope", "Wrap JSON output in standard envelope")
|
package/dist/commands/serve.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { resolve } from "node:path";
|
|
2
|
-
import { findProjectRoot, formatHostForUrl, loadProjectConfig, startActionDockServer } from "@actiondock/core";
|
|
3
|
+
import { findProjectRoot, formatHostForUrl, loadProjectConfig, startActionDockServer, } from "@actiondock/core";
|
|
3
4
|
import { ArgumentError, ExecutionError } from "../errors.js";
|
|
4
5
|
import { writeStderr, writeStdout } from "../renderer.js";
|
|
5
|
-
import { getEffectiveOptions, parseByteSize } from "../utils/index.js";
|
|
6
|
+
import { ensureSelfSignedCertificate, getEffectiveOptions, parseByteSize } from "../utils/index.js";
|
|
6
7
|
/**
|
|
7
8
|
* 注册 serve HTTP 服务启动命令。
|
|
8
9
|
*
|
|
@@ -23,6 +24,11 @@ export function registerServeCommand(program, context) {
|
|
|
23
24
|
.option("--no-mcp", "Disable unified MCP protocol endpoint at /mcp")
|
|
24
25
|
.option("--allow-query-token", "Allow passing authentication token via URL query parameter (?token=xxx)")
|
|
25
26
|
.option("--management", "Enable config and state management API endpoints (disabled by default for security)")
|
|
27
|
+
.option("--https", "Enable native HTTPS transport (auto-generates self-signed certificate if none provided)")
|
|
28
|
+
.option("--tls-cert <path>", "Path to TLS certificate file (or set ACTIONDOCK_TLS_CERT)")
|
|
29
|
+
.option("--tls-key <path>", "Path to TLS private key file (or set ACTIONDOCK_TLS_KEY)")
|
|
30
|
+
.option("--tls-ca <path>", "Path to TLS CA certificate file (or set ACTIONDOCK_TLS_CA)")
|
|
31
|
+
.option("--tls-passphrase <passphrase>", "Passphrase for TLS private key (or set ACTIONDOCK_TLS_PASSPHRASE)")
|
|
26
32
|
.option("-d, --dir <path>", "Project root directory (default: current working directory)")
|
|
27
33
|
.action(async (rawOptions, cmd) => {
|
|
28
34
|
const options = getEffectiveOptions(rawOptions, cmd);
|
|
@@ -34,6 +40,45 @@ export function registerServeCommand(program, context) {
|
|
|
34
40
|
const enableManagement = Boolean(options.management);
|
|
35
41
|
const corsOrigins = options.corsOrigin && options.corsOrigin.length > 0 ? options.corsOrigin : undefined;
|
|
36
42
|
const exposeDebugInfo = Boolean(options.exposeDebugInfo);
|
|
43
|
+
const httpsEnabled = Boolean(options.https ||
|
|
44
|
+
options.tlsCert ||
|
|
45
|
+
options.tlsKey ||
|
|
46
|
+
process.env?.ACTIONDOCK_HTTPS === "true" ||
|
|
47
|
+
process.env?.ACTIONDOCK_HTTPS === "1" ||
|
|
48
|
+
process.env?.ACTIONDOCK_TLS_CERT ||
|
|
49
|
+
process.env?.ACTIONDOCK_TLS_KEY);
|
|
50
|
+
let tls;
|
|
51
|
+
if (httpsEnabled) {
|
|
52
|
+
const certPath = options.tlsCert || (typeof process !== "undefined" ? process.env?.ACTIONDOCK_TLS_CERT : undefined);
|
|
53
|
+
const keyPath = options.tlsKey || (typeof process !== "undefined" ? process.env?.ACTIONDOCK_TLS_KEY : undefined);
|
|
54
|
+
const caPath = options.tlsCa || (typeof process !== "undefined" ? process.env?.ACTIONDOCK_TLS_CA : undefined);
|
|
55
|
+
const passphrase = options.tlsPassphrase || (typeof process !== "undefined" ? process.env?.ACTIONDOCK_TLS_PASSPHRASE : undefined);
|
|
56
|
+
if (certPath && keyPath) {
|
|
57
|
+
try {
|
|
58
|
+
tls = {
|
|
59
|
+
certPath: resolve(certPath),
|
|
60
|
+
keyPath: resolve(keyPath),
|
|
61
|
+
ca: caPath ? readFileSync(resolve(caPath), "utf-8") : undefined,
|
|
62
|
+
passphrase,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
throw new ArgumentError(`Failed to read TLS certificate/key files: ${err.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (certPath || keyPath) {
|
|
70
|
+
throw new ArgumentError("Both --tls-cert and --tls-key must be provided together");
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const selfSigned = await ensureSelfSignedCertificate({ host });
|
|
74
|
+
tls = {
|
|
75
|
+
cert: selfSigned.cert,
|
|
76
|
+
key: selfSigned.key,
|
|
77
|
+
certPath: selfSigned.certPath,
|
|
78
|
+
keyPath: selfSigned.keyPath,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
37
82
|
let maxBodyBytes;
|
|
38
83
|
if (options.maxBody) {
|
|
39
84
|
try {
|
|
@@ -95,18 +140,24 @@ export function registerServeCommand(program, context) {
|
|
|
95
140
|
enableMcp,
|
|
96
141
|
enableManagement,
|
|
97
142
|
mcpHandler,
|
|
143
|
+
tls,
|
|
98
144
|
projectRoot: projectRoot || undefined,
|
|
99
145
|
});
|
|
100
146
|
const displayHost = formatHostForUrl(host);
|
|
101
147
|
const actualEndpointHost = formatHostForUrl(host === "0.0.0.0" ? "127.0.0.1" : host);
|
|
148
|
+
const scheme = tls ? "https" : "http";
|
|
102
149
|
writeStdout(`\n======================================================`, context);
|
|
103
150
|
writeStdout(` ActionDock 2.0 HTTP Runner Server`, context);
|
|
104
151
|
writeStdout(`======================================================`, context);
|
|
105
|
-
writeStdout(` * Listening on:
|
|
152
|
+
writeStdout(` * Listening on: ${scheme}://${displayHost}:${server.port}`, context);
|
|
106
153
|
writeStdout(` * Project: ${projectName}`, context);
|
|
107
154
|
if (projectRoot && exposeDebugInfo) {
|
|
108
155
|
writeStdout(` * Root Path: ${projectRoot}`, context);
|
|
109
156
|
}
|
|
157
|
+
if (tls) {
|
|
158
|
+
const tlsDesc = tls.certPath ? `Enabled (${tls.certPath})` : "Enabled (Self-Signed)";
|
|
159
|
+
writeStdout(` * TLS / HTTPS: ${tlsDesc}`, context);
|
|
160
|
+
}
|
|
110
161
|
const authDesc = token
|
|
111
162
|
? (allowQueryToken ? "Bearer Token / Query Token Enabled" : "Bearer Token Enabled (Query Token Disabled)")
|
|
112
163
|
: "Disabled (Public/Local)";
|
|
@@ -114,9 +165,9 @@ export function registerServeCommand(program, context) {
|
|
|
114
165
|
writeStdout(` * Management APIs: ${enableManagement ? "Enabled" : "Disabled (Default)"}`, context);
|
|
115
166
|
writeStdout(` * CORS Origins: ${corsOrigins ? corsOrigins.join(", ") : "Disabled (Default)"}`, context);
|
|
116
167
|
writeStdout(` * Max Body Size: ${options.maxBody || "1mb"}`, context);
|
|
117
|
-
writeStdout(` * Health Endpoint:
|
|
168
|
+
writeStdout(` * Health Endpoint: ${scheme}://${actualEndpointHost}:${server.port}/api/v1/health`, context);
|
|
118
169
|
if (enableMcp) {
|
|
119
|
-
writeStdout(` * MCP Endpoint:
|
|
170
|
+
writeStdout(` * MCP Endpoint: ${scheme}://${actualEndpointHost}:${server.port}/mcp`, context);
|
|
120
171
|
}
|
|
121
172
|
writeStdout(`======================================================\n`, context);
|
|
122
173
|
writeStdout(`Server is ready to accept remote requests.`, context);
|
package/dist/commands/state.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { decodeStateKey, findProjectRoot, loadProjectConfig, resolvePackageRoot, } from "@actiondock/core";
|
|
2
2
|
import { ArgumentError, ExecutionError } from "../errors.js";
|
|
3
3
|
import { renderResult, renderStateList, writeStdout } from "../renderer.js";
|
|
4
|
-
import { getEffectiveOptions, getTargetRoot, resolveIntent, withTarget } from "../utils/index.js";
|
|
4
|
+
import { applyTargetOptions, getEffectiveOptions, getTargetRoot, resolveIntent, withTarget, } from "../utils/index.js";
|
|
5
5
|
import { renderLinkedPackagesStateList, renderProjectScopedStateList, } from "./state-list.js";
|
|
6
6
|
/**
|
|
7
7
|
* 注册 state 状态管理命令(get、set、delete、clear、keys、list)。
|
|
@@ -70,15 +70,12 @@ export function registerStateCommands(program, context) {
|
|
|
70
70
|
});
|
|
71
71
|
};
|
|
72
72
|
// state list
|
|
73
|
-
stateCmd
|
|
73
|
+
applyTargetOptions(stateCmd
|
|
74
74
|
.command("list [prefix]")
|
|
75
75
|
.description("List state keys in current project or linked packages")
|
|
76
76
|
.option("-P, --package <id>", "Target package ID or path")
|
|
77
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")
|
|
78
|
+
.option("-n, --namespace <ns>", "Filter keys under specific namespace (omit to list all namespaces)"))
|
|
82
79
|
.option("-i, --intent <pattern>", "Regex or fuzzy intent filter; falls back to full list when no match")
|
|
83
80
|
.option("--detail", "Include metadata (ttl, expiresAt, size, updatedAt) in JSON output")
|
|
84
81
|
.option("--no-fallback", "Disable fallback to full list when no items match intent")
|
|
@@ -87,15 +84,12 @@ export function registerStateCommands(program, context) {
|
|
|
87
84
|
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
88
85
|
.action(handleListKeys);
|
|
89
86
|
// state keys 别名
|
|
90
|
-
stateCmd
|
|
87
|
+
applyTargetOptions(stateCmd
|
|
91
88
|
.command("keys [prefix]")
|
|
92
89
|
.description("Alias for 'ad state list [prefix]'")
|
|
93
90
|
.option("-P, --package <id>", "Target package ID or path")
|
|
94
91
|
.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")
|
|
92
|
+
.option("-n, --namespace <ns>", "Filter keys under specific namespace"))
|
|
99
93
|
.option("-i, --intent <pattern>", "Regex or fuzzy intent filter")
|
|
100
94
|
.option("--detail", "Include metadata in JSON output")
|
|
101
95
|
.option("--no-fallback", "Disable fallback")
|
|
@@ -104,15 +98,12 @@ export function registerStateCommands(program, context) {
|
|
|
104
98
|
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
105
99
|
.action(handleListKeys);
|
|
106
100
|
// state get <key>
|
|
107
|
-
stateCmd
|
|
101
|
+
applyTargetOptions(stateCmd
|
|
108
102
|
.command("get <key>")
|
|
109
103
|
.description("Get state value by key (supports composite 'ns:key' and detail mode)")
|
|
110
104
|
.option("-P, --package <id>", "Target package ID or path")
|
|
111
105
|
.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")
|
|
106
|
+
.option("-n, --namespace <ns>", "Explicit namespace scope for key"))
|
|
116
107
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
117
108
|
.option("--json", "Output as JSON")
|
|
118
109
|
.option("--envelope", "Wrap JSON output in standard envelope")
|
|
@@ -164,16 +155,13 @@ export function registerStateCommands(program, context) {
|
|
|
164
155
|
}, { localRoot: () => getTargetRoot(options.package, rawKey).root });
|
|
165
156
|
});
|
|
166
157
|
// state set <key> <value>
|
|
167
|
-
stateCmd
|
|
158
|
+
applyTargetOptions(stateCmd
|
|
168
159
|
.command("set <key> <value>")
|
|
169
160
|
.description("Set state key-value (supports JSON value, composite 'ns:key', and --ttl)")
|
|
170
161
|
.option("-P, --package <id>", "Target package ID or path")
|
|
171
162
|
.option("-a, --action <id>", "Action identifier scope")
|
|
172
163
|
.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")
|
|
164
|
+
.option("--ttl <seconds>", "Time-To-Live expiration in seconds"))
|
|
177
165
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
178
166
|
.action(async (rawKey, rawVal, rawOptions, cmd) => {
|
|
179
167
|
const options = getEffectiveOptions(rawOptions, cmd);
|
|
@@ -223,16 +211,13 @@ export function registerStateCommands(program, context) {
|
|
|
223
211
|
}, { localRoot: () => getTargetRoot(options.package, rawKey).root });
|
|
224
212
|
});
|
|
225
213
|
// state delete <key>
|
|
226
|
-
stateCmd
|
|
214
|
+
applyTargetOptions(stateCmd
|
|
227
215
|
.command("delete <key>")
|
|
228
216
|
.alias("rm")
|
|
229
217
|
.description("Delete state key entry")
|
|
230
218
|
.option("-P, --package <id>", "Target package ID or path")
|
|
231
219
|
.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")
|
|
220
|
+
.option("-n, --namespace <ns>", "Explicit namespace scope for key"))
|
|
236
221
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
237
222
|
.action(async (rawKey, rawOptions, cmd) => {
|
|
238
223
|
const options = getEffectiveOptions(rawOptions, cmd);
|
|
@@ -267,17 +252,14 @@ export function registerStateCommands(program, context) {
|
|
|
267
252
|
}, { localRoot: () => getTargetRoot(options.package, rawKey).root });
|
|
268
253
|
});
|
|
269
254
|
// state clear
|
|
270
|
-
stateCmd
|
|
255
|
+
applyTargetOptions(stateCmd
|
|
271
256
|
.command("clear")
|
|
272
257
|
.alias("clean")
|
|
273
258
|
.description("Clear state entries (supports namespace cleanup or global wipe via --all)")
|
|
274
259
|
.option("-P, --package <id>", "Target package ID or path")
|
|
275
260
|
.option("--action <id>", "Action identifier scope")
|
|
276
261
|
.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")
|
|
262
|
+
.option("-a, --all", "Dangerously clear all state namespaces for the package"))
|
|
281
263
|
.option("--data-dir <path>", "Custom database storage directory")
|
|
282
264
|
.action(async (rawOptions, cmd) => {
|
|
283
265
|
const options = getEffectiveOptions(rawOptions, cmd);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 证书与私钥数据对。
|
|
3
|
+
*/
|
|
4
|
+
export interface CertificatePair {
|
|
5
|
+
/** 证书 PEM 文本内容 */
|
|
6
|
+
cert: string;
|
|
7
|
+
/** 私钥 PEM 文本内容 */
|
|
8
|
+
key: string;
|
|
9
|
+
/** 证书物理文件路径 */
|
|
10
|
+
certPath: string;
|
|
11
|
+
/** 私钥物理文件路径 */
|
|
12
|
+
keyPath: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 收集自签名证书所需的 SAN (Subject Alternative Name) 扩展项列表。
|
|
16
|
+
* 全面支持 localhost、127.0.0.1、::1、系统主机名以及绑定 0.0.0.0 时的本机局域网 IP。
|
|
17
|
+
*/
|
|
18
|
+
export declare function collectSanEntries(bindHost?: string): Array<{
|
|
19
|
+
type: 2 | 7;
|
|
20
|
+
value?: string;
|
|
21
|
+
ip?: string;
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* 校验证书是否在有效期内且剩余天数大于设定阈值。
|
|
25
|
+
*/
|
|
26
|
+
export declare function isCertificateValid(certPem: string, minRemainingDays?: number): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* 校验证书是否涵盖目标主机名或 IP 地址。
|
|
29
|
+
* 读取 X509 证书的 subjectAltName,检验当前绑定主机是否已被该证书覆盖。
|
|
30
|
+
*
|
|
31
|
+
* @param certPem 证书 PEM 文本
|
|
32
|
+
* @param host 待校验的目标主机名或 IP 地址
|
|
33
|
+
*/
|
|
34
|
+
export declare function certificateCoversHost(certPem: string, host?: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* 自动加载或重新签发自签名证书。
|
|
37
|
+
* 遵循严格的存储路径规范、有效性校验与原子文件写入机制。
|
|
38
|
+
*/
|
|
39
|
+
export declare function ensureSelfSignedCertificate(options?: {
|
|
40
|
+
customHome?: string;
|
|
41
|
+
host?: string;
|
|
42
|
+
}): Promise<CertificatePair>;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { X509Certificate, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { isIP } from "node:net";
|
|
4
|
+
import { hostname, networkInterfaces } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { getActionDockHome } from "@actiondock/core";
|
|
7
|
+
import { generate } from "selfsigned";
|
|
8
|
+
/**
|
|
9
|
+
* 判定字符串是否为有效 IPv4 或 IPv6 地址。
|
|
10
|
+
*/
|
|
11
|
+
function isIpAddress(str) {
|
|
12
|
+
return isIP(str) !== 0;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* 收集自签名证书所需的 SAN (Subject Alternative Name) 扩展项列表。
|
|
16
|
+
* 全面支持 localhost、127.0.0.1、::1、系统主机名以及绑定 0.0.0.0 时的本机局域网 IP。
|
|
17
|
+
*/
|
|
18
|
+
export function collectSanEntries(bindHost) {
|
|
19
|
+
const dnsSet = new Set();
|
|
20
|
+
const ipSet = new Set();
|
|
21
|
+
// 基础回环名称与 IP
|
|
22
|
+
dnsSet.add("localhost");
|
|
23
|
+
ipSet.add("127.0.0.1");
|
|
24
|
+
ipSet.add("::1");
|
|
25
|
+
// 系统主机名
|
|
26
|
+
const sysHostname = hostname();
|
|
27
|
+
if (sysHostname) {
|
|
28
|
+
if (isIpAddress(sysHostname)) {
|
|
29
|
+
ipSet.add(sysHostname);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
dnsSet.add(sysHostname);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// 显式指定的主机地址
|
|
36
|
+
if (bindHost && bindHost !== "0.0.0.0" && bindHost !== "::") {
|
|
37
|
+
if (isIpAddress(bindHost)) {
|
|
38
|
+
ipSet.add(bindHost);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
dnsSet.add(bindHost);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// 当绑定 0.0.0.0 或未指定时,枚举全部本机网络接口地址
|
|
45
|
+
if (!bindHost || bindHost === "0.0.0.0" || bindHost === "::") {
|
|
46
|
+
const interfaces = networkInterfaces();
|
|
47
|
+
for (const ifaceList of Object.values(interfaces)) {
|
|
48
|
+
if (!ifaceList)
|
|
49
|
+
continue;
|
|
50
|
+
for (const iface of ifaceList) {
|
|
51
|
+
if (!iface.address)
|
|
52
|
+
continue;
|
|
53
|
+
let addr = iface.address;
|
|
54
|
+
const percentIdx = addr.indexOf("%");
|
|
55
|
+
if (percentIdx !== -1) {
|
|
56
|
+
addr = addr.slice(0, percentIdx);
|
|
57
|
+
}
|
|
58
|
+
ipSet.add(addr);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const entries = [];
|
|
63
|
+
for (const dns of dnsSet) {
|
|
64
|
+
entries.push({ type: 2, value: dns });
|
|
65
|
+
}
|
|
66
|
+
for (const ip of ipSet) {
|
|
67
|
+
entries.push({ type: 7, ip });
|
|
68
|
+
}
|
|
69
|
+
return entries;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 校验证书是否在有效期内且剩余天数大于设定阈值。
|
|
73
|
+
*/
|
|
74
|
+
export function isCertificateValid(certPem, minRemainingDays = 7) {
|
|
75
|
+
try {
|
|
76
|
+
const x509 = new X509Certificate(certPem);
|
|
77
|
+
const validToMs = new Date(x509.validTo).getTime();
|
|
78
|
+
const minRemainingMs = minRemainingDays * 24 * 60 * 60 * 1000;
|
|
79
|
+
return validToMs - Date.now() > minRemainingMs;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 校验证书是否涵盖目标主机名或 IP 地址。
|
|
87
|
+
* 读取 X509 证书的 subjectAltName,检验当前绑定主机是否已被该证书覆盖。
|
|
88
|
+
*
|
|
89
|
+
* @param certPem 证书 PEM 文本
|
|
90
|
+
* @param host 待校验的目标主机名或 IP 地址
|
|
91
|
+
*/
|
|
92
|
+
export function certificateCoversHost(certPem, host) {
|
|
93
|
+
try {
|
|
94
|
+
const x509 = new X509Certificate(certPem);
|
|
95
|
+
const san = x509.subjectAltName;
|
|
96
|
+
if (!san) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const dnsNames = new Set();
|
|
100
|
+
const ipAddresses = new Set();
|
|
101
|
+
const parts = san.split(",").map((s) => s.trim());
|
|
102
|
+
for (const part of parts) {
|
|
103
|
+
if (part.startsWith("DNS:")) {
|
|
104
|
+
dnsNames.add(part.slice(4).trim().toLowerCase());
|
|
105
|
+
}
|
|
106
|
+
else if (part.startsWith("IP Address:")) {
|
|
107
|
+
ipAddresses.add(part.slice(11).trim().toLowerCase());
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// 基础回环覆盖检查
|
|
111
|
+
if (!dnsNames.has("localhost") || !ipAddresses.has("127.0.0.1")) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
// 若未显式指定主机,或指定为通配地址,需保证所有当前活动网卡 IP 均已被覆盖
|
|
115
|
+
if (!host || host === "0.0.0.0" || host === "::") {
|
|
116
|
+
const interfaces = networkInterfaces();
|
|
117
|
+
for (const ifaceList of Object.values(interfaces)) {
|
|
118
|
+
if (!ifaceList)
|
|
119
|
+
continue;
|
|
120
|
+
for (const iface of ifaceList) {
|
|
121
|
+
if (!iface.address)
|
|
122
|
+
continue;
|
|
123
|
+
let addr = iface.address.toLowerCase();
|
|
124
|
+
const percentIdx = addr.indexOf("%");
|
|
125
|
+
if (percentIdx !== -1) {
|
|
126
|
+
addr = addr.slice(0, percentIdx);
|
|
127
|
+
}
|
|
128
|
+
if (!ipAddresses.has(addr)) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
const trimmedHost = host.trim().toLowerCase();
|
|
136
|
+
if (isIpAddress(trimmedHost)) {
|
|
137
|
+
return ipAddresses.has(trimmedHost);
|
|
138
|
+
}
|
|
139
|
+
return dnsNames.has(trimmedHost);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 自动加载或重新签发自签名证书。
|
|
147
|
+
* 遵循严格的存储路径规范、有效性校验与原子文件写入机制。
|
|
148
|
+
*/
|
|
149
|
+
export async function ensureSelfSignedCertificate(options) {
|
|
150
|
+
const baseHome = getActionDockHome(options?.customHome);
|
|
151
|
+
const certDir = join(baseHome, ".actiondock", "certs");
|
|
152
|
+
const certPath = join(certDir, "localhost.crt");
|
|
153
|
+
const keyPath = join(certDir, "localhost.key");
|
|
154
|
+
if (existsSync(certPath) && existsSync(keyPath)) {
|
|
155
|
+
try {
|
|
156
|
+
const cert = readFileSync(certPath, "utf-8");
|
|
157
|
+
const key = readFileSync(keyPath, "utf-8");
|
|
158
|
+
if (isCertificateValid(cert, 7) && certificateCoversHost(cert, options?.host)) {
|
|
159
|
+
return { cert, key, certPath, keyPath };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// 忽略读取失败并触发重新签发
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
mkdirSync(certDir, { recursive: true, mode: 0o700 });
|
|
167
|
+
try {
|
|
168
|
+
chmodSync(certDir, 0o700);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// 忽略不支持 chmod 的环境
|
|
172
|
+
}
|
|
173
|
+
const altNames = collectSanEntries(options?.host);
|
|
174
|
+
const notAfterDate = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000);
|
|
175
|
+
const pems = await generate([{ name: "commonName", value: "localhost" }], {
|
|
176
|
+
notAfterDate,
|
|
177
|
+
keySize: 2048,
|
|
178
|
+
algorithm: "sha256",
|
|
179
|
+
extensions: [
|
|
180
|
+
{
|
|
181
|
+
name: "subjectAltName",
|
|
182
|
+
altNames,
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
const nonce = `${Date.now()}.${randomUUID()}`;
|
|
187
|
+
const tmpKeyPath = join(certDir, `localhost.key.tmp.${nonce}`);
|
|
188
|
+
const tmpCertPath = join(certDir, `localhost.crt.tmp.${nonce}`);
|
|
189
|
+
writeFileSync(tmpKeyPath, pems.private, { encoding: "utf-8", mode: 0o600 });
|
|
190
|
+
try {
|
|
191
|
+
chmodSync(tmpKeyPath, 0o600);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// 忽略不支持 chmod 的环境
|
|
195
|
+
}
|
|
196
|
+
writeFileSync(tmpCertPath, pems.cert, { encoding: "utf-8", mode: 0o644 });
|
|
197
|
+
renameSync(tmpKeyPath, keyPath);
|
|
198
|
+
try {
|
|
199
|
+
chmodSync(keyPath, 0o600);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// 忽略不支持 chmod 的环境
|
|
203
|
+
}
|
|
204
|
+
renameSync(tmpCertPath, certPath);
|
|
205
|
+
return {
|
|
206
|
+
cert: pems.cert,
|
|
207
|
+
key: pems.private,
|
|
208
|
+
certPath,
|
|
209
|
+
keyPath,
|
|
210
|
+
};
|
|
211
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
package/dist/utils/target.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { type ActionDockTarget, type ResolvedTarget } from "@actiondock/core";
|
|
2
|
+
import type { Command } from "commander";
|
|
2
3
|
import type { CliContext } from "../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* 统一为 Commander 命令节点挂载目标环境解析相关选项(单一事实源)。
|
|
6
|
+
* 包括:-p/--profile, -s/--server, -t/--token, -k/--insecure, --allow-insecure-http。
|
|
7
|
+
*
|
|
8
|
+
* @param command 目标 Commander 命令节点
|
|
9
|
+
* @returns 挂载选项后的命令节点
|
|
10
|
+
*/
|
|
11
|
+
export declare function applyTargetOptions(command: Command): Command;
|
|
3
12
|
/**
|
|
4
13
|
* 目标解析与 Target 创建所需的统一选项视图。
|
|
5
14
|
* 与既有命令内联解析逻辑保持逐字段一致,作为唯一事实源供各命令复用。
|
|
@@ -15,6 +24,10 @@ export interface TargetResolutionOptions {
|
|
|
15
24
|
token?: string;
|
|
16
25
|
/** 自定义数据目录(对应 --data-dir) */
|
|
17
26
|
dataDir?: string;
|
|
27
|
+
/** 是否跳过 TLS 证书合法性校验(对应 -k, --insecure) */
|
|
28
|
+
insecure?: boolean;
|
|
29
|
+
/** 是否允许向非回环地址发送明文 HTTP 请求(对应 --allow-insecure-http) */
|
|
30
|
+
allowInsecureHttp?: boolean;
|
|
18
31
|
}
|
|
19
32
|
/**
|
|
20
33
|
* 高阶目标执行辅助:统一封装「目标解析 → Target 创建 → 业务回调 → try/finally close」样板。
|
|
@@ -22,7 +35,7 @@ export interface TargetResolutionOptions {
|
|
|
22
35
|
* 回调获得创建好的 target 与解析后的目标拓扑信息;无论回调成功或抛出,
|
|
23
36
|
* 都保证 target 资源被正确释放,业务异常原样透传。
|
|
24
37
|
*
|
|
25
|
-
* @param options 命令选项视图(profile/server/token/package/dataDir)
|
|
38
|
+
* @param options 命令选项视图(profile/server/token/package/dataDir/insecure/allowInsecureHttp)
|
|
26
39
|
* @param context CLI 上下文
|
|
27
40
|
* @param fn 业务回调(target 为已创建的门面实例,resolved 为拓扑解析结果)
|
|
28
41
|
* @param localOptions 本地分支附加选项(如 scanLinkedPackages)与 localRoot 回退策略
|