@tomflow/proflow-dev-tunnel 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/DOCS.md +7 -6
- package/README.md +3 -2
- package/SETUP.md +23 -26
- package/dist/deployment/adapter.d.ts +185 -85
- package/dist/deployment/adapter.js +112 -163
- package/dist/deployment/descriptor.d.ts +2 -2
- package/dist/deployment/descriptor.js +2 -2
- package/dist/deployment/requirements.d.ts +1 -1
- package/dist/src/cli.js +25 -127
- package/dist/src/resource-adapter.d.ts +33 -0
- package/dist/src/resource-adapter.js +271 -5
- package/package.json +4 -3
- package/proflow.module.json +2 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
-
import { writeModuleSharedFacts, } from "@tomflow/proflow-module-contract";
|
|
5
|
-
import { createDevTunnelRuntime } from "../src/resource-adapter.js";
|
|
4
|
+
import { readModuleSharedFacts, writeModuleSharedFacts, } from "@tomflow/proflow-module-contract";
|
|
5
|
+
import { createDevTunnelAutomation, createDevTunnelRuntime, verifyProvisionedPublicBaseUrl, } from "../src/resource-adapter.js";
|
|
6
6
|
import { descriptor } from "./descriptor.js";
|
|
7
7
|
const base = {
|
|
8
8
|
contract: "deployment.result.v1",
|
|
@@ -11,77 +11,6 @@ const base = {
|
|
|
11
11
|
moduleRef: descriptor.moduleRef,
|
|
12
12
|
moduleVersion: descriptor.moduleVersion,
|
|
13
13
|
};
|
|
14
|
-
const setupPlan = {
|
|
15
|
-
steps: [
|
|
16
|
-
{
|
|
17
|
-
id: "STEP-DEV-TUNNEL-01",
|
|
18
|
-
title: "检查 Dev Tunnel CLI 与登录",
|
|
19
|
-
description: "确认 CLI 可用,必要时启动 Microsoft 登录。",
|
|
20
|
-
state: "TODO",
|
|
21
|
-
responsible: "USER",
|
|
22
|
-
execution: {
|
|
23
|
-
interactive: "pnpm exec -- proflow-dev-tunnel setup 01",
|
|
24
|
-
nonInteractive: "pnpm exec -- proflow-dev-tunnel setup 01",
|
|
25
|
-
},
|
|
26
|
-
requiredInputs: [],
|
|
27
|
-
verify: "devtunnel user show",
|
|
28
|
-
successCondition: "Microsoft 登录状态可被 CLI 观察",
|
|
29
|
-
humanAction: "若浏览器打开 Microsoft 登录页,完成账号登录。",
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
id: "STEP-DEV-TUNNEL-02",
|
|
33
|
-
title: "选择或创建持久 Tunnel",
|
|
34
|
-
description: "列出现有 Tunnel;不存在时由脚本创建。",
|
|
35
|
-
state: "TODO",
|
|
36
|
-
responsible: "USER",
|
|
37
|
-
execution: {
|
|
38
|
-
interactive: "pnpm exec -- proflow-dev-tunnel setup 02",
|
|
39
|
-
nonInteractive: "pnpm exec -- proflow-dev-tunnel setup 02 --tunnel-id <id>",
|
|
40
|
-
},
|
|
41
|
-
requiredInputs: [
|
|
42
|
-
{ name: "tunnelId", description: "Tunnel ID", sensitive: false },
|
|
43
|
-
],
|
|
44
|
-
verify: "devtunnel show <id>",
|
|
45
|
-
successCondition: "持久 Tunnel 可以被 devtunnel show 观察",
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
id: "STEP-DEV-TUNNEL-03",
|
|
49
|
-
title: "配置入口并保存公开 URL",
|
|
50
|
-
description: "为 Platform Host 建立映射,保存公开 HTTPS URL。",
|
|
51
|
-
state: "TODO",
|
|
52
|
-
responsible: "USER",
|
|
53
|
-
execution: {
|
|
54
|
-
interactive: "pnpm exec -- proflow-dev-tunnel setup 03",
|
|
55
|
-
nonInteractive: "pnpm exec -- proflow-dev-tunnel setup 03 --tunnel-id <id> --public-base-url <url>",
|
|
56
|
-
},
|
|
57
|
-
requiredInputs: [
|
|
58
|
-
{ name: "tunnelId", description: "Tunnel ID", sensitive: false },
|
|
59
|
-
{
|
|
60
|
-
name: "publicBaseUrl",
|
|
61
|
-
description: "公开 HTTPS URL",
|
|
62
|
-
sensitive: false,
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
verify: "pnpm exec -- proflow-dev-tunnel verify",
|
|
66
|
-
successCondition: "公开 HTTPS URL 已保存并可验证",
|
|
67
|
-
humanAction: "确认 Dev Tunnel 门户或 CLI 显示的公开 HTTPS URL。",
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
id: "STEP-DEV-TUNNEL-04",
|
|
71
|
-
title: "验证 Dev Tunnel",
|
|
72
|
-
description: "检查登录、配置和公开入口。",
|
|
73
|
-
state: "TODO",
|
|
74
|
-
responsible: "AI",
|
|
75
|
-
execution: {
|
|
76
|
-
interactive: "pnpm exec -- proflow-dev-tunnel setup 04",
|
|
77
|
-
nonInteractive: "pnpm exec -- proflow-dev-tunnel verify",
|
|
78
|
-
},
|
|
79
|
-
requiredInputs: [],
|
|
80
|
-
verify: "pnpm exec -- proflow-dev-tunnel verify",
|
|
81
|
-
successCondition: "dev-tunnel.setupStatus=READY",
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
};
|
|
85
14
|
const processEffect = "Manage the dev-tunnel public ingress process";
|
|
86
15
|
const stateDir = (context) => join(resolve(context.workspaceRoot), ".proflow", "runtime", "external-resources", "dev-tunnel");
|
|
87
16
|
const stateFile = (context) => join(stateDir(context), "setup.json");
|
|
@@ -110,20 +39,6 @@ async function writeState(context, state) {
|
|
|
110
39
|
`, { encoding: "utf8", mode: 0o600 });
|
|
111
40
|
await rename(tmp, stateFile(context));
|
|
112
41
|
}
|
|
113
|
-
function setupInput(context) {
|
|
114
|
-
if (typeof context.input !== "object" ||
|
|
115
|
-
context.input === null ||
|
|
116
|
-
Array.isArray(context.input))
|
|
117
|
-
return {};
|
|
118
|
-
const tunnelId = Reflect.get(context.input, "tunnelId");
|
|
119
|
-
const publicBaseUrl = Reflect.get(context.input, "publicBaseUrl");
|
|
120
|
-
return {
|
|
121
|
-
...(typeof tunnelId === "string" && tunnelId ? { tunnelId } : {}),
|
|
122
|
-
...(typeof publicBaseUrl === "string" && publicBaseUrl
|
|
123
|
-
? { publicBaseUrl }
|
|
124
|
-
: {}),
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
42
|
function runtime(context, state) {
|
|
128
43
|
return createDevTunnelRuntime({
|
|
129
44
|
...(state
|
|
@@ -132,7 +47,7 @@ function runtime(context, state) {
|
|
|
132
47
|
processStateFile: processFile(context),
|
|
133
48
|
});
|
|
134
49
|
}
|
|
135
|
-
|
|
50
|
+
const baseBehaviorAdapter = {
|
|
136
51
|
install: async (context) => {
|
|
137
52
|
await mkdir(stateDir(context), { recursive: true, mode: 0o700 });
|
|
138
53
|
const state = await readState(context);
|
|
@@ -196,7 +111,7 @@ export const behaviorAdapter = {
|
|
|
196
111
|
{
|
|
197
112
|
scope: "SETUP",
|
|
198
113
|
code: "TUNNEL_SETUP_REQUIRED",
|
|
199
|
-
message: "
|
|
114
|
+
message: "尚未完成持久 Tunnel 自动配置",
|
|
200
115
|
relatedModuleRefs: [],
|
|
201
116
|
nextCommand: "platform setup --module dev-tunnel",
|
|
202
117
|
},
|
|
@@ -206,10 +121,6 @@ export const behaviorAdapter = {
|
|
|
206
121
|
observedEffects: [],
|
|
207
122
|
};
|
|
208
123
|
}
|
|
209
|
-
await writeModuleSharedFacts(context, descriptor.moduleRef, {
|
|
210
|
-
tunnelId: state.tunnelId,
|
|
211
|
-
publicBaseUrl: state.publicBaseUrl,
|
|
212
|
-
});
|
|
213
124
|
const rt = runtime(context, state);
|
|
214
125
|
const login = await rt.loginStatus();
|
|
215
126
|
const observed = await rt.status();
|
|
@@ -262,76 +173,6 @@ export const behaviorAdapter = {
|
|
|
262
173
|
observedEffects: [],
|
|
263
174
|
};
|
|
264
175
|
},
|
|
265
|
-
setup: async (context) => {
|
|
266
|
-
await mkdir(stateDir(context), { recursive: true, mode: 0o700 });
|
|
267
|
-
const previous = await readState(context);
|
|
268
|
-
const supplied = setupInput(context);
|
|
269
|
-
const candidate = { ...(previous ?? {}), ...supplied };
|
|
270
|
-
const rt = runtime(context, previous);
|
|
271
|
-
const login = await rt.loginStatus();
|
|
272
|
-
if (login !== "LOGGED_IN")
|
|
273
|
-
return {
|
|
274
|
-
result: {
|
|
275
|
-
...base,
|
|
276
|
-
ok: false,
|
|
277
|
-
status: "ACTION_REQUIRED",
|
|
278
|
-
data: setupPlan,
|
|
279
|
-
actionRequired: {
|
|
280
|
-
action: "complete-tunnel-login",
|
|
281
|
-
description: `Run devtunnel user login, complete Microsoft authentication, then rerun platform setup --module dev-tunnel --workspace ${JSON.stringify(context.workspaceRoot)}.`,
|
|
282
|
-
},
|
|
283
|
-
},
|
|
284
|
-
observedEffects: [],
|
|
285
|
-
};
|
|
286
|
-
if (typeof candidate.tunnelId !== "string" ||
|
|
287
|
-
typeof candidate.publicBaseUrl !== "string")
|
|
288
|
-
return {
|
|
289
|
-
result: {
|
|
290
|
-
...base,
|
|
291
|
-
ok: false,
|
|
292
|
-
status: "ACTION_REQUIRED",
|
|
293
|
-
data: setupPlan,
|
|
294
|
-
actionRequired: {
|
|
295
|
-
action: "select-or-create-tunnel",
|
|
296
|
-
description: "Create/select the persistent tunnel, then run proflow-dev-tunnel setup --tunnel-id <id> --public-base-url <url>.",
|
|
297
|
-
},
|
|
298
|
-
},
|
|
299
|
-
observedEffects: [],
|
|
300
|
-
};
|
|
301
|
-
try {
|
|
302
|
-
const url = new URL(candidate.publicBaseUrl);
|
|
303
|
-
if (url.protocol !== "https:")
|
|
304
|
-
throw new TypeError("publicBaseUrl must be HTTPS");
|
|
305
|
-
const state = {
|
|
306
|
-
contract: "proflow.dev-tunnel-setup.v1",
|
|
307
|
-
tunnelId: candidate.tunnelId,
|
|
308
|
-
publicBaseUrl: url.href,
|
|
309
|
-
};
|
|
310
|
-
await writeState(context, state);
|
|
311
|
-
await writeModuleSharedFacts(context, descriptor.moduleRef, {
|
|
312
|
-
tunnelId: state.tunnelId,
|
|
313
|
-
publicBaseUrl: state.publicBaseUrl,
|
|
314
|
-
});
|
|
315
|
-
return { result: base, observedEffects: [] };
|
|
316
|
-
}
|
|
317
|
-
catch (error) {
|
|
318
|
-
return {
|
|
319
|
-
result: {
|
|
320
|
-
...base,
|
|
321
|
-
ok: false,
|
|
322
|
-
status: "ACTION_REQUIRED",
|
|
323
|
-
data: setupPlan,
|
|
324
|
-
actionRequired: {
|
|
325
|
-
action: "correct-tunnel-facts",
|
|
326
|
-
description: error instanceof Error
|
|
327
|
-
? error.message
|
|
328
|
-
: "Tunnel setup facts are invalid",
|
|
329
|
-
},
|
|
330
|
-
},
|
|
331
|
-
observedEffects: [],
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
},
|
|
335
176
|
docs: async (_context) => ({
|
|
336
177
|
result: {
|
|
337
178
|
...base,
|
|
@@ -452,3 +293,111 @@ export const behaviorAdapter = {
|
|
|
452
293
|
}
|
|
453
294
|
},
|
|
454
295
|
};
|
|
296
|
+
function gatewayPort(facts) {
|
|
297
|
+
const raw = facts?.localBaseUrl;
|
|
298
|
+
if (typeof raw !== "string")
|
|
299
|
+
throw new Error("agent-gateway localBaseUrl shared fact is unavailable");
|
|
300
|
+
const url = new URL(raw);
|
|
301
|
+
const port = Number(url.port);
|
|
302
|
+
if (url.protocol !== "http:" ||
|
|
303
|
+
url.hostname !== "127.0.0.1" ||
|
|
304
|
+
!Number.isInteger(port) ||
|
|
305
|
+
port < 1 ||
|
|
306
|
+
port > 65_535)
|
|
307
|
+
throw new Error("agent-gateway localBaseUrl shared fact is invalid");
|
|
308
|
+
return port;
|
|
309
|
+
}
|
|
310
|
+
const setupFailed = (error) => ({
|
|
311
|
+
result: {
|
|
312
|
+
...base,
|
|
313
|
+
ok: false,
|
|
314
|
+
status: "FAILED",
|
|
315
|
+
error: {
|
|
316
|
+
code: "SETUP_FAILED",
|
|
317
|
+
message: error instanceof Error ? error.message : "Dev Tunnel setup failed",
|
|
318
|
+
retryable: true,
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
observedEffects: [],
|
|
322
|
+
});
|
|
323
|
+
export function createDevTunnelBehaviorAdapter(dependencies) {
|
|
324
|
+
const automation = dependencies?.automation ??
|
|
325
|
+
createDevTunnelAutomation({ command: "devtunnel" });
|
|
326
|
+
const createRuntime = dependencies?.createRuntime ?? createDevTunnelRuntime;
|
|
327
|
+
const verifyPublicBaseUrl = dependencies?.verifyPublicBaseUrl ?? verifyProvisionedPublicBaseUrl;
|
|
328
|
+
return {
|
|
329
|
+
...baseBehaviorAdapter,
|
|
330
|
+
setup: async (context) => {
|
|
331
|
+
try {
|
|
332
|
+
await mkdir(stateDir(context), { recursive: true, mode: 0o700 });
|
|
333
|
+
const port = gatewayPort(await readModuleSharedFacts(context, "agent-gateway"));
|
|
334
|
+
await automation.ensureLogin();
|
|
335
|
+
const previous = await readState(context);
|
|
336
|
+
let tunnelId;
|
|
337
|
+
let safeToStartNewHost = false;
|
|
338
|
+
const createAndVerifyTunnel = async () => {
|
|
339
|
+
const createdTunnelId = await automation.createTunnel();
|
|
340
|
+
const created = await automation.inspectTunnel(createdTunnelId);
|
|
341
|
+
if (created.state !== "EXISTS")
|
|
342
|
+
throw new Error("new Dev Tunnel could not be verified by show --json");
|
|
343
|
+
return createdTunnelId;
|
|
344
|
+
};
|
|
345
|
+
if (previous) {
|
|
346
|
+
const inspected = await automation.inspectTunnel(previous.tunnelId);
|
|
347
|
+
if (inspected.state === "UNKNOWN")
|
|
348
|
+
throw new Error("workspace Tunnel remote state is UNKNOWN");
|
|
349
|
+
if (inspected.state === "EXISTS") {
|
|
350
|
+
tunnelId = previous.tunnelId;
|
|
351
|
+
safeToStartNewHost = inspected.hostState === "STOPPED";
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
tunnelId = await createAndVerifyTunnel();
|
|
355
|
+
safeToStartNewHost = true;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
tunnelId = await createAndVerifyTunnel();
|
|
360
|
+
safeToStartNewHost = true;
|
|
361
|
+
}
|
|
362
|
+
await automation.ensurePort(tunnelId, port);
|
|
363
|
+
const host = createRuntime({
|
|
364
|
+
tunnelId,
|
|
365
|
+
processStateFile: processFile(context),
|
|
366
|
+
});
|
|
367
|
+
const observed = await host.status();
|
|
368
|
+
let started = false;
|
|
369
|
+
if (observed.state !== "RUNNING") {
|
|
370
|
+
if (observed.state === "UNKNOWN" && !safeToStartNewHost)
|
|
371
|
+
throw new Error("Tunnel host state is UNKNOWN; refusing to start a duplicate host");
|
|
372
|
+
const launched = await host.start();
|
|
373
|
+
if (launched.state !== "RUNNING")
|
|
374
|
+
throw new Error("Dev Tunnel host did not reach RUNNING");
|
|
375
|
+
started = true;
|
|
376
|
+
}
|
|
377
|
+
const publicBaseUrl = await automation.discoverPublicBaseUrl(tunnelId, port);
|
|
378
|
+
const url = new URL(publicBaseUrl);
|
|
379
|
+
if (url.protocol !== "https:")
|
|
380
|
+
throw new Error("discovered publicBaseUrl must be HTTPS");
|
|
381
|
+
await verifyPublicBaseUrl(url.href);
|
|
382
|
+
const state = {
|
|
383
|
+
contract: "proflow.dev-tunnel-setup.v1",
|
|
384
|
+
tunnelId,
|
|
385
|
+
publicBaseUrl: url.href,
|
|
386
|
+
};
|
|
387
|
+
await writeState(context, state);
|
|
388
|
+
await writeModuleSharedFacts(context, descriptor.moduleRef, {
|
|
389
|
+
tunnelId,
|
|
390
|
+
publicBaseUrl: state.publicBaseUrl,
|
|
391
|
+
});
|
|
392
|
+
return {
|
|
393
|
+
result: { ...base, data: state },
|
|
394
|
+
observedEffects: started ? [processEffect] : [],
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
return setupFailed(error);
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
export const behaviorAdapter = createDevTunnelBehaviorAdapter();
|
|
@@ -3,7 +3,7 @@ export declare const descriptor: {
|
|
|
3
3
|
readonly contractVersion: "1.0.0";
|
|
4
4
|
readonly moduleRef: "dev-tunnel";
|
|
5
5
|
readonly packageName: "@tomflow/proflow-dev-tunnel";
|
|
6
|
-
readonly moduleVersion: "0.1.
|
|
6
|
+
readonly moduleVersion: "0.1.16";
|
|
7
7
|
readonly kind: "external-resource";
|
|
8
8
|
readonly templateVersion: "1.0.0";
|
|
9
9
|
readonly platformCompatibility: ">=1.0.0 <2.0.0";
|
|
@@ -21,7 +21,7 @@ export declare const descriptor: {
|
|
|
21
21
|
readonly command: "devtunnel";
|
|
22
22
|
}, {
|
|
23
23
|
readonly kind: "human";
|
|
24
|
-
readonly action: "Complete
|
|
24
|
+
readonly action: "Complete GitHub browser authorization when login is required";
|
|
25
25
|
}];
|
|
26
26
|
readonly configSlots: readonly [];
|
|
27
27
|
readonly effects: readonly [{
|
|
@@ -3,7 +3,7 @@ export const descriptor = {
|
|
|
3
3
|
contractVersion: "1.0.0",
|
|
4
4
|
moduleRef: "dev-tunnel",
|
|
5
5
|
packageName: "@tomflow/proflow-dev-tunnel",
|
|
6
|
-
moduleVersion: "0.1.
|
|
6
|
+
moduleVersion: "0.1.16",
|
|
7
7
|
kind: "external-resource",
|
|
8
8
|
templateVersion: "1.0.0",
|
|
9
9
|
platformCompatibility: ">=1.0.0 <2.0.0",
|
|
@@ -25,7 +25,7 @@ export const descriptor = {
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
kind: "human",
|
|
28
|
-
action: "Complete
|
|
28
|
+
action: "Complete GitHub browser authorization when login is required",
|
|
29
29
|
},
|
|
30
30
|
],
|
|
31
31
|
configSlots: [],
|
|
@@ -3,5 +3,5 @@ export declare const requirements: readonly [{
|
|
|
3
3
|
readonly command: "devtunnel";
|
|
4
4
|
}, {
|
|
5
5
|
readonly kind: "human";
|
|
6
|
-
readonly action: "Complete
|
|
6
|
+
readonly action: "Complete GitHub browser authorization when login is required";
|
|
7
7
|
}];
|
package/dist/src/cli.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { createInterface } from "node:readline/promises";
|
|
4
2
|
import { behaviorAdapter } from "../deployment/adapter.js";
|
|
5
3
|
function reportFatal(error) {
|
|
6
4
|
process.stderr.write(`✕ 配置失败\n ${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -12,136 +10,36 @@ function option(args, name) {
|
|
|
12
10
|
const index = args.indexOf(name);
|
|
13
11
|
return index >= 0 ? args[index + 1] : undefined;
|
|
14
12
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (login.status !== 0) {
|
|
22
|
-
process.stdout.write("◆ 需要 Microsoft 登录,正在启动登录流程…\n");
|
|
23
|
-
login = spawnSync("devtunnel", ["user", "login"], { stdio: "inherit" });
|
|
24
|
-
if (login.status !== 0)
|
|
25
|
-
throw new Error("Microsoft 登录未完成");
|
|
26
|
-
}
|
|
27
|
-
process.stdout.write("✓ Microsoft 登录已就绪\n");
|
|
28
|
-
}
|
|
29
|
-
async function selectTunnel(args) {
|
|
30
|
-
ensureCliAndLogin();
|
|
31
|
-
let tunnelId = option(args, "--tunnel-id");
|
|
32
|
-
if (!tunnelId && !process.stdin.isTTY)
|
|
33
|
-
throw new Error("非交互环境必须提供 --tunnel-id");
|
|
34
|
-
process.stdout.write("\n现有持久 Tunnel:\n");
|
|
35
|
-
spawnSync("devtunnel", ["list"], { stdio: "inherit" });
|
|
36
|
-
const prompt = createInterface({
|
|
37
|
-
input: process.stdin,
|
|
38
|
-
output: process.stdout,
|
|
39
|
-
});
|
|
40
|
-
try {
|
|
41
|
-
tunnelId ||= await prompt.question("\n◆ 输入要使用的 Tunnel ID\n> ");
|
|
42
|
-
const existing = spawnSync("devtunnel", ["show", tunnelId], {
|
|
43
|
-
stdio: "ignore",
|
|
44
|
-
});
|
|
45
|
-
if (existing.status !== 0) {
|
|
46
|
-
if (process.stdin.isTTY) {
|
|
47
|
-
const answer = await prompt.question(`未找到 ${tunnelId},现在创建?[Y/n] `);
|
|
48
|
-
if (answer.trim() && !/^y(es)?$/i.test(answer.trim()))
|
|
49
|
-
throw new Error("尚未选择持久 Tunnel");
|
|
50
|
-
}
|
|
51
|
-
const created = spawnSync("devtunnel", ["create", tunnelId], {
|
|
52
|
-
stdio: "inherit",
|
|
53
|
-
});
|
|
54
|
-
if (created.status !== 0)
|
|
55
|
-
throw new Error("创建持久 Tunnel 失败");
|
|
56
|
-
}
|
|
57
|
-
process.stdout.write(`✓ 持久 Tunnel 已就绪:${tunnelId}\n`);
|
|
58
|
-
return tunnelId;
|
|
59
|
-
}
|
|
60
|
-
finally {
|
|
61
|
-
prompt.close();
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
async function inputs(args) {
|
|
65
|
-
let tunnelId = option(args, "--tunnel-id"), publicBaseUrl = option(args, "--public-base-url");
|
|
66
|
-
if ((!tunnelId || !publicBaseUrl) && !process.stdin.isTTY)
|
|
67
|
-
throw new Error("非交互环境必须提供 --tunnel-id 和 --public-base-url");
|
|
68
|
-
if (!tunnelId || !publicBaseUrl) {
|
|
69
|
-
const prompt = createInterface({
|
|
70
|
-
input: process.stdin,
|
|
71
|
-
output: process.stdout,
|
|
72
|
-
});
|
|
73
|
-
try {
|
|
74
|
-
process.stdout.write("\nDev Tunnel 配置\n\n 脚本将检查 CLI、完成 Microsoft 登录,并协助选择或创建持久 Tunnel(隧道)。\n\n");
|
|
75
|
-
ensureCliAndLogin();
|
|
76
|
-
process.stdout.write("\n现有持久 Tunnel:\n");
|
|
77
|
-
spawnSync("devtunnel", ["list"], { stdio: "inherit" });
|
|
78
|
-
tunnelId ||= await prompt.question("\n◆ 输入要使用的 Tunnel ID(不存在时可创建)\n> ");
|
|
79
|
-
const existing = spawnSync("devtunnel", ["show", tunnelId], {
|
|
80
|
-
stdio: "ignore",
|
|
81
|
-
});
|
|
82
|
-
if (existing.status !== 0) {
|
|
83
|
-
const answer = await prompt.question(`未找到 ${tunnelId},现在创建持久 Tunnel?[Y/n] `);
|
|
84
|
-
if (answer.trim() && !/^y(es)?$/i.test(answer.trim()))
|
|
85
|
-
throw new Error("尚未选择可用的持久 Tunnel");
|
|
86
|
-
const created = spawnSync("devtunnel", ["create", tunnelId], {
|
|
87
|
-
stdio: "inherit",
|
|
88
|
-
});
|
|
89
|
-
if (created.status !== 0)
|
|
90
|
-
throw new Error("创建持久 Tunnel 失败");
|
|
91
|
-
process.stdout.write("✓ 持久 Tunnel 已创建\n");
|
|
92
|
-
}
|
|
93
|
-
else
|
|
94
|
-
process.stdout.write("✓ 已选择现有持久 Tunnel\n");
|
|
95
|
-
process.stdout.write("\n请为 Platform Host 的本地端口建立映射并启动 Tunnel;devtunnel 会显示公开 HTTPS 地址。\n");
|
|
96
|
-
publicBaseUrl ||= await prompt.question("◆ 粘贴公开 HTTPS URL\n> ");
|
|
97
|
-
}
|
|
98
|
-
finally {
|
|
99
|
-
prompt.close();
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return { tunnelId, publicBaseUrl };
|
|
103
|
-
}
|
|
104
|
-
const args = process.argv.slice(2), command = args[0] ?? "setup", workspaceRoot = option(args, "--workspace") ?? process.cwd();
|
|
105
|
-
if (args.includes("--json"))
|
|
106
|
-
throw new Error("不支持的选项 --json");
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const command = args[0] ?? "setup";
|
|
15
|
+
const workspaceRoot = option(args, "--workspace") ?? process.cwd();
|
|
16
|
+
for (const forbidden of ["--tunnel-id", "--public-base-url", "--port"])
|
|
17
|
+
if (args.includes(forbidden))
|
|
18
|
+
throw new Error(`${forbidden} 已移除;Dev Tunnel setup 不接受用户配置`);
|
|
107
19
|
if (command === "setup") {
|
|
108
20
|
const step = args[1]?.startsWith("--") ? undefined : args[1];
|
|
109
|
-
if (step
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
await selectTunnel(args);
|
|
116
|
-
process.stdout.write("→ 下一步:setup 03(配置入口并保存公开 URL)\n");
|
|
117
|
-
process.exit(0);
|
|
21
|
+
if (step !== undefined)
|
|
22
|
+
throw new Error("分步 setup 01..04 已移除;请直接运行 setup");
|
|
23
|
+
process.stdout.write("正在自动准备 Microsoft Dev Tunnel;若登录失效,浏览器将打开 GitHub 授权页。\n");
|
|
24
|
+
const setup = await behaviorAdapter.setup({ workspaceRoot });
|
|
25
|
+
if (setup.result.status === "SUCCEEDED") {
|
|
26
|
+
process.stdout.write("✓ Dev Tunnel 已自动配置并发布公开入口\n");
|
|
118
27
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
process.
|
|
122
|
-
? "✓ Dev Tunnel 验证通过\n"
|
|
123
|
-
: "✕ Dev Tunnel 尚未就绪\n");
|
|
124
|
-
if (result.result.data.setupStatus !== "READY")
|
|
125
|
-
process.exitCode = 1;
|
|
126
|
-
process.exit();
|
|
28
|
+
else {
|
|
29
|
+
process.stderr.write(`✕ Dev Tunnel 自动配置失败\n ${setup.result.error?.message ?? "unknown setup failure"}\n`);
|
|
30
|
+
process.exitCode = 1;
|
|
127
31
|
}
|
|
128
|
-
if (step !== undefined && step !== "03")
|
|
129
|
-
throw new Error(`UNSUPPORTED_SETUP_STEP:${step}`);
|
|
130
|
-
const result = await behaviorAdapter.setup({
|
|
131
|
-
workspaceRoot,
|
|
132
|
-
input: await inputs(args),
|
|
133
|
-
});
|
|
134
|
-
process.stdout.write(result.result.status === "SUCCEEDED"
|
|
135
|
-
? "\n✓ Tunnel 配置已保存\n✓ 登录与公开地址验证通过\n"
|
|
136
|
-
: `\n✕ Tunnel 尚未就绪\n ${result.result.actionRequired.description}\n`);
|
|
137
32
|
}
|
|
138
33
|
else if (command === "verify") {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
34
|
+
const status = await behaviorAdapter.status({ workspaceRoot });
|
|
35
|
+
if (status.result.data.setupStatus === "READY") {
|
|
36
|
+
process.stdout.write("验证通过。\n");
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
process.stderr.write("验证未通过:配置尚未就绪。\n");
|
|
144
40
|
process.exitCode = 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
process.stdout.write("用法:proflow-dev-tunnel setup [--workspace <path>] | verify [--workspace <path>]\n");
|
|
145
45
|
}
|
|
146
|
-
else
|
|
147
|
-
process.stdout.write("用法:proflow-dev-tunnel setup [--tunnel-id <id> --public-base-url <url>]\n");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
export type DevTunnelState = "STOPPED" | "RUNNING" | "UNKNOWN";
|
|
2
3
|
export type DevTunnelLoginStatus = "LOGGED_IN" | "NOT_LOGGED_IN" | "UNKNOWN";
|
|
3
4
|
export interface DevTunnelObservation {
|
|
@@ -12,7 +13,23 @@ export interface CommandResult {
|
|
|
12
13
|
}
|
|
13
14
|
export type CommandRunner = (command: string, args: string[], options?: {
|
|
14
15
|
timeoutMs?: number;
|
|
16
|
+
interactive?: boolean;
|
|
15
17
|
}) => Promise<CommandResult>;
|
|
18
|
+
export type DevTunnelHostState = "STOPPED" | "RUNNING" | "UNKNOWN";
|
|
19
|
+
export type DevTunnelInspection = {
|
|
20
|
+
state: "EXISTS";
|
|
21
|
+
hostState: DevTunnelHostState;
|
|
22
|
+
} | {
|
|
23
|
+
state: "MISSING" | "UNKNOWN";
|
|
24
|
+
hostState: "UNKNOWN";
|
|
25
|
+
};
|
|
26
|
+
export interface DevTunnelAutomation {
|
|
27
|
+
ensureLogin(): Promise<"LOGGED_IN">;
|
|
28
|
+
inspectTunnel(tunnelId: string): Promise<DevTunnelInspection>;
|
|
29
|
+
createTunnel(): Promise<string>;
|
|
30
|
+
ensurePort(tunnelId: string, port: number): Promise<"REUSED" | "CREATED" | "UPDATED">;
|
|
31
|
+
discoverPublicBaseUrl(tunnelId: string, port: number): Promise<string>;
|
|
32
|
+
}
|
|
16
33
|
export interface DevTunnelRuntime {
|
|
17
34
|
readonly command: string;
|
|
18
35
|
status(): Promise<DevTunnelObservation>;
|
|
@@ -22,6 +39,21 @@ export interface DevTunnelRuntime {
|
|
|
22
39
|
stop(): Promise<DevTunnelObservation>;
|
|
23
40
|
restart(): Promise<DevTunnelObservation>;
|
|
24
41
|
}
|
|
42
|
+
declare const tunnelPortSchema: z.ZodObject<{
|
|
43
|
+
portNumber: z.ZodNumber;
|
|
44
|
+
protocol: z.ZodOptional<z.ZodString>;
|
|
45
|
+
portForwardingUris: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
46
|
+
portUri: z.ZodOptional<z.ZodString>;
|
|
47
|
+
clientConnections: z.ZodOptional<z.ZodNumber>;
|
|
48
|
+
}, z.core.$loose>;
|
|
49
|
+
export type DevTunnelPort = z.infer<typeof tunnelPortSchema>;
|
|
50
|
+
export declare function parseDevTunnelLoginStatus(input: unknown): DevTunnelLoginStatus;
|
|
51
|
+
export declare function discoverPublicBaseUrl(input: unknown, port: number): string;
|
|
52
|
+
export declare function createDevTunnelAutomation(input?: {
|
|
53
|
+
command?: string;
|
|
54
|
+
runCommand?: CommandRunner;
|
|
55
|
+
}): DevTunnelAutomation;
|
|
56
|
+
export declare function verifyProvisionedPublicBaseUrl(publicBaseUrl: string, timeoutMs?: number): Promise<void>;
|
|
25
57
|
export declare function createDevTunnelRuntime(input: {
|
|
26
58
|
command?: string;
|
|
27
59
|
tunnelId?: string;
|
|
@@ -56,3 +88,4 @@ export interface PublicIngressVerifyOptions {
|
|
|
56
88
|
export declare function probeTlsProtocol(host: string, port: number, timeoutMs: number): Promise<string | undefined>;
|
|
57
89
|
export declare function readResponseChars(response: Response, ceiling: number): Promise<number | undefined>;
|
|
58
90
|
export declare function verifyPublicIngress(publicBaseUrl: string, options?: PublicIngressVerifyOptions): Promise<PublicIngressVerification>;
|
|
91
|
+
export {};
|