@superblocksteam/sdk 2.0.151 → 2.0.152-next.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.
Files changed (84) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/cli-replacement/automatic-upgrades.d.ts.map +1 -1
  3. package/dist/cli-replacement/automatic-upgrades.js +8 -6
  4. package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
  5. package/dist/cli-replacement/dev-s3-restore.test.mjs +1 -0
  6. package/dist/cli-replacement/dev-s3-restore.test.mjs.map +1 -1
  7. package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs +1 -0
  8. package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs.map +1 -1
  9. package/dist/cli-replacement/dev.d.mts +148 -1
  10. package/dist/cli-replacement/dev.d.mts.map +1 -1
  11. package/dist/cli-replacement/dev.interception.test.mjs +259 -4
  12. package/dist/cli-replacement/dev.interception.test.mjs.map +1 -1
  13. package/dist/cli-replacement/dev.mjs +429 -28
  14. package/dist/cli-replacement/dev.mjs.map +1 -1
  15. package/dist/client.billing-usage.test.js +25 -0
  16. package/dist/client.billing-usage.test.js.map +1 -1
  17. package/dist/client.d.ts +19 -0
  18. package/dist/client.d.ts.map +1 -1
  19. package/dist/client.js.map +1 -1
  20. package/dist/collect-sdk-apis.d.mts +61 -3
  21. package/dist/collect-sdk-apis.d.mts.map +1 -1
  22. package/dist/collect-sdk-apis.mjs +97 -6
  23. package/dist/collect-sdk-apis.mjs.map +1 -1
  24. package/dist/collect-sdk-apis.test.mjs +277 -1
  25. package/dist/collect-sdk-apis.test.mjs.map +1 -1
  26. package/dist/dev-utils/dev-server-metrics.d.mts +17 -0
  27. package/dist/dev-utils/dev-server-metrics.d.mts.map +1 -1
  28. package/dist/dev-utils/dev-server-metrics.dependency-failure.test.d.mts +2 -0
  29. package/dist/dev-utils/dev-server-metrics.dependency-failure.test.d.mts.map +1 -0
  30. package/dist/dev-utils/dev-server-metrics.dependency-failure.test.mjs +108 -0
  31. package/dist/dev-utils/dev-server-metrics.dependency-failure.test.mjs.map +1 -0
  32. package/dist/dev-utils/dev-server-metrics.mjs +16 -2
  33. package/dist/dev-utils/dev-server-metrics.mjs.map +1 -1
  34. package/dist/dev-utils/dev-server.d.mts +9 -0
  35. package/dist/dev-utils/dev-server.d.mts.map +1 -1
  36. package/dist/dev-utils/dev-server.mjs +44 -27
  37. package/dist/dev-utils/dev-server.mjs.map +1 -1
  38. package/dist/dev-utils/dev-server.status.test.mjs +31 -1
  39. package/dist/dev-utils/dev-server.status.test.mjs.map +1 -1
  40. package/dist/dev-utils/fatal-exit.d.mts +17 -4
  41. package/dist/dev-utils/fatal-exit.d.mts.map +1 -1
  42. package/dist/dev-utils/fatal-exit.mjs +4 -3
  43. package/dist/dev-utils/fatal-exit.mjs.map +1 -1
  44. package/dist/extract-api-integrations.d.mts +1 -1
  45. package/dist/extract-api-integrations.d.mts.map +1 -1
  46. package/dist/extract-api-integrations.mjs +1 -1
  47. package/dist/extract-api-integrations.mjs.map +1 -1
  48. package/dist/flag.d.ts +5 -0
  49. package/dist/flag.d.ts.map +1 -1
  50. package/dist/flag.js +7 -0
  51. package/dist/flag.js.map +1 -1
  52. package/dist/flag.test.js +18 -0
  53. package/dist/flag.test.js.map +1 -1
  54. package/dist/index.d.ts +1 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js.map +1 -1
  57. package/dist/types/common.d.ts +1 -0
  58. package/dist/types/common.d.ts.map +1 -1
  59. package/dist/types/common.js.map +1 -1
  60. package/dist/vite-plugin-generate-api-build-manifest.d.mts.map +1 -1
  61. package/dist/vite-plugin-generate-api-build-manifest.mjs +46 -0
  62. package/dist/vite-plugin-generate-api-build-manifest.mjs.map +1 -1
  63. package/package.json +6 -6
  64. package/src/cli-replacement/automatic-upgrades.ts +8 -6
  65. package/src/cli-replacement/dev-s3-restore.test.mts +1 -0
  66. package/src/cli-replacement/dev-startup-git-before-dbfs-order.test.mts +1 -0
  67. package/src/cli-replacement/dev.interception.test.mts +334 -10
  68. package/src/cli-replacement/dev.mts +499 -33
  69. package/src/client.billing-usage.test.ts +26 -0
  70. package/src/client.ts +21 -0
  71. package/src/collect-sdk-apis.mts +141 -5
  72. package/src/collect-sdk-apis.test.mts +380 -1
  73. package/src/dev-utils/dev-server-metrics.dependency-failure.test.mts +149 -0
  74. package/src/dev-utils/dev-server-metrics.mts +25 -1
  75. package/src/dev-utils/dev-server.mts +52 -29
  76. package/src/dev-utils/dev-server.status.test.mts +42 -0
  77. package/src/dev-utils/fatal-exit.mts +18 -4
  78. package/src/extract-api-integrations.mts +3 -0
  79. package/src/flag.test.ts +21 -0
  80. package/src/flag.ts +8 -0
  81. package/src/index.ts +1 -0
  82. package/src/types/common.ts +1 -0
  83. package/src/vite-plugin-generate-api-build-manifest.mts +74 -4
  84. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,149 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import type { devServerMetrics as DevServerMetricsSingleton } from "./dev-server-metrics.mjs";
4
+
5
+ /**
6
+ * Dependency-failure counter emission tests.
7
+ *
8
+ * These assert on what the COUNTER receives, not on what the recorder method
9
+ * was called with. That distinction is the whole point of this file: an earlier
10
+ * revision of `recordUpgradeFailure` accepted an `outcome` argument and passed
11
+ * it down, but the shared `recordDependencyFailure` helper rebuilt its own
12
+ * `labels` object from a fixed destructure — so `outcome` was silently dropped
13
+ * on the way to `counter.add()`. A test that spies on `recordUpgradeFailure`
14
+ * passes against that bug; only a test that listens at the meter boundary
15
+ * catches it.
16
+ *
17
+ * The fake meter is captured at `getMeter()`, mirroring
18
+ * `dev-server-metrics.boot.test.mts`. The only substitutions are the two
19
+ * telemetry-bootstrap boundaries the module depends on
20
+ * (`isTelemetryInitialized`, `getMeter`).
21
+ */
22
+
23
+ type Add = { name: string; value: number; attrs: Record<string, unknown> };
24
+
25
+ const h = vi.hoisted((): { adds: Add[]; telemetryUp: boolean } => ({
26
+ adds: [],
27
+ telemetryUp: true,
28
+ }));
29
+
30
+ vi.mock("@superblocksteam/telemetry", () => ({
31
+ isTelemetryInitialized: () => h.telemetryUp,
32
+ }));
33
+
34
+ vi.mock("../telemetry/index.js", () => ({
35
+ getMeter: () => ({
36
+ createCounter: (name: string) => ({
37
+ add: (value: number, attrs: Record<string, unknown>) =>
38
+ h.adds.push({ name, value, attrs }),
39
+ }),
40
+ createHistogram: () => ({ record: () => {} }),
41
+ }),
42
+ }));
43
+
44
+ const addsFor = (name: string) => h.adds.filter((a) => a.name === name);
45
+ const UPGRADE_COUNTER = "dev_server_auto_upgrade_failure_total";
46
+ const INSTALL_COUNTER = "dev_server_initial_install_failure_total";
47
+
48
+ describe("devServerMetrics dependency-failure counters", () => {
49
+ let devServerMetrics: typeof DevServerMetricsSingleton;
50
+
51
+ beforeEach(async () => {
52
+ h.adds = [];
53
+ h.telemetryUp = true;
54
+ vi.resetModules();
55
+ ({ devServerMetrics } = await import("./dev-server-metrics.mjs"));
56
+ });
57
+
58
+ it("puts `outcome` on the auto-upgrade counter the exporter actually sees", () => {
59
+ devServerMetrics.recordUpgradeFailure({
60
+ category: "unknown",
61
+ npmErrorCode: "ETARGET",
62
+ hasAnyRegistryConfigured: true,
63
+ outcome: "exit",
64
+ });
65
+
66
+ const upgrades = addsFor(UPGRADE_COUNTER);
67
+ expect(upgrades).toHaveLength(1);
68
+ expect(upgrades[0].value).toBe(1);
69
+ expect(upgrades[0].attrs).toEqual({
70
+ category: "unknown",
71
+ npm_error_code: "ETARGET",
72
+ has_any_registry_configured: "true",
73
+ outcome: "exit",
74
+ });
75
+ });
76
+
77
+ it("distinguishes the degrade outcome from the exit outcome on the counter", () => {
78
+ devServerMetrics.recordUpgradeFailure({
79
+ category: "registry_unreachable",
80
+ outcome: "degrade",
81
+ });
82
+
83
+ const upgrades = addsFor(UPGRADE_COUNTER);
84
+ expect(upgrades).toHaveLength(1);
85
+ expect(upgrades[0].attrs).toMatchObject({ outcome: "degrade" });
86
+ });
87
+
88
+ it("omits `outcome` entirely when the caller did not supply one", () => {
89
+ // A missing outcome must not mint an `outcome: undefined` facet — an
90
+ // undefined attribute value is not the same series as no attribute.
91
+ devServerMetrics.recordUpgradeFailure({ category: "tls_failed" });
92
+
93
+ const upgrades = addsFor(UPGRADE_COUNTER);
94
+ expect(upgrades).toHaveLength(1);
95
+ expect(upgrades[0].attrs).not.toHaveProperty("outcome");
96
+ });
97
+
98
+ it("keeps the initial-install counter's label set unchanged (no `outcome`)", () => {
99
+ // The install path has no exit/degrade split, so adding an empty `outcome`
100
+ // facet there would split the existing series for no diagnostic gain.
101
+ devServerMetrics.recordInitialInstallFailure({
102
+ category: "dependency_conflict",
103
+ npmErrorCode: "ERESOLVE",
104
+ hasAnyRegistryConfigured: false,
105
+ });
106
+
107
+ const installs = addsFor(INSTALL_COUNTER);
108
+ expect(installs).toHaveLength(1);
109
+ expect(installs[0].attrs).toEqual({
110
+ category: "dependency_conflict",
111
+ npm_error_code: "ERESOLVE",
112
+ has_any_registry_configured: "false",
113
+ });
114
+ });
115
+
116
+ it("still bounds npm_error_code and has_any_registry_configured with `outcome` present", () => {
117
+ devServerMetrics.recordUpgradeFailure({
118
+ category: "unknown",
119
+ npmErrorCode: "`;rm -rf /#",
120
+ outcome: "exit",
121
+ });
122
+
123
+ const upgrades = addsFor(UPGRADE_COUNTER);
124
+ expect(upgrades[0].attrs).toEqual({
125
+ category: "unknown",
126
+ npm_error_code: "other",
127
+ has_any_registry_configured: "unknown",
128
+ outcome: "exit",
129
+ });
130
+ });
131
+
132
+ it("replays a buffered upgrade failure with its `outcome` intact", () => {
133
+ // The exit path records while telemetry may still be down; the label has to
134
+ // survive the buffer round-trip, not just the direct call.
135
+ h.telemetryUp = false;
136
+ devServerMetrics.recordUpgradeFailure({
137
+ category: "unknown",
138
+ outcome: "exit",
139
+ });
140
+ expect(addsFor(UPGRADE_COUNTER)).toHaveLength(0);
141
+
142
+ h.telemetryUp = true;
143
+ devServerMetrics.flush();
144
+
145
+ const upgrades = addsFor(UPGRADE_COUNTER);
146
+ expect(upgrades).toHaveLength(1);
147
+ expect(upgrades[0].attrs).toMatchObject({ outcome: "exit" });
148
+ });
149
+ });
@@ -414,6 +414,15 @@ class DevServerMetrics {
414
414
  * - `npm_error_code` is a bounded npm error code string (e.g. "E401",
415
415
  * "ERESOLVE") or `"none"` when absent.
416
416
  * - `has_any_registry_configured` is `"true"` / `"false"` / `"unknown"`.
417
+ * - `outcome`, when the caller supplies one, is a closed enum owned by that
418
+ * caller (see `recordUpgradeFailure`).
419
+ *
420
+ * Every label a caller passes must be destructured here AND placed into
421
+ * `labels`. This helper is the only thing that reaches `counter.add`, so a
422
+ * field accepted by a public wrapper but omitted here is silently dropped on
423
+ * the way to the exporter — and TypeScript cannot catch it, because `labels`
424
+ * is a typed variable rather than a fresh object literal at the call site.
425
+ * That is exactly how `outcome` went missing.
417
426
  *
418
427
  * Do NOT pass raw package names, registry hostnames, or rawError — those are
419
428
  * high-cardinality or contain customer-infra identifiers.
@@ -425,10 +434,12 @@ class DevServerMetrics {
425
434
  category,
426
435
  npmErrorCode,
427
436
  hasAnyRegistryConfigured,
437
+ outcome,
428
438
  }: {
429
439
  category: string;
430
440
  npmErrorCode?: string;
431
441
  hasAnyRegistryConfigured?: boolean;
442
+ outcome?: string;
432
443
  },
433
444
  ): void {
434
445
  const labels = {
@@ -438,6 +449,11 @@ class DevServerMetrics {
438
449
  hasAnyRegistryConfigured === undefined
439
450
  ? "unknown"
440
451
  : String(hasAnyRegistryConfigured),
452
+ // Spread conditionally rather than assigning `outcome` directly: the
453
+ // initial-install counter has no exit/degrade split, and emitting an
454
+ // `outcome: undefined` attribute there would fork its existing series
455
+ // for no diagnostic gain.
456
+ ...(outcome === undefined ? {} : { outcome }),
441
457
  };
442
458
  this.record(() => {
443
459
  getMeter().createCounter(counterName, { description }).add(1, labels);
@@ -463,10 +479,18 @@ class DevServerMetrics {
463
479
  category: string;
464
480
  npmErrorCode?: string;
465
481
  hasAnyRegistryConfigured?: boolean;
482
+ /**
483
+ * What the failure did to this process: `degrade` kept the current version
484
+ * running, `exit` terminated so the sandbox is replaced. Without this the
485
+ * two outcomes are indistinguishable, so a crash-loop of exiting pods looks
486
+ * identical to a fleet of pods quietly serving the older version.
487
+ * Low-cardinality: a closed two-value enum.
488
+ */
489
+ outcome?: "degrade" | "exit";
466
490
  }): void {
467
491
  this.recordDependencyFailure(
468
492
  "dev_server_auto_upgrade_failure_total",
469
- "Count of degraded dev-server startups where a best-effort CLI/library auto-upgrade failed and the pod kept the current version.",
493
+ "Count of dev-server startups where a best-effort CLI/library auto-upgrade failed, labelled by outcome: `degrade` (kept the current version) or `exit` (terminated so the sandbox is replaced).",
470
494
  labels,
471
495
  );
472
496
  }
@@ -90,6 +90,36 @@ export function getDevServerTracer() {
90
90
  return getTracer();
91
91
  }
92
92
 
93
+ /**
94
+ * Every `/_sb_*` control endpoint, keyed by its low-cardinality metrics label.
95
+ * The request path is always `/` + the label.
96
+ *
97
+ * The value is why the endpoint must answer even when Vite is not up, which
98
+ * exempts it from the "hold the request until Vite is initialized" middleware,
99
+ * or null when the endpoint is held like ordinary traffic. Typed as an
100
+ * exhaustive `Record` over `DevServerEndpoint`, so an endpoint cannot be added
101
+ * without declaring its hold behavior. Both `endpointForUrl` and
102
+ * `isDevServerControlPath` read this table, so the metrics label and the
103
+ * exemption cannot drift apart.
104
+ */
105
+ const DEV_SERVER_ENDPOINTS: Readonly<Record<DevServerEndpoint, string | null>> =
106
+ {
107
+ _sb_activate: null,
108
+ _sb_activity: "idle-reaper poll; reads the activity tracker, not Vite",
109
+ _sb_connect: "carries the Vite init failure the editor surfaces",
110
+ _sb_disconnect: "teardown has to work when Vite never came up",
111
+ _sb_health:
112
+ "the editor gates page load on it; holding it hides the failure",
113
+ _sb_persist: "archives the workspace off disk; never touches Vite",
114
+ _sb_ready: null,
115
+ _sb_status: "reports serverErrors, which only matter once Vite failed",
116
+ _sb_update_token: null,
117
+ };
118
+
119
+ function isDevServerEndpoint(label: string): label is DevServerEndpoint {
120
+ return Object.hasOwn(DEV_SERVER_ENDPOINTS, label);
121
+ }
122
+
93
123
  /**
94
124
  * Maps a dev-server request URL onto a low-cardinality endpoint label for
95
125
  * metrics. Returns undefined for non-`/_sb_*` URLs (Vite-proxied traffic).
@@ -99,28 +129,22 @@ function endpointForUrl(
99
129
  ): DevServerEndpoint | undefined {
100
130
  if (!url) return undefined;
101
131
  const path = url.split("?", 1)[0];
102
- switch (path) {
103
- case "/_sb_health":
104
- return "_sb_health";
105
- case "/_sb_connect":
106
- return "_sb_connect";
107
- case "/_sb_activate":
108
- return "_sb_activate";
109
- case "/_sb_disconnect":
110
- return "_sb_disconnect";
111
- case "/_sb_status":
112
- return "_sb_status";
113
- case "/_sb_activity":
114
- return "_sb_activity";
115
- case "/_sb_update_token":
116
- return "_sb_update_token";
117
- case "/_sb_persist":
118
- return "_sb_persist";
119
- case "/_sb_ready":
120
- return "_sb_ready";
121
- default:
122
- return undefined;
123
- }
132
+ if (!path.startsWith("/")) return undefined;
133
+ const label = path.slice(1);
134
+ return isDevServerEndpoint(label) ? label : undefined;
135
+ }
136
+
137
+ /**
138
+ * True when a request must be answered even if Vite never initialized, so the
139
+ * hold middleware lets it through. `DEV_SERVER_ENDPOINTS` says which endpoints
140
+ * qualify and why.
141
+ *
142
+ * Compares the path, not the raw URL: the editor polls `/_sb_health?t=<epoch>`
143
+ * with a cache-buster, so a raw-URL comparison drops that poll into the hold.
144
+ */
145
+ export function isDevServerControlPath(url: string | undefined): boolean {
146
+ const endpoint = endpointForUrl(url);
147
+ return endpoint !== undefined && DEV_SERVER_ENDPOINTS[endpoint] !== null;
124
148
  }
125
149
 
126
150
  /**
@@ -911,13 +935,7 @@ export async function createDevServer({
911
935
  res.setHeader("Cache-Control", "no-store, max-age=0");
912
936
  res.setHeader("x-csb-no-sw-proxy", "1");
913
937
 
914
- if (
915
- req.url === "/_sb_connect" ||
916
- req.url === "/_sb_disconnect" ||
917
- req.url === "/_sb_health" ||
918
- req.url === "/_sb_status" ||
919
- req.url === "/_sb_activity"
920
- ) {
938
+ if (isDevServerControlPath(req.url)) {
921
939
  return next();
922
940
  }
923
941
 
@@ -1441,6 +1459,10 @@ export async function createDevServer({
1441
1459
  featureFlags?.inPlaceTemplateRestartEnabled(),
1442
1460
  "SUPERBLOCKS_DEV_SERVER_IN_PLACE_TEMPLATE_RESTART_ENABLED",
1443
1461
  ),
1462
+ autoCommitCheckpointsToGitEnabled: flagOrEnv(
1463
+ featureFlags?.removeManualCommitsEnabled(),
1464
+ "SUPERBLOCKS_REMOVE_MANUAL_COMMITS_ENABLED",
1465
+ ),
1444
1466
  },
1445
1467
  });
1446
1468
  // Note: vitePromise itself gets a no-op .catch() at its construction site
@@ -1517,6 +1539,7 @@ async function startVite({
1517
1539
  features: {
1518
1540
  enableSessionRecording: boolean;
1519
1541
  inPlaceTemplateRestartEnabled: boolean;
1542
+ autoCommitCheckpointsToGitEnabled: boolean;
1520
1543
  };
1521
1544
  prepareInPlaceRestart: (options?: { flipRestart?: true }) => Promise<void>;
1522
1545
  }): Promise<{
@@ -10,6 +10,7 @@ import {
10
10
  buildStatusPayload,
11
11
  DEV_SERVER_BOOT_ID,
12
12
  DEV_SERVER_STARTED_AT,
13
+ isDevServerControlPath,
13
14
  } from "./dev-server.mjs";
14
15
 
15
16
  describe("buildStatusPayload", () => {
@@ -90,3 +91,44 @@ describe("buildHealthResponse", () => {
90
91
  expect(health.startedAt).toBe(DEV_SERVER_STARTED_AT);
91
92
  });
92
93
  });
94
+
95
+ describe("isDevServerControlPath", () => {
96
+ it.each([
97
+ "/_sb_connect",
98
+ "/_sb_disconnect",
99
+ "/_sb_health",
100
+ "/_sb_status",
101
+ "/_sb_activity",
102
+ "/_sb_persist",
103
+ ])("treats %s as a control path", (url) => {
104
+ expect(isDevServerControlPath(url)).toBe(true);
105
+ });
106
+
107
+ it.each(["/_sb_activate", "/_sb_ready", "/_sb_update_token"])(
108
+ "leaves %s subject to the vite hold",
109
+ (url) => {
110
+ expect(isDevServerControlPath(url)).toBe(false);
111
+ },
112
+ );
113
+
114
+ it("treats a control path carrying a query string as a control path", () => {
115
+ expect(isDevServerControlPath("/_sb_health?t=1785959985730")).toBe(true);
116
+ expect(isDevServerControlPath("/_sb_connect?foo=bar&baz=1")).toBe(true);
117
+ expect(isDevServerControlPath("/_sb_persist?t=1")).toBe(true);
118
+ });
119
+
120
+ it("does not claim vite-proxied traffic", () => {
121
+ expect(isDevServerControlPath("/src/main.tsx")).toBe(false);
122
+ expect(isDevServerControlPath("/_sb_healthcheck")).toBe(false);
123
+ expect(isDevServerControlPath("/_sb_health/extra")).toBe(false);
124
+ expect(isDevServerControlPath(undefined)).toBe(false);
125
+ });
126
+
127
+ // The lookup is by object key, so Object.prototype members must not match.
128
+ it.each(["/toString", "/constructor", "/hasOwnProperty", "/__proto__"])(
129
+ "does not treat %s as a control path",
130
+ (url) => {
131
+ expect(isDevServerControlPath(url)).toBe(false);
132
+ },
133
+ );
134
+ });
@@ -25,9 +25,10 @@
25
25
  * `handler=containerExit` line for the same exit. A signal kill / OOM emits only
26
26
  * the `containerExit` line (no JS handler ran); a JS crash where shutdown exits
27
27
  * first with code 0 emits only the JS-handler line (the supervisor sees a clean
28
- * exit). So a raw `count(event=dev_server_fatal_exit)` over-counts the double
29
- * case: deduplicate by pod within a short time window for a true unique-crash
30
- * count.
28
+ * exit). A deliberate startup exit emits `handler=startupFailure` from the child
29
+ * plus the supervisor's `containerExit` line, i.e. the same double shape. So a
30
+ * raw `count(event=dev_server_fatal_exit)` over-counts the double case:
31
+ * deduplicate by pod within a short time window for a true unique-crash count.
31
32
  *
32
33
  * Signal-kill field note: for a signal kill the cold parent emits
33
34
  * `exit_code=null signal=SIGKILL` (Node's `child.on("exit")` gives a null code +
@@ -49,11 +50,24 @@
49
50
  * and the warm entrypoint shell emit this
50
51
  * value. They cannot import this module,
51
52
  * so they repeat the same string.
53
+ * startupFailure `dev()` decided at startup that it
54
+ * cannot serve (sync/lock/setup failure,
55
+ * or a dependency upgrade that may have
56
+ * tainted the live tree in a disposable
57
+ * sandbox) and is exiting deliberately.
58
+ * No JS error handler is involved; the
59
+ * adjacent `[dev-server] Startup failed
60
+ * during …` line names the stage.
61
+ *
62
+ * Adding a VALUE here (as `startupFailure` was) needs no change in the two
63
+ * emitters that cannot import this module — they each emit their own fixed
64
+ * value. Only adding a FIELD does; see the SYNC note on `buildFatalExitLog`.
52
65
  */
53
66
  type FatalExitHandler =
54
67
  | "uncaughtException"
55
68
  | "unhandledRejection"
56
- | "containerExit";
69
+ | "containerExit"
70
+ | "startupFailure";
57
71
 
58
72
  /** Fixed token so a log-based metric or a log search can pick out these lines. */
59
73
  export const FATAL_EXIT_EVENT = "dev_server_fatal_exit";
@@ -1,5 +1,8 @@
1
1
  export {
2
+ extractApiIntegrations,
2
3
  extractIntegrationIdsFromSource,
3
4
  extractIntegrationsFromSource,
5
+ resolveRelativeImportPath,
6
+ type ExtractApiIntegrationsResult,
4
7
  type IntegrationInfo,
5
8
  } from "@superblocksteam/vite-plugin-file-sync/extract-api-integrations";
package/src/flag.test.ts CHANGED
@@ -41,6 +41,27 @@ describe("flagOrEnv", () => {
41
41
  });
42
42
  });
43
43
 
44
+ describe("FeatureFlags.removeManualCommitsEnabled", () => {
45
+ it("defaults to false when the flag is absent", () => {
46
+ const flags = new FeatureFlags({});
47
+ expect(flags.removeManualCommitsEnabled()).toBe(false);
48
+ });
49
+
50
+ it("returns true when the flag is enabled", () => {
51
+ const flags = new FeatureFlags({
52
+ "superblocks.remove-manual-commits.enabled": true,
53
+ });
54
+ expect(flags.removeManualCommitsEnabled()).toBe(true);
55
+ });
56
+
57
+ it("returns false when the flag is explicitly disabled", () => {
58
+ const flags = new FeatureFlags({
59
+ "superblocks.remove-manual-commits.enabled": false,
60
+ });
61
+ expect(flags.removeManualCommitsEnabled()).toBe(false);
62
+ });
63
+ });
64
+
44
65
  describe("FeatureFlags.inPlaceTemplateRestartEnabled", () => {
45
66
  it("defaults to false when the flag is absent", () => {
46
67
  const flags = new FeatureFlags({});
package/src/flag.ts CHANGED
@@ -59,6 +59,14 @@ export class FeatureFlags {
59
59
  return this.flags["superblocks.dev-server.memory-metrics.enabled"] ?? false;
60
60
  }
61
61
 
62
+ /**
63
+ * Gates the unified version history work. While off, autosave keeps its
64
+ * original fixed-window cadence so dev servers behave exactly as before.
65
+ */
66
+ removeManualCommitsEnabled(): boolean {
67
+ return this.flags["superblocks.remove-manual-commits.enabled"] ?? false;
68
+ }
69
+
62
70
  /**
63
71
  * Gates the in-place dev-server process restart after a v2->v3 template
64
72
  * flip. When enabled (and a restart supervisor is present), the
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ export {
21
21
  type UpdateOrgBillingUsageLimitRequest,
22
22
  type UpdateUserBillingUsageLimitRequest,
23
23
  type UserCreditLimit,
24
+ type UsageGroupRow,
24
25
  type UsageRecordRow,
25
26
  } from "./client.js";
26
27
 
@@ -20,6 +20,7 @@ export interface FlagBootstrap {
20
20
  "superblocks.dev-server.memory-metrics.enabled"?: boolean;
21
21
  "superblocks.dev-server.in-place-template-restart.enabled"?: boolean;
22
22
  "superblocks.deploy.include-dot-superblocks"?: boolean;
23
+ "superblocks.remove-manual-commits.enabled"?: boolean;
23
24
  }
24
25
 
25
26
  export interface TokenConfig {
@@ -17,6 +17,7 @@ import {
17
17
 
18
18
  import { collectSdkApisFromRegistry } from "./collect-sdk-apis.mjs";
19
19
  import { getLogger } from "./telemetry/logging.js";
20
+ import type { Logger } from "./telemetry/logging.js";
20
21
 
21
22
  const BUILD_MANIFEST_SUFFIX = path.join(
22
23
  "lib",
@@ -35,6 +36,32 @@ function isBuildManifestModule(id: string): boolean {
35
36
  );
36
37
  }
37
38
 
39
+ /**
40
+ * Emits a structured WARN when the logger supports it, falling back to a
41
+ * plain `warn` with just the human-readable message otherwise.
42
+ * `warnStructured` is optional on the `Logger` type (future/alternate
43
+ * implementations may omit it), so calling it with `?.` alone would let an
44
+ * auth-relevant warning disappear silently on such a logger. Both call
45
+ * sites in this plugin route through here instead of duplicating the
46
+ * fallback branch.
47
+ *
48
+ * `message` must be the plain, uncolored text — it becomes the OTel log
49
+ * body when `warnStructured` is available, and ANSI escapes have no place
50
+ * in a structured body. Coloring is applied only in the console-facing
51
+ * fallback branch below.
52
+ */
53
+ function warnStructured(
54
+ logger: Logger,
55
+ message: string,
56
+ attributes: Record<string, string | number | boolean>,
57
+ ): void {
58
+ if (logger.warnStructured) {
59
+ logger.warnStructured(message, attributes);
60
+ } else {
61
+ logger.warn(yellow(message));
62
+ }
63
+ }
64
+
38
65
  /**
39
66
  * Vite plugin that generates a build manifest for the application.
40
67
  *
@@ -108,10 +135,53 @@ export function generateApiBuildManifestPlugin(root: string) {
108
135
  );
109
136
 
110
137
  const sdkApis = sdkApiEnabled
111
- ? await collectSdkApisFromRegistry(root, {
112
- pathExists: (p) => fs.pathExists(p),
113
- readFile: (p, enc) => fs.readFile(p, enc),
114
- })
138
+ ? await collectSdkApisFromRegistry(
139
+ root,
140
+ {
141
+ pathExists: (p) => fs.pathExists(p),
142
+ readFile: (p, enc) => fs.readFile(p, enc),
143
+ realpath: (p) => fs.realpath(p),
144
+ },
145
+ {
146
+ onUnresolvedIntegrations: (
147
+ apiName,
148
+ entryPoint,
149
+ unresolvedKeys,
150
+ ) => {
151
+ warnStructured(
152
+ logger,
153
+ `[superblocks] Could not resolve integration(s) ${unresolvedKeys
154
+ .map((k) => `"${k}"`)
155
+ .join(
156
+ ", ",
157
+ )} declared in ${entryPoint} (API "${apiName}") — check-auth may be skipped for this API in deployed/preview mode. Declare the integration or its ID in the same file.`,
158
+ {
159
+ // Bounded/numeric only — api name, entry point, and the
160
+ // unresolved keys themselves are free-form text and stay
161
+ // in the message body (searchable, not faceted) to keep
162
+ // OTel log attribute cardinality low. See the
163
+ // `LogAttributes` doc comment in ./telemetry/logging.ts.
164
+ "sdk.integrations.unresolved_count": unresolvedKeys.length,
165
+ },
166
+ );
167
+ },
168
+ onExtractionError: (apiName, entryPoint, error) => {
169
+ const errorMessage =
170
+ error instanceof Error ? error.message : String(error);
171
+ warnStructured(
172
+ logger,
173
+ `[superblocks] Failed to extract integrations from ${entryPoint} (API "${apiName}"): ${errorMessage} — check-auth may be skipped for this API in deployed/preview mode.`,
174
+ {
175
+ // Bounded constant only — api name, entry point, and the
176
+ // raw error message are free-form text and stay in the
177
+ // message body. See the `LogAttributes` doc comment in
178
+ // ./telemetry/logging.ts.
179
+ "sdk.extraction.error_category": "read_or_extract_failed",
180
+ },
181
+ );
182
+ },
183
+ },
184
+ )
115
185
  : {};
116
186
 
117
187
  manifestCode = `export default ${JSON.stringify({