@hs-x/cli 0.2.2 → 0.2.4
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/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +18 -0
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/deploy.d.ts +49 -0
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +174 -8
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +189 -16
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/help-command.d.ts.map +1 -1
- package/dist/commands/help-command.js +9 -4
- package/dist/commands/help-command.js.map +1 -1
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/link.d.ts +1 -1
- package/dist/commands/status.js +49 -4
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/update.d.ts +3 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +103 -25
- package/dist/commands/update.js.map +1 -1
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/errors-registry.d.ts.map +1 -1
- package/dist/errors-registry.js +45 -0
- package/dist/errors-registry.js.map +1 -1
- package/dist/reporter/human.d.ts.map +1 -1
- package/dist/reporter/human.js +2 -1
- package/dist/reporter/human.js.map +1 -1
- package/dist/reporter/index.d.ts +2 -0
- package/dist/reporter/index.d.ts.map +1 -1
- package/dist/reporter/index.js +1 -0
- package/dist/reporter/index.js.map +1 -1
- package/dist/reporter/types.d.ts +7 -0
- package/dist/reporter/types.d.ts.map +1 -1
- package/dist/reporter/types.js.map +1 -1
- package/package.json +7 -7
package/dist/commands/deploy.js
CHANGED
|
@@ -324,6 +324,10 @@ function isRecord(value) {
|
|
|
324
324
|
return typeof value === 'object' && value !== null;
|
|
325
325
|
}
|
|
326
326
|
export async function deployCommand({ argv, root, json, }) {
|
|
327
|
+
// Stamp the whole invocation up front: the human summary reporter is created
|
|
328
|
+
// late (render time), so without this "Deployed … in 1ms" measured only the
|
|
329
|
+
// summary step rather than the deploy itself.
|
|
330
|
+
const commandStartedAt = Date.now();
|
|
327
331
|
if (argv[1] === 'promote') {
|
|
328
332
|
return deployPromoteCommand({ argv, json });
|
|
329
333
|
}
|
|
@@ -659,6 +663,7 @@ export async function deployCommand({ argv, root, json, }) {
|
|
|
659
663
|
else {
|
|
660
664
|
renderDeployHuman({
|
|
661
665
|
argv,
|
|
666
|
+
startedAt: commandStartedAt,
|
|
662
667
|
plan,
|
|
663
668
|
root,
|
|
664
669
|
planOnly,
|
|
@@ -834,6 +839,7 @@ function renderDeployHuman(input) {
|
|
|
834
839
|
const reporter = createReporter({
|
|
835
840
|
command: 'deploy',
|
|
836
841
|
argv: input.argv,
|
|
842
|
+
startedAt: input.startedAt,
|
|
837
843
|
});
|
|
838
844
|
const subject = basename(input.root);
|
|
839
845
|
reporter.header(subject);
|
|
@@ -882,7 +888,12 @@ function renderDeployHuman(input) {
|
|
|
882
888
|
if (input.controlPlanePlan.sharedCloudflareQuota) {
|
|
883
889
|
reporter.info(`Shared Cloudflare quota: ${input.controlPlanePlan.sharedCloudflareQuota.visibleSiblingAccountIds.join(', ')}`);
|
|
884
890
|
}
|
|
885
|
-
|
|
891
|
+
// Credential leases only exist on linked, control-plane-backed deploys;
|
|
892
|
+
// "Leases: 0" was unexplained jargon on every unlinked deploy, so say
|
|
893
|
+
// nothing when there are none.
|
|
894
|
+
if (input.controlPlanePlan.leases.length > 0) {
|
|
895
|
+
reporter.info(`Cloudflare credential leases issued by the control plane: ${input.controlPlanePlan.leases.length}`);
|
|
896
|
+
}
|
|
886
897
|
}
|
|
887
898
|
if (input.controlPlaneRecord) {
|
|
888
899
|
reporter.info(`Local control plane record status: ${input.controlPlaneRecord.status}`);
|
|
@@ -897,7 +908,7 @@ function renderDeployHuman(input) {
|
|
|
897
908
|
reporter.info(`Cloudflare deploy: ${deployed}`);
|
|
898
909
|
}
|
|
899
910
|
if (!input.linked && input.cloudflareDeployResult) {
|
|
900
|
-
reporter.info(`
|
|
911
|
+
reporter.info(`Deploy history for this machine: ${machineDashboardUrl(input.machineId)}`);
|
|
901
912
|
}
|
|
902
913
|
if (input.hubspotUploadResult) {
|
|
903
914
|
const upload = input.hubspotUploadResult;
|
|
@@ -1332,6 +1343,7 @@ async function executeCloudflareWorkerDeploy({ argv, root, worker, index, source
|
|
|
1332
1343
|
...(installRuntimeBinding
|
|
1333
1344
|
? { installKvNamespaceId: installRuntimeBinding.installKvNamespaceId }
|
|
1334
1345
|
: {}),
|
|
1346
|
+
...(controlPlaneUrl ? { checkpointDataset: 'hsx_checkpoint', observability: true } : {}),
|
|
1335
1347
|
...(tenantDataPlane
|
|
1336
1348
|
? {
|
|
1337
1349
|
flagsKvNamespaceId: tenantDataPlane.flagsKvNamespaceId,
|
|
@@ -1355,6 +1367,37 @@ async function executeCloudflareWorkerDeploy({ argv, root, worker, index, source
|
|
|
1355
1367
|
tenantDataPlane?.flagsKvNamespaceName,
|
|
1356
1368
|
tenantDataPlane?.tenantD1DatabaseName,
|
|
1357
1369
|
].filter((name) => typeof name === 'string' && name.toLowerCase().startsWith(accountResourcePrefix));
|
|
1370
|
+
// wrangler does NOT auto-apply D1 migrations on deploy (found live:
|
|
1371
|
+
// tenant DBs had zero tables across every real deploy since ADR-014) —
|
|
1372
|
+
// apply them explicitly before the worker code that expects them lands.
|
|
1373
|
+
const migrationsDirExists = await stat(join(root, 'node_modules', '@hs-x', 'runtime', 'migrations'))
|
|
1374
|
+
.then((entry) => entry.isDirectory())
|
|
1375
|
+
.catch(() => false);
|
|
1376
|
+
if (tenantDataPlane?.tenantD1DatabaseName && !dryRun && !migrationsDirExists) {
|
|
1377
|
+
process.stderr.write('warning: @hs-x/runtime did not ship its tenant migrations (pre-0.2.4 package) — tenant D1 tables were NOT applied; upgrade @hs-x/runtime.\n');
|
|
1378
|
+
}
|
|
1379
|
+
if (tenantDataPlane?.tenantD1DatabaseName && !dryRun && migrationsDirExists) {
|
|
1380
|
+
try {
|
|
1381
|
+
await runCloudflareCommand([
|
|
1382
|
+
'bun',
|
|
1383
|
+
'x',
|
|
1384
|
+
'wrangler',
|
|
1385
|
+
'd1',
|
|
1386
|
+
'migrations',
|
|
1387
|
+
'apply',
|
|
1388
|
+
tenantDataPlane.tenantD1DatabaseName,
|
|
1389
|
+
'--remote',
|
|
1390
|
+
'--config',
|
|
1391
|
+
relative(root, configPath),
|
|
1392
|
+
], { cwd: root, env: cloudflareDeployEnv(argv) });
|
|
1393
|
+
}
|
|
1394
|
+
catch (error) {
|
|
1395
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1396
|
+
if (!/No migrations to apply/i.test(message)) {
|
|
1397
|
+
throw new Error(`Tenant D1 migrations failed: ${message.slice(-300)}`);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1358
1401
|
const command = [
|
|
1359
1402
|
'bun',
|
|
1360
1403
|
'x',
|
|
@@ -1458,7 +1501,19 @@ async function executeCloudflareWorkerDeploy({ argv, root, worker, index, source
|
|
|
1458
1501
|
installKvNamespaceName: installRuntimeBinding.installKvNamespaceName,
|
|
1459
1502
|
tokenKeySecretName: installRuntimeBinding.tokenKeySecretName,
|
|
1460
1503
|
runtimeBaseUrl: resolvedRuntimeUrl,
|
|
1461
|
-
...(tenantDataPlane
|
|
1504
|
+
...(tenantDataPlane
|
|
1505
|
+
? {
|
|
1506
|
+
syncGrantSecretName: tenantDataPlane.syncGrantSecretName,
|
|
1507
|
+
// Read-through legs need these registered: exemplars read the
|
|
1508
|
+
// tenant D1; flag tooling reads the flags KV.
|
|
1509
|
+
tenantD1DatabaseId: tenantDataPlane.tenantD1DatabaseId,
|
|
1510
|
+
tenantD1DatabaseName: tenantDataPlane.tenantD1DatabaseName,
|
|
1511
|
+
flagsKvNamespaceId: tenantDataPlane.flagsKvNamespaceId,
|
|
1512
|
+
...(tenantDataPlane.flagsKvNamespaceName
|
|
1513
|
+
? { flagsKvNamespaceName: tenantDataPlane.flagsKvNamespaceName }
|
|
1514
|
+
: {}),
|
|
1515
|
+
}
|
|
1516
|
+
: {}),
|
|
1462
1517
|
});
|
|
1463
1518
|
}
|
|
1464
1519
|
catch (error) {
|
|
@@ -1643,6 +1698,10 @@ async function putDeployInstallRuntimeBinding(input) {
|
|
|
1643
1698
|
tokenKeySecretName: input.tokenKeySecretName,
|
|
1644
1699
|
...(input.runtimeBaseUrl ? { runtimeBaseUrl: input.runtimeBaseUrl } : {}),
|
|
1645
1700
|
...(input.syncGrantSecretName ? { syncGrantSecretName: input.syncGrantSecretName } : {}),
|
|
1701
|
+
...(input.tenantD1DatabaseId ? { tenantD1DatabaseId: input.tenantD1DatabaseId } : {}),
|
|
1702
|
+
...(input.tenantD1DatabaseName ? { tenantD1DatabaseName: input.tenantD1DatabaseName } : {}),
|
|
1703
|
+
...(input.flagsKvNamespaceId ? { flagsKvNamespaceId: input.flagsKvNamespaceId } : {}),
|
|
1704
|
+
...(input.flagsKvNamespaceName ? { flagsKvNamespaceName: input.flagsKvNamespaceName } : {}),
|
|
1646
1705
|
},
|
|
1647
1706
|
});
|
|
1648
1707
|
const body = await response.json();
|
|
@@ -1824,6 +1883,14 @@ export function renderWranglerConfig(input) {
|
|
|
1824
1883
|
`name = "${tomlString(input.workerName)}"`,
|
|
1825
1884
|
`main = "${tomlString(input.entrypointPath)}"`,
|
|
1826
1885
|
`compatibility_date = "${tomlString(input.compatibilityDate)}"`,
|
|
1886
|
+
// nodejs_compat: guide + scaffold code reads `process.env.X` at module
|
|
1887
|
+
// scope (e.g. defineSource hmac secrets); without it the Worker fails
|
|
1888
|
+
// validation with "process is not defined" (Cloudflare error 10021,
|
|
1889
|
+
// cold-stranger run #2 blocker A). With nodejs_compat and a
|
|
1890
|
+
// compatibility_date >= 2025-04-01, Workers auto-populate process.env
|
|
1891
|
+
// from bindings/secrets; the explicit populate flag keeps that true even
|
|
1892
|
+
// when --compatibility-date is set to an older date.
|
|
1893
|
+
'compatibility_flags = [ "nodejs_compat", "nodejs_compat_populate_process_env" ]',
|
|
1827
1894
|
];
|
|
1828
1895
|
if (input.installKvNamespaceId) {
|
|
1829
1896
|
lines.push('', '[[kv_namespaces]]', 'binding = "INSTALL_KV"', `id = "${tomlString(input.installKvNamespaceId)}"`);
|
|
@@ -1835,6 +1902,15 @@ export function renderWranglerConfig(input) {
|
|
|
1835
1902
|
// ADR-015/ADR-014 project-scoped tenant D1 (flag config + installer PII +
|
|
1836
1903
|
// platform events). migrations_dir lets wrangler auto-apply the tenant
|
|
1837
1904
|
// migrations on deploy.
|
|
1905
|
+
if (input.observability) {
|
|
1906
|
+
// Workers Logs (GA): ingested + indexed by Cloudflare for 7 days; the
|
|
1907
|
+
// dashboard live-tail reads them via the observability telemetry API
|
|
1908
|
+
// with the account's stored token (read-through program, 2026-06-10).
|
|
1909
|
+
lines.push('', '[observability]', 'enabled = true', '', '[observability.logs]', 'invocation_logs = true');
|
|
1910
|
+
}
|
|
1911
|
+
if (input.checkpointDataset) {
|
|
1912
|
+
lines.push('', '[[analytics_engine_datasets]]', 'binding = "CHECKPOINT"', `dataset = "${tomlString(input.checkpointDataset)}"`);
|
|
1913
|
+
}
|
|
1838
1914
|
if (input.tenantD1DatabaseId) {
|
|
1839
1915
|
lines.push('', '[[d1_databases]]', 'binding = "TENANT_DB"');
|
|
1840
1916
|
if (input.tenantD1DatabaseName) {
|
|
@@ -1870,7 +1946,7 @@ async function readDeployHubSpotOAuthSecret(input) {
|
|
|
1870
1946
|
function extractWorkerUrl(output) {
|
|
1871
1947
|
return /https:\/\/[a-zA-Z0-9.-]+\.workers\.dev\b/.exec(output)?.[0];
|
|
1872
1948
|
}
|
|
1873
|
-
async function ensureHubSpotRuntimeProjectArtifacts({ argv, root, app, workers, cloudflareDeployResult, needsRuntime, controlPlanePlan, }) {
|
|
1949
|
+
export async function ensureHubSpotRuntimeProjectArtifacts({ argv, root, app, workers, cloudflareDeployResult, needsRuntime, controlPlanePlan, }) {
|
|
1874
1950
|
if (!needsRuntime) {
|
|
1875
1951
|
return;
|
|
1876
1952
|
}
|
|
@@ -1891,6 +1967,14 @@ async function ensureHubSpotRuntimeProjectArtifacts({ argv, root, app, workers,
|
|
|
1891
1967
|
});
|
|
1892
1968
|
for (const [file, contents] of Object.entries(generated.files)) {
|
|
1893
1969
|
const path = join(root, file);
|
|
1970
|
+
// src/app/cards/package.json is user-owned once it exists: HubSpot's own
|
|
1971
|
+
// build errors instruct users to add card dependencies (e.g. hs-uix) to
|
|
1972
|
+
// it, and regenerating the fixed-dep version here silently dropped those
|
|
1973
|
+
// deps so the card never built (cold-stranger run #2 blocker B). Keep the
|
|
1974
|
+
// user's file verbatim; only generate one when absent.
|
|
1975
|
+
if (file === 'src/app/cards/package.json' && (await stat(path).catch(() => undefined))) {
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1894
1978
|
await mkdir(dirname(path), { recursive: true });
|
|
1895
1979
|
await writeFile(path, contents);
|
|
1896
1980
|
}
|
|
@@ -2695,6 +2779,7 @@ async function requestHostedControlPlaneDeployRecordAndMaybePromote({ request, p
|
|
|
2695
2779
|
signedManifest: request.manifest,
|
|
2696
2780
|
signature: `sig_${plan.deployId}`,
|
|
2697
2781
|
bundleKey: deployBundleKey({ projectId: request.projectId, deployId: plan.deployId }),
|
|
2782
|
+
...(await collectDeployProvenance(userId)),
|
|
2698
2783
|
},
|
|
2699
2784
|
});
|
|
2700
2785
|
const body = await response.json();
|
|
@@ -2833,14 +2918,59 @@ function stringProperty(source, propertyName) {
|
|
|
2833
2918
|
return match ? { [propertyName]: match } : {};
|
|
2834
2919
|
}
|
|
2835
2920
|
function schemaProperty(source) {
|
|
2836
|
-
|
|
2921
|
+
// Balanced-brace extraction. The previous non-greedy regex stopped at the
|
|
2922
|
+
// first `}` inside an object-form field (`score: { type: "number", … }`),
|
|
2923
|
+
// truncating the schema and mis-reading nested keys (`type`) as property
|
|
2924
|
+
// names — which made portal-schema plans wrong for any schema that mixes
|
|
2925
|
+
// shorthand strings with object declarations.
|
|
2926
|
+
const body = objectLiteralBody(source, 'schema');
|
|
2837
2927
|
if (!body) {
|
|
2838
2928
|
return {};
|
|
2839
2929
|
}
|
|
2840
2930
|
const schema = {};
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2931
|
+
let index = 0;
|
|
2932
|
+
while (index < body.length) {
|
|
2933
|
+
const entry = /^[\s,]*([A-Za-z_$][\w$]*)\s*:\s*/.exec(body.slice(index));
|
|
2934
|
+
if (!entry || !entry[1])
|
|
2935
|
+
break;
|
|
2936
|
+
const name = entry[1];
|
|
2937
|
+
const valueStart = index + entry[0].length;
|
|
2938
|
+
const char = body[valueStart];
|
|
2939
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
2940
|
+
const close = body.indexOf(char, valueStart + 1);
|
|
2941
|
+
if (close === -1)
|
|
2942
|
+
break;
|
|
2943
|
+
schema[name] = body.slice(valueStart + 1, close);
|
|
2944
|
+
index = close + 1;
|
|
2945
|
+
}
|
|
2946
|
+
else if (char === '{') {
|
|
2947
|
+
let depth = 0;
|
|
2948
|
+
let end = -1;
|
|
2949
|
+
for (let i = valueStart; i < body.length; i += 1) {
|
|
2950
|
+
if (body[i] === '{')
|
|
2951
|
+
depth += 1;
|
|
2952
|
+
else if (body[i] === '}') {
|
|
2953
|
+
depth -= 1;
|
|
2954
|
+
if (depth === 0) {
|
|
2955
|
+
end = i;
|
|
2956
|
+
break;
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
if (end === -1)
|
|
2961
|
+
break;
|
|
2962
|
+
const inner = body.slice(valueStart + 1, end);
|
|
2963
|
+
const type = /type\s*:\s*["'`]([^"'`]+)["'`]/.exec(inner)?.[1];
|
|
2964
|
+
const property = /property\s*:\s*["'`]([^"'`]+)["'`]/.exec(inner)?.[1];
|
|
2965
|
+
schema[name] = { ...(type ? { type } : {}), ...(property ? { property } : {}) };
|
|
2966
|
+
index = end + 1;
|
|
2967
|
+
}
|
|
2968
|
+
else {
|
|
2969
|
+
// Unsupported value shape (identifier, number); skip to the next entry.
|
|
2970
|
+
const nextComma = body.indexOf(',', valueStart);
|
|
2971
|
+
if (nextComma === -1)
|
|
2972
|
+
break;
|
|
2973
|
+
index = nextComma + 1;
|
|
2844
2974
|
}
|
|
2845
2975
|
}
|
|
2846
2976
|
return Object.keys(schema).length > 0 ? { schema } : {};
|
|
@@ -3148,4 +3278,40 @@ const deployPromoteSubCmd = Command.make('promote', {
|
|
|
3148
3278
|
yield* runQuarantined(opts.json, () => deployCommand({ argv, root, json: opts.json }));
|
|
3149
3279
|
})));
|
|
3150
3280
|
export const deployCmd = deployBaseCmd.pipe(Command.withSubcommands([deployPromoteSubCmd]));
|
|
3281
|
+
/**
|
|
3282
|
+
* Provenance for the active-deploy card, collected where it's cheapest: the
|
|
3283
|
+
* CLI knows its own version, the git state of the tree it deployed, and the
|
|
3284
|
+
* session identity. Everything best-effort — a non-git tree stamps nothing.
|
|
3285
|
+
*/
|
|
3286
|
+
async function collectDeployProvenance(userId) {
|
|
3287
|
+
const git = async (args) => {
|
|
3288
|
+
try {
|
|
3289
|
+
const proc = Bun.spawn({ cmd: ['git', ...args], stdout: 'pipe', stderr: 'ignore' });
|
|
3290
|
+
const [out, code] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
3291
|
+
const trimmed = out.trim();
|
|
3292
|
+
return code === 0 && trimmed ? trimmed : undefined;
|
|
3293
|
+
}
|
|
3294
|
+
catch {
|
|
3295
|
+
return undefined;
|
|
3296
|
+
}
|
|
3297
|
+
};
|
|
3298
|
+
const [gitCommit, gitBranch] = await Promise.all([
|
|
3299
|
+
git(['rev-parse', '--short', 'HEAD']),
|
|
3300
|
+
git(['rev-parse', '--abbrev-ref', 'HEAD']),
|
|
3301
|
+
]);
|
|
3302
|
+
let deployedBy;
|
|
3303
|
+
try {
|
|
3304
|
+
const { loadStore, activeSession } = await import('../account-store.js');
|
|
3305
|
+
deployedBy = activeSession(await loadStore())?.email ?? userId;
|
|
3306
|
+
}
|
|
3307
|
+
catch {
|
|
3308
|
+
deployedBy = userId;
|
|
3309
|
+
}
|
|
3310
|
+
return {
|
|
3311
|
+
cliVersion: `hs-x@${CLI_VERSION}`,
|
|
3312
|
+
...(gitCommit ? { gitCommit } : {}),
|
|
3313
|
+
...(gitBranch ? { gitBranch } : {}),
|
|
3314
|
+
...(deployedBy ? { deployedBy } : {}),
|
|
3315
|
+
};
|
|
3316
|
+
}
|
|
3151
3317
|
//# sourceMappingURL=deploy.js.map
|