@hs-x/cli 0.2.6 → 0.3.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/LICENSE +202 -0
- package/README.md +25 -8
- package/dist/audit-interactive.d.ts +29 -0
- package/dist/audit-interactive.d.ts.map +1 -0
- package/dist/audit-interactive.js +156 -0
- package/dist/audit-interactive.js.map +1 -0
- package/dist/bin/hs-x.js.map +1 -1
- package/dist/checkpoint-interactive.d.ts +24 -0
- package/dist/checkpoint-interactive.d.ts.map +1 -0
- package/dist/checkpoint-interactive.js +65 -0
- package/dist/checkpoint-interactive.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +266 -39
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/kit.d.ts +1 -1
- package/dist/cli/kit.d.ts.map +1 -1
- package/dist/cli/kit.js +19 -3
- package/dist/cli/kit.js.map +1 -1
- package/dist/cloudflare-auth.d.ts +8 -0
- package/dist/cloudflare-auth.d.ts.map +1 -1
- package/dist/cloudflare-auth.js +12 -2
- package/dist/cloudflare-auth.js.map +1 -1
- package/dist/cloudflare-oauth-store.d.ts +9 -1
- package/dist/cloudflare-oauth-store.d.ts.map +1 -1
- package/dist/cloudflare-oauth-store.js +23 -4
- package/dist/cloudflare-oauth-store.js.map +1 -1
- package/dist/cloudflare-oauth.d.ts +1 -0
- package/dist/cloudflare-oauth.d.ts.map +1 -1
- package/dist/cloudflare-oauth.js +6 -2
- package/dist/cloudflare-oauth.js.map +1 -1
- package/dist/cloudflare-worker-versions.d.ts +184 -0
- package/dist/cloudflare-worker-versions.d.ts.map +1 -0
- package/dist/cloudflare-worker-versions.js +360 -0
- package/dist/cloudflare-worker-versions.js.map +1 -0
- package/dist/commands/account.d.ts +28 -13
- package/dist/commands/account.d.ts.map +1 -1
- package/dist/commands/account.js +219 -69
- package/dist/commands/account.js.map +1 -1
- package/dist/commands/api.d.ts +1 -1
- package/dist/commands/api.d.ts.map +1 -1
- package/dist/commands/api.js +6 -10
- package/dist/commands/api.js.map +1 -1
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +13 -7
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +239 -125
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/control-plane-read.d.ts +72 -11
- package/dist/commands/control-plane-read.d.ts.map +1 -1
- package/dist/commands/control-plane-read.js +1035 -58
- package/dist/commands/control-plane-read.js.map +1 -1
- package/dist/commands/deploy-promote.d.ts +25 -3
- package/dist/commands/deploy-promote.d.ts.map +1 -1
- package/dist/commands/deploy-promote.js +314 -38
- package/dist/commands/deploy-promote.js.map +1 -1
- package/dist/commands/deploy.d.ts +67 -4
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +1333 -255
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +5 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +502 -107
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts +15 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +131 -20
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/flags.d.ts +108 -6
- package/dist/commands/flags.d.ts.map +1 -1
- package/dist/commands/flags.js +996 -75
- package/dist/commands/flags.js.map +1 -1
- package/dist/commands/help-command.d.ts +14 -0
- package/dist/commands/help-command.d.ts.map +1 -1
- package/dist/commands/help-command.js +144 -44
- package/dist/commands/help-command.js.map +1 -1
- package/dist/commands/history.js +14 -10
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/init.d.ts +4 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +69 -12
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/link.d.ts +2 -3
- package/dist/commands/link.d.ts.map +1 -1
- package/dist/commands/link.js +201 -101
- package/dist/commands/link.js.map +1 -1
- package/dist/commands/login.d.ts +2 -2
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +22 -5
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/migrate.d.ts +23 -3
- package/dist/commands/migrate.d.ts.map +1 -1
- package/dist/commands/migrate.js +500 -73
- package/dist/commands/migrate.js.map +1 -1
- package/dist/commands/rollback.d.ts +42 -2
- package/dist/commands/rollback.d.ts.map +1 -1
- package/dist/commands/rollback.js +422 -58
- package/dist/commands/rollback.js.map +1 -1
- package/dist/commands/secrets.d.ts +76 -3
- package/dist/commands/secrets.d.ts.map +1 -1
- package/dist/commands/secrets.js +328 -41
- package/dist/commands/secrets.js.map +1 -1
- package/dist/commands/status.d.ts +13 -2
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +154 -17
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/unlink.d.ts +2 -2
- package/dist/commands/unlink.d.ts.map +1 -1
- package/dist/commands/unlink.js +2 -2
- package/dist/commands/unlink.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +4 -3
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/validate.d.ts +12 -3
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +70 -8
- 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-fetch.d.ts.map +1 -1
- package/dist/control-plane-fetch.js +5 -4
- package/dist/control-plane-fetch.js.map +1 -1
- package/dist/deploy-revision-capture.d.ts +48 -0
- package/dist/deploy-revision-capture.d.ts.map +1 -0
- package/dist/deploy-revision-capture.js +125 -0
- package/dist/deploy-revision-capture.js.map +1 -0
- package/dist/dev/compat-shim.d.ts.map +1 -1
- package/dist/dev/compat-shim.js +10 -35
- package/dist/dev/compat-shim.js.map +1 -1
- package/dist/dev/event-bus.d.ts +5 -2
- package/dist/dev/event-bus.d.ts.map +1 -1
- package/dist/dev/event-bus.js +4 -1
- package/dist/dev/event-bus.js.map +1 -1
- package/dist/dev/hubspot-logs-poller.d.ts +35 -0
- package/dist/dev/hubspot-logs-poller.d.ts.map +1 -0
- package/dist/dev/hubspot-logs-poller.js +94 -0
- package/dist/dev/hubspot-logs-poller.js.map +1 -0
- package/dist/dev/invoke.d.ts +9 -1
- package/dist/dev/invoke.d.ts.map +1 -1
- package/dist/dev/invoke.js +69 -20
- package/dist/dev/invoke.js.map +1 -1
- package/dist/dev/keyboard.d.ts +31 -0
- package/dist/dev/keyboard.d.ts.map +1 -0
- package/dist/dev/keyboard.js +143 -0
- package/dist/dev/keyboard.js.map +1 -0
- package/dist/dev/log-server.d.ts +7 -0
- package/dist/dev/log-server.d.ts.map +1 -1
- package/dist/dev/log-server.js +68 -31
- package/dist/dev/log-server.js.map +1 -1
- package/dist/dev/migrated-card.d.ts +17 -0
- package/dist/dev/migrated-card.d.ts.map +1 -0
- package/dist/dev/migrated-card.js +114 -0
- package/dist/dev/migrated-card.js.map +1 -0
- package/dist/dev/stream-filter.d.ts +40 -0
- package/dist/dev/stream-filter.d.ts.map +1 -0
- package/dist/dev/stream-filter.js +93 -0
- package/dist/dev/stream-filter.js.map +1 -0
- package/dist/dev/stream-renderer.d.ts +34 -0
- package/dist/dev/stream-renderer.d.ts.map +1 -1
- package/dist/dev/stream-renderer.js +222 -8
- package/dist/dev/stream-renderer.js.map +1 -1
- package/dist/errors-registry.d.ts.map +1 -1
- package/dist/errors-registry.js +280 -36
- package/dist/errors-registry.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +8 -0
- package/dist/help.js.map +1 -1
- package/dist/hubspot-app-logs.d.ts +76 -0
- package/dist/hubspot-app-logs.d.ts.map +1 -0
- package/dist/hubspot-app-logs.js +323 -0
- package/dist/hubspot-app-logs.js.map +1 -0
- package/dist/hubspot-auth.d.ts +8 -2
- package/dist/hubspot-auth.d.ts.map +1 -1
- package/dist/hubspot-auth.js +36 -12
- package/dist/hubspot-auth.js.map +1 -1
- package/dist/hubspot-developer-client.js +119 -11
- package/dist/hubspot-developer-client.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/init/templates.d.ts +10 -0
- package/dist/init/templates.d.ts.map +1 -1
- package/dist/init/templates.js +157 -14
- package/dist/init/templates.js.map +1 -1
- package/dist/link-migration-journal.d.ts +131 -0
- package/dist/link-migration-journal.d.ts.map +1 -0
- package/dist/link-migration-journal.js +395 -0
- package/dist/link-migration-journal.js.map +1 -0
- package/dist/link-preflight.d.ts +78 -0
- package/dist/link-preflight.d.ts.map +1 -0
- package/dist/link-preflight.js +231 -0
- package/dist/link-preflight.js.map +1 -0
- package/dist/local-project-picker.d.ts +34 -0
- package/dist/local-project-picker.d.ts.map +1 -0
- package/dist/local-project-picker.js +121 -0
- package/dist/local-project-picker.js.map +1 -0
- package/dist/logs-interactive.d.ts +37 -0
- package/dist/logs-interactive.d.ts.map +1 -0
- package/dist/logs-interactive.js +110 -0
- package/dist/logs-interactive.js.map +1 -0
- package/dist/output-context.d.ts +13 -0
- package/dist/output-context.d.ts.map +1 -0
- package/dist/output-context.js +46 -0
- package/dist/output-context.js.map +1 -0
- package/dist/project-context.d.ts +55 -0
- package/dist/project-context.d.ts.map +1 -0
- package/dist/project-context.js +115 -0
- package/dist/project-context.js.map +1 -0
- package/dist/project-picker.d.ts +65 -0
- package/dist/project-picker.d.ts.map +1 -0
- package/dist/project-picker.js +227 -0
- package/dist/project-picker.js.map +1 -0
- package/dist/prompt.d.ts +8 -0
- package/dist/prompt.d.ts.map +1 -1
- package/dist/prompt.js +81 -13
- package/dist/prompt.js.map +1 -1
- package/dist/reporter/human.d.ts +10 -2
- package/dist/reporter/human.d.ts.map +1 -1
- package/dist/reporter/human.js +82 -14
- package/dist/reporter/human.js.map +1 -1
- package/dist/reporter/index.d.ts.map +1 -1
- package/dist/reporter/index.js +7 -5
- package/dist/reporter/index.js.map +1 -1
- package/dist/reporter/json.d.ts +2 -1
- package/dist/reporter/json.d.ts.map +1 -1
- package/dist/reporter/json.js +10 -7
- package/dist/reporter/json.js.map +1 -1
- package/dist/reporter/style.d.ts +19 -0
- package/dist/reporter/style.d.ts.map +1 -1
- package/dist/reporter/style.js +126 -6
- package/dist/reporter/style.js.map +1 -1
- package/dist/reporter/types.d.ts +34 -4
- package/dist/reporter/types.d.ts.map +1 -1
- package/dist/reporter/types.js.map +1 -1
- package/dist/rollback-traffic.d.ts +85 -0
- package/dist/rollback-traffic.d.ts.map +1 -0
- package/dist/rollback-traffic.js +204 -0
- package/dist/rollback-traffic.js.map +1 -0
- package/dist/services/account-store.d.ts +3 -1
- package/dist/services/account-store.d.ts.map +1 -1
- package/dist/services/account-store.js +2 -1
- package/dist/services/account-store.js.map +1 -1
- package/dist/services/app-paths.d.ts.map +1 -1
- package/dist/services/app-paths.js +3 -1
- package/dist/services/app-paths.js.map +1 -1
- package/dist/services/cloudflare-auth.d.ts +1 -1
- package/dist/services/cloudflare-auth.d.ts.map +1 -1
- package/dist/services/cloudflare-auth.js +2 -2
- package/dist/services/cloudflare-auth.js.map +1 -1
- package/dist/services/cloudflare-kv.d.ts.map +1 -1
- package/dist/services/cloudflare-kv.js.map +1 -1
- package/dist/services/command-history.d.ts.map +1 -1
- package/dist/services/command-history.js.map +1 -1
- package/dist/services/config.d.ts.map +1 -1
- package/dist/services/config.js +1 -1
- package/dist/services/config.js.map +1 -1
- package/dist/services/control-plane.d.ts.map +1 -1
- package/dist/services/control-plane.js +3 -3
- package/dist/services/control-plane.js.map +1 -1
- package/dist/services/env-loader.d.ts.map +1 -1
- package/dist/services/env-loader.js +1 -3
- package/dist/services/env-loader.js.map +1 -1
- package/dist/services/http.d.ts.map +1 -1
- package/dist/services/http.js.map +1 -1
- package/dist/services/hubspot-dev-client.d.ts.map +1 -1
- package/dist/services/hubspot-dev-client.js +30 -0
- package/dist/services/hubspot-dev-client.js.map +1 -1
- package/dist/services/live.d.ts +2 -2
- package/dist/services/live.d.ts.map +1 -1
- package/dist/services/live.js +1 -1
- package/dist/services/live.js.map +1 -1
- package/dist/services/machine-id.d.ts.map +1 -1
- package/dist/services/machine-id.js.map +1 -1
- package/dist/services/reporter.d.ts +8 -5
- package/dist/services/reporter.d.ts.map +1 -1
- package/dist/services/reporter.js +6 -4
- package/dist/services/reporter.js.map +1 -1
- package/dist/services/tenant-state.d.ts.map +1 -1
- package/dist/services/tenant-state.js.map +1 -1
- package/dist/state-store.d.ts +4 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/state-store.js +15 -0
- package/dist/state-store.js.map +1 -1
- package/dist/status-drilldown.d.ts +24 -0
- package/dist/status-drilldown.d.ts.map +1 -0
- package/dist/status-drilldown.js +50 -0
- package/dist/status-drilldown.js.map +1 -0
- package/dist/tenant-state.d.ts.map +1 -1
- package/dist/tenant-state.js +3 -2
- package/dist/tenant-state.js.map +1 -1
- package/dist/token-key-custody.d.ts +91 -0
- package/dist/token-key-custody.d.ts.map +1 -0
- package/dist/token-key-custody.js +375 -0
- package/dist/token-key-custody.js.map +1 -0
- package/package.json +13 -14
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import * as Command from '@effect/cli/Command';
|
|
2
|
+
import * as Options from '@effect/cli/Options';
|
|
2
3
|
import { HttpClientRequest, Schema, schemas } from '@hs-x/types';
|
|
3
4
|
import { Config, Effect, Option } from 'effect';
|
|
4
5
|
import { isLinked } from '../account-store.js';
|
|
6
|
+
import { auditExplorer, filterAuditRows, promptAuditTimeRange, renderAuditDetail, } from '../audit-interactive.js';
|
|
7
|
+
import { checkpointDrilldown, } from '../checkpoint-interactive.js';
|
|
5
8
|
import { CliError, cliError, exitWith } from '../cli-error.js';
|
|
6
9
|
import { accountIdOption, controlPlaneUrlOption, jsonOption, projectIdOption, runHandler, userIdOption, } from '../cli/kit.js';
|
|
7
10
|
import { DEFAULT_CONTROL_PLANE_URL } from '../config.js';
|
|
8
11
|
import { executeCliHttpEffect } from '../effect-http.js';
|
|
12
|
+
import { fetchHubSpotLogsPayload, mergeLogRows, parseHubSpotLogSystemTypes, resolveHubSpotLogsSource, } from '../hubspot-app-logs.js';
|
|
13
|
+
import { promptInteractiveLogsSelection } from '../logs-interactive.js';
|
|
9
14
|
import { readProjectBinding } from '../project-binding.js';
|
|
15
|
+
import { ProjectPickerCancelled, discoverProjects, resolveProjectSelection, } from '../project-picker.js';
|
|
16
|
+
import { isInteractive, promptSelect } from '../prompt.js';
|
|
10
17
|
import { AccountStore } from '../services/account-store.js';
|
|
11
18
|
import { ControlPlaneClient } from '../services/control-plane.js';
|
|
12
19
|
import { Cwd } from '../services/cwd.js';
|
|
@@ -81,11 +88,283 @@ function renderCheckpoint(checkpoint) {
|
|
|
81
88
|
lines.push(`Sources: ${sourceParts.join(', ')}`);
|
|
82
89
|
return `${lines.join('\n')}\n`;
|
|
83
90
|
}
|
|
91
|
+
export function renderCheckpointExemplar(failure) {
|
|
92
|
+
const exemplar = failure.exemplar;
|
|
93
|
+
const lines = [
|
|
94
|
+
`Capability: ${exemplar.capabilityId}`,
|
|
95
|
+
`Worker: ${exemplar.workerName}`,
|
|
96
|
+
`Deploy: ${exemplar.deployId}`,
|
|
97
|
+
`Outcome: ${exemplar.outcome}`,
|
|
98
|
+
`Observed: ${exemplar.observedAt}`,
|
|
99
|
+
`Duration: ${exemplar.durationMs}ms`,
|
|
100
|
+
`Portal: ${exemplar.portalId}`,
|
|
101
|
+
`Fingerprint: ${failure.fingerprint}`,
|
|
102
|
+
`Redacted input:\n${JSON.stringify(exemplar.redactedInput, null, 2)}`,
|
|
103
|
+
];
|
|
104
|
+
if (exemplar.redactedOutput !== undefined) {
|
|
105
|
+
lines.push(`Redacted output:\n${JSON.stringify(exemplar.redactedOutput, null, 2)}`);
|
|
106
|
+
}
|
|
107
|
+
if (exemplar.redactedError !== undefined) {
|
|
108
|
+
lines.push(`Redacted error:\n${JSON.stringify(exemplar.redactedError, null, 2)}`);
|
|
109
|
+
}
|
|
110
|
+
return `${lines.join('\n')}\n`;
|
|
111
|
+
}
|
|
112
|
+
function clampInt(value, min, max) {
|
|
113
|
+
if (!Number.isFinite(value))
|
|
114
|
+
return min;
|
|
115
|
+
return Math.min(Math.max(Math.trunc(value), min), max);
|
|
116
|
+
}
|
|
117
|
+
function readString(row, key) {
|
|
118
|
+
const value = row[key];
|
|
119
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
120
|
+
}
|
|
121
|
+
function readNumber(row, key) {
|
|
122
|
+
const value = row[key];
|
|
123
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
124
|
+
}
|
|
125
|
+
function parseLogFilters(values) {
|
|
126
|
+
const filters = [];
|
|
127
|
+
for (const value of values) {
|
|
128
|
+
for (const rawPart of value.split(',')) {
|
|
129
|
+
const part = rawPart.trim();
|
|
130
|
+
if (!part)
|
|
131
|
+
continue;
|
|
132
|
+
const match = /^([a-zA-Z][a-zA-Z0-9_.-]*)(!=|>=|<=|=|>|<|~)(.+)$/.exec(part);
|
|
133
|
+
if (!match) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
message: `Invalid log filter "${part}". Use key=value, key!=value, key~text, or status>=500.`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const [, key, op, rawFilterValue] = match;
|
|
140
|
+
const filterValue = rawFilterValue?.trim() ?? '';
|
|
141
|
+
if (!key || filterValue.length === 0) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
message: `Invalid log filter "${part}". Filters need a key and value.`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
filters.push({ key, op: op, value: filterValue });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { ok: true, filters };
|
|
151
|
+
}
|
|
152
|
+
function parseSampleRate(value) {
|
|
153
|
+
if (value === undefined || value.trim() === '')
|
|
154
|
+
return { ok: true };
|
|
155
|
+
const rate = Number(value);
|
|
156
|
+
if (!Number.isFinite(rate) || rate <= 0 || rate > 1) {
|
|
157
|
+
return {
|
|
158
|
+
ok: false,
|
|
159
|
+
message: '--sample must be a number greater than 0 and less than or equal to 1.',
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return { ok: true, rate };
|
|
163
|
+
}
|
|
164
|
+
function compareLogFilter(row, filter) {
|
|
165
|
+
const raw = row[filter.key];
|
|
166
|
+
if (raw === undefined || raw === null)
|
|
167
|
+
return false;
|
|
168
|
+
const rowText = String(raw);
|
|
169
|
+
const wantedText = filter.value;
|
|
170
|
+
if (filter.op === '~') {
|
|
171
|
+
return rowText.toLowerCase().includes(wantedText.toLowerCase());
|
|
172
|
+
}
|
|
173
|
+
const rowNumber = typeof raw === 'number' ? raw : Number(rowText);
|
|
174
|
+
const wantedNumber = Number(wantedText);
|
|
175
|
+
const canCompareNumber = Number.isFinite(rowNumber) && Number.isFinite(wantedNumber);
|
|
176
|
+
if (filter.op === '>=' || filter.op === '<=' || filter.op === '>' || filter.op === '<') {
|
|
177
|
+
if (!canCompareNumber)
|
|
178
|
+
return false;
|
|
179
|
+
if (filter.op === '>=')
|
|
180
|
+
return rowNumber >= wantedNumber;
|
|
181
|
+
if (filter.op === '<=')
|
|
182
|
+
return rowNumber <= wantedNumber;
|
|
183
|
+
if (filter.op === '>')
|
|
184
|
+
return rowNumber > wantedNumber;
|
|
185
|
+
return rowNumber < wantedNumber;
|
|
186
|
+
}
|
|
187
|
+
const matches = canCompareNumber
|
|
188
|
+
? rowNumber === wantedNumber
|
|
189
|
+
: rowText.toLowerCase() === wantedText.toLowerCase();
|
|
190
|
+
return filter.op === '=' ? matches : !matches;
|
|
191
|
+
}
|
|
192
|
+
function stableLogRowKey(row) {
|
|
193
|
+
const invocationId = readString(row, 'invocationId');
|
|
194
|
+
if (invocationId)
|
|
195
|
+
return `invocation:${invocationId}`;
|
|
196
|
+
return [
|
|
197
|
+
readString(row, 'timestamp') ?? '',
|
|
198
|
+
readString(row, 'level') ?? '',
|
|
199
|
+
readString(row, 'message') ?? '',
|
|
200
|
+
readString(row, 'event') ?? '',
|
|
201
|
+
].join('|');
|
|
202
|
+
}
|
|
203
|
+
function orderLogRowsAscending(rows) {
|
|
204
|
+
return [...rows].sort((a, b) => (readString(a, 'timestamp') ?? '').localeCompare(readString(b, 'timestamp') ?? ''));
|
|
205
|
+
}
|
|
206
|
+
function hashString(value) {
|
|
207
|
+
let hash = 2166136261;
|
|
208
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
209
|
+
hash ^= value.charCodeAt(index);
|
|
210
|
+
hash = Math.imul(hash, 16777619);
|
|
211
|
+
}
|
|
212
|
+
return hash >>> 0;
|
|
213
|
+
}
|
|
214
|
+
function applyLogFilters(rows, filters, sampleRate) {
|
|
215
|
+
return rows.filter((row) => {
|
|
216
|
+
if (!filters.every((filter) => compareLogFilter(row, filter)))
|
|
217
|
+
return false;
|
|
218
|
+
if (sampleRate === undefined || sampleRate >= 1)
|
|
219
|
+
return true;
|
|
220
|
+
return hashString(stableLogRowKey(row)) / 0xffffffff <= sampleRate;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function asLogsPayload(body) {
|
|
224
|
+
if (!isRecord(body))
|
|
225
|
+
return { rows: [], error: 'invalid_response' };
|
|
226
|
+
const rows = Array.isArray(body.rows)
|
|
227
|
+
? body.rows.filter(isRecord).map((row) => ({ origin: 'workers', ...row }))
|
|
228
|
+
: [];
|
|
229
|
+
return {
|
|
230
|
+
...(typeof body.projectId === 'string' ? { projectId: body.projectId } : {}),
|
|
231
|
+
rows,
|
|
232
|
+
...(typeof body.error === 'string' && body.error.length > 0 ? { error: body.error } : {}),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/** ok/warn/fail for a log row, from level, outcome, and HTTP status. */
|
|
236
|
+
function logRowStatus(row) {
|
|
237
|
+
const status = readNumber(row, 'status');
|
|
238
|
+
const outcome = (readString(row, 'outcome') ?? '').toLowerCase();
|
|
239
|
+
const level = (readString(row, 'level') ?? '').toLowerCase();
|
|
240
|
+
if (level === 'error' ||
|
|
241
|
+
outcome === 'error' ||
|
|
242
|
+
outcome === 'exception' ||
|
|
243
|
+
(status !== undefined && status >= 500)) {
|
|
244
|
+
return 'fail';
|
|
245
|
+
}
|
|
246
|
+
if (level === 'warn' || (status !== undefined && status >= 400))
|
|
247
|
+
return 'warn';
|
|
248
|
+
if (outcome === 'ok' || outcome === 'success' || (status !== undefined && status < 400)) {
|
|
249
|
+
return 'ok';
|
|
250
|
+
}
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
function originTag(row, showOrigin) {
|
|
254
|
+
if (!showOrigin)
|
|
255
|
+
return undefined;
|
|
256
|
+
return readString(row, 'origin') === 'hubspot' ? 'hs' : 'cf';
|
|
257
|
+
}
|
|
258
|
+
/** One railed row per invocation: glyph, cyan route, bright result, dim tail. */
|
|
259
|
+
function invocationReporterRow(row, showOrigin) {
|
|
260
|
+
const source = readString(row, 'source') ?? 'request';
|
|
261
|
+
const target = readString(row, 'target');
|
|
262
|
+
const method = readString(row, 'method');
|
|
263
|
+
const status = readNumber(row, 'status');
|
|
264
|
+
const outcome = readString(row, 'outcome');
|
|
265
|
+
const durationMs = readNumber(row, 'durationMs');
|
|
266
|
+
const invocationId = readString(row, 'invocationId') ?? 'unknown-invocation';
|
|
267
|
+
const rowStatus = logRowStatus(row);
|
|
268
|
+
const value = [method, status === undefined ? undefined : String(status)]
|
|
269
|
+
.filter((part) => part !== undefined)
|
|
270
|
+
.join(' ') ||
|
|
271
|
+
outcome ||
|
|
272
|
+
'invocation';
|
|
273
|
+
const detail = [
|
|
274
|
+
readString(row, 'timestamp') ?? 'unknown-time',
|
|
275
|
+
durationMs === undefined ? undefined : `${durationMs}ms`,
|
|
276
|
+
originTag(row, showOrigin),
|
|
277
|
+
invocationId,
|
|
278
|
+
]
|
|
279
|
+
.filter((part) => part !== undefined)
|
|
280
|
+
.join(' · ');
|
|
281
|
+
return {
|
|
282
|
+
key: target ? `${source}/${target}` : source,
|
|
283
|
+
value,
|
|
284
|
+
detail,
|
|
285
|
+
...(rowStatus ? { status: rowStatus } : {}),
|
|
286
|
+
...(rowStatus === 'fail' || rowStatus === 'warn'
|
|
287
|
+
? { hint: `hs-x logs --trace ${invocationId}` }
|
|
288
|
+
: {}),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/** Raw tail/trace view: level-keyed rows; console output follows as a block. */
|
|
292
|
+
function tailReporterRow(row, showOrigin) {
|
|
293
|
+
const level = readString(row, 'level') ?? 'log';
|
|
294
|
+
const rowStatus = level === 'error' ? 'fail' : level === 'warn' ? 'warn' : undefined;
|
|
295
|
+
const detail = [
|
|
296
|
+
readString(row, 'timestamp') ?? 'unknown-time',
|
|
297
|
+
readString(row, 'event'),
|
|
298
|
+
readString(row, 'outcome'),
|
|
299
|
+
originTag(row, showOrigin),
|
|
300
|
+
]
|
|
301
|
+
.filter((part) => part !== undefined && part.length > 0)
|
|
302
|
+
.join(' · ');
|
|
303
|
+
return {
|
|
304
|
+
key: level,
|
|
305
|
+
value: readString(row, 'message') ?? '',
|
|
306
|
+
detail,
|
|
307
|
+
...(rowStatus ? { status: rowStatus } : {}),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Render a batch of log rows through the railed renderer. Invocations render
|
|
312
|
+
* as one aligned rail; tail/trace rows render individually so each row's
|
|
313
|
+
* captured console output can follow it verbatim.
|
|
314
|
+
*/
|
|
315
|
+
function emitLogRows(reporter, rows, view, showOrigin) {
|
|
316
|
+
return Effect.gen(function* () {
|
|
317
|
+
if (rows.length === 0) {
|
|
318
|
+
yield* reporter.info(view === 'invocations'
|
|
319
|
+
? 'No invocations found in this window.'
|
|
320
|
+
: 'No log rows found in this window.');
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (view === 'invocations') {
|
|
324
|
+
yield* reporter.rows(rows.map((row) => invocationReporterRow(row, showOrigin)));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
for (const row of rows) {
|
|
328
|
+
yield* reporter.rows([tailReporterRow(row, showOrigin)]);
|
|
329
|
+
const consoleOutput = readString(row, 'consoleOutput');
|
|
330
|
+
if (consoleOutput)
|
|
331
|
+
yield* reporter.block(consoleOutput);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
/** Outcome tally for the logs footer; only invocation rows carry outcomes. */
|
|
336
|
+
function logRowCounts(rows) {
|
|
337
|
+
const counts = { ok: 0, warn: 0, fail: 0 };
|
|
338
|
+
for (const row of rows) {
|
|
339
|
+
const status = logRowStatus(row);
|
|
340
|
+
if (status)
|
|
341
|
+
counts[status] += 1;
|
|
342
|
+
}
|
|
343
|
+
return counts;
|
|
344
|
+
}
|
|
84
345
|
export function auditHelpText() {
|
|
85
346
|
return `hs-x audit
|
|
86
347
|
|
|
348
|
+
Browse a bounded, redacted account audit feed. Bare hs-x audit opens an
|
|
349
|
+
interactive account, time-range, filter, and event-detail explorer. Scripts use
|
|
350
|
+
the explicit list form; JSON and non-TTY execution never prompt.
|
|
351
|
+
|
|
87
352
|
Commands:
|
|
88
|
-
hs-x audit
|
|
353
|
+
hs-x audit
|
|
354
|
+
hs-x audit list [--account-id <id>] [--since <24h|7d|ISO>] [--limit <n>]
|
|
355
|
+
|
|
356
|
+
Filters:
|
|
357
|
+
--action <action> Exact audit action
|
|
358
|
+
--resource <type[/id]> Resource type or exact type/id
|
|
359
|
+
--actor <user-id> Actor user id
|
|
360
|
+
--outcome <outcome> success, failure, or in_progress
|
|
361
|
+
--since <time> Relative duration (30m, 24h, 7d) or ISO timestamp
|
|
362
|
+
--limit <n> Maximum rows, clamped to 1–200 (default 50)
|
|
363
|
+
--json Emit the bounded redacted result
|
|
364
|
+
|
|
365
|
+
Advanced:
|
|
366
|
+
--control-plane-url <url> Override control plane (HSX_CONTROL_PLANE_URL)
|
|
367
|
+
--user-id <id> Attribution for control-plane records
|
|
89
368
|
`;
|
|
90
369
|
}
|
|
91
370
|
export function driftHelpText() {
|
|
@@ -100,21 +379,70 @@ from the .hs-x/project.json binding that link/deploy maintain.
|
|
|
100
379
|
`;
|
|
101
380
|
}
|
|
102
381
|
export function checkpointHelpText() {
|
|
103
|
-
return `hs-x
|
|
382
|
+
return `hs-x checkpoint
|
|
104
383
|
|
|
105
384
|
Commands:
|
|
106
|
-
hs-x
|
|
385
|
+
hs-x checkpoint --project <id> [--json]
|
|
107
386
|
|
|
108
387
|
Find project ids with hs-x list. --project is an alias of --project-id.
|
|
109
388
|
Inside a deployed project directory the flag is optional — the id is read
|
|
110
|
-
from the .hs-x/project.json binding that link/deploy maintain.
|
|
389
|
+
from the .hs-x/project.json binding that link/deploy maintain. In an
|
|
390
|
+
interactive terminal, recent failures can drill into matching Workers error
|
|
391
|
+
logs or their redacted representative invocation. Machine output never prompts.
|
|
111
392
|
|
|
112
393
|
Advanced:
|
|
113
394
|
--control-plane-url <url> Override control plane (HSX_CONTROL_PLANE_URL)
|
|
114
395
|
--user-id <id> Attribution for control-plane records
|
|
396
|
+
`;
|
|
397
|
+
}
|
|
398
|
+
export function logsHelpText() {
|
|
399
|
+
return `hs-x logs
|
|
400
|
+
|
|
401
|
+
One timeline for everything your app does: Workers Logs rows from your
|
|
402
|
+
deployed runtime ([cf]) merged with HubSpot's app logs ([hs] — card and
|
|
403
|
+
extension renders, production frontend logger output, webhook deliveries,
|
|
404
|
+
API calls, OAuth authorizations). Each side degrades independently: Workers
|
|
405
|
+
Logs need a linked HS-X account, HubSpot logs need only a deployed app and
|
|
406
|
+
HubSpot CLI credentials.
|
|
407
|
+
|
|
408
|
+
Run bare hs-x logs in a terminal for a guided project, source, log-type,
|
|
409
|
+
and time-range picker. A project bound in the current directory is selected
|
|
410
|
+
by default. Flags skip the picker for scripts and repeatable workflows.
|
|
115
411
|
|
|
116
|
-
|
|
117
|
-
hs-x
|
|
412
|
+
Commands:
|
|
413
|
+
hs-x logs --project <id> [--account-id <id>] [--json]
|
|
414
|
+
hs-x logs --project <id> --trace <invocation-id>
|
|
415
|
+
hs-x logs --project <id> --follow --filter source=action,status>=500
|
|
416
|
+
hs-x logs --source hubspot --filter outcome=error
|
|
417
|
+
|
|
418
|
+
Find project ids with hs-x list. --project is an alias of --project-id.
|
|
419
|
+
Inside a deployed project directory the project flag is optional — the id is
|
|
420
|
+
read from the .hs-x/project.json binding that link/deploy maintain.
|
|
421
|
+
|
|
422
|
+
Options:
|
|
423
|
+
--account-id <id> HS-X account (default: the active account)
|
|
424
|
+
--source <name> all (default), workers, or hubspot
|
|
425
|
+
--type <name> HubSpot log type; repeat or comma-separate values
|
|
426
|
+
--errors-only Show errors only (server-side for HubSpot)
|
|
427
|
+
--limit <count> Rows to read, clamped to 1-200 (default: 50)
|
|
428
|
+
--since-minutes <count> Lookback window, clamped to 1-10080 (default: 15)
|
|
429
|
+
--trace <id> Read logger lines for one invocation, or HubSpot
|
|
430
|
+
rows matching a trace id (with console output)
|
|
431
|
+
--invocation <id> Alias of --trace
|
|
432
|
+
--raw Read raw tail events instead of invocation rows
|
|
433
|
+
--filter <expr> Filter rows; supports key=value, key!=value, key~text, status>=500
|
|
434
|
+
(rows carry origin=workers|hubspot and, for HubSpot
|
|
435
|
+
rows, systemType/errorType/portalId)
|
|
436
|
+
--sample <rate> Deterministically sample rows, 0 < rate <= 1
|
|
437
|
+
--follow Poll and append new rows until interrupted
|
|
438
|
+
--tail Alias of --follow
|
|
439
|
+
--interval-seconds <n> Follow polling interval, clamped to 1-60 (default: 3)
|
|
440
|
+
|
|
441
|
+
Advanced:
|
|
442
|
+
--app-id <id> HubSpot app id (default: the .hs-x/hubspot.json
|
|
443
|
+
binding recorded by deploy)
|
|
444
|
+
--control-plane-url <url> Override control plane (HSX_CONTROL_PLANE_URL)
|
|
445
|
+
--user-id <id> Attribution for control-plane records
|
|
118
446
|
`;
|
|
119
447
|
}
|
|
120
448
|
export function listHelpText() {
|
|
@@ -218,10 +546,17 @@ function resolveFromOptionOrEnv(option, envKey) {
|
|
|
218
546
|
return fromEnv.length > 0 ? fromEnv : undefined;
|
|
219
547
|
});
|
|
220
548
|
}
|
|
221
|
-
/**
|
|
549
|
+
/**
|
|
550
|
+
* Authenticated GET via the `ControlPlaneClient`. The hosted control plane
|
|
551
|
+
* requires a real login session; only an explicitly overridden/local control
|
|
552
|
+
* plane may use the x-hsx-user-id development fallback.
|
|
553
|
+
*/
|
|
222
554
|
function cpGet(controlPlaneUrl, path, userId) {
|
|
223
555
|
return Effect.gen(function* () {
|
|
224
556
|
const client = yield* ControlPlaneClient;
|
|
557
|
+
if (new URL(controlPlaneUrl).origin === new URL(DEFAULT_CONTROL_PLANE_URL).origin) {
|
|
558
|
+
return yield* client.fetch({ controlPlaneUrl, path });
|
|
559
|
+
}
|
|
225
560
|
const headers = yield* client.authHeaders(userId);
|
|
226
561
|
const url = new URL(path, controlPlaneUrl);
|
|
227
562
|
const request = HttpClientRequest.get(url).pipe(HttpClientRequest.setHeaders(headers));
|
|
@@ -253,27 +588,183 @@ function resolveProjectId(opts) {
|
|
|
253
588
|
return binding?.projectId;
|
|
254
589
|
});
|
|
255
590
|
}
|
|
591
|
+
/**
|
|
592
|
+
* Resolve an operations project without changing machine behavior. Explicit
|
|
593
|
+
* flag/env ids stay authoritative and never trigger catalog discovery. A local
|
|
594
|
+
* binding remains authoritative outside a TTY, but in a human TTY it is only
|
|
595
|
+
* the default hint for the account-scoped project picker.
|
|
596
|
+
*/
|
|
597
|
+
export function resolveOperationProjectForRead(opts, input) {
|
|
598
|
+
return Effect.gen(function* () {
|
|
599
|
+
const authoritative = yield* resolveFromOptionOrEnv(opts.projectId, 'HSX_PROJECT_ID');
|
|
600
|
+
if (authoritative)
|
|
601
|
+
return { projectId: authoritative };
|
|
602
|
+
const cwd = yield* Cwd;
|
|
603
|
+
const binding = yield* Effect.promise(() => readProjectBinding(cwd));
|
|
604
|
+
if (!isInteractive()) {
|
|
605
|
+
return binding?.projectId ? { projectId: binding.projectId } : undefined;
|
|
606
|
+
}
|
|
607
|
+
const accountId = yield* resolveAccountId(opts, binding?.accountId);
|
|
608
|
+
if (!accountId) {
|
|
609
|
+
return binding?.projectId ? { projectId: binding.projectId } : undefined;
|
|
610
|
+
}
|
|
611
|
+
const controlPlaneUrl = yield* resolveControlPlaneUrl(opts.controlPlaneUrl);
|
|
612
|
+
const userId = yield* resolveUserId(opts);
|
|
613
|
+
const client = yield* ControlPlaneClient;
|
|
614
|
+
const controlPlane = {
|
|
615
|
+
request: (request) => Effect.runPromise(cpGet(controlPlaneUrl, request.path, userId).pipe(Effect.provideService(ControlPlaneClient, client))),
|
|
616
|
+
};
|
|
617
|
+
return yield* Effect.tryPromise({
|
|
618
|
+
try: async () => {
|
|
619
|
+
const projects = await discoverProjects({ accountId, controlPlane });
|
|
620
|
+
if (projects.length === 0)
|
|
621
|
+
throw new Error(input.emptyMessage);
|
|
622
|
+
const project = await resolveProjectSelection({
|
|
623
|
+
accountId,
|
|
624
|
+
projects,
|
|
625
|
+
...(binding?.projectId ? { localProjectId: binding.projectId } : {}),
|
|
626
|
+
allowPrompt: true,
|
|
627
|
+
prompts: { select: promptSelect },
|
|
628
|
+
message: input.message,
|
|
629
|
+
});
|
|
630
|
+
return { projectId: project.id, accountId, project, projects };
|
|
631
|
+
},
|
|
632
|
+
catch: (cause) => cause,
|
|
633
|
+
}).pipe(Effect.catchAll((cause) => cause instanceof ProjectPickerCancelled
|
|
634
|
+
? Effect.fail(exitWith(130))
|
|
635
|
+
: failBespoke(opts.json, input.command, cause instanceof Error ? cause.message : String(cause)).pipe(Effect.as(undefined))));
|
|
636
|
+
});
|
|
637
|
+
}
|
|
256
638
|
const PROJECT_ID_HINT = 'pass --project <id> (find ids with `hs-x list`) or run inside a deployed project directory';
|
|
257
639
|
// ─── audit ──────────────────────────────────────────────────────────────────
|
|
258
|
-
|
|
640
|
+
const auditOptions = {
|
|
641
|
+
...cpReadOptions,
|
|
642
|
+
limit: Options.integer('limit').pipe(Options.withDefault(50)),
|
|
643
|
+
since: Options.text('since').pipe(Options.optional),
|
|
644
|
+
action: Options.text('action').pipe(Options.optional),
|
|
645
|
+
resource: Options.text('resource').pipe(Options.optional),
|
|
646
|
+
actor: Options.text('actor').pipe(Options.optional),
|
|
647
|
+
outcome: Options.text('outcome').pipe(Options.optional),
|
|
648
|
+
};
|
|
649
|
+
export const auditCmd = Command.make('audit', auditOptions, (opts) => runHandler('audit list', { json: opts.json }, auditBody(opts, { explorer: true })));
|
|
259
650
|
// Same handler exposed as the `list` subcommand, so the dispatch can build the
|
|
260
651
|
// legacy `audit list` shape: `Command.make('audit').withSubcommands([auditListCmd])`.
|
|
261
|
-
export const auditListCmd = Command.make('list',
|
|
262
|
-
function
|
|
652
|
+
export const auditListCmd = Command.make('list', auditOptions, (opts) => runHandler('audit list', { json: opts.json }, auditBody(opts, { explorer: false })));
|
|
653
|
+
export function parseAuditSince(value, now = Date.now()) {
|
|
654
|
+
if (!value)
|
|
655
|
+
return { ok: true };
|
|
656
|
+
const relative = /^(\d+)(m|h|d)$/i.exec(value.trim());
|
|
657
|
+
if (relative) {
|
|
658
|
+
const amount = Number(relative[1]);
|
|
659
|
+
if (!Number.isSafeInteger(amount) || amount <= 0) {
|
|
660
|
+
return {
|
|
661
|
+
ok: false,
|
|
662
|
+
message: `Invalid --since "${value}". Use values like 30m, 24h, 7d, or an ISO timestamp.`,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
const unit = relative[2]?.toLowerCase();
|
|
666
|
+
const multiplier = unit === 'd' ? 86_400_000 : unit === 'h' ? 3_600_000 : 60_000;
|
|
667
|
+
return { ok: true, since: new Date(now - amount * multiplier).toISOString() };
|
|
668
|
+
}
|
|
669
|
+
const timestamp = Date.parse(value);
|
|
670
|
+
if (!Number.isFinite(timestamp)) {
|
|
671
|
+
return {
|
|
672
|
+
ok: false,
|
|
673
|
+
message: `Invalid --since "${value}". Use values like 30m, 24h, 7d, or an ISO timestamp.`,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
return { ok: true, since: new Date(timestamp).toISOString() };
|
|
677
|
+
}
|
|
678
|
+
function auditBody(opts, context) {
|
|
263
679
|
return Effect.gen(function* () {
|
|
264
|
-
|
|
680
|
+
// Fall back to the stored default account (parity with `hs-x list`) so a
|
|
681
|
+
// logged-in user doesn't have to re-pass --account-id / HSX_ACCOUNT_ID.
|
|
682
|
+
let accountId = yield* resolveFromOptionOrEnv(opts.accountId, 'HSX_ACCOUNT_ID');
|
|
683
|
+
const accounts = yield* AccountStore;
|
|
684
|
+
const store = yield* accounts.load;
|
|
685
|
+
if (!accountId && context.explorer && !opts.json && isInteractive()) {
|
|
686
|
+
const candidates = Object.values(store.accounts).sort((left, right) => (left.displayName ?? left.id).localeCompare(right.displayName ?? right.id));
|
|
687
|
+
if (candidates.length === 1)
|
|
688
|
+
accountId = candidates[0]?.id;
|
|
689
|
+
else if (candidates.length > 1) {
|
|
690
|
+
accountId = yield* Effect.promise(() => promptSelect({
|
|
691
|
+
message: 'Which account audit should be inspected?',
|
|
692
|
+
options: candidates.map((account) => ({
|
|
693
|
+
value: account.id,
|
|
694
|
+
label: account.displayName ?? account.id,
|
|
695
|
+
description: account.id,
|
|
696
|
+
})),
|
|
697
|
+
...(store.defaultAccountId ? { default: store.defaultAccountId } : {}),
|
|
698
|
+
}));
|
|
699
|
+
if (!accountId)
|
|
700
|
+
return yield* Effect.fail(exitWith(130));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
accountId ??= store.defaultAccountId;
|
|
265
704
|
if (!accountId) {
|
|
266
|
-
return yield* failBespoke(opts.json, 'audit list', 'Audit list requires --account-id
|
|
705
|
+
return yield* failBespoke(opts.json, 'audit list', 'Audit list requires an HS-X account: run `hs-x login`, or pass --account-id / HSX_ACCOUNT_ID.');
|
|
706
|
+
}
|
|
707
|
+
const parsedSince = parseAuditSince(Option.getOrUndefined(opts.since));
|
|
708
|
+
if (!parsedSince.ok)
|
|
709
|
+
return yield* failBespoke(opts.json, 'audit list', parsedSince.message);
|
|
710
|
+
let since = parsedSince.since;
|
|
711
|
+
if (context.explorer && !opts.json && isInteractive() && Option.isNone(opts.since)) {
|
|
712
|
+
const range = yield* Effect.promise(() => promptAuditTimeRange());
|
|
713
|
+
if (range === undefined)
|
|
714
|
+
return yield* Effect.fail(exitWith(130));
|
|
715
|
+
since = range === 'all' ? undefined : new Date(Date.now() - range * 60_000).toISOString();
|
|
716
|
+
}
|
|
717
|
+
const limit = clampInt(opts.limit, 1, 200);
|
|
718
|
+
const action = Option.getOrUndefined(opts.action);
|
|
719
|
+
const resource = Option.getOrUndefined(opts.resource);
|
|
720
|
+
const actor = Option.getOrUndefined(opts.actor);
|
|
721
|
+
const outcome = Option.getOrUndefined(opts.outcome);
|
|
722
|
+
if (outcome && !['success', 'failure', 'in_progress'].includes(outcome)) {
|
|
723
|
+
return yield* failBespoke(opts.json, 'audit list', `Invalid --outcome "${outcome}". Use success, failure, or in_progress.`);
|
|
267
724
|
}
|
|
725
|
+
const filters = {
|
|
726
|
+
limit,
|
|
727
|
+
...(since ? { since } : {}),
|
|
728
|
+
...(action ? { action } : {}),
|
|
729
|
+
...(resource ? { resource } : {}),
|
|
730
|
+
...(actor ? { actor } : {}),
|
|
731
|
+
...(outcome ? { outcome } : {}),
|
|
732
|
+
};
|
|
733
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
734
|
+
if (filters.since)
|
|
735
|
+
params.set('since', filters.since);
|
|
736
|
+
if (filters.action)
|
|
737
|
+
params.set('action', filters.action);
|
|
738
|
+
if (filters.resource)
|
|
739
|
+
params.set('resource', filters.resource);
|
|
740
|
+
if (filters.actor)
|
|
741
|
+
params.set('actor', filters.actor);
|
|
742
|
+
if (filters.outcome)
|
|
743
|
+
params.set('outcome', filters.outcome);
|
|
268
744
|
const controlPlaneUrl = yield* resolveControlPlaneUrl(opts.controlPlaneUrl);
|
|
269
745
|
const userId = yield* resolveUserId(opts);
|
|
270
|
-
const response = yield* cpGet(controlPlaneUrl, `/v1/accounts/${encodeURIComponent(accountId)}/audit`, userId);
|
|
746
|
+
const response = yield* cpGet(controlPlaneUrl, `/v1/accounts/${encodeURIComponent(accountId)}/audit?${params.toString()}`, userId);
|
|
271
747
|
const body = yield* Effect.promise(() => response.json());
|
|
272
748
|
if (!response.ok) {
|
|
273
749
|
return yield* failBespoke(opts.json, 'audit list', responseMessage(body, response.status, 'Control-plane audit list failed'));
|
|
274
750
|
}
|
|
275
|
-
const
|
|
276
|
-
|
|
751
|
+
const decodedRows = Schema.decodeUnknownSync(Schema.Array(schemas.AuditRow))(body);
|
|
752
|
+
// Apply the same constraints client-side for compatibility with older
|
|
753
|
+
// control planes that ignore the new query parameters.
|
|
754
|
+
const rows = filterAuditRows(decodedRows, filters);
|
|
755
|
+
const result = {
|
|
756
|
+
ok: true,
|
|
757
|
+
command: 'audit list',
|
|
758
|
+
mode: 'control-plane-audit',
|
|
759
|
+
accountId,
|
|
760
|
+
limit,
|
|
761
|
+
...(filters.since ? { since: filters.since } : {}),
|
|
762
|
+
...(filters.action ? { action: filters.action } : {}),
|
|
763
|
+
...(filters.resource ? { resource: filters.resource } : {}),
|
|
764
|
+
...(filters.actor ? { actor: filters.actor } : {}),
|
|
765
|
+
...(filters.outcome ? { outcome: filters.outcome } : {}),
|
|
766
|
+
rows,
|
|
767
|
+
};
|
|
277
768
|
const reporter = yield* Reporter;
|
|
278
769
|
if (opts.json) {
|
|
279
770
|
yield* reporter.raw(`${JSON.stringify(result, null, 2)}\n`);
|
|
@@ -282,6 +773,15 @@ function auditBody(opts) {
|
|
|
282
773
|
yield* reporter.header(accountId);
|
|
283
774
|
yield* reporter.block(renderAuditRows(rows));
|
|
284
775
|
yield* reporter.done(`${rows.length} row${rows.length === 1 ? '' : 's'}`);
|
|
776
|
+
if (context.explorer) {
|
|
777
|
+
yield* auditExplorer(rows, {
|
|
778
|
+
detail: (row) => Effect.gen(function* () {
|
|
779
|
+
yield* reporter.header(`${row.action} audit event`);
|
|
780
|
+
yield* reporter.block(renderAuditDetail(row));
|
|
781
|
+
yield* reporter.done('Audit event');
|
|
782
|
+
}),
|
|
783
|
+
});
|
|
784
|
+
}
|
|
285
785
|
});
|
|
286
786
|
}
|
|
287
787
|
// ─── drift ────────────────────────────────────────────────────────────────────
|
|
@@ -298,7 +798,12 @@ function driftBody(opts) {
|
|
|
298
798
|
hint: UNLINKED_DRIFT_HINT,
|
|
299
799
|
});
|
|
300
800
|
}
|
|
301
|
-
const
|
|
801
|
+
const selection = yield* resolveOperationProjectForRead(opts, {
|
|
802
|
+
command: 'drift',
|
|
803
|
+
message: 'Which project do you want to inspect for drift?',
|
|
804
|
+
emptyMessage: 'No projects yet. Run `hs-x deploy` from a project to create one.',
|
|
805
|
+
});
|
|
806
|
+
const projectId = selection?.projectId;
|
|
302
807
|
if (!projectId) {
|
|
303
808
|
return yield* failBespoke(opts.json, 'drift', `Drift status needs a project: ${PROJECT_ID_HINT}.`);
|
|
304
809
|
}
|
|
@@ -344,17 +849,13 @@ function unlinkedRequiresLinkEff(input) {
|
|
|
344
849
|
}
|
|
345
850
|
// ─── list (projects) ──────────────────────────────────────────────────────────
|
|
346
851
|
export const listCmd = Command.make('list', cpReadOptions, (opts) => runHandler('list', { json: opts.json }, listProjectsBody(opts)));
|
|
347
|
-
function
|
|
348
|
-
if (projects.length === 0) {
|
|
349
|
-
return 'No projects yet. Run `hs-x deploy` from a project to create one.\n';
|
|
350
|
-
}
|
|
351
|
-
const widestId = Math.max(...projects.map((p) => p.id.length));
|
|
852
|
+
function projectRows(projects) {
|
|
352
853
|
const widestName = Math.max(...projects.map((p) => p.displayName.length));
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
854
|
+
return projects.map((p) => ({
|
|
855
|
+
key: p.id,
|
|
856
|
+
value: p.displayName.padEnd(widestName),
|
|
857
|
+
detail: p.lastDeployAt ? `last deploy ${p.lastDeployAt}` : 'no recorded deploys',
|
|
858
|
+
}));
|
|
358
859
|
}
|
|
359
860
|
function listProjectsBody(opts) {
|
|
360
861
|
return Effect.gen(function* () {
|
|
@@ -375,14 +876,25 @@ function listProjectsBody(opts) {
|
|
|
375
876
|
return yield* failBespoke(opts.json, 'list', responseMessage(body, response.status, 'Control-plane project list failed'));
|
|
376
877
|
}
|
|
377
878
|
const { projects } = Schema.decodeUnknownSync(schemas.ListProjectsResponse)(body);
|
|
378
|
-
const result = {
|
|
879
|
+
const result = {
|
|
880
|
+
ok: true,
|
|
881
|
+
command: 'list',
|
|
882
|
+
mode: 'control-plane-projects',
|
|
883
|
+
accountId,
|
|
884
|
+
projects,
|
|
885
|
+
};
|
|
379
886
|
const reporter = yield* Reporter;
|
|
380
887
|
if (opts.json) {
|
|
381
888
|
yield* reporter.raw(`${JSON.stringify(result, null, 2)}\n`);
|
|
382
889
|
return;
|
|
383
890
|
}
|
|
384
891
|
yield* reporter.header(accountId);
|
|
385
|
-
|
|
892
|
+
if (projects.length === 0) {
|
|
893
|
+
yield* reporter.info('No projects yet. Run `hs-x deploy` from a project to create one.');
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
yield* reporter.rows(projectRows(projects));
|
|
897
|
+
}
|
|
386
898
|
if (projects.length > 0) {
|
|
387
899
|
yield* reporter.info('Use an id with project-scoped commands: `hs-x logs --project <id>`.');
|
|
388
900
|
}
|
|
@@ -400,7 +912,7 @@ function listProjectsBody(opts) {
|
|
|
400
912
|
* just means the CP has nothing for it — a project with no drift snapshot is
|
|
401
913
|
* healthy, not broken.
|
|
402
914
|
*/
|
|
403
|
-
export function fetchRemoteProjectStatus(opts, projectId) {
|
|
915
|
+
export function fetchRemoteProjectStatus(opts, projectId, prefetchedProjects) {
|
|
404
916
|
return Effect.gen(function* () {
|
|
405
917
|
const controlPlaneUrl = yield* resolveControlPlaneUrl(opts.controlPlaneUrl);
|
|
406
918
|
const userId = yield* resolveUserId(opts);
|
|
@@ -413,34 +925,30 @@ export function fetchRemoteProjectStatus(opts, projectId) {
|
|
|
413
925
|
return decoded._tag === 'Right' ? decoded.right : undefined;
|
|
414
926
|
});
|
|
415
927
|
const encodedProject = encodeURIComponent(projectId);
|
|
416
|
-
const [routing, drift, checkpoint] = yield* Effect.all([
|
|
928
|
+
const [routing, drift, checkpoint, revisions] = yield* Effect.all([
|
|
417
929
|
fetchSection(`/v1/projects/${encodedProject}/routing`, schemas.ProjectRoutingRead),
|
|
418
930
|
fetchSection(`/v1/projects/${encodedProject}/drift`, schemas.DriftRead),
|
|
419
931
|
fetchSection(`/v1/projects/${encodedProject}/checkpoint`, schemas.CheckpointPanelRead),
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
const store = yield* accounts.load;
|
|
425
|
-
accountId = store.defaultAccountId;
|
|
426
|
-
}
|
|
427
|
-
const projectList = accountId
|
|
428
|
-
? yield* fetchSection(`/v1/accounts/${encodeURIComponent(accountId)}/projects`, schemas.ListProjectsResponse)
|
|
429
|
-
: undefined;
|
|
430
|
-
const project = projectList?.projects.find((candidate) => candidate.id === projectId);
|
|
932
|
+
fetchSection(`/v1/projects/${encodedProject}/revisions`, schemas.ProjectRevisionRead),
|
|
933
|
+
], { concurrency: 4 });
|
|
934
|
+
const project = prefetchedProjects?.find((candidate) => candidate.id === projectId);
|
|
935
|
+
const catalogAccountId = prefetchedProjects?.[0]?.accountId;
|
|
431
936
|
return {
|
|
432
937
|
...(project ? { project } : {}),
|
|
433
938
|
...(routing ? { routing } : {}),
|
|
434
939
|
...(drift ? { drift } : {}),
|
|
435
940
|
...(checkpoint ? { checkpoint } : {}),
|
|
436
|
-
...(
|
|
941
|
+
...(revisions ? { revisions } : {}),
|
|
942
|
+
...(prefetchedProjects && !project && catalogAccountId
|
|
943
|
+
? { missingOnAccountId: catalogAccountId }
|
|
944
|
+
: {}),
|
|
437
945
|
};
|
|
438
946
|
});
|
|
439
947
|
}
|
|
440
948
|
/** `hs-x status --project <id>` — the remote project view as a command body. */
|
|
441
|
-
export function remoteStatusBody(opts, projectId) {
|
|
949
|
+
export function remoteStatusBody(opts, projectId, prefetchedProjects) {
|
|
442
950
|
return Effect.gen(function* () {
|
|
443
|
-
const remote = yield* fetchRemoteProjectStatus(opts, projectId);
|
|
951
|
+
const remote = yield* fetchRemoteProjectStatus(opts, projectId, prefetchedProjects);
|
|
444
952
|
if (remote.missingOnAccountId && !remote.routing && !remote.drift && !remote.checkpoint) {
|
|
445
953
|
return yield* failBespoke(opts.json, 'status', `No project ${projectId} on account ${remote.missingOnAccountId}. Find ids with \`hs-x list\`.`);
|
|
446
954
|
}
|
|
@@ -465,9 +973,22 @@ export function remoteStatusBody(opts, projectId) {
|
|
|
465
973
|
});
|
|
466
974
|
}
|
|
467
975
|
export function renderRemoteStatus(input) {
|
|
976
|
+
// Recorded revisions are the authoritative deploy history — a project can have
|
|
977
|
+
// many recorded deploys with nothing promoted, so `lastDeployAt` (from the
|
|
978
|
+
// project list, populated on promotion) is not a reliable "last deploy".
|
|
979
|
+
const recorded = input.revisions?.revisions ?? [];
|
|
980
|
+
const lastRecorded = recorded.length > 0 ? recorded[recorded.length - 1] : undefined;
|
|
981
|
+
const lastDeploy = input.project?.lastDeployAt ??
|
|
982
|
+
(lastRecorded ? `${lastRecorded.recordedAt} (${lastRecorded.deployId})` : undefined) ??
|
|
983
|
+
'none recorded';
|
|
468
984
|
const lines = [
|
|
469
985
|
`Project: ${input.projectId}${input.project ? ` (${input.project.displayName})` : ''}`,
|
|
470
|
-
`Last deploy: ${
|
|
986
|
+
`Last deploy: ${lastDeploy}`,
|
|
987
|
+
...(recorded.length > 0
|
|
988
|
+
? [
|
|
989
|
+
`Deploys recorded: ${recorded.length} (active ${input.revisions?.activeDeployId ?? 'none'})`,
|
|
990
|
+
]
|
|
991
|
+
: []),
|
|
471
992
|
];
|
|
472
993
|
if (input.routing && input.routing.routes.length > 0) {
|
|
473
994
|
const widest = Math.max(...input.routing.routes.map((route) => route.environment.length));
|
|
@@ -491,30 +1012,30 @@ export function renderRemoteStatus(input) {
|
|
|
491
1012
|
}
|
|
492
1013
|
return `${lines.join('\n')}\n`;
|
|
493
1014
|
}
|
|
494
|
-
// ─── checkpoint
|
|
495
|
-
|
|
496
|
-
function
|
|
497
|
-
return Command.make(name, cpReadOptions, (opts) => runHandler(name, { json: opts.json }, checkpointBody(name, opts)));
|
|
498
|
-
}
|
|
499
|
-
export const checkpointCmd = makeCheckpointCmd('checkpoint');
|
|
500
|
-
export const logsCmd = makeCheckpointCmd('logs');
|
|
501
|
-
function checkpointBody(commandName, opts) {
|
|
1015
|
+
// ─── checkpoint ───────────────────────────────────────────────────────────────
|
|
1016
|
+
export const checkpointCmd = Command.make('checkpoint', cpReadOptions, (opts) => runHandler('checkpoint', { json: opts.json }, checkpointBody(opts)));
|
|
1017
|
+
function checkpointBody(opts) {
|
|
502
1018
|
return Effect.gen(function* () {
|
|
503
|
-
const
|
|
1019
|
+
const selection = yield* resolveOperationProjectForRead(opts, {
|
|
1020
|
+
command: 'checkpoint',
|
|
1021
|
+
message: 'Which project do you want to inspect?',
|
|
1022
|
+
emptyMessage: 'No projects yet. Run `hs-x deploy` from a project to create one.',
|
|
1023
|
+
});
|
|
1024
|
+
const projectId = selection?.projectId;
|
|
504
1025
|
if (!projectId) {
|
|
505
|
-
return yield* failBespoke(opts.json,
|
|
1026
|
+
return yield* failBespoke(opts.json, 'checkpoint', `Checkpoint status needs a project: ${PROJECT_ID_HINT}.`);
|
|
506
1027
|
}
|
|
507
1028
|
const controlPlaneUrl = yield* resolveControlPlaneUrl(opts.controlPlaneUrl);
|
|
508
1029
|
const userId = yield* resolveUserId(opts);
|
|
509
1030
|
const response = yield* cpGet(controlPlaneUrl, `/v1/projects/${encodeURIComponent(projectId)}/checkpoint`, userId);
|
|
510
1031
|
const body = yield* Effect.promise(() => response.json());
|
|
511
1032
|
if (!response.ok) {
|
|
512
|
-
return yield* failBespoke(opts.json,
|
|
1033
|
+
return yield* failBespoke(opts.json, 'checkpoint', responseMessage(body, response.status, 'Control-plane checkpoint status failed'));
|
|
513
1034
|
}
|
|
514
1035
|
const checkpoint = Schema.decodeUnknownSync(schemas.CheckpointPanelRead)(body);
|
|
515
1036
|
const result = {
|
|
516
1037
|
ok: true,
|
|
517
|
-
command:
|
|
1038
|
+
command: 'checkpoint',
|
|
518
1039
|
mode: 'control-plane-checkpoint',
|
|
519
1040
|
projectId,
|
|
520
1041
|
checkpoint,
|
|
@@ -527,8 +1048,459 @@ function checkpointBody(commandName, opts) {
|
|
|
527
1048
|
yield* reporter.header(projectId);
|
|
528
1049
|
yield* reporter.block(renderCheckpoint(checkpoint));
|
|
529
1050
|
yield* reporter.done();
|
|
1051
|
+
yield* checkpointDrilldown(checkpoint, {
|
|
1052
|
+
actions: {
|
|
1053
|
+
logs: (failure) => logsBodyForCheckpoint(opts, selection ?? { projectId }, checkpoint, failure),
|
|
1054
|
+
exemplar: (failure) => Effect.gen(function* () {
|
|
1055
|
+
yield* reporter.header(`${failure.errorClass} exemplar`);
|
|
1056
|
+
yield* reporter.block(renderCheckpointExemplar(failure));
|
|
1057
|
+
yield* reporter.done('Representative invocation');
|
|
1058
|
+
}),
|
|
1059
|
+
},
|
|
1060
|
+
});
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
// ─── logs ─────────────────────────────────────────────────────────────────────
|
|
1064
|
+
const logsOptions = {
|
|
1065
|
+
...cpReadOptions,
|
|
1066
|
+
limit: Options.integer('limit').pipe(Options.withDefault(50)),
|
|
1067
|
+
sinceMinutes: Options.integer('since-minutes').pipe(Options.withDefault(15)),
|
|
1068
|
+
trace: Options.text('trace').pipe(Options.withAlias('invocation'), Options.optional),
|
|
1069
|
+
raw: Options.boolean('raw').pipe(Options.withDefault(false)),
|
|
1070
|
+
filter: Options.text('filter').pipe(Options.repeated),
|
|
1071
|
+
sample: Options.text('sample').pipe(Options.optional),
|
|
1072
|
+
follow: Options.boolean('follow').pipe(Options.withAlias('tail'), Options.withDefault(false)),
|
|
1073
|
+
intervalSeconds: Options.integer('interval-seconds').pipe(Options.withDefault(3)),
|
|
1074
|
+
source: Options.text('source').pipe(Options.optional),
|
|
1075
|
+
appId: Options.text('app-id').pipe(Options.optional),
|
|
1076
|
+
type: Options.text('type').pipe(Options.repeated),
|
|
1077
|
+
errorsOnly: Options.boolean('errors-only').pipe(Options.withDefault(false)),
|
|
1078
|
+
};
|
|
1079
|
+
const LOG_SELECTOR_FLAGS = [
|
|
1080
|
+
'--source',
|
|
1081
|
+
'--project',
|
|
1082
|
+
'--project-id',
|
|
1083
|
+
'--app-id',
|
|
1084
|
+
'--type',
|
|
1085
|
+
'--since-minutes',
|
|
1086
|
+
'--limit',
|
|
1087
|
+
'--trace',
|
|
1088
|
+
'--invocation',
|
|
1089
|
+
'--raw',
|
|
1090
|
+
'--filter',
|
|
1091
|
+
'--sample',
|
|
1092
|
+
'--follow',
|
|
1093
|
+
'--tail',
|
|
1094
|
+
'--errors-only',
|
|
1095
|
+
];
|
|
1096
|
+
function hasExplicitLogSelector(argv = process.argv.slice(2)) {
|
|
1097
|
+
return argv.some((arg) => LOG_SELECTOR_FLAGS.some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
|
|
1098
|
+
}
|
|
1099
|
+
function isErrorLogRow(row) {
|
|
1100
|
+
if (row.level === 'error' || row.errorType)
|
|
1101
|
+
return true;
|
|
1102
|
+
if (typeof row.status === 'number' && row.status >= 400)
|
|
1103
|
+
return true;
|
|
1104
|
+
return typeof row.outcome === 'string' && row.outcome !== 'ok';
|
|
1105
|
+
}
|
|
1106
|
+
function parseLogSource(value) {
|
|
1107
|
+
const normalized = (value ?? 'all').toLowerCase();
|
|
1108
|
+
if (normalized === 'all' || normalized === 'merged') {
|
|
1109
|
+
return { ok: true, wantWorkers: true, wantHubspot: true };
|
|
1110
|
+
}
|
|
1111
|
+
if (normalized === 'workers' || normalized === 'cf' || normalized === 'cloudflare') {
|
|
1112
|
+
return { ok: true, wantWorkers: true, wantHubspot: false };
|
|
1113
|
+
}
|
|
1114
|
+
if (normalized === 'hubspot' || normalized === 'hs') {
|
|
1115
|
+
return { ok: true, wantWorkers: false, wantHubspot: true };
|
|
1116
|
+
}
|
|
1117
|
+
return { ok: false, message: `Invalid --source "${value}". Use all, workers, or hubspot.` };
|
|
1118
|
+
}
|
|
1119
|
+
export const logsCmd = Command.make('logs', logsOptions, (opts) => runHandler('logs', { json: opts.json }, logsBody(opts)));
|
|
1120
|
+
function resolveAccountId(opts, localAccountId) {
|
|
1121
|
+
return Effect.gen(function* () {
|
|
1122
|
+
const explicit = yield* resolveFromOptionOrEnv(opts.accountId, 'HSX_ACCOUNT_ID');
|
|
1123
|
+
if (explicit)
|
|
1124
|
+
return explicit;
|
|
1125
|
+
if (localAccountId)
|
|
1126
|
+
return localAccountId;
|
|
1127
|
+
const accounts = yield* AccountStore;
|
|
1128
|
+
const store = yield* accounts.load;
|
|
1129
|
+
return store.defaultAccountId;
|
|
530
1130
|
});
|
|
531
1131
|
}
|
|
1132
|
+
/** Actionable remediation for the known Workers Logs error slugs. */
|
|
1133
|
+
function workersErrorHint(error) {
|
|
1134
|
+
if (error === 'cloudflare_credential_invalid') {
|
|
1135
|
+
return 'the stored Cloudflare credential was rejected (expired or revoked) — reconnect with `hs-x connect cloudflare`';
|
|
1136
|
+
}
|
|
1137
|
+
if (error === 'observability_permission_missing') {
|
|
1138
|
+
return 'the stored Cloudflare token cannot read Workers Observability — reconnect with `hs-x connect cloudflare`, or use an API token with the Workers Observability read permission';
|
|
1139
|
+
}
|
|
1140
|
+
if (error === 'cloudflare_not_connected') {
|
|
1141
|
+
return 'no Cloudflare account is connected — run `hs-x connect cloudflare`';
|
|
1142
|
+
}
|
|
1143
|
+
return undefined;
|
|
1144
|
+
}
|
|
1145
|
+
function withWorkersErrorHint(error) {
|
|
1146
|
+
const hint = workersErrorHint(error);
|
|
1147
|
+
return hint ? `${error} (${hint})` : error;
|
|
1148
|
+
}
|
|
1149
|
+
function logsBody(opts, context = {}) {
|
|
1150
|
+
return Effect.gen(function* () {
|
|
1151
|
+
const cwd = yield* Cwd;
|
|
1152
|
+
const localProjectBinding = yield* Effect.promise(() => readProjectBinding(cwd));
|
|
1153
|
+
const requestedSource = parseLogSource(Option.getOrUndefined(opts.source));
|
|
1154
|
+
const shouldResolveWorkersProject = requestedSource.ok && requestedSource.wantWorkers;
|
|
1155
|
+
const projectSelection = shouldResolveWorkersProject
|
|
1156
|
+
? (context.projectSelection ??
|
|
1157
|
+
(yield* resolveOperationProjectForRead(opts, {
|
|
1158
|
+
command: 'logs',
|
|
1159
|
+
message: 'Which project do you want logs for?',
|
|
1160
|
+
emptyMessage: 'No projects yet. Run `hs-x deploy` from a project to create one.',
|
|
1161
|
+
})))
|
|
1162
|
+
: undefined;
|
|
1163
|
+
const currentProjectId = projectSelection?.projectId ??
|
|
1164
|
+
(yield* resolveProjectId(opts)) ??
|
|
1165
|
+
localProjectBinding?.projectId;
|
|
1166
|
+
const accountId = projectSelection?.accountId ??
|
|
1167
|
+
(yield* resolveAccountId(opts, localProjectBinding?.accountId));
|
|
1168
|
+
const controlPlaneUrl = yield* resolveControlPlaneUrl(opts.controlPlaneUrl);
|
|
1169
|
+
const userId = yield* resolveUserId(opts);
|
|
1170
|
+
const shouldPrompt = !opts.json &&
|
|
1171
|
+
isInteractive() &&
|
|
1172
|
+
(context.forceInteractiveSetup === true || !hasExplicitLogSelector());
|
|
1173
|
+
let interactiveSelection = undefined;
|
|
1174
|
+
if (shouldPrompt) {
|
|
1175
|
+
if (!accountId) {
|
|
1176
|
+
return yield* failBespoke(false, 'logs', 'Logs require an HS-X account: run `hs-x login`, or pass --account-id / HSX_ACCOUNT_ID.');
|
|
1177
|
+
}
|
|
1178
|
+
if (!currentProjectId) {
|
|
1179
|
+
return yield* failBespoke(false, 'logs', `Logs need a project: ${PROJECT_ID_HINT}.`);
|
|
1180
|
+
}
|
|
1181
|
+
interactiveSelection = yield* Effect.promise(() => promptInteractiveLogsSelection({
|
|
1182
|
+
projects: projectSelection?.projects ?? [],
|
|
1183
|
+
selectedProjectId: currentProjectId,
|
|
1184
|
+
...(localProjectBinding?.projectId
|
|
1185
|
+
? { hubspotProjectId: localProjectBinding.projectId }
|
|
1186
|
+
: {}),
|
|
1187
|
+
}));
|
|
1188
|
+
if (!interactiveSelection) {
|
|
1189
|
+
return yield* Effect.fail(exitWith(130));
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
const projectId = interactiveSelection?.projectId ?? currentProjectId;
|
|
1193
|
+
const parsedFilters = parseLogFilters(opts.filter);
|
|
1194
|
+
if (!parsedFilters.ok) {
|
|
1195
|
+
return yield* failBespoke(opts.json, 'logs', parsedFilters.message);
|
|
1196
|
+
}
|
|
1197
|
+
const parsedSample = parseSampleRate(Option.getOrUndefined(opts.sample));
|
|
1198
|
+
if (!parsedSample.ok) {
|
|
1199
|
+
return yield* failBespoke(opts.json, 'logs', parsedSample.message);
|
|
1200
|
+
}
|
|
1201
|
+
const follow = interactiveSelection?.follow ?? opts.follow;
|
|
1202
|
+
const errorsOnly = interactiveSelection?.errorsOnly ?? opts.errorsOnly;
|
|
1203
|
+
const parsedSystemTypes = interactiveSelection?.systemTypes
|
|
1204
|
+
? { ok: true, systemTypes: interactiveSelection.systemTypes }
|
|
1205
|
+
: parseHubSpotLogSystemTypes(opts.type);
|
|
1206
|
+
if (!parsedSystemTypes.ok) {
|
|
1207
|
+
return yield* failBespoke(opts.json, 'logs', parsedSystemTypes.message);
|
|
1208
|
+
}
|
|
1209
|
+
if (follow && opts.json) {
|
|
1210
|
+
return yield* failBespoke(opts.json, 'logs', '`hs-x logs --follow`/`--tail` streams terminal output. Omit --json for follow mode.');
|
|
1211
|
+
}
|
|
1212
|
+
const sourceChoice = parseLogSource(interactiveSelection?.source ?? Option.getOrUndefined(opts.source));
|
|
1213
|
+
if (!sourceChoice.ok) {
|
|
1214
|
+
return yield* failBespoke(opts.json, 'logs', sourceChoice.message);
|
|
1215
|
+
}
|
|
1216
|
+
const { wantWorkers, wantHubspot } = sourceChoice;
|
|
1217
|
+
// The two sources resolve independently: Workers Logs need an HS-X account
|
|
1218
|
+
// and project id (the control plane mediates the read); HubSpot app logs
|
|
1219
|
+
// need only the local `.hs-x/hubspot.json` binding plus PAK credentials.
|
|
1220
|
+
// A missing prerequisite is fatal only when it leaves no source at all.
|
|
1221
|
+
const explicitAppId = Option.getOrUndefined(opts.appId);
|
|
1222
|
+
const selectedDifferentProject = projectSelection?.projects !== undefined && localProjectBinding?.projectId !== projectId;
|
|
1223
|
+
const hubspotResolution = wantHubspot
|
|
1224
|
+
? selectedDifferentProject && explicitAppId === undefined
|
|
1225
|
+
? {
|
|
1226
|
+
ok: false,
|
|
1227
|
+
reason: 'the selected project is not the project in the current directory; pass --app-id to include its HubSpot app logs',
|
|
1228
|
+
}
|
|
1229
|
+
: yield* Effect.promise(() => resolveHubSpotLogsSource({
|
|
1230
|
+
cwd,
|
|
1231
|
+
...(explicitAppId !== undefined ? { appId: explicitAppId } : {}),
|
|
1232
|
+
}))
|
|
1233
|
+
: undefined;
|
|
1234
|
+
const hubspotSource = hubspotResolution?.ok
|
|
1235
|
+
? hubspotResolution.source
|
|
1236
|
+
: undefined;
|
|
1237
|
+
const hubspotReason = hubspotResolution && !hubspotResolution.ok ? hubspotResolution.reason : undefined;
|
|
1238
|
+
const workersReason = !wantWorkers
|
|
1239
|
+
? 'not requested (--source hubspot)'
|
|
1240
|
+
: !accountId
|
|
1241
|
+
? 'no HS-X account (run `hs-x login`, or pass --account-id / HSX_ACCOUNT_ID)'
|
|
1242
|
+
: !projectId
|
|
1243
|
+
? 'no project id'
|
|
1244
|
+
: undefined;
|
|
1245
|
+
const workersActive = workersReason === undefined;
|
|
1246
|
+
if (!workersActive && !hubspotSource) {
|
|
1247
|
+
if (wantWorkers && !accountId) {
|
|
1248
|
+
return yield* failBespoke(opts.json, 'logs', 'Logs require an HS-X account: run `hs-x login`, or pass --account-id / HSX_ACCOUNT_ID.');
|
|
1249
|
+
}
|
|
1250
|
+
if (wantWorkers && !projectId) {
|
|
1251
|
+
return yield* failBespoke(opts.json, 'logs', `Logs need a project: ${PROJECT_ID_HINT}.`);
|
|
1252
|
+
}
|
|
1253
|
+
return yield* failBespoke(opts.json, 'logs', `HubSpot app logs unavailable: ${hubspotReason ?? 'unknown'}.`);
|
|
1254
|
+
}
|
|
1255
|
+
const trace = Option.getOrUndefined(opts.trace);
|
|
1256
|
+
const limit = clampInt(opts.limit, 1, 200);
|
|
1257
|
+
const sinceMinutes = clampInt(interactiveSelection?.sinceMinutes ?? opts.sinceMinutes, 1, 7 * 24 * 60);
|
|
1258
|
+
const intervalSeconds = clampInt(opts.intervalSeconds, 1, 60);
|
|
1259
|
+
const view = trace ? 'trace' : opts.raw ? 'tail' : 'invocations';
|
|
1260
|
+
// Legacy single-source semantics only when Workers Logs is the sole
|
|
1261
|
+
// source: a control-plane failure hard-fails exactly as before. With a
|
|
1262
|
+
// second source in play, each side degrades to a warning instead.
|
|
1263
|
+
const strictWorkers = workersActive && !hubspotSource;
|
|
1264
|
+
const fetchWorkers = Effect.gen(function* () {
|
|
1265
|
+
const params = new URLSearchParams({ sinceMinutes: String(sinceMinutes) });
|
|
1266
|
+
if (trace) {
|
|
1267
|
+
params.set('invocation', trace);
|
|
1268
|
+
}
|
|
1269
|
+
else {
|
|
1270
|
+
// Workers Observability does not yet expose an errors-only predicate,
|
|
1271
|
+
// so read its maximum page before filtering to avoid hiding an error
|
|
1272
|
+
// behind a page of newer successful requests.
|
|
1273
|
+
params.set('limit', String(errorsOnly ? 200 : limit));
|
|
1274
|
+
if (!opts.raw)
|
|
1275
|
+
params.set('view', 'invocations');
|
|
1276
|
+
}
|
|
1277
|
+
const response = yield* cpGet(controlPlaneUrl, `/v1/accounts/${encodeURIComponent(accountId ?? '')}/projects/${encodeURIComponent(projectId ?? '')}/tail?${params.toString()}`, userId);
|
|
1278
|
+
const body = yield* Effect.promise(() => response.json());
|
|
1279
|
+
if (!response.ok) {
|
|
1280
|
+
const message = responseMessage(body, response.status, 'Control-plane logs read failed');
|
|
1281
|
+
if (strictWorkers) {
|
|
1282
|
+
yield* failBespoke(opts.json, 'logs', message);
|
|
1283
|
+
return { rows: [] };
|
|
1284
|
+
}
|
|
1285
|
+
return { rows: [], error: message };
|
|
1286
|
+
}
|
|
1287
|
+
return asLogsPayload(body);
|
|
1288
|
+
});
|
|
1289
|
+
const fetchHubspot = hubspotSource
|
|
1290
|
+
? Effect.promise(() => fetchHubSpotLogsPayload({
|
|
1291
|
+
source: hubspotSource,
|
|
1292
|
+
sinceMinutes,
|
|
1293
|
+
limit,
|
|
1294
|
+
systemTypes: parsedSystemTypes.systemTypes,
|
|
1295
|
+
errorsOnly,
|
|
1296
|
+
...(trace ? { trace } : {}),
|
|
1297
|
+
}).catch((cause) => ({
|
|
1298
|
+
rows: [],
|
|
1299
|
+
error: cause instanceof Error ? cause.message : String(cause),
|
|
1300
|
+
})))
|
|
1301
|
+
: Effect.succeed(undefined);
|
|
1302
|
+
const fetchPayload = Effect.gen(function* () {
|
|
1303
|
+
const [workers, hubspot] = yield* Effect.all([workersActive ? fetchWorkers : Effect.succeed(undefined), fetchHubspot], { concurrency: 2 });
|
|
1304
|
+
const workersRows = errorsOnly
|
|
1305
|
+
? (workers?.rows ?? []).filter(isErrorLogRow)
|
|
1306
|
+
: (workers?.rows ?? []);
|
|
1307
|
+
const hubspotRows = errorsOnly
|
|
1308
|
+
? (hubspot?.rows ?? []).filter(isErrorLogRow)
|
|
1309
|
+
: (hubspot?.rows ?? []);
|
|
1310
|
+
const filteredWorkers = applyLogFilters(workersRows, parsedFilters.filters, parsedSample.rate);
|
|
1311
|
+
const filteredHubspot = applyLogFilters(hubspotRows, parsedFilters.filters, parsedSample.rate);
|
|
1312
|
+
const rows = hubspotSource
|
|
1313
|
+
? mergeLogRows(filteredWorkers, filteredHubspot, trace ? Number.MAX_SAFE_INTEGER : limit)
|
|
1314
|
+
: trace
|
|
1315
|
+
? filteredWorkers
|
|
1316
|
+
: filteredWorkers.slice(0, limit);
|
|
1317
|
+
const workersError = workers?.error;
|
|
1318
|
+
const hubspotError = hubspot && 'error' in hubspot ? hubspot.error : undefined;
|
|
1319
|
+
const allFailed = (workersActive ? workersError !== undefined : true) &&
|
|
1320
|
+
(hubspotSource ? hubspotError !== undefined : true);
|
|
1321
|
+
const failedSystemTypes = hubspot && 'failedSystemTypes' in hubspot
|
|
1322
|
+
? hubspot.failedSystemTypes
|
|
1323
|
+
: undefined;
|
|
1324
|
+
return {
|
|
1325
|
+
rows,
|
|
1326
|
+
...(allFailed ? { fatalError: workersError ?? hubspotError ?? 'log read failed' } : {}),
|
|
1327
|
+
...(workersError !== undefined ? { workersError } : {}),
|
|
1328
|
+
...(hubspotError !== undefined ? { hubspotError } : {}),
|
|
1329
|
+
...(failedSystemTypes && failedSystemTypes.length > 0
|
|
1330
|
+
? { hubspotFailedSystemTypes: failedSystemTypes }
|
|
1331
|
+
: {}),
|
|
1332
|
+
};
|
|
1333
|
+
});
|
|
1334
|
+
const payload = yield* fetchPayload;
|
|
1335
|
+
const mode = workersActive && hubspotSource
|
|
1336
|
+
? 'merged-logs'
|
|
1337
|
+
: hubspotSource
|
|
1338
|
+
? 'hubspot-app-logs'
|
|
1339
|
+
: 'workers-logs';
|
|
1340
|
+
const result = {
|
|
1341
|
+
ok: !payload.fatalError,
|
|
1342
|
+
command: 'logs',
|
|
1343
|
+
mode,
|
|
1344
|
+
view,
|
|
1345
|
+
...(accountId ? { accountId } : {}),
|
|
1346
|
+
...(projectId ? { projectId } : {}),
|
|
1347
|
+
sinceMinutes,
|
|
1348
|
+
limit,
|
|
1349
|
+
...(wantHubspot ? { hubspotLogTypes: parsedSystemTypes.systemTypes } : {}),
|
|
1350
|
+
...(errorsOnly ? { errorsOnly: true } : {}),
|
|
1351
|
+
...(parsedFilters.filters.length > 0 ? { filters: parsedFilters.filters } : {}),
|
|
1352
|
+
...(parsedSample.rate !== undefined ? { sample: parsedSample.rate } : {}),
|
|
1353
|
+
...(trace ? { invocationId: trace } : {}),
|
|
1354
|
+
sources: {
|
|
1355
|
+
workers: {
|
|
1356
|
+
active: workersActive,
|
|
1357
|
+
...(workersReason !== undefined ? { reason: workersReason } : {}),
|
|
1358
|
+
...(payload.workersError !== undefined ? { error: payload.workersError } : {}),
|
|
1359
|
+
},
|
|
1360
|
+
hubspot: hubspotSource
|
|
1361
|
+
? {
|
|
1362
|
+
active: true,
|
|
1363
|
+
appId: hubspotSource.appId,
|
|
1364
|
+
developerAccountId: hubspotSource.developerAccountId,
|
|
1365
|
+
...(payload.hubspotError !== undefined ? { error: payload.hubspotError } : {}),
|
|
1366
|
+
...(payload.hubspotFailedSystemTypes
|
|
1367
|
+
? { failedSystemTypes: payload.hubspotFailedSystemTypes }
|
|
1368
|
+
: {}),
|
|
1369
|
+
}
|
|
1370
|
+
: {
|
|
1371
|
+
active: false,
|
|
1372
|
+
...(wantHubspot && hubspotReason !== undefined
|
|
1373
|
+
? { reason: hubspotReason }
|
|
1374
|
+
: { reason: 'not requested (--source workers)' }),
|
|
1375
|
+
},
|
|
1376
|
+
},
|
|
1377
|
+
rows: payload.rows,
|
|
1378
|
+
...(payload.fatalError ? { error: payload.fatalError } : {}),
|
|
1379
|
+
};
|
|
1380
|
+
const reporter = yield* Reporter;
|
|
1381
|
+
const showOrigin = hubspotSource !== undefined && workersActive;
|
|
1382
|
+
const reportPartialFailures = Effect.gen(function* () {
|
|
1383
|
+
if (!payload.fatalError && payload.workersError !== undefined) {
|
|
1384
|
+
yield* reporter.info(`Workers Logs unavailable this read: ${withWorkersErrorHint(payload.workersError)}`);
|
|
1385
|
+
}
|
|
1386
|
+
if (!payload.fatalError && hubspotSource && payload.hubspotError !== undefined) {
|
|
1387
|
+
yield* reporter.info(`HubSpot app logs unavailable this read: ${payload.hubspotError}`);
|
|
1388
|
+
}
|
|
1389
|
+
if (payload.hubspotFailedSystemTypes && payload.hubspotFailedSystemTypes.length > 0) {
|
|
1390
|
+
yield* reporter.info(`HubSpot log types skipped (query failed): ${payload.hubspotFailedSystemTypes.join(', ')}`);
|
|
1391
|
+
}
|
|
1392
|
+
if (wantHubspot && !hubspotSource && workersActive && hubspotReason !== undefined) {
|
|
1393
|
+
yield* reporter.info(`HubSpot app logs skipped: ${hubspotReason}.`);
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
const headerLabel = projectId ?? hubspotSource?.projectName ?? `app ${hubspotSource?.appId}`;
|
|
1397
|
+
if (follow) {
|
|
1398
|
+
const seen = new Set();
|
|
1399
|
+
yield* reporter.header(trace ? `${headerLabel} invocation ${trace}` : `${headerLabel} live`);
|
|
1400
|
+
const printNewRows = (rows) => Effect.gen(function* () {
|
|
1401
|
+
const fresh = orderLogRowsAscending(rows.filter((row) => {
|
|
1402
|
+
const key = stableLogRowKey(row);
|
|
1403
|
+
if (seen.has(key))
|
|
1404
|
+
return false;
|
|
1405
|
+
seen.add(key);
|
|
1406
|
+
return true;
|
|
1407
|
+
}));
|
|
1408
|
+
if (fresh.length > 0) {
|
|
1409
|
+
yield* emitLogRows(reporter, fresh, view, showOrigin);
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
yield* printNewRows(payload.rows);
|
|
1413
|
+
yield* reportPartialFailures;
|
|
1414
|
+
if (payload.fatalError) {
|
|
1415
|
+
yield* reporter.error('HSX_E_LOGS_UNAVAILABLE', `Logs unavailable: ${withWorkersErrorHint(payload.fatalError)}`);
|
|
1416
|
+
return yield* Effect.fail(exitWith(1, 'HSX_E_LOGS_UNAVAILABLE'));
|
|
1417
|
+
}
|
|
1418
|
+
while (true) {
|
|
1419
|
+
yield* Effect.promise(() => new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1000)));
|
|
1420
|
+
const next = yield* fetchPayload;
|
|
1421
|
+
yield* printNewRows(next.rows);
|
|
1422
|
+
if (next.fatalError) {
|
|
1423
|
+
yield* reporter.error('HSX_E_LOGS_UNAVAILABLE', `Logs unavailable: ${withWorkersErrorHint(next.fatalError)}`);
|
|
1424
|
+
return yield* Effect.fail(exitWith(1, 'HSX_E_LOGS_UNAVAILABLE'));
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
if (opts.json) {
|
|
1429
|
+
yield* reporter.raw(`${JSON.stringify(result, null, 2)}\n`);
|
|
1430
|
+
if (payload.fatalError)
|
|
1431
|
+
return yield* Effect.fail(exitWith(1, 'HSX_E_LOGS_UNAVAILABLE'));
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
yield* reporter.header(trace ? `${headerLabel} invocation ${trace}` : headerLabel);
|
|
1435
|
+
yield* emitLogRows(reporter, payload.rows, view, showOrigin);
|
|
1436
|
+
yield* reportPartialFailures;
|
|
1437
|
+
if (payload.fatalError) {
|
|
1438
|
+
yield* reporter.error('HSX_E_LOGS_UNAVAILABLE', `Logs unavailable: ${withWorkersErrorHint(payload.fatalError)}`);
|
|
1439
|
+
return yield* Effect.fail(exitWith(1, 'HSX_E_LOGS_UNAVAILABLE'));
|
|
1440
|
+
}
|
|
1441
|
+
yield* reporter.done(`${payload.rows.length} row${payload.rows.length === 1 ? '' : 's'}`, 0, view === 'invocations' && payload.rows.length > 0 ? logRowCounts(payload.rows) : undefined);
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
function optionsForSelectedProject(opts, selection) {
|
|
1445
|
+
return {
|
|
1446
|
+
json: false,
|
|
1447
|
+
controlPlaneUrl: opts.controlPlaneUrl,
|
|
1448
|
+
userId: opts.userId,
|
|
1449
|
+
projectId: Option.some(selection.projectId),
|
|
1450
|
+
accountId: selection.accountId ? Option.some(selection.accountId) : opts.accountId,
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
function checkpointWindowMinutes(checkpoint) {
|
|
1454
|
+
const from = Date.parse(checkpoint.window.from);
|
|
1455
|
+
const to = Date.parse(checkpoint.window.to);
|
|
1456
|
+
if (!Number.isFinite(from) || !Number.isFinite(to) || to <= from)
|
|
1457
|
+
return 24 * 60;
|
|
1458
|
+
return clampInt(Math.ceil((to - from) / 60_000), 1, 7 * 24 * 60);
|
|
1459
|
+
}
|
|
1460
|
+
/** In-process logs view used by the human checkpoint failure drill-down. */
|
|
1461
|
+
export function logsBodyForCheckpoint(opts, selection, checkpoint, failure) {
|
|
1462
|
+
const selected = optionsForSelectedProject(opts, selection);
|
|
1463
|
+
return logsBody({
|
|
1464
|
+
...selected,
|
|
1465
|
+
limit: 50,
|
|
1466
|
+
sinceMinutes: checkpointWindowMinutes(checkpoint),
|
|
1467
|
+
trace: Option.none(),
|
|
1468
|
+
raw: false,
|
|
1469
|
+
filter: [`target=${failure.capabilityId}`],
|
|
1470
|
+
sample: Option.none(),
|
|
1471
|
+
follow: false,
|
|
1472
|
+
intervalSeconds: 3,
|
|
1473
|
+
source: Option.some('workers'),
|
|
1474
|
+
appId: Option.none(),
|
|
1475
|
+
type: [],
|
|
1476
|
+
errorsOnly: true,
|
|
1477
|
+
}, { projectSelection: selection });
|
|
1478
|
+
}
|
|
1479
|
+
/** In-process command bodies used by the human status drill-down. */
|
|
1480
|
+
export function logsBodyForStatus(opts, selection) {
|
|
1481
|
+
const selected = optionsForSelectedProject(opts, selection);
|
|
1482
|
+
return logsBody({
|
|
1483
|
+
...selected,
|
|
1484
|
+
limit: 50,
|
|
1485
|
+
sinceMinutes: 15,
|
|
1486
|
+
trace: Option.none(),
|
|
1487
|
+
raw: false,
|
|
1488
|
+
filter: [],
|
|
1489
|
+
sample: Option.none(),
|
|
1490
|
+
follow: false,
|
|
1491
|
+
intervalSeconds: 3,
|
|
1492
|
+
source: Option.none(),
|
|
1493
|
+
appId: Option.none(),
|
|
1494
|
+
type: [],
|
|
1495
|
+
errorsOnly: false,
|
|
1496
|
+
}, { projectSelection: selection, forceInteractiveSetup: true });
|
|
1497
|
+
}
|
|
1498
|
+
export function routesBodyForStatus(opts, selection) {
|
|
1499
|
+
return routingBody('routes', optionsForSelectedProject(opts, selection));
|
|
1500
|
+
}
|
|
1501
|
+
export function driftBodyForStatus(opts, selection) {
|
|
1502
|
+
return driftBody(optionsForSelectedProject(opts, selection));
|
|
1503
|
+
}
|
|
532
1504
|
// ─── routing / route / routes ──────────────────────────────────────────────────
|
|
533
1505
|
/** Single Command value reused for `routing`, the `route` alias, and `routes`. */
|
|
534
1506
|
function makeRoutingCmd(name) {
|
|
@@ -540,7 +1512,12 @@ export const routesCmd = makeRoutingCmd('routes');
|
|
|
540
1512
|
function routingBody(commandName, opts) {
|
|
541
1513
|
return Effect.gen(function* () {
|
|
542
1514
|
const reportName = commandName === 'routes' ? 'routes' : 'routing';
|
|
543
|
-
const
|
|
1515
|
+
const selection = yield* resolveOperationProjectForRead(opts, {
|
|
1516
|
+
command: reportName,
|
|
1517
|
+
message: 'Which project do you want routing for?',
|
|
1518
|
+
emptyMessage: 'No projects yet. Run `hs-x deploy` from a project to create one.',
|
|
1519
|
+
});
|
|
1520
|
+
const projectId = selection?.projectId;
|
|
544
1521
|
if (!projectId) {
|
|
545
1522
|
return yield* failBespoke(opts.json, reportName, `${commandName === 'routes' ? 'Routes need' : 'Routing status needs'} a project: ${PROJECT_ID_HINT}.`);
|
|
546
1523
|
}
|