@hs-x/cli 0.1.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 +1001 -0
- package/dist/account-store.d.ts +51 -0
- package/dist/account-store.d.ts.map +1 -0
- package/dist/account-store.js +138 -0
- package/dist/account-store.js.map +1 -0
- package/dist/bin/hs-x.d.ts +3 -0
- package/dist/bin/hs-x.d.ts.map +1 -0
- package/dist/bin/hs-x.js +47 -0
- package/dist/bin/hs-x.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +595 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli-error.d.ts +36 -0
- package/dist/cli-error.d.ts.map +1 -0
- package/dist/cli-error.js +40 -0
- package/dist/cli-error.js.map +1 -0
- package/dist/cloudflare-auth.d.ts +25 -0
- package/dist/cloudflare-auth.d.ts.map +1 -0
- package/dist/cloudflare-auth.js +251 -0
- package/dist/cloudflare-auth.js.map +1 -0
- package/dist/cloudflare-kv.d.ts +23 -0
- package/dist/cloudflare-kv.d.ts.map +1 -0
- package/dist/cloudflare-kv.js +101 -0
- package/dist/cloudflare-kv.js.map +1 -0
- package/dist/cloudflare-oauth-store.d.ts +16 -0
- package/dist/cloudflare-oauth-store.d.ts.map +1 -0
- package/dist/cloudflare-oauth-store.js +80 -0
- package/dist/cloudflare-oauth-store.js.map +1 -0
- package/dist/cloudflare-oauth.d.ts +82 -0
- package/dist/cloudflare-oauth.d.ts.map +1 -0
- package/dist/cloudflare-oauth.js +336 -0
- package/dist/cloudflare-oauth.js.map +1 -0
- package/dist/cloudflare-pointer.d.ts +13 -0
- package/dist/cloudflare-pointer.d.ts.map +1 -0
- package/dist/cloudflare-pointer.js +46 -0
- package/dist/cloudflare-pointer.js.map +1 -0
- package/dist/command-history.d.ts +7 -0
- package/dist/command-history.d.ts.map +1 -0
- package/dist/command-history.js +34 -0
- package/dist/command-history.js.map +1 -0
- package/dist/commands/account.d.ts +7 -0
- package/dist/commands/account.d.ts.map +1 -0
- package/dist/commands/account.js +315 -0
- package/dist/commands/account.js.map +1 -0
- package/dist/commands/api.d.ts +36 -0
- package/dist/commands/api.d.ts.map +1 -0
- package/dist/commands/api.js +521 -0
- package/dist/commands/api.js.map +1 -0
- package/dist/commands/completion.d.ts +7 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +121 -0
- package/dist/commands/completion.js.map +1 -0
- package/dist/commands/connect.d.ts +7 -0
- package/dist/commands/connect.d.ts.map +1 -0
- package/dist/commands/connect.js +1123 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/control-plane-read.d.ts +22 -0
- package/dist/commands/control-plane-read.d.ts.map +1 -0
- package/dist/commands/control-plane-read.js +350 -0
- package/dist/commands/control-plane-read.js.map +1 -0
- package/dist/commands/deploy-promote.d.ts +14 -0
- package/dist/commands/deploy-promote.d.ts.map +1 -0
- package/dist/commands/deploy-promote.js +105 -0
- package/dist/commands/deploy-promote.js.map +1 -0
- package/dist/commands/deploy.d.ts +18 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +2764 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/dev.d.ts +7 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +913 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +258 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/flags.d.ts +22 -0
- package/dist/commands/flags.d.ts.map +1 -0
- package/dist/commands/flags.js +185 -0
- package/dist/commands/flags.js.map +1 -0
- package/dist/commands/help-command.d.ts +13 -0
- package/dist/commands/help-command.d.ts.map +1 -0
- package/dist/commands/help-command.js +482 -0
- package/dist/commands/help-command.js.map +1 -0
- package/dist/commands/history.d.ts +6 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +42 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +233 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/link.d.ts +26 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +441 -0
- package/dist/commands/link.js.map +1 -0
- package/dist/commands/login.d.ts +8 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +381 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/migrate.d.ts +8 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +258 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/rollback.d.ts +21 -0
- package/dist/commands/rollback.d.ts.map +1 -0
- package/dist/commands/rollback.js +301 -0
- package/dist/commands/rollback.js.map +1 -0
- package/dist/commands/secrets.d.ts +7 -0
- package/dist/commands/secrets.d.ts.map +1 -0
- package/dist/commands/secrets.js +230 -0
- package/dist/commands/secrets.js.map +1 -0
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +241 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/unlink.d.ts +21 -0
- package/dist/commands/unlink.d.ts.map +1 -0
- package/dist/commands/unlink.js +83 -0
- package/dist/commands/unlink.js.map +1 -0
- package/dist/commands/update.d.ts +11 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +154 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/validate.d.ts +9 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +39 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/config.d.ts +12 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +4 -0
- package/dist/constants.js.map +1 -0
- package/dist/control-plane-fetch.d.ts +34 -0
- package/dist/control-plane-fetch.d.ts.map +1 -0
- package/dist/control-plane-fetch.js +73 -0
- package/dist/control-plane-fetch.js.map +1 -0
- package/dist/control-plane-loader.d.ts +16 -0
- package/dist/control-plane-loader.d.ts.map +1 -0
- package/dist/control-plane-loader.js +24 -0
- package/dist/control-plane-loader.js.map +1 -0
- package/dist/dev/compat-shim.d.ts +40 -0
- package/dist/dev/compat-shim.d.ts.map +1 -0
- package/dist/dev/compat-shim.js +65 -0
- package/dist/dev/compat-shim.js.map +1 -0
- package/dist/dev/event-bus.d.ts +27 -0
- package/dist/dev/event-bus.d.ts.map +1 -0
- package/dist/dev/event-bus.js +32 -0
- package/dist/dev/event-bus.js.map +1 -0
- package/dist/dev/log-server.d.ts +52 -0
- package/dist/dev/log-server.d.ts.map +1 -0
- package/dist/dev/log-server.js +216 -0
- package/dist/dev/log-server.js.map +1 -0
- package/dist/dev/session-manager.d.ts +33 -0
- package/dist/dev/session-manager.d.ts.map +1 -0
- package/dist/dev/session-manager.js +132 -0
- package/dist/dev/session-manager.js.map +1 -0
- package/dist/dev/stream-renderer.d.ts +22 -0
- package/dist/dev/stream-renderer.d.ts.map +1 -0
- package/dist/dev/stream-renderer.js +65 -0
- package/dist/dev/stream-renderer.js.map +1 -0
- package/dist/dev/tunnel.d.ts +40 -0
- package/dist/dev/tunnel.d.ts.map +1 -0
- package/dist/dev/tunnel.js +139 -0
- package/dist/dev/tunnel.js.map +1 -0
- package/dist/effect-http.d.ts +10 -0
- package/dist/effect-http.d.ts.map +1 -0
- package/dist/effect-http.js +38 -0
- package/dist/effect-http.js.map +1 -0
- package/dist/errors-registry.d.ts +11 -0
- package/dist/errors-registry.d.ts.map +1 -0
- package/dist/errors-registry.js +554 -0
- package/dist/errors-registry.js.map +1 -0
- package/dist/errors.d.ts +58 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +30 -0
- package/dist/errors.js.map +1 -0
- package/dist/help.d.ts +6 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +100 -0
- package/dist/help.js.map +1 -0
- package/dist/history.d.ts +15 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +69 -0
- package/dist/history.js.map +1 -0
- package/dist/hubspot-auth.d.ts +53 -0
- package/dist/hubspot-auth.d.ts.map +1 -0
- package/dist/hubspot-auth.js +301 -0
- package/dist/hubspot-auth.js.map +1 -0
- package/dist/hubspot-developer-client.d.ts +10 -0
- package/dist/hubspot-developer-client.d.ts.map +1 -0
- package/dist/hubspot-developer-client.js +212 -0
- package/dist/hubspot-developer-client.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/init/templates.d.ts +18 -0
- package/dist/init/templates.d.ts.map +1 -0
- package/dist/init/templates.js +239 -0
- package/dist/init/templates.js.map +1 -0
- package/dist/load-env.d.ts +16 -0
- package/dist/load-env.d.ts.map +1 -0
- package/dist/load-env.js +69 -0
- package/dist/load-env.js.map +1 -0
- package/dist/machine-id.d.ts +3 -0
- package/dist/machine-id.d.ts.map +1 -0
- package/dist/machine-id.js +41 -0
- package/dist/machine-id.js.map +1 -0
- package/dist/paths.d.ts +4 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +19 -0
- package/dist/paths.js.map +1 -0
- package/dist/prompt.d.ts +43 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +379 -0
- package/dist/prompt.js.map +1 -0
- package/dist/reporter/human.d.ts +28 -0
- package/dist/reporter/human.d.ts.map +1 -0
- package/dist/reporter/human.js +126 -0
- package/dist/reporter/human.js.map +1 -0
- package/dist/reporter/index.d.ts +14 -0
- package/dist/reporter/index.d.ts.map +1 -0
- package/dist/reporter/index.js +37 -0
- package/dist/reporter/index.js.map +1 -0
- package/dist/reporter/json.d.ts +43 -0
- package/dist/reporter/json.d.ts.map +1 -0
- package/dist/reporter/json.js +146 -0
- package/dist/reporter/json.js.map +1 -0
- package/dist/reporter/style.d.ts +34 -0
- package/dist/reporter/style.d.ts.map +1 -0
- package/dist/reporter/style.js +97 -0
- package/dist/reporter/style.js.map +1 -0
- package/dist/reporter/types.d.ts +41 -0
- package/dist/reporter/types.d.ts.map +1 -0
- package/dist/reporter/types.js +2 -0
- package/dist/reporter/types.js.map +1 -0
- package/dist/result.d.ts +4 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +2 -0
- package/dist/result.js.map +1 -0
- package/dist/services/account-store.d.ts +31 -0
- package/dist/services/account-store.d.ts.map +1 -0
- package/dist/services/account-store.js +135 -0
- package/dist/services/account-store.js.map +1 -0
- package/dist/services/app-paths.d.ts +25 -0
- package/dist/services/app-paths.d.ts.map +1 -0
- package/dist/services/app-paths.js +34 -0
- package/dist/services/app-paths.js.map +1 -0
- package/dist/services/cloudflare-auth.d.ts +83 -0
- package/dist/services/cloudflare-auth.d.ts.map +1 -0
- package/dist/services/cloudflare-auth.js +30 -0
- package/dist/services/cloudflare-auth.js.map +1 -0
- package/dist/services/cloudflare-kv.d.ts +45 -0
- package/dist/services/cloudflare-kv.d.ts.map +1 -0
- package/dist/services/cloudflare-kv.js +151 -0
- package/dist/services/cloudflare-kv.js.map +1 -0
- package/dist/services/command-history.d.ts +29 -0
- package/dist/services/command-history.d.ts.map +1 -0
- package/dist/services/command-history.js +62 -0
- package/dist/services/command-history.js.map +1 -0
- package/dist/services/control-plane.d.ts +32 -0
- package/dist/services/control-plane.d.ts.map +1 -0
- package/dist/services/control-plane.js +57 -0
- package/dist/services/control-plane.js.map +1 -0
- package/dist/services/env-loader.d.ts +18 -0
- package/dist/services/env-loader.d.ts.map +1 -0
- package/dist/services/env-loader.js +34 -0
- package/dist/services/env-loader.js.map +1 -0
- package/dist/services/http.d.ts +19 -0
- package/dist/services/http.d.ts.map +1 -0
- package/dist/services/http.js +9 -0
- package/dist/services/http.js.map +1 -0
- package/dist/services/live.d.ts +16 -0
- package/dist/services/live.d.ts.map +1 -0
- package/dist/services/live.js +26 -0
- package/dist/services/live.js.map +1 -0
- package/dist/services/machine-id.d.ts +18 -0
- package/dist/services/machine-id.d.ts.map +1 -0
- package/dist/services/machine-id.js +39 -0
- package/dist/services/machine-id.js.map +1 -0
- package/dist/services/reporter.d.ts +55 -0
- package/dist/services/reporter.d.ts.map +1 -0
- package/dist/services/reporter.js +49 -0
- package/dist/services/reporter.js.map +1 -0
- package/dist/state-store.d.ts +39 -0
- package/dist/state-store.d.ts.map +1 -0
- package/dist/state-store.js +89 -0
- package/dist/state-store.js.map +1 -0
- package/dist/telemetry.d.ts +13 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +129 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/tenant-state.d.ts +69 -0
- package/dist/tenant-state.d.ts.map +1 -0
- package/dist/tenant-state.js +161 -0
- package/dist/tenant-state.js.map +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { generateProjectArtifacts } from '@hs-x/codegen';
|
|
5
|
+
import { HttpClientRequest, Schema, schemas } from '@hs-x/types';
|
|
6
|
+
import { BRAND_ORANGE_SGR, CLI_VERSION } from '../constants.js';
|
|
7
|
+
import { controlPlaneAuthHeaders } from '../control-plane-fetch.js';
|
|
8
|
+
import { executeCliHttp } from '../effect-http.js';
|
|
9
|
+
import { formatConfigUrl, loadCliConfig } from '../config.js';
|
|
10
|
+
import { createReporter } from '../reporter/index.js';
|
|
11
|
+
import { isInteractive, promptMultiSelect, promptSelect, promptText } from '../prompt.js';
|
|
12
|
+
async function hostedHttp(input) {
|
|
13
|
+
const headers = { ...(input.headers ?? {}) };
|
|
14
|
+
let request = HttpClientRequest.make(input.method ?? 'GET')(input.url).pipe(HttpClientRequest.setHeaders(headers));
|
|
15
|
+
if (input.body !== undefined) {
|
|
16
|
+
headers['content-type'] = headers['content-type'] ?? 'application/json';
|
|
17
|
+
request = request.pipe(HttpClientRequest.setHeaders(headers), HttpClientRequest.bodyText(typeof input.body === 'string' ? input.body : JSON.stringify(input.body), headers['content-type']));
|
|
18
|
+
}
|
|
19
|
+
return executeCliHttp(request);
|
|
20
|
+
}
|
|
21
|
+
function isRecord(value) {
|
|
22
|
+
return typeof value === 'object' && value !== null;
|
|
23
|
+
}
|
|
24
|
+
function renderDevHeader(env, stderr, { subject, url }) {
|
|
25
|
+
const isTTY = Boolean(stderr.isTTY);
|
|
26
|
+
const color = isTTY && env.NO_COLOR !== '1';
|
|
27
|
+
const orange = (s) => (color ? `\x1b[${BRAND_ORANGE_SGR}m${s}\x1b[0m` : s);
|
|
28
|
+
const bold = (s) => (color ? `\x1b[1m${s}\x1b[0m` : s);
|
|
29
|
+
const dim = (s) => (color ? `\x1b[2m${s}\x1b[0m` : s);
|
|
30
|
+
const arrow = '▲';
|
|
31
|
+
const sep = '→';
|
|
32
|
+
stderr.write(`\n ${orange(arrow)} ${bold('hs-x dev')} ${bold(subject)} ${dim(sep)} ${url}\n\n`);
|
|
33
|
+
}
|
|
34
|
+
function makeDevRequestLogger(env, stderr, defaultAccountId) {
|
|
35
|
+
const isTTY = Boolean(stderr.isTTY);
|
|
36
|
+
const color = isTTY && env.NO_COLOR !== '1';
|
|
37
|
+
const dim = (s) => (color ? `\x1b[2m${s}\x1b[0m` : s);
|
|
38
|
+
const green = (s) => (color ? `\x1b[32m${s}\x1b[0m` : s);
|
|
39
|
+
const red = (s) => (color ? `\x1b[31m${s}\x1b[0m` : s);
|
|
40
|
+
const yellow = (s) => (color ? `\x1b[33m${s}\x1b[0m` : s);
|
|
41
|
+
const cyan = (s) => (color ? `\x1b[36m${s}\x1b[0m` : s);
|
|
42
|
+
const magenta = (s) => (color ? `\x1b[35m${s}\x1b[0m` : s);
|
|
43
|
+
return ({ method, path, status, durationMs, source = 'local', accountId, detail }) => {
|
|
44
|
+
const statusColor = status >= 500 ? red : status >= 400 ? yellow : green;
|
|
45
|
+
const symbol = status >= 400 ? '✗' : '✓';
|
|
46
|
+
const methodStr = method.padEnd(5);
|
|
47
|
+
const pathStr = path.length > 35 ? path : path.padEnd(35);
|
|
48
|
+
const statusStr = statusColor(String(status).padStart(3));
|
|
49
|
+
const durationStr = `${durationMs}ms`.padStart(7);
|
|
50
|
+
const effectiveAccountId = accountId ?? defaultAccountId;
|
|
51
|
+
const acct = effectiveAccountId ? ` ${dim(effectiveAccountId)}` : '';
|
|
52
|
+
const sourceColor = source === 'prod' ? magenta : cyan;
|
|
53
|
+
const sourceLabel = sourceColor(source.padEnd(5));
|
|
54
|
+
stderr.write(` ${methodStr} ${pathStr} ${statusStr} ${durationStr}${acct} ${statusColor(symbol)} ${sourceLabel}\n`);
|
|
55
|
+
if (detail) {
|
|
56
|
+
stderr.write(` ${dim('└')} ${dim(detail)}\n`);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function resolveAccountIdOrPrompt(input) {
|
|
61
|
+
const fromFlag = resolveFlag(input.argv, '--account-id') ?? process.env.HSX_ACCOUNT_ID;
|
|
62
|
+
if (fromFlag && fromFlag.length > 0)
|
|
63
|
+
return { accountId: fromFlag, fromPrompt: false };
|
|
64
|
+
const { loadStore } = await import('../account-store.js');
|
|
65
|
+
const store = await loadStore();
|
|
66
|
+
const ids = Object.keys(store.accounts);
|
|
67
|
+
if (ids.length === 0)
|
|
68
|
+
return { fromPrompt: false };
|
|
69
|
+
const defaultId = store.defaultAccountId;
|
|
70
|
+
// Non-interactive, --json, or --yes: accept the configured default rather than
|
|
71
|
+
// silently degrading to "no account picked" — that path led deploy to exit 0
|
|
72
|
+
// after building artifacts only, looking like success.
|
|
73
|
+
const yes = input.argv.includes('--yes') || input.argv.includes('-y');
|
|
74
|
+
if (input.json || !isInteractive() || yes) {
|
|
75
|
+
if (defaultId && ids.includes(defaultId)) {
|
|
76
|
+
return { accountId: defaultId, fromPrompt: false };
|
|
77
|
+
}
|
|
78
|
+
return { fromPrompt: false };
|
|
79
|
+
}
|
|
80
|
+
const picked = await promptSelect({
|
|
81
|
+
message: `Which HS-X account for ${input.purpose}?`,
|
|
82
|
+
...(defaultId && ids.includes(defaultId) ? { default: defaultId } : {}),
|
|
83
|
+
options: ids.map((id) => {
|
|
84
|
+
const a = store.accounts[id];
|
|
85
|
+
return {
|
|
86
|
+
value: id,
|
|
87
|
+
label: id,
|
|
88
|
+
description: a
|
|
89
|
+
? `portal ${a.hubspotPortalId} — ${a.displayName}${id === defaultId ? ' (default)' : ''}`
|
|
90
|
+
: '',
|
|
91
|
+
};
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
return picked ? { accountId: picked, fromPrompt: true } : { fromPrompt: false };
|
|
95
|
+
}
|
|
96
|
+
async function promptMissingText(message, current, validate) {
|
|
97
|
+
if (current && current.length > 0)
|
|
98
|
+
return current;
|
|
99
|
+
const answer = await promptText({
|
|
100
|
+
message,
|
|
101
|
+
...(validate ? { validate } : {}),
|
|
102
|
+
});
|
|
103
|
+
return answer === undefined ? undefined : answer;
|
|
104
|
+
}
|
|
105
|
+
async function readHsxConfigProjectId(root) {
|
|
106
|
+
try {
|
|
107
|
+
const contents = await readFile(join(root, 'hsx.config.ts'), 'utf8');
|
|
108
|
+
const match = contents.match(/\bname\s*:\s*["'`]([^"'`]+)["'`]/);
|
|
109
|
+
return match?.[1] ? toProjectId(match[1]) : undefined;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function toProjectId(value) {
|
|
116
|
+
return value
|
|
117
|
+
.trim()
|
|
118
|
+
.toLowerCase()
|
|
119
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
120
|
+
.replace(/^-+|-+$/g, '');
|
|
121
|
+
}
|
|
122
|
+
function cancelledResult(argv, command) {
|
|
123
|
+
const reporter = createReporter({ command, argv });
|
|
124
|
+
reporter.info('Cancelled.');
|
|
125
|
+
return { exitCode: 130 };
|
|
126
|
+
}
|
|
127
|
+
export async function devCommand({ argv, root, json, }) {
|
|
128
|
+
if (argv[1] === 'cleanup') {
|
|
129
|
+
return await devCleanupCommand({ argv, json });
|
|
130
|
+
}
|
|
131
|
+
if (argv[1] === 'status') {
|
|
132
|
+
return await devStatusCommand({ argv, json });
|
|
133
|
+
}
|
|
134
|
+
const port = Number(resolveFlag(argv, '--port') ?? '8787');
|
|
135
|
+
const workers = await discoverWorkerManifests(root);
|
|
136
|
+
await generateProjectArtifacts({ root, workers });
|
|
137
|
+
let portalId = resolveFlag(argv, '--portal');
|
|
138
|
+
let targetOrigin = resolveFlag(argv, '--target-origin');
|
|
139
|
+
const resolvedDevAcct = await resolveAccountIdOrPrompt({
|
|
140
|
+
argv,
|
|
141
|
+
json,
|
|
142
|
+
purpose: 'this dev session',
|
|
143
|
+
});
|
|
144
|
+
const accountId = resolvedDevAcct.accountId;
|
|
145
|
+
const devCliConfig = loadCliConfig(argv);
|
|
146
|
+
// Portal id: HubSpot account ids in our store look like `portal-46993937`;
|
|
147
|
+
// strip the prefix when the user didn't pass --portal explicitly.
|
|
148
|
+
if (!portalId && accountId) {
|
|
149
|
+
const derived = accountId.replace(/^portal[-_]?/i, '');
|
|
150
|
+
if (/^\d+$/.test(derived))
|
|
151
|
+
portalId = derived;
|
|
152
|
+
}
|
|
153
|
+
// Project id: prefer flag/env, otherwise derive from `name` in hsx.config.ts.
|
|
154
|
+
// Developers shouldn't have to repeat what's already in the project file.
|
|
155
|
+
const projectId = devCliConfig.projectId ?? (await readHsxConfigProjectId(root));
|
|
156
|
+
// Control plane is a single deployed instance; default to it. Overrides
|
|
157
|
+
// (env or flag) exist for self-host / local-cp test runs only.
|
|
158
|
+
const controlPlaneUrl = formatConfigUrl(devCliConfig.controlPlaneUrl);
|
|
159
|
+
const developerId = resolveFlag(argv, '--developer-id') ??
|
|
160
|
+
devCliConfig.userId;
|
|
161
|
+
const sessionId = resolveFlag(argv, '--session-id') ??
|
|
162
|
+
`devsess_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
163
|
+
let ttlSeconds = Number(resolveFlag(argv, '--ttl-seconds') ?? '7200');
|
|
164
|
+
const selectedCapabilities = resolveFlags(argv, '--capability');
|
|
165
|
+
const allCapabilities = workers.flatMap((worker) => worker.capabilities
|
|
166
|
+
.filter((capability) => capability.runtimeNeeds.includes('worker'))
|
|
167
|
+
.map((capability) => ({
|
|
168
|
+
id: capability.id,
|
|
169
|
+
kind: capability.kind,
|
|
170
|
+
worker: worker.name,
|
|
171
|
+
})));
|
|
172
|
+
const allCapabilityIds = allCapabilities.map((c) => c.id);
|
|
173
|
+
if (!json && isInteractive() && (controlPlaneUrl || portalId || targetOrigin)) {
|
|
174
|
+
portalId = await promptMissingText('HubSpot portal id for dev override', portalId, (value) => /^\d+$/.test(value) ? undefined : 'Portal id must be numeric.');
|
|
175
|
+
if (portalId === undefined)
|
|
176
|
+
return cancelledResult(argv, 'dev');
|
|
177
|
+
targetOrigin = await promptMissingText('Public dev target origin', targetOrigin, (value) => {
|
|
178
|
+
try {
|
|
179
|
+
const url = new URL(value);
|
|
180
|
+
return url.protocol === 'https:' || url.hostname === '127.0.0.1' || url.hostname === 'localhost'
|
|
181
|
+
? undefined
|
|
182
|
+
: 'Use an https URL, localhost, or 127.0.0.1.';
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return 'Enter a valid URL.';
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
if (targetOrigin === undefined)
|
|
189
|
+
return cancelledResult(argv, 'dev');
|
|
190
|
+
if (!resolveFlag(argv, '--ttl-seconds')) {
|
|
191
|
+
const ttl = await promptSelect({
|
|
192
|
+
message: 'Dev override TTL',
|
|
193
|
+
default: '7200',
|
|
194
|
+
options: [
|
|
195
|
+
{ value: '3600', label: '1 hour', description: 'short local session' },
|
|
196
|
+
{ value: '7200', label: '2 hours', description: 'default' },
|
|
197
|
+
{ value: '14400', label: '4 hours', description: 'long pairing/debugging session' },
|
|
198
|
+
],
|
|
199
|
+
});
|
|
200
|
+
if (ttl === undefined)
|
|
201
|
+
return cancelledResult(argv, 'dev');
|
|
202
|
+
ttlSeconds = Number(ttl);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// Probe the HubSpot project IR up front so the picker can show UI-extension
|
|
206
|
+
// nodes alongside HS-X capabilities. No-op if hsproject.json isn't present.
|
|
207
|
+
const uiExtensionNodes = await probeHubSpotUiNodes({
|
|
208
|
+
root,
|
|
209
|
+
portalIdFlag: portalId,
|
|
210
|
+
accountId,
|
|
211
|
+
});
|
|
212
|
+
let capabilityIds = selectedCapabilities.length > 0 ? selectedCapabilities : allCapabilityIds;
|
|
213
|
+
let selectedUiNodeUids = uiExtensionNodes && uiExtensionNodes.uids.length > 0 ? uiExtensionNodes.uids : undefined;
|
|
214
|
+
// Unified picker: ask once over HS-X capabilities + HubSpot UI nodes.
|
|
215
|
+
const totalCandidates = allCapabilities.length + (uiExtensionNodes?.uids.length ?? 0);
|
|
216
|
+
if (portalId &&
|
|
217
|
+
!json &&
|
|
218
|
+
selectedCapabilities.length === 0 &&
|
|
219
|
+
totalCandidates > 1 &&
|
|
220
|
+
isInteractive()) {
|
|
221
|
+
const CAP_PREFIX = 'cap:';
|
|
222
|
+
const UI_PREFIX = 'ui:';
|
|
223
|
+
const options = [
|
|
224
|
+
...allCapabilities.map((c) => ({
|
|
225
|
+
value: `${CAP_PREFIX}${c.id}`,
|
|
226
|
+
label: c.id,
|
|
227
|
+
description: `${c.kind} · worker ${c.worker} → cloudflare override`,
|
|
228
|
+
})),
|
|
229
|
+
...(uiExtensionNodes?.uids ?? []).map((uid) => ({
|
|
230
|
+
value: `${UI_PREFIX}${uid}`,
|
|
231
|
+
label: uid,
|
|
232
|
+
description: 'ui-extension · hubspot dev-session',
|
|
233
|
+
})),
|
|
234
|
+
];
|
|
235
|
+
const initial = options.map((o) => o.value);
|
|
236
|
+
const picked = await promptMultiSelect({
|
|
237
|
+
message: `Which components should run in dev mode? (portal ${portalId})`,
|
|
238
|
+
initial,
|
|
239
|
+
minSelected: 1,
|
|
240
|
+
options,
|
|
241
|
+
});
|
|
242
|
+
if (picked === undefined)
|
|
243
|
+
return cancelledResult(argv, 'dev');
|
|
244
|
+
capabilityIds = picked
|
|
245
|
+
.filter((v) => v.startsWith(CAP_PREFIX))
|
|
246
|
+
.map((v) => v.slice(CAP_PREFIX.length));
|
|
247
|
+
selectedUiNodeUids = picked
|
|
248
|
+
.filter((v) => v.startsWith(UI_PREFIX))
|
|
249
|
+
.map((v) => v.slice(UI_PREFIX.length));
|
|
250
|
+
}
|
|
251
|
+
let devOverrideRegistration;
|
|
252
|
+
// We register a dev override whenever the user has a HubSpot portal in
|
|
253
|
+
// play. `hs-x connect` gives us the portal id implicitly; only edge cases
|
|
254
|
+
// (no connected account, no --portal flag) skip registration.
|
|
255
|
+
const shouldRegisterOverride = Boolean(portalId);
|
|
256
|
+
if (shouldRegisterOverride) {
|
|
257
|
+
if (!accountId) {
|
|
258
|
+
throw new Error('Dev override needs a connected HubSpot account. Run `hs-x connect` or pass --account-id.');
|
|
259
|
+
}
|
|
260
|
+
if (!projectId) {
|
|
261
|
+
throw new Error('Could not determine project id. Add `name: "..."` to `hsx.config.ts`, set `HSX_PROJECT_ID`, or pass --project-id.');
|
|
262
|
+
}
|
|
263
|
+
if (!Number.isFinite(ttlSeconds) || ttlSeconds <= 0) {
|
|
264
|
+
throw new Error('--ttl-seconds must be a positive number.');
|
|
265
|
+
}
|
|
266
|
+
if (capabilityIds.length === 0) {
|
|
267
|
+
throw new Error('No worker-backed capabilities were found for dev override registration.');
|
|
268
|
+
}
|
|
269
|
+
// In JSON / scripted mode we still register here (without an auto-managed
|
|
270
|
+
// tunnel — scripts pass --target-origin themselves). Interactive mode
|
|
271
|
+
// registers later, after starting the sidecar + auto-tunnel, so the
|
|
272
|
+
// telemetry URL can be plumbed through.
|
|
273
|
+
if (json) {
|
|
274
|
+
if (!targetOrigin) {
|
|
275
|
+
throw new Error('Dev override registration in --json mode requires --target-origin <url>.');
|
|
276
|
+
}
|
|
277
|
+
devOverrideRegistration = await requestDevOverrideRegistration({
|
|
278
|
+
controlPlaneUrl,
|
|
279
|
+
accountId,
|
|
280
|
+
userId: developerId,
|
|
281
|
+
request: Schema.decodeUnknownSync(schemas.DevOverrideRegisterRequest)({
|
|
282
|
+
projectId,
|
|
283
|
+
portalId,
|
|
284
|
+
targetOrigin,
|
|
285
|
+
developerId,
|
|
286
|
+
sessionId,
|
|
287
|
+
capabilityIds,
|
|
288
|
+
ttlSeconds,
|
|
289
|
+
}),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (json) {
|
|
294
|
+
write(`${JSON.stringify({
|
|
295
|
+
ok: true,
|
|
296
|
+
command: 'dev',
|
|
297
|
+
url: `http://127.0.0.1:${port}`,
|
|
298
|
+
workers,
|
|
299
|
+
...(devOverrideRegistration
|
|
300
|
+
? { mode: 'control-plane-dev-override', devOverrideRegistration }
|
|
301
|
+
: {}),
|
|
302
|
+
}, null, 2)}\n`);
|
|
303
|
+
return { exitCode: 0 };
|
|
304
|
+
}
|
|
305
|
+
const startupBegan = Date.now();
|
|
306
|
+
const renderRequestLog = makeDevRequestLogger(process.env, process.stderr, accountId);
|
|
307
|
+
const server = createServer((request, response) => {
|
|
308
|
+
const requestStarted = Date.now();
|
|
309
|
+
const method = request.method ?? 'GET';
|
|
310
|
+
const path = request.url ?? '/';
|
|
311
|
+
response.on('finish', () => {
|
|
312
|
+
renderRequestLog({
|
|
313
|
+
method,
|
|
314
|
+
path,
|
|
315
|
+
status: response.statusCode,
|
|
316
|
+
durationMs: Date.now() - requestStarted,
|
|
317
|
+
source: 'local',
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
if (path === '/_hsx/health') {
|
|
321
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
322
|
+
response.end(JSON.stringify({ ok: true, cliVersion: CLI_VERSION }));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (path === '/_hsx/manifest') {
|
|
326
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
327
|
+
response.end(JSON.stringify({ workers }, null, 2));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
response.writeHead(404, { 'content-type': 'application/json' });
|
|
331
|
+
response.end(JSON.stringify({
|
|
332
|
+
ok: false,
|
|
333
|
+
error: 'The v0 dev server exposes /_hsx/health and /_hsx/manifest.',
|
|
334
|
+
}));
|
|
335
|
+
});
|
|
336
|
+
await new Promise((resolveListen) => server.listen(port, '127.0.0.1', resolveListen));
|
|
337
|
+
const elapsed = Date.now() - startupBegan;
|
|
338
|
+
const reporter = createReporter({ command: 'dev', argv, entry: true });
|
|
339
|
+
reporter.banner();
|
|
340
|
+
const capabilityCount = workers.reduce((c, w) => c + w.capabilities.length, 0);
|
|
341
|
+
const subject = devOverrideRegistration ? `portal ${devOverrideRegistration.portalId}` : 'local';
|
|
342
|
+
const url = `http://127.0.0.1:${port}`;
|
|
343
|
+
// Optional: start a HubSpot UI-extension dev session if this directory
|
|
344
|
+
// contains an hsproject.json AND the user selected at least one UI node.
|
|
345
|
+
const hubSpotDevSession = selectedUiNodeUids && selectedUiNodeUids.length > 0
|
|
346
|
+
? await maybeStartHubSpotDevSession({
|
|
347
|
+
root,
|
|
348
|
+
argv,
|
|
349
|
+
json,
|
|
350
|
+
accountId,
|
|
351
|
+
reporter,
|
|
352
|
+
selectedNodeUids: selectedUiNodeUids,
|
|
353
|
+
})
|
|
354
|
+
: undefined;
|
|
355
|
+
// HS-X dev sidecar — receives frontend logs (and later: backend/request
|
|
356
|
+
// events) from inside the card iframe and surfaces them in the terminal.
|
|
357
|
+
// Stable default port 9099 so card code can hard-default the dev-log URL.
|
|
358
|
+
const hsxLogPort = Number(resolveFlag(argv, '--hsx-log-port') ?? '9099');
|
|
359
|
+
const devLogStream = await startHsxDevLogStream({
|
|
360
|
+
port: hsxLogPort,
|
|
361
|
+
reporter,
|
|
362
|
+
});
|
|
363
|
+
// Auto-managed Cloudflare quick tunnel — gives the deployed Worker a public
|
|
364
|
+
// URL to POST telemetry envelopes to the local sidecar. The developer must
|
|
365
|
+
// never have to run cloudflared themselves; that's a non-negotiable default.
|
|
366
|
+
const devTunnel = shouldRegisterOverride
|
|
367
|
+
? await startHsxDevTunnel({ port: hsxLogPort, reporter })
|
|
368
|
+
: undefined;
|
|
369
|
+
// Now register the dev override. If the developer didn't pass
|
|
370
|
+
// `--target-origin`, default to observe mode and let the deployed Worker
|
|
371
|
+
// run as normal — we only need the tunnel for the telemetry tee.
|
|
372
|
+
if (shouldRegisterOverride && controlPlaneUrl && portalId && accountId && projectId) {
|
|
373
|
+
const overrideMode = targetOrigin ? 'proxy' : 'observe';
|
|
374
|
+
const effectiveTargetOrigin = targetOrigin ?? devTunnel?.url ?? `http://127.0.0.1:${hsxLogPort}`;
|
|
375
|
+
devOverrideRegistration = await requestDevOverrideRegistration({
|
|
376
|
+
controlPlaneUrl,
|
|
377
|
+
accountId,
|
|
378
|
+
userId: developerId,
|
|
379
|
+
request: Schema.decodeUnknownSync(schemas.DevOverrideRegisterRequest)({
|
|
380
|
+
projectId,
|
|
381
|
+
portalId,
|
|
382
|
+
targetOrigin: effectiveTargetOrigin,
|
|
383
|
+
developerId,
|
|
384
|
+
sessionId,
|
|
385
|
+
capabilityIds,
|
|
386
|
+
ttlSeconds,
|
|
387
|
+
mode: overrideMode,
|
|
388
|
+
...(devTunnel ? { telemetryUrl: `${devTunnel.url}/__hsx/event` } : {}),
|
|
389
|
+
}),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
// Vercel-style header: arrow, command, subject, → URL
|
|
393
|
+
renderDevHeader(process.env, process.stderr, { subject, url });
|
|
394
|
+
reporter.info(`Ready in ${elapsed}ms — ${workers.length} worker${workers.length === 1 ? '' : 's'} · ${capabilityCount} capabilit${capabilityCount === 1 ? 'y' : 'ies'}${devOverrideRegistration ? ` · portal ${devOverrideRegistration.portalId}` : ''}${hubSpotDevSession ? ` · ui-extension session ${hubSpotDevSession.sessionId}` : ''}${devLogStream ? ` · log stream ${devLogStream.url}` : ''}${devTunnel ? ` · tunnel ${devTunnel.url}` : ''}`);
|
|
395
|
+
reporter.info('');
|
|
396
|
+
reporter.info('Press Ctrl+C to stop');
|
|
397
|
+
reporter.info('');
|
|
398
|
+
await new Promise((resolveClose) => {
|
|
399
|
+
const close = async () => {
|
|
400
|
+
if (hubSpotDevSession) {
|
|
401
|
+
try {
|
|
402
|
+
await hubSpotDevSession.cleanup();
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
renderRequestLog({
|
|
406
|
+
method: 'CLEANUP',
|
|
407
|
+
path: '/dev-session',
|
|
408
|
+
status: 500,
|
|
409
|
+
durationMs: 0,
|
|
410
|
+
source: 'local',
|
|
411
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (devTunnel) {
|
|
416
|
+
try {
|
|
417
|
+
await devTunnel.cleanup();
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
// best-effort: tunnel cleanup failures should not block exit.
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (devLogStream) {
|
|
424
|
+
try {
|
|
425
|
+
await devLogStream.cleanup();
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
// best-effort: sidecar shutdown failures should not block exit.
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
server.close(() => resolveClose());
|
|
432
|
+
};
|
|
433
|
+
process.once('SIGINT', close);
|
|
434
|
+
process.once('SIGTERM', close);
|
|
435
|
+
});
|
|
436
|
+
return { exitCode: 0 };
|
|
437
|
+
}
|
|
438
|
+
async function startHsxDevLogStream(input) {
|
|
439
|
+
try {
|
|
440
|
+
const [{ createDevEventBus }, { startDevLogServer }, { startDevStreamRenderer }] = await Promise.all([
|
|
441
|
+
import('../dev/event-bus.js'),
|
|
442
|
+
import('../dev/log-server.js'),
|
|
443
|
+
import('../dev/stream-renderer.js'),
|
|
444
|
+
]);
|
|
445
|
+
const bus = createDevEventBus();
|
|
446
|
+
const server = await startDevLogServer({ bus, port: input.port });
|
|
447
|
+
const renderer = startDevStreamRenderer({ bus });
|
|
448
|
+
return {
|
|
449
|
+
url: server.url,
|
|
450
|
+
async cleanup() {
|
|
451
|
+
renderer.stop();
|
|
452
|
+
await server.close();
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
input.reporter.warn('HSX_W_DEV_LOG_STREAM_FAILED', `Could not start dev log stream: ${error instanceof Error ? error.message : String(error)}`);
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async function startHsxDevTunnel(input) {
|
|
462
|
+
const step = input.reporter.step('Cloudflare quick tunnel');
|
|
463
|
+
try {
|
|
464
|
+
const { startCloudflaredTunnel } = await import('../dev/tunnel.js');
|
|
465
|
+
const tunnel = await startCloudflaredTunnel({ port: input.port });
|
|
466
|
+
step.ok(tunnel.url);
|
|
467
|
+
return tunnel;
|
|
468
|
+
}
|
|
469
|
+
catch (error) {
|
|
470
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
471
|
+
step.warn(msg);
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async function probeHubSpotUiNodes(input) {
|
|
476
|
+
const { access } = await import('node:fs/promises');
|
|
477
|
+
const { join: pathJoin } = await import('node:path');
|
|
478
|
+
const projectFile = pathJoin(input.root, 'hsproject.json');
|
|
479
|
+
try {
|
|
480
|
+
await access(projectFile);
|
|
481
|
+
}
|
|
482
|
+
catch {
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
const portalId = Number.parseInt(input.portalIdFlag ?? input.accountId?.replace(/^portal[-_]?/i, '') ?? '', 10);
|
|
486
|
+
if (!Number.isFinite(portalId))
|
|
487
|
+
return undefined;
|
|
488
|
+
try {
|
|
489
|
+
const { loadDevStack } = await import('../dev/compat-shim.js');
|
|
490
|
+
const stack = await loadDevStack();
|
|
491
|
+
const { readFile: readProj } = await import('node:fs/promises');
|
|
492
|
+
const projectConfig = JSON.parse(await readProj(projectFile, 'utf8'));
|
|
493
|
+
const ir = await stack.translateForLocalDev({
|
|
494
|
+
projectSourceDir: pathJoin(input.root, projectConfig.srcDir),
|
|
495
|
+
platformVersion: projectConfig.platformVersion,
|
|
496
|
+
accountId: portalId,
|
|
497
|
+
}, {});
|
|
498
|
+
return { uids: Object.keys(ir.intermediateNodesIndexedByUid), portalId };
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
// IR translate failed — caller falls back to capability-only behavior.
|
|
502
|
+
return undefined;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
async function maybeStartHubSpotDevSession(input) {
|
|
506
|
+
if (input.argv.includes('--no-hubspot') || input.argv.includes('--no-ui-extensions'))
|
|
507
|
+
return;
|
|
508
|
+
const portalFlag = resolveFlag(input.argv, '--portal');
|
|
509
|
+
const portalId = Number.parseInt(portalFlag ?? input.accountId?.replace(/^portal[-_]?/i, '') ?? '', 10);
|
|
510
|
+
if (!Number.isFinite(portalId)) {
|
|
511
|
+
input.reporter.warn('HSX_W_DEV_HUBSPOT_NO_PORTAL', 'Found hsproject.json but no HubSpot portal id — pass --portal <id> to enable the UI-extension dev session.');
|
|
512
|
+
return undefined;
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
const { startDevSession } = await import('../dev/session-manager.js');
|
|
516
|
+
const session = await startDevSession({
|
|
517
|
+
accountId: portalId,
|
|
518
|
+
projectDir: input.root,
|
|
519
|
+
selectedNodeUids: input.selectedNodeUids,
|
|
520
|
+
});
|
|
521
|
+
input.reporter
|
|
522
|
+
.step('HubSpot UI-extension session')
|
|
523
|
+
.ok(`id ${session.sessionId} · ${session.nodeUids.length} component${session.nodeUids.length === 1 ? '' : 's'}`);
|
|
524
|
+
return session;
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
input.reporter.warn('HSX_W_DEV_HUBSPOT_SESSION_FAILED', `Could not start UI-extension dev session: ${error instanceof Error ? error.message : String(error)}`);
|
|
528
|
+
return undefined;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async function devStatusCommand({ argv, json, }) {
|
|
532
|
+
const accountId = resolveFlag(argv, '--account-id') ?? process.env.HSX_ACCOUNT_ID;
|
|
533
|
+
const controlPlaneUrl = resolveFlag(argv, '--control-plane-url') ?? process.env.HSX_CONTROL_PLANE_URL;
|
|
534
|
+
const userId = resolveFlag(argv, '--user-id') ??
|
|
535
|
+
resolveFlag(argv, '--developer-id') ??
|
|
536
|
+
process.env.HSX_USER_ID ??
|
|
537
|
+
'local-cli-user';
|
|
538
|
+
if (!accountId || !controlPlaneUrl) {
|
|
539
|
+
throw new Error('Dev status requires --control-plane-url and --account-id.');
|
|
540
|
+
}
|
|
541
|
+
const active = await requestDevOverrideStatus({ controlPlaneUrl, accountId, userId });
|
|
542
|
+
const result = {
|
|
543
|
+
ok: true,
|
|
544
|
+
command: 'dev status',
|
|
545
|
+
mode: 'control-plane-dev-override-status',
|
|
546
|
+
accountId,
|
|
547
|
+
active,
|
|
548
|
+
};
|
|
549
|
+
if (json) {
|
|
550
|
+
write(`${JSON.stringify(result, null, 2)}\n`);
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
const reporter = createReporter({ command: 'dev status', argv });
|
|
554
|
+
reporter.header(accountId);
|
|
555
|
+
reporter.block(renderDevOverrides(active.overrides));
|
|
556
|
+
reporter.done(`${active.overrides.length} active`);
|
|
557
|
+
}
|
|
558
|
+
return { exitCode: 0 };
|
|
559
|
+
}
|
|
560
|
+
async function devCleanupCommand({ argv, json, }) {
|
|
561
|
+
const accountId = resolveFlag(argv, '--account-id') ?? process.env.HSX_ACCOUNT_ID;
|
|
562
|
+
const controlPlaneUrl = resolveFlag(argv, '--control-plane-url') ?? process.env.HSX_CONTROL_PLANE_URL;
|
|
563
|
+
const userId = resolveFlag(argv, '--user-id') ??
|
|
564
|
+
resolveFlag(argv, '--developer-id') ??
|
|
565
|
+
process.env.HSX_USER_ID ??
|
|
566
|
+
'local-cli-user';
|
|
567
|
+
const sessionId = resolveFlag(argv, '--session-id');
|
|
568
|
+
if (!accountId || !controlPlaneUrl || !sessionId) {
|
|
569
|
+
throw new Error('Dev cleanup requires --control-plane-url, --account-id, and --session-id.');
|
|
570
|
+
}
|
|
571
|
+
const cleared = await requestDevOverrideCleanup({
|
|
572
|
+
controlPlaneUrl,
|
|
573
|
+
accountId,
|
|
574
|
+
userId,
|
|
575
|
+
sessionId,
|
|
576
|
+
});
|
|
577
|
+
const result = {
|
|
578
|
+
ok: true,
|
|
579
|
+
command: 'dev cleanup',
|
|
580
|
+
mode: 'control-plane-dev-override-cleanup',
|
|
581
|
+
accountId,
|
|
582
|
+
sessionId,
|
|
583
|
+
cleared,
|
|
584
|
+
};
|
|
585
|
+
if (json) {
|
|
586
|
+
write(`${JSON.stringify(result, null, 2)}\n`);
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
const reporter = createReporter({ command: 'dev cleanup', argv });
|
|
590
|
+
reporter.header(sessionId);
|
|
591
|
+
reporter.step(`Cleared ${cleared.cleared.length} dev overrides`).ok();
|
|
592
|
+
reporter.done();
|
|
593
|
+
}
|
|
594
|
+
return { exitCode: 0 };
|
|
595
|
+
}
|
|
596
|
+
async function requestDevOverrideRegistration({ controlPlaneUrl, accountId, userId, request, }) {
|
|
597
|
+
const response = await hostedHttp({
|
|
598
|
+
url: new URL(`/v1/accounts/${encodeURIComponent(accountId)}/dev-overrides`, controlPlaneUrl),
|
|
599
|
+
method: 'POST',
|
|
600
|
+
headers: await controlPlaneAuthHeaders(userId),
|
|
601
|
+
body: request,
|
|
602
|
+
});
|
|
603
|
+
const body = await response.json();
|
|
604
|
+
if (!response.ok) {
|
|
605
|
+
throw new Error(isRecord(body) && typeof body.message === 'string'
|
|
606
|
+
? body.message
|
|
607
|
+
: `Control-plane dev override registration failed with status ${response.status}`);
|
|
608
|
+
}
|
|
609
|
+
return Schema.decodeUnknownSync(schemas.DevOverrideRegisterResponse)(body);
|
|
610
|
+
}
|
|
611
|
+
async function requestDevOverrideStatus({ controlPlaneUrl, accountId, userId, }) {
|
|
612
|
+
const response = await hostedHttp({
|
|
613
|
+
url: new URL(`/v1/accounts/${encodeURIComponent(accountId)}/dev-overrides`, controlPlaneUrl),
|
|
614
|
+
headers: await controlPlaneAuthHeaders(userId),
|
|
615
|
+
});
|
|
616
|
+
const body = await response.json();
|
|
617
|
+
if (!response.ok) {
|
|
618
|
+
throw new Error(isRecord(body) && typeof body.message === 'string'
|
|
619
|
+
? body.message
|
|
620
|
+
: `Control-plane dev override status failed with status ${response.status}`);
|
|
621
|
+
}
|
|
622
|
+
return Schema.decodeUnknownSync(schemas.DevOverrideListResponse)(body);
|
|
623
|
+
}
|
|
624
|
+
async function requestDevOverrideCleanup({ controlPlaneUrl, accountId, userId, sessionId, }) {
|
|
625
|
+
const response = await hostedHttp({
|
|
626
|
+
url: new URL(`/v1/accounts/${encodeURIComponent(accountId)}/dev-overrides/${encodeURIComponent(sessionId)}`, controlPlaneUrl),
|
|
627
|
+
method: 'DELETE',
|
|
628
|
+
headers: await controlPlaneAuthHeaders(userId),
|
|
629
|
+
});
|
|
630
|
+
const body = await response.json();
|
|
631
|
+
if (!response.ok) {
|
|
632
|
+
throw new Error(isRecord(body) && typeof body.message === 'string'
|
|
633
|
+
? body.message
|
|
634
|
+
: `Control-plane dev override cleanup failed with status ${response.status}`);
|
|
635
|
+
}
|
|
636
|
+
return Schema.decodeUnknownSync(schemas.DevOverrideClearResponse)(body);
|
|
637
|
+
}
|
|
638
|
+
async function discoverWorkerManifests(root, options = {}) {
|
|
639
|
+
const files = await collectFiles(join(root, 'src'));
|
|
640
|
+
const workers = [];
|
|
641
|
+
for (const file of files) {
|
|
642
|
+
const source = await readFile(file, 'utf8');
|
|
643
|
+
const workerName = /defineWorker\s*\(\s*["'`]([^"'`]+)["'`]/.exec(source)?.[1];
|
|
644
|
+
if (!workerName) {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const toolCapabilities = [
|
|
648
|
+
...source.matchAll(/(?:worker\.)?(?:tool|action)\s*\(\s*["'`]([^"'`]+)["'`]\s*,\s*\{([\s\S]*?)(?:async\s+)?handler\s*(?:[:(])/g),
|
|
649
|
+
].map((match) => ({
|
|
650
|
+
kind: 'tool',
|
|
651
|
+
id: match[1] ?? 'unknown',
|
|
652
|
+
label: /label\s*:\s*["'`]([^"'`]+)["'`]/.exec(match[2] ?? '')?.[1] ?? match[1] ?? 'unknown',
|
|
653
|
+
objectType: /objectType\s*:\s*["'`]([^"'`]+)["'`]/.exec(match[2] ?? '')?.[1] ?? 'unknown',
|
|
654
|
+
...objectFieldsProperty(match[2] ?? '', 'input'),
|
|
655
|
+
...objectFieldsProperty(match[2] ?? '', 'output'),
|
|
656
|
+
runtimeNeeds: ['worker'],
|
|
657
|
+
}));
|
|
658
|
+
const cardBackendCapabilities = [
|
|
659
|
+
...source.matchAll(/(?:worker\.)?cardBackend\s*\(\s*["'`]([^"'`]+)["'`]\s*,\s*\{([\s\S]*?)(?:async\s+)?handler\s*(?:[:(])/g),
|
|
660
|
+
].map((match) => ({
|
|
661
|
+
kind: 'card-backend',
|
|
662
|
+
id: match[1] ?? 'unknown',
|
|
663
|
+
label: /label\s*:\s*["'`]([^"'`]+)["'`]/.exec(match[2] ?? '')?.[1] ?? match[1] ?? 'unknown',
|
|
664
|
+
runtimeNeeds: ['worker'],
|
|
665
|
+
}));
|
|
666
|
+
const sourceDefinitions = discoverSourceDefinitions(source);
|
|
667
|
+
const syncCapabilities = [
|
|
668
|
+
...source.matchAll(/worker\.sync\s*\(\s*([^,]+)\s*,\s*\{([\s\S]*?)\}\s*\)/g),
|
|
669
|
+
].map((match) => {
|
|
670
|
+
const sourceOrId = (match[1] ?? '').trim();
|
|
671
|
+
const definition = match[2] ?? '';
|
|
672
|
+
const id = /^["'`]([^"'`]+)["'`]$/.exec(sourceOrId)?.[1];
|
|
673
|
+
const sourceDefinition = sourceDefinitions.get(sourceOrId);
|
|
674
|
+
const sourceManifest = sourceDefinition?.kind === 'push'
|
|
675
|
+
? {
|
|
676
|
+
kind: 'push',
|
|
677
|
+
name: sourceDefinition.name,
|
|
678
|
+
webhookPath: `/webhooks/${sourceDefinition.name}`,
|
|
679
|
+
...(sourceDefinition.auth ? { auth: { type: sourceDefinition.auth } } : {}),
|
|
680
|
+
}
|
|
681
|
+
: sourceDefinition?.kind === 'pull'
|
|
682
|
+
? {
|
|
683
|
+
kind: 'pull',
|
|
684
|
+
name: sourceDefinition.name,
|
|
685
|
+
...(sourceDefinition.auth ? { auth: { type: sourceDefinition.auth } } : {}),
|
|
686
|
+
}
|
|
687
|
+
: undefined;
|
|
688
|
+
return {
|
|
689
|
+
kind: 'sync',
|
|
690
|
+
id: id ?? sourceManifest?.name ?? 'unknown',
|
|
691
|
+
...(/label\s*:\s*["'`]([^"'`]+)["'`]/.exec(definition)?.[1]
|
|
692
|
+
? { label: /label\s*:\s*["'`]([^"'`]+)["'`]/.exec(definition)?.[1] }
|
|
693
|
+
: {}),
|
|
694
|
+
schedule: /schedule\s*:\s*["'`]([^"'`]+)["'`]/.exec(definition)?.[1] ?? 'manual',
|
|
695
|
+
...stringProperty(definition, 'into'),
|
|
696
|
+
...schemaProperty(definition),
|
|
697
|
+
manageSchema: options.noManageSchema ? false : discoverManageSchemaMode(definition),
|
|
698
|
+
...(sourceManifest ? { source: sourceManifest } : {}),
|
|
699
|
+
runtimeNeeds: sourceManifest?.kind === 'push'
|
|
700
|
+
? ['worker', 'durable-object', 'queue']
|
|
701
|
+
: ['worker', 'durable-object'],
|
|
702
|
+
};
|
|
703
|
+
});
|
|
704
|
+
workers.push({
|
|
705
|
+
name: workerName,
|
|
706
|
+
capabilities: [...toolCapabilities, ...cardBackendCapabilities, ...syncCapabilities],
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
return workers;
|
|
710
|
+
}
|
|
711
|
+
function stringProperty(source, propertyName) {
|
|
712
|
+
const match = new RegExp(`${propertyName}\\s*:\\s*["'\`]([^"'\`]+)["'\`]`).exec(source)?.[1];
|
|
713
|
+
return match ? { [propertyName]: match } : {};
|
|
714
|
+
}
|
|
715
|
+
function schemaProperty(source) {
|
|
716
|
+
const body = /schema\s*:\s*\{([\s\S]*?)\}\s*(?:,|$)/.exec(source)?.[1];
|
|
717
|
+
if (!body) {
|
|
718
|
+
return {};
|
|
719
|
+
}
|
|
720
|
+
const schema = {};
|
|
721
|
+
for (const match of body.matchAll(/([A-Za-z_$][\w$]*)\s*:\s*["'`]([^"'`]+)["'`]/g)) {
|
|
722
|
+
if (match[1] && match[2]) {
|
|
723
|
+
schema[match[1]] = match[2];
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return Object.keys(schema).length > 0 ? { schema } : {};
|
|
727
|
+
}
|
|
728
|
+
function objectFieldsProperty(source, propertyName) {
|
|
729
|
+
const body = objectLiteralBody(source, propertyName);
|
|
730
|
+
if (!body) {
|
|
731
|
+
return {};
|
|
732
|
+
}
|
|
733
|
+
const fields = {};
|
|
734
|
+
for (const match of body.matchAll(/([A-Za-z_$][\w$]*)\s*:\s*\{([\s\S]*?)\}\s*,?/g)) {
|
|
735
|
+
const fieldName = match[1];
|
|
736
|
+
const definition = match[2] ?? '';
|
|
737
|
+
if (!fieldName) {
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
fields[fieldName] = {
|
|
741
|
+
...stringProperty(definition, 'type'),
|
|
742
|
+
...stringProperty(definition, 'label'),
|
|
743
|
+
...literalProperty(definition, 'default'),
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
return Object.keys(fields).length > 0 ? { [propertyName]: fields } : {};
|
|
747
|
+
}
|
|
748
|
+
function objectLiteralBody(source, propertyName) {
|
|
749
|
+
const start = new RegExp(`${propertyName}\\s*:\\s*\\{`).exec(source);
|
|
750
|
+
if (!start) {
|
|
751
|
+
return undefined;
|
|
752
|
+
}
|
|
753
|
+
let depth = 0;
|
|
754
|
+
const bodyStart = start.index + start[0].length;
|
|
755
|
+
for (let index = bodyStart; index < source.length; index += 1) {
|
|
756
|
+
const char = source[index];
|
|
757
|
+
if (char === '{')
|
|
758
|
+
depth += 1;
|
|
759
|
+
if (char === '}') {
|
|
760
|
+
if (depth === 0) {
|
|
761
|
+
return source.slice(bodyStart, index);
|
|
762
|
+
}
|
|
763
|
+
depth -= 1;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return undefined;
|
|
767
|
+
}
|
|
768
|
+
function literalProperty(source, propertyName) {
|
|
769
|
+
const match = new RegExp(`${propertyName}\\s*:\\s*([^,}\\n]+)`).exec(source)?.[1]?.trim();
|
|
770
|
+
if (!match) {
|
|
771
|
+
return {};
|
|
772
|
+
}
|
|
773
|
+
if (/^["'`]/.test(match)) {
|
|
774
|
+
return stringProperty(source, propertyName);
|
|
775
|
+
}
|
|
776
|
+
const numeric = Number(match);
|
|
777
|
+
if (Number.isFinite(numeric)) {
|
|
778
|
+
return { [propertyName]: numeric };
|
|
779
|
+
}
|
|
780
|
+
if (match === 'true')
|
|
781
|
+
return { [propertyName]: true };
|
|
782
|
+
if (match === 'false')
|
|
783
|
+
return { [propertyName]: false };
|
|
784
|
+
return {};
|
|
785
|
+
}
|
|
786
|
+
function discoverManageSchemaMode(definition) {
|
|
787
|
+
const literal = /manageSchema\s*:\s*["'`](properties|full)["'`]/.exec(definition)?.[1];
|
|
788
|
+
if (literal === 'properties' || literal === 'full') {
|
|
789
|
+
return literal;
|
|
790
|
+
}
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
function discoverSourceDefinitions(source) {
|
|
794
|
+
const definitions = new Map();
|
|
795
|
+
for (const match of source.matchAll(/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*defineSource(\.push)?\s*\(\s*\{([\s\S]*?)\}\s*\)/g)) {
|
|
796
|
+
const variableName = match[1];
|
|
797
|
+
const body = match[3] ?? '';
|
|
798
|
+
if (!variableName) {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const name = /name\s*:\s*["'`]([^"'`]+)["'`]/.exec(body)?.[1] ?? variableName;
|
|
802
|
+
const auth = /auth\s*:\s*\{\s*type\s*:\s*["'`](bearer|oauth2|basic|hmac)["'`]/.exec(body)?.[1];
|
|
803
|
+
definitions.set(variableName, {
|
|
804
|
+
kind: match[2] ? 'push' : 'pull',
|
|
805
|
+
name,
|
|
806
|
+
...(auth ? { auth } : {}),
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
return definitions;
|
|
810
|
+
}
|
|
811
|
+
async function collectFiles(root) {
|
|
812
|
+
const files = [];
|
|
813
|
+
async function walk(dir) {
|
|
814
|
+
let entries;
|
|
815
|
+
try {
|
|
816
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
817
|
+
}
|
|
818
|
+
catch {
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
for (const entry of entries) {
|
|
822
|
+
const path = join(dir, entry.name);
|
|
823
|
+
if (entry.isDirectory()) {
|
|
824
|
+
await walk(path);
|
|
825
|
+
}
|
|
826
|
+
else if (entry.isFile() && /\.(?:ts|tsx|js|jsx)$/.test(entry.name)) {
|
|
827
|
+
files.push(path);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
try {
|
|
832
|
+
if ((await stat(root)).isDirectory()) {
|
|
833
|
+
await walk(root);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
return [];
|
|
838
|
+
}
|
|
839
|
+
return files.sort();
|
|
840
|
+
}
|
|
841
|
+
function renderDevOverrides(overrides) {
|
|
842
|
+
if (overrides.length === 0) {
|
|
843
|
+
return 'No active dev overrides found.\n';
|
|
844
|
+
}
|
|
845
|
+
const now = new Date();
|
|
846
|
+
const portalWidth = Math.max('Portal'.length, ...overrides.map((o) => o.portalId.length));
|
|
847
|
+
const capabilityWidth = Math.max('Capability'.length, ...overrides.map((o) => o.capabilityId.length));
|
|
848
|
+
const sessionWidth = Math.max('Session'.length, ...overrides.map((o) => o.sessionId.length));
|
|
849
|
+
const lines = [
|
|
850
|
+
`${'Portal'.padEnd(portalWidth)} ${'Capability'.padEnd(capabilityWidth)} ${'Session'.padEnd(sessionWidth)} Expires Target`,
|
|
851
|
+
];
|
|
852
|
+
for (const override of overrides) {
|
|
853
|
+
lines.push(`${override.portalId.padEnd(portalWidth)} ${override.capabilityId.padEnd(capabilityWidth)} ${override.sessionId.padEnd(sessionWidth)} ${formatDevExpiry(now, override.expiresAt).padEnd(7)} ${override.targetOrigin}`);
|
|
854
|
+
}
|
|
855
|
+
return `${lines.join('\n')}\n`;
|
|
856
|
+
}
|
|
857
|
+
function formatDevExpiry(now, expiresAt) {
|
|
858
|
+
const expires = Date.parse(expiresAt);
|
|
859
|
+
if (!Number.isFinite(expires))
|
|
860
|
+
return expiresAt;
|
|
861
|
+
const ms = expires - now.getTime();
|
|
862
|
+
if (ms <= 0)
|
|
863
|
+
return 'expired';
|
|
864
|
+
const minutes = Math.ceil(ms / 60_000);
|
|
865
|
+
if (minutes < 60)
|
|
866
|
+
return `${minutes}m`;
|
|
867
|
+
const hours = Math.ceil(minutes / 60);
|
|
868
|
+
if (hours < 48)
|
|
869
|
+
return `${hours}h`;
|
|
870
|
+
return `${Math.ceil(hours / 24)}d`;
|
|
871
|
+
}
|
|
872
|
+
function isFlagValue(value) {
|
|
873
|
+
return value !== undefined && value.length > 0 && !value.startsWith('-');
|
|
874
|
+
}
|
|
875
|
+
function resolveFlag(argv, flag) {
|
|
876
|
+
const index = argv.indexOf(flag);
|
|
877
|
+
if (index !== -1) {
|
|
878
|
+
const next = argv[index + 1];
|
|
879
|
+
if (isFlagValue(next))
|
|
880
|
+
return next;
|
|
881
|
+
}
|
|
882
|
+
const prefix = `${flag}=`;
|
|
883
|
+
const found = argv.find((arg) => arg.startsWith(prefix));
|
|
884
|
+
if (!found)
|
|
885
|
+
return undefined;
|
|
886
|
+
const value = found.slice(prefix.length);
|
|
887
|
+
return value.length > 0 ? value : undefined;
|
|
888
|
+
}
|
|
889
|
+
function resolveFlags(argv, flag) {
|
|
890
|
+
const values = [];
|
|
891
|
+
const prefix = `${flag}=`;
|
|
892
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
893
|
+
const arg = argv[index];
|
|
894
|
+
if (arg === flag) {
|
|
895
|
+
const next = argv[index + 1];
|
|
896
|
+
if (isFlagValue(next)) {
|
|
897
|
+
values.push(next);
|
|
898
|
+
index += 1;
|
|
899
|
+
}
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (arg?.startsWith(prefix)) {
|
|
903
|
+
const value = arg.slice(prefix.length);
|
|
904
|
+
if (value.length > 0)
|
|
905
|
+
values.push(value);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return values;
|
|
909
|
+
}
|
|
910
|
+
function write(message) {
|
|
911
|
+
process.stdout.write(message);
|
|
912
|
+
}
|
|
913
|
+
//# sourceMappingURL=dev.js.map
|