@hs-x/cli 0.3.6 → 0.4.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 +126 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +58 -59
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/kit.d.ts.map +1 -1
- package/dist/cli/kit.js +1 -1
- package/dist/cli/kit.js.map +1 -1
- package/dist/cloudflare-auth.d.ts +19 -0
- package/dist/cloudflare-auth.d.ts.map +1 -1
- package/dist/cloudflare-auth.js +83 -2
- package/dist/cloudflare-auth.js.map +1 -1
- package/dist/cloudflare-oauth.d.ts.map +1 -1
- package/dist/cloudflare-oauth.js +18 -19
- package/dist/cloudflare-oauth.js.map +1 -1
- package/dist/cloudflare-scoped-resource.d.ts +41 -0
- package/dist/cloudflare-scoped-resource.d.ts.map +1 -0
- package/dist/cloudflare-scoped-resource.js +74 -0
- package/dist/cloudflare-scoped-resource.js.map +1 -0
- package/dist/command-catalog.d.ts +13 -0
- package/dist/command-catalog.d.ts.map +1 -0
- package/dist/command-catalog.js +50 -0
- package/dist/command-catalog.js.map +1 -0
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +287 -47
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +274 -38
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/control-plane-read.d.ts +31 -3
- package/dist/commands/control-plane-read.d.ts.map +1 -1
- package/dist/commands/control-plane-read.js +136 -37
- package/dist/commands/control-plane-read.js.map +1 -1
- package/dist/commands/deploy-retry.d.ts +20 -0
- package/dist/commands/deploy-retry.d.ts.map +1 -0
- package/dist/commands/deploy-retry.js +106 -0
- package/dist/commands/deploy-retry.js.map +1 -0
- package/dist/commands/deploy.d.ts +16 -8
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +1027 -373
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +19 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +1378 -88
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +27 -10
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/flags.d.ts +9 -4
- package/dist/commands/flags.d.ts.map +1 -1
- package/dist/commands/flags.js +208 -80
- package/dist/commands/flags.js.map +1 -1
- package/dist/commands/help-command.d.ts.map +1 -1
- package/dist/commands/help-command.js +177 -86
- package/dist/commands/help-command.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +5 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/link.d.ts.map +1 -1
- package/dist/commands/link.js +74 -15
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/project.d.ts +107 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +586 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/secrets.d.ts +2 -2
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +15 -5
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/validate.d.ts +19 -0
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +26 -7
- package/dist/commands/validate.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/control-plane-tenant-provisioning.d.ts +24 -0
- package/dist/control-plane-tenant-provisioning.d.ts.map +1 -0
- package/dist/control-plane-tenant-provisioning.js +85 -0
- package/dist/control-plane-tenant-provisioning.js.map +1 -0
- package/dist/deploy-targets.d.ts +24 -0
- package/dist/deploy-targets.d.ts.map +1 -0
- package/dist/deploy-targets.js +38 -0
- package/dist/deploy-targets.js.map +1 -0
- package/dist/dev/account-selection.d.ts +16 -0
- package/dist/dev/account-selection.d.ts.map +1 -0
- package/dist/dev/account-selection.js +22 -0
- package/dist/dev/account-selection.js.map +1 -0
- package/dist/dev/browser-log-bridge.d.ts +32 -0
- package/dist/dev/browser-log-bridge.d.ts.map +1 -0
- package/dist/dev/browser-log-bridge.js +156 -0
- package/dist/dev/browser-log-bridge.js.map +1 -0
- package/dist/dev/compat-shim.d.ts +3 -3
- package/dist/dev/compat-shim.d.ts.map +1 -1
- package/dist/dev/compat-shim.js +29 -4
- package/dist/dev/compat-shim.js.map +1 -1
- package/dist/dev/component-selection.d.ts +25 -0
- package/dist/dev/component-selection.d.ts.map +1 -0
- package/dist/dev/component-selection.js +79 -0
- package/dist/dev/component-selection.js.map +1 -0
- package/dist/dev/console-capture.d.ts +18 -0
- package/dist/dev/console-capture.d.ts.map +1 -0
- package/dist/dev/console-capture.js +57 -0
- package/dist/dev/console-capture.js.map +1 -0
- package/dist/dev/event-bus.d.ts +1 -1
- package/dist/dev/event-bus.js +1 -1
- package/dist/dev/frontend-log-relay.d.ts +27 -0
- package/dist/dev/frontend-log-relay.d.ts.map +1 -0
- package/dist/dev/frontend-log-relay.js +102 -0
- package/dist/dev/frontend-log-relay.js.map +1 -0
- package/dist/dev/invoke.d.ts +3 -4
- package/dist/dev/invoke.d.ts.map +1 -1
- package/dist/dev/invoke.js +9 -4
- package/dist/dev/invoke.js.map +1 -1
- package/dist/dev/keyboard.d.ts +1 -0
- package/dist/dev/keyboard.d.ts.map +1 -1
- package/dist/dev/keyboard.js +9 -3
- package/dist/dev/keyboard.js.map +1 -1
- package/dist/dev/log-server.js +3 -0
- package/dist/dev/log-server.js.map +1 -1
- package/dist/dev/loopback-http-listener.d.ts +18 -0
- package/dist/dev/loopback-http-listener.d.ts.map +1 -0
- package/dist/dev/loopback-http-listener.js +91 -0
- package/dist/dev/loopback-http-listener.js.map +1 -0
- package/dist/dev/managed-session.d.ts +124 -0
- package/dist/dev/managed-session.d.ts.map +1 -0
- package/dist/dev/managed-session.js +597 -0
- package/dist/dev/managed-session.js.map +1 -0
- package/dist/dev/port-process.d.ts +21 -0
- package/dist/dev/port-process.d.ts.map +1 -0
- package/dist/dev/port-process.js +145 -0
- package/dist/dev/port-process.js.map +1 -0
- package/dist/dev/session-manager.d.ts +11 -1
- package/dist/dev/session-manager.d.ts.map +1 -1
- package/dist/dev/session-manager.js +65 -14
- package/dist/dev/session-manager.js.map +1 -1
- package/dist/dev/stream-event-format.d.ts +14 -0
- package/dist/dev/stream-event-format.d.ts.map +1 -0
- package/dist/dev/stream-event-format.js +262 -0
- package/dist/dev/stream-event-format.js.map +1 -0
- package/dist/dev/stream-filter.d.ts.map +1 -1
- package/dist/dev/stream-filter.js +16 -1
- package/dist/dev/stream-filter.js.map +1 -1
- package/dist/dev/stream-renderer.d.ts +4 -2
- package/dist/dev/stream-renderer.d.ts.map +1 -1
- package/dist/dev/stream-renderer.js +224 -84
- package/dist/dev/stream-renderer.js.map +1 -1
- package/dist/errors-registry.d.ts.map +1 -1
- package/dist/errors-registry.js +246 -0
- package/dist/errors-registry.js.map +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +20 -3
- package/dist/help.js.map +1 -1
- package/dist/hubspot-app-logs.d.ts +38 -5
- package/dist/hubspot-app-logs.d.ts.map +1 -1
- package/dist/hubspot-app-logs.js +88 -17
- package/dist/hubspot-app-logs.js.map +1 -1
- package/dist/hubspot-auth.d.ts +19 -1
- package/dist/hubspot-auth.d.ts.map +1 -1
- package/dist/hubspot-auth.js +228 -83
- package/dist/hubspot-auth.js.map +1 -1
- package/dist/hubspot-developer-client.d.ts +87 -0
- package/dist/hubspot-developer-client.d.ts.map +1 -1
- package/dist/hubspot-developer-client.js +232 -1
- package/dist/hubspot-developer-client.js.map +1 -1
- package/dist/hubspot-project-archive.d.ts +21 -0
- package/dist/hubspot-project-archive.d.ts.map +1 -0
- package/dist/hubspot-project-archive.js +281 -0
- package/dist/hubspot-project-archive.js.map +1 -0
- package/dist/hubspot-project.d.ts +32 -0
- package/dist/hubspot-project.d.ts.map +1 -0
- package/dist/hubspot-project.js +269 -0
- package/dist/hubspot-project.js.map +1 -0
- package/dist/local-project-picker.js +1 -1
- package/dist/local-project-picker.js.map +1 -1
- package/dist/machine-artifact.d.ts +16 -0
- package/dist/machine-artifact.d.ts.map +1 -0
- package/dist/machine-artifact.js +18 -0
- package/dist/machine-artifact.js.map +1 -0
- package/dist/machine-id.d.ts +15 -0
- package/dist/machine-id.d.ts.map +1 -1
- package/dist/machine-id.js +53 -0
- package/dist/machine-id.js.map +1 -1
- package/dist/mutation-consent.d.ts +20 -0
- package/dist/mutation-consent.d.ts.map +1 -0
- package/dist/mutation-consent.js +9 -0
- package/dist/mutation-consent.js.map +1 -0
- package/dist/output-context.d.ts +3 -0
- package/dist/output-context.d.ts.map +1 -1
- package/dist/output-context.js +7 -1
- package/dist/output-context.js.map +1 -1
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +123 -48
- package/dist/prompt.js.map +1 -1
- package/dist/reporter/index.js +1 -1
- package/dist/reporter/index.js.map +1 -1
- package/dist/reporter/style.d.ts.map +1 -1
- package/dist/reporter/style.js +5 -0
- package/dist/reporter/style.js.map +1 -1
- package/dist/services/hubspot-dev-client.js +3 -0
- package/dist/services/hubspot-dev-client.js.map +1 -1
- package/dist/services/telemetry.d.ts +1 -2
- package/dist/services/telemetry.d.ts.map +1 -1
- package/dist/services/telemetry.js +1 -10
- package/dist/services/telemetry.js.map +1 -1
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +1 -7
- package/dist/telemetry.js.map +1 -1
- package/dist/tenant-data-plane-provisioning.d.ts +35 -0
- package/dist/tenant-data-plane-provisioning.d.ts.map +1 -0
- package/dist/tenant-data-plane-provisioning.js +38 -0
- package/dist/tenant-data-plane-provisioning.js.map +1 -0
- package/package.json +14 -12
package/dist/commands/dev.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
1
2
|
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { closeSync, existsSync, openSync } from 'node:fs';
|
|
2
4
|
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
3
5
|
import { createServer } from 'node:http';
|
|
4
|
-
import { join } from 'node:path';
|
|
6
|
+
import { join, resolve as resolvePath } from 'node:path';
|
|
5
7
|
import * as Args from '@effect/cli/Args';
|
|
6
8
|
import * as Command from '@effect/cli/Command';
|
|
7
9
|
import * as Options from '@effect/cli/Options';
|
|
@@ -13,11 +15,20 @@ import { cwdOption, jsonOption, runHandler, runQuarantined } from '../cli/kit.js
|
|
|
13
15
|
import { formatConfigUrl, loadCliConfig } from '../config.js';
|
|
14
16
|
import { CLI_VERSION } from '../constants.js';
|
|
15
17
|
import { controlPlaneAuthHeaders } from '../control-plane-fetch.js';
|
|
18
|
+
import { selectHubSpotDirectDevAccounts } from '../dev/account-selection.js';
|
|
19
|
+
import { listHubSpotDevComponentCandidates, resolveHubSpotDevComponentSelectors, } from '../dev/component-selection.js';
|
|
20
|
+
import { createManagedDevEventWriter, createManagedDevSession, markManagedDevSessionReady, markManagedDevSessionState, prepareManagedDevSessionRestart, publicManagedDevSession, readManagedDevEvents, readManagedDevSession, reconcileManagedDevSession, setManagedDevSessionPid, stopManagedDevSessionProcess, waitForManagedDevSessionOwner, } from '../dev/managed-session.js';
|
|
21
|
+
import { findListeningProcess, isHubSpotDevPortConflict, stopProcessCommand, terminateProcessGracefully, } from '../dev/port-process.js';
|
|
16
22
|
import { executeCliHttp } from '../effect-http.js';
|
|
23
|
+
import { discoverHubSpotAccounts, exchangePak } from '../hubspot-auth.js';
|
|
24
|
+
import { detectLocalProjectMode, validateHubSpotProject } from '../hubspot-project.js';
|
|
17
25
|
import { LocalProjectPickerCancelled, resolveLocalProject } from '../local-project-picker.js';
|
|
18
|
-
import {
|
|
26
|
+
import { requestMutationConsent } from '../mutation-consent.js';
|
|
27
|
+
import { isMachineOutput } from '../output-context.js';
|
|
28
|
+
import { isInteractive, promptConfirm, promptMultiSelect, promptSelect } from '../prompt.js';
|
|
19
29
|
import { createReporter } from '../reporter/index.js';
|
|
20
30
|
import { Cwd } from '../services/cwd.js';
|
|
31
|
+
import { Reporter as ReporterService } from '../services/reporter.js';
|
|
21
32
|
async function hostedHttp(input) {
|
|
22
33
|
const headers = { ...(input.headers ?? {}) };
|
|
23
34
|
let request = HttpClientRequest.make(input.method ?? 'GET')(input.url).pipe(HttpClientRequest.setHeaders(headers));
|
|
@@ -33,6 +44,21 @@ function isRecord(value) {
|
|
|
33
44
|
function isErrnoException(value) {
|
|
34
45
|
return value instanceof Error && typeof value.code === 'string';
|
|
35
46
|
}
|
|
47
|
+
function describeDevSessionFailure(cause) {
|
|
48
|
+
const error = isRecord(cause) ? cause : undefined;
|
|
49
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
50
|
+
const status = typeof error?.status === 'number' ? `HTTP ${error.status}` : undefined;
|
|
51
|
+
const data = isRecord(error?.data) ? error.data : undefined;
|
|
52
|
+
const responseMessage = typeof data?.message === 'string' ? data.message : undefined;
|
|
53
|
+
const validationErrors = Array.isArray(error?.validationErrors)
|
|
54
|
+
? error.validationErrors.filter((item) => typeof item === 'string')
|
|
55
|
+
: [];
|
|
56
|
+
const correlationId = typeof error?.correlationId === 'string' ? `correlation ${error.correlationId}` : undefined;
|
|
57
|
+
return [...new Set([message, status, responseMessage, ...validationErrors, correlationId])]
|
|
58
|
+
.filter((item) => Boolean(item))
|
|
59
|
+
.join(' · ');
|
|
60
|
+
}
|
|
61
|
+
const HUBSPOT_UI_DEV_PORT = 5173;
|
|
36
62
|
function makeDevLogPaint(env, stderr) {
|
|
37
63
|
const isTTY = Boolean(stderr.isTTY);
|
|
38
64
|
const color = isTTY && env.NO_COLOR !== '1';
|
|
@@ -177,7 +203,512 @@ async function resolveDefaultPortalId(input) {
|
|
|
177
203
|
}
|
|
178
204
|
return undefined;
|
|
179
205
|
}
|
|
180
|
-
|
|
206
|
+
async function recoverHubSpotDevPort(input) {
|
|
207
|
+
const listener = await findListeningProcess(HUBSPOT_UI_DEV_PORT);
|
|
208
|
+
if (!listener)
|
|
209
|
+
return { kind: 'none' };
|
|
210
|
+
const summary = `Port ${listener.port} is held by PID ${listener.pid}${listener.command ? ` (${listener.command})` : ''}.`;
|
|
211
|
+
const recovery = stopProcessCommand(listener.pid);
|
|
212
|
+
if (input.json) {
|
|
213
|
+
write(`${JSON.stringify({
|
|
214
|
+
schema_version: 1,
|
|
215
|
+
command: 'dev',
|
|
216
|
+
ok: false,
|
|
217
|
+
error: {
|
|
218
|
+
code: 'HSX_E_DEV_PORT_IN_USE',
|
|
219
|
+
message: summary,
|
|
220
|
+
process: listener,
|
|
221
|
+
recovery,
|
|
222
|
+
},
|
|
223
|
+
}, null, 2)}\n`);
|
|
224
|
+
return { kind: 'exit', result: { exitCode: 1 } };
|
|
225
|
+
}
|
|
226
|
+
input.reporter.header('HubSpot dev server already running');
|
|
227
|
+
input.reporter.rows([
|
|
228
|
+
{ key: 'port', value: String(listener.port), status: 'warn' },
|
|
229
|
+
{ key: 'pid', value: String(listener.pid) },
|
|
230
|
+
...(listener.cwd ? [{ key: 'directory', value: listener.cwd }] : []),
|
|
231
|
+
...(listener.command ? [{ key: 'command', value: listener.command }] : []),
|
|
232
|
+
]);
|
|
233
|
+
if (!isInteractive()) {
|
|
234
|
+
input.reporter.error('HSX_E_DEV_PORT_IN_USE', summary, {
|
|
235
|
+
hint: `Stop that exact process with \`${recovery}\`, or rerun in an interactive terminal to confirm and retry automatically.`,
|
|
236
|
+
docs_url: 'https://hs-x.dev/errors/HSX_E_DEV_PORT_IN_USE',
|
|
237
|
+
});
|
|
238
|
+
input.reporter.done(undefined, 1);
|
|
239
|
+
return { kind: 'exit', result: { exitCode: 1 } };
|
|
240
|
+
}
|
|
241
|
+
const confirmed = await promptConfirm({
|
|
242
|
+
message: `Stop PID ${listener.pid} and retry this dev session?`,
|
|
243
|
+
default: false,
|
|
244
|
+
});
|
|
245
|
+
if (confirmed === undefined) {
|
|
246
|
+
return { kind: 'exit', result: cancelledResult(input.argv, 'dev') };
|
|
247
|
+
}
|
|
248
|
+
if (!confirmed) {
|
|
249
|
+
input.reporter.info(`Left PID ${listener.pid} running.`);
|
|
250
|
+
input.reporter.done('Dev session not started', 1);
|
|
251
|
+
return { kind: 'exit', result: { exitCode: 1 } };
|
|
252
|
+
}
|
|
253
|
+
const step = input.reporter.step(`Stopping PID ${listener.pid}`);
|
|
254
|
+
const stopped = await terminateProcessGracefully(listener.pid);
|
|
255
|
+
if (!stopped.ok) {
|
|
256
|
+
step.warn(stopped.message ?? 'could not stop process');
|
|
257
|
+
input.reporter.error('HSX_E_DEV_PORT_IN_USE', `Could not stop PID ${listener.pid}; it was left running.`, { hint: `Stop it manually with \`${recovery}\`, then retry.` });
|
|
258
|
+
input.reporter.done(undefined, 1);
|
|
259
|
+
return { kind: 'exit', result: { exitCode: 1 } };
|
|
260
|
+
}
|
|
261
|
+
step.ok('stopped; retrying');
|
|
262
|
+
return { kind: 'stopped' };
|
|
263
|
+
}
|
|
264
|
+
async function hubSpotDirectDevCommand({ argv, root, json, }) {
|
|
265
|
+
const reporter = createReporter({ command: 'dev', argv, entry: !json });
|
|
266
|
+
const profile = resolveFlag(argv, '--profile');
|
|
267
|
+
const validation = await validateHubSpotProject({ root, ...(profile ? { profile } : {}) });
|
|
268
|
+
if (!validation.ok) {
|
|
269
|
+
const result = {
|
|
270
|
+
ok: false,
|
|
271
|
+
command: 'dev',
|
|
272
|
+
mode: 'hubspot-direct',
|
|
273
|
+
error: 'HubSpot project validation failed.',
|
|
274
|
+
diagnostics: validation.diagnostics,
|
|
275
|
+
};
|
|
276
|
+
if (json)
|
|
277
|
+
write(`${JSON.stringify(result, null, 2)}\n`);
|
|
278
|
+
else {
|
|
279
|
+
reporter.header('HubSpot direct');
|
|
280
|
+
reporter.rows(validation.diagnostics.map((diagnostic) => ({
|
|
281
|
+
key: diagnostic.code,
|
|
282
|
+
value: diagnostic.message,
|
|
283
|
+
status: diagnostic.severity === 'error' ? 'fail' : 'warn',
|
|
284
|
+
...(diagnostic.file ? { detail: diagnostic.file } : {}),
|
|
285
|
+
})));
|
|
286
|
+
reporter.done('Validation failed', 1);
|
|
287
|
+
}
|
|
288
|
+
return { exitCode: 1 };
|
|
289
|
+
}
|
|
290
|
+
const discovered = await discoverHubSpotAccounts(root);
|
|
291
|
+
const accounts = discovered?.accounts ?? [];
|
|
292
|
+
const requestedProjectAccount = resolveFlag(argv, '--project-account') ??
|
|
293
|
+
resolveFlag(argv, '--account') ??
|
|
294
|
+
resolveFlag(argv, '--portal');
|
|
295
|
+
const requestedTestingAccount = resolveFlag(argv, '--testing-account');
|
|
296
|
+
const { projectAccount, testingAccount: selectedTestingAccount } = selectHubSpotDirectDevAccounts({
|
|
297
|
+
accounts,
|
|
298
|
+
...(requestedProjectAccount ? { requestedProjectAccount } : {}),
|
|
299
|
+
...(requestedTestingAccount ? { requestedTestingAccount } : {}),
|
|
300
|
+
});
|
|
301
|
+
if (requestedProjectAccount && !projectAccount) {
|
|
302
|
+
const message = `HubSpot project account "${requestedProjectAccount}" is not configured. Run \`hs account list\` or pass a configured --project-account.`;
|
|
303
|
+
if (json) {
|
|
304
|
+
write(`${JSON.stringify({
|
|
305
|
+
schema_version: 1,
|
|
306
|
+
command: 'dev',
|
|
307
|
+
ok: false,
|
|
308
|
+
error: { code: 'HSX_E_DEV_SESSION_NO_HUBSPOT_ACCOUNT', message },
|
|
309
|
+
}, null, 2)}\n`);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
reporter.error('HSX_E_DEV_SESSION_NO_HUBSPOT_ACCOUNT', message);
|
|
313
|
+
reporter.done(undefined, 1);
|
|
314
|
+
}
|
|
315
|
+
return { exitCode: 1 };
|
|
316
|
+
}
|
|
317
|
+
if (!projectAccount) {
|
|
318
|
+
const message = 'HubSpot-direct dev needs an authenticated HubSpot project account. Run `hs account auth`, select a default with `hs account use`, or pass --project-account.';
|
|
319
|
+
if (json) {
|
|
320
|
+
write(`${JSON.stringify({
|
|
321
|
+
schema_version: 1,
|
|
322
|
+
command: 'dev',
|
|
323
|
+
ok: false,
|
|
324
|
+
error: { code: 'HSX_E_DEV_SESSION_NO_HUBSPOT_ACCOUNT', message },
|
|
325
|
+
}, null, 2)}\n`);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
reporter.error('HSX_E_DEV_SESSION_NO_HUBSPOT_ACCOUNT', message);
|
|
329
|
+
reporter.done(undefined, 1);
|
|
330
|
+
}
|
|
331
|
+
return { exitCode: 1 };
|
|
332
|
+
}
|
|
333
|
+
if (requestedTestingAccount && !selectedTestingAccount) {
|
|
334
|
+
const message = `HubSpot testing account "${requestedTestingAccount}" is not configured. Run \`hs account list\` or pass a configured --testing-account.`;
|
|
335
|
+
if (json) {
|
|
336
|
+
write(`${JSON.stringify({
|
|
337
|
+
schema_version: 1,
|
|
338
|
+
command: 'dev',
|
|
339
|
+
ok: false,
|
|
340
|
+
error: { code: 'HSX_E_DEV_SESSION_NO_HUBSPOT_ACCOUNT', message },
|
|
341
|
+
}, null, 2)}\n`);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
reporter.error('HSX_E_DEV_SESSION_NO_HUBSPOT_ACCOUNT', message);
|
|
345
|
+
reporter.done(undefined, 1);
|
|
346
|
+
}
|
|
347
|
+
return { exitCode: 1 };
|
|
348
|
+
}
|
|
349
|
+
const testingAccount = selectedTestingAccount ?? projectAccount;
|
|
350
|
+
if (testingAccount.personalAccessKey) {
|
|
351
|
+
try {
|
|
352
|
+
const exchanged = await exchangePak(testingAccount.personalAccessKey);
|
|
353
|
+
if (exchanged.portalId !== testingAccount.portalId) {
|
|
354
|
+
throw new Error(`the configured PAK belongs to account ${exchanged.portalId}, not ${testingAccount.portalId}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch (cause) {
|
|
358
|
+
const message = `HubSpot testing account ${testingAccount.name} (${testingAccount.portalId}) needs fresh authentication: ${cause instanceof Error ? cause.message : String(cause)} Run \`hs account auth\` for that account, then retry.`;
|
|
359
|
+
if (json) {
|
|
360
|
+
write(`${JSON.stringify({
|
|
361
|
+
schema_version: 1,
|
|
362
|
+
command: 'dev',
|
|
363
|
+
ok: false,
|
|
364
|
+
error: { code: 'HSX_E_DEV_SESSION_NO_HUBSPOT_ACCOUNT', message },
|
|
365
|
+
}, null, 2)}\n`);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
reporter.error('HSX_E_DEV_SESSION_NO_HUBSPOT_ACCOUNT', message);
|
|
369
|
+
reporter.done(undefined, 1);
|
|
370
|
+
}
|
|
371
|
+
return { exitCode: 1 };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// Check the fixed HubSpot UI-extension port before translating the project;
|
|
375
|
+
// otherwise a conflict is discovered only after the comparatively expensive
|
|
376
|
+
// IR probe/build path has already run.
|
|
377
|
+
const existingDevServer = await recoverHubSpotDevPort({ argv, json, reporter });
|
|
378
|
+
if (existingDevServer.kind === 'exit')
|
|
379
|
+
return existingDevServer.result;
|
|
380
|
+
const uiExtensionNodes = await probeHubSpotUiNodes({
|
|
381
|
+
root,
|
|
382
|
+
portalIdFlag: String(projectAccount.portalId),
|
|
383
|
+
accountId: undefined,
|
|
384
|
+
...(profile ? { profile } : {}),
|
|
385
|
+
});
|
|
386
|
+
const requestedComponents = resolveFlags(argv, '--component');
|
|
387
|
+
const resolvedComponents = resolveHubSpotDevComponentSelectors(uiExtensionNodes?.candidates ?? [], requestedComponents);
|
|
388
|
+
if (!resolvedComponents.ok) {
|
|
389
|
+
if (json) {
|
|
390
|
+
write(`${JSON.stringify({
|
|
391
|
+
schema_version: 1,
|
|
392
|
+
command: 'dev',
|
|
393
|
+
ok: false,
|
|
394
|
+
error: { code: 'HSX_E_DEV_SESSION_NO_NODES', message: resolvedComponents.message },
|
|
395
|
+
}, null, 2)}\n`);
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
reporter.error('HSX_E_DEV_SESSION_NO_NODES', resolvedComponents.message);
|
|
399
|
+
reporter.done(undefined, 1);
|
|
400
|
+
}
|
|
401
|
+
return { exitCode: 1 };
|
|
402
|
+
}
|
|
403
|
+
let selectedNodeUids = uiExtensionNodes && uiExtensionNodes.candidates.length > 0
|
|
404
|
+
? resolvedComponents.uids
|
|
405
|
+
: undefined;
|
|
406
|
+
if (requestedComponents.length === 0 &&
|
|
407
|
+
uiExtensionNodes &&
|
|
408
|
+
uiExtensionNodes.candidates.length > 1 &&
|
|
409
|
+
!json &&
|
|
410
|
+
isInteractive()) {
|
|
411
|
+
const picked = await promptMultiSelect({
|
|
412
|
+
message: `Which HubSpot components should run in dev mode? (testing account ${testingAccount.portalId})`,
|
|
413
|
+
initial: [],
|
|
414
|
+
minSelected: 1,
|
|
415
|
+
options: uiExtensionNodes.candidates.map((candidate) => ({
|
|
416
|
+
value: candidate.uid,
|
|
417
|
+
label: candidate.label,
|
|
418
|
+
description: candidate.description,
|
|
419
|
+
})),
|
|
420
|
+
});
|
|
421
|
+
if (picked === undefined)
|
|
422
|
+
return cancelledResult(argv, 'dev');
|
|
423
|
+
selectedNodeUids = picked;
|
|
424
|
+
}
|
|
425
|
+
let directDevLogStream;
|
|
426
|
+
if (!json) {
|
|
427
|
+
const hsxLogPortValue = resolveFlag(argv, '--hsx-log-port') ?? '9099';
|
|
428
|
+
const hsxLogPort = Number(hsxLogPortValue);
|
|
429
|
+
if (!Number.isInteger(hsxLogPort) || hsxLogPort < 1 || hsxLogPort > 65535) {
|
|
430
|
+
const code = 'HSX_E_INPUT_BAD_FIELD';
|
|
431
|
+
reporter.error(code, `--hsx-log-port must be an integer between 1 and 65535 (got \`${hsxLogPortValue}\`).`, {
|
|
432
|
+
hint: 'Example: hs-x dev --hsx-log-port 9099',
|
|
433
|
+
docs_url: `https://hs-x.dev/errors/${code}`,
|
|
434
|
+
});
|
|
435
|
+
reporter.done(undefined, 10);
|
|
436
|
+
return { exitCode: 10 };
|
|
437
|
+
}
|
|
438
|
+
const { parseOnlySelectors } = await import('../dev/stream-filter.js');
|
|
439
|
+
const onlyParsed = parseOnlySelectors(resolveFlags(argv, '--only'));
|
|
440
|
+
if (!onlyParsed.ok) {
|
|
441
|
+
const code = 'HSX_E_DEV_BAD_ONLY';
|
|
442
|
+
reporter.error(code, onlyParsed.message, {
|
|
443
|
+
hint: 'Selectors: frontend, hubspot, errors, warnings, or free text. Repeat --only to combine.',
|
|
444
|
+
docs_url: `https://hs-x.dev/errors/${code}`,
|
|
445
|
+
});
|
|
446
|
+
reporter.done(undefined, 2);
|
|
447
|
+
return { exitCode: 2 };
|
|
448
|
+
}
|
|
449
|
+
const managedSessionId = resolveFlag(argv, '--managed-session');
|
|
450
|
+
directDevLogStream = await startHsxDevLogStream({
|
|
451
|
+
port: hsxLogPort,
|
|
452
|
+
root,
|
|
453
|
+
reporter,
|
|
454
|
+
filter: onlyParsed.filter,
|
|
455
|
+
...(managedSessionId ? { managedSessionId } : {}),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
const { startDevSession } = await import('../dev/session-manager.js');
|
|
459
|
+
const startDirectSession = () => startDevSession({
|
|
460
|
+
projectAccountId: projectAccount.portalId,
|
|
461
|
+
testingAccountId: testingAccount.portalId,
|
|
462
|
+
projectDir: root,
|
|
463
|
+
...(selectedNodeUids ? { selectedNodeUids } : {}),
|
|
464
|
+
...(profile ? { profile } : {}),
|
|
465
|
+
...(directDevLogStream
|
|
466
|
+
? {
|
|
467
|
+
onFrontendLog: (entry) => {
|
|
468
|
+
directDevLogStream?.bus.emit({
|
|
469
|
+
lane: 'frontend',
|
|
470
|
+
level: entry.level,
|
|
471
|
+
ts: entry.ts,
|
|
472
|
+
msg: entry.msg,
|
|
473
|
+
...(entry.source ? { source: entry.source } : {}),
|
|
474
|
+
...(entry.fields ? { fields: entry.fields } : {}),
|
|
475
|
+
});
|
|
476
|
+
},
|
|
477
|
+
onBackendLog: (entry) => {
|
|
478
|
+
directDevLogStream?.bus.emit({
|
|
479
|
+
lane: 'backend',
|
|
480
|
+
level: entry.level,
|
|
481
|
+
ts: Date.now(),
|
|
482
|
+
msg: entry.message,
|
|
483
|
+
source: `console.${entry.method}`,
|
|
484
|
+
});
|
|
485
|
+
},
|
|
486
|
+
}
|
|
487
|
+
: {}),
|
|
488
|
+
});
|
|
489
|
+
let session;
|
|
490
|
+
let sessionFailure;
|
|
491
|
+
try {
|
|
492
|
+
session = await startDirectSession();
|
|
493
|
+
}
|
|
494
|
+
catch (initialCause) {
|
|
495
|
+
sessionFailure = initialCause;
|
|
496
|
+
if (isHubSpotDevPortConflict(initialCause)) {
|
|
497
|
+
const recovery = await recoverHubSpotDevPort({ argv, json, reporter });
|
|
498
|
+
if (recovery.kind === 'exit') {
|
|
499
|
+
try {
|
|
500
|
+
await directDevLogStream?.cleanup();
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
// The port-conflict response is already complete.
|
|
504
|
+
}
|
|
505
|
+
return recovery.result;
|
|
506
|
+
}
|
|
507
|
+
if (recovery.kind === 'stopped') {
|
|
508
|
+
try {
|
|
509
|
+
session = await startDirectSession();
|
|
510
|
+
sessionFailure = undefined;
|
|
511
|
+
}
|
|
512
|
+
catch (retryCause) {
|
|
513
|
+
sessionFailure = retryCause;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (!session) {
|
|
519
|
+
if (directDevLogStream) {
|
|
520
|
+
try {
|
|
521
|
+
await directDevLogStream.cleanup();
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
// The dev-session error below is the actionable failure.
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
const message = describeDevSessionFailure(sessionFailure);
|
|
528
|
+
if (json) {
|
|
529
|
+
write(`${JSON.stringify({
|
|
530
|
+
schema_version: 1,
|
|
531
|
+
command: 'dev',
|
|
532
|
+
ok: false,
|
|
533
|
+
error: { code: 'HSX_E_DEV_SESSION_REGISTER', message },
|
|
534
|
+
}, null, 2)}\n`);
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
reporter.error('HSX_E_DEV_SESSION_REGISTER', message);
|
|
538
|
+
reporter.done(undefined, 1);
|
|
539
|
+
}
|
|
540
|
+
return { exitCode: 1 };
|
|
541
|
+
}
|
|
542
|
+
const managedSessionId = resolveFlag(argv, '--managed-session');
|
|
543
|
+
if (managedSessionId) {
|
|
544
|
+
await markManagedDevSessionReady(managedSessionId, {
|
|
545
|
+
projectAccountId: projectAccount.portalId,
|
|
546
|
+
testingAccountId: testingAccount.portalId,
|
|
547
|
+
components: session.nodeUids,
|
|
548
|
+
...(directDevLogStream
|
|
549
|
+
? { logPort: Number(resolveFlag(argv, '--hsx-log-port') ?? '9099') }
|
|
550
|
+
: {}),
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
const started = {
|
|
554
|
+
ok: true,
|
|
555
|
+
command: 'dev',
|
|
556
|
+
mode: 'hubspot-direct',
|
|
557
|
+
portalId: testingAccount.portalId,
|
|
558
|
+
projectAccountId: projectAccount.portalId,
|
|
559
|
+
testingAccountId: testingAccount.portalId,
|
|
560
|
+
sessionId: session.sessionId,
|
|
561
|
+
components: session.nodeUids,
|
|
562
|
+
controlPlaneUsed: false,
|
|
563
|
+
cloudflareUsed: false,
|
|
564
|
+
};
|
|
565
|
+
if (json)
|
|
566
|
+
write(`${JSON.stringify(started, null, 2)}\n`);
|
|
567
|
+
else {
|
|
568
|
+
reporter.header(`${projectAccount.name} → ${testingAccount.name}`);
|
|
569
|
+
reporter.rows([
|
|
570
|
+
{ key: 'mode', value: 'HubSpot direct' },
|
|
571
|
+
{ key: 'project-account', value: `${projectAccount.name} (${projectAccount.portalId})` },
|
|
572
|
+
{ key: 'testing-account', value: `${testingAccount.name} (${testingAccount.portalId})` },
|
|
573
|
+
{ key: 'ui-extensions', value: `session ${session.sessionId}`, status: 'ok' },
|
|
574
|
+
{ key: 'components', value: String(session.nodeUids.length) },
|
|
575
|
+
...(directDevLogStream
|
|
576
|
+
? [
|
|
577
|
+
{
|
|
578
|
+
key: 'logs',
|
|
579
|
+
value: 'frontend + backend + HubSpot events',
|
|
580
|
+
detail: directDevLogStream.url,
|
|
581
|
+
status: 'ok',
|
|
582
|
+
},
|
|
583
|
+
]
|
|
584
|
+
: []),
|
|
585
|
+
{ key: 'Cloudflare', value: 'not used' },
|
|
586
|
+
{ key: 'HS-X control plane', value: 'not used' },
|
|
587
|
+
]);
|
|
588
|
+
if (!directDevLogStream?.renderer.hasStatusLine())
|
|
589
|
+
reporter.info('Press Ctrl+C to stop');
|
|
590
|
+
}
|
|
591
|
+
const directKeyboard = directDevLogStream
|
|
592
|
+
? (await import('../dev/keyboard.js')).attachDevKeyboard({
|
|
593
|
+
renderer: directDevLogStream.renderer,
|
|
594
|
+
onQuit: () => process.kill(process.pid, 'SIGINT'),
|
|
595
|
+
})
|
|
596
|
+
: undefined;
|
|
597
|
+
directDevLogStream?.renderer.ready();
|
|
598
|
+
await new Promise((resolveClose) => {
|
|
599
|
+
let closing = false;
|
|
600
|
+
const close = async () => {
|
|
601
|
+
if (closing)
|
|
602
|
+
return;
|
|
603
|
+
closing = true;
|
|
604
|
+
directKeyboard?.stop();
|
|
605
|
+
if (managedSessionId) {
|
|
606
|
+
try {
|
|
607
|
+
await markManagedDevSessionState(managedSessionId, 'stopping', undefined, {
|
|
608
|
+
expectedPid: process.pid,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
// Continue cleanup even if the managed state record is unavailable.
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
await session.cleanup();
|
|
617
|
+
}
|
|
618
|
+
finally {
|
|
619
|
+
try {
|
|
620
|
+
await directDevLogStream?.cleanup();
|
|
621
|
+
}
|
|
622
|
+
finally {
|
|
623
|
+
if (managedSessionId) {
|
|
624
|
+
try {
|
|
625
|
+
await markManagedDevSessionState(managedSessionId, 'stopped', undefined, {
|
|
626
|
+
expectedPid: process.pid,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
// The process exit will be reconciled by the next status request.
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
resolveClose();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
process.once('SIGINT', close);
|
|
638
|
+
process.once('SIGTERM', close);
|
|
639
|
+
});
|
|
640
|
+
return { exitCode: 0 };
|
|
641
|
+
}
|
|
642
|
+
const MANAGED_DEV_SUBCOMMANDS = new Set(['start', 'logs', 'stop', 'restart']);
|
|
643
|
+
const KNOWN_DEV_SUBCOMMANDS = new Set([...MANAGED_DEV_SUBCOMMANDS, 'invoke', 'cleanup', 'status']);
|
|
644
|
+
const MANAGED_DEV_LANES = new Set(['frontend', 'backend', 'request', 'hubspot']);
|
|
645
|
+
const MANAGED_DEV_LEVELS = new Set(['debug', 'info', 'warn', 'error']);
|
|
646
|
+
export async function devCommand({ argv, root, json, reporter, }) {
|
|
647
|
+
const managedSessionId = resolveFlag(argv, '--managed-session');
|
|
648
|
+
if (managedSessionId) {
|
|
649
|
+
await waitForManagedDevSessionOwner(managedSessionId);
|
|
650
|
+
try {
|
|
651
|
+
const result = await devCommandCore({ argv, root, json: false });
|
|
652
|
+
if (result.exitCode !== 0) {
|
|
653
|
+
const current = await readManagedDevSession(managedSessionId);
|
|
654
|
+
if (current.state === 'starting') {
|
|
655
|
+
await markManagedDevSessionState(managedSessionId, 'failed', `hs-x dev exited with code ${result.exitCode} during startup.`, { expectedGeneration: current.generation, expectedPid: process.pid });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
catch (cause) {
|
|
661
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
662
|
+
try {
|
|
663
|
+
const current = await readManagedDevSession(managedSessionId);
|
|
664
|
+
if (current.state !== 'stopped' && current.state !== 'failed') {
|
|
665
|
+
await markManagedDevSessionState(managedSessionId, 'failed', message, {
|
|
666
|
+
expectedPid: process.pid,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
catch {
|
|
671
|
+
// Preserve the original startup failure if the state file is unavailable.
|
|
672
|
+
}
|
|
673
|
+
throw cause;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const subcommand = argv[1];
|
|
677
|
+
if (subcommand && !subcommand.startsWith('-') && !KNOWN_DEV_SUBCOMMANDS.has(subcommand)) {
|
|
678
|
+
const outputReporter = reporter ?? createReporter({ command: 'dev', argv });
|
|
679
|
+
outputReporter.error('HSX_E_INPUT_INVALID', `Unknown dev subcommand "${subcommand}".`, {
|
|
680
|
+
hint: 'Expected start, logs, stop, restart, invoke, cleanup, or status. Run `hs-x dev --help`.',
|
|
681
|
+
});
|
|
682
|
+
outputReporter.done(undefined, 2);
|
|
683
|
+
return { exitCode: 2 };
|
|
684
|
+
}
|
|
685
|
+
if (subcommand &&
|
|
686
|
+
KNOWN_DEV_SUBCOMMANDS.has(subcommand) &&
|
|
687
|
+
subcommand !== 'invoke' &&
|
|
688
|
+
argv[2] !== undefined &&
|
|
689
|
+
!argv[2].startsWith('-')) {
|
|
690
|
+
const outputReporter = reporter ?? createReporter({ command: `dev ${subcommand}`, argv });
|
|
691
|
+
outputReporter.error('HSX_E_INPUT_INVALID', `Unexpected argument "${argv[2]}" after \`hs-x dev ${subcommand}\`.`);
|
|
692
|
+
outputReporter.done(undefined, 2);
|
|
693
|
+
return { exitCode: 2 };
|
|
694
|
+
}
|
|
695
|
+
const reporterInput = reporter ? { reporter } : {};
|
|
696
|
+
if (subcommand === 'logs')
|
|
697
|
+
return managedDevLogsCommand({ argv, json, ...reporterInput });
|
|
698
|
+
if (subcommand === 'stop')
|
|
699
|
+
return managedDevStopCommand({ argv, json, ...reporterInput });
|
|
700
|
+
if (subcommand === 'restart') {
|
|
701
|
+
return managedDevRestartCommand({ argv, json, ...reporterInput });
|
|
702
|
+
}
|
|
703
|
+
if (subcommand === 'status' && resolveFlag(argv, '--session')) {
|
|
704
|
+
return managedDevStatusCommand({ argv, json, ...reporterInput });
|
|
705
|
+
}
|
|
706
|
+
if (subcommand === 'start' || argv.includes('--detach')) {
|
|
707
|
+
return managedDevStartCommand({ argv, root, json, ...reporterInput });
|
|
708
|
+
}
|
|
709
|
+
return devCommandCore({ argv, root, json });
|
|
710
|
+
}
|
|
711
|
+
async function devCommandCore({ argv, root, json, }) {
|
|
181
712
|
if (argv[1] === 'cleanup') {
|
|
182
713
|
return await devCleanupCommand({ argv, json });
|
|
183
714
|
}
|
|
@@ -193,7 +724,7 @@ export async function devCommand({ argv, root, json, }) {
|
|
|
193
724
|
prompts: { select: promptSelect },
|
|
194
725
|
});
|
|
195
726
|
if (selected && selected.root !== root) {
|
|
196
|
-
return await
|
|
727
|
+
return await devCommandCore({ argv, root: selected.root, json });
|
|
197
728
|
}
|
|
198
729
|
}
|
|
199
730
|
catch (error) {
|
|
@@ -205,6 +736,9 @@ export async function devCommand({ argv, root, json, }) {
|
|
|
205
736
|
if (argv[1] === 'invoke') {
|
|
206
737
|
return await devInvokeCommand({ argv, root, json });
|
|
207
738
|
}
|
|
739
|
+
if ((await detectLocalProjectMode(root)) === 'hubspot-direct') {
|
|
740
|
+
return await hubSpotDirectDevCommand({ argv, root, json });
|
|
741
|
+
}
|
|
208
742
|
const portFlag = resolveFlag(argv, '--port');
|
|
209
743
|
const port = Number(portFlag ?? '8787');
|
|
210
744
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
@@ -663,11 +1197,13 @@ export async function devCommand({ argv, root, json, }) {
|
|
|
663
1197
|
reporter.done(undefined, 2);
|
|
664
1198
|
return { exitCode: 2 };
|
|
665
1199
|
}
|
|
1200
|
+
const managedLogSessionId = resolveFlag(argv, '--managed-session');
|
|
666
1201
|
const devLogStream = await startHsxDevLogStream({
|
|
667
1202
|
port: hsxLogPort,
|
|
668
1203
|
root,
|
|
669
1204
|
reporter,
|
|
670
1205
|
filter: onlyParsed.filter,
|
|
1206
|
+
...(managedLogSessionId ? { managedSessionId: managedLogSessionId } : {}),
|
|
671
1207
|
});
|
|
672
1208
|
if (devLogStream) {
|
|
673
1209
|
const paint = makeDevLogPaint(process.env, process.stderr);
|
|
@@ -727,6 +1263,14 @@ export async function devCommand({ argv, root, json, }) {
|
|
|
727
1263
|
}),
|
|
728
1264
|
});
|
|
729
1265
|
}
|
|
1266
|
+
const managedSessionId = resolveFlag(argv, '--managed-session');
|
|
1267
|
+
if (managedSessionId) {
|
|
1268
|
+
await markManagedDevSessionReady(managedSessionId, {
|
|
1269
|
+
components: selectedUiNodeUids ?? capabilityIds,
|
|
1270
|
+
devPort: port,
|
|
1271
|
+
...(devLogStream ? { logPort: hsxLogPort } : {}),
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
730
1274
|
// Railed startup checklist: identity header, then one row per surface.
|
|
731
1275
|
reporter.header(subject);
|
|
732
1276
|
reporter.rows([
|
|
@@ -794,8 +1338,22 @@ export async function devCommand({ argv, root, json, }) {
|
|
|
794
1338
|
: undefined;
|
|
795
1339
|
devLogStream?.renderer.ready();
|
|
796
1340
|
await new Promise((resolveClose) => {
|
|
1341
|
+
let closing = false;
|
|
797
1342
|
const close = async () => {
|
|
1343
|
+
if (closing)
|
|
1344
|
+
return;
|
|
1345
|
+
closing = true;
|
|
798
1346
|
keyboard?.stop();
|
|
1347
|
+
if (managedSessionId) {
|
|
1348
|
+
try {
|
|
1349
|
+
await markManagedDevSessionState(managedSessionId, 'stopping', undefined, {
|
|
1350
|
+
expectedPid: process.pid,
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
catch {
|
|
1354
|
+
// Continue cleanup even if the managed state record is unavailable.
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
799
1357
|
// Signal-driven shutdown can terminate the Bun process before awaited
|
|
800
1358
|
// cleanup settles. Restore temporarily stamped migration sources
|
|
801
1359
|
// synchronously first; the remaining network/process cleanup is
|
|
@@ -848,13 +1406,615 @@ export async function devCommand({ argv, root, json, }) {
|
|
|
848
1406
|
// best-effort: sidecar shutdown failures should not block exit.
|
|
849
1407
|
}
|
|
850
1408
|
}
|
|
851
|
-
server.close(() =>
|
|
1409
|
+
server.close(async () => {
|
|
1410
|
+
if (managedSessionId) {
|
|
1411
|
+
try {
|
|
1412
|
+
await markManagedDevSessionState(managedSessionId, 'stopped', undefined, {
|
|
1413
|
+
expectedPid: process.pid,
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
catch {
|
|
1417
|
+
// The process exit will be reconciled by the next status request.
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
resolveClose();
|
|
1421
|
+
});
|
|
852
1422
|
};
|
|
853
1423
|
process.once('SIGINT', close);
|
|
854
1424
|
process.once('SIGTERM', close);
|
|
855
1425
|
});
|
|
856
1426
|
return { exitCode: 0 };
|
|
857
1427
|
}
|
|
1428
|
+
async function managedDevStartCommand(input) {
|
|
1429
|
+
const reporter = input.reporter ??
|
|
1430
|
+
createReporter({ command: 'dev start', argv: input.argv, entry: !input.json });
|
|
1431
|
+
let waitMs;
|
|
1432
|
+
try {
|
|
1433
|
+
waitMs = parsePositiveSeconds(input.argv, '--wait-seconds', 60) * 1_000;
|
|
1434
|
+
}
|
|
1435
|
+
catch (cause) {
|
|
1436
|
+
return managedDevCommandFailure(reporter, cause, 'HSX_E_INPUT_BAD_FIELD');
|
|
1437
|
+
}
|
|
1438
|
+
const root = resolvePath(input.root);
|
|
1439
|
+
const consent = await requestMutationConsent({
|
|
1440
|
+
yes: input.argv.some((argument) => argument === '--yes' || argument === '-y'),
|
|
1441
|
+
machineOutput: input.json || isMachineOutput(),
|
|
1442
|
+
interactive: isInteractive(),
|
|
1443
|
+
message: `Start an agent-managed background dev session in ${root}?`,
|
|
1444
|
+
confirm: promptConfirm,
|
|
1445
|
+
});
|
|
1446
|
+
if (consent !== 'proceed') {
|
|
1447
|
+
const message = consent === 'require-yes'
|
|
1448
|
+
? 'Starting a managed dev session in non-interactive or machine-output mode requires --yes.'
|
|
1449
|
+
: 'Start cancelled.';
|
|
1450
|
+
reporter.data({ target: { root } });
|
|
1451
|
+
reporter.error('HSX_E_INPUT_INVALID', message, {
|
|
1452
|
+
hint: 'Review the target root, then rerun `hs-x dev start --yes` with the same dev flags.',
|
|
1453
|
+
});
|
|
1454
|
+
reporter.done(undefined, consent === 'require-yes' ? 2 : 130);
|
|
1455
|
+
return { exitCode: consent === 'require-yes' ? 2 : 130 };
|
|
1456
|
+
}
|
|
1457
|
+
const launchArgv = managedDevLaunchArgv(input.argv);
|
|
1458
|
+
const session = await createManagedDevSession({
|
|
1459
|
+
root,
|
|
1460
|
+
launchArgv,
|
|
1461
|
+
});
|
|
1462
|
+
try {
|
|
1463
|
+
await launchManagedDevChild(session);
|
|
1464
|
+
const result = await waitForManagedDevStartup(session.sessionId, waitMs);
|
|
1465
|
+
const safe = await publicManagedDevSession(result);
|
|
1466
|
+
if (safe.state === 'failed' || safe.state === 'stopped') {
|
|
1467
|
+
reporter.data({ session: safe });
|
|
1468
|
+
reporter.error('HSX_E_DEV_MANAGED_START', safe.error ?? `Managed dev session ${safe.sessionId} stopped before it became ready.`, { hint: `Inspect ${safe.stderrPath} for the complete startup output.` });
|
|
1469
|
+
reporter.done(undefined, 1);
|
|
1470
|
+
return { exitCode: 1 };
|
|
1471
|
+
}
|
|
1472
|
+
renderManagedDevSession(reporter, safe, 'started');
|
|
1473
|
+
reporter.data({
|
|
1474
|
+
session: safe,
|
|
1475
|
+
commands: managedDevSessionCommands(safe.sessionId),
|
|
1476
|
+
});
|
|
1477
|
+
if (safe.state !== 'ready') {
|
|
1478
|
+
reporter.warn('HSX_W_DEV_MANAGED_STARTING', `Managed dev session ${safe.sessionId} is still starting. Check it with \`hs-x dev status --session ${safe.sessionId}\`.`);
|
|
1479
|
+
}
|
|
1480
|
+
else if (!safe.logPort) {
|
|
1481
|
+
reporter.warn('HSX_W_DEV_MANAGED_LOG_STREAM', 'The dev server is ready, but its unified log sidecar is unavailable. Inspect stderrPath and restart with a free --hsx-log-port.');
|
|
1482
|
+
}
|
|
1483
|
+
reporter.done(safe.state === 'ready' ? 'Dev session ready' : 'Dev session still starting');
|
|
1484
|
+
return { exitCode: 0 };
|
|
1485
|
+
}
|
|
1486
|
+
catch (cause) {
|
|
1487
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1488
|
+
try {
|
|
1489
|
+
await markManagedDevSessionState(session.sessionId, 'failed', message, {
|
|
1490
|
+
expectedGeneration: session.generation,
|
|
1491
|
+
expectedStates: ['starting'],
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
catch {
|
|
1495
|
+
// Keep the launch error as the primary failure.
|
|
1496
|
+
}
|
|
1497
|
+
reporter.error('HSX_E_DEV_MANAGED_START', message, {
|
|
1498
|
+
hint: `Inspect ${session.stderrPath} for startup output.`,
|
|
1499
|
+
});
|
|
1500
|
+
reporter.done(undefined, 1);
|
|
1501
|
+
return { exitCode: 1 };
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
async function managedDevStatusCommand(input) {
|
|
1505
|
+
const reporter = input.reporter ?? createReporter({ command: 'dev status', argv: input.argv });
|
|
1506
|
+
const sessionId = requireManagedSessionId(input.argv, reporter);
|
|
1507
|
+
if (!sessionId)
|
|
1508
|
+
return { exitCode: 2 };
|
|
1509
|
+
try {
|
|
1510
|
+
const safe = await publicManagedDevSession(await reconcileManagedDevSession(sessionId));
|
|
1511
|
+
renderManagedDevSession(reporter, safe, 'status');
|
|
1512
|
+
reporter.data({ session: safe, commands: managedDevSessionCommands(sessionId) });
|
|
1513
|
+
reporter.done();
|
|
1514
|
+
return { exitCode: 0 };
|
|
1515
|
+
}
|
|
1516
|
+
catch (cause) {
|
|
1517
|
+
return managedDevCommandFailure(reporter, cause, 'HSX_E_DEV_MANAGED_SESSION');
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
async function managedDevStopCommand(input) {
|
|
1521
|
+
const reporter = input.reporter ?? createReporter({ command: 'dev stop', argv: input.argv });
|
|
1522
|
+
const sessionId = requireManagedSessionId(input.argv, reporter);
|
|
1523
|
+
if (!sessionId)
|
|
1524
|
+
return { exitCode: 2 };
|
|
1525
|
+
try {
|
|
1526
|
+
const current = await reconcileManagedDevSession(sessionId);
|
|
1527
|
+
const safeCurrent = await publicManagedDevSession(current);
|
|
1528
|
+
renderManagedDevSession(reporter, safeCurrent, 'stop target');
|
|
1529
|
+
if (current.state === 'stopped' || current.state === 'failed') {
|
|
1530
|
+
reporter.data({ session: safeCurrent });
|
|
1531
|
+
reporter.done('Dev session is already stopped');
|
|
1532
|
+
return { exitCode: 0 };
|
|
1533
|
+
}
|
|
1534
|
+
const consent = await requestMutationConsent({
|
|
1535
|
+
yes: input.argv.includes('--yes'),
|
|
1536
|
+
machineOutput: input.json || isMachineOutput(),
|
|
1537
|
+
interactive: isInteractive(),
|
|
1538
|
+
message: `Stop managed dev session ${sessionId} (PID ${current.pid})?`,
|
|
1539
|
+
confirm: promptConfirm,
|
|
1540
|
+
});
|
|
1541
|
+
if (consent !== 'proceed') {
|
|
1542
|
+
const message = consent === 'require-yes'
|
|
1543
|
+
? 'Stopping a managed dev session in non-interactive or machine-output mode requires --yes.'
|
|
1544
|
+
: 'Stop cancelled.';
|
|
1545
|
+
reporter.data({ session: safeCurrent });
|
|
1546
|
+
reporter.error('HSX_E_INPUT_INVALID', message, {
|
|
1547
|
+
hint: `Review the session above, then run \`hs-x dev stop --session ${sessionId} --yes\`.`,
|
|
1548
|
+
});
|
|
1549
|
+
reporter.done(undefined, consent === 'require-yes' ? 2 : 130);
|
|
1550
|
+
return { exitCode: consent === 'require-yes' ? 2 : 130 };
|
|
1551
|
+
}
|
|
1552
|
+
const stopped = await stopManagedDevSessionProcess(sessionId, {
|
|
1553
|
+
force: input.argv.includes('--force'),
|
|
1554
|
+
});
|
|
1555
|
+
const safe = await publicManagedDevSession(stopped);
|
|
1556
|
+
reporter.data({ session: safe });
|
|
1557
|
+
reporter.step(`Stopped managed dev session ${sessionId}`).ok(`PID ${current.pid}`);
|
|
1558
|
+
reporter.done('Dev session stopped');
|
|
1559
|
+
return { exitCode: 0 };
|
|
1560
|
+
}
|
|
1561
|
+
catch (cause) {
|
|
1562
|
+
return managedDevCommandFailure(reporter, cause, 'HSX_E_DEV_MANAGED_STOP');
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
async function managedDevRestartCommand(input) {
|
|
1566
|
+
const reporter = input.reporter ?? createReporter({ command: 'dev restart', argv: input.argv });
|
|
1567
|
+
const sessionId = requireManagedSessionId(input.argv, reporter);
|
|
1568
|
+
if (!sessionId)
|
|
1569
|
+
return { exitCode: 2 };
|
|
1570
|
+
try {
|
|
1571
|
+
const waitMs = parsePositiveSeconds(input.argv, '--wait-seconds', 60) * 1_000;
|
|
1572
|
+
const current = await reconcileManagedDevSession(sessionId);
|
|
1573
|
+
const safeCurrent = await publicManagedDevSession(current);
|
|
1574
|
+
renderManagedDevSession(reporter, safeCurrent, 'restart target');
|
|
1575
|
+
const consent = await requestMutationConsent({
|
|
1576
|
+
yes: input.argv.includes('--yes'),
|
|
1577
|
+
machineOutput: input.json || isMachineOutput(),
|
|
1578
|
+
interactive: isInteractive(),
|
|
1579
|
+
message: `Restart managed dev session ${sessionId}${safeCurrent.running ? ` (PID ${current.pid})` : ''}?`,
|
|
1580
|
+
confirm: promptConfirm,
|
|
1581
|
+
});
|
|
1582
|
+
if (consent !== 'proceed') {
|
|
1583
|
+
const message = consent === 'require-yes'
|
|
1584
|
+
? 'Restarting a managed dev session in non-interactive or machine-output mode requires --yes.'
|
|
1585
|
+
: 'Restart cancelled.';
|
|
1586
|
+
reporter.data({ session: safeCurrent });
|
|
1587
|
+
reporter.error('HSX_E_INPUT_INVALID', message, {
|
|
1588
|
+
hint: `Review the session above, then run \`hs-x dev restart --session ${sessionId} --yes\`.`,
|
|
1589
|
+
});
|
|
1590
|
+
reporter.done(undefined, consent === 'require-yes' ? 2 : 130);
|
|
1591
|
+
return { exitCode: consent === 'require-yes' ? 2 : 130 };
|
|
1592
|
+
}
|
|
1593
|
+
const restartBase = safeCurrent.running
|
|
1594
|
+
? await stopManagedDevSessionProcess(sessionId, { force: input.argv.includes('--force') })
|
|
1595
|
+
: current;
|
|
1596
|
+
const restarting = await prepareManagedDevSessionRestart(sessionId, {
|
|
1597
|
+
expectedGeneration: restartBase.generation,
|
|
1598
|
+
expectedPid: restartBase.pid,
|
|
1599
|
+
expectedStates: [restartBase.state],
|
|
1600
|
+
});
|
|
1601
|
+
await launchManagedDevChild(restarting);
|
|
1602
|
+
const result = await waitForManagedDevStartup(sessionId, waitMs);
|
|
1603
|
+
const safe = await publicManagedDevSession(result);
|
|
1604
|
+
if (safe.state === 'failed' || safe.state === 'stopped') {
|
|
1605
|
+
reporter.data({ session: safe });
|
|
1606
|
+
reporter.error('HSX_E_DEV_MANAGED_RESTART', safe.error ?? `Managed dev session ${sessionId} stopped while restarting.`, { hint: `Inspect ${safe.stderrPath} for the complete restart output.` });
|
|
1607
|
+
reporter.done(undefined, 1);
|
|
1608
|
+
return { exitCode: 1 };
|
|
1609
|
+
}
|
|
1610
|
+
renderManagedDevSession(reporter, safe, 'restarted');
|
|
1611
|
+
reporter.data({ session: safe, commands: managedDevSessionCommands(sessionId) });
|
|
1612
|
+
if (safe.state === 'ready' && !safe.logPort) {
|
|
1613
|
+
reporter.warn('HSX_W_DEV_MANAGED_LOG_STREAM', 'The dev server restarted, but its unified log sidecar is unavailable. Inspect stderrPath and retry with a free --hsx-log-port.');
|
|
1614
|
+
}
|
|
1615
|
+
reporter.done(safe.state === 'ready' ? 'Dev session restarted' : 'Dev session still starting');
|
|
1616
|
+
return { exitCode: 0 };
|
|
1617
|
+
}
|
|
1618
|
+
catch (cause) {
|
|
1619
|
+
return managedDevCommandFailure(reporter, cause, 'HSX_E_DEV_MANAGED_RESTART');
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
async function managedDevLogsCommand(input) {
|
|
1623
|
+
const reporter = input.reporter ?? createReporter({ command: 'dev logs', argv: input.argv });
|
|
1624
|
+
const sessionId = requireManagedSessionId(input.argv, reporter);
|
|
1625
|
+
if (!sessionId)
|
|
1626
|
+
return { exitCode: 2 };
|
|
1627
|
+
if (input.argv.includes('--follow') && input.json) {
|
|
1628
|
+
reporter.error('HSX_E_DEV_LOGS_OUTPUT', '`--follow` is an event stream and cannot be combined with `--json`.', { hint: 'Use `--json-stream` for machine-readable follow mode.' });
|
|
1629
|
+
reporter.done(undefined, 2);
|
|
1630
|
+
return { exitCode: 2 };
|
|
1631
|
+
}
|
|
1632
|
+
try {
|
|
1633
|
+
let session = await reconcileManagedDevSession(sessionId);
|
|
1634
|
+
const cursor = parseNonNegativeInteger(input.argv, '--cursor', 0);
|
|
1635
|
+
const limit = parsePositiveInteger(input.argv, '--limit', 200, 10_000);
|
|
1636
|
+
const filter = parseManagedDevEventFilter(input.argv);
|
|
1637
|
+
if (!input.argv.includes('--follow')) {
|
|
1638
|
+
const result = await readManagedDevEvents({ session, cursor, limit, filter });
|
|
1639
|
+
if (reporter.mode === 'json' || reporter.mode === 'ndjson') {
|
|
1640
|
+
reporter.data({
|
|
1641
|
+
session_id: sessionId,
|
|
1642
|
+
cursor,
|
|
1643
|
+
next_cursor: result.nextCursor,
|
|
1644
|
+
end_cursor: result.endCursor,
|
|
1645
|
+
events: result.events,
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
else {
|
|
1649
|
+
reporter.header(`${sessionId} · ${result.events.length} event${result.events.length === 1 ? '' : 's'}`);
|
|
1650
|
+
const view = await startManagedDevLogView({ follow: false, filter });
|
|
1651
|
+
try {
|
|
1652
|
+
for (const match of result.events)
|
|
1653
|
+
view.emit(match.event);
|
|
1654
|
+
}
|
|
1655
|
+
finally {
|
|
1656
|
+
view.stop();
|
|
1657
|
+
}
|
|
1658
|
+
reporter.info(`Next cursor: ${result.nextCursor}`);
|
|
1659
|
+
}
|
|
1660
|
+
reporter.done();
|
|
1661
|
+
return { exitCode: 0 };
|
|
1662
|
+
}
|
|
1663
|
+
let nextCursor = cursor;
|
|
1664
|
+
let interrupted = false;
|
|
1665
|
+
let terminalSince;
|
|
1666
|
+
const interrupt = () => {
|
|
1667
|
+
interrupted = true;
|
|
1668
|
+
};
|
|
1669
|
+
process.once('SIGINT', interrupt);
|
|
1670
|
+
process.once('SIGTERM', interrupt);
|
|
1671
|
+
if (reporter.mode !== 'ndjson') {
|
|
1672
|
+
reporter.header(`${sessionId} · following from cursor ${cursor}`);
|
|
1673
|
+
}
|
|
1674
|
+
const followReadFilter = reporter.mode === 'ndjson' ? filter : managedDevFollowReadFilter(filter);
|
|
1675
|
+
let view;
|
|
1676
|
+
try {
|
|
1677
|
+
view =
|
|
1678
|
+
reporter.mode === 'ndjson'
|
|
1679
|
+
? undefined
|
|
1680
|
+
: await startManagedDevLogView({ follow: true, filter, onQuit: interrupt });
|
|
1681
|
+
view?.ready();
|
|
1682
|
+
while (!interrupted) {
|
|
1683
|
+
const previousCursor = nextCursor;
|
|
1684
|
+
const result = await readManagedDevEvents({
|
|
1685
|
+
session,
|
|
1686
|
+
cursor: nextCursor,
|
|
1687
|
+
limit,
|
|
1688
|
+
...(followReadFilter ? { filter: followReadFilter } : {}),
|
|
1689
|
+
});
|
|
1690
|
+
for (const match of result.events) {
|
|
1691
|
+
if (reporter.mode === 'ndjson') {
|
|
1692
|
+
reporter.event('dev.logs.event', {
|
|
1693
|
+
session_id: sessionId,
|
|
1694
|
+
cursor: match.cursor,
|
|
1695
|
+
event: match.event,
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
else {
|
|
1699
|
+
view?.emit(match.event);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
nextCursor = result.nextCursor;
|
|
1703
|
+
session = await reconcileManagedDevSession(sessionId);
|
|
1704
|
+
const terminal = session.state === 'stopped' || session.state === 'failed';
|
|
1705
|
+
if (terminal && nextCursor >= result.endCursor) {
|
|
1706
|
+
terminalSince ??= Date.now();
|
|
1707
|
+
if (Date.now() - terminalSince >= 1_000)
|
|
1708
|
+
break;
|
|
1709
|
+
}
|
|
1710
|
+
else {
|
|
1711
|
+
terminalSince = undefined;
|
|
1712
|
+
}
|
|
1713
|
+
if (nextCursor >= result.endCursor || nextCursor === previousCursor)
|
|
1714
|
+
await delayMs(250);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
finally {
|
|
1718
|
+
process.removeListener('SIGINT', interrupt);
|
|
1719
|
+
process.removeListener('SIGTERM', interrupt);
|
|
1720
|
+
view?.stop();
|
|
1721
|
+
}
|
|
1722
|
+
if (reporter.mode === 'ndjson') {
|
|
1723
|
+
reporter.event('dev.logs.cursor', { session_id: sessionId, next_cursor: nextCursor });
|
|
1724
|
+
}
|
|
1725
|
+
else {
|
|
1726
|
+
reporter.info(`Detached at cursor ${nextCursor}`);
|
|
1727
|
+
}
|
|
1728
|
+
reporter.done();
|
|
1729
|
+
return { exitCode: 0 };
|
|
1730
|
+
}
|
|
1731
|
+
catch (cause) {
|
|
1732
|
+
return managedDevCommandFailure(reporter, cause, 'HSX_E_DEV_MANAGED_LOGS');
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
function managedDevLaunchArgv(argv) {
|
|
1736
|
+
const output = [];
|
|
1737
|
+
const orchestrationValueFlags = new Set([
|
|
1738
|
+
'--managed-session',
|
|
1739
|
+
'--session',
|
|
1740
|
+
'--cursor',
|
|
1741
|
+
'--lane',
|
|
1742
|
+
'--level',
|
|
1743
|
+
'--search',
|
|
1744
|
+
'--since',
|
|
1745
|
+
'--limit',
|
|
1746
|
+
'--wait-seconds',
|
|
1747
|
+
]);
|
|
1748
|
+
const orchestrationBooleanFlags = new Set([
|
|
1749
|
+
'--detach',
|
|
1750
|
+
'--follow',
|
|
1751
|
+
'--json',
|
|
1752
|
+
'--json-stream',
|
|
1753
|
+
'--yes',
|
|
1754
|
+
'-y',
|
|
1755
|
+
]);
|
|
1756
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1757
|
+
const value = argv[index];
|
|
1758
|
+
if (!value)
|
|
1759
|
+
continue;
|
|
1760
|
+
if (index === 1 && value === 'start')
|
|
1761
|
+
continue;
|
|
1762
|
+
if (orchestrationBooleanFlags.has(value))
|
|
1763
|
+
continue;
|
|
1764
|
+
if (orchestrationValueFlags.has(value)) {
|
|
1765
|
+
index += 1;
|
|
1766
|
+
continue;
|
|
1767
|
+
}
|
|
1768
|
+
if ([...orchestrationValueFlags].some((flag) => value.startsWith(`${flag}=`)))
|
|
1769
|
+
continue;
|
|
1770
|
+
output.push(value);
|
|
1771
|
+
}
|
|
1772
|
+
return output;
|
|
1773
|
+
}
|
|
1774
|
+
async function launchManagedDevChild(session) {
|
|
1775
|
+
const entryCandidate = process.argv[1];
|
|
1776
|
+
const entry = entryCandidate ? resolvePath(entryCandidate) : undefined;
|
|
1777
|
+
const entryArgs = entry && existsSync(entry) ? [entry] : [];
|
|
1778
|
+
const stdoutFd = openSync(session.stdoutPath, 'a', 0o600);
|
|
1779
|
+
const stderrFd = openSync(session.stderrPath, 'a', 0o600);
|
|
1780
|
+
try {
|
|
1781
|
+
const child = spawn(process.execPath, [...entryArgs, ...session.launchArgv, '--managed-session', session.sessionId], {
|
|
1782
|
+
cwd: session.root,
|
|
1783
|
+
env: process.env,
|
|
1784
|
+
detached: true,
|
|
1785
|
+
stdio: ['ignore', stdoutFd, stderrFd],
|
|
1786
|
+
});
|
|
1787
|
+
await new Promise((resolveSpawn, rejectSpawn) => {
|
|
1788
|
+
child.once('spawn', resolveSpawn);
|
|
1789
|
+
child.once('error', rejectSpawn);
|
|
1790
|
+
});
|
|
1791
|
+
if (!child.pid)
|
|
1792
|
+
throw new Error('Detached hs-x dev process did not report a PID.');
|
|
1793
|
+
try {
|
|
1794
|
+
await setManagedDevSessionPid(session.sessionId, child.pid, {
|
|
1795
|
+
expectedGeneration: session.generation,
|
|
1796
|
+
expectedPid: 0,
|
|
1797
|
+
expectedStates: ['starting'],
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
catch (cause) {
|
|
1801
|
+
try {
|
|
1802
|
+
child.kill('SIGTERM');
|
|
1803
|
+
}
|
|
1804
|
+
catch {
|
|
1805
|
+
// The guarded state update is still the primary failure.
|
|
1806
|
+
}
|
|
1807
|
+
throw cause;
|
|
1808
|
+
}
|
|
1809
|
+
child.unref();
|
|
1810
|
+
return child.pid;
|
|
1811
|
+
}
|
|
1812
|
+
finally {
|
|
1813
|
+
closeSync(stdoutFd);
|
|
1814
|
+
closeSync(stderrFd);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
async function waitForManagedDevStartup(sessionId, timeoutMs) {
|
|
1818
|
+
const deadline = Date.now() + timeoutMs;
|
|
1819
|
+
let session = await reconcileManagedDevSession(sessionId);
|
|
1820
|
+
while (Date.now() < deadline && session.state === 'starting') {
|
|
1821
|
+
await delayMs(100);
|
|
1822
|
+
session = await reconcileManagedDevSession(sessionId);
|
|
1823
|
+
}
|
|
1824
|
+
return session;
|
|
1825
|
+
}
|
|
1826
|
+
function managedDevSessionCommands(sessionId) {
|
|
1827
|
+
return {
|
|
1828
|
+
logs: `hs-x dev logs --session ${sessionId} --follow`,
|
|
1829
|
+
logs_json: `hs-x dev logs --session ${sessionId} --follow --json-stream`,
|
|
1830
|
+
status: `hs-x dev status --session ${sessionId} --json`,
|
|
1831
|
+
restart: `hs-x dev restart --session ${sessionId} --yes`,
|
|
1832
|
+
stop: `hs-x dev stop --session ${sessionId} --yes`,
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
function renderManagedDevSession(reporter, session, subject) {
|
|
1836
|
+
if (reporter.mode === 'json' || reporter.mode === 'ndjson')
|
|
1837
|
+
return;
|
|
1838
|
+
reporter.header(`${subject} · ${session.sessionId}`);
|
|
1839
|
+
reporter.rows([
|
|
1840
|
+
{
|
|
1841
|
+
key: 'state',
|
|
1842
|
+
value: session.state,
|
|
1843
|
+
status: session.state === 'ready' ? 'ok' : session.state === 'failed' ? 'fail' : 'warn',
|
|
1844
|
+
},
|
|
1845
|
+
{ key: 'pid', value: session.pid > 1 ? String(session.pid) : 'not assigned' },
|
|
1846
|
+
{ key: 'generation', value: String(session.generation) },
|
|
1847
|
+
{ key: 'root', value: session.root },
|
|
1848
|
+
...(session.projectAccountId
|
|
1849
|
+
? [{ key: 'project-account', value: String(session.projectAccountId) }]
|
|
1850
|
+
: []),
|
|
1851
|
+
...(session.testingAccountId
|
|
1852
|
+
? [{ key: 'testing-account', value: String(session.testingAccountId) }]
|
|
1853
|
+
: []),
|
|
1854
|
+
...(session.components
|
|
1855
|
+
? [{ key: 'components', value: session.components.join(', ') || 'none' }]
|
|
1856
|
+
: []),
|
|
1857
|
+
...(session.logPort ? [{ key: 'log-port', value: String(session.logPort) }] : []),
|
|
1858
|
+
]);
|
|
1859
|
+
if (session.error)
|
|
1860
|
+
reporter.info(`Last error: ${session.error}`);
|
|
1861
|
+
reporter.info(`Logs: hs-x dev logs --session ${session.sessionId} --follow`);
|
|
1862
|
+
}
|
|
1863
|
+
function requireManagedSessionId(argv, reporter) {
|
|
1864
|
+
const sessionId = resolveFlag(argv, '--session');
|
|
1865
|
+
if (sessionId)
|
|
1866
|
+
return sessionId;
|
|
1867
|
+
reporter.error('HSX_E_INPUT_INVALID', 'A managed dev session id is required.', {
|
|
1868
|
+
hint: 'Pass the id returned by `hs-x dev start`, for example `--session dev_…`.',
|
|
1869
|
+
});
|
|
1870
|
+
reporter.done(undefined, 2);
|
|
1871
|
+
return undefined;
|
|
1872
|
+
}
|
|
1873
|
+
function managedDevCommandFailure(reporter, cause, code) {
|
|
1874
|
+
reporter.error(code, cause instanceof Error ? cause.message : String(cause));
|
|
1875
|
+
reporter.done(undefined, 1);
|
|
1876
|
+
return { exitCode: 1 };
|
|
1877
|
+
}
|
|
1878
|
+
function parseManagedDevEventFilter(argv) {
|
|
1879
|
+
const laneValues = resolveFlags(argv, '--lane');
|
|
1880
|
+
for (const lane of laneValues) {
|
|
1881
|
+
if (!MANAGED_DEV_LANES.has(lane)) {
|
|
1882
|
+
throw new Error(`Unknown --lane "${lane}". Expected frontend, backend, request, or hubspot.`);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
const levelValues = resolveFlags(argv, '--level');
|
|
1886
|
+
for (const level of levelValues) {
|
|
1887
|
+
if (!MANAGED_DEV_LEVELS.has(level)) {
|
|
1888
|
+
throw new Error(`Unknown --level "${level}". Expected debug, info, warn, or error.`);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
const sinceValue = resolveFlag(argv, '--since');
|
|
1892
|
+
const searchValue = resolveFlag(argv, '--search');
|
|
1893
|
+
return {
|
|
1894
|
+
...(laneValues.length > 0
|
|
1895
|
+
? { lanes: new Set(laneValues) }
|
|
1896
|
+
: {}),
|
|
1897
|
+
...(levelValues.length > 0
|
|
1898
|
+
? { levels: new Set(levelValues) }
|
|
1899
|
+
: {}),
|
|
1900
|
+
...(searchValue ? { search: searchValue } : {}),
|
|
1901
|
+
...(sinceValue ? { since: parseManagedSince(sinceValue) } : {}),
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
function parseManagedSince(value) {
|
|
1905
|
+
const relative = /^(\d+)(ms|s|m|h|d)$/.exec(value);
|
|
1906
|
+
if (relative) {
|
|
1907
|
+
const amount = Number(relative[1]);
|
|
1908
|
+
const unit = relative[2];
|
|
1909
|
+
const multiplier = unit === 'ms'
|
|
1910
|
+
? 1
|
|
1911
|
+
: unit === 's'
|
|
1912
|
+
? 1_000
|
|
1913
|
+
: unit === 'm'
|
|
1914
|
+
? 60_000
|
|
1915
|
+
: unit === 'h'
|
|
1916
|
+
? 3_600_000
|
|
1917
|
+
: 86_400_000;
|
|
1918
|
+
return Date.now() - amount * multiplier;
|
|
1919
|
+
}
|
|
1920
|
+
const parsed = Date.parse(value);
|
|
1921
|
+
if (!Number.isFinite(parsed)) {
|
|
1922
|
+
throw new Error(`Invalid --since value "${value}". Use an ISO timestamp or a duration like 30s, 5m, or 2h.`);
|
|
1923
|
+
}
|
|
1924
|
+
return parsed;
|
|
1925
|
+
}
|
|
1926
|
+
function parseNonNegativeInteger(argv, flag, fallback) {
|
|
1927
|
+
const raw = resolveFlag(argv, flag);
|
|
1928
|
+
if (raw === undefined)
|
|
1929
|
+
return fallback;
|
|
1930
|
+
const value = Number(raw);
|
|
1931
|
+
if (!Number.isInteger(value) || value < 0)
|
|
1932
|
+
throw new Error(`${flag} must be a non-negative integer.`);
|
|
1933
|
+
return value;
|
|
1934
|
+
}
|
|
1935
|
+
function parsePositiveInteger(argv, flag, fallback, maximum = Number.MAX_SAFE_INTEGER) {
|
|
1936
|
+
const raw = resolveFlag(argv, flag);
|
|
1937
|
+
if (raw === undefined)
|
|
1938
|
+
return fallback;
|
|
1939
|
+
const value = Number(raw);
|
|
1940
|
+
if (!Number.isInteger(value) || value < 1 || value > maximum) {
|
|
1941
|
+
throw new Error(`${flag} must be an integer between 1 and ${maximum}.`);
|
|
1942
|
+
}
|
|
1943
|
+
return value;
|
|
1944
|
+
}
|
|
1945
|
+
function parsePositiveSeconds(argv, flag, fallback) {
|
|
1946
|
+
return parsePositiveInteger(argv, flag, fallback, 600);
|
|
1947
|
+
}
|
|
1948
|
+
async function startManagedDevLogView(input) {
|
|
1949
|
+
const [eventBusModule, keyboardModule, formatModule, rendererModule] = await Promise.all([
|
|
1950
|
+
import('../dev/event-bus.js'),
|
|
1951
|
+
import('../dev/keyboard.js'),
|
|
1952
|
+
import('../dev/stream-event-format.js'),
|
|
1953
|
+
import('../dev/stream-renderer.js'),
|
|
1954
|
+
]);
|
|
1955
|
+
const bus = eventBusModule.createDevEventBus();
|
|
1956
|
+
const renderer = rendererModule.startDevStreamRenderer({
|
|
1957
|
+
bus,
|
|
1958
|
+
filter: managedDevEventFilterToStreamFilter(input.filter),
|
|
1959
|
+
...(input.follow ? { deferStatus: true } : { isTTY: false }),
|
|
1960
|
+
});
|
|
1961
|
+
const keyboard = input.follow
|
|
1962
|
+
? keyboardModule.attachDevKeyboard({
|
|
1963
|
+
renderer,
|
|
1964
|
+
onQuit: input.onQuit ?? (() => { }),
|
|
1965
|
+
})
|
|
1966
|
+
: undefined;
|
|
1967
|
+
return {
|
|
1968
|
+
emit(event) {
|
|
1969
|
+
bus.emit(formatModule.managedDevEventToDevEvent(event));
|
|
1970
|
+
},
|
|
1971
|
+
ready() {
|
|
1972
|
+
renderer.ready();
|
|
1973
|
+
},
|
|
1974
|
+
stop() {
|
|
1975
|
+
keyboard?.stop();
|
|
1976
|
+
renderer.stop();
|
|
1977
|
+
},
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
function managedDevEventFilterToStreamFilter(filter) {
|
|
1981
|
+
const levels = filter.levels;
|
|
1982
|
+
const level = levels?.size === 1 && levels.has('error')
|
|
1983
|
+
? 'error'
|
|
1984
|
+
: levels?.size === 2 && levels.has('warn') && levels.has('error')
|
|
1985
|
+
? 'warn'
|
|
1986
|
+
: 'all';
|
|
1987
|
+
return {
|
|
1988
|
+
lanes: filter.lanes,
|
|
1989
|
+
level,
|
|
1990
|
+
texts: filter.search ? [filter.search] : [],
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
/**
|
|
1994
|
+
* Human follow mode keeps lane/search/threshold filtering in the renderer so
|
|
1995
|
+
* keyboard controls can widen the retained view. Timestamp and exact level
|
|
1996
|
+
* sets that the threshold model cannot express remain storage-side. Machine
|
|
1997
|
+
* streams never use this split and retain their original cursor/filter path.
|
|
1998
|
+
*/
|
|
1999
|
+
function managedDevFollowReadFilter(filter) {
|
|
2000
|
+
const levels = filter.levels;
|
|
2001
|
+
const rendererCanRepresentLevels = levels === undefined ||
|
|
2002
|
+
(levels.size === 1 && levels.has('error')) ||
|
|
2003
|
+
(levels.size === 2 && levels.has('warn') && levels.has('error')) ||
|
|
2004
|
+
(levels.size === 4 &&
|
|
2005
|
+
levels.has('debug') &&
|
|
2006
|
+
levels.has('info') &&
|
|
2007
|
+
levels.has('warn') &&
|
|
2008
|
+
levels.has('error'));
|
|
2009
|
+
const readFilter = {
|
|
2010
|
+
...(filter.since !== undefined ? { since: filter.since } : {}),
|
|
2011
|
+
...(!rendererCanRepresentLevels && levels ? { levels } : {}),
|
|
2012
|
+
};
|
|
2013
|
+
return Object.keys(readFilter).length > 0 ? readFilter : undefined;
|
|
2014
|
+
}
|
|
2015
|
+
function delayMs(ms) {
|
|
2016
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2017
|
+
}
|
|
858
2018
|
async function startHsxDevLogStream(input) {
|
|
859
2019
|
try {
|
|
860
2020
|
const [{ createDevEventBus }, { startDevLogServer }, { startDevStreamRenderer }] = await Promise.all([
|
|
@@ -870,6 +2030,12 @@ async function startHsxDevLogStream(input) {
|
|
|
870
2030
|
deferStatus: true,
|
|
871
2031
|
...(input.filter ? { filter: input.filter } : {}),
|
|
872
2032
|
});
|
|
2033
|
+
const managedEventWriter = input.managedSessionId
|
|
2034
|
+
? createManagedDevEventWriter(await readManagedDevSession(input.managedSessionId))
|
|
2035
|
+
: undefined;
|
|
2036
|
+
const unsubscribeManagedEvents = managedEventWriter
|
|
2037
|
+
? bus.subscribe((event) => managedEventWriter.log(event))
|
|
2038
|
+
: undefined;
|
|
873
2039
|
// HubSpot-side lane: renders/proxy failures that never reach our Worker
|
|
874
2040
|
// only exist in HubSpot's app logs — poll them into the same stream.
|
|
875
2041
|
// Missing binding/credentials just means the lane stays off; `hs-x dev`
|
|
@@ -901,8 +2067,9 @@ async function startHsxDevLogStream(input) {
|
|
|
901
2067
|
bus,
|
|
902
2068
|
async cleanup() {
|
|
903
2069
|
hubspotPoller?.stop();
|
|
2070
|
+
unsubscribeManagedEvents?.();
|
|
904
2071
|
renderer.stop();
|
|
905
|
-
await server.close();
|
|
2072
|
+
await Promise.all([server.close(), managedEventWriter?.close()]);
|
|
906
2073
|
},
|
|
907
2074
|
};
|
|
908
2075
|
}
|
|
@@ -952,8 +2119,9 @@ async function probeHubSpotUiNodes(input) {
|
|
|
952
2119
|
projectSourceDir: pathJoin(input.root, projectConfig.srcDir),
|
|
953
2120
|
platformVersion: projectConfig.platformVersion,
|
|
954
2121
|
accountId: portalId,
|
|
955
|
-
}, {});
|
|
956
|
-
|
|
2122
|
+
}, input.profile ? { profile: input.profile } : {});
|
|
2123
|
+
const candidates = listHubSpotDevComponentCandidates(ir.intermediateNodesIndexedByUid);
|
|
2124
|
+
return { uids: candidates.map((candidate) => candidate.uid), candidates, portalId };
|
|
957
2125
|
}
|
|
958
2126
|
catch {
|
|
959
2127
|
// IR translate failed — caller falls back to capability-only behavior.
|
|
@@ -971,10 +2139,12 @@ async function maybeStartHubSpotDevSession(input) {
|
|
|
971
2139
|
}
|
|
972
2140
|
try {
|
|
973
2141
|
const { startDevSession } = await import('../dev/session-manager.js');
|
|
2142
|
+
const profile = resolveFlag(input.argv, '--profile');
|
|
974
2143
|
const session = await startDevSession({
|
|
975
2144
|
accountId: portalId,
|
|
976
2145
|
projectDir: input.root,
|
|
977
2146
|
selectedNodeUids: input.selectedNodeUids,
|
|
2147
|
+
...(profile ? { profile } : {}),
|
|
978
2148
|
});
|
|
979
2149
|
input.reporter
|
|
980
2150
|
.step('HubSpot UI-extension session')
|
|
@@ -982,7 +2152,7 @@ async function maybeStartHubSpotDevSession(input) {
|
|
|
982
2152
|
return session;
|
|
983
2153
|
}
|
|
984
2154
|
catch (error) {
|
|
985
|
-
input.reporter.warn('HSX_W_DEV_HUBSPOT_SESSION_FAILED', `Could not start UI-extension dev session: ${
|
|
2155
|
+
input.reporter.warn('HSX_W_DEV_HUBSPOT_SESSION_FAILED', `Could not start UI-extension dev session: ${describeDevSessionFailure(error)}`);
|
|
986
2156
|
return undefined;
|
|
987
2157
|
}
|
|
988
2158
|
}
|
|
@@ -1301,6 +2471,43 @@ async function devCleanupCommand({ argv, json, }) {
|
|
|
1301
2471
|
if (!accountId || !controlPlaneUrl || !sessionId) {
|
|
1302
2472
|
throw new Error('Dev cleanup requires --control-plane-url, --account-id, and --session-id.');
|
|
1303
2473
|
}
|
|
2474
|
+
const reporter = createReporter({ command: 'dev cleanup', argv });
|
|
2475
|
+
const machineOutput = json || isMachineOutput();
|
|
2476
|
+
const interactive = isInteractive();
|
|
2477
|
+
const yes = argv.some((arg) => arg === '--yes' || arg === '-y');
|
|
2478
|
+
if (!machineOutput && interactive) {
|
|
2479
|
+
reporter.block([
|
|
2480
|
+
'Dev cleanup plan',
|
|
2481
|
+
`Account: ${accountId}`,
|
|
2482
|
+
`Session: ${sessionId}`,
|
|
2483
|
+
`Control plane: ${controlPlaneUrl}`,
|
|
2484
|
+
'Change: clear every dev override registered by this session.',
|
|
2485
|
+
].join('\n'));
|
|
2486
|
+
}
|
|
2487
|
+
const consent = await requestMutationConsent({
|
|
2488
|
+
yes,
|
|
2489
|
+
machineOutput,
|
|
2490
|
+
interactive,
|
|
2491
|
+
message: `Clear dev overrides for session ${sessionId}?`,
|
|
2492
|
+
confirm: promptConfirm,
|
|
2493
|
+
});
|
|
2494
|
+
if (consent === 'require-yes') {
|
|
2495
|
+
const message = 'Dev cleanup requires explicit consent in machine or non-interactive mode. Re-run with --yes.';
|
|
2496
|
+
if (json) {
|
|
2497
|
+
reporter.raw(`${JSON.stringify({ ok: false, error: message }, null, 2)}\n`);
|
|
2498
|
+
}
|
|
2499
|
+
else {
|
|
2500
|
+
reporter.error('HSX_E_INPUT_INVALID', message, {
|
|
2501
|
+
hint: '`--yes` acknowledges this cleanup plan; it does not select or broaden the target session.',
|
|
2502
|
+
});
|
|
2503
|
+
// The Effect boundary emits the single terminal event for NDJSON.
|
|
2504
|
+
if (reporter.mode !== 'ndjson')
|
|
2505
|
+
reporter.done(undefined, 10);
|
|
2506
|
+
}
|
|
2507
|
+
return { exitCode: 10 };
|
|
2508
|
+
}
|
|
2509
|
+
if (consent === 'cancelled')
|
|
2510
|
+
return cancelledResult(argv, 'dev cleanup');
|
|
1304
2511
|
const cleared = await requestDevOverrideCleanup({
|
|
1305
2512
|
controlPlaneUrl,
|
|
1306
2513
|
accountId,
|
|
@@ -1319,7 +2526,6 @@ async function devCleanupCommand({ argv, json, }) {
|
|
|
1319
2526
|
write(`${JSON.stringify(result, null, 2)}\n`);
|
|
1320
2527
|
}
|
|
1321
2528
|
else {
|
|
1322
|
-
const reporter = createReporter({ command: 'dev cleanup', argv });
|
|
1323
2529
|
reporter.header(sessionId);
|
|
1324
2530
|
reporter.step(`Cleared ${cleared.cleared.length} dev overrides`).ok();
|
|
1325
2531
|
reporter.done();
|
|
@@ -1719,11 +2925,11 @@ function write(message) {
|
|
|
1719
2925
|
// as a silent `exitWith(code)` (the legacy body renders its own output) so
|
|
1720
2926
|
// `runHandler` does not re-render.
|
|
1721
2927
|
//
|
|
1722
|
-
// The first positional argument selects the
|
|
2928
|
+
// The first positional argument selects the managed-session and legacy subcommands
|
|
1723
2929
|
// the legacy body branches on via `argv[1]`.
|
|
1724
2930
|
// A positional (not an option) so `hs-x dev cleanup` / `dev status` parse — the
|
|
1725
2931
|
// legacy body branches on `argv[1]`.
|
|
1726
|
-
const devSubcommandArg = Args.text({ name: 'subcommand' }).pipe(Args.withDescription('Optional dev subcommand: invoke | cleanup | status'), Args.optional);
|
|
2932
|
+
const devSubcommandArg = Args.text({ name: 'subcommand' }).pipe(Args.withDescription('Optional dev subcommand: start | logs | stop | restart | invoke | cleanup | status'), Args.optional);
|
|
1727
2933
|
// Second positional: the capability id for `dev invoke <capability-id>`.
|
|
1728
2934
|
const devTargetArg = Args.text({ name: 'target' }).pipe(Args.withDescription('Capability id for `dev invoke`'), Args.optional);
|
|
1729
2935
|
const devInputOption = Options.text('input').pipe(Options.optional);
|
|
@@ -1734,6 +2940,10 @@ const devRemoteOption = Options.boolean('remote').pipe(Options.withDefault(false
|
|
|
1734
2940
|
const devUrlOption = Options.text('url').pipe(Options.optional);
|
|
1735
2941
|
const portOption = Options.text('port').pipe(Options.optional);
|
|
1736
2942
|
const portalOption = Options.text('portal').pipe(Options.optional);
|
|
2943
|
+
const accountOption = Options.text('account').pipe(Options.optional);
|
|
2944
|
+
const projectAccountOption = Options.text('project-account').pipe(Options.optional);
|
|
2945
|
+
const testingAccountOption = Options.text('testing-account').pipe(Options.optional);
|
|
2946
|
+
const profileOption = Options.text('profile').pipe(Options.withAlias('p'), Options.optional);
|
|
1737
2947
|
const targetOriginOption = Options.text('target-origin').pipe(Options.optional);
|
|
1738
2948
|
const telemetryOriginOption = Options.text('telemetry-origin').pipe(Options.optional);
|
|
1739
2949
|
const accountIdOption = Options.text('account-id').pipe(Options.optional);
|
|
@@ -1741,12 +2951,24 @@ const projectIdOption = Options.text('project-id').pipe(Options.optional);
|
|
|
1741
2951
|
const developerIdOption = Options.text('developer-id').pipe(Options.optional);
|
|
1742
2952
|
const userIdOption = Options.text('user-id').pipe(Options.optional);
|
|
1743
2953
|
const sessionIdOption = Options.text('session-id').pipe(Options.optional);
|
|
2954
|
+
const managedSessionOption = Options.text('session').pipe(Options.optional);
|
|
2955
|
+
const internalManagedSessionOption = Options.text('managed-session').pipe(Options.optional);
|
|
1744
2956
|
const ttlSecondsOption = Options.text('ttl-seconds').pipe(Options.optional);
|
|
1745
2957
|
const hsxLogPortOption = Options.text('hsx-log-port').pipe(Options.optional);
|
|
1746
2958
|
const controlPlaneUrlOption = Options.text('control-plane-url').pipe(Options.optional);
|
|
1747
2959
|
const runtimeControlPlaneUrlOption = Options.text('runtime-control-plane-url').pipe(Options.optional);
|
|
1748
2960
|
const capabilityOption = Options.text('capability').pipe(Options.repeated);
|
|
2961
|
+
const componentOption = Options.text('component').pipe(Options.repeated);
|
|
1749
2962
|
const onlyOption = Options.text('only').pipe(Options.repeated);
|
|
2963
|
+
const laneOption = Options.text('lane').pipe(Options.repeated);
|
|
2964
|
+
const levelOption = Options.text('level').pipe(Options.repeated);
|
|
2965
|
+
const searchOption = Options.text('search').pipe(Options.optional);
|
|
2966
|
+
const sinceOption = Options.text('since').pipe(Options.optional);
|
|
2967
|
+
const cursorOption = Options.text('cursor').pipe(Options.optional);
|
|
2968
|
+
const limitOption = Options.text('limit').pipe(Options.optional);
|
|
2969
|
+
const waitSecondsOption = Options.text('wait-seconds').pipe(Options.optional);
|
|
2970
|
+
const detachOption = Options.boolean('detach').pipe(Options.withDefault(false));
|
|
2971
|
+
const followOption = Options.boolean('follow').pipe(Options.withDefault(false));
|
|
1750
2972
|
const pickOption = Options.boolean('pick').pipe(Options.withDefault(false));
|
|
1751
2973
|
const noHubspotOption = Options.boolean('no-hubspot').pipe(Options.withDefault(false));
|
|
1752
2974
|
const noUiExtensionsOption = Options.boolean('no-ui-extensions').pipe(Options.withDefault(false));
|
|
@@ -1765,6 +2987,10 @@ export const devCmd = Command.make('dev', {
|
|
|
1765
2987
|
url: devUrlOption,
|
|
1766
2988
|
port: portOption,
|
|
1767
2989
|
portal: portalOption,
|
|
2990
|
+
account: accountOption,
|
|
2991
|
+
projectAccount: projectAccountOption,
|
|
2992
|
+
testingAccount: testingAccountOption,
|
|
2993
|
+
profile: profileOption,
|
|
1768
2994
|
targetOrigin: targetOriginOption,
|
|
1769
2995
|
telemetryOrigin: telemetryOriginOption,
|
|
1770
2996
|
accountId: accountIdOption,
|
|
@@ -1772,90 +2998,151 @@ export const devCmd = Command.make('dev', {
|
|
|
1772
2998
|
developerId: developerIdOption,
|
|
1773
2999
|
userId: userIdOption,
|
|
1774
3000
|
sessionId: sessionIdOption,
|
|
3001
|
+
session: managedSessionOption,
|
|
3002
|
+
managedSession: internalManagedSessionOption,
|
|
1775
3003
|
ttlSeconds: ttlSecondsOption,
|
|
1776
3004
|
hsxLogPort: hsxLogPortOption,
|
|
1777
3005
|
controlPlaneUrl: controlPlaneUrlOption,
|
|
1778
3006
|
runtimeControlPlaneUrl: runtimeControlPlaneUrlOption,
|
|
1779
3007
|
capability: capabilityOption,
|
|
3008
|
+
component: componentOption,
|
|
1780
3009
|
only: onlyOption,
|
|
3010
|
+
lane: laneOption,
|
|
3011
|
+
level: levelOption,
|
|
3012
|
+
search: searchOption,
|
|
3013
|
+
since: sinceOption,
|
|
3014
|
+
cursor: cursorOption,
|
|
3015
|
+
limit: limitOption,
|
|
3016
|
+
waitSeconds: waitSecondsOption,
|
|
3017
|
+
detach: detachOption,
|
|
3018
|
+
follow: followOption,
|
|
1781
3019
|
pick: pickOption,
|
|
1782
3020
|
noHubspot: noHubspotOption,
|
|
1783
3021
|
noUiExtensions: noUiExtensionsOption,
|
|
1784
3022
|
yes: yesOption,
|
|
1785
3023
|
force: forceOption,
|
|
1786
|
-
}, (opts) =>
|
|
1787
|
-
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
argv
|
|
1798
|
-
|
|
1799
|
-
argv
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
3024
|
+
}, (opts) => {
|
|
3025
|
+
const subcommand = Option.isSome(opts.subcommand) ? opts.subcommand.value : undefined;
|
|
3026
|
+
const managedSubcommand = subcommand !== undefined &&
|
|
3027
|
+
(MANAGED_DEV_SUBCOMMANDS.has(subcommand) ||
|
|
3028
|
+
(subcommand === 'status' && Option.isSome(opts.session)));
|
|
3029
|
+
const reportCommand = managedSubcommand ? `dev ${subcommand}` : 'dev';
|
|
3030
|
+
return runHandler(reportCommand, { json: opts.json }, Effect.gen(function* () {
|
|
3031
|
+
const effectReporter = yield* ReporterService;
|
|
3032
|
+
// root: injected cwd, overridden by --cwd.
|
|
3033
|
+
const injectedCwd = yield* Cwd;
|
|
3034
|
+
const root = Option.getOrElse(opts.cwd, () => injectedCwd);
|
|
3035
|
+
// Reconstruct the legacy argv the quarantined body parses (it reads
|
|
3036
|
+
// its flags off argv via `resolveFlag`/`resolveFlags` and branches on
|
|
3037
|
+
// `argv[1]` for the cleanup/status subcommands).
|
|
3038
|
+
const argv = ['dev'];
|
|
3039
|
+
if (Option.isSome(opts.subcommand))
|
|
3040
|
+
argv.push(opts.subcommand.value);
|
|
3041
|
+
if (Option.isSome(opts.target))
|
|
3042
|
+
argv.push(opts.target.value);
|
|
3043
|
+
if (Option.isSome(opts.input))
|
|
3044
|
+
argv.push('--input', opts.input.value);
|
|
3045
|
+
if (Option.isSome(opts.object))
|
|
3046
|
+
argv.push('--object', opts.object.value);
|
|
3047
|
+
if (Option.isSome(opts.install))
|
|
3048
|
+
argv.push('--install', opts.install.value);
|
|
3049
|
+
if (opts.last)
|
|
3050
|
+
argv.push('--last');
|
|
3051
|
+
if (opts.remote)
|
|
3052
|
+
argv.push('--remote');
|
|
3053
|
+
if (Option.isSome(opts.url))
|
|
3054
|
+
argv.push('--url', opts.url.value);
|
|
3055
|
+
if (opts.json)
|
|
3056
|
+
argv.push('--json');
|
|
3057
|
+
if (Option.isSome(opts.port))
|
|
3058
|
+
argv.push('--port', opts.port.value);
|
|
3059
|
+
if (Option.isSome(opts.portal))
|
|
3060
|
+
argv.push('--portal', opts.portal.value);
|
|
3061
|
+
if (Option.isSome(opts.account))
|
|
3062
|
+
argv.push('--account', opts.account.value);
|
|
3063
|
+
if (Option.isSome(opts.projectAccount)) {
|
|
3064
|
+
argv.push('--project-account', opts.projectAccount.value);
|
|
3065
|
+
}
|
|
3066
|
+
if (Option.isSome(opts.testingAccount)) {
|
|
3067
|
+
argv.push('--testing-account', opts.testingAccount.value);
|
|
3068
|
+
}
|
|
3069
|
+
if (Option.isSome(opts.profile))
|
|
3070
|
+
argv.push('--profile', opts.profile.value);
|
|
3071
|
+
if (Option.isSome(opts.targetOrigin)) {
|
|
3072
|
+
argv.push('--target-origin', opts.targetOrigin.value);
|
|
3073
|
+
}
|
|
3074
|
+
if (Option.isSome(opts.telemetryOrigin)) {
|
|
3075
|
+
argv.push('--telemetry-origin', opts.telemetryOrigin.value);
|
|
3076
|
+
}
|
|
3077
|
+
if (Option.isSome(opts.accountId))
|
|
3078
|
+
argv.push('--account-id', opts.accountId.value);
|
|
3079
|
+
if (Option.isSome(opts.projectId))
|
|
3080
|
+
argv.push('--project-id', opts.projectId.value);
|
|
3081
|
+
if (Option.isSome(opts.developerId)) {
|
|
3082
|
+
argv.push('--developer-id', opts.developerId.value);
|
|
3083
|
+
}
|
|
3084
|
+
if (Option.isSome(opts.userId))
|
|
3085
|
+
argv.push('--user-id', opts.userId.value);
|
|
3086
|
+
if (Option.isSome(opts.sessionId))
|
|
3087
|
+
argv.push('--session-id', opts.sessionId.value);
|
|
3088
|
+
if (Option.isSome(opts.session))
|
|
3089
|
+
argv.push('--session', opts.session.value);
|
|
3090
|
+
if (Option.isSome(opts.managedSession)) {
|
|
3091
|
+
argv.push('--managed-session', opts.managedSession.value);
|
|
3092
|
+
}
|
|
3093
|
+
if (Option.isSome(opts.ttlSeconds))
|
|
3094
|
+
argv.push('--ttl-seconds', opts.ttlSeconds.value);
|
|
3095
|
+
if (Option.isSome(opts.hsxLogPort))
|
|
3096
|
+
argv.push('--hsx-log-port', opts.hsxLogPort.value);
|
|
3097
|
+
if (Option.isSome(opts.controlPlaneUrl)) {
|
|
3098
|
+
argv.push('--control-plane-url', opts.controlPlaneUrl.value);
|
|
3099
|
+
}
|
|
3100
|
+
if (Option.isSome(opts.runtimeControlPlaneUrl)) {
|
|
3101
|
+
argv.push('--runtime-control-plane-url', opts.runtimeControlPlaneUrl.value);
|
|
3102
|
+
}
|
|
3103
|
+
for (const capability of opts.capability)
|
|
3104
|
+
argv.push('--capability', capability);
|
|
3105
|
+
for (const component of opts.component)
|
|
3106
|
+
argv.push('--component', component);
|
|
3107
|
+
for (const only of opts.only)
|
|
3108
|
+
argv.push('--only', only);
|
|
3109
|
+
for (const lane of opts.lane)
|
|
3110
|
+
argv.push('--lane', lane);
|
|
3111
|
+
for (const level of opts.level)
|
|
3112
|
+
argv.push('--level', level);
|
|
3113
|
+
if (Option.isSome(opts.search))
|
|
3114
|
+
argv.push('--search', opts.search.value);
|
|
3115
|
+
if (Option.isSome(opts.since))
|
|
3116
|
+
argv.push('--since', opts.since.value);
|
|
3117
|
+
if (Option.isSome(opts.cursor))
|
|
3118
|
+
argv.push('--cursor', opts.cursor.value);
|
|
3119
|
+
if (Option.isSome(opts.limit))
|
|
3120
|
+
argv.push('--limit', opts.limit.value);
|
|
3121
|
+
if (Option.isSome(opts.waitSeconds)) {
|
|
3122
|
+
argv.push('--wait-seconds', opts.waitSeconds.value);
|
|
3123
|
+
}
|
|
3124
|
+
if (opts.detach)
|
|
3125
|
+
argv.push('--detach');
|
|
3126
|
+
if (opts.follow)
|
|
3127
|
+
argv.push('--follow');
|
|
3128
|
+
if (opts.pick)
|
|
3129
|
+
argv.push('--pick');
|
|
3130
|
+
if (opts.noHubspot)
|
|
3131
|
+
argv.push('--no-hubspot');
|
|
3132
|
+
if (opts.noUiExtensions)
|
|
3133
|
+
argv.push('--no-ui-extensions');
|
|
3134
|
+
if (opts.yes)
|
|
3135
|
+
argv.push('--yes');
|
|
3136
|
+
if (opts.force)
|
|
3137
|
+
argv.push('--force');
|
|
3138
|
+
yield* runQuarantined(reportCommand, opts.json, () => devCommand({
|
|
3139
|
+
argv,
|
|
3140
|
+
root,
|
|
3141
|
+
json: opts.json,
|
|
3142
|
+
reporter: effectReporter.implementation,
|
|
3143
|
+
}));
|
|
3144
|
+
}));
|
|
3145
|
+
});
|
|
1859
3146
|
/**
|
|
1860
3147
|
* `hs-x dev invoke --remote`: dispatch at the DEPLOYED Worker, where install
|
|
1861
3148
|
* tokens live — handlers using ctx.hubspot run against the real portal. URL
|
|
@@ -1872,6 +3159,9 @@ async function devInvokeRemote(args) {
|
|
|
1872
3159
|
}
|
|
1873
3160
|
let url = explicitUrl;
|
|
1874
3161
|
if (!url) {
|
|
3162
|
+
if (!mapping) {
|
|
3163
|
+
return fail('HSX_E_DEV_UNKNOWN_CAPABILITY', `No capability \`${capabilityId}\` in .hs-x/manifest.json.`);
|
|
3164
|
+
}
|
|
1875
3165
|
const { resolveCloudflareCredentials } = await import('../cloudflare-kv.js');
|
|
1876
3166
|
const credentials = resolveCloudflareCredentials(argv);
|
|
1877
3167
|
const { loadCloudflarePointer } = await import('../cloudflare-pointer.js');
|
|
@@ -1900,9 +3190,9 @@ async function devInvokeRemote(args) {
|
|
|
1900
3190
|
: {}),
|
|
1901
3191
|
...(payloadOverrides.install !== undefined ? { install: payloadOverrides.install } : {}),
|
|
1902
3192
|
};
|
|
1903
|
-
//
|
|
1904
|
-
//
|
|
1905
|
-
//
|
|
3193
|
+
// Deployed invokes require a configured secret and valid v3 signature. If
|
|
3194
|
+
// .dev.vars has no secret, the remote bootstrap worker fails closed with 503;
|
|
3195
|
+
// only the separate in-process local adapter opts into unsigned execution.
|
|
1906
3196
|
const devVars = await loadDevVars(root);
|
|
1907
3197
|
const signatureSecret = typeof devVars.HSX_HUBSPOT_CLIENT_SECRET === 'string' &&
|
|
1908
3198
|
devVars.HSX_HUBSPOT_CLIENT_SECRET.length > 0
|