@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.
Files changed (37) hide show
  1. package/dist/commands/connect.d.ts.map +1 -1
  2. package/dist/commands/connect.js +18 -0
  3. package/dist/commands/connect.js.map +1 -1
  4. package/dist/commands/deploy.d.ts +49 -0
  5. package/dist/commands/deploy.d.ts.map +1 -1
  6. package/dist/commands/deploy.js +174 -8
  7. package/dist/commands/deploy.js.map +1 -1
  8. package/dist/commands/dev.d.ts.map +1 -1
  9. package/dist/commands/dev.js +189 -16
  10. package/dist/commands/dev.js.map +1 -1
  11. package/dist/commands/help-command.d.ts.map +1 -1
  12. package/dist/commands/help-command.js +9 -4
  13. package/dist/commands/help-command.js.map +1 -1
  14. package/dist/commands/init.d.ts +2 -2
  15. package/dist/commands/link.d.ts +1 -1
  16. package/dist/commands/status.js +49 -4
  17. package/dist/commands/status.js.map +1 -1
  18. package/dist/commands/update.d.ts +3 -1
  19. package/dist/commands/update.d.ts.map +1 -1
  20. package/dist/commands/update.js +103 -25
  21. package/dist/commands/update.js.map +1 -1
  22. package/dist/constants.d.ts +1 -1
  23. package/dist/constants.js +1 -1
  24. package/dist/errors-registry.d.ts.map +1 -1
  25. package/dist/errors-registry.js +45 -0
  26. package/dist/errors-registry.js.map +1 -1
  27. package/dist/reporter/human.d.ts.map +1 -1
  28. package/dist/reporter/human.js +2 -1
  29. package/dist/reporter/human.js.map +1 -1
  30. package/dist/reporter/index.d.ts +2 -0
  31. package/dist/reporter/index.d.ts.map +1 -1
  32. package/dist/reporter/index.js +1 -0
  33. package/dist/reporter/index.js.map +1 -1
  34. package/dist/reporter/types.d.ts +7 -0
  35. package/dist/reporter/types.d.ts.map +1 -1
  36. package/dist/reporter/types.js.map +1 -1
  37. package/package.json +7 -7
@@ -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
- reporter.info(`Leases: ${input.controlPlanePlan.leases.length}`);
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(`Machine dashboard: ${machineDashboardUrl(input.machineId)}`);
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 ? { syncGrantSecretName: tenantDataPlane.syncGrantSecretName } : {}),
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
- const body = /schema\s*:\s*\{([\s\S]*?)\}\s*(?:,|$)/.exec(source)?.[1];
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
- for (const match of body.matchAll(/([A-Za-z_$][\w$]*)\s*:\s*["'`]([^"'`]+)["'`]/g)) {
2842
- if (match[1] && match[2]) {
2843
- schema[match[1]] = match[2];
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