@slip-stream-kit/config 0.39.0 → 0.40.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.
@@ -1,4 +1,4 @@
1
- export { DEPLOY_CONFIG_FILE, deployConfigSchema } from '../lib/deploy/deploy-config-schema';
1
+ export { DEPLOY_CONFIG_FILE, deployConfigSchema, serviceDependencies } from '../lib/deploy/deploy-config-schema';
2
2
  export type { DeployConfig, DeployService } from '../lib/deploy/deploy-config-schema';
3
3
  export { packageConfigSchema } from '../lib/package-config/package-config-schema';
4
4
  export { DEV_SERVING_MARKER, E2E_MODE_ENV } from '../lib/playwright/playwright';
package/dist/index.js CHANGED
@@ -26,8 +26,64 @@ var deployServiceSchema = z.strictObject({
26
26
  * `false` leaves the service out of the deploy-all workflow; a list keeps it in deploy-all for those
27
27
  * environments only, e.g. `['prod']`. Either way it stays selectable wherever it may deploy.
28
28
  */
29
- deployAll: z.union([z.boolean(), z.array(kebabName).nonempty()]).optional()
29
+ deployAll: z.union([z.boolean(), z.array(kebabName).nonempty()]).optional(),
30
+ /**
31
+ * Services this one waits for in the same run, e.g. two services that redeploy one API Gateway stage.
32
+ * A dependency that fails stops it; one that was not selected or not offered here does not.
33
+ */
34
+ after: z.array(kebabName).nonempty().optional(),
35
+ /**
36
+ * Makes the service a step rather than a checkbox, e.g. DB migrations: it runs whenever one of these
37
+ * services deploys, before them, and its failure stops them.
38
+ */
39
+ runsBefore: z.array(kebabName).nonempty().optional()
30
40
  });
41
+ var serviceDependencies = (services) => {
42
+ const dependencies = new Map(
43
+ Object.entries(services).map(([name, service]) => {
44
+ return [name, [...service.after ?? []]];
45
+ })
46
+ );
47
+ for (const [step, service] of Object.entries(services)) {
48
+ for (const target of service.runsBefore ?? []) dependencies.get(target)?.push(step);
49
+ }
50
+ return dependencies;
51
+ };
52
+ var checkServiceOrder = (services, ctx) => {
53
+ for (const [name, service] of Object.entries(services)) {
54
+ for (const key of ["after", "runsBefore"]) {
55
+ for (const [index, other] of (service[key] ?? []).entries()) {
56
+ if (other === name) {
57
+ ctx.addIssue({ code: "custom", path: ["services", name, key, index], message: "names the service itself" });
58
+ } else if (!(other in services)) {
59
+ ctx.addIssue({ code: "custom", path: ["services", name, key, index], message: `no service '${other}'` });
60
+ }
61
+ }
62
+ }
63
+ if (service.runsBefore !== void 0 && service.deployAll !== void 0) {
64
+ ctx.addIssue({
65
+ code: "custom",
66
+ path: ["services", name],
67
+ message: "a runsBefore step deploys with its services, so deployAll does not apply"
68
+ });
69
+ }
70
+ }
71
+ const dependencies = serviceDependencies(services);
72
+ const done = /* @__PURE__ */ new Set();
73
+ const visiting = /* @__PURE__ */ new Set();
74
+ const visit = (name, chain) => {
75
+ if (done.has(name)) return;
76
+ if (visiting.has(name)) {
77
+ ctx.addIssue({ code: "custom", path: ["services"], message: `order cycle: ${[...chain, name].join(" \u2192 ")}` });
78
+ return;
79
+ }
80
+ visiting.add(name);
81
+ for (const dependency of dependencies.get(name) ?? []) visit(dependency, [...chain, name]);
82
+ visiting.delete(name);
83
+ done.add(name);
84
+ };
85
+ for (const name of dependencies.keys()) visit(name, []);
86
+ };
31
87
  var deployConfigSchema = z.strictObject({
32
88
  /**
33
89
  * The `<project>` in this repo's `/<project>/...` SSM parameters, e.g. `hulyo`, so a deploy script
@@ -63,7 +119,15 @@ var deployConfigSchema = z.strictObject({
63
119
  * which the CLI ignores. A repo that delivers prod needs `prod` in `environments` first: delivery
64
120
  * dispatches `deploy-all.yml` with it.
65
121
  */
66
- replaceV1: z.boolean().optional()
122
+ replaceV1: z.boolean().optional(),
123
+ /**
124
+ * The deploy job reads its environment from this Doppler project, config = the dispatched environment,
125
+ * with the token in the `<env>_DOPPLER_TOKEN` secret: AWS keys (and an `AWS_DEPLOY_ROLE_ARN` to
126
+ * assume), Slack and every secret the deploy scripts read. No AWS secrets are passed then.
127
+ */
128
+ doppler: z.strictObject({
129
+ project: z.string().regex(/^[a-z0-9][a-z0-9_-]*$/, "must be a Doppler project name, e.g. nomadream")
130
+ }).optional()
67
131
  }).superRefine((config, ctx) => {
68
132
  if (Object.keys(config.services).length === 0) {
69
133
  ctx.addIssue({ code: "custom", path: ["services"], message: "declare at least one service" });
@@ -79,6 +143,7 @@ var deployConfigSchema = z.strictObject({
79
143
  ctx.addIssue({ code: "custom", path: ["services", name], message: "set envs or skipEnvs, not both" });
80
144
  }
81
145
  }
146
+ checkServiceOrder(config.services, ctx);
82
147
  for (const [index, file] of (config.preDeploy ?? []).entries()) {
83
148
  if (config.terraform?.includes(file)) {
84
149
  ctx.addIssue({ code: "custom", path: ["preDeploy", index], message: "already listed under terraform" });
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/lib/deploy/deploy-config-schema.ts", "../src/lib/package-config/package-config.ts", "../src/lib/vendor/config-schema.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\n\n/**\n * Pure (node-free) schema for a consumer repo's deploy manifest. `infra-kit deploy-sync` reads it and\n * generates the repo's deploy workflows from it, so CI runs plain YAML and never needs the CLI.\n *\n * A separate file rather than a `deploy` key in `infra-kit.json` / `infra-kit.config.ts`: both of those\n * are strict schemas parsed by every installed CLI and by the vite plugin, so a new key would break\n * every teammate and CI build still on an older release. Nothing but `deploy-sync` reads this file.\n */\nexport const DEPLOY_CONFIG_FILE = 'infra-kit.deploy.ts'\n\n/** Environment and service names end up in job ids, input keys and secret names. */\nconst kebabName = z.string().regex(/^[a-z][a-z0-9-]*$/, 'must be lower-case kebab-case, e.g. client-be')\n\n/** A reusable workflow in the consumer's own `.github/workflows/`, by file name. */\nconst localWorkflowFile = z\n .string()\n .regex(/^[\\w.-]+\\.ya?ml$/, 'must be a workflow file name in .github/workflows, e.g. _deploy-media-jobs.yml')\n\nconst secretName = z.string().regex(/^[A-Z_][A-Z0-9_]*$/, 'must be an upper-case secret name, e.g. MONITORING_IP')\n\n/** Service names become dispatch inputs, so they must not shadow the ones every workflow has. */\nconst RESERVED_SERVICE_NAMES = new Set(['environment'])\n\nexport const deployServiceSchema = z.strictObject({\n /** The only environments this service deploys to. Omitted = every environment. */\n envs: z.array(kebabName).nonempty().optional(),\n /** Environments this service never deploys to; every other one is allowed. Exclusive with `envs`. */\n skipEnvs: z.array(kebabName).nonempty().optional(),\n /** GitHub runner label. Omitted = the shared job's default (`ubuntu-24.04-arm`). */\n runner: z\n .string()\n .regex(/^[\\w.-]+$/, 'must be a runner label, e.g. macos-14')\n .optional(),\n /**\n * Deploy through this repo's own reusable workflow instead of `devops/scripts/deploy-<service>.sh`\n * in the shared job. It is called with `branch` and `environment` inputs and `secrets: inherit`.\n */\n workflow: localWorkflowFile.optional(),\n /**\n * The service's TypeScript deploy, run by `infra-kit deploy-run`, as a repo-relative path; put it in\n * the app it deploys (`apps/client/api/deploy.ts`) and it defaults to that app. Omitted =\n * `devops/scripts/deploy-<service>.sh`.\n */\n deploy: z\n .string()\n .regex(/^(?!.*\\.\\.)[\\w./-]+\\.ts$/, 'must be a repo-relative .ts path, e.g. apps/client/api/deploy.ts')\n .optional(),\n /**\n * `false` leaves the service out of the deploy-all workflow; a list keeps it in deploy-all for those\n * environments only, e.g. `['prod']`. Either way it stays selectable wherever it may deploy.\n */\n deployAll: z.union([z.boolean(), z.array(kebabName).nonempty()]).optional(),\n})\n\nexport const deployConfigSchema = z\n .strictObject({\n /**\n * The `<project>` in this repo's `/<project>/...` SSM parameters, e.g. `hulyo`, so a deploy script\n * shared across repos reads its parameters without hard-coding the prefix.\n */\n project: z\n .string()\n .regex(/^[a-z0-9][a-z0-9-]*$/, 'must be lower-case letters, digits and dashes, e.g. hulyo')\n .optional(),\n /** The environments the generated workflows offer, in dispatch-menu order. */\n environments: z.array(kebabName).nonempty(),\n /** Preselected in the dispatch menu. Omitted = the first environment. */\n defaultEnvironment: kebabName.optional(),\n /** Every deployable service, keyed by its `devops/scripts/deploy-<name>.sh` suffix, in menu order. */\n services: z.record(kebabName, deployServiceSchema),\n /** Reusable workflows applied before any service deploys; skippable per run. */\n terraform: z.array(localWorkflowFile).optional(),\n /**\n * Reusable workflows run after terraform (applied or skipped) and before any service, e.g. publishing\n * a Lambda layer the services resolve at deploy time. Not skipped with terraform; a failure stops every service.\n */\n preDeploy: z.array(localWorkflowFile).optional(),\n /** Reusable workflow called once every deploy job finished without a failure. */\n notifySuccess: localWorkflowFile.optional(),\n /** Repo secrets exported, under the same name, to every deploy script. */\n extraEnv: z.array(secretName).optional(),\n /**\n * Minutes the deploy step of the shared job may run. Omitted = the shared job's default, 25. A\n * timed-out step fails and keeps its logs, where a job cancelled at GitHub's 360-minute cap loses them.\n */\n deployTimeoutMinutes: z.number().int().min(1).max(360).optional(),\n /**\n * Generate under the v1 names, `deploy-all.yml` and `deploy-selected-services.yml`, replacing the\n * hand-written files. Those are the files `infra-kit release deploy-*` dispatches, so the manifest\n * becomes what the CLI deploys and reads its service gates from. Omitted = the `-v2` pilot files,\n * which the CLI ignores. A repo that delivers prod needs `prod` in `environments` first: delivery\n * dispatches `deploy-all.yml` with it.\n */\n replaceV1: z.boolean().optional(),\n })\n .superRefine((config, ctx) => {\n if (Object.keys(config.services).length === 0) {\n ctx.addIssue({ code: 'custom', path: ['services'], message: 'declare at least one service' })\n }\n\n for (const [name, service] of Object.entries(config.services)) {\n if (RESERVED_SERVICE_NAMES.has(name)) {\n ctx.addIssue({ code: 'custom', path: ['services', name], message: 'is a dispatch input of its own' })\n }\n\n if (service.workflow !== undefined && service.deploy !== undefined) {\n ctx.addIssue({ code: 'custom', path: ['services', name], message: 'set workflow or deploy, not both' })\n }\n\n if (service.envs !== undefined && service.skipEnvs !== undefined) {\n ctx.addIssue({ code: 'custom', path: ['services', name], message: 'set envs or skipEnvs, not both' })\n }\n }\n\n for (const [index, file] of (config.preDeploy ?? []).entries()) {\n if (config.terraform?.includes(file)) {\n ctx.addIssue({ code: 'custom', path: ['preDeploy', index], message: 'already listed under terraform' })\n }\n }\n\n if (config.defaultEnvironment !== undefined && !config.environments.includes(config.defaultEnvironment)) {\n ctx.addIssue({\n code: 'custom',\n path: ['defaultEnvironment'],\n message: `must be one of environments (${config.environments.join(', ')})`,\n })\n }\n })\n\nexport type DeployService = z.infer<typeof deployServiceSchema>\nexport type DeployConfig = z.infer<typeof deployConfigSchema>\n\n/**\n * Identity helper for authoring a type-safe `infra-kit.deploy.ts`.\n *\n * A manifest can equally use `import type { DeployConfig }` with `satisfies DeployConfig`: the type\n * import is erased when the CLI loads the file, so it keeps working against an older installed\n * `@slip-stream-kit/config` that predates this helper.\n *\n * @example\n * export default defineDeployConfig({\n * environments: ['dev', 'stage'],\n * services: { 'client-be': {}, 'client-fe': { runner: 'macos-14' } },\n * })\n */\nexport const defineDeployConfig = (config: DeployConfig): DeployConfig => {\n return config\n}\n", "/**\n * Validation rules for a single workspace package, declared in its\n * `infra-kit.config.ts`. Every field is optional: a key left unset falls back to\n * the active baseline, and a key set replaces that default wholesale (per-key, no\n * array concatenation) so a package can opt out with an explicit empty array.\n *\n * The baselines themselves (`DEFAULT_RULES` / `ROOT_DEFAULT_RULES`) deliberately do\n * NOT live here \u2014 they are audit POLICY, and policy belongs to the `infra-kit` CLI\n * that enforces it, not to the package a consumer installs to author a config.\n *\n * Most packages need none of these \u2014 the standard rules live in the baseline, so\n * a typical config is just `defineConfig(() => ({}))`.\n *\n * @example\n * // infra-kit.config.ts\n * import { defineConfig } from '@slip-stream-kit/config'\n *\n * export default defineConfig(() => ({}))\n */\nexport interface InfraKitPackageConfig {\n /** Scripts that must be present in the package's package.json `scripts` map. */\n requiredScripts?: string[]\n /** Files (relative to the package root) that must exist on disk. */\n requiredFiles?: string[]\n /** Turborepo expectations \u2014 only meaningful where a turbo.json lives (the root). */\n turbo?: {\n /** Tasks that must be defined in turbo.json `tasks`. */\n requiredTasks?: string[]\n }\n /**\n * The env var holding this app part's deployed URL, e.g. `CLIENT_URL`. The value lives in Doppler, per\n * environment \u2014 prod's host is not `<env>`-shaped, so no template in code can name it. The dev proxy sends\n * a UI's cloud routes there, and e2e runs against it in cloud mode. Required on a UI whose `dev.proxy`\n * has a route that can go to cloud, and on any `e2e.target`.\n */\n deployedUrlEnv?: string\n /** Local-dev configuration. Accepted-and-inert to the audit; consumed by the dev server. */\n dev?: InfraKitDev\n /** Where this e2e package's suite runs. Consumed by `infraKitE2e()` and the CLI; `server: 'static'` also narrows the audit. */\n e2e?: InfraKitE2e\n /**\n * Explicit override for infra-kit's directory/dependency-based package-type detection (used to\n * pick the agent guidance rendered into this package's `CLAUDE.md`). Detection already covers\n * both consumer repos untouched, so this is an escape hatch for the cases it gets wrong \u2014 leaving\n * it unset is never an error.\n *\n * **Ordering precondition:** the CLI's own pinned `packageConfigSchema` \u2014 not this package's\n * version \u2014 governs what a consumer's `infra-kit.config.ts` may contain, and it is a\n * `z.strictObject`. Only write `type` here once the CLI in use is a version that carries this\n * key; an older CLI rejects the key outright and skips the rest of that package's audit checks.\n *\n * @example\n * // infra-kit.config.ts\n * export default defineConfig(() => ({ type: 'frontend' }))\n */\n type?: InfraKitPackageType\n}\n\n/** Package kinds infra-kit's agent guidance renders type-specific rules for. */\nexport type InfraKitPackageType = 'frontend' | 'backend' | 'lib' | 'e2e' | 'mobile'\n\n/** A proxy route's allowed backend source. */\nexport type InfraKitDevProxySource = 'local' | 'cloud'\n\nexport interface InfraKitDevProxyRoute {\n /** Backend package this route targets when resolved locally. */\n packageName: string\n /** Capabilities this route can resolve to. Must be non-empty. */\n from: InfraKitDevProxySource[]\n /**\n * Source used when a local backend for this package isn't active. Required when\n * `from` lists more than one source; redundant (and omitted) for a single-source\n * route. When set, must be one of `from`.\n */\n default?: InfraKitDevProxySource\n}\n\nexport interface InfraKitDevProxy {\n /**\n * The local backend's URL template \u2014 `<release>`/`<packageName>` are substituted at dev time. A cloud route\n * goes to the package's `deployedUrlEnv` instead. Consulted only for a fragment from a pre-v2 CLI: a\n * current one publishes the exact `origin` (`<release>.<packageName>.<repo>.localhost`) it registered.\n */\n templates: {\n local: string\n }\n /** Path-prefix (e.g. `/api`, `/api/v1`, `/media`) \u2192 route definition. */\n routes: Record<string, InfraKitDevProxyRoute>\n}\n\nexport interface InfraKitDev {\n proxy?: InfraKitDevProxy\n env?: InfraKitDevEnv\n}\n\n/**\n * Env overrides for this package's dev server only, applied over the loaded (Doppler) env when vite starts\n * it \u2014 never on `build`. For variables another build mode needs that break local dev, e.g. a mobile-only\n * absolute origin that sends requests around the dev proxy.\n *\n * @example\n * // apps/client/ui/infra-kit.config.ts\n * export default defineConfig(() => ({ dev: { env: { unset: ['VITE_ORIGIN_DOMAIN'] } } }))\n */\nexport interface InfraKitDevEnv {\n /** Variables removed from the dev server's env. */\n unset?: string[]\n /** Variables set (or overridden) in the dev server's env. A name cannot also be in `unset`. */\n set?: Record<string, string>\n}\n\n/**\n * Where an e2e package's suite runs. With `target`, `infraKitE2e()` (`@slip-stream-kit/config/playwright`)\n * reads it: a run is local by default \u2014 `target` on this worktree, served by `infra-kit dev <app>` \u2014 and\n * cloud only when asked (`INFRA_KIT_E2E=cloud`, or CI), against the URL in the target package's `deployedUrlEnv`.\n * With `server: 'static'`, see {@link InfraKitE2eStatic}.\n *\n * @example\n * // apps/client/tests/infra-kit.config.ts\n * export default defineConfig(() => ({ e2e: { target: 'client/ui' } }))\n */\nexport type InfraKitE2e = InfraKitE2eTarget | InfraKitE2eStatic\n\nexport interface InfraKitE2eTarget {\n /** The package under test, `<app>/ui` or `<app>/api`. */\n target: string\n server?: never\n}\n\n/**\n * A suite whose own `webServer` serves a static production build \u2014 e.g. one asserting the canonical and `og:`\n * URLs a build inlines, which a dev server never produces. `infraKitE2e()` refuses it, `infra-kit e2e` does\n * not list it, and the audit drops its `infraKitE2e` requirement; the other `playwright.config.ts` rules stay.\n *\n * @example\n * // apps/client/tests/infra-kit.config.ts\n * export default defineConfig(() => ({ e2e: { server: 'static' } }))\n */\nexport interface InfraKitE2eStatic {\n server: 'static'\n target?: never\n}\n\n/**\n * Accepted shapes for a package config's default export \u2014 mirrors Vite's\n * `defineConfig` input: a plain object, a sync factory, or an async factory.\n */\nexport type InfraKitPackageConfigInput =\n InfraKitPackageConfig | (() => InfraKitPackageConfig) | (() => Promise<InfraKitPackageConfig>)\n\n/**\n * Identity helper that gives `infra-kit.config.ts` authors full type inference\n * and editor autocomplete without changing the value \u2014 exactly like Vite's\n * `defineConfig`. Resolution of the factory form happens in the CLI's loader, not here.\n *\n * @example\n * export default defineConfig(() => ({}))\n * @example\n * export default defineConfig(() => ({ requiredScripts: [] }))\n */\nexport const defineConfig = (config: InfraKitPackageConfigInput): InfraKitPackageConfigInput => {\n return config\n}\n", "import { z } from 'zod'\n\n/**\n * Pure (node-free) vendor config schema + authoring helper. Kept separate from\n * `config.ts` (which imports node builtins for the runtime loader) so the public\n * lib entry can re-export `defineVendorConfig` without dragging node types into\n * the emitted `.d.ts`.\n */\n\n/**\n * Filename a source repo provides at its root to declare WHAT `vendor sync`\n * copies (`copy[]`). Lives ONLY on the write path \u2014 `vendor check` never loads it.\n * WHERE/WHICH to stamp (`workspaceDir` + `targets`) is machine-local and lives in\n * the user-global factory config (`~/.infra-kit/vendor.json`).\n */\nexport const VENDOR_CONFIG_FILE = 'vendor.config.ts'\n\n/**\n * A non-empty, repo-relative path with no `..` segments and no absolute prefix\n * (POSIX `/`, UNC/`\\`, or a Windows drive like `C:`). Containment guard for\n * vendor copy items so a malicious/typo config can't read or write outside the\n * source/target repo roots. Kept as a string/regex check (no `node:path`) to\n * respect this file's node-free constraint \u2014 `sync-ops.ts` does the resolved\n * runtime containment assert as defense in depth.\n */\nconst safeRelPath = z.string().refine(\n (p) => {\n const isAbsolute = /^(?:[/\\\\]|[a-z]:)/i.test(p)\n const hasDotDotSegment = p.split(/[\\\\/]/).includes('..')\n\n return !isAbsolute && !hasDotDotSegment && p.trim().length > 0\n },\n { message: 'must be a non-empty repo-relative path without \"..\" segments' },\n)\n\n/**\n * One item to sync from the source repo into each target. `vendored: true` marks\n * workspace packages that must land under `vendor/` (the single-source-of-truth\n * code); everything else is root-level tooling that stays at the repo root.\n */\nexport const vendorCopyItemSchema = z.object({\n name: z.string(),\n source: safeRelPath,\n target: safeRelPath,\n type: z.enum(['file', 'directory']),\n vendored: z.boolean().optional(),\n})\n\nexport const vendorConfigSchema = z\n .object({\n /** Files/dirs to copy. Items with `vendored: true` land under `vendor/`. */\n copy: z.array(vendorCopyItemSchema),\n })\n // Reject stray keys so a leftover `targets` (now machine-local, in\n // ~/.infra-kit/vendor.json) yields a clear \"unrecognized key\" error rather\n // than being silently ignored.\n .strict()\n\nexport type VendorCopyItem = z.infer<typeof vendorCopyItemSchema>\nexport type VendorConfig = z.infer<typeof vendorConfigSchema>\n\n/**\n * Identity helper for authoring a type-safe `vendor.config.ts` in a source repo.\n * Re-exported from the public lib entry so a source repo can\n * `import { defineVendorConfig } from 'infra-kit'`.\n *\n * NOTE: a `vendor.config.ts` must be type-strippable \u2014 Node's native type\n * stripping (Node >= 24) loads it without a build step, which forbids `enum`,\n * `namespace`, and parameter properties.\n *\n * @example\n * export default defineVendorConfig({\n * copy: [{ name: 'Configs', source: 'vendor/configs', target: 'vendor/configs', type: 'directory', vendored: true }],\n * })\n */\nexport const defineVendorConfig = (config: VendorConfig): VendorConfig => {\n return config\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\n\n/**\n * Pure (node-free) schema for a consumer repo's deploy manifest. `infra-kit deploy-sync` reads it and\n * generates the repo's deploy workflows from it, so CI runs plain YAML and never needs the CLI.\n *\n * A separate file rather than a `deploy` key in `infra-kit.json` / `infra-kit.config.ts`: both of those\n * are strict schemas parsed by every installed CLI and by the vite plugin, so a new key would break\n * every teammate and CI build still on an older release. Nothing but `deploy-sync` reads this file.\n */\nexport const DEPLOY_CONFIG_FILE = 'infra-kit.deploy.ts'\n\n/** Environment and service names end up in job ids, input keys and secret names. */\nconst kebabName = z.string().regex(/^[a-z][a-z0-9-]*$/, 'must be lower-case kebab-case, e.g. client-be')\n\n/** A reusable workflow in the consumer's own `.github/workflows/`, by file name. */\nconst localWorkflowFile = z\n .string()\n .regex(/^[\\w.-]+\\.ya?ml$/, 'must be a workflow file name in .github/workflows, e.g. _deploy-media-jobs.yml')\n\nconst secretName = z.string().regex(/^[A-Z_][A-Z0-9_]*$/, 'must be an upper-case secret name, e.g. MONITORING_IP')\n\n/** Service names become dispatch inputs, so they must not shadow the ones every workflow has. */\nconst RESERVED_SERVICE_NAMES = new Set(['environment'])\n\nexport const deployServiceSchema = z.strictObject({\n /** The only environments this service deploys to. Omitted = every environment. */\n envs: z.array(kebabName).nonempty().optional(),\n /** Environments this service never deploys to; every other one is allowed. Exclusive with `envs`. */\n skipEnvs: z.array(kebabName).nonempty().optional(),\n /** GitHub runner label. Omitted = the shared job's default (`ubuntu-24.04-arm`). */\n runner: z\n .string()\n .regex(/^[\\w.-]+$/, 'must be a runner label, e.g. macos-14')\n .optional(),\n /**\n * Deploy through this repo's own reusable workflow instead of `devops/scripts/deploy-<service>.sh`\n * in the shared job. It is called with `branch` and `environment` inputs and `secrets: inherit`.\n */\n workflow: localWorkflowFile.optional(),\n /**\n * The service's TypeScript deploy, run by `infra-kit deploy-run`, as a repo-relative path; put it in\n * the app it deploys (`apps/client/api/deploy.ts`) and it defaults to that app. Omitted =\n * `devops/scripts/deploy-<service>.sh`.\n */\n deploy: z\n .string()\n .regex(/^(?!.*\\.\\.)[\\w./-]+\\.ts$/, 'must be a repo-relative .ts path, e.g. apps/client/api/deploy.ts')\n .optional(),\n /**\n * `false` leaves the service out of the deploy-all workflow; a list keeps it in deploy-all for those\n * environments only, e.g. `['prod']`. Either way it stays selectable wherever it may deploy.\n */\n deployAll: z.union([z.boolean(), z.array(kebabName).nonempty()]).optional(),\n /**\n * Services this one waits for in the same run, e.g. two services that redeploy one API Gateway stage.\n * A dependency that fails stops it; one that was not selected or not offered here does not.\n */\n after: z.array(kebabName).nonempty().optional(),\n /**\n * Makes the service a step rather than a checkbox, e.g. DB migrations: it runs whenever one of these\n * services deploys, before them, and its failure stops them.\n */\n runsBefore: z.array(kebabName).nonempty().optional(),\n})\n\ntype ServiceMap = Record<string, z.infer<typeof deployServiceSchema>>\n\n/** Every service mapped to the ones it waits for, whether named by its `after` or by a step's `runsBefore`. */\nexport const serviceDependencies = (services: ServiceMap): Map<string, string[]> => {\n const dependencies = new Map(\n Object.entries(services).map(([name, service]) => {\n return [name, [...(service.after ?? [])]]\n }),\n )\n\n for (const [step, service] of Object.entries(services)) {\n for (const target of service.runsBefore ?? []) dependencies.get(target)?.push(step)\n }\n\n return dependencies\n}\n\nconst checkServiceOrder = (services: ServiceMap, ctx: z.RefinementCtx): void => {\n for (const [name, service] of Object.entries(services)) {\n for (const key of ['after', 'runsBefore'] as const) {\n for (const [index, other] of (service[key] ?? []).entries()) {\n if (other === name) {\n ctx.addIssue({ code: 'custom', path: ['services', name, key, index], message: 'names the service itself' })\n } else if (!(other in services)) {\n ctx.addIssue({ code: 'custom', path: ['services', name, key, index], message: `no service '${other}'` })\n }\n }\n }\n\n if (service.runsBefore !== undefined && service.deployAll !== undefined) {\n ctx.addIssue({\n code: 'custom',\n path: ['services', name],\n message: 'a runsBefore step deploys with its services, so deployAll does not apply',\n })\n }\n }\n\n const dependencies = serviceDependencies(services)\n const done = new Set<string>()\n const visiting = new Set<string>()\n\n const visit = (name: string, chain: string[]): void => {\n if (done.has(name)) return\n\n if (visiting.has(name)) {\n ctx.addIssue({ code: 'custom', path: ['services'], message: `order cycle: ${[...chain, name].join(' \u2192 ')}` })\n\n return\n }\n\n visiting.add(name)\n\n for (const dependency of dependencies.get(name) ?? []) visit(dependency, [...chain, name])\n\n visiting.delete(name)\n done.add(name)\n }\n\n for (const name of dependencies.keys()) visit(name, [])\n}\n\nexport const deployConfigSchema = z\n .strictObject({\n /**\n * The `<project>` in this repo's `/<project>/...` SSM parameters, e.g. `hulyo`, so a deploy script\n * shared across repos reads its parameters without hard-coding the prefix.\n */\n project: z\n .string()\n .regex(/^[a-z0-9][a-z0-9-]*$/, 'must be lower-case letters, digits and dashes, e.g. hulyo')\n .optional(),\n /** The environments the generated workflows offer, in dispatch-menu order. */\n environments: z.array(kebabName).nonempty(),\n /** Preselected in the dispatch menu. Omitted = the first environment. */\n defaultEnvironment: kebabName.optional(),\n /** Every deployable service, keyed by its `devops/scripts/deploy-<name>.sh` suffix, in menu order. */\n services: z.record(kebabName, deployServiceSchema),\n /** Reusable workflows applied before any service deploys; skippable per run. */\n terraform: z.array(localWorkflowFile).optional(),\n /**\n * Reusable workflows run after terraform (applied or skipped) and before any service, e.g. publishing\n * a Lambda layer the services resolve at deploy time. Not skipped with terraform; a failure stops every service.\n */\n preDeploy: z.array(localWorkflowFile).optional(),\n /** Reusable workflow called once every deploy job finished without a failure. */\n notifySuccess: localWorkflowFile.optional(),\n /** Repo secrets exported, under the same name, to every deploy script. */\n extraEnv: z.array(secretName).optional(),\n /**\n * Minutes the deploy step of the shared job may run. Omitted = the shared job's default, 25. A\n * timed-out step fails and keeps its logs, where a job cancelled at GitHub's 360-minute cap loses them.\n */\n deployTimeoutMinutes: z.number().int().min(1).max(360).optional(),\n /**\n * Generate under the v1 names, `deploy-all.yml` and `deploy-selected-services.yml`, replacing the\n * hand-written files. Those are the files `infra-kit release deploy-*` dispatches, so the manifest\n * becomes what the CLI deploys and reads its service gates from. Omitted = the `-v2` pilot files,\n * which the CLI ignores. A repo that delivers prod needs `prod` in `environments` first: delivery\n * dispatches `deploy-all.yml` with it.\n */\n replaceV1: z.boolean().optional(),\n /**\n * The deploy job reads its environment from this Doppler project, config = the dispatched environment,\n * with the token in the `<env>_DOPPLER_TOKEN` secret: AWS keys (and an `AWS_DEPLOY_ROLE_ARN` to\n * assume), Slack and every secret the deploy scripts read. No AWS secrets are passed then.\n */\n doppler: z\n .strictObject({\n project: z.string().regex(/^[a-z0-9][a-z0-9_-]*$/, 'must be a Doppler project name, e.g. nomadream'),\n })\n .optional(),\n })\n .superRefine((config, ctx) => {\n if (Object.keys(config.services).length === 0) {\n ctx.addIssue({ code: 'custom', path: ['services'], message: 'declare at least one service' })\n }\n\n for (const [name, service] of Object.entries(config.services)) {\n if (RESERVED_SERVICE_NAMES.has(name)) {\n ctx.addIssue({ code: 'custom', path: ['services', name], message: 'is a dispatch input of its own' })\n }\n\n if (service.workflow !== undefined && service.deploy !== undefined) {\n ctx.addIssue({ code: 'custom', path: ['services', name], message: 'set workflow or deploy, not both' })\n }\n\n if (service.envs !== undefined && service.skipEnvs !== undefined) {\n ctx.addIssue({ code: 'custom', path: ['services', name], message: 'set envs or skipEnvs, not both' })\n }\n }\n\n checkServiceOrder(config.services, ctx)\n\n for (const [index, file] of (config.preDeploy ?? []).entries()) {\n if (config.terraform?.includes(file)) {\n ctx.addIssue({ code: 'custom', path: ['preDeploy', index], message: 'already listed under terraform' })\n }\n }\n\n if (config.defaultEnvironment !== undefined && !config.environments.includes(config.defaultEnvironment)) {\n ctx.addIssue({\n code: 'custom',\n path: ['defaultEnvironment'],\n message: `must be one of environments (${config.environments.join(', ')})`,\n })\n }\n })\n\nexport type DeployService = z.infer<typeof deployServiceSchema>\nexport type DeployConfig = z.infer<typeof deployConfigSchema>\n\n/**\n * Identity helper for authoring a type-safe `infra-kit.deploy.ts`.\n *\n * A manifest can equally use `import type { DeployConfig }` with `satisfies DeployConfig`: the type\n * import is erased when the CLI loads the file, so it keeps working against an older installed\n * `@slip-stream-kit/config` that predates this helper.\n *\n * @example\n * export default defineDeployConfig({\n * environments: ['dev', 'stage'],\n * services: { 'client-be': {}, 'client-fe': { runner: 'macos-14' } },\n * })\n */\nexport const defineDeployConfig = (config: DeployConfig): DeployConfig => {\n return config\n}\n", "/**\n * Validation rules for a single workspace package, declared in its\n * `infra-kit.config.ts`. Every field is optional: a key left unset falls back to\n * the active baseline, and a key set replaces that default wholesale (per-key, no\n * array concatenation) so a package can opt out with an explicit empty array.\n *\n * The baselines themselves (`DEFAULT_RULES` / `ROOT_DEFAULT_RULES`) deliberately do\n * NOT live here \u2014 they are audit POLICY, and policy belongs to the `infra-kit` CLI\n * that enforces it, not to the package a consumer installs to author a config.\n *\n * Most packages need none of these \u2014 the standard rules live in the baseline, so\n * a typical config is just `defineConfig(() => ({}))`.\n *\n * @example\n * // infra-kit.config.ts\n * import { defineConfig } from '@slip-stream-kit/config'\n *\n * export default defineConfig(() => ({}))\n */\nexport interface InfraKitPackageConfig {\n /** Scripts that must be present in the package's package.json `scripts` map. */\n requiredScripts?: string[]\n /** Files (relative to the package root) that must exist on disk. */\n requiredFiles?: string[]\n /** Turborepo expectations \u2014 only meaningful where a turbo.json lives (the root). */\n turbo?: {\n /** Tasks that must be defined in turbo.json `tasks`. */\n requiredTasks?: string[]\n }\n /**\n * The env var holding this app part's deployed URL, e.g. `CLIENT_URL`. The value lives in Doppler, per\n * environment \u2014 prod's host is not `<env>`-shaped, so no template in code can name it. The dev proxy sends\n * a UI's cloud routes there, and e2e runs against it in cloud mode. Required on a UI whose `dev.proxy`\n * has a route that can go to cloud, and on any `e2e.target`.\n */\n deployedUrlEnv?: string\n /** Local-dev configuration. Accepted-and-inert to the audit; consumed by the dev server. */\n dev?: InfraKitDev\n /** Where this e2e package's suite runs. Consumed by `infraKitE2e()` and the CLI; `server: 'static'` also narrows the audit. */\n e2e?: InfraKitE2e\n /**\n * Explicit override for infra-kit's directory/dependency-based package-type detection (used to\n * pick the agent guidance rendered into this package's `CLAUDE.md`). Detection already covers\n * both consumer repos untouched, so this is an escape hatch for the cases it gets wrong \u2014 leaving\n * it unset is never an error.\n *\n * **Ordering precondition:** the CLI's own pinned `packageConfigSchema` \u2014 not this package's\n * version \u2014 governs what a consumer's `infra-kit.config.ts` may contain, and it is a\n * `z.strictObject`. Only write `type` here once the CLI in use is a version that carries this\n * key; an older CLI rejects the key outright and skips the rest of that package's audit checks.\n *\n * @example\n * // infra-kit.config.ts\n * export default defineConfig(() => ({ type: 'frontend' }))\n */\n type?: InfraKitPackageType\n}\n\n/** Package kinds infra-kit's agent guidance renders type-specific rules for. */\nexport type InfraKitPackageType = 'frontend' | 'backend' | 'lib' | 'e2e' | 'mobile'\n\n/** A proxy route's allowed backend source. */\nexport type InfraKitDevProxySource = 'local' | 'cloud'\n\nexport interface InfraKitDevProxyRoute {\n /** Backend package this route targets when resolved locally. */\n packageName: string\n /** Capabilities this route can resolve to. Must be non-empty. */\n from: InfraKitDevProxySource[]\n /**\n * Source used when a local backend for this package isn't active. Required when\n * `from` lists more than one source; redundant (and omitted) for a single-source\n * route. When set, must be one of `from`.\n */\n default?: InfraKitDevProxySource\n}\n\nexport interface InfraKitDevProxy {\n /**\n * The local backend's URL template \u2014 `<release>`/`<packageName>` are substituted at dev time. A cloud route\n * goes to the package's `deployedUrlEnv` instead. Consulted only for a fragment from a pre-v2 CLI: a\n * current one publishes the exact `origin` (`<release>.<packageName>.<repo>.localhost`) it registered.\n */\n templates: {\n local: string\n }\n /** Path-prefix (e.g. `/api`, `/api/v1`, `/media`) \u2192 route definition. */\n routes: Record<string, InfraKitDevProxyRoute>\n}\n\nexport interface InfraKitDev {\n proxy?: InfraKitDevProxy\n env?: InfraKitDevEnv\n}\n\n/**\n * Env overrides for this package's dev server only, applied over the loaded (Doppler) env when vite starts\n * it \u2014 never on `build`. For variables another build mode needs that break local dev, e.g. a mobile-only\n * absolute origin that sends requests around the dev proxy.\n *\n * @example\n * // apps/client/ui/infra-kit.config.ts\n * export default defineConfig(() => ({ dev: { env: { unset: ['VITE_ORIGIN_DOMAIN'] } } }))\n */\nexport interface InfraKitDevEnv {\n /** Variables removed from the dev server's env. */\n unset?: string[]\n /** Variables set (or overridden) in the dev server's env. A name cannot also be in `unset`. */\n set?: Record<string, string>\n}\n\n/**\n * Where an e2e package's suite runs. With `target`, `infraKitE2e()` (`@slip-stream-kit/config/playwright`)\n * reads it: a run is local by default \u2014 `target` on this worktree, served by `infra-kit dev <app>` \u2014 and\n * cloud only when asked (`INFRA_KIT_E2E=cloud`, or CI), against the URL in the target package's `deployedUrlEnv`.\n * With `server: 'static'`, see {@link InfraKitE2eStatic}.\n *\n * @example\n * // apps/client/tests/infra-kit.config.ts\n * export default defineConfig(() => ({ e2e: { target: 'client/ui' } }))\n */\nexport type InfraKitE2e = InfraKitE2eTarget | InfraKitE2eStatic\n\nexport interface InfraKitE2eTarget {\n /** The package under test, `<app>/ui` or `<app>/api`. */\n target: string\n server?: never\n}\n\n/**\n * A suite whose own `webServer` serves a static production build \u2014 e.g. one asserting the canonical and `og:`\n * URLs a build inlines, which a dev server never produces. `infraKitE2e()` refuses it, `infra-kit e2e` does\n * not list it, and the audit drops its `infraKitE2e` requirement; the other `playwright.config.ts` rules stay.\n *\n * @example\n * // apps/client/tests/infra-kit.config.ts\n * export default defineConfig(() => ({ e2e: { server: 'static' } }))\n */\nexport interface InfraKitE2eStatic {\n server: 'static'\n target?: never\n}\n\n/**\n * Accepted shapes for a package config's default export \u2014 mirrors Vite's\n * `defineConfig` input: a plain object, a sync factory, or an async factory.\n */\nexport type InfraKitPackageConfigInput =\n InfraKitPackageConfig | (() => InfraKitPackageConfig) | (() => Promise<InfraKitPackageConfig>)\n\n/**\n * Identity helper that gives `infra-kit.config.ts` authors full type inference\n * and editor autocomplete without changing the value \u2014 exactly like Vite's\n * `defineConfig`. Resolution of the factory form happens in the CLI's loader, not here.\n *\n * @example\n * export default defineConfig(() => ({}))\n * @example\n * export default defineConfig(() => ({ requiredScripts: [] }))\n */\nexport const defineConfig = (config: InfraKitPackageConfigInput): InfraKitPackageConfigInput => {\n return config\n}\n", "import { z } from 'zod'\n\n/**\n * Pure (node-free) vendor config schema + authoring helper. Kept separate from\n * `config.ts` (which imports node builtins for the runtime loader) so the public\n * lib entry can re-export `defineVendorConfig` without dragging node types into\n * the emitted `.d.ts`.\n */\n\n/**\n * Filename a source repo provides at its root to declare WHAT `vendor sync`\n * copies (`copy[]`). Lives ONLY on the write path \u2014 `vendor check` never loads it.\n * WHERE/WHICH to stamp (`workspaceDir` + `targets`) is machine-local and lives in\n * the user-global factory config (`~/.infra-kit/vendor.json`).\n */\nexport const VENDOR_CONFIG_FILE = 'vendor.config.ts'\n\n/**\n * A non-empty, repo-relative path with no `..` segments and no absolute prefix\n * (POSIX `/`, UNC/`\\`, or a Windows drive like `C:`). Containment guard for\n * vendor copy items so a malicious/typo config can't read or write outside the\n * source/target repo roots. Kept as a string/regex check (no `node:path`) to\n * respect this file's node-free constraint \u2014 `sync-ops.ts` does the resolved\n * runtime containment assert as defense in depth.\n */\nconst safeRelPath = z.string().refine(\n (p) => {\n const isAbsolute = /^(?:[/\\\\]|[a-z]:)/i.test(p)\n const hasDotDotSegment = p.split(/[\\\\/]/).includes('..')\n\n return !isAbsolute && !hasDotDotSegment && p.trim().length > 0\n },\n { message: 'must be a non-empty repo-relative path without \"..\" segments' },\n)\n\n/**\n * One item to sync from the source repo into each target. `vendored: true` marks\n * workspace packages that must land under `vendor/` (the single-source-of-truth\n * code); everything else is root-level tooling that stays at the repo root.\n */\nexport const vendorCopyItemSchema = z.object({\n name: z.string(),\n source: safeRelPath,\n target: safeRelPath,\n type: z.enum(['file', 'directory']),\n vendored: z.boolean().optional(),\n})\n\nexport const vendorConfigSchema = z\n .object({\n /** Files/dirs to copy. Items with `vendored: true` land under `vendor/`. */\n copy: z.array(vendorCopyItemSchema),\n })\n // Reject stray keys so a leftover `targets` (now machine-local, in\n // ~/.infra-kit/vendor.json) yields a clear \"unrecognized key\" error rather\n // than being silently ignored.\n .strict()\n\nexport type VendorCopyItem = z.infer<typeof vendorCopyItemSchema>\nexport type VendorConfig = z.infer<typeof vendorConfigSchema>\n\n/**\n * Identity helper for authoring a type-safe `vendor.config.ts` in a source repo.\n * Re-exported from the public lib entry so a source repo can\n * `import { defineVendorConfig } from 'infra-kit'`.\n *\n * NOTE: a `vendor.config.ts` must be type-strippable \u2014 Node's native type\n * stripping (Node >= 24) loads it without a build step, which forbids `enum`,\n * `namespace`, and parameter properties.\n *\n * @example\n * export default defineVendorConfig({\n * copy: [{ name: 'Configs', source: 'vendor/configs', target: 'vendor/configs', type: 'directory', vendored: true }],\n * })\n */\nexport const defineVendorConfig = (config: VendorConfig): VendorConfig => {\n return config\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["z", "z"]
7
7
  }
package/dist/internal.js CHANGED
@@ -27,8 +27,64 @@ var deployServiceSchema = z.strictObject({
27
27
  * `false` leaves the service out of the deploy-all workflow; a list keeps it in deploy-all for those
28
28
  * environments only, e.g. `['prod']`. Either way it stays selectable wherever it may deploy.
29
29
  */
30
- deployAll: z.union([z.boolean(), z.array(kebabName).nonempty()]).optional()
30
+ deployAll: z.union([z.boolean(), z.array(kebabName).nonempty()]).optional(),
31
+ /**
32
+ * Services this one waits for in the same run, e.g. two services that redeploy one API Gateway stage.
33
+ * A dependency that fails stops it; one that was not selected or not offered here does not.
34
+ */
35
+ after: z.array(kebabName).nonempty().optional(),
36
+ /**
37
+ * Makes the service a step rather than a checkbox, e.g. DB migrations: it runs whenever one of these
38
+ * services deploys, before them, and its failure stops them.
39
+ */
40
+ runsBefore: z.array(kebabName).nonempty().optional()
31
41
  });
42
+ var serviceDependencies = (services) => {
43
+ const dependencies = new Map(
44
+ Object.entries(services).map(([name, service]) => {
45
+ return [name, [...service.after ?? []]];
46
+ })
47
+ );
48
+ for (const [step, service] of Object.entries(services)) {
49
+ for (const target of service.runsBefore ?? []) dependencies.get(target)?.push(step);
50
+ }
51
+ return dependencies;
52
+ };
53
+ var checkServiceOrder = (services, ctx) => {
54
+ for (const [name, service] of Object.entries(services)) {
55
+ for (const key of ["after", "runsBefore"]) {
56
+ for (const [index, other] of (service[key] ?? []).entries()) {
57
+ if (other === name) {
58
+ ctx.addIssue({ code: "custom", path: ["services", name, key, index], message: "names the service itself" });
59
+ } else if (!(other in services)) {
60
+ ctx.addIssue({ code: "custom", path: ["services", name, key, index], message: `no service '${other}'` });
61
+ }
62
+ }
63
+ }
64
+ if (service.runsBefore !== void 0 && service.deployAll !== void 0) {
65
+ ctx.addIssue({
66
+ code: "custom",
67
+ path: ["services", name],
68
+ message: "a runsBefore step deploys with its services, so deployAll does not apply"
69
+ });
70
+ }
71
+ }
72
+ const dependencies = serviceDependencies(services);
73
+ const done = /* @__PURE__ */ new Set();
74
+ const visiting = /* @__PURE__ */ new Set();
75
+ const visit = (name, chain) => {
76
+ if (done.has(name)) return;
77
+ if (visiting.has(name)) {
78
+ ctx.addIssue({ code: "custom", path: ["services"], message: `order cycle: ${[...chain, name].join(" \u2192 ")}` });
79
+ return;
80
+ }
81
+ visiting.add(name);
82
+ for (const dependency of dependencies.get(name) ?? []) visit(dependency, [...chain, name]);
83
+ visiting.delete(name);
84
+ done.add(name);
85
+ };
86
+ for (const name of dependencies.keys()) visit(name, []);
87
+ };
32
88
  var deployConfigSchema = z.strictObject({
33
89
  /**
34
90
  * The `<project>` in this repo's `/<project>/...` SSM parameters, e.g. `hulyo`, so a deploy script
@@ -64,7 +120,15 @@ var deployConfigSchema = z.strictObject({
64
120
  * which the CLI ignores. A repo that delivers prod needs `prod` in `environments` first: delivery
65
121
  * dispatches `deploy-all.yml` with it.
66
122
  */
67
- replaceV1: z.boolean().optional()
123
+ replaceV1: z.boolean().optional(),
124
+ /**
125
+ * The deploy job reads its environment from this Doppler project, config = the dispatched environment,
126
+ * with the token in the `<env>_DOPPLER_TOKEN` secret: AWS keys (and an `AWS_DEPLOY_ROLE_ARN` to
127
+ * assume), Slack and every secret the deploy scripts read. No AWS secrets are passed then.
128
+ */
129
+ doppler: z.strictObject({
130
+ project: z.string().regex(/^[a-z0-9][a-z0-9_-]*$/, "must be a Doppler project name, e.g. nomadream")
131
+ }).optional()
68
132
  }).superRefine((config, ctx) => {
69
133
  if (Object.keys(config.services).length === 0) {
70
134
  ctx.addIssue({ code: "custom", path: ["services"], message: "declare at least one service" });
@@ -80,6 +144,7 @@ var deployConfigSchema = z.strictObject({
80
144
  ctx.addIssue({ code: "custom", path: ["services", name], message: "set envs or skipEnvs, not both" });
81
145
  }
82
146
  }
147
+ checkServiceOrder(config.services, ctx);
83
148
  for (const [index, file] of (config.preDeploy ?? []).entries()) {
84
149
  if (config.terraform?.includes(file)) {
85
150
  ctx.addIssue({ code: "custom", path: ["preDeploy", index], message: "already listed under terraform" });
@@ -541,6 +606,7 @@ export {
541
606
  readRelease,
542
607
  readRepoIdentity,
543
608
  readRepoSlug,
609
+ serviceDependencies,
544
610
  slugifyHostLabel,
545
611
  slugifyRelease,
546
612
  vendorConfigSchema,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/lib/deploy/deploy-config-schema.ts", "../src/lib/package-config/package-config-schema.ts", "../src/lib/release-slug/read-release.ts", "../src/lib/release-slug/release-slug.ts", "../src/lib/release-slug/repo-slug.ts", "../src/lib/release-slug/app-alias.ts", "../src/lib/vite/vite.ts", "../src/lib/playwright/playwright.ts", "../src/lib/vendor/config-schema.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\n\n/**\n * Pure (node-free) schema for a consumer repo's deploy manifest. `infra-kit deploy-sync` reads it and\n * generates the repo's deploy workflows from it, so CI runs plain YAML and never needs the CLI.\n *\n * A separate file rather than a `deploy` key in `infra-kit.json` / `infra-kit.config.ts`: both of those\n * are strict schemas parsed by every installed CLI and by the vite plugin, so a new key would break\n * every teammate and CI build still on an older release. Nothing but `deploy-sync` reads this file.\n */\nexport const DEPLOY_CONFIG_FILE = 'infra-kit.deploy.ts'\n\n/** Environment and service names end up in job ids, input keys and secret names. */\nconst kebabName = z.string().regex(/^[a-z][a-z0-9-]*$/, 'must be lower-case kebab-case, e.g. client-be')\n\n/** A reusable workflow in the consumer's own `.github/workflows/`, by file name. */\nconst localWorkflowFile = z\n .string()\n .regex(/^[\\w.-]+\\.ya?ml$/, 'must be a workflow file name in .github/workflows, e.g. _deploy-media-jobs.yml')\n\nconst secretName = z.string().regex(/^[A-Z_][A-Z0-9_]*$/, 'must be an upper-case secret name, e.g. MONITORING_IP')\n\n/** Service names become dispatch inputs, so they must not shadow the ones every workflow has. */\nconst RESERVED_SERVICE_NAMES = new Set(['environment'])\n\nexport const deployServiceSchema = z.strictObject({\n /** The only environments this service deploys to. Omitted = every environment. */\n envs: z.array(kebabName).nonempty().optional(),\n /** Environments this service never deploys to; every other one is allowed. Exclusive with `envs`. */\n skipEnvs: z.array(kebabName).nonempty().optional(),\n /** GitHub runner label. Omitted = the shared job's default (`ubuntu-24.04-arm`). */\n runner: z\n .string()\n .regex(/^[\\w.-]+$/, 'must be a runner label, e.g. macos-14')\n .optional(),\n /**\n * Deploy through this repo's own reusable workflow instead of `devops/scripts/deploy-<service>.sh`\n * in the shared job. It is called with `branch` and `environment` inputs and `secrets: inherit`.\n */\n workflow: localWorkflowFile.optional(),\n /**\n * The service's TypeScript deploy, run by `infra-kit deploy-run`, as a repo-relative path; put it in\n * the app it deploys (`apps/client/api/deploy.ts`) and it defaults to that app. Omitted =\n * `devops/scripts/deploy-<service>.sh`.\n */\n deploy: z\n .string()\n .regex(/^(?!.*\\.\\.)[\\w./-]+\\.ts$/, 'must be a repo-relative .ts path, e.g. apps/client/api/deploy.ts')\n .optional(),\n /**\n * `false` leaves the service out of the deploy-all workflow; a list keeps it in deploy-all for those\n * environments only, e.g. `['prod']`. Either way it stays selectable wherever it may deploy.\n */\n deployAll: z.union([z.boolean(), z.array(kebabName).nonempty()]).optional(),\n})\n\nexport const deployConfigSchema = z\n .strictObject({\n /**\n * The `<project>` in this repo's `/<project>/...` SSM parameters, e.g. `hulyo`, so a deploy script\n * shared across repos reads its parameters without hard-coding the prefix.\n */\n project: z\n .string()\n .regex(/^[a-z0-9][a-z0-9-]*$/, 'must be lower-case letters, digits and dashes, e.g. hulyo')\n .optional(),\n /** The environments the generated workflows offer, in dispatch-menu order. */\n environments: z.array(kebabName).nonempty(),\n /** Preselected in the dispatch menu. Omitted = the first environment. */\n defaultEnvironment: kebabName.optional(),\n /** Every deployable service, keyed by its `devops/scripts/deploy-<name>.sh` suffix, in menu order. */\n services: z.record(kebabName, deployServiceSchema),\n /** Reusable workflows applied before any service deploys; skippable per run. */\n terraform: z.array(localWorkflowFile).optional(),\n /**\n * Reusable workflows run after terraform (applied or skipped) and before any service, e.g. publishing\n * a Lambda layer the services resolve at deploy time. Not skipped with terraform; a failure stops every service.\n */\n preDeploy: z.array(localWorkflowFile).optional(),\n /** Reusable workflow called once every deploy job finished without a failure. */\n notifySuccess: localWorkflowFile.optional(),\n /** Repo secrets exported, under the same name, to every deploy script. */\n extraEnv: z.array(secretName).optional(),\n /**\n * Minutes the deploy step of the shared job may run. Omitted = the shared job's default, 25. A\n * timed-out step fails and keeps its logs, where a job cancelled at GitHub's 360-minute cap loses them.\n */\n deployTimeoutMinutes: z.number().int().min(1).max(360).optional(),\n /**\n * Generate under the v1 names, `deploy-all.yml` and `deploy-selected-services.yml`, replacing the\n * hand-written files. Those are the files `infra-kit release deploy-*` dispatches, so the manifest\n * becomes what the CLI deploys and reads its service gates from. Omitted = the `-v2` pilot files,\n * which the CLI ignores. A repo that delivers prod needs `prod` in `environments` first: delivery\n * dispatches `deploy-all.yml` with it.\n */\n replaceV1: z.boolean().optional(),\n })\n .superRefine((config, ctx) => {\n if (Object.keys(config.services).length === 0) {\n ctx.addIssue({ code: 'custom', path: ['services'], message: 'declare at least one service' })\n }\n\n for (const [name, service] of Object.entries(config.services)) {\n if (RESERVED_SERVICE_NAMES.has(name)) {\n ctx.addIssue({ code: 'custom', path: ['services', name], message: 'is a dispatch input of its own' })\n }\n\n if (service.workflow !== undefined && service.deploy !== undefined) {\n ctx.addIssue({ code: 'custom', path: ['services', name], message: 'set workflow or deploy, not both' })\n }\n\n if (service.envs !== undefined && service.skipEnvs !== undefined) {\n ctx.addIssue({ code: 'custom', path: ['services', name], message: 'set envs or skipEnvs, not both' })\n }\n }\n\n for (const [index, file] of (config.preDeploy ?? []).entries()) {\n if (config.terraform?.includes(file)) {\n ctx.addIssue({ code: 'custom', path: ['preDeploy', index], message: 'already listed under terraform' })\n }\n }\n\n if (config.defaultEnvironment !== undefined && !config.environments.includes(config.defaultEnvironment)) {\n ctx.addIssue({\n code: 'custom',\n path: ['defaultEnvironment'],\n message: `must be one of environments (${config.environments.join(', ')})`,\n })\n }\n })\n\nexport type DeployService = z.infer<typeof deployServiceSchema>\nexport type DeployConfig = z.infer<typeof deployConfigSchema>\n\n/**\n * Identity helper for authoring a type-safe `infra-kit.deploy.ts`.\n *\n * A manifest can equally use `import type { DeployConfig }` with `satisfies DeployConfig`: the type\n * import is erased when the CLI loads the file, so it keeps working against an older installed\n * `@slip-stream-kit/config` that predates this helper.\n *\n * @example\n * export default defineDeployConfig({\n * environments: ['dev', 'stage'],\n * services: { 'client-be': {}, 'client-fe': { runner: 'macos-14' } },\n * })\n */\nexport const defineDeployConfig = (config: DeployConfig): DeployConfig => {\n return config\n}\n", "import { z } from 'zod'\n\n/**\n * Schema for the resolved (post-factory) package config object. `strictObject`\n * rejects unknown keys so typos in `infra-kit.config.ts` surface as validation\n * errors instead of being silently ignored.\n *\n * Kept in its own module \u2014 separate from the public `defineConfig`/types entry \u2014\n * so the published `infra-kit` type surface stays free of a `zod` import.\n */\nconst ENV_VAR_NAME = /^[A-Z_][A-Z0-9_]*$/\n\n/**\n * A key the schema used to accept. Refused with the replacement named, rather than as an anonymous\n * unknown key: the config is TypeScript, so there is no migration to rewrite it \u2014 the message is the fix.\n */\nconst retired = (replacement: string) => {\n return z.never({ error: replacement }).optional()\n}\n\nexport const packageConfigSchema = z\n .strictObject({\n requiredScripts: z.array(z.string().min(1)).optional(),\n requiredFiles: z.array(z.string().min(1)).optional(),\n // No `.default()` here: `.partial()` elsewhere in the config-loading pipeline preserves ZodDefault,\n // so a default on an optional key would make an EMPTY override layer parse to that default and\n // shallow-merge over a real setting from an earlier layer. Defaults belong at the read site.\n type: z.enum(['frontend', 'backend', 'lib', 'e2e', 'mobile']).optional(),\n // The deployed origin differs per environment (prod is not `<env>`-shaped), so the config names the\n // variable and Doppler holds the value \u2014 the one source both the dev proxy and e2e read.\n deployedUrlEnv: z\n .string()\n .regex(ENV_VAR_NAME, 'deployedUrlEnv must be an env var name (e.g. `CLIENT_URL`)')\n .optional(),\n turbo: z\n .strictObject({\n requiredTasks: z.array(z.string().min(1)).optional(),\n })\n .optional(),\n dev: z\n .strictObject({\n proxy: z\n .strictObject({\n templates: z.strictObject({\n // Accepted-but-warned this release regardless of scheme (the portless daemon this template\n // targets serves TLS only, so a non-`https://` value is almost certainly a mistake \u2014 see the\n // warning emitted from `loadDev` in `../vite/vite.ts`). Make `https://` a hard schema\n // requirement next release, once both consumer repos have had a release to pick up the warning.\n local: z.string().min(1),\n cloud: retired(\n \"`templates.cloud` was replaced by `deployedUrlEnv`: set `deployedUrlEnv: '<APP>_URL'` at the top of this config and keep the URL per environment in Doppler\",\n ),\n }),\n routes: z.record(\n z.string().min(1),\n z\n .strictObject({\n packageName: z.string().min(1),\n from: z.array(z.enum(['local', 'cloud'])).min(1),\n default: z.enum(['local', 'cloud']).optional(),\n })\n .refine(\n (route) => {\n return route.from.length <= 1 || route.default !== undefined\n },\n {\n message: 'default is required when `from` has more than one source',\n },\n )\n .refine(\n (route) => {\n return route.default === undefined || route.from.includes(route.default)\n },\n {\n message: 'default must be listed in `from`',\n },\n ),\n ),\n })\n .optional(),\n env: z\n .strictObject({\n unset: z.array(z.string().regex(ENV_VAR_NAME, 'dev.env.unset entries must be env var names')).optional(),\n set: z\n .record(z.string().regex(ENV_VAR_NAME, 'dev.env.set keys must be env var names'), z.string())\n .optional(),\n })\n .refine(\n (env) => {\n return !env.unset?.some((name) => {\n return env.set?.[name] !== undefined\n })\n },\n { message: 'a variable cannot be in both `dev.env.unset` and `dev.env.set`' },\n )\n .optional(),\n })\n .optional(),\n e2e: z\n .strictObject({\n target: z\n .string()\n .regex(/^[^/\\s]+\\/(?:ui|api)$/, 'target must be `<app>/ui` or `<app>/api` (e.g. `client/ui`)')\n .optional(),\n server: z.literal('static').optional(),\n baseUrlEnv: retired(\n \"`e2e.baseUrlEnv` was removed: the deployed URL is the target package's own `deployedUrlEnv`\",\n ),\n cloud: retired(\"`e2e.cloud` was removed: the deployed URL is the target package's own `deployedUrlEnv`\"),\n })\n // One object with a refine rather than a union of two: a union reports a retired key as a bare\n // \"Invalid input\" instead of its replacement message.\n .refine(\n (e2e) => {\n return (e2e.target === undefined) !== (e2e.server === undefined)\n },\n {\n message:\n \"`e2e` declares exactly one of `target` (the package `infra-kit dev` serves, e.g. `client/ui`) or `server: 'static'` (the suite serves its own build)\",\n },\n )\n .optional(),\n })\n .refine(\n (config) => {\n const routes = Object.values(config.dev?.proxy?.routes ?? {})\n\n return (\n config.deployedUrlEnv !== undefined ||\n !routes.some((route) => {\n return route.from.includes('cloud')\n })\n )\n },\n {\n message:\n \"a `dev.proxy` route that can go to cloud needs `deployedUrlEnv` \u2014 the variable holding this app's deployed URL\",\n path: ['deployedUrlEnv'],\n },\n )\n", "import { execFileSync } from 'node:child_process'\n\nimport { DEFAULT_RELEASE_SLUG, slugifyRelease } from './release-slug'\n\n/**\n * The `<release>` label for the git branch `cwd` is on, or {@link DEFAULT_RELEASE_SLUG} outside a repo or on\n * a branch that slugifies to nothing. Never throws.\n *\n * The dev-server's alias writer, the vite helper and the Playwright helper all name the same host from it,\n * so there is one implementation: a second copy that disagreed would print a URL no alias backs.\n *\n * @example\n * readRelease('/repo-on-feature/HUL-123') // => 'hul-123'\n */\nexport const readRelease = (cwd: string): string => {\n try {\n // eslint-disable-next-line sonarjs/no-os-command-from-path\n const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {\n cwd,\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'ignore'],\n }).trim()\n\n return slugifyRelease(branch) || DEFAULT_RELEASE_SLUG\n } catch {\n return DEFAULT_RELEASE_SLUG\n }\n}\n", "/**\n * Shared, dependency-light release-slug helper.\n *\n * Extracted from `lib/vite/vite.ts` so BOTH the published `infra-kit/vite` helper\n * (which re-exports it) and the dev-server's dev-context fragment writer derive the\n * `<release>` hostname segment from ONE implementation \u2014 the recorded slug and the\n * helper-computed slug can never drift. Pure (regex only, no imports) so importing it\n * into the lightweight `infra-kit/vite` bundle stays cheap.\n */\n\n/** RFC 1035: a DNS label holds at most 63 octets; a longer one makes the whole hostname invalid. */\nconst MAX_DNS_LABEL_LENGTH = 63\n\n/**\n * Slugify an arbitrary string into a single DNS label: lowercase, collapse every\n * non-alphanumeric run to `-`, drop leading/trailing separators, and cap it at 63 characters. Returns `''`\n * when the input carries no alphanumeric run (the caller must treat that as \"no label\").\n *\n * portless rejects any hostname outside `[a-z0-9.-]`, so every segment fed into a\n * `<release>.<packageName>.<repo>.localhost` alias must pass through here. A scoped npm name\n * (`@hulyo/client-ui`) is the motivating case: registering it raw fails with\n * `Invalid hostname`, and because the driver swallows that into a best-effort `false`,\n * the whole hero-URL path degrades silently to `localhost:<port>`.\n *\n * Distinct from {@link slugifyRelease}: this does NOT strip a git-flow prefix, so a\n * package legitimately named `fix-utils` keeps its `fix-` prefix.\n *\n * @example\n * slugifyHostLabel('@hulyo/client-ui') // => 'hulyo-client-ui'\n * slugifyHostLabel('backend-api') // => 'backend-api'\n */\nexport const slugifyHostLabel = (label: string): string => {\n // Collect the alphanumeric runs and join with `-`. Doing it this way (rather\n // than collapse-then-trim) is linear and sidesteps a super-linear trim regex,\n // while inherently dropping any leading/trailing separators.\n const runs = label.toLowerCase().match(/[a-z0-9]+/g)\n const capped = runs ? runs.join('-').slice(0, MAX_DNS_LABEL_LENGTH) : ''\n\n // Runs are joined by single dashes, so the cut leaves at most one behind.\n return capped.endsWith('-') ? capped.slice(0, -1) : capped\n}\n\n/**\n * Slugify a git branch into a `<release>` token: strip a leading git-flow prefix\n * (`feature/`, `release/`, \u2026), then reduce the remainder to a single DNS label.\n *\n * @example\n * slugifyRelease('release/2.4') // => '2-4'\n * slugifyRelease('feature/HUL-123') // => 'hul-123'\n */\nexport const slugifyRelease = (branch: string): string => {\n return slugifyHostLabel(branch.replace(/^(?:feature|feat|release|hotfix|bugfix|fix|chore)\\//i, ''))\n}\n\n/**\n * The `<release>` label used when no git branch resolves (outside a repo, or a branch that slugifies to\n * nothing). Both the dev-server's alias writer and `infra-kit/vite`'s template interpolation fall back\n * to this SAME constant \u2014 a divergence here would emit a hostname no alias backs.\n */\nexport const DEFAULT_RELEASE_SLUG = 'local'\n", "import { execFileSync } from 'node:child_process'\nimport fs from 'node:fs'\nimport path from 'node:path'\n\nimport { slugifyHostLabel } from './release-slug'\n\n/** The `<repo>` label used outside a git repo, or when the repo's directory name slugifies to nothing. */\nexport const DEFAULT_REPO_SLUG = 'repo'\n\nexport interface RepoIdentity {\n /**\n * The repo's own directory, shared by every worktree: the main checkout, or a bare repo's `foo.git`\n * itself (never its parent, which would take in every sibling). `null` outside git and for a submodule,\n * which has no main checkout to converge on \u2014 callers fall back to their own toplevel.\n */\n root: string | null\n /** The `<repo>` alias label: {@link root}'s `infra-kit.json` `aliasRepo`, else its name minus `.git`, slugified. */\n slug: string\n}\n\n/**\n * The `aliasRepo` the main checkout's `infra-kit.json` sets, slugified, or `''` when it sets none.\n *\n * Read from the main checkout rather than the caller's worktree so every worktree names the repo alike, and\n * with a bare `JSON.parse` rather than the CLI's loader: the vite and Playwright helpers import this, and the\n * CLI's strict schema has already refused a bad value before any server registered under it.\n */\nconst readConfiguredRepoSlug = (root: string): string => {\n try {\n const config: unknown = JSON.parse(fs.readFileSync(path.join(root, 'infra-kit.json'), 'utf-8'))\n const aliasRepo = (config as { aliasRepo?: unknown } | null)?.aliasRepo\n\n return typeof aliasRepo === 'string' ? slugifyHostLabel(aliasRepo) : ''\n } catch {\n return ''\n }\n}\n\n/**\n * Which repository `cwd` belongs to, read from `git rev-parse --git-common-dir` so every worktree of one\n * repo gives the same answer (`hulyo-monorepo-worktrees/feature/x` is still `hulyo-monorepo`). Never throws.\n *\n * The one source for both the dev alias's `<repo>` label and the directories `env-load` scopes a load to,\n * so the two can never disagree about what \"this repo\" is. Git rather than `infra-kit.json`'s\n * `envManagement` name: git is already required for `<release>`, and the config name is optional, layered,\n * and would pull the config loader into the lightweight vite/Playwright helpers. Only the label can be\n * overridden, by `aliasRepo`, to shorten the host; `root` always comes from git.\n *\n * @example\n * readRepoIdentity('/projects/hulyo-monorepo-worktrees/feature/x/apps/web/ui')\n * // => { root: '/projects/hulyo-monorepo', slug: 'hulyo-monorepo' }\n */\nexport const readRepoIdentity = (cwd: string): RepoIdentity => {\n let commonDir: string\n\n try {\n // eslint-disable-next-line sonarjs/no-os-command-from-path\n commonDir = execFileSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {\n cwd,\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'ignore'],\n }).trim()\n } catch {\n return { root: null, slug: DEFAULT_REPO_SLUG }\n }\n\n const name = path.basename(commonDir)\n\n if (commonDir.includes(`${path.sep}.git${path.sep}modules${path.sep}`)) {\n return { root: null, slug: slugifyHostLabel(name) || DEFAULT_REPO_SLUG }\n }\n\n // `.git` (a normal checkout) or a dot-dir like `.bare` sits inside the repo's directory; anything else is\n // a bare repo, which is its own directory.\n const root = name.startsWith('.') ? path.dirname(commonDir) : commonDir\n\n return {\n root,\n slug:\n readConfiguredRepoSlug(root) || slugifyHostLabel(path.basename(root).replace(/\\.git$/, '')) || DEFAULT_REPO_SLUG,\n }\n}\n\n/** The `<repo>` alias label \u2014 {@link readRepoIdentity}'s `slug`. */\nexport const readRepoSlug = (cwd: string): string => {\n return readRepoIdentity(cwd).slug\n}\n", "import { readRelease } from './read-release'\nimport { slugifyHostLabel } from './release-slug'\nimport { readRepoSlug } from './repo-slug'\n\n/**\n * The portless alias NAME (`<release>.<label>.<repo>`, no `.localhost`) for the package in `appDir`.\n *\n * `<repo>` is what keeps two repos apart: travelist and hulyo both have a `website-ui` and both cut\n * same-named branches, and without it their aliases were identical, so `dev --reuse` adopted the other\n * repo's server. The dev-server registers this name, and `infraKitE2e()` and `infra-kit e2e` aim at it,\n * so all three must come from here.\n *\n * @throws When the package name yields no legal DNS label.\n * @example\n * readAppAliasName('@hulyo/website-ui', '/projects/hulyo-monorepo/apps/website/ui') // => 'dev.hulyo-website-ui.hulyo-monorepo'\n */\nexport const readAppAliasName = (packageName: string, appDir: string): string => {\n const label = slugifyHostLabel(packageName)\n\n if (label === '') {\n throw new Error(`infra-kit: package name \"${packageName}\" has no letters or digits to build a hostname from.`)\n }\n\n return `${readRelease(appDir)}.${label}.${readRepoSlug(appDir)}`\n}\n\n/** {@link readAppAliasName} as the hostname a browser dials (`<release>.<label>.<repo>.localhost`). */\nexport const readAppAliasHost = (packageName: string, appDir: string): string => {\n return `${readAppAliasName(packageName, appDir)}.localhost`\n}\n", "import { Buffer } from 'node:buffer'\nimport fs from 'node:fs'\nimport net from 'node:net'\nimport path from 'node:path'\nimport process from 'node:process'\nimport { pathToFileURL } from 'node:url'\nimport { z } from 'zod'\n\nimport type {\n InfraKitDev,\n InfraKitDevEnv,\n InfraKitDevProxy,\n InfraKitDevProxyRoute,\n InfraKitDevProxySource,\n InfraKitPackageConfig,\n} from '../package-config/package-config'\nimport { packageConfigSchema } from '../package-config/package-config-schema'\nimport { readRelease } from '../release-slug/read-release'\nimport { slugifyHostLabel, slugifyRelease } from '../release-slug/release-slug'\n\n/**\n * Env-name handle read at vite-config time to fill the `<env>` placeholder in a\n * cloud proxy target. Inlined (not imported from `src/lib/constants`) to keep the\n * `infra-kit/vite` bundle's import graph tiny \u2014 see the module header. Kept\n * byte-identical to `INFRA_KIT_ENV_VAR` in `src/lib/constants/constants.ts`.\n */\nconst INFRA_KIT_ENV = 'INFRA_KIT_ENV'\n\n/** Per-package config filename, mirrored from the CLI's `PACKAGE_CONFIG_FILE`. */\nconst PACKAGE_CONFIG_FILE = 'infra-kit.config.ts'\n\n/**\n * Repo-relative dev-context fragment DIRECTORY, searched upward from cwd. Each\n * runner (single-process or Orca pane) writes its OWN `<app>.json` fragment here\n * recording its real bound port + release; the helper merges them (see\n * {@link readLocalContext}). This is the current source of truth.\n */\nconst DEV_CONTEXT_DIR = path.join('.infra-kit', 'dev-context')\n\n/**\n * Legacy single-file dev-context manifest, searched upward from cwd. Read only as\n * a transitional back-compat path when {@link DEV_CONTEXT_DIR} is absent (the\n * directory wins per-package when both exist).\n */\nconst DEV_CONTEXT_FILE = path.join('.infra-kit', 'dev-context.json')\n\n// Kept lenient (not `.strict()`) so extra writer fields never reject a valid fragment, and\n// deliberately un-`refine`d: a rejected fragment would drop a started package to `cloud` instead of\n// falling into legacy mode, which is what makes the rollback path work.\n/**\n * One `.infra-kit/dev-context/<app>.json` fragment. `package` is the load-bearing\n * field the helper reads (\u2192 localSet); `origin` is **the authoritative local target** \u2014\n * the exact origin the runner registered, obeyed verbatim by {@link resolveLocalTarget}.\n * The runner is the only party that knows the scheme and port its alias is really served\n * on, so the helper never re-derives them when `origin` is present.\n *\n * `release`, `alias` and `proxyPort` are the LEGACY inputs, read only when `origin` is\n * absent \u2014 which can only happen when an OLD CLI wrote the fragment (a CLI-only rollback).\n * `port` is no longer a proxy target (the direct `127.0.0.1:<port>` branch is gone) \u2014 it\n * survives as diagnostic provenance. `pid` is the staleness guard: {@link readFragmentDir}\n * drops a fragment whose writer is no longer alive, so a crashed runner cannot keep a\n * package pinned `local` at a dead alias. `writtenAt` is provenance only.\n */\nconst devContextFragmentSchema = z.object({\n package: z.string(),\n port: z.number(),\n pid: z.number().optional(),\n writtenAt: z.number().optional(),\n release: z.string().optional(),\n alias: z.string().optional(),\n proxyPort: z.number().optional(),\n origin: z.string().optional(),\n v: z.number().optional(),\n})\n\n// Why the promise is load-bearing: it is what makes the guard in {@link resolveLocalTarget} possible.\n// Without it, a missing `origin` is ambiguous \u2014 it means BOTH \"an old CLI wrote this, legacy mode is\n// correct\" AND \"a current CLI wrote a broken fragment, legacy mode is catastrophic\" \u2014 and the helper\n// has to guess. It guesses legacy, rebuilds the target from a `templates.local` that says `http://`,\n// and proxies plain HTTP into a TLS listener. Silently, because portless answers :80 with a 302\n// rather than refusing.\n/**\n * Wire version of the dev-context fragment \u2014 the ONE contract between the `infra-kit` CLI (which writes\n * fragments) and this helper (which reads them). They ship as SEPARATE npm packages on separate cadences,\n * so this number is the only thing either side can trust about the other.\n *\n * `v >= 2` is a PROMISE that `origin` is present and authoritative.\n *\n * A fragment with NO `v` is a pre-v2 (legacy) writer and must still be honoured: rejecting it would drop\n * a running package to `cloud` instead of falling into legacy mode, and that rollback path is exactly why\n * the schema above is lenient and un-`refine`d. Absent or unknown `v` therefore NEVER rejects.\n */\nexport const DEV_CONTEXT_WIRE_VERSION = 2\n\n/**\n * A single Vite `server.proxy` entry. `changeOrigin` is always set. `cookieDomainRewrite` is cloud-only\n * (it keeps an HTTPS cloud BE's cookies usable from a local FE) and is always the EMPTY string, which\n * makes vite delete the whole `; Domain=\u2026` segment rather than rewrite it (`rewriteCookieProperty`\n * returns `''` for a falsy replacement). A host-only cookie is then scoped to whatever host the browser\n * actually used, so it works from `127.0.0.1`, `localhost` and a `*.localhost` portless alias alike \u2014\n * whereas a literal `Domain=localhost` is rejected outright by an IP host, and by a multi-label\n * `*.localhost` origin under the RFC 6265 \u00A75.3 public-suffix rule. `secure: false` appears on BOTH sources\n * but for different reasons: a cloud target may serve a cert the local store rejects, and a local target\n * is served by portless from its own private CA \u2014 set there only for a loopback host (see\n * {@link isLoopbackTarget}). `headers` is present only when HTTP Basic Auth is injected (see\n * {@link buildBasicAuthHeader}) \u2014 it carries the `Authorization` header applied\n * uniformly to every route so upstream environments behind auth (e2e/staging)\n * stay reachable.\n */\nexport interface InfraKitViteProxyEntry {\n target: string\n changeOrigin: true\n secure?: false\n cookieDomainRewrite?: ''\n headers?: Record<string, string>\n}\n\n/** A Vite `server.proxy`-shaped map: path-prefix \u2192 proxy entry. */\nexport type InfraKitViteProxy = Record<string, InfraKitViteProxyEntry>\n\n/** Explicit HTTP Basic Auth credentials, overriding the `E2E__BASIC_AUTH_*` env vars. */\nexport interface InfraKitBasicAuth {\n username: string\n password: string\n}\n\nexport interface InfraKitDevOptions {\n /** Package dir whose `infra-kit.config.ts` is loaded. Defaults to `process.cwd()`. */\n cwd?: string\n /**\n * Explicit HTTP Basic Auth credentials injected into every proxy route as an\n * `Authorization` header. Takes precedence over the `E2E__BASIC_AUTH_USERNAME`\n * / `E2E__BASIC_AUTH_PASSWORD` env vars. Omit to use the env-based default.\n */\n basicAuth?: InfraKitBasicAuth\n /**\n * Vite's `command`. Pass it (`defineConfig(async ({ command }) => ({ server: await infraKitDev({ command }) }))`)\n * so `build` is a no-op: `server` is irrelevant to a build and proxy resolution would otherwise\n * fail-fast on a cloud route with no sourced env. Omit (or `'serve'`) for the dev-server config.\n */\n command?: 'build' | 'serve'\n /**\n * Apply the package's `dev.env` to `process.env` (see {@link applyDevEnv}). Default `true`. The plugin passes\n * `false` under vitest, which also runs `serve` config hooks but must see the env the tests were given.\n */\n applyDevEnv?: boolean\n /**\n * Explicit dev-server port. Omit for a **per-worktree dynamic** free port (a fresh OS-assigned\n * port) so N simultaneous git worktrees never collide on Vite's default `5173`; Vite prints the\n * chosen URL. Pass a fixed number only when an external contract pins the port.\n */\n port?: number\n /**\n * Interface the dev server binds. Defaults to {@link LOOPBACK_V4} so a portless alias can reach it\n * (Vite's own `localhost` default binds `[::1]` only, which the proxy cannot dial). Override when\n * the dev server must be reachable off the loopback \u2014 `'0.0.0.0'` or `true` inside a container, on\n * a LAN, or for an e2e runner on another host.\n */\n host?: string | boolean\n}\n\n/**\n * Build the `Authorization: Basic <base64(user:pass)>` header value. Credentials\n * come from `override` when given, else from `E2E__BASIC_AUTH_USERNAME` /\n * `E2E__BASIC_AUTH_PASSWORD` (NOTE: double underscore). Returns `undefined` when\n * either half is missing, so callers add no `headers` key at all.\n */\nconst buildBasicAuthHeader = (env: NodeJS.ProcessEnv, override?: InfraKitBasicAuth): string | undefined => {\n const username = override?.username ?? env.E2E__BASIC_AUTH_USERNAME\n const password = override?.password ?? env.E2E__BASIC_AUTH_PASSWORD\n\n if (!username || !password) return undefined\n\n const encoded = Buffer.from(`${username}:${password}`).toString('base64')\n\n return `Basic ${encoded}`\n}\n\n/**\n * Re-exported from the shared, dependency-light `lib/release-slug` module so the\n * published `infra-kit/vite` surface (`entry/vite.ts:7`) is unchanged while the\n * dev-server's fragment writer derives `<release>` from the SAME implementation\n * (no slug drift). See {@link slugifyRelease}.\n */\nexport { slugifyRelease }\n\n/** Fill the `<release>`/`<packageName>`/`<env>` placeholders in a URL template. */\nconst interpolate = (template: string, values: { release: string; packageName: string; env: string }): string => {\n return template\n .replaceAll('<release>', values.release)\n .replaceAll('<packageName>', values.packageName)\n .replaceAll('<env>', values.env)\n}\n\n/** The port a bare `http://` target implies, and the proxy's default \u2014 never spelled out in a URL. */\nconst DEFAULT_HTTP_PORT = 80\n\n/**\n * The interface the vite dev server binds. Pinned to IPv4 loopback because portless dials its routes\n * on `127.0.0.1`, while vite's default `host: 'localhost'` combined with its\n * `dns.setDefaultResultOrder('verbatim')` binds `[::1]` ONLY \u2014 the alias then resolves, the proxy\n * connects, and the connection is refused, surfacing as a 502 on every UI request. Backends never hit\n * this because `ServerlessLocalRun` already binds `127.0.0.1`. Still loopback-only: this narrows the\n * address family, it does not expose the dev server to the network.\n */\nconst LOOPBACK_V4 = '127.0.0.1'\n\n/**\n * LEGACY only ({@link resolveLegacyTarget}): graft the `proxyPort` a pre-v2 CLI recorded onto the\n * interpolated `templates.local` target. Such a CLI ran portless on whatever port it could bind and wrote\n * that port into the fragment; a current one publishes the finished `origin` and writes no `proxyPort`.\n * A template that already pins its own port keeps it.\n */\nconst withProxyPort = (target: string, proxyPort: number | undefined): string => {\n if (proxyPort == null || proxyPort === DEFAULT_HTTP_PORT) return target\n\n let url: URL\n\n try {\n url = new URL(target)\n } catch {\n throw new Error(\n `@slip-stream-kit/config/vite: dev.proxy templates.local resolved to \"${target}\", which is not a valid URL, so the proxy port ${proxyPort} cannot be applied.`,\n )\n }\n\n if (url.port !== '') return target\n url.port = String(proxyPort)\n\n // `URL.toString()` synthesizes a `/` pathname for an authority-only URL, so the common port-free\n // template would come back as `\u2026localhost:1355/`. Strip that ONLY when the template had no trailing\n // slash of its own \u2014 a template ending in `/` (e.g. `\u2026localhost/base/`) means it, and silently\n // trimming it would change the path the frontend proxies to.\n const grafted = url.toString()\n\n return !target.endsWith('/') && grafted.endsWith('/') ? grafted.slice(0, -1) : grafted\n}\n\n/**\n * The lowest port a daemon can bind without root. An origin-less fragment recording a port AT OR ABOVE\n * this was written by an old CLI whose `spawnDaemon` always passed `--no-tls`, so infra-kit itself could\n * only ever have put PLAIN HTTP there. Below it (80/443) the daemon was installed out-of-band, where\n * portless defaults to TLS ON \u2014 which is precisely why the legacy scheme is left alone down there.\n */\nconst LEGACY_SELF_SPAWNED_PORT_FLOOR = 1024\n\n// Why each branch is what it is:\n//\n// `>= 1024` is a **mitigation, not a proof** \u2014 infra-kit's own `spawnDaemon` always passed `--no-tls`,\n// so an unprivileged daemon *infra-kit created* was always plain HTTP; a HAND-STARTED TLS daemon on\n// such a port (portless's own default is TLS) would be wrongly downgraded here. That corner is\n// accepted: forcing `http` reproduces what the old CLI + old helper already emitted, so it regresses\n// nothing that worked.\n//\n// `< 1024` (80/443) or absent means the daemon was installed out-of-band and may well be TLS, and we\n// cannot know. Forcing `http` on a `:443` fragment would emit `http://<alias>:443`, plain HTTP into a\n// TLS listener \u2014 the exact silent failure this design exists to remove.\n/**\n * Legacy target resolution \u2014 reached ONLY for an origin-less fragment, i.e. one written by an OLD CLI\n * (a CLI-only rollback). It must reproduce the old helper rather than reject anything, because that is\n * what lets the CLI be reverted without touching a single consumer repo.\n *\n * `proxyPort >= 1024`: force the scheme to `http`.\n *\n * `proxyPort < 1024` (80/443) or absent: reproduce the old helper verbatim and NEVER rewrite the scheme.\n */\nconst resolveLegacyTarget = (target: string, proxyPort: number | undefined): string => {\n if (proxyPort != null && proxyPort >= LEGACY_SELF_SPAWNED_PORT_FLOOR) {\n return withProxyPort(target.replace(/^https:\\/\\//iu, 'http://'), proxyPort)\n }\n\n return withProxyPort(target, proxyPort)\n}\n\n/**\n * The proxy target for a route resolved to `local`. The runner PUBLISHES the contract and the helper OBEYS\n * it: when the fragment carries an `origin`, that string is the target verbatim \u2014 the runner is the only\n * party that knows the scheme and port the alias it registered is actually served on, and this helper ships\n * on its own release cadence, so any re-derivation here is a guess that can silently disagree.\n *\n * `templates.local` is consulted ONLY in legacy mode (see {@link resolveLegacyTarget}).\n *\n * The target proxies to a stable HOSTNAME, and the runner re-points that alias at the freshly-bound port\n * on every `--watch` restart, so it self-heals without Vite reloading its config.\n */\nconst resolveLocalTarget = (args: {\n route: InfraKitDevProxyRoute\n templates: InfraKitDevProxy['templates']\n env: string | undefined\n getRelease: () => string\n info: LocalPackageInfo | undefined\n}): string => {\n const { route, templates, env, getRelease, info } = args\n\n if (info?.origin) return info.origin\n\n // The fragment declared a wire version, which PROMISES an `origin` \u2014 and there isn't one. Falling into\n // legacy mode here would rebuild the target from `templates.local` and downgrade it to `http://`, i.e.\n // proxy plain HTTP into portless's TLS listener. That failure is silent (:80 answers with a 302 rather\n // than refusing), so nothing downstream would catch it. A fragment with NO `v` is a genuinely old writer\n // and still falls through to legacy below \u2014 that rollback path is intact. This branch is only the\n // impossible one: a writer that announced v2 and then broke its own promise.\n if (info?.wire != null) {\n throw new Error(\n `@slip-stream-kit/config/vite: the dev-context fragment for \"${route.packageName}\" declares wire v${info.wire} but ` +\n `carries no \\`origin\\`. A v${DEV_CONTEXT_WIRE_VERSION} writer must publish the exact origin it ` +\n `registered. Refusing to guess a target rather than silently proxy plain HTTP at a TLS listener \u2014 ` +\n `restart \\`infra-kit dev\\`, and if it persists the CLI and this helper are incompatible.`,\n )\n }\n\n const target = interpolate(templates.local, {\n // Prefer THIS package's runner-recorded release slug (R4) over the single global git derivation,\n // so cross-branch FE/BE pairings don't drift the emitted `<release>` from the segment the runner\n // aliased. Fall back to the global git slug only when the fragment carried no release.\n release: info?.release ?? getRelease(),\n // The local template resolves to a `<release>.<packageName>.localhost` alias the dev-server\n // registered with portless \u2014 which slugifies the package name to a legal DNS label. Slugify\n // identically here or a scoped name (`@hulyo/client-ui`) emits a target that no alias backs.\n // There is no `<repo>` token on purpose: only a pre-v2 CLI writes an origin-less fragment, and that\n // CLI registered its aliases without a repo label, so the legacy template is exact for it.\n packageName: slugifyHostLabel(route.packageName),\n env: env ?? '',\n })\n\n return resolveLegacyTarget(target, info?.proxyPort)\n}\n\n/**\n * Pick the effective source for a route: `local` when the route lists `local` as\n * a capability AND its packageName is in the local dev set; otherwise the fallback\n * \u2014 the declared `default` for a multi-source route, or the sole `from` entry for\n * a single-source one. Always resolves (the schema guarantees a usable fallback).\n */\nconst pickSource = (route: InfraKitDevProxyRoute, localSet: ReadonlySet<string>): InfraKitDevProxySource => {\n // `from` is guaranteed non-empty by the schema, so `from[0]` is always present.\n const fallback = route.default ?? route.from[0]!\n\n return route.from.includes('local') && localSet.has(route.packageName) ? 'local' : fallback\n}\n\n/**\n * Where this UI's cloud routes go: the value of its `deployedUrlEnv`, as the loaded environment set it.\n * The variable's NAME rides along so an unset one can be named in the error.\n */\nexport interface CloudOrigin {\n envVar: string | undefined\n url: string | undefined\n}\n\ninterface ResolveRouteArgs {\n routePath: string\n route: InfraKitDevProxyRoute\n templates: InfraKitDevProxy['templates']\n localSet: ReadonlySet<string>\n env: string | undefined\n cloud: CloudOrigin\n getRelease: () => string\n /** Per-package runtime data (recorded release/port) from the dev-context merge. */\n localInfo?: ReadonlyMap<string, LocalPackageInfo>\n}\n\n/** Hostnames that can only be this machine \u2014 the only ones a private-CA cert is allowed to go unchecked on. */\nconst LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1'])\n\n/**\n * Is `target` unambiguously on this machine? Only then may {@link resolveRoute} disable cert validation:\n * `templates.local` is an unconstrained string in a consumer-authored config, so an unconditional\n * `secure: false` in a PUBLISHED helper would silently switch off validation for whatever URL a consumer\n * wrote there. Scoping it to `.localhost`/loopback is what makes \"no MITM surface\" true by construction.\n * An unparseable target is NOT local (the caller then leaves `secure` unset \u2014 fail closed).\n */\nconst isLoopbackTarget = (target: string): boolean => {\n let hostname: string\n\n try {\n hostname = new URL(target).hostname.toLowerCase()\n } catch {\n return false\n }\n\n // `URL.hostname` brackets an IPv6 literal (`[::1]`); compare the bare address.\n const bare = hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname\n\n return bare.endsWith('.localhost') || LOOPBACK_HOSTNAMES.has(bare)\n}\n\n/** Resolve one route into a Vite proxy entry (or throw an actionable error). */\nconst resolveRoute = ({\n routePath,\n route,\n templates,\n localSet,\n env,\n cloud,\n getRelease,\n localInfo,\n}: ResolveRouteArgs): InfraKitViteProxyEntry => {\n const source = pickSource(route, localSet)\n\n if (source === 'local') {\n const target = resolveLocalTarget({ route, templates, env, getRelease, info: localInfo?.get(route.packageName) })\n\n // The local target is now HTTPS, served with a cert minted by portless's own private CA. Node's\n // http-proxy validates the upstream chain against the system store, which does not contain that CA,\n // so every proxied request fails with SELF_SIGNED_CERT_IN_CHAIN (confirmed empirically \u2014 see\n // `.omc/research/portless-https-spike.md`). Only a loopback target earns the exemption.\n return isLoopbackTarget(target) ? { target, changeOrigin: true, secure: false } : { target, changeOrigin: true }\n }\n\n if (!cloud.url) {\n const missing = cloud.envVar\n ? `${cloud.envVar} is not set`\n : 'this package declares no `deployedUrlEnv` to read it from'\n\n throw new Error(\n `@slip-stream-kit/config/vite: proxy route \"${routePath}\" resolves to the deployed app, but ${missing}. Load an environment from Doppler first (e.g. \\`infra-kit env-load -c dev\\`).`,\n )\n }\n\n return { target: cloud.url, changeOrigin: true, secure: false, cookieDomainRewrite: '' }\n}\n\ninterface ResolveProxyArgs {\n proxy: InfraKitDevProxy\n localSet: ReadonlySet<string>\n env: string | undefined\n cloud: CloudOrigin\n getRelease: () => string\n /** Pre-computed `Authorization` header value applied uniformly to every route. */\n authHeader?: string\n /** Per-package runtime data (recorded release/port) from the dev-context merge. */\n localInfo?: ReadonlyMap<string, LocalPackageInfo>\n}\n\n// Why order matters at all: vite matches `server.proxy` **first-hit-wins over insertion order** \u2014\n// `doesProxyContextMatchUrl` is `context[0] === '^' && new RegExp(context).test(url) ||\n// url.startsWith(context)` \u2014 so a config that happens to list `/api` before `/api/v1/cronjob`\n// silently routes every cronjob call to whatever `/api` resolves to. Emitting longest-first makes the\n// consumer's key order irrelevant for literal prefixes.\n//\n// **Length is the correct key, and provably so:** if key `B` captures a URL that key `A` also\n// captures, then both are prefixes of that URL, so one is a prefix of the other \u2014 `A.startsWith(B)` \u2014\n// and hence `A.length >= B.length`. Sorting by descending length therefore always places the more\n// specific of any overlapping pair first. Segment counting would be *worse*, because `startsWith` is\n// not segment-aware: `/api` captures `/api-docs` too, yet both are \"one segment\".\n//\n// **Why regex keys bail out.** A `^`-anchored key is schema-legal (`routes` is\n// `z.record(z.string().min(1), \u2026)`), and its capture set is unknowable to a sorter \u2014 length says\n// nothing about breadth, and there is no position, ahead or behind, that is right in general: a broad\n// `'^/(api|dynamic)'` must lose to `/api/v1/cronjob`, while a narrow one must not. Returning the\n// consumer's own order is at least the order they can reason about. No consumer uses a regex key\n// today; this exists so the sort can never invert one's intent.\n/**\n * Order the emitted routes so a more specific prefix always beats a more general one, by descending\n * key length. A `^`-anchored (regex) key is schema-legal and unsortable, so if ANY key is a regex the\n * map is returned in the consumer's own order, untouched.\n *\n * Governs only the routes INFRA-KIT emits. A consumer who writes their own `server.proxy` keeps\n * priority regardless: vite merges a plugin's config over the user's, placing user keys first.\n */\nconst orderBySpecificity = (routes: InfraKitViteProxy): InfraKitViteProxy => {\n const keys = Object.keys(routes)\n\n if (\n keys.some((key) => {\n return key.startsWith('^')\n })\n )\n return routes\n\n const ordered: InfraKitViteProxy = {}\n\n // `toSorted` is stable, so equal-length keys keep the consumer's relative order.\n for (const key of keys.toSorted((a, b) => {\n return b.length - a.length\n }))\n ordered[key] = routes[key]!\n\n return ordered\n}\n\n/**\n * Pure resolver: turn a `dev.proxy` config + resolved inputs (local set, env,\n * lazy release) into a Vite `server.proxy` map. Side-effect-free so the\n * resolution shape is fully unit-testable. When `authHeader` is set, every route\n * entry gets a matching `headers.Authorization`; otherwise no `headers` key.\n */\nexport const resolveProxyConfig = ({\n proxy,\n localSet,\n env,\n cloud,\n getRelease,\n authHeader,\n localInfo,\n}: ResolveProxyArgs): InfraKitViteProxy => {\n const result: InfraKitViteProxy = {}\n const headers = authHeader ? { Authorization: authHeader } : undefined\n\n for (const [routePath, route] of Object.entries(proxy.routes)) {\n const entry = resolveRoute({\n routePath,\n route,\n templates: proxy.templates,\n localSet,\n env,\n cloud,\n getRelease,\n localInfo,\n })\n\n result[routePath] = headers ? { ...entry, headers } : entry\n }\n\n return orderBySpecificity(result)\n}\n\n/** One `dev.proxy` route as {@link resolveProxyConfig} would resolve it, for a caller that reports rather than proxies. */\nexport interface ProxyRouteDescription {\n path: string\n packageName: string\n source: InfraKitDevProxySource\n /** `null` when it cannot be resolved: a cloud route whose URL is not loaded, or a local fragment that broke its wire promise. */\n target: string | null\n}\n\n/**\n * The route-by-route local/cloud split a UI's vite proxy is running with, most specific first. The same\n * `pickSource` and target resolution the proxy uses, but it never throws \u2014 `infra-kit e2e` reports the\n * topology it is testing against, and an unresolvable route is a finding there, not a crash.\n */\nexport const describeProxyRoutes = ({\n proxy,\n localContext,\n env,\n cloud,\n getRelease,\n}: {\n proxy: InfraKitDevProxy\n localContext: LocalContext\n env: string | undefined\n cloud: CloudOrigin\n getRelease: () => string\n}): ProxyRouteDescription[] => {\n const paths = Object.keys(proxy.routes).toSorted((a, b) => {\n return b.length - a.length\n })\n\n return paths.map((routePath) => {\n const route = proxy.routes[routePath]!\n const source = pickSource(route, localContext.packages)\n let target: string | null = null\n\n if (source === 'cloud') {\n target = cloud.url ?? null\n } else {\n try {\n target = resolveLocalTarget({\n route,\n templates: proxy.templates,\n env,\n getRelease,\n info: localContext.info.get(route.packageName),\n })\n } catch {\n target = null\n }\n }\n\n return { path: routePath, packageName: route.packageName, source, target }\n })\n}\n\n/**\n * Warn (never throw) when `dev.proxy.templates.local` is not `https://`. The portless daemon that\n * serves the local template is TLS-only, and {@link resolveLegacyTarget} only ever downgrades\n * https -> http for a legacy self-spawned port \u2014 it never upgrades \u2014 so an `http://` template\n * silently proxies into nothing reachable. Warn-first this release (schema still accepts any\n * non-empty string, see `packageConfigSchema`) so an unknown legit `http://` consumer isn't bricked;\n * make this a hard schema requirement next release.\n */\nconst warnIfNonHttpsLocalTemplate = (dev: InfraKitDev | undefined, configPath: string): void => {\n const local = dev?.proxy?.templates.local\n\n if (local !== undefined && !local.startsWith('https://')) {\n console.warn(\n `@slip-stream-kit/config: dev.proxy.templates.local in ${configPath} is \"${local}\", which is not an ` +\n `https:// URL. The local dev proxy target must be https:// (the portless daemon it points at serves ` +\n `TLS only) \u2014 https:// will become a REQUIRED value in a future release.`,\n )\n }\n}\n\n/** Memoize a zero-arg thunk so `<release>` git resolution runs at most once. */\nconst once = <T>(fn: () => T): (() => T) => {\n let cached: { value: T } | undefined\n\n return () => {\n cached ??= { value: fn() }\n\n return cached.value\n }\n}\n\n// Reached from two different processes, and only one of them is ours. Under the CLI (`infra-kit dev`,\n// `audit`) the entry has installed {@link ../node-warnings.suppressTypelessPackageJsonWarning}, so a\n// consumer package with no `\"type\"` loads quietly. Under `infraKitDev()` in a consumer's own\n// `vite.config.ts` we are a library in their process and do NOT patch their globals, so such a package\n// still prints Node's MODULE_TYPELESS_PACKAGE_JSON banner there. Harmless (it is a double-parse\n// notice), and latent while consumer UI packages declare `\"type\": \"module\"`.\n/** The deployed URL `deployedUrlEnv` names, as the loaded environment set it. */\nexport const readCloudOrigin = (envVar: string | undefined, env: NodeJS.ProcessEnv = process.env): CloudOrigin => {\n return { envVar, url: envVar ? env[envVar] || undefined : undefined }\n}\n\n/**\n * Apply a package's `dev.env` to `env` in place. It has to be this process's env rather than the spawn's:\n * `infra-kit dev` runs every UI under ONE turbo child, so only the UI's own vite process can tell them apart.\n * Vite runs config hooks before `loadEnv`, so a change made here reaches `import.meta.env`. `unset` only\n * removes the inherited variable: a value in the package's `.env` files still reaches `import.meta.env`.\n */\nexport const applyDevEnv = (devEnv: InfraKitDevEnv | undefined, env: NodeJS.ProcessEnv = process.env): void => {\n for (const name of devEnv?.unset ?? []) {\n delete env[name]\n }\n\n Object.assign(env, devEnv?.set)\n}\n\n/**\n * Load and validate a package's `infra-kit.config.ts`, or `undefined` when it is absent. The `.ts` config is\n * evaluated via Node's native type stripping (Node >= 24) \u2014 the same mechanism the CLI's config loader uses.\n * Cache-busted by mtime so repeated dev-server reloads pick up edits.\n */\nexport const loadPackageConfig = async (cwd: string): Promise<InfraKitPackageConfig | undefined> => {\n const configPath = path.join(cwd, PACKAGE_CONFIG_FILE)\n\n if (!fs.existsSync(configPath)) return undefined\n\n const stat = fs.statSync(configPath)\n const moduleUrl = `${pathToFileURL(configPath).href}?mtime=${Number(stat.mtimeMs)}`\n\n const imported = (await import(moduleUrl)) as { default?: unknown }\n const rawExport = imported.default\n\n if (rawExport === undefined) return undefined\n\n const resolved = typeof rawExport === 'function' ? await (rawExport as () => unknown)() : rawExport\n\n const parsed = packageConfigSchema.safeParse(resolved)\n\n if (!parsed.success) {\n throw new Error(\n `@slip-stream-kit/config: invalid ${PACKAGE_CONFIG_FILE} at ${configPath}: ${z.prettifyError(parsed.error)}`,\n )\n }\n\n return parsed.data as InfraKitPackageConfig\n}\n\n/** A package's `dev` block, or `undefined` when the config or the `dev` key is absent. */\nexport const loadDev = async (cwd: string): Promise<InfraKitDev | undefined> => {\n const dev = (await loadPackageConfig(cwd))?.dev\n\n warnIfNonHttpsLocalTemplate(dev, path.join(cwd, PACKAGE_CONFIG_FILE))\n\n return dev\n}\n\n/** Coerce a parsed dev-context.json into the set of locally-running package names. */\nconst extractPackages = (parsed: unknown): string[] => {\n if (Array.isArray(parsed)) {\n return parsed.filter((v): v is string => {\n return typeof v === 'string'\n })\n }\n\n if (parsed !== null && typeof parsed === 'object') {\n const candidate = (parsed as Record<string, unknown>).packages ?? (parsed as Record<string, unknown>).localPackages\n\n if (Array.isArray(candidate)) {\n return candidate.filter((v): v is string => {\n return typeof v === 'string'\n })\n }\n }\n\n return []\n}\n\n/** Search upward from `start` for `relative`, returning the first hit or undefined. */\nconst findUp = (start: string, relative: string): string | undefined => {\n let dir = path.resolve(start)\n\n for (;;) {\n const candidate = path.join(dir, relative)\n\n if (fs.existsSync(candidate)) return candidate\n\n const parent = path.dirname(dir)\n\n if (parent === dir) return undefined\n\n dir = parent\n }\n}\n\n/** Per-package runtime data merged from the dev-context fragment directory. */\nexport interface LocalPackageInfo {\n /** The real bound port the runner recorded. Provenance only \u2014 the proxy target is the alias host. */\n port: number\n /**\n * The authoritative local target (`https://<release>.<packageName>.<repo>.localhost`), published by the\n * runner. Present \u2192 {@link resolveLocalTarget} uses it verbatim and consults no template. Absent \u2192\n * the fragment came from an old CLI and the legacy template path runs.\n */\n origin?: string\n /**\n * The fragment's declared wire version (see {@link DEV_CONTEXT_WIRE_VERSION}). Absent \u2192 a pre-v2 writer,\n * so legacy mode is the correct reading. Present \u2192 the writer PROMISED an `origin`, and a missing one is\n * a broken fragment rather than an old one.\n */\n wire?: number\n /** LEGACY (no `origin`): the runner-recorded release slug, preferred over the helper's git derivation. */\n release?: string\n /** LEGACY (no `origin`): `<release>.<packageName>.localhost`, present only when an alias was registered. */\n alias?: string\n /** LEGACY (no `origin`): the port {@link alias} resolves on. `80` (the default) is implicit in the template. */\n proxyPort?: number\n}\n\n/**\n * The locally-running package set plus a `package \u2192 { port, release }` map merged\n * from the dev-context fragments. `packages` feeds `pickSource`; `info` lets a\n * `local` route emit the per-package recorded release (see {@link resolveRoute}).\n */\nexport interface LocalContext {\n packages: ReadonlySet<string>\n info: ReadonlyMap<string, LocalPackageInfo>\n}\n\n/** An empty {@link LocalContext} (frontend-only / dev-context absent). */\nconst emptyLocalContext = (): LocalContext => {\n return { packages: new Set(), info: new Map() }\n}\n\n/**\n * Is the runner that wrote a fragment still alive? Signal `0` performs the permission and existence\n * checks without delivering anything. `ESRCH` is the one answer that means \"gone\"; `EPERM` means the\n * pid exists under another uid, which is still a live process. A fragment with no `pid` predates the\n * field and is trusted (back-compat) rather than dropped.\n */\nconst isFragmentWriterAlive = (pid: number | undefined): boolean => {\n if (pid == null) return true\n\n try {\n process.kill(pid, 0)\n\n return true\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === 'EPERM'\n }\n}\n\n/**\n * Merge every `<app>.json` fragment in the dev-context directory into a\n * {@link LocalContext}. Two DISTINCT failure branches (do NOT collapse them into\n * one directory-wide catch):\n * - `readdir`-ENOENT (directory vanished mid-race) \u2192 empty context, matching the\n * dir-absent back-compat behaviour.\n * - a single corrupt/truncated/invalid `<app>.json` \u2192 that ONE fragment is\n * SKIPPED (per-fragment `safeParse` isolation); the rest still merge, so one bad\n * fragment never collapses the whole localSet to empty (which would silently\n * drop a started package to cloud).\n */\nconst readFragmentDir = (dir: string): LocalContext => {\n let entries: string[]\n\n try {\n entries = fs.readdirSync(dir)\n } catch {\n return emptyLocalContext()\n }\n\n const packages = new Set<string>()\n const info = new Map<string, LocalPackageInfo>()\n\n for (const name of entries) {\n if (!name.endsWith('.json')) continue\n\n try {\n const parsed = devContextFragmentSchema.safeParse(JSON.parse(fs.readFileSync(path.join(dir, name), 'utf-8')))\n\n if (!parsed.success) continue\n\n // A runner killed with SIGKILL (or crashed) leaves its fragment AND its portless alias behind.\n // Trusting it would route this package's `/api` at an alias nothing serves \u2014 a silent 502 with\n // no diagnostic. The writer already stamps its pid; honour it.\n if (!isFragmentWriterAlive(parsed.data.pid)) continue\n\n packages.add(parsed.data.package)\n info.set(parsed.data.package, {\n port: parsed.data.port,\n origin: parsed.data.origin,\n release: parsed.data.release,\n alias: parsed.data.alias,\n proxyPort: parsed.data.proxyPort,\n wire: parsed.data.v,\n })\n } catch {\n // Corrupt/truncated fragment (JSON.parse / read failure): skip ONLY this one.\n continue\n }\n }\n\n return { packages, info }\n}\n\n/**\n * Read the locally-running package context, searched upward from `cwd`. Prefers\n * the `.infra-kit/dev-context/` fragment DIRECTORY (merged via\n * {@link readFragmentDir}); when it is absent, falls back to the legacy single\n * `.infra-kit/dev-context.json` file (read as before \u2014 no per-package release);\n * when neither exists \u2192 empty context (frontend-only, every route resolves cloud).\n */\nexport const readLocalContext = (cwd: string): LocalContext => {\n const dir = findUp(cwd, DEV_CONTEXT_DIR)\n\n if (dir) return readFragmentDir(dir)\n\n const legacy = findUp(cwd, DEV_CONTEXT_FILE)\n\n if (!legacy) return emptyLocalContext()\n\n try {\n return { packages: new Set(extractPackages(JSON.parse(fs.readFileSync(legacy, 'utf-8')))), info: new Map() }\n } catch {\n return emptyLocalContext()\n }\n}\n\n/**\n * Read the set of locally-running packages (searched upward from `cwd`). Thin\n * wrapper over {@link readLocalContext} preserving the historical `Set`-returning\n * contract. Absent dev-context \u2192 empty set (frontend-only cloud resolution).\n */\nexport const readLocalSet = (cwd: string): ReadonlySet<string> => {\n return readLocalContext(cwd).packages\n}\n\n/**\n * An OS-assigned free TCP port on 127.0.0.1 \u2014 the per-worktree dev-server port (mirrors the\n * backend's `listen(0)`). This probes then releases the port, so there is a small TOCTOU window\n * before Vite binds it; Vite's default `strictPort: false` absorbs a lost race by stepping to the\n * next free port. Only a consumer that sets `strictPort: true` would hard-fail on the rare collision.\n */\nconst getFreePort = (): Promise<number> => {\n return new Promise((resolve, reject) => {\n const srv = net.createServer()\n\n srv.unref()\n srv.on('error', reject)\n srv.listen(0, '127.0.0.1', () => {\n const address = srv.address()\n const port = typeof address === 'object' && address !== null ? address.port : 0\n\n srv.close(() => {\n return resolve(port)\n })\n })\n })\n}\n\n/** Env var carrying the runner-assigned `{ \"<ui-package>\": <port> }` map (Layer B). */\nconst UI_PORTS_ENV = 'INFRA_KIT_UI_PORTS'\n\n/** This package's `package.json` `name`, or `null` (unreadable / nameless). */\nconst readPackageName = (cwd: string): string | null => {\n try {\n const parsed = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8')) as { name?: unknown }\n\n return typeof parsed.name === 'string' ? parsed.name : null\n } catch {\n return null\n }\n}\n\n/**\n * What the runner assigned THIS UI. `alias` is present only in the widened record form, i.e. only when the\n * runner both assigned the port AND registered the hostname \u2014 which is exactly the condition for pointing\n * HMR at that hostname. The legacy bare-number form yields no alias.\n */\ninterface ManagedUi {\n port: number\n alias?: string\n}\n\n/**\n * What `infra-kit dev` assigned THIS UI via `INFRA_KIT_UI_PORTS` (Layer B), or `null` when the map is\n * absent / this package isn't in it / it's malformed. Present \u2192 the helper binds the port with\n * `strictPort` so vite lands on exactly the port the runner already aliased. Absent (a standalone\n * `vite dev`) \u2192 the caller falls back to a free port, byte-identical to today.\n *\n * Accepts BOTH the widened `{ port, alias }` record and the legacy bare `<port>` number an older runner\n * writes \u2014 a new helper paired with an old CLI must still bind the assigned port.\n */\nconst resolveManagedUi = (cwd: string): ManagedUi | null => {\n const raw = process.env[UI_PORTS_ENV]\n\n if (raw == null || raw === '') return null\n\n const name = readPackageName(cwd)\n\n if (name == null) return null\n\n try {\n const entry = (JSON.parse(raw) as Record<string, unknown>)[name]\n\n if (typeof entry === 'number') return { port: entry }\n\n if (entry === null || typeof entry !== 'object') return null\n\n const { port, alias } = entry as { port?: unknown; alias?: unknown }\n\n if (typeof port !== 'number') return null\n\n return { port, alias: typeof alias === 'string' ? alias : undefined }\n } catch {\n return null\n }\n}\n\n/** The port an `https://` alias is served on \u2014 never spelled out in a URL, and what the HMR client dials. */\nconst HTTPS_PORT = 443\n\n// This is `server.ws`, NOT the older `server.hmr`: vite 8 deprecated `server.hmr.{protocol,host,port,\n// path,clientPort,timeout,server}` in favour of `server.ws.*` and warns on every dev boot that sets\n// them \u2014 a warning consumers were seeing in the wild, emitted by us. The field names are unchanged\n// (vite's `WsOptions` is `HmrOptions` minus `overlay`), so this is a key rename, not a behaviour\n// change. Vite 8 ships `setupHmrWsOptionCompat` to copy a legacy `hmr` onto `ws`, which is what makes\n// `ws` the forward direction and `hmr` the legacy one. Emitting both keys is NOT an option \u2014 it would\n// re-trigger the very deprecation warning being removed.\n/**\n * Vite's `server.ws` override, pointing the HMR client at the alias instead of at the raw dev-server port.\n * The page is loaded over `https://<alias>`, so its websocket must be `wss://` on the same origin or the\n * browser blocks it as mixed content; `clientPort` is the proxy's implicit `:443`, not vite's bound port.\n *\n * **Requires vite >= 8.** `server.ws` does not exist before it. Neither this package (zod-only) nor\n * `@slip-stream-kit/vite` (which imports vite as `import type`, erased at runtime) can detect the\n * version, so the floor is a documented requirement rather than a runtime check.\n */\nexport interface InfraKitViteWs {\n protocol: 'wss'\n host: string\n clientPort: number\n}\n\n/**\n * Resolve a package's `dev` block into a ready-made Vite `server` config: a per-worktree dev-server\n * `port` plus the `proxy` map. Loads the package's `infra-kit.config.ts`, merges the local dev set\n * from the `.infra-kit/dev-context/` fragment directory, and interpolates the local/cloud templates.\n * `<env>` comes from `INFRA_KIT_ENV`; `<release>` from each package's runner-recorded fragment when\n * present, else the slugified git branch (computed lazily, only when a local route needs it).\n * The package's `dev.env` is applied to `process.env` first (see {@link applyDevEnv}).\n *\n * `port` defaults to a fresh OS-assigned free port so simultaneous git worktrees never collide on\n * Vite's `5173` (override via `options.port`). `host` defaults to {@link LOOPBACK_V4} so the portless\n * alias can actually reach it (override via `options.host` for containers/LAN). `ws` is emitted only\n * when `infra-kit dev` registered an alias for this UI, so a bare `vite dev` keeps vite's own HMR\n * defaults. Pass `command` so `build` is a no-op (empty proxy, no port) \u2014 a build ignores `server` and\n * proxy resolution would otherwise fail-fast on a cloud route with no sourced env.\n *\n * @example\n * // vite.config.ts \u2014 the whole `server` field is the helper's output\n * import { infraKitDev } from 'infra-kit/vite'\n * export default defineConfig(async ({ command }) => ({ server: await infraKitDev({ command }) }))\n */\nexport const infraKitDev = async (\n options: InfraKitDevOptions = {},\n): Promise<{\n port?: number\n host?: string | boolean\n strictPort?: boolean\n ws?: InfraKitViteWs\n proxy: InfraKitViteProxy\n}> => {\n const cwd = options.cwd ?? process.cwd()\n\n // A build ignores `server`; skip the port + proxy work (proxy resolution would fail-fast on a\n // cloud route with no sourced env).\n if (options.command === 'build') return { proxy: {} }\n\n // Port precedence: explicit `options.port` > the runner-assigned Layer-B port (bound `strictPort` so\n // vite lands on exactly the aliased port) > a fresh free port (today's default; the portless-off path).\n const managed = options.port == null ? resolveManagedUi(cwd) : null\n const port = options.port ?? managed?.port ?? (await getFreePort())\n const strictPort = managed != null\n const host = options.host ?? LOOPBACK_V4\n\n // Override the HMR websocket ONLY when the runner handed us the alias it registered. A bare `vite dev`\n // (no runner) has no alias, and a computable hostname is not a registered one \u2014 pointing the HMR client\n // at a hostname nothing resolves would break hot reload on a path that works today.\n const ws: InfraKitViteWs | undefined =\n managed?.alias == null ? undefined : { protocol: 'wss', host: managed.alias, clientPort: HTTPS_PORT }\n\n const server = { port, host, ...(strictPort ? { strictPort } : {}), ...(ws ? { ws } : {}) }\n const config = await loadPackageConfig(cwd)\n const dev = config?.dev\n\n warnIfNonHttpsLocalTemplate(dev, path.join(cwd, PACKAGE_CONFIG_FILE))\n if (options.applyDevEnv !== false) applyDevEnv(dev?.env)\n\n if (!dev?.proxy) return { ...server, proxy: {} }\n\n const { packages: localSet, info: localInfo } = readLocalContext(cwd)\n const env = process.env[INFRA_KIT_ENV]\n const cloud = readCloudOrigin(config?.deployedUrlEnv)\n const authHeader = buildBasicAuthHeader(process.env, options.basicAuth)\n\n const getRelease = once(() => {\n return readRelease(cwd)\n })\n\n return {\n ...server,\n proxy: resolveProxyConfig({ proxy: dev.proxy, localSet, env, cloud, getRelease, authHeader, localInfo }),\n }\n}\n\n/**\n * Convenience wrapper returning just the proxy map (for spreading into\n * `server.proxy` directly). Equivalent to `(await infraKitDev(options)).proxy`.\n */\nexport const infraKitProxy = async (options: InfraKitDevOptions = {}): Promise<InfraKitViteProxy> => {\n return (await infraKitDev(options)).proxy\n}\n", "import fs from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\n\nimport { readAppAliasHost } from '../release-slug/app-alias'\nimport { loadPackageConfig } from '../vite/vite'\n\n/**\n * The line `infra-kit dev --reuse` prints once every app it serves answers its health probe. Playwright's\n * `webServer.wait.stdout` waits on it \u2014 a URL cannot stand in for it, because portless answers 404 for a\n * hostname nothing serves, and Playwright counts a 404 as \"server is up\".\n */\nexport const DEV_SERVING_MARKER = 'infra-kit dev: serving'\n\n/** `local` | `cloud` \u2014 which side a run targets. Unset: `cloud` on CI (no dev servers there), else `local`. */\nexport const E2E_MODE_ENV = 'INFRA_KIT_E2E'\n\nconst PREFIX = '@slip-stream-kit/config/playwright:'\n\nexport type InfraKitE2eMode = 'local' | 'cloud'\n\n/** The subset of Playwright's `TestConfig['webServer']` this helper fills in. */\nexport interface InfraKitWebServer {\n name: string\n command: string\n cwd: string\n wait: { stdout: RegExp }\n gracefulShutdown: { signal: 'SIGTERM'; timeout: number }\n timeout: number\n stdout: 'ignore'\n stderr: 'pipe'\n}\n\nexport interface InfraKitE2eSetup {\n mode: InfraKitE2eMode\n /** For `use.baseURL`. Never written back to the environment: see {@link infraKitE2e}. */\n baseURL: string\n /** The target's `deployedUrlEnv` \u2014 where a cloud run's URL comes from. Undefined when it declares none. */\n deployedUrlEnv: string | undefined\n /** For `use.ignoreHTTPSErrors`: local aliases are signed by portless's own CA. */\n ignoreHTTPSErrors: boolean\n /** For `webServer`. `undefined` for a cloud run \u2014 nothing is started. */\n webServer: InfraKitWebServer | undefined\n}\n\nexport interface InfraKitE2eOptions {\n /** The e2e package's directory (the one holding `infra-kit.config.ts`). Defaults to `process.cwd()`. */\n dir?: string\n env?: NodeJS.ProcessEnv\n /** How long a cold `infra-kit dev` may take to serve everything. Defaults to 5 minutes. */\n timeoutMs?: number\n}\n\nconst findWorkspaceRoot = (start: string): string => {\n let dir = start\n\n while (!fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) {\n const parent = path.dirname(dir)\n\n if (parent === dir) throw new Error(`${PREFIX} no pnpm-workspace.yaml above ${start}`)\n dir = parent\n }\n\n return dir\n}\n\nconst resolveMode = (env: NodeJS.ProcessEnv): InfraKitE2eMode => {\n const raw = env[E2E_MODE_ENV]\n\n if (raw === 'local' || raw === 'cloud') return raw\n if (raw) throw new Error(`${PREFIX} ${E2E_MODE_ENV}=\"${raw}\" (expected \"local\" or \"cloud\")`)\n\n return env.CI ? 'cloud' : 'local'\n}\n\n/**\n * Point a Playwright config at this worktree: the local alias of the package's `e2e.target`, served by\n * `infra-kit dev <app>` \u2014 reused when this worktree already runs it, started and stopped around the run\n * otherwise. A cloud run (`INFRA_KIT_E2E=cloud`, or CI) points at the deployed URL and starts nothing.\n *\n * Every entry point \u2014 `playwright test`, `--ui`, `--debug`, the editor extension, `infra-kit e2e` \u2014 goes\n * through the config, which is why this lives here rather than in a CLI command.\n *\n * @example\n * // apps/client/tests/playwright.config.ts\n * const e2e = await infraKitE2e({ dir: import.meta.dirname })\n *\n * export default defineConfig({\n * webServer: e2e.webServer,\n * use: { baseURL: e2e.baseURL, ignoreHTTPSErrors: e2e.ignoreHTTPSErrors },\n * })\n */\nexport const infraKitE2e = async (options: InfraKitE2eOptions = {}): Promise<InfraKitE2eSetup> => {\n const dir = options.dir ?? process.cwd()\n const env = options.env ?? process.env\n const e2e = (await loadPackageConfig(dir))?.e2e\n\n if (!e2e) {\n throw new Error(`${PREFIX} ${path.join(dir, 'infra-kit.config.ts')} declares no \\`e2e\\` block`)\n }\n\n if (e2e.server === 'static') {\n throw new Error(\n `${PREFIX} ${path.join(dir, 'infra-kit.config.ts')} declares \\`e2e: { server: 'static' }\\` \u2014 the suite serves its own build, so its playwright.config.ts sets baseURL and webServer itself instead of calling infraKitE2e()`,\n )\n }\n\n const root = findWorkspaceRoot(dir)\n const [app] = e2e.target.split('/') as [string]\n const targetDir = path.join(root, 'apps', e2e.target)\n const deployedUrlEnv = (await loadPackageConfig(targetDir))?.deployedUrlEnv\n const mode = resolveMode(env)\n\n if (mode === 'cloud') {\n if (!deployedUrlEnv) {\n throw new Error(\n `${PREFIX} a cloud run needs ${path.join(targetDir, 'infra-kit.config.ts')} to declare \\`deployedUrlEnv\\` \u2014 the variable holding ${e2e.target}'s deployed URL`,\n )\n }\n\n const baseURL = env[deployedUrlEnv]\n\n if (!baseURL) {\n throw new Error(\n `${PREFIX} a cloud run of ${e2e.target} needs ${deployedUrlEnv}, and it is not set. Load an environment with \\`infra-kit env-load -c <env>\\`, or unset ${E2E_MODE_ENV} to run locally.`,\n )\n }\n\n return { mode, baseURL, deployedUrlEnv, ignoreHTTPSErrors: false, webServer: undefined }\n }\n\n const packageName = (JSON.parse(fs.readFileSync(path.join(targetDir, 'package.json'), 'utf8')) as { name?: string })\n .name\n\n if (!packageName) throw new Error(`${PREFIX} ${targetDir}/package.json has no name`)\n\n // The deployed URL stays in the environment untouched: the dev server `webServer` starts inherits it,\n // and proxies the UI's cloud-only routes there. Overwriting it with the local alias would loop them back.\n const baseURL = `https://${readAppAliasHost(packageName, targetDir)}`\n\n return {\n mode,\n baseURL,\n deployedUrlEnv,\n ignoreHTTPSErrors: true,\n webServer: {\n name: 'infra-kit dev',\n command: `infra-kit dev ${app} --no-watch --reuse`,\n cwd: root,\n wait: { stdout: new RegExp(DEV_SERVING_MARKER) },\n // `dev` reaps its turbo/vite process groups on SIGTERM; Playwright's default SIGKILL would orphan them.\n gracefulShutdown: { signal: 'SIGTERM', timeout: 30_000 },\n timeout: options.timeoutMs ?? 300_000,\n stdout: 'ignore',\n stderr: 'pipe',\n },\n }\n}\n", "import { z } from 'zod'\n\n/**\n * Pure (node-free) vendor config schema + authoring helper. Kept separate from\n * `config.ts` (which imports node builtins for the runtime loader) so the public\n * lib entry can re-export `defineVendorConfig` without dragging node types into\n * the emitted `.d.ts`.\n */\n\n/**\n * Filename a source repo provides at its root to declare WHAT `vendor sync`\n * copies (`copy[]`). Lives ONLY on the write path \u2014 `vendor check` never loads it.\n * WHERE/WHICH to stamp (`workspaceDir` + `targets`) is machine-local and lives in\n * the user-global factory config (`~/.infra-kit/vendor.json`).\n */\nexport const VENDOR_CONFIG_FILE = 'vendor.config.ts'\n\n/**\n * A non-empty, repo-relative path with no `..` segments and no absolute prefix\n * (POSIX `/`, UNC/`\\`, or a Windows drive like `C:`). Containment guard for\n * vendor copy items so a malicious/typo config can't read or write outside the\n * source/target repo roots. Kept as a string/regex check (no `node:path`) to\n * respect this file's node-free constraint \u2014 `sync-ops.ts` does the resolved\n * runtime containment assert as defense in depth.\n */\nconst safeRelPath = z.string().refine(\n (p) => {\n const isAbsolute = /^(?:[/\\\\]|[a-z]:)/i.test(p)\n const hasDotDotSegment = p.split(/[\\\\/]/).includes('..')\n\n return !isAbsolute && !hasDotDotSegment && p.trim().length > 0\n },\n { message: 'must be a non-empty repo-relative path without \"..\" segments' },\n)\n\n/**\n * One item to sync from the source repo into each target. `vendored: true` marks\n * workspace packages that must land under `vendor/` (the single-source-of-truth\n * code); everything else is root-level tooling that stays at the repo root.\n */\nexport const vendorCopyItemSchema = z.object({\n name: z.string(),\n source: safeRelPath,\n target: safeRelPath,\n type: z.enum(['file', 'directory']),\n vendored: z.boolean().optional(),\n})\n\nexport const vendorConfigSchema = z\n .object({\n /** Files/dirs to copy. Items with `vendored: true` land under `vendor/`. */\n copy: z.array(vendorCopyItemSchema),\n })\n // Reject stray keys so a leftover `targets` (now machine-local, in\n // ~/.infra-kit/vendor.json) yields a clear \"unrecognized key\" error rather\n // than being silently ignored.\n .strict()\n\nexport type VendorCopyItem = z.infer<typeof vendorCopyItemSchema>\nexport type VendorConfig = z.infer<typeof vendorConfigSchema>\n\n/**\n * Identity helper for authoring a type-safe `vendor.config.ts` in a source repo.\n * Re-exported from the public lib entry so a source repo can\n * `import { defineVendorConfig } from 'infra-kit'`.\n *\n * NOTE: a `vendor.config.ts` must be type-strippable \u2014 Node's native type\n * stripping (Node >= 24) loads it without a build step, which forbids `enum`,\n * `namespace`, and parameter properties.\n *\n * @example\n * export default defineVendorConfig({\n * copy: [{ name: 'Configs', source: 'vendor/configs', target: 'vendor/configs', type: 'directory', vendored: true }],\n * })\n */\nexport const defineVendorConfig = (config: VendorConfig): VendorConfig => {\n return config\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\n\n/**\n * Pure (node-free) schema for a consumer repo's deploy manifest. `infra-kit deploy-sync` reads it and\n * generates the repo's deploy workflows from it, so CI runs plain YAML and never needs the CLI.\n *\n * A separate file rather than a `deploy` key in `infra-kit.json` / `infra-kit.config.ts`: both of those\n * are strict schemas parsed by every installed CLI and by the vite plugin, so a new key would break\n * every teammate and CI build still on an older release. Nothing but `deploy-sync` reads this file.\n */\nexport const DEPLOY_CONFIG_FILE = 'infra-kit.deploy.ts'\n\n/** Environment and service names end up in job ids, input keys and secret names. */\nconst kebabName = z.string().regex(/^[a-z][a-z0-9-]*$/, 'must be lower-case kebab-case, e.g. client-be')\n\n/** A reusable workflow in the consumer's own `.github/workflows/`, by file name. */\nconst localWorkflowFile = z\n .string()\n .regex(/^[\\w.-]+\\.ya?ml$/, 'must be a workflow file name in .github/workflows, e.g. _deploy-media-jobs.yml')\n\nconst secretName = z.string().regex(/^[A-Z_][A-Z0-9_]*$/, 'must be an upper-case secret name, e.g. MONITORING_IP')\n\n/** Service names become dispatch inputs, so they must not shadow the ones every workflow has. */\nconst RESERVED_SERVICE_NAMES = new Set(['environment'])\n\nexport const deployServiceSchema = z.strictObject({\n /** The only environments this service deploys to. Omitted = every environment. */\n envs: z.array(kebabName).nonempty().optional(),\n /** Environments this service never deploys to; every other one is allowed. Exclusive with `envs`. */\n skipEnvs: z.array(kebabName).nonempty().optional(),\n /** GitHub runner label. Omitted = the shared job's default (`ubuntu-24.04-arm`). */\n runner: z\n .string()\n .regex(/^[\\w.-]+$/, 'must be a runner label, e.g. macos-14')\n .optional(),\n /**\n * Deploy through this repo's own reusable workflow instead of `devops/scripts/deploy-<service>.sh`\n * in the shared job. It is called with `branch` and `environment` inputs and `secrets: inherit`.\n */\n workflow: localWorkflowFile.optional(),\n /**\n * The service's TypeScript deploy, run by `infra-kit deploy-run`, as a repo-relative path; put it in\n * the app it deploys (`apps/client/api/deploy.ts`) and it defaults to that app. Omitted =\n * `devops/scripts/deploy-<service>.sh`.\n */\n deploy: z\n .string()\n .regex(/^(?!.*\\.\\.)[\\w./-]+\\.ts$/, 'must be a repo-relative .ts path, e.g. apps/client/api/deploy.ts')\n .optional(),\n /**\n * `false` leaves the service out of the deploy-all workflow; a list keeps it in deploy-all for those\n * environments only, e.g. `['prod']`. Either way it stays selectable wherever it may deploy.\n */\n deployAll: z.union([z.boolean(), z.array(kebabName).nonempty()]).optional(),\n /**\n * Services this one waits for in the same run, e.g. two services that redeploy one API Gateway stage.\n * A dependency that fails stops it; one that was not selected or not offered here does not.\n */\n after: z.array(kebabName).nonempty().optional(),\n /**\n * Makes the service a step rather than a checkbox, e.g. DB migrations: it runs whenever one of these\n * services deploys, before them, and its failure stops them.\n */\n runsBefore: z.array(kebabName).nonempty().optional(),\n})\n\ntype ServiceMap = Record<string, z.infer<typeof deployServiceSchema>>\n\n/** Every service mapped to the ones it waits for, whether named by its `after` or by a step's `runsBefore`. */\nexport const serviceDependencies = (services: ServiceMap): Map<string, string[]> => {\n const dependencies = new Map(\n Object.entries(services).map(([name, service]) => {\n return [name, [...(service.after ?? [])]]\n }),\n )\n\n for (const [step, service] of Object.entries(services)) {\n for (const target of service.runsBefore ?? []) dependencies.get(target)?.push(step)\n }\n\n return dependencies\n}\n\nconst checkServiceOrder = (services: ServiceMap, ctx: z.RefinementCtx): void => {\n for (const [name, service] of Object.entries(services)) {\n for (const key of ['after', 'runsBefore'] as const) {\n for (const [index, other] of (service[key] ?? []).entries()) {\n if (other === name) {\n ctx.addIssue({ code: 'custom', path: ['services', name, key, index], message: 'names the service itself' })\n } else if (!(other in services)) {\n ctx.addIssue({ code: 'custom', path: ['services', name, key, index], message: `no service '${other}'` })\n }\n }\n }\n\n if (service.runsBefore !== undefined && service.deployAll !== undefined) {\n ctx.addIssue({\n code: 'custom',\n path: ['services', name],\n message: 'a runsBefore step deploys with its services, so deployAll does not apply',\n })\n }\n }\n\n const dependencies = serviceDependencies(services)\n const done = new Set<string>()\n const visiting = new Set<string>()\n\n const visit = (name: string, chain: string[]): void => {\n if (done.has(name)) return\n\n if (visiting.has(name)) {\n ctx.addIssue({ code: 'custom', path: ['services'], message: `order cycle: ${[...chain, name].join(' \u2192 ')}` })\n\n return\n }\n\n visiting.add(name)\n\n for (const dependency of dependencies.get(name) ?? []) visit(dependency, [...chain, name])\n\n visiting.delete(name)\n done.add(name)\n }\n\n for (const name of dependencies.keys()) visit(name, [])\n}\n\nexport const deployConfigSchema = z\n .strictObject({\n /**\n * The `<project>` in this repo's `/<project>/...` SSM parameters, e.g. `hulyo`, so a deploy script\n * shared across repos reads its parameters without hard-coding the prefix.\n */\n project: z\n .string()\n .regex(/^[a-z0-9][a-z0-9-]*$/, 'must be lower-case letters, digits and dashes, e.g. hulyo')\n .optional(),\n /** The environments the generated workflows offer, in dispatch-menu order. */\n environments: z.array(kebabName).nonempty(),\n /** Preselected in the dispatch menu. Omitted = the first environment. */\n defaultEnvironment: kebabName.optional(),\n /** Every deployable service, keyed by its `devops/scripts/deploy-<name>.sh` suffix, in menu order. */\n services: z.record(kebabName, deployServiceSchema),\n /** Reusable workflows applied before any service deploys; skippable per run. */\n terraform: z.array(localWorkflowFile).optional(),\n /**\n * Reusable workflows run after terraform (applied or skipped) and before any service, e.g. publishing\n * a Lambda layer the services resolve at deploy time. Not skipped with terraform; a failure stops every service.\n */\n preDeploy: z.array(localWorkflowFile).optional(),\n /** Reusable workflow called once every deploy job finished without a failure. */\n notifySuccess: localWorkflowFile.optional(),\n /** Repo secrets exported, under the same name, to every deploy script. */\n extraEnv: z.array(secretName).optional(),\n /**\n * Minutes the deploy step of the shared job may run. Omitted = the shared job's default, 25. A\n * timed-out step fails and keeps its logs, where a job cancelled at GitHub's 360-minute cap loses them.\n */\n deployTimeoutMinutes: z.number().int().min(1).max(360).optional(),\n /**\n * Generate under the v1 names, `deploy-all.yml` and `deploy-selected-services.yml`, replacing the\n * hand-written files. Those are the files `infra-kit release deploy-*` dispatches, so the manifest\n * becomes what the CLI deploys and reads its service gates from. Omitted = the `-v2` pilot files,\n * which the CLI ignores. A repo that delivers prod needs `prod` in `environments` first: delivery\n * dispatches `deploy-all.yml` with it.\n */\n replaceV1: z.boolean().optional(),\n /**\n * The deploy job reads its environment from this Doppler project, config = the dispatched environment,\n * with the token in the `<env>_DOPPLER_TOKEN` secret: AWS keys (and an `AWS_DEPLOY_ROLE_ARN` to\n * assume), Slack and every secret the deploy scripts read. No AWS secrets are passed then.\n */\n doppler: z\n .strictObject({\n project: z.string().regex(/^[a-z0-9][a-z0-9_-]*$/, 'must be a Doppler project name, e.g. nomadream'),\n })\n .optional(),\n })\n .superRefine((config, ctx) => {\n if (Object.keys(config.services).length === 0) {\n ctx.addIssue({ code: 'custom', path: ['services'], message: 'declare at least one service' })\n }\n\n for (const [name, service] of Object.entries(config.services)) {\n if (RESERVED_SERVICE_NAMES.has(name)) {\n ctx.addIssue({ code: 'custom', path: ['services', name], message: 'is a dispatch input of its own' })\n }\n\n if (service.workflow !== undefined && service.deploy !== undefined) {\n ctx.addIssue({ code: 'custom', path: ['services', name], message: 'set workflow or deploy, not both' })\n }\n\n if (service.envs !== undefined && service.skipEnvs !== undefined) {\n ctx.addIssue({ code: 'custom', path: ['services', name], message: 'set envs or skipEnvs, not both' })\n }\n }\n\n checkServiceOrder(config.services, ctx)\n\n for (const [index, file] of (config.preDeploy ?? []).entries()) {\n if (config.terraform?.includes(file)) {\n ctx.addIssue({ code: 'custom', path: ['preDeploy', index], message: 'already listed under terraform' })\n }\n }\n\n if (config.defaultEnvironment !== undefined && !config.environments.includes(config.defaultEnvironment)) {\n ctx.addIssue({\n code: 'custom',\n path: ['defaultEnvironment'],\n message: `must be one of environments (${config.environments.join(', ')})`,\n })\n }\n })\n\nexport type DeployService = z.infer<typeof deployServiceSchema>\nexport type DeployConfig = z.infer<typeof deployConfigSchema>\n\n/**\n * Identity helper for authoring a type-safe `infra-kit.deploy.ts`.\n *\n * A manifest can equally use `import type { DeployConfig }` with `satisfies DeployConfig`: the type\n * import is erased when the CLI loads the file, so it keeps working against an older installed\n * `@slip-stream-kit/config` that predates this helper.\n *\n * @example\n * export default defineDeployConfig({\n * environments: ['dev', 'stage'],\n * services: { 'client-be': {}, 'client-fe': { runner: 'macos-14' } },\n * })\n */\nexport const defineDeployConfig = (config: DeployConfig): DeployConfig => {\n return config\n}\n", "import { z } from 'zod'\n\n/**\n * Schema for the resolved (post-factory) package config object. `strictObject`\n * rejects unknown keys so typos in `infra-kit.config.ts` surface as validation\n * errors instead of being silently ignored.\n *\n * Kept in its own module \u2014 separate from the public `defineConfig`/types entry \u2014\n * so the published `infra-kit` type surface stays free of a `zod` import.\n */\nconst ENV_VAR_NAME = /^[A-Z_][A-Z0-9_]*$/\n\n/**\n * A key the schema used to accept. Refused with the replacement named, rather than as an anonymous\n * unknown key: the config is TypeScript, so there is no migration to rewrite it \u2014 the message is the fix.\n */\nconst retired = (replacement: string) => {\n return z.never({ error: replacement }).optional()\n}\n\nexport const packageConfigSchema = z\n .strictObject({\n requiredScripts: z.array(z.string().min(1)).optional(),\n requiredFiles: z.array(z.string().min(1)).optional(),\n // No `.default()` here: `.partial()` elsewhere in the config-loading pipeline preserves ZodDefault,\n // so a default on an optional key would make an EMPTY override layer parse to that default and\n // shallow-merge over a real setting from an earlier layer. Defaults belong at the read site.\n type: z.enum(['frontend', 'backend', 'lib', 'e2e', 'mobile']).optional(),\n // The deployed origin differs per environment (prod is not `<env>`-shaped), so the config names the\n // variable and Doppler holds the value \u2014 the one source both the dev proxy and e2e read.\n deployedUrlEnv: z\n .string()\n .regex(ENV_VAR_NAME, 'deployedUrlEnv must be an env var name (e.g. `CLIENT_URL`)')\n .optional(),\n turbo: z\n .strictObject({\n requiredTasks: z.array(z.string().min(1)).optional(),\n })\n .optional(),\n dev: z\n .strictObject({\n proxy: z\n .strictObject({\n templates: z.strictObject({\n // Accepted-but-warned this release regardless of scheme (the portless daemon this template\n // targets serves TLS only, so a non-`https://` value is almost certainly a mistake \u2014 see the\n // warning emitted from `loadDev` in `../vite/vite.ts`). Make `https://` a hard schema\n // requirement next release, once both consumer repos have had a release to pick up the warning.\n local: z.string().min(1),\n cloud: retired(\n \"`templates.cloud` was replaced by `deployedUrlEnv`: set `deployedUrlEnv: '<APP>_URL'` at the top of this config and keep the URL per environment in Doppler\",\n ),\n }),\n routes: z.record(\n z.string().min(1),\n z\n .strictObject({\n packageName: z.string().min(1),\n from: z.array(z.enum(['local', 'cloud'])).min(1),\n default: z.enum(['local', 'cloud']).optional(),\n })\n .refine(\n (route) => {\n return route.from.length <= 1 || route.default !== undefined\n },\n {\n message: 'default is required when `from` has more than one source',\n },\n )\n .refine(\n (route) => {\n return route.default === undefined || route.from.includes(route.default)\n },\n {\n message: 'default must be listed in `from`',\n },\n ),\n ),\n })\n .optional(),\n env: z\n .strictObject({\n unset: z.array(z.string().regex(ENV_VAR_NAME, 'dev.env.unset entries must be env var names')).optional(),\n set: z\n .record(z.string().regex(ENV_VAR_NAME, 'dev.env.set keys must be env var names'), z.string())\n .optional(),\n })\n .refine(\n (env) => {\n return !env.unset?.some((name) => {\n return env.set?.[name] !== undefined\n })\n },\n { message: 'a variable cannot be in both `dev.env.unset` and `dev.env.set`' },\n )\n .optional(),\n })\n .optional(),\n e2e: z\n .strictObject({\n target: z\n .string()\n .regex(/^[^/\\s]+\\/(?:ui|api)$/, 'target must be `<app>/ui` or `<app>/api` (e.g. `client/ui`)')\n .optional(),\n server: z.literal('static').optional(),\n baseUrlEnv: retired(\n \"`e2e.baseUrlEnv` was removed: the deployed URL is the target package's own `deployedUrlEnv`\",\n ),\n cloud: retired(\"`e2e.cloud` was removed: the deployed URL is the target package's own `deployedUrlEnv`\"),\n })\n // One object with a refine rather than a union of two: a union reports a retired key as a bare\n // \"Invalid input\" instead of its replacement message.\n .refine(\n (e2e) => {\n return (e2e.target === undefined) !== (e2e.server === undefined)\n },\n {\n message:\n \"`e2e` declares exactly one of `target` (the package `infra-kit dev` serves, e.g. `client/ui`) or `server: 'static'` (the suite serves its own build)\",\n },\n )\n .optional(),\n })\n .refine(\n (config) => {\n const routes = Object.values(config.dev?.proxy?.routes ?? {})\n\n return (\n config.deployedUrlEnv !== undefined ||\n !routes.some((route) => {\n return route.from.includes('cloud')\n })\n )\n },\n {\n message:\n \"a `dev.proxy` route that can go to cloud needs `deployedUrlEnv` \u2014 the variable holding this app's deployed URL\",\n path: ['deployedUrlEnv'],\n },\n )\n", "import { execFileSync } from 'node:child_process'\n\nimport { DEFAULT_RELEASE_SLUG, slugifyRelease } from './release-slug'\n\n/**\n * The `<release>` label for the git branch `cwd` is on, or {@link DEFAULT_RELEASE_SLUG} outside a repo or on\n * a branch that slugifies to nothing. Never throws.\n *\n * The dev-server's alias writer, the vite helper and the Playwright helper all name the same host from it,\n * so there is one implementation: a second copy that disagreed would print a URL no alias backs.\n *\n * @example\n * readRelease('/repo-on-feature/HUL-123') // => 'hul-123'\n */\nexport const readRelease = (cwd: string): string => {\n try {\n // eslint-disable-next-line sonarjs/no-os-command-from-path\n const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {\n cwd,\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'ignore'],\n }).trim()\n\n return slugifyRelease(branch) || DEFAULT_RELEASE_SLUG\n } catch {\n return DEFAULT_RELEASE_SLUG\n }\n}\n", "/**\n * Shared, dependency-light release-slug helper.\n *\n * Extracted from `lib/vite/vite.ts` so BOTH the published `infra-kit/vite` helper\n * (which re-exports it) and the dev-server's dev-context fragment writer derive the\n * `<release>` hostname segment from ONE implementation \u2014 the recorded slug and the\n * helper-computed slug can never drift. Pure (regex only, no imports) so importing it\n * into the lightweight `infra-kit/vite` bundle stays cheap.\n */\n\n/** RFC 1035: a DNS label holds at most 63 octets; a longer one makes the whole hostname invalid. */\nconst MAX_DNS_LABEL_LENGTH = 63\n\n/**\n * Slugify an arbitrary string into a single DNS label: lowercase, collapse every\n * non-alphanumeric run to `-`, drop leading/trailing separators, and cap it at 63 characters. Returns `''`\n * when the input carries no alphanumeric run (the caller must treat that as \"no label\").\n *\n * portless rejects any hostname outside `[a-z0-9.-]`, so every segment fed into a\n * `<release>.<packageName>.<repo>.localhost` alias must pass through here. A scoped npm name\n * (`@hulyo/client-ui`) is the motivating case: registering it raw fails with\n * `Invalid hostname`, and because the driver swallows that into a best-effort `false`,\n * the whole hero-URL path degrades silently to `localhost:<port>`.\n *\n * Distinct from {@link slugifyRelease}: this does NOT strip a git-flow prefix, so a\n * package legitimately named `fix-utils` keeps its `fix-` prefix.\n *\n * @example\n * slugifyHostLabel('@hulyo/client-ui') // => 'hulyo-client-ui'\n * slugifyHostLabel('backend-api') // => 'backend-api'\n */\nexport const slugifyHostLabel = (label: string): string => {\n // Collect the alphanumeric runs and join with `-`. Doing it this way (rather\n // than collapse-then-trim) is linear and sidesteps a super-linear trim regex,\n // while inherently dropping any leading/trailing separators.\n const runs = label.toLowerCase().match(/[a-z0-9]+/g)\n const capped = runs ? runs.join('-').slice(0, MAX_DNS_LABEL_LENGTH) : ''\n\n // Runs are joined by single dashes, so the cut leaves at most one behind.\n return capped.endsWith('-') ? capped.slice(0, -1) : capped\n}\n\n/**\n * Slugify a git branch into a `<release>` token: strip a leading git-flow prefix\n * (`feature/`, `release/`, \u2026), then reduce the remainder to a single DNS label.\n *\n * @example\n * slugifyRelease('release/2.4') // => '2-4'\n * slugifyRelease('feature/HUL-123') // => 'hul-123'\n */\nexport const slugifyRelease = (branch: string): string => {\n return slugifyHostLabel(branch.replace(/^(?:feature|feat|release|hotfix|bugfix|fix|chore)\\//i, ''))\n}\n\n/**\n * The `<release>` label used when no git branch resolves (outside a repo, or a branch that slugifies to\n * nothing). Both the dev-server's alias writer and `infra-kit/vite`'s template interpolation fall back\n * to this SAME constant \u2014 a divergence here would emit a hostname no alias backs.\n */\nexport const DEFAULT_RELEASE_SLUG = 'local'\n", "import { execFileSync } from 'node:child_process'\nimport fs from 'node:fs'\nimport path from 'node:path'\n\nimport { slugifyHostLabel } from './release-slug'\n\n/** The `<repo>` label used outside a git repo, or when the repo's directory name slugifies to nothing. */\nexport const DEFAULT_REPO_SLUG = 'repo'\n\nexport interface RepoIdentity {\n /**\n * The repo's own directory, shared by every worktree: the main checkout, or a bare repo's `foo.git`\n * itself (never its parent, which would take in every sibling). `null` outside git and for a submodule,\n * which has no main checkout to converge on \u2014 callers fall back to their own toplevel.\n */\n root: string | null\n /** The `<repo>` alias label: {@link root}'s `infra-kit.json` `aliasRepo`, else its name minus `.git`, slugified. */\n slug: string\n}\n\n/**\n * The `aliasRepo` the main checkout's `infra-kit.json` sets, slugified, or `''` when it sets none.\n *\n * Read from the main checkout rather than the caller's worktree so every worktree names the repo alike, and\n * with a bare `JSON.parse` rather than the CLI's loader: the vite and Playwright helpers import this, and the\n * CLI's strict schema has already refused a bad value before any server registered under it.\n */\nconst readConfiguredRepoSlug = (root: string): string => {\n try {\n const config: unknown = JSON.parse(fs.readFileSync(path.join(root, 'infra-kit.json'), 'utf-8'))\n const aliasRepo = (config as { aliasRepo?: unknown } | null)?.aliasRepo\n\n return typeof aliasRepo === 'string' ? slugifyHostLabel(aliasRepo) : ''\n } catch {\n return ''\n }\n}\n\n/**\n * Which repository `cwd` belongs to, read from `git rev-parse --git-common-dir` so every worktree of one\n * repo gives the same answer (`hulyo-monorepo-worktrees/feature/x` is still `hulyo-monorepo`). Never throws.\n *\n * The one source for both the dev alias's `<repo>` label and the directories `env-load` scopes a load to,\n * so the two can never disagree about what \"this repo\" is. Git rather than `infra-kit.json`'s\n * `envManagement` name: git is already required for `<release>`, and the config name is optional, layered,\n * and would pull the config loader into the lightweight vite/Playwright helpers. Only the label can be\n * overridden, by `aliasRepo`, to shorten the host; `root` always comes from git.\n *\n * @example\n * readRepoIdentity('/projects/hulyo-monorepo-worktrees/feature/x/apps/web/ui')\n * // => { root: '/projects/hulyo-monorepo', slug: 'hulyo-monorepo' }\n */\nexport const readRepoIdentity = (cwd: string): RepoIdentity => {\n let commonDir: string\n\n try {\n // eslint-disable-next-line sonarjs/no-os-command-from-path\n commonDir = execFileSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {\n cwd,\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'ignore'],\n }).trim()\n } catch {\n return { root: null, slug: DEFAULT_REPO_SLUG }\n }\n\n const name = path.basename(commonDir)\n\n if (commonDir.includes(`${path.sep}.git${path.sep}modules${path.sep}`)) {\n return { root: null, slug: slugifyHostLabel(name) || DEFAULT_REPO_SLUG }\n }\n\n // `.git` (a normal checkout) or a dot-dir like `.bare` sits inside the repo's directory; anything else is\n // a bare repo, which is its own directory.\n const root = name.startsWith('.') ? path.dirname(commonDir) : commonDir\n\n return {\n root,\n slug:\n readConfiguredRepoSlug(root) || slugifyHostLabel(path.basename(root).replace(/\\.git$/, '')) || DEFAULT_REPO_SLUG,\n }\n}\n\n/** The `<repo>` alias label \u2014 {@link readRepoIdentity}'s `slug`. */\nexport const readRepoSlug = (cwd: string): string => {\n return readRepoIdentity(cwd).slug\n}\n", "import { readRelease } from './read-release'\nimport { slugifyHostLabel } from './release-slug'\nimport { readRepoSlug } from './repo-slug'\n\n/**\n * The portless alias NAME (`<release>.<label>.<repo>`, no `.localhost`) for the package in `appDir`.\n *\n * `<repo>` is what keeps two repos apart: travelist and hulyo both have a `website-ui` and both cut\n * same-named branches, and without it their aliases were identical, so `dev --reuse` adopted the other\n * repo's server. The dev-server registers this name, and `infraKitE2e()` and `infra-kit e2e` aim at it,\n * so all three must come from here.\n *\n * @throws When the package name yields no legal DNS label.\n * @example\n * readAppAliasName('@hulyo/website-ui', '/projects/hulyo-monorepo/apps/website/ui') // => 'dev.hulyo-website-ui.hulyo-monorepo'\n */\nexport const readAppAliasName = (packageName: string, appDir: string): string => {\n const label = slugifyHostLabel(packageName)\n\n if (label === '') {\n throw new Error(`infra-kit: package name \"${packageName}\" has no letters or digits to build a hostname from.`)\n }\n\n return `${readRelease(appDir)}.${label}.${readRepoSlug(appDir)}`\n}\n\n/** {@link readAppAliasName} as the hostname a browser dials (`<release>.<label>.<repo>.localhost`). */\nexport const readAppAliasHost = (packageName: string, appDir: string): string => {\n return `${readAppAliasName(packageName, appDir)}.localhost`\n}\n", "import { Buffer } from 'node:buffer'\nimport fs from 'node:fs'\nimport net from 'node:net'\nimport path from 'node:path'\nimport process from 'node:process'\nimport { pathToFileURL } from 'node:url'\nimport { z } from 'zod'\n\nimport type {\n InfraKitDev,\n InfraKitDevEnv,\n InfraKitDevProxy,\n InfraKitDevProxyRoute,\n InfraKitDevProxySource,\n InfraKitPackageConfig,\n} from '../package-config/package-config'\nimport { packageConfigSchema } from '../package-config/package-config-schema'\nimport { readRelease } from '../release-slug/read-release'\nimport { slugifyHostLabel, slugifyRelease } from '../release-slug/release-slug'\n\n/**\n * Env-name handle read at vite-config time to fill the `<env>` placeholder in a\n * cloud proxy target. Inlined (not imported from `src/lib/constants`) to keep the\n * `infra-kit/vite` bundle's import graph tiny \u2014 see the module header. Kept\n * byte-identical to `INFRA_KIT_ENV_VAR` in `src/lib/constants/constants.ts`.\n */\nconst INFRA_KIT_ENV = 'INFRA_KIT_ENV'\n\n/** Per-package config filename, mirrored from the CLI's `PACKAGE_CONFIG_FILE`. */\nconst PACKAGE_CONFIG_FILE = 'infra-kit.config.ts'\n\n/**\n * Repo-relative dev-context fragment DIRECTORY, searched upward from cwd. Each\n * runner (single-process or Orca pane) writes its OWN `<app>.json` fragment here\n * recording its real bound port + release; the helper merges them (see\n * {@link readLocalContext}). This is the current source of truth.\n */\nconst DEV_CONTEXT_DIR = path.join('.infra-kit', 'dev-context')\n\n/**\n * Legacy single-file dev-context manifest, searched upward from cwd. Read only as\n * a transitional back-compat path when {@link DEV_CONTEXT_DIR} is absent (the\n * directory wins per-package when both exist).\n */\nconst DEV_CONTEXT_FILE = path.join('.infra-kit', 'dev-context.json')\n\n// Kept lenient (not `.strict()`) so extra writer fields never reject a valid fragment, and\n// deliberately un-`refine`d: a rejected fragment would drop a started package to `cloud` instead of\n// falling into legacy mode, which is what makes the rollback path work.\n/**\n * One `.infra-kit/dev-context/<app>.json` fragment. `package` is the load-bearing\n * field the helper reads (\u2192 localSet); `origin` is **the authoritative local target** \u2014\n * the exact origin the runner registered, obeyed verbatim by {@link resolveLocalTarget}.\n * The runner is the only party that knows the scheme and port its alias is really served\n * on, so the helper never re-derives them when `origin` is present.\n *\n * `release`, `alias` and `proxyPort` are the LEGACY inputs, read only when `origin` is\n * absent \u2014 which can only happen when an OLD CLI wrote the fragment (a CLI-only rollback).\n * `port` is no longer a proxy target (the direct `127.0.0.1:<port>` branch is gone) \u2014 it\n * survives as diagnostic provenance. `pid` is the staleness guard: {@link readFragmentDir}\n * drops a fragment whose writer is no longer alive, so a crashed runner cannot keep a\n * package pinned `local` at a dead alias. `writtenAt` is provenance only.\n */\nconst devContextFragmentSchema = z.object({\n package: z.string(),\n port: z.number(),\n pid: z.number().optional(),\n writtenAt: z.number().optional(),\n release: z.string().optional(),\n alias: z.string().optional(),\n proxyPort: z.number().optional(),\n origin: z.string().optional(),\n v: z.number().optional(),\n})\n\n// Why the promise is load-bearing: it is what makes the guard in {@link resolveLocalTarget} possible.\n// Without it, a missing `origin` is ambiguous \u2014 it means BOTH \"an old CLI wrote this, legacy mode is\n// correct\" AND \"a current CLI wrote a broken fragment, legacy mode is catastrophic\" \u2014 and the helper\n// has to guess. It guesses legacy, rebuilds the target from a `templates.local` that says `http://`,\n// and proxies plain HTTP into a TLS listener. Silently, because portless answers :80 with a 302\n// rather than refusing.\n/**\n * Wire version of the dev-context fragment \u2014 the ONE contract between the `infra-kit` CLI (which writes\n * fragments) and this helper (which reads them). They ship as SEPARATE npm packages on separate cadences,\n * so this number is the only thing either side can trust about the other.\n *\n * `v >= 2` is a PROMISE that `origin` is present and authoritative.\n *\n * A fragment with NO `v` is a pre-v2 (legacy) writer and must still be honoured: rejecting it would drop\n * a running package to `cloud` instead of falling into legacy mode, and that rollback path is exactly why\n * the schema above is lenient and un-`refine`d. Absent or unknown `v` therefore NEVER rejects.\n */\nexport const DEV_CONTEXT_WIRE_VERSION = 2\n\n/**\n * A single Vite `server.proxy` entry. `changeOrigin` is always set. `cookieDomainRewrite` is cloud-only\n * (it keeps an HTTPS cloud BE's cookies usable from a local FE) and is always the EMPTY string, which\n * makes vite delete the whole `; Domain=\u2026` segment rather than rewrite it (`rewriteCookieProperty`\n * returns `''` for a falsy replacement). A host-only cookie is then scoped to whatever host the browser\n * actually used, so it works from `127.0.0.1`, `localhost` and a `*.localhost` portless alias alike \u2014\n * whereas a literal `Domain=localhost` is rejected outright by an IP host, and by a multi-label\n * `*.localhost` origin under the RFC 6265 \u00A75.3 public-suffix rule. `secure: false` appears on BOTH sources\n * but for different reasons: a cloud target may serve a cert the local store rejects, and a local target\n * is served by portless from its own private CA \u2014 set there only for a loopback host (see\n * {@link isLoopbackTarget}). `headers` is present only when HTTP Basic Auth is injected (see\n * {@link buildBasicAuthHeader}) \u2014 it carries the `Authorization` header applied\n * uniformly to every route so upstream environments behind auth (e2e/staging)\n * stay reachable.\n */\nexport interface InfraKitViteProxyEntry {\n target: string\n changeOrigin: true\n secure?: false\n cookieDomainRewrite?: ''\n headers?: Record<string, string>\n}\n\n/** A Vite `server.proxy`-shaped map: path-prefix \u2192 proxy entry. */\nexport type InfraKitViteProxy = Record<string, InfraKitViteProxyEntry>\n\n/** Explicit HTTP Basic Auth credentials, overriding the `E2E__BASIC_AUTH_*` env vars. */\nexport interface InfraKitBasicAuth {\n username: string\n password: string\n}\n\nexport interface InfraKitDevOptions {\n /** Package dir whose `infra-kit.config.ts` is loaded. Defaults to `process.cwd()`. */\n cwd?: string\n /**\n * Explicit HTTP Basic Auth credentials injected into every proxy route as an\n * `Authorization` header. Takes precedence over the `E2E__BASIC_AUTH_USERNAME`\n * / `E2E__BASIC_AUTH_PASSWORD` env vars. Omit to use the env-based default.\n */\n basicAuth?: InfraKitBasicAuth\n /**\n * Vite's `command`. Pass it (`defineConfig(async ({ command }) => ({ server: await infraKitDev({ command }) }))`)\n * so `build` is a no-op: `server` is irrelevant to a build and proxy resolution would otherwise\n * fail-fast on a cloud route with no sourced env. Omit (or `'serve'`) for the dev-server config.\n */\n command?: 'build' | 'serve'\n /**\n * Apply the package's `dev.env` to `process.env` (see {@link applyDevEnv}). Default `true`. The plugin passes\n * `false` under vitest, which also runs `serve` config hooks but must see the env the tests were given.\n */\n applyDevEnv?: boolean\n /**\n * Explicit dev-server port. Omit for a **per-worktree dynamic** free port (a fresh OS-assigned\n * port) so N simultaneous git worktrees never collide on Vite's default `5173`; Vite prints the\n * chosen URL. Pass a fixed number only when an external contract pins the port.\n */\n port?: number\n /**\n * Interface the dev server binds. Defaults to {@link LOOPBACK_V4} so a portless alias can reach it\n * (Vite's own `localhost` default binds `[::1]` only, which the proxy cannot dial). Override when\n * the dev server must be reachable off the loopback \u2014 `'0.0.0.0'` or `true` inside a container, on\n * a LAN, or for an e2e runner on another host.\n */\n host?: string | boolean\n}\n\n/**\n * Build the `Authorization: Basic <base64(user:pass)>` header value. Credentials\n * come from `override` when given, else from `E2E__BASIC_AUTH_USERNAME` /\n * `E2E__BASIC_AUTH_PASSWORD` (NOTE: double underscore). Returns `undefined` when\n * either half is missing, so callers add no `headers` key at all.\n */\nconst buildBasicAuthHeader = (env: NodeJS.ProcessEnv, override?: InfraKitBasicAuth): string | undefined => {\n const username = override?.username ?? env.E2E__BASIC_AUTH_USERNAME\n const password = override?.password ?? env.E2E__BASIC_AUTH_PASSWORD\n\n if (!username || !password) return undefined\n\n const encoded = Buffer.from(`${username}:${password}`).toString('base64')\n\n return `Basic ${encoded}`\n}\n\n/**\n * Re-exported from the shared, dependency-light `lib/release-slug` module so the\n * published `infra-kit/vite` surface (`entry/vite.ts:7`) is unchanged while the\n * dev-server's fragment writer derives `<release>` from the SAME implementation\n * (no slug drift). See {@link slugifyRelease}.\n */\nexport { slugifyRelease }\n\n/** Fill the `<release>`/`<packageName>`/`<env>` placeholders in a URL template. */\nconst interpolate = (template: string, values: { release: string; packageName: string; env: string }): string => {\n return template\n .replaceAll('<release>', values.release)\n .replaceAll('<packageName>', values.packageName)\n .replaceAll('<env>', values.env)\n}\n\n/** The port a bare `http://` target implies, and the proxy's default \u2014 never spelled out in a URL. */\nconst DEFAULT_HTTP_PORT = 80\n\n/**\n * The interface the vite dev server binds. Pinned to IPv4 loopback because portless dials its routes\n * on `127.0.0.1`, while vite's default `host: 'localhost'` combined with its\n * `dns.setDefaultResultOrder('verbatim')` binds `[::1]` ONLY \u2014 the alias then resolves, the proxy\n * connects, and the connection is refused, surfacing as a 502 on every UI request. Backends never hit\n * this because `ServerlessLocalRun` already binds `127.0.0.1`. Still loopback-only: this narrows the\n * address family, it does not expose the dev server to the network.\n */\nconst LOOPBACK_V4 = '127.0.0.1'\n\n/**\n * LEGACY only ({@link resolveLegacyTarget}): graft the `proxyPort` a pre-v2 CLI recorded onto the\n * interpolated `templates.local` target. Such a CLI ran portless on whatever port it could bind and wrote\n * that port into the fragment; a current one publishes the finished `origin` and writes no `proxyPort`.\n * A template that already pins its own port keeps it.\n */\nconst withProxyPort = (target: string, proxyPort: number | undefined): string => {\n if (proxyPort == null || proxyPort === DEFAULT_HTTP_PORT) return target\n\n let url: URL\n\n try {\n url = new URL(target)\n } catch {\n throw new Error(\n `@slip-stream-kit/config/vite: dev.proxy templates.local resolved to \"${target}\", which is not a valid URL, so the proxy port ${proxyPort} cannot be applied.`,\n )\n }\n\n if (url.port !== '') return target\n url.port = String(proxyPort)\n\n // `URL.toString()` synthesizes a `/` pathname for an authority-only URL, so the common port-free\n // template would come back as `\u2026localhost:1355/`. Strip that ONLY when the template had no trailing\n // slash of its own \u2014 a template ending in `/` (e.g. `\u2026localhost/base/`) means it, and silently\n // trimming it would change the path the frontend proxies to.\n const grafted = url.toString()\n\n return !target.endsWith('/') && grafted.endsWith('/') ? grafted.slice(0, -1) : grafted\n}\n\n/**\n * The lowest port a daemon can bind without root. An origin-less fragment recording a port AT OR ABOVE\n * this was written by an old CLI whose `spawnDaemon` always passed `--no-tls`, so infra-kit itself could\n * only ever have put PLAIN HTTP there. Below it (80/443) the daemon was installed out-of-band, where\n * portless defaults to TLS ON \u2014 which is precisely why the legacy scheme is left alone down there.\n */\nconst LEGACY_SELF_SPAWNED_PORT_FLOOR = 1024\n\n// Why each branch is what it is:\n//\n// `>= 1024` is a **mitigation, not a proof** \u2014 infra-kit's own `spawnDaemon` always passed `--no-tls`,\n// so an unprivileged daemon *infra-kit created* was always plain HTTP; a HAND-STARTED TLS daemon on\n// such a port (portless's own default is TLS) would be wrongly downgraded here. That corner is\n// accepted: forcing `http` reproduces what the old CLI + old helper already emitted, so it regresses\n// nothing that worked.\n//\n// `< 1024` (80/443) or absent means the daemon was installed out-of-band and may well be TLS, and we\n// cannot know. Forcing `http` on a `:443` fragment would emit `http://<alias>:443`, plain HTTP into a\n// TLS listener \u2014 the exact silent failure this design exists to remove.\n/**\n * Legacy target resolution \u2014 reached ONLY for an origin-less fragment, i.e. one written by an OLD CLI\n * (a CLI-only rollback). It must reproduce the old helper rather than reject anything, because that is\n * what lets the CLI be reverted without touching a single consumer repo.\n *\n * `proxyPort >= 1024`: force the scheme to `http`.\n *\n * `proxyPort < 1024` (80/443) or absent: reproduce the old helper verbatim and NEVER rewrite the scheme.\n */\nconst resolveLegacyTarget = (target: string, proxyPort: number | undefined): string => {\n if (proxyPort != null && proxyPort >= LEGACY_SELF_SPAWNED_PORT_FLOOR) {\n return withProxyPort(target.replace(/^https:\\/\\//iu, 'http://'), proxyPort)\n }\n\n return withProxyPort(target, proxyPort)\n}\n\n/**\n * The proxy target for a route resolved to `local`. The runner PUBLISHES the contract and the helper OBEYS\n * it: when the fragment carries an `origin`, that string is the target verbatim \u2014 the runner is the only\n * party that knows the scheme and port the alias it registered is actually served on, and this helper ships\n * on its own release cadence, so any re-derivation here is a guess that can silently disagree.\n *\n * `templates.local` is consulted ONLY in legacy mode (see {@link resolveLegacyTarget}).\n *\n * The target proxies to a stable HOSTNAME, and the runner re-points that alias at the freshly-bound port\n * on every `--watch` restart, so it self-heals without Vite reloading its config.\n */\nconst resolveLocalTarget = (args: {\n route: InfraKitDevProxyRoute\n templates: InfraKitDevProxy['templates']\n env: string | undefined\n getRelease: () => string\n info: LocalPackageInfo | undefined\n}): string => {\n const { route, templates, env, getRelease, info } = args\n\n if (info?.origin) return info.origin\n\n // The fragment declared a wire version, which PROMISES an `origin` \u2014 and there isn't one. Falling into\n // legacy mode here would rebuild the target from `templates.local` and downgrade it to `http://`, i.e.\n // proxy plain HTTP into portless's TLS listener. That failure is silent (:80 answers with a 302 rather\n // than refusing), so nothing downstream would catch it. A fragment with NO `v` is a genuinely old writer\n // and still falls through to legacy below \u2014 that rollback path is intact. This branch is only the\n // impossible one: a writer that announced v2 and then broke its own promise.\n if (info?.wire != null) {\n throw new Error(\n `@slip-stream-kit/config/vite: the dev-context fragment for \"${route.packageName}\" declares wire v${info.wire} but ` +\n `carries no \\`origin\\`. A v${DEV_CONTEXT_WIRE_VERSION} writer must publish the exact origin it ` +\n `registered. Refusing to guess a target rather than silently proxy plain HTTP at a TLS listener \u2014 ` +\n `restart \\`infra-kit dev\\`, and if it persists the CLI and this helper are incompatible.`,\n )\n }\n\n const target = interpolate(templates.local, {\n // Prefer THIS package's runner-recorded release slug (R4) over the single global git derivation,\n // so cross-branch FE/BE pairings don't drift the emitted `<release>` from the segment the runner\n // aliased. Fall back to the global git slug only when the fragment carried no release.\n release: info?.release ?? getRelease(),\n // The local template resolves to a `<release>.<packageName>.localhost` alias the dev-server\n // registered with portless \u2014 which slugifies the package name to a legal DNS label. Slugify\n // identically here or a scoped name (`@hulyo/client-ui`) emits a target that no alias backs.\n // There is no `<repo>` token on purpose: only a pre-v2 CLI writes an origin-less fragment, and that\n // CLI registered its aliases without a repo label, so the legacy template is exact for it.\n packageName: slugifyHostLabel(route.packageName),\n env: env ?? '',\n })\n\n return resolveLegacyTarget(target, info?.proxyPort)\n}\n\n/**\n * Pick the effective source for a route: `local` when the route lists `local` as\n * a capability AND its packageName is in the local dev set; otherwise the fallback\n * \u2014 the declared `default` for a multi-source route, or the sole `from` entry for\n * a single-source one. Always resolves (the schema guarantees a usable fallback).\n */\nconst pickSource = (route: InfraKitDevProxyRoute, localSet: ReadonlySet<string>): InfraKitDevProxySource => {\n // `from` is guaranteed non-empty by the schema, so `from[0]` is always present.\n const fallback = route.default ?? route.from[0]!\n\n return route.from.includes('local') && localSet.has(route.packageName) ? 'local' : fallback\n}\n\n/**\n * Where this UI's cloud routes go: the value of its `deployedUrlEnv`, as the loaded environment set it.\n * The variable's NAME rides along so an unset one can be named in the error.\n */\nexport interface CloudOrigin {\n envVar: string | undefined\n url: string | undefined\n}\n\ninterface ResolveRouteArgs {\n routePath: string\n route: InfraKitDevProxyRoute\n templates: InfraKitDevProxy['templates']\n localSet: ReadonlySet<string>\n env: string | undefined\n cloud: CloudOrigin\n getRelease: () => string\n /** Per-package runtime data (recorded release/port) from the dev-context merge. */\n localInfo?: ReadonlyMap<string, LocalPackageInfo>\n}\n\n/** Hostnames that can only be this machine \u2014 the only ones a private-CA cert is allowed to go unchecked on. */\nconst LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1'])\n\n/**\n * Is `target` unambiguously on this machine? Only then may {@link resolveRoute} disable cert validation:\n * `templates.local` is an unconstrained string in a consumer-authored config, so an unconditional\n * `secure: false` in a PUBLISHED helper would silently switch off validation for whatever URL a consumer\n * wrote there. Scoping it to `.localhost`/loopback is what makes \"no MITM surface\" true by construction.\n * An unparseable target is NOT local (the caller then leaves `secure` unset \u2014 fail closed).\n */\nconst isLoopbackTarget = (target: string): boolean => {\n let hostname: string\n\n try {\n hostname = new URL(target).hostname.toLowerCase()\n } catch {\n return false\n }\n\n // `URL.hostname` brackets an IPv6 literal (`[::1]`); compare the bare address.\n const bare = hostname.startsWith('[') && hostname.endsWith(']') ? hostname.slice(1, -1) : hostname\n\n return bare.endsWith('.localhost') || LOOPBACK_HOSTNAMES.has(bare)\n}\n\n/** Resolve one route into a Vite proxy entry (or throw an actionable error). */\nconst resolveRoute = ({\n routePath,\n route,\n templates,\n localSet,\n env,\n cloud,\n getRelease,\n localInfo,\n}: ResolveRouteArgs): InfraKitViteProxyEntry => {\n const source = pickSource(route, localSet)\n\n if (source === 'local') {\n const target = resolveLocalTarget({ route, templates, env, getRelease, info: localInfo?.get(route.packageName) })\n\n // The local target is now HTTPS, served with a cert minted by portless's own private CA. Node's\n // http-proxy validates the upstream chain against the system store, which does not contain that CA,\n // so every proxied request fails with SELF_SIGNED_CERT_IN_CHAIN (confirmed empirically \u2014 see\n // `.omc/research/portless-https-spike.md`). Only a loopback target earns the exemption.\n return isLoopbackTarget(target) ? { target, changeOrigin: true, secure: false } : { target, changeOrigin: true }\n }\n\n if (!cloud.url) {\n const missing = cloud.envVar\n ? `${cloud.envVar} is not set`\n : 'this package declares no `deployedUrlEnv` to read it from'\n\n throw new Error(\n `@slip-stream-kit/config/vite: proxy route \"${routePath}\" resolves to the deployed app, but ${missing}. Load an environment from Doppler first (e.g. \\`infra-kit env-load -c dev\\`).`,\n )\n }\n\n return { target: cloud.url, changeOrigin: true, secure: false, cookieDomainRewrite: '' }\n}\n\ninterface ResolveProxyArgs {\n proxy: InfraKitDevProxy\n localSet: ReadonlySet<string>\n env: string | undefined\n cloud: CloudOrigin\n getRelease: () => string\n /** Pre-computed `Authorization` header value applied uniformly to every route. */\n authHeader?: string\n /** Per-package runtime data (recorded release/port) from the dev-context merge. */\n localInfo?: ReadonlyMap<string, LocalPackageInfo>\n}\n\n// Why order matters at all: vite matches `server.proxy` **first-hit-wins over insertion order** \u2014\n// `doesProxyContextMatchUrl` is `context[0] === '^' && new RegExp(context).test(url) ||\n// url.startsWith(context)` \u2014 so a config that happens to list `/api` before `/api/v1/cronjob`\n// silently routes every cronjob call to whatever `/api` resolves to. Emitting longest-first makes the\n// consumer's key order irrelevant for literal prefixes.\n//\n// **Length is the correct key, and provably so:** if key `B` captures a URL that key `A` also\n// captures, then both are prefixes of that URL, so one is a prefix of the other \u2014 `A.startsWith(B)` \u2014\n// and hence `A.length >= B.length`. Sorting by descending length therefore always places the more\n// specific of any overlapping pair first. Segment counting would be *worse*, because `startsWith` is\n// not segment-aware: `/api` captures `/api-docs` too, yet both are \"one segment\".\n//\n// **Why regex keys bail out.** A `^`-anchored key is schema-legal (`routes` is\n// `z.record(z.string().min(1), \u2026)`), and its capture set is unknowable to a sorter \u2014 length says\n// nothing about breadth, and there is no position, ahead or behind, that is right in general: a broad\n// `'^/(api|dynamic)'` must lose to `/api/v1/cronjob`, while a narrow one must not. Returning the\n// consumer's own order is at least the order they can reason about. No consumer uses a regex key\n// today; this exists so the sort can never invert one's intent.\n/**\n * Order the emitted routes so a more specific prefix always beats a more general one, by descending\n * key length. A `^`-anchored (regex) key is schema-legal and unsortable, so if ANY key is a regex the\n * map is returned in the consumer's own order, untouched.\n *\n * Governs only the routes INFRA-KIT emits. A consumer who writes their own `server.proxy` keeps\n * priority regardless: vite merges a plugin's config over the user's, placing user keys first.\n */\nconst orderBySpecificity = (routes: InfraKitViteProxy): InfraKitViteProxy => {\n const keys = Object.keys(routes)\n\n if (\n keys.some((key) => {\n return key.startsWith('^')\n })\n )\n return routes\n\n const ordered: InfraKitViteProxy = {}\n\n // `toSorted` is stable, so equal-length keys keep the consumer's relative order.\n for (const key of keys.toSorted((a, b) => {\n return b.length - a.length\n }))\n ordered[key] = routes[key]!\n\n return ordered\n}\n\n/**\n * Pure resolver: turn a `dev.proxy` config + resolved inputs (local set, env,\n * lazy release) into a Vite `server.proxy` map. Side-effect-free so the\n * resolution shape is fully unit-testable. When `authHeader` is set, every route\n * entry gets a matching `headers.Authorization`; otherwise no `headers` key.\n */\nexport const resolveProxyConfig = ({\n proxy,\n localSet,\n env,\n cloud,\n getRelease,\n authHeader,\n localInfo,\n}: ResolveProxyArgs): InfraKitViteProxy => {\n const result: InfraKitViteProxy = {}\n const headers = authHeader ? { Authorization: authHeader } : undefined\n\n for (const [routePath, route] of Object.entries(proxy.routes)) {\n const entry = resolveRoute({\n routePath,\n route,\n templates: proxy.templates,\n localSet,\n env,\n cloud,\n getRelease,\n localInfo,\n })\n\n result[routePath] = headers ? { ...entry, headers } : entry\n }\n\n return orderBySpecificity(result)\n}\n\n/** One `dev.proxy` route as {@link resolveProxyConfig} would resolve it, for a caller that reports rather than proxies. */\nexport interface ProxyRouteDescription {\n path: string\n packageName: string\n source: InfraKitDevProxySource\n /** `null` when it cannot be resolved: a cloud route whose URL is not loaded, or a local fragment that broke its wire promise. */\n target: string | null\n}\n\n/**\n * The route-by-route local/cloud split a UI's vite proxy is running with, most specific first. The same\n * `pickSource` and target resolution the proxy uses, but it never throws \u2014 `infra-kit e2e` reports the\n * topology it is testing against, and an unresolvable route is a finding there, not a crash.\n */\nexport const describeProxyRoutes = ({\n proxy,\n localContext,\n env,\n cloud,\n getRelease,\n}: {\n proxy: InfraKitDevProxy\n localContext: LocalContext\n env: string | undefined\n cloud: CloudOrigin\n getRelease: () => string\n}): ProxyRouteDescription[] => {\n const paths = Object.keys(proxy.routes).toSorted((a, b) => {\n return b.length - a.length\n })\n\n return paths.map((routePath) => {\n const route = proxy.routes[routePath]!\n const source = pickSource(route, localContext.packages)\n let target: string | null = null\n\n if (source === 'cloud') {\n target = cloud.url ?? null\n } else {\n try {\n target = resolveLocalTarget({\n route,\n templates: proxy.templates,\n env,\n getRelease,\n info: localContext.info.get(route.packageName),\n })\n } catch {\n target = null\n }\n }\n\n return { path: routePath, packageName: route.packageName, source, target }\n })\n}\n\n/**\n * Warn (never throw) when `dev.proxy.templates.local` is not `https://`. The portless daemon that\n * serves the local template is TLS-only, and {@link resolveLegacyTarget} only ever downgrades\n * https -> http for a legacy self-spawned port \u2014 it never upgrades \u2014 so an `http://` template\n * silently proxies into nothing reachable. Warn-first this release (schema still accepts any\n * non-empty string, see `packageConfigSchema`) so an unknown legit `http://` consumer isn't bricked;\n * make this a hard schema requirement next release.\n */\nconst warnIfNonHttpsLocalTemplate = (dev: InfraKitDev | undefined, configPath: string): void => {\n const local = dev?.proxy?.templates.local\n\n if (local !== undefined && !local.startsWith('https://')) {\n console.warn(\n `@slip-stream-kit/config: dev.proxy.templates.local in ${configPath} is \"${local}\", which is not an ` +\n `https:// URL. The local dev proxy target must be https:// (the portless daemon it points at serves ` +\n `TLS only) \u2014 https:// will become a REQUIRED value in a future release.`,\n )\n }\n}\n\n/** Memoize a zero-arg thunk so `<release>` git resolution runs at most once. */\nconst once = <T>(fn: () => T): (() => T) => {\n let cached: { value: T } | undefined\n\n return () => {\n cached ??= { value: fn() }\n\n return cached.value\n }\n}\n\n// Reached from two different processes, and only one of them is ours. Under the CLI (`infra-kit dev`,\n// `audit`) the entry has installed {@link ../node-warnings.suppressTypelessPackageJsonWarning}, so a\n// consumer package with no `\"type\"` loads quietly. Under `infraKitDev()` in a consumer's own\n// `vite.config.ts` we are a library in their process and do NOT patch their globals, so such a package\n// still prints Node's MODULE_TYPELESS_PACKAGE_JSON banner there. Harmless (it is a double-parse\n// notice), and latent while consumer UI packages declare `\"type\": \"module\"`.\n/** The deployed URL `deployedUrlEnv` names, as the loaded environment set it. */\nexport const readCloudOrigin = (envVar: string | undefined, env: NodeJS.ProcessEnv = process.env): CloudOrigin => {\n return { envVar, url: envVar ? env[envVar] || undefined : undefined }\n}\n\n/**\n * Apply a package's `dev.env` to `env` in place. It has to be this process's env rather than the spawn's:\n * `infra-kit dev` runs every UI under ONE turbo child, so only the UI's own vite process can tell them apart.\n * Vite runs config hooks before `loadEnv`, so a change made here reaches `import.meta.env`. `unset` only\n * removes the inherited variable: a value in the package's `.env` files still reaches `import.meta.env`.\n */\nexport const applyDevEnv = (devEnv: InfraKitDevEnv | undefined, env: NodeJS.ProcessEnv = process.env): void => {\n for (const name of devEnv?.unset ?? []) {\n delete env[name]\n }\n\n Object.assign(env, devEnv?.set)\n}\n\n/**\n * Load and validate a package's `infra-kit.config.ts`, or `undefined` when it is absent. The `.ts` config is\n * evaluated via Node's native type stripping (Node >= 24) \u2014 the same mechanism the CLI's config loader uses.\n * Cache-busted by mtime so repeated dev-server reloads pick up edits.\n */\nexport const loadPackageConfig = async (cwd: string): Promise<InfraKitPackageConfig | undefined> => {\n const configPath = path.join(cwd, PACKAGE_CONFIG_FILE)\n\n if (!fs.existsSync(configPath)) return undefined\n\n const stat = fs.statSync(configPath)\n const moduleUrl = `${pathToFileURL(configPath).href}?mtime=${Number(stat.mtimeMs)}`\n\n const imported = (await import(moduleUrl)) as { default?: unknown }\n const rawExport = imported.default\n\n if (rawExport === undefined) return undefined\n\n const resolved = typeof rawExport === 'function' ? await (rawExport as () => unknown)() : rawExport\n\n const parsed = packageConfigSchema.safeParse(resolved)\n\n if (!parsed.success) {\n throw new Error(\n `@slip-stream-kit/config: invalid ${PACKAGE_CONFIG_FILE} at ${configPath}: ${z.prettifyError(parsed.error)}`,\n )\n }\n\n return parsed.data as InfraKitPackageConfig\n}\n\n/** A package's `dev` block, or `undefined` when the config or the `dev` key is absent. */\nexport const loadDev = async (cwd: string): Promise<InfraKitDev | undefined> => {\n const dev = (await loadPackageConfig(cwd))?.dev\n\n warnIfNonHttpsLocalTemplate(dev, path.join(cwd, PACKAGE_CONFIG_FILE))\n\n return dev\n}\n\n/** Coerce a parsed dev-context.json into the set of locally-running package names. */\nconst extractPackages = (parsed: unknown): string[] => {\n if (Array.isArray(parsed)) {\n return parsed.filter((v): v is string => {\n return typeof v === 'string'\n })\n }\n\n if (parsed !== null && typeof parsed === 'object') {\n const candidate = (parsed as Record<string, unknown>).packages ?? (parsed as Record<string, unknown>).localPackages\n\n if (Array.isArray(candidate)) {\n return candidate.filter((v): v is string => {\n return typeof v === 'string'\n })\n }\n }\n\n return []\n}\n\n/** Search upward from `start` for `relative`, returning the first hit or undefined. */\nconst findUp = (start: string, relative: string): string | undefined => {\n let dir = path.resolve(start)\n\n for (;;) {\n const candidate = path.join(dir, relative)\n\n if (fs.existsSync(candidate)) return candidate\n\n const parent = path.dirname(dir)\n\n if (parent === dir) return undefined\n\n dir = parent\n }\n}\n\n/** Per-package runtime data merged from the dev-context fragment directory. */\nexport interface LocalPackageInfo {\n /** The real bound port the runner recorded. Provenance only \u2014 the proxy target is the alias host. */\n port: number\n /**\n * The authoritative local target (`https://<release>.<packageName>.<repo>.localhost`), published by the\n * runner. Present \u2192 {@link resolveLocalTarget} uses it verbatim and consults no template. Absent \u2192\n * the fragment came from an old CLI and the legacy template path runs.\n */\n origin?: string\n /**\n * The fragment's declared wire version (see {@link DEV_CONTEXT_WIRE_VERSION}). Absent \u2192 a pre-v2 writer,\n * so legacy mode is the correct reading. Present \u2192 the writer PROMISED an `origin`, and a missing one is\n * a broken fragment rather than an old one.\n */\n wire?: number\n /** LEGACY (no `origin`): the runner-recorded release slug, preferred over the helper's git derivation. */\n release?: string\n /** LEGACY (no `origin`): `<release>.<packageName>.localhost`, present only when an alias was registered. */\n alias?: string\n /** LEGACY (no `origin`): the port {@link alias} resolves on. `80` (the default) is implicit in the template. */\n proxyPort?: number\n}\n\n/**\n * The locally-running package set plus a `package \u2192 { port, release }` map merged\n * from the dev-context fragments. `packages` feeds `pickSource`; `info` lets a\n * `local` route emit the per-package recorded release (see {@link resolveRoute}).\n */\nexport interface LocalContext {\n packages: ReadonlySet<string>\n info: ReadonlyMap<string, LocalPackageInfo>\n}\n\n/** An empty {@link LocalContext} (frontend-only / dev-context absent). */\nconst emptyLocalContext = (): LocalContext => {\n return { packages: new Set(), info: new Map() }\n}\n\n/**\n * Is the runner that wrote a fragment still alive? Signal `0` performs the permission and existence\n * checks without delivering anything. `ESRCH` is the one answer that means \"gone\"; `EPERM` means the\n * pid exists under another uid, which is still a live process. A fragment with no `pid` predates the\n * field and is trusted (back-compat) rather than dropped.\n */\nconst isFragmentWriterAlive = (pid: number | undefined): boolean => {\n if (pid == null) return true\n\n try {\n process.kill(pid, 0)\n\n return true\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === 'EPERM'\n }\n}\n\n/**\n * Merge every `<app>.json` fragment in the dev-context directory into a\n * {@link LocalContext}. Two DISTINCT failure branches (do NOT collapse them into\n * one directory-wide catch):\n * - `readdir`-ENOENT (directory vanished mid-race) \u2192 empty context, matching the\n * dir-absent back-compat behaviour.\n * - a single corrupt/truncated/invalid `<app>.json` \u2192 that ONE fragment is\n * SKIPPED (per-fragment `safeParse` isolation); the rest still merge, so one bad\n * fragment never collapses the whole localSet to empty (which would silently\n * drop a started package to cloud).\n */\nconst readFragmentDir = (dir: string): LocalContext => {\n let entries: string[]\n\n try {\n entries = fs.readdirSync(dir)\n } catch {\n return emptyLocalContext()\n }\n\n const packages = new Set<string>()\n const info = new Map<string, LocalPackageInfo>()\n\n for (const name of entries) {\n if (!name.endsWith('.json')) continue\n\n try {\n const parsed = devContextFragmentSchema.safeParse(JSON.parse(fs.readFileSync(path.join(dir, name), 'utf-8')))\n\n if (!parsed.success) continue\n\n // A runner killed with SIGKILL (or crashed) leaves its fragment AND its portless alias behind.\n // Trusting it would route this package's `/api` at an alias nothing serves \u2014 a silent 502 with\n // no diagnostic. The writer already stamps its pid; honour it.\n if (!isFragmentWriterAlive(parsed.data.pid)) continue\n\n packages.add(parsed.data.package)\n info.set(parsed.data.package, {\n port: parsed.data.port,\n origin: parsed.data.origin,\n release: parsed.data.release,\n alias: parsed.data.alias,\n proxyPort: parsed.data.proxyPort,\n wire: parsed.data.v,\n })\n } catch {\n // Corrupt/truncated fragment (JSON.parse / read failure): skip ONLY this one.\n continue\n }\n }\n\n return { packages, info }\n}\n\n/**\n * Read the locally-running package context, searched upward from `cwd`. Prefers\n * the `.infra-kit/dev-context/` fragment DIRECTORY (merged via\n * {@link readFragmentDir}); when it is absent, falls back to the legacy single\n * `.infra-kit/dev-context.json` file (read as before \u2014 no per-package release);\n * when neither exists \u2192 empty context (frontend-only, every route resolves cloud).\n */\nexport const readLocalContext = (cwd: string): LocalContext => {\n const dir = findUp(cwd, DEV_CONTEXT_DIR)\n\n if (dir) return readFragmentDir(dir)\n\n const legacy = findUp(cwd, DEV_CONTEXT_FILE)\n\n if (!legacy) return emptyLocalContext()\n\n try {\n return { packages: new Set(extractPackages(JSON.parse(fs.readFileSync(legacy, 'utf-8')))), info: new Map() }\n } catch {\n return emptyLocalContext()\n }\n}\n\n/**\n * Read the set of locally-running packages (searched upward from `cwd`). Thin\n * wrapper over {@link readLocalContext} preserving the historical `Set`-returning\n * contract. Absent dev-context \u2192 empty set (frontend-only cloud resolution).\n */\nexport const readLocalSet = (cwd: string): ReadonlySet<string> => {\n return readLocalContext(cwd).packages\n}\n\n/**\n * An OS-assigned free TCP port on 127.0.0.1 \u2014 the per-worktree dev-server port (mirrors the\n * backend's `listen(0)`). This probes then releases the port, so there is a small TOCTOU window\n * before Vite binds it; Vite's default `strictPort: false` absorbs a lost race by stepping to the\n * next free port. Only a consumer that sets `strictPort: true` would hard-fail on the rare collision.\n */\nconst getFreePort = (): Promise<number> => {\n return new Promise((resolve, reject) => {\n const srv = net.createServer()\n\n srv.unref()\n srv.on('error', reject)\n srv.listen(0, '127.0.0.1', () => {\n const address = srv.address()\n const port = typeof address === 'object' && address !== null ? address.port : 0\n\n srv.close(() => {\n return resolve(port)\n })\n })\n })\n}\n\n/** Env var carrying the runner-assigned `{ \"<ui-package>\": <port> }` map (Layer B). */\nconst UI_PORTS_ENV = 'INFRA_KIT_UI_PORTS'\n\n/** This package's `package.json` `name`, or `null` (unreadable / nameless). */\nconst readPackageName = (cwd: string): string | null => {\n try {\n const parsed = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8')) as { name?: unknown }\n\n return typeof parsed.name === 'string' ? parsed.name : null\n } catch {\n return null\n }\n}\n\n/**\n * What the runner assigned THIS UI. `alias` is present only in the widened record form, i.e. only when the\n * runner both assigned the port AND registered the hostname \u2014 which is exactly the condition for pointing\n * HMR at that hostname. The legacy bare-number form yields no alias.\n */\ninterface ManagedUi {\n port: number\n alias?: string\n}\n\n/**\n * What `infra-kit dev` assigned THIS UI via `INFRA_KIT_UI_PORTS` (Layer B), or `null` when the map is\n * absent / this package isn't in it / it's malformed. Present \u2192 the helper binds the port with\n * `strictPort` so vite lands on exactly the port the runner already aliased. Absent (a standalone\n * `vite dev`) \u2192 the caller falls back to a free port, byte-identical to today.\n *\n * Accepts BOTH the widened `{ port, alias }` record and the legacy bare `<port>` number an older runner\n * writes \u2014 a new helper paired with an old CLI must still bind the assigned port.\n */\nconst resolveManagedUi = (cwd: string): ManagedUi | null => {\n const raw = process.env[UI_PORTS_ENV]\n\n if (raw == null || raw === '') return null\n\n const name = readPackageName(cwd)\n\n if (name == null) return null\n\n try {\n const entry = (JSON.parse(raw) as Record<string, unknown>)[name]\n\n if (typeof entry === 'number') return { port: entry }\n\n if (entry === null || typeof entry !== 'object') return null\n\n const { port, alias } = entry as { port?: unknown; alias?: unknown }\n\n if (typeof port !== 'number') return null\n\n return { port, alias: typeof alias === 'string' ? alias : undefined }\n } catch {\n return null\n }\n}\n\n/** The port an `https://` alias is served on \u2014 never spelled out in a URL, and what the HMR client dials. */\nconst HTTPS_PORT = 443\n\n// This is `server.ws`, NOT the older `server.hmr`: vite 8 deprecated `server.hmr.{protocol,host,port,\n// path,clientPort,timeout,server}` in favour of `server.ws.*` and warns on every dev boot that sets\n// them \u2014 a warning consumers were seeing in the wild, emitted by us. The field names are unchanged\n// (vite's `WsOptions` is `HmrOptions` minus `overlay`), so this is a key rename, not a behaviour\n// change. Vite 8 ships `setupHmrWsOptionCompat` to copy a legacy `hmr` onto `ws`, which is what makes\n// `ws` the forward direction and `hmr` the legacy one. Emitting both keys is NOT an option \u2014 it would\n// re-trigger the very deprecation warning being removed.\n/**\n * Vite's `server.ws` override, pointing the HMR client at the alias instead of at the raw dev-server port.\n * The page is loaded over `https://<alias>`, so its websocket must be `wss://` on the same origin or the\n * browser blocks it as mixed content; `clientPort` is the proxy's implicit `:443`, not vite's bound port.\n *\n * **Requires vite >= 8.** `server.ws` does not exist before it. Neither this package (zod-only) nor\n * `@slip-stream-kit/vite` (which imports vite as `import type`, erased at runtime) can detect the\n * version, so the floor is a documented requirement rather than a runtime check.\n */\nexport interface InfraKitViteWs {\n protocol: 'wss'\n host: string\n clientPort: number\n}\n\n/**\n * Resolve a package's `dev` block into a ready-made Vite `server` config: a per-worktree dev-server\n * `port` plus the `proxy` map. Loads the package's `infra-kit.config.ts`, merges the local dev set\n * from the `.infra-kit/dev-context/` fragment directory, and interpolates the local/cloud templates.\n * `<env>` comes from `INFRA_KIT_ENV`; `<release>` from each package's runner-recorded fragment when\n * present, else the slugified git branch (computed lazily, only when a local route needs it).\n * The package's `dev.env` is applied to `process.env` first (see {@link applyDevEnv}).\n *\n * `port` defaults to a fresh OS-assigned free port so simultaneous git worktrees never collide on\n * Vite's `5173` (override via `options.port`). `host` defaults to {@link LOOPBACK_V4} so the portless\n * alias can actually reach it (override via `options.host` for containers/LAN). `ws` is emitted only\n * when `infra-kit dev` registered an alias for this UI, so a bare `vite dev` keeps vite's own HMR\n * defaults. Pass `command` so `build` is a no-op (empty proxy, no port) \u2014 a build ignores `server` and\n * proxy resolution would otherwise fail-fast on a cloud route with no sourced env.\n *\n * @example\n * // vite.config.ts \u2014 the whole `server` field is the helper's output\n * import { infraKitDev } from 'infra-kit/vite'\n * export default defineConfig(async ({ command }) => ({ server: await infraKitDev({ command }) }))\n */\nexport const infraKitDev = async (\n options: InfraKitDevOptions = {},\n): Promise<{\n port?: number\n host?: string | boolean\n strictPort?: boolean\n ws?: InfraKitViteWs\n proxy: InfraKitViteProxy\n}> => {\n const cwd = options.cwd ?? process.cwd()\n\n // A build ignores `server`; skip the port + proxy work (proxy resolution would fail-fast on a\n // cloud route with no sourced env).\n if (options.command === 'build') return { proxy: {} }\n\n // Port precedence: explicit `options.port` > the runner-assigned Layer-B port (bound `strictPort` so\n // vite lands on exactly the aliased port) > a fresh free port (today's default; the portless-off path).\n const managed = options.port == null ? resolveManagedUi(cwd) : null\n const port = options.port ?? managed?.port ?? (await getFreePort())\n const strictPort = managed != null\n const host = options.host ?? LOOPBACK_V4\n\n // Override the HMR websocket ONLY when the runner handed us the alias it registered. A bare `vite dev`\n // (no runner) has no alias, and a computable hostname is not a registered one \u2014 pointing the HMR client\n // at a hostname nothing resolves would break hot reload on a path that works today.\n const ws: InfraKitViteWs | undefined =\n managed?.alias == null ? undefined : { protocol: 'wss', host: managed.alias, clientPort: HTTPS_PORT }\n\n const server = { port, host, ...(strictPort ? { strictPort } : {}), ...(ws ? { ws } : {}) }\n const config = await loadPackageConfig(cwd)\n const dev = config?.dev\n\n warnIfNonHttpsLocalTemplate(dev, path.join(cwd, PACKAGE_CONFIG_FILE))\n if (options.applyDevEnv !== false) applyDevEnv(dev?.env)\n\n if (!dev?.proxy) return { ...server, proxy: {} }\n\n const { packages: localSet, info: localInfo } = readLocalContext(cwd)\n const env = process.env[INFRA_KIT_ENV]\n const cloud = readCloudOrigin(config?.deployedUrlEnv)\n const authHeader = buildBasicAuthHeader(process.env, options.basicAuth)\n\n const getRelease = once(() => {\n return readRelease(cwd)\n })\n\n return {\n ...server,\n proxy: resolveProxyConfig({ proxy: dev.proxy, localSet, env, cloud, getRelease, authHeader, localInfo }),\n }\n}\n\n/**\n * Convenience wrapper returning just the proxy map (for spreading into\n * `server.proxy` directly). Equivalent to `(await infraKitDev(options)).proxy`.\n */\nexport const infraKitProxy = async (options: InfraKitDevOptions = {}): Promise<InfraKitViteProxy> => {\n return (await infraKitDev(options)).proxy\n}\n", "import fs from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\n\nimport { readAppAliasHost } from '../release-slug/app-alias'\nimport { loadPackageConfig } from '../vite/vite'\n\n/**\n * The line `infra-kit dev --reuse` prints once every app it serves answers its health probe. Playwright's\n * `webServer.wait.stdout` waits on it \u2014 a URL cannot stand in for it, because portless answers 404 for a\n * hostname nothing serves, and Playwright counts a 404 as \"server is up\".\n */\nexport const DEV_SERVING_MARKER = 'infra-kit dev: serving'\n\n/** `local` | `cloud` \u2014 which side a run targets. Unset: `cloud` on CI (no dev servers there), else `local`. */\nexport const E2E_MODE_ENV = 'INFRA_KIT_E2E'\n\nconst PREFIX = '@slip-stream-kit/config/playwright:'\n\nexport type InfraKitE2eMode = 'local' | 'cloud'\n\n/** The subset of Playwright's `TestConfig['webServer']` this helper fills in. */\nexport interface InfraKitWebServer {\n name: string\n command: string\n cwd: string\n wait: { stdout: RegExp }\n gracefulShutdown: { signal: 'SIGTERM'; timeout: number }\n timeout: number\n stdout: 'ignore'\n stderr: 'pipe'\n}\n\nexport interface InfraKitE2eSetup {\n mode: InfraKitE2eMode\n /** For `use.baseURL`. Never written back to the environment: see {@link infraKitE2e}. */\n baseURL: string\n /** The target's `deployedUrlEnv` \u2014 where a cloud run's URL comes from. Undefined when it declares none. */\n deployedUrlEnv: string | undefined\n /** For `use.ignoreHTTPSErrors`: local aliases are signed by portless's own CA. */\n ignoreHTTPSErrors: boolean\n /** For `webServer`. `undefined` for a cloud run \u2014 nothing is started. */\n webServer: InfraKitWebServer | undefined\n}\n\nexport interface InfraKitE2eOptions {\n /** The e2e package's directory (the one holding `infra-kit.config.ts`). Defaults to `process.cwd()`. */\n dir?: string\n env?: NodeJS.ProcessEnv\n /** How long a cold `infra-kit dev` may take to serve everything. Defaults to 5 minutes. */\n timeoutMs?: number\n}\n\nconst findWorkspaceRoot = (start: string): string => {\n let dir = start\n\n while (!fs.existsSync(path.join(dir, 'pnpm-workspace.yaml'))) {\n const parent = path.dirname(dir)\n\n if (parent === dir) throw new Error(`${PREFIX} no pnpm-workspace.yaml above ${start}`)\n dir = parent\n }\n\n return dir\n}\n\nconst resolveMode = (env: NodeJS.ProcessEnv): InfraKitE2eMode => {\n const raw = env[E2E_MODE_ENV]\n\n if (raw === 'local' || raw === 'cloud') return raw\n if (raw) throw new Error(`${PREFIX} ${E2E_MODE_ENV}=\"${raw}\" (expected \"local\" or \"cloud\")`)\n\n return env.CI ? 'cloud' : 'local'\n}\n\n/**\n * Point a Playwright config at this worktree: the local alias of the package's `e2e.target`, served by\n * `infra-kit dev <app>` \u2014 reused when this worktree already runs it, started and stopped around the run\n * otherwise. A cloud run (`INFRA_KIT_E2E=cloud`, or CI) points at the deployed URL and starts nothing.\n *\n * Every entry point \u2014 `playwright test`, `--ui`, `--debug`, the editor extension, `infra-kit e2e` \u2014 goes\n * through the config, which is why this lives here rather than in a CLI command.\n *\n * @example\n * // apps/client/tests/playwright.config.ts\n * const e2e = await infraKitE2e({ dir: import.meta.dirname })\n *\n * export default defineConfig({\n * webServer: e2e.webServer,\n * use: { baseURL: e2e.baseURL, ignoreHTTPSErrors: e2e.ignoreHTTPSErrors },\n * })\n */\nexport const infraKitE2e = async (options: InfraKitE2eOptions = {}): Promise<InfraKitE2eSetup> => {\n const dir = options.dir ?? process.cwd()\n const env = options.env ?? process.env\n const e2e = (await loadPackageConfig(dir))?.e2e\n\n if (!e2e) {\n throw new Error(`${PREFIX} ${path.join(dir, 'infra-kit.config.ts')} declares no \\`e2e\\` block`)\n }\n\n if (e2e.server === 'static') {\n throw new Error(\n `${PREFIX} ${path.join(dir, 'infra-kit.config.ts')} declares \\`e2e: { server: 'static' }\\` \u2014 the suite serves its own build, so its playwright.config.ts sets baseURL and webServer itself instead of calling infraKitE2e()`,\n )\n }\n\n const root = findWorkspaceRoot(dir)\n const [app] = e2e.target.split('/') as [string]\n const targetDir = path.join(root, 'apps', e2e.target)\n const deployedUrlEnv = (await loadPackageConfig(targetDir))?.deployedUrlEnv\n const mode = resolveMode(env)\n\n if (mode === 'cloud') {\n if (!deployedUrlEnv) {\n throw new Error(\n `${PREFIX} a cloud run needs ${path.join(targetDir, 'infra-kit.config.ts')} to declare \\`deployedUrlEnv\\` \u2014 the variable holding ${e2e.target}'s deployed URL`,\n )\n }\n\n const baseURL = env[deployedUrlEnv]\n\n if (!baseURL) {\n throw new Error(\n `${PREFIX} a cloud run of ${e2e.target} needs ${deployedUrlEnv}, and it is not set. Load an environment with \\`infra-kit env-load -c <env>\\`, or unset ${E2E_MODE_ENV} to run locally.`,\n )\n }\n\n return { mode, baseURL, deployedUrlEnv, ignoreHTTPSErrors: false, webServer: undefined }\n }\n\n const packageName = (JSON.parse(fs.readFileSync(path.join(targetDir, 'package.json'), 'utf8')) as { name?: string })\n .name\n\n if (!packageName) throw new Error(`${PREFIX} ${targetDir}/package.json has no name`)\n\n // The deployed URL stays in the environment untouched: the dev server `webServer` starts inherits it,\n // and proxies the UI's cloud-only routes there. Overwriting it with the local alias would loop them back.\n const baseURL = `https://${readAppAliasHost(packageName, targetDir)}`\n\n return {\n mode,\n baseURL,\n deployedUrlEnv,\n ignoreHTTPSErrors: true,\n webServer: {\n name: 'infra-kit dev',\n command: `infra-kit dev ${app} --no-watch --reuse`,\n cwd: root,\n wait: { stdout: new RegExp(DEV_SERVING_MARKER) },\n // `dev` reaps its turbo/vite process groups on SIGTERM; Playwright's default SIGKILL would orphan them.\n gracefulShutdown: { signal: 'SIGTERM', timeout: 30_000 },\n timeout: options.timeoutMs ?? 300_000,\n stdout: 'ignore',\n stderr: 'pipe',\n },\n }\n}\n", "import { z } from 'zod'\n\n/**\n * Pure (node-free) vendor config schema + authoring helper. Kept separate from\n * `config.ts` (which imports node builtins for the runtime loader) so the public\n * lib entry can re-export `defineVendorConfig` without dragging node types into\n * the emitted `.d.ts`.\n */\n\n/**\n * Filename a source repo provides at its root to declare WHAT `vendor sync`\n * copies (`copy[]`). Lives ONLY on the write path \u2014 `vendor check` never loads it.\n * WHERE/WHICH to stamp (`workspaceDir` + `targets`) is machine-local and lives in\n * the user-global factory config (`~/.infra-kit/vendor.json`).\n */\nexport const VENDOR_CONFIG_FILE = 'vendor.config.ts'\n\n/**\n * A non-empty, repo-relative path with no `..` segments and no absolute prefix\n * (POSIX `/`, UNC/`\\`, or a Windows drive like `C:`). Containment guard for\n * vendor copy items so a malicious/typo config can't read or write outside the\n * source/target repo roots. Kept as a string/regex check (no `node:path`) to\n * respect this file's node-free constraint \u2014 `sync-ops.ts` does the resolved\n * runtime containment assert as defense in depth.\n */\nconst safeRelPath = z.string().refine(\n (p) => {\n const isAbsolute = /^(?:[/\\\\]|[a-z]:)/i.test(p)\n const hasDotDotSegment = p.split(/[\\\\/]/).includes('..')\n\n return !isAbsolute && !hasDotDotSegment && p.trim().length > 0\n },\n { message: 'must be a non-empty repo-relative path without \"..\" segments' },\n)\n\n/**\n * One item to sync from the source repo into each target. `vendored: true` marks\n * workspace packages that must land under `vendor/` (the single-source-of-truth\n * code); everything else is root-level tooling that stays at the repo root.\n */\nexport const vendorCopyItemSchema = z.object({\n name: z.string(),\n source: safeRelPath,\n target: safeRelPath,\n type: z.enum(['file', 'directory']),\n vendored: z.boolean().optional(),\n})\n\nexport const vendorConfigSchema = z\n .object({\n /** Files/dirs to copy. Items with `vendored: true` land under `vendor/`. */\n copy: z.array(vendorCopyItemSchema),\n })\n // Reject stray keys so a leftover `targets` (now machine-local, in\n // ~/.infra-kit/vendor.json) yields a clear \"unrecognized key\" error rather\n // than being silently ignored.\n .strict()\n\nexport type VendorCopyItem = z.infer<typeof vendorCopyItemSchema>\nexport type VendorConfig = z.infer<typeof vendorConfigSchema>\n\n/**\n * Identity helper for authoring a type-safe `vendor.config.ts` in a source repo.\n * Re-exported from the public lib entry so a source repo can\n * `import { defineVendorConfig } from 'infra-kit'`.\n *\n * NOTE: a `vendor.config.ts` must be type-strippable \u2014 Node's native type\n * stripping (Node >= 24) loads it without a build step, which forbids `enum`,\n * `namespace`, and parameter properties.\n *\n * @example\n * export default defineVendorConfig({\n * copy: [{ name: 'Configs', source: 'vendor/configs', target: 'vendor/configs', type: 'directory', vendored: true }],\n * })\n */\nexport const defineVendorConfig = (config: VendorConfig): VendorConfig => {\n return config\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["z", "execFileSync", "execFileSync", "fs", "path", "z", "path", "z", "path", "fs", "z", "z"]
7
7
  }
@@ -15,7 +15,12 @@ export declare const deployServiceSchema: z.ZodObject<{
15
15
  workflow: z.ZodOptional<z.ZodString>;
16
16
  deploy: z.ZodOptional<z.ZodString>;
17
17
  deployAll: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodArray<z.ZodString>]>>;
18
+ after: z.ZodOptional<z.ZodArray<z.ZodString>>;
19
+ runsBefore: z.ZodOptional<z.ZodArray<z.ZodString>>;
18
20
  }, z.core.$strict>;
21
+ type ServiceMap = Record<string, z.infer<typeof deployServiceSchema>>;
22
+ /** Every service mapped to the ones it waits for, whether named by its `after` or by a step's `runsBefore`. */
23
+ export declare const serviceDependencies: (services: ServiceMap) => Map<string, string[]>;
19
24
  export declare const deployConfigSchema: z.ZodObject<{
20
25
  project: z.ZodOptional<z.ZodString>;
21
26
  environments: z.ZodArray<z.ZodString>;
@@ -27,6 +32,8 @@ export declare const deployConfigSchema: z.ZodObject<{
27
32
  workflow: z.ZodOptional<z.ZodString>;
28
33
  deploy: z.ZodOptional<z.ZodString>;
29
34
  deployAll: z.ZodOptional<z.ZodUnion<readonly [z.ZodBoolean, z.ZodArray<z.ZodString>]>>;
35
+ after: z.ZodOptional<z.ZodArray<z.ZodString>>;
36
+ runsBefore: z.ZodOptional<z.ZodArray<z.ZodString>>;
30
37
  }, z.core.$strict>>;
31
38
  terraform: z.ZodOptional<z.ZodArray<z.ZodString>>;
32
39
  preDeploy: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -34,6 +41,9 @@ export declare const deployConfigSchema: z.ZodObject<{
34
41
  extraEnv: z.ZodOptional<z.ZodArray<z.ZodString>>;
35
42
  deployTimeoutMinutes: z.ZodOptional<z.ZodNumber>;
36
43
  replaceV1: z.ZodOptional<z.ZodBoolean>;
44
+ doppler: z.ZodOptional<z.ZodObject<{
45
+ project: z.ZodString;
46
+ }, z.core.$strict>>;
37
47
  }, z.core.$strict>;
38
48
  export type DeployService = z.infer<typeof deployServiceSchema>;
39
49
  export type DeployConfig = z.infer<typeof deployConfigSchema>;
@@ -51,3 +61,4 @@ export type DeployConfig = z.infer<typeof deployConfigSchema>;
51
61
  * })
52
62
  */
53
63
  export declare const defineDeployConfig: (config: DeployConfig) => DeployConfig;
64
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slip-stream-kit/config",
3
3
  "type": "module",
4
- "version": "0.39.0",
4
+ "version": "0.40.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ArthurSaenz/infra-kit.git",