@ashlr/stack-core 0.1.1 → 0.2.2

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 (101) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +26 -15
  3. package/package.json +1 -1
  4. package/src/__tests__/_harness.ts +67 -7
  5. package/src/__tests__/agent-onboard.test.ts +217 -0
  6. package/src/__tests__/api-key-providers.test.ts +6 -2
  7. package/src/__tests__/base-urls.test.ts +162 -0
  8. package/src/__tests__/catalog-index.test.ts +14 -20
  9. package/src/__tests__/cloudflare-provider.test.ts +9 -10
  10. package/src/__tests__/completion-ci.test.ts +239 -23
  11. package/src/__tests__/config-own-services.test.ts +183 -0
  12. package/src/__tests__/config-split.test.ts +206 -2
  13. package/src/__tests__/config.test.ts +2 -1
  14. package/src/__tests__/credential-staging.test.ts +659 -0
  15. package/src/__tests__/deps.test.ts +2 -1
  16. package/src/__tests__/detect-source.test.ts +1 -1
  17. package/src/__tests__/github-provider.test.ts +3 -2
  18. package/src/__tests__/helpers.test.ts +24 -4
  19. package/src/__tests__/http.test.ts +2 -2
  20. package/src/__tests__/inference.test.ts +19 -13
  21. package/src/__tests__/interactive-prompt.test.ts +106 -0
  22. package/src/__tests__/mcp-mirror.test.ts +116 -0
  23. package/src/__tests__/neon-provider.test.ts +136 -3
  24. package/src/__tests__/new-providers.test.ts +470 -0
  25. package/src/__tests__/oauth-port.test.ts +84 -0
  26. package/src/__tests__/openai-provider.test.ts +1 -1
  27. package/src/__tests__/phantom-contract.test.ts +177 -0
  28. package/src/__tests__/phantom-harness.test.ts +125 -8
  29. package/src/__tests__/phantom-project-preflight.test.ts +118 -0
  30. package/src/__tests__/phantom-wire.test.ts +31 -20
  31. package/src/__tests__/pipeline-integration.test.ts +1 -1
  32. package/src/__tests__/pipeline-recovery.test.ts +29 -28
  33. package/src/__tests__/pipeline-rollback.test.ts +509 -0
  34. package/src/__tests__/pipeline.test.ts +3 -1
  35. package/src/__tests__/pm-detect.test.ts +113 -0
  36. package/src/__tests__/provider-deprovision.test.ts +269 -0
  37. package/src/__tests__/provider-parity.test.ts +46 -0
  38. package/src/__tests__/publish-script.test.ts +659 -0
  39. package/src/__tests__/recipe.test.ts +4 -2
  40. package/src/__tests__/recommend-apply-e2e.test.ts +14 -27
  41. package/src/__tests__/registry.test.ts +11 -2
  42. package/src/__tests__/release-artifacts.test.ts +55 -0
  43. package/src/__tests__/secret-redaction.test.ts +10 -10
  44. package/src/__tests__/security.test.ts +9 -10
  45. package/src/__tests__/supabase-provider.test.ts +6 -4
  46. package/src/__tests__/swap.test.ts +114 -0
  47. package/src/__tests__/telemetry.test.ts +354 -0
  48. package/src/__tests__/template-litellm-gateway.test.ts +41 -0
  49. package/src/__tests__/template-temporal-worker-phantom.test.ts +31 -0
  50. package/src/__tests__/templates.test.ts +1 -1
  51. package/src/__tests__/turso-provider.test.ts +2 -5
  52. package/src/agent-onboard.ts +346 -0
  53. package/src/ai/catalog-index.ts +68 -10
  54. package/src/ai/circuit-breaker.ts +3 -6
  55. package/src/ai/cost-tracker.ts +6 -15
  56. package/src/ai/inference.ts +19 -11
  57. package/src/ai/phantom-wire.ts +29 -52
  58. package/src/ai/recipe.ts +69 -0
  59. package/src/base-urls.ts +71 -0
  60. package/src/catalog.ts +281 -2
  61. package/src/config.ts +31 -18
  62. package/src/detect-source.ts +36 -8
  63. package/src/detect.ts +13 -5
  64. package/src/http.ts +6 -5
  65. package/src/index.ts +15 -0
  66. package/src/mcp-writer.ts +97 -2
  67. package/src/oauth.ts +85 -53
  68. package/src/phantom.ts +193 -15
  69. package/src/pipeline.ts +177 -68
  70. package/src/pm-detect.ts +56 -0
  71. package/src/providers/_api-key.ts +18 -17
  72. package/src/providers/_base.ts +28 -1
  73. package/src/providers/_credential-stage.ts +237 -0
  74. package/src/providers/_helpers.ts +55 -9
  75. package/src/providers/anthropic.ts +2 -1
  76. package/src/providers/auth0.ts +27 -0
  77. package/src/providers/aws.ts +32 -33
  78. package/src/providers/cloudflare.ts +18 -37
  79. package/src/providers/convex.ts +1 -2
  80. package/src/providers/datadog.ts +29 -0
  81. package/src/providers/digitalocean.ts +30 -0
  82. package/src/providers/gcp.ts +36 -0
  83. package/src/providers/github.ts +37 -35
  84. package/src/providers/grafana.ts +29 -0
  85. package/src/providers/hetzner.ts +29 -0
  86. package/src/providers/index.ts +13 -1
  87. package/src/providers/launchdarkly.ts +33 -0
  88. package/src/providers/mixpanel.ts +37 -0
  89. package/src/providers/neon.ts +50 -26
  90. package/src/providers/openai.ts +2 -1
  91. package/src/providers/plausible.ts +26 -0
  92. package/src/providers/sentry.ts +17 -17
  93. package/src/providers/stripe.ts +1 -2
  94. package/src/providers/supabase.ts +66 -37
  95. package/src/providers/turso.ts +49 -38
  96. package/src/providers/vercel.ts +50 -20
  97. package/src/providers/workos.ts +27 -0
  98. package/src/registry.ts +7 -11
  99. package/src/swap.ts +108 -0
  100. package/src/telemetry.ts +267 -0
  101. package/src/templates.ts +6 -2
@@ -1,7 +1,8 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { mkdtempSync, readFileSync, writeFileSync, existsSync } from "node:fs";
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { parse as parseToml } from "smol-toml";
5
6
  import {
6
7
  CONFIG_FILENAME,
7
8
  LOCAL_FILENAME,
@@ -22,7 +23,8 @@ describe("committed + local config split", () => {
22
23
  });
23
24
 
24
25
  afterEach(() => {
25
- if (previousRegistryDir === undefined) delete process.env.STACK_REGISTRY_DIR;
26
+ if (previousRegistryDir === undefined)
27
+ Reflect.deleteProperty(process.env, "STACK_REGISTRY_DIR");
26
28
  else process.env.STACK_REGISTRY_DIR = previousRegistryDir;
27
29
  });
28
30
 
@@ -136,3 +138,205 @@ default = true
136
138
  expect(shape).toContain("OPENAI_API_KEY");
137
139
  });
138
140
  });
141
+
142
+ describe("public config reference matches produced data", () => {
143
+ let cwd: string;
144
+ let registry: string;
145
+ let previousRegistryDir: string | undefined;
146
+
147
+ beforeEach(() => {
148
+ cwd = mkdtempSync(join(tmpdir(), "stack-schema-config-"));
149
+ registry = mkdtempSync(join(tmpdir(), "stack-schema-registry-"));
150
+ previousRegistryDir = process.env.STACK_REGISTRY_DIR;
151
+ process.env.STACK_REGISTRY_DIR = registry;
152
+ });
153
+
154
+ afterEach(() => {
155
+ if (previousRegistryDir === undefined)
156
+ Reflect.deleteProperty(process.env, "STACK_REGISTRY_DIR");
157
+ else process.env.STACK_REGISTRY_DIR = previousRegistryDir;
158
+ rmSync(cwd, { recursive: true, force: true });
159
+ rmSync(registry, { recursive: true, force: true });
160
+ });
161
+
162
+ function reference() {
163
+ return JSON.parse(
164
+ readFileSync(new URL("../../../site/public/openapi.json", import.meta.url), "utf-8"),
165
+ ).components.schemas;
166
+ }
167
+
168
+ // These are field-coverage checks on actual serialized/merged values, not a
169
+ // JSON Schema validator or a claim that readConfig validates arbitrary input.
170
+ function expectDocumentedFields(
171
+ value: object,
172
+ schema: { properties: object; required?: string[] },
173
+ ) {
174
+ for (const key of Object.keys(value)) expect(schema.properties).toHaveProperty(key);
175
+ for (const key of schema.required ?? []) expect(value).toHaveProperty(key);
176
+ }
177
+
178
+ function expectTable(value: unknown): asserts value is Record<string, unknown> {
179
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
180
+ throw new Error("Expected a parsed TOML table in the synthetic config fixture.");
181
+ }
182
+ }
183
+
184
+ test("StackToml describes the parsed committed output of a blank write", async () => {
185
+ await writeConfig(emptyConfig(), cwd);
186
+ const shape = parseToml(readFileSync(join(cwd, CONFIG_FILENAME), "utf-8"));
187
+ const schema = reference().StackToml;
188
+
189
+ expectDocumentedFields(shape, schema);
190
+ expect(Object.keys(shape)).toEqual(["stack", "services", "environments"]);
191
+ expect(schema.required).toEqual(Object.keys(shape));
192
+ expect(schema.properties.stack.properties.version.type).toBe("string");
193
+ expect(schema.properties).not.toHaveProperty("version");
194
+ expect(schema.properties).not.toHaveProperty("project");
195
+ expect(shape.stack).toEqual({ version: "1" });
196
+ expect(shape.services).toEqual({});
197
+ expect(shape.environments).toEqual([{ name: "dev", default: true }]);
198
+ });
199
+
200
+ test("committed service fields stay separate from the full merged ServiceEntry", async () => {
201
+ const config = emptyConfig("fixture-template");
202
+ const metadata = { note: "synthetic metadata", nested: { enabled: true } };
203
+ config.services.primary = {
204
+ provider: "supabase",
205
+ resource_id: "fixture-resource",
206
+ region: "fixture-region",
207
+ secrets: ["FIXTURE_URL"],
208
+ mcp: "fixture-mcp",
209
+ meta: metadata,
210
+ created_at: "fixture-time",
211
+ created_by: "fixture-owner",
212
+ };
213
+ await writeConfig(config, cwd);
214
+ const shape = parseToml(readFileSync(join(cwd, CONFIG_FILENAME), "utf-8"));
215
+ const local = parseToml(readFileSync(join(cwd, LOCAL_FILENAME), "utf-8"));
216
+ const merged = await readConfig(cwd);
217
+ const schemas = reference();
218
+ const committed = schemas.StackToml.properties.services.additionalProperties;
219
+ expectTable(shape.services);
220
+ expectTable(shape.services.primary);
221
+
222
+ expect(committed).not.toHaveProperty("$ref");
223
+ expectDocumentedFields(shape.services.primary, committed);
224
+ expect(committed.required).toEqual(["provider", "secrets"]);
225
+ expect(Object.keys(committed.properties)).toEqual(["provider", "secrets", "mcp"]);
226
+ expect(shape.services).toEqual({
227
+ primary: { provider: "supabase", secrets: ["FIXTURE_URL"], mcp: "fixture-mcp" },
228
+ });
229
+ expect(local.stack).toEqual({ project_id: config.stack.project_id });
230
+ expect(local.services).toEqual({
231
+ primary: {
232
+ resource_id: "fixture-resource",
233
+ region: "fixture-region",
234
+ meta: metadata,
235
+ created_at: "fixture-time",
236
+ created_by: "fixture-owner",
237
+ },
238
+ });
239
+ expect(merged.services.primary).toEqual(config.services.primary);
240
+ expectDocumentedFields(merged.services.primary, schemas.ServiceEntry);
241
+ expect(schemas.ServiceEntry.required).toEqual(["provider", "secrets", "created_at"]);
242
+ expect(schemas.ServiceEntry.properties.created_at).not.toHaveProperty("format");
243
+ expect(schemas.ServiceEntry.properties.meta.type).toBe("object");
244
+ expect(schemas.ServiceEntry.properties.secrets.items.type).toBe("string");
245
+ expectDocumentedFields(merged.stack, {
246
+ properties: { version: {}, project_id: {}, template: {} },
247
+ });
248
+ });
249
+
250
+ test("merged entry documentation covers actual instances independently of the committed subset", async () => {
251
+ const config = emptyConfig();
252
+ config.services.custom_slot = {
253
+ provider: "neon",
254
+ secrets: [],
255
+ resource_id: "opaque-fixture-id",
256
+ meta: { arbitrary: "not a credential fixture" },
257
+ created_at: "not-a-date-validator",
258
+ };
259
+ await writeConfig(config, cwd);
260
+ const merged = await readConfig(cwd);
261
+ const schema = reference().ServiceEntry;
262
+
263
+ expectDocumentedFields(merged.services.custom_slot, schema);
264
+ expect(schema.properties.resource_id.type).toBe("string");
265
+ expect(schema.properties.meta.type).toBe("object");
266
+ expect(schema.properties.created_at.type).toBe("string");
267
+ expect(merged.services.custom_slot.secrets).toEqual([]);
268
+ expect(merged.services.custom_slot.mcp).toBeUndefined();
269
+ });
270
+
271
+ test("the writer preserves other version strings and an empty environments array", async () => {
272
+ const config = emptyConfig();
273
+ config.stack.version = "fixture-future-version";
274
+ config.environments = [];
275
+ config.services.first = { provider: "neon", secrets: [], created_at: "fixture" };
276
+ config.services.second = { provider: "neon", secrets: [], created_at: "fixture" };
277
+ await writeConfig(config, cwd);
278
+ const shape = parseToml(readFileSync(join(cwd, CONFIG_FILENAME), "utf-8"));
279
+ const schema = reference().StackToml;
280
+
281
+ expect(shape.stack).toEqual({ version: "fixture-future-version" });
282
+ expect(shape.environments).toEqual([]);
283
+ expect(shape.services).toEqual({
284
+ first: { provider: "neon", secrets: [] },
285
+ second: { provider: "neon", secrets: [] },
286
+ });
287
+ expect(schema.properties.stack.properties.version.type).toBe(typeof config.stack.version);
288
+ expect(schema.properties.stack.properties.version).not.toHaveProperty("const");
289
+ expect(schema.properties.stack.properties.version).not.toHaveProperty("enum");
290
+ expect(schema.properties.environments.type).toBe("array");
291
+ expect(schema.properties.environments).not.toHaveProperty("minItems");
292
+ expect((await readConfig(cwd)).environments).toEqual([{ name: "dev", default: true }]);
293
+ });
294
+
295
+ test("environment documentation covers actual overrides without enforcing one default", async () => {
296
+ const config = emptyConfig();
297
+ config.environments = [
298
+ { name: "one", default: true, overrides: { FIXTURE_URL: "_ONE" } },
299
+ { name: "two", default: true },
300
+ ];
301
+ await writeConfig(config, cwd);
302
+ const merged = await readConfig(cwd);
303
+ const schema = reference().StackToml.properties.environments.items;
304
+
305
+ expect(merged.environments).toEqual(config.environments);
306
+ for (const entry of merged.environments) expectDocumentedFields(entry, schema);
307
+ expect(schema.required).toEqual(["name"]);
308
+ expect(schema.properties.name.type).toBe("string");
309
+ expect(schema.properties.default.type).toBe("boolean");
310
+ expect(schema.properties.overrides.additionalProperties.type).toBe("string");
311
+ });
312
+
313
+ test("normal defaults and legacy precedence remain distinct from canonical file documentation", async () => {
314
+ writeFileSync(
315
+ join(cwd, CONFIG_FILENAME),
316
+ '[stack]\nversion = "fixture"\n[services.item]\nprovider = "neon"\nsecrets = []\nregion = "ignored-shape-region"\n',
317
+ );
318
+ writeFileSync(
319
+ join(cwd, LOCAL_FILENAME),
320
+ '[stack]\nproject_id = "fixture-local"\n[services.item]\nregion = "local-region"\n',
321
+ );
322
+ const normal = await readConfig(cwd);
323
+ expect(normal.stack.project_id).toBe("fixture-local");
324
+ expect(normal.services.item.region).toBe("local-region");
325
+ expect(normal.services.item.created_at).toBe("1970-01-01T00:00:00Z");
326
+ expect(normal.environments).toEqual([{ name: "dev", default: true }]);
327
+
328
+ writeFileSync(
329
+ join(cwd, CONFIG_FILENAME),
330
+ 'environments = []\n[stack]\nversion = "fixture"\nproject_id = "fixture-legacy"\n[services.item]\nresource_id = "legacy-resource"\n',
331
+ );
332
+ const legacy = await readConfig(cwd);
333
+ expect(legacy.stack.project_id).toBe("fixture-legacy");
334
+ expect(legacy.services.item.provider).toBe("item");
335
+ expect(legacy.services.item.secrets).toEqual([]);
336
+ expect(legacy.services.item.resource_id).toBe("legacy-resource");
337
+ expect(legacy.services.item.region).toBeUndefined();
338
+ expect(legacy.environments).toEqual([]);
339
+ expect(legacy.services.item.created_at).not.toBe("1970-01-01T00:00:00Z");
340
+ expect(readFileSync(join(cwd, LOCAL_FILENAME), "utf-8")).toContain("fixture-local");
341
+ });
342
+ });
@@ -24,7 +24,8 @@ describe("config", () => {
24
24
  });
25
25
 
26
26
  afterEach(() => {
27
- if (previousRegistryDir === undefined) delete process.env.STACK_REGISTRY_DIR;
27
+ if (previousRegistryDir === undefined)
28
+ Reflect.deleteProperty(process.env, "STACK_REGISTRY_DIR");
28
29
  else process.env.STACK_REGISTRY_DIR = previousRegistryDir;
29
30
  });
30
31