@tdk-landscape/tdk-cli-core 1.3.49 → 1.3.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/layer-autoresearch/SKILL.md +48 -0
- package/.claude/skills/tdk-troubleshoot/SKILL.md +222 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +40 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +18 -0
- package/.github/dependabot.yml +33 -0
- package/.github/pull_request_template.md +5 -0
- package/.github/workflows/ci.yml +187 -0
- package/.github/workflows/release-binaries.yml +19 -9
- package/.github/zizmor.yml +9 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +42 -0
- package/LICENSE +21 -0
- package/README.md +149 -0
- package/SECURITY.md +17 -0
- package/benchmarks/results/container-scale-2026-09-25T20-54-49-736Z.json +45 -0
- package/benchmarks/results/container-scale-2026-09-25T21-01-20-540Z.json +150 -0
- package/benchmarks/results/container-scale-2026-09-25T21-15-01-441Z.json +73 -0
- package/benchmarks/results/container-scale-2026-09-25T22-09-01-630Z.json +77 -0
- package/benchmarks/results/container-scale-2026-09-25T22-18-28-371Z.json +202 -0
- package/cli/README.md +25 -0
- package/cli/biome.json +1 -1
- package/cli/dist/cli.d.ts +3 -0
- package/cli/dist/cli.d.ts.map +1 -0
- package/cli/dist/cli.js +64 -0
- package/cli/dist/cli.js.map +1 -0
- package/cli/dist/commands/completion.d.ts +3 -0
- package/cli/dist/commands/completion.d.ts.map +1 -0
- package/cli/dist/commands/completion.js +294 -0
- package/cli/dist/commands/completion.js.map +1 -0
- package/cli/dist/commands/config.d.ts +3 -0
- package/cli/dist/commands/config.d.ts.map +1 -0
- package/cli/dist/commands/config.js +231 -0
- package/cli/dist/commands/config.js.map +1 -0
- package/cli/dist/commands/doctor.d.ts +19 -0
- package/cli/dist/commands/doctor.d.ts.map +1 -0
- package/cli/dist/commands/doctor.js +745 -0
- package/cli/dist/commands/doctor.js.map +1 -0
- package/cli/dist/commands/down.d.ts +3 -0
- package/cli/dist/commands/down.d.ts.map +1 -0
- package/cli/dist/commands/down.js +32 -0
- package/cli/dist/commands/down.js.map +1 -0
- package/cli/dist/commands/help.d.ts +2 -0
- package/cli/dist/commands/help.d.ts.map +1 -0
- package/cli/dist/commands/help.js +138 -0
- package/cli/dist/commands/help.js.map +1 -0
- package/cli/dist/commands/networks.d.ts +3 -0
- package/cli/dist/commands/networks.d.ts.map +1 -0
- package/cli/dist/commands/networks.js +260 -0
- package/cli/dist/commands/networks.js.map +1 -0
- package/cli/dist/commands/project.d.ts +3 -0
- package/cli/dist/commands/project.d.ts.map +1 -0
- package/cli/dist/commands/project.js +370 -0
- package/cli/dist/commands/project.js.map +1 -0
- package/cli/dist/commands/projects.d.ts +3 -0
- package/cli/dist/commands/projects.d.ts.map +1 -0
- package/cli/dist/commands/projects.js +71 -0
- package/cli/dist/commands/projects.js.map +1 -0
- package/cli/dist/commands/resource.d.ts +69 -0
- package/cli/dist/commands/resource.d.ts.map +1 -0
- package/cli/dist/commands/resource.js +579 -0
- package/cli/dist/commands/resource.js.map +1 -0
- package/cli/dist/commands/resources.d.ts +3 -0
- package/cli/dist/commands/resources.d.ts.map +1 -0
- package/cli/dist/commands/resources.js +73 -0
- package/cli/dist/commands/resources.js.map +1 -0
- package/cli/dist/commands/stack.d.ts +3 -0
- package/cli/dist/commands/stack.d.ts.map +1 -0
- package/cli/dist/commands/stack.js +97 -0
- package/cli/dist/commands/stack.js.map +1 -0
- package/cli/dist/commands/stacks.d.ts +3 -0
- package/cli/dist/commands/stacks.d.ts.map +1 -0
- package/cli/dist/commands/stacks.js +43 -0
- package/cli/dist/commands/stacks.js.map +1 -0
- package/cli/dist/commands/status.d.ts +3 -0
- package/cli/dist/commands/status.d.ts.map +1 -0
- package/cli/dist/commands/status.js +71 -0
- package/cli/dist/commands/status.js.map +1 -0
- package/cli/dist/commands/ui.d.ts +3 -0
- package/cli/dist/commands/ui.d.ts.map +1 -0
- package/cli/dist/commands/ui.js +445 -0
- package/cli/dist/commands/ui.js.map +1 -0
- package/cli/dist/commands/up.d.ts +3 -0
- package/cli/dist/commands/up.d.ts.map +1 -0
- package/cli/dist/commands/up.js +124 -0
- package/cli/dist/commands/up.js.map +1 -0
- package/cli/dist/commands/upgrade.d.ts +11 -0
- package/cli/dist/commands/upgrade.d.ts.map +1 -0
- package/cli/dist/commands/upgrade.js +496 -0
- package/cli/dist/commands/upgrade.js.map +1 -0
- package/cli/dist/commands/version.d.ts +3 -0
- package/cli/dist/commands/version.d.ts.map +1 -0
- package/cli/dist/commands/version.js +9 -0
- package/cli/dist/commands/version.js.map +1 -0
- package/cli/dist/components/Accessible.d.ts +8 -0
- package/cli/dist/components/Accessible.d.ts.map +1 -0
- package/cli/dist/components/Accessible.js +10 -0
- package/cli/dist/components/Accessible.js.map +1 -0
- package/cli/dist/components/BaseTooltip.d.ts +4 -0
- package/cli/dist/components/BaseTooltip.d.ts.map +1 -0
- package/cli/dist/components/BaseTooltip.js +28 -0
- package/cli/dist/components/BaseTooltip.js.map +1 -0
- package/cli/dist/components/DetailPanel.d.ts +4 -0
- package/cli/dist/components/DetailPanel.d.ts.map +1 -0
- package/cli/dist/components/DetailPanel.js +18 -0
- package/cli/dist/components/DetailPanel.js.map +1 -0
- package/cli/dist/components/FileTree.d.ts +4 -0
- package/cli/dist/components/FileTree.d.ts.map +1 -0
- package/cli/dist/components/FileTree.js +47 -0
- package/cli/dist/components/FileTree.js.map +1 -0
- package/cli/dist/components/ResourceSelectInput.d.ts +4 -0
- package/cli/dist/components/ResourceSelectInput.d.ts.map +1 -0
- package/cli/dist/components/ResourceSelectInput.js +7 -0
- package/cli/dist/components/ResourceSelectInput.js.map +1 -0
- package/cli/dist/components/ResourceTable.d.ts +4 -0
- package/cli/dist/components/ResourceTable.d.ts.map +1 -0
- package/cli/dist/components/ResourceTable.js +18 -0
- package/cli/dist/components/ResourceTable.js.map +1 -0
- package/cli/dist/components/TabBar.d.ts +4 -0
- package/cli/dist/components/TabBar.d.ts.map +1 -0
- package/cli/dist/components/TabBar.js +17 -0
- package/cli/dist/components/TabBar.js.map +1 -0
- package/cli/dist/components/Tooltip.d.ts +12 -0
- package/cli/dist/components/Tooltip.d.ts.map +1 -0
- package/cli/dist/components/Tooltip.js +12 -0
- package/cli/dist/components/Tooltip.js.map +1 -0
- package/cli/dist/components/index.d.ts +8 -0
- package/cli/dist/components/index.d.ts.map +1 -0
- package/cli/dist/components/index.js +8 -0
- package/cli/dist/components/index.js.map +1 -0
- package/cli/dist/config/platform-standards.d.ts +159 -0
- package/cli/dist/config/platform-standards.d.ts.map +1 -0
- package/cli/dist/config/platform-standards.js +181 -0
- package/cli/dist/config/platform-standards.js.map +1 -0
- package/cli/dist/generator/extension-fetch.d.ts +25 -0
- package/cli/dist/generator/extension-fetch.d.ts.map +1 -0
- package/cli/dist/generator/extension-fetch.js +259 -0
- package/cli/dist/generator/extension-fetch.js.map +1 -0
- package/cli/dist/generator/template-engine.d.ts +62 -0
- package/cli/dist/generator/template-engine.d.ts.map +1 -0
- package/cli/dist/generator/template-engine.js +623 -0
- package/cli/dist/generator/template-engine.js.map +1 -0
- package/cli/dist/index.d.ts +11 -0
- package/cli/dist/index.d.ts.map +1 -0
- package/cli/dist/index.js +11 -0
- package/cli/dist/index.js.map +1 -0
- package/cli/dist/types/index.d.ts +257 -0
- package/cli/dist/types/index.d.ts.map +1 -0
- package/cli/dist/types/index.js +17 -0
- package/cli/dist/types/index.js.map +1 -0
- package/cli/dist/utils/cache.d.ts +19 -0
- package/cli/dist/utils/cache.d.ts.map +1 -0
- package/cli/dist/utils/cache.js +64 -0
- package/cli/dist/utils/cache.js.map +1 -0
- package/cli/dist/utils/command-helpers.d.ts +17 -0
- package/cli/dist/utils/command-helpers.d.ts.map +1 -0
- package/cli/dist/utils/command-helpers.js +41 -0
- package/cli/dist/utils/command-helpers.js.map +1 -0
- package/cli/dist/utils/constants.d.ts +53 -0
- package/cli/dist/utils/constants.d.ts.map +1 -0
- package/cli/dist/utils/constants.js +115 -0
- package/cli/dist/utils/constants.js.map +1 -0
- package/cli/dist/utils/discovery-context.d.ts +4 -0
- package/cli/dist/utils/discovery-context.d.ts.map +1 -0
- package/cli/dist/utils/discovery-context.js +39 -0
- package/cli/dist/utils/discovery-context.js.map +1 -0
- package/cli/dist/utils/docker.d.ts +4 -0
- package/cli/dist/utils/docker.d.ts.map +1 -0
- package/cli/dist/utils/docker.js +19 -0
- package/cli/dist/utils/docker.js.map +1 -0
- package/cli/dist/utils/doctor-runtime.d.ts +81 -0
- package/cli/dist/utils/doctor-runtime.d.ts.map +1 -0
- package/cli/dist/utils/doctor-runtime.js +498 -0
- package/cli/dist/utils/doctor-runtime.js.map +1 -0
- package/cli/dist/utils/env-validator.d.ts +15 -0
- package/cli/dist/utils/env-validator.d.ts.map +1 -0
- package/cli/dist/utils/env-validator.js +109 -0
- package/cli/dist/utils/env-validator.js.map +1 -0
- package/cli/dist/utils/errors.d.ts +31 -0
- package/cli/dist/utils/errors.d.ts.map +1 -0
- package/cli/dist/utils/errors.js +140 -0
- package/cli/dist/utils/errors.js.map +1 -0
- package/cli/dist/utils/file-helpers.d.ts +14 -0
- package/cli/dist/utils/file-helpers.d.ts.map +1 -0
- package/cli/dist/utils/file-helpers.js +45 -0
- package/cli/dist/utils/file-helpers.js.map +1 -0
- package/cli/dist/utils/formatting.d.ts +30 -0
- package/cli/dist/utils/formatting.d.ts.map +1 -0
- package/cli/dist/utils/formatting.js +164 -0
- package/cli/dist/utils/formatting.js.map +1 -0
- package/cli/dist/utils/paths.d.ts +3 -0
- package/cli/dist/utils/paths.d.ts.map +1 -0
- package/cli/dist/utils/paths.js +41 -0
- package/cli/dist/utils/paths.js.map +1 -0
- package/cli/dist/utils/port-assignment.d.ts +6 -0
- package/cli/dist/utils/port-assignment.d.ts.map +1 -0
- package/cli/dist/utils/port-assignment.js +81 -0
- package/cli/dist/utils/port-assignment.js.map +1 -0
- package/cli/dist/utils/project-features.d.ts +23 -0
- package/cli/dist/utils/project-features.d.ts.map +1 -0
- package/cli/dist/utils/project-features.js +98 -0
- package/cli/dist/utils/project-features.js.map +1 -0
- package/cli/dist/utils/project-templates.d.ts +7 -0
- package/cli/dist/utils/project-templates.d.ts.map +1 -0
- package/cli/dist/utils/project-templates.js +24 -0
- package/cli/dist/utils/project-templates.js.map +1 -0
- package/cli/dist/utils/prompt.d.ts +28 -0
- package/cli/dist/utils/prompt.d.ts.map +1 -0
- package/cli/dist/utils/prompt.js +79 -0
- package/cli/dist/utils/prompt.js.map +1 -0
- package/cli/dist/utils/resource-features.d.ts +29 -0
- package/cli/dist/utils/resource-features.d.ts.map +1 -0
- package/cli/dist/utils/resource-features.js +175 -0
- package/cli/dist/utils/resource-features.js.map +1 -0
- package/cli/dist/utils/service-urls.d.ts +30 -0
- package/cli/dist/utils/service-urls.d.ts.map +1 -0
- package/cli/dist/utils/service-urls.js +93 -0
- package/cli/dist/utils/service-urls.js.map +1 -0
- package/cli/dist/utils/services.d.ts +18 -0
- package/cli/dist/utils/services.d.ts.map +1 -0
- package/cli/dist/utils/services.js +292 -0
- package/cli/dist/utils/services.js.map +1 -0
- package/cli/dist/utils/stack-features.d.ts +22 -0
- package/cli/dist/utils/stack-features.d.ts.map +1 -0
- package/cli/dist/utils/stack-features.js +13 -0
- package/cli/dist/utils/stack-features.js.map +1 -0
- package/cli/dist/utils/tar.d.ts +7 -0
- package/cli/dist/utils/tar.d.ts.map +1 -0
- package/cli/dist/utils/tar.js +21 -0
- package/cli/dist/utils/tar.js.map +1 -0
- package/cli/dist/utils/tilt.d.ts +20 -0
- package/cli/dist/utils/tilt.d.ts.map +1 -0
- package/cli/dist/utils/tilt.js +95 -0
- package/cli/dist/utils/tilt.js.map +1 -0
- package/cli/dist/utils/validation.d.ts +16 -0
- package/cli/dist/utils/validation.d.ts.map +1 -0
- package/cli/dist/utils/validation.js +58 -0
- package/cli/dist/utils/validation.js.map +1 -0
- package/cli/package.json +11 -12
- package/cli/src/commands/__tests__/always-enabled-infra.test.ts +2 -8
- package/cli/src/commands/__tests__/doctor.test.ts +181 -8
- package/cli/src/commands/__tests__/phases-model.test.ts +2 -4
- package/cli/src/commands/__tests__/project-resource-features-e2e.test.ts +4 -24
- package/cli/src/commands/__tests__/saas-starter-integration.test.ts +8 -10
- package/cli/src/commands/__tests__/stack-features-e2e.test.ts +10 -4
- package/cli/src/commands/__tests__/upgrade.test.ts +23 -13
- package/cli/src/commands/doctor.ts +136 -0
- package/cli/src/commands/help.ts +13 -3
- package/cli/src/commands/project.ts +11 -14
- package/cli/src/commands/projects.ts +2 -2
- package/cli/src/commands/up.ts +13 -4
- package/cli/src/commands/upgrade.ts +11 -4
- package/cli/src/config/platform-standards.test.ts +6 -6
- package/cli/src/generator/__tests__/extension-fetch.test.ts +36 -14
- package/cli/src/generator/extension-fetch.ts +12 -5
- package/cli/src/generator/template-engine.ts +19 -21
- package/cli/src/types/index.ts +2 -0
- package/cli/src/utils/__tests__/tilt.test.ts +29 -0
- package/cli/src/utils/constants.ts +4 -0
- package/cli/src/utils/doctor-runtime.ts +67 -27
- package/cli/src/utils/env-validator.ts +2 -2
- package/cli/src/utils/project-templates.ts +2 -1
- package/cli/src/utils/prompt.ts +2 -4
- package/cli/src/utils/resource-features.ts +10 -4
- package/cli/src/utils/services.ts +2 -1
- package/cli/src/utils/tilt.ts +20 -0
- package/engine/assets/docker/wake-gateway/Dockerfile +26 -0
- package/engine/assets/docker/wake-gateway/decide.test.ts +87 -0
- package/engine/assets/docker/wake-gateway/decide.ts +75 -0
- package/engine/assets/docker/wake-gateway/server.ts +220 -0
- package/engine/topologies/platform/docker/build/golden_image_dockerfile.star +13 -23
- package/engine/topologies/platform/docker/compose/compose.star +7 -14
- package/engine/topologies/platform/docker/compose/compose_helpers.star +1 -1
- package/engine/topologies/platform/docker/compose/traefik_standalone.star +44 -6
- package/engine/topologies/platform/docker/config/healthcheck.star +56 -0
- package/engine/topologies/platform/docker/constants.star +10 -0
- package/engine/topologies/platform/docker/layers/l1_base_layers.star +1 -1
- package/engine/topologies/platform/docker/layers/l4_runtime_layers.star +55 -20
- package/engine/topologies/platform/docker/networking/traefik_static_routes.star +107 -0
- package/engine/topologies/tilt/generators/typescript/backend_tsconfig.star +2 -2
- package/engine/topologies/tilt/generators/typescript/frontend_tsconfig.star +1 -1
- package/engine/topologies/tilt/manifest/schema.star +6 -0
- package/engine/topologies/tilt/manifest/validator.star +16 -0
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +107 -8
- package/engine/topologies/tilt/resources/orchestrator/builders/typescript.star +12 -6
- package/engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star +2 -1
- package/knip.json +1 -1
- package/openspec/changes/optimize-runtime-docker-layers/.openspec.yaml +3 -0
- package/openspec/changes/optimize-runtime-docker-layers/design.md +81 -0
- package/openspec/changes/optimize-runtime-docker-layers/proposal.md +44 -0
- package/openspec/changes/optimize-runtime-docker-layers/specs/runtime-container-footprint/spec.md +79 -0
- package/openspec/changes/optimize-runtime-docker-layers/tasks.md +36 -0
- package/openspec/changes/prioritized-cold-start/.openspec.yaml +4 -0
- package/openspec/changes/prioritized-cold-start/design.md +113 -0
- package/openspec/changes/prioritized-cold-start/proposal.md +40 -0
- package/openspec/changes/prioritized-cold-start/specs/on-demand-resource-startup/spec.md +92 -0
- package/openspec/changes/prioritized-cold-start/tasks.md +46 -0
- package/openspec/config.yaml +1 -0
- package/package.json +35 -5
- package/scripts/autoresearch/layer-eval.sh +65 -0
- package/scripts/benchmark/README.md +25 -0
- package/scripts/benchmark/container-scale.ts +503 -0
- package/scripts/benchmark/pre-release-gate.sh +9 -0
- package/scripts/measure-idle-footprint.sh +58 -0
- package/tests/tilt-engine/test_runtime_container_footprint.py +162 -0
- package/tests/tilt-engine/test_sablier_defer_start.py +196 -0
- package/tests/tilt-engine/test_standalone_traefik.py +38 -0
- package/tests/tilt-engine/test_traefik_static_wake_route.py +119 -0
- package/bun.lock +0 -538
|
@@ -104,6 +104,35 @@ describe("buildTiltUpArgs", () => {
|
|
|
104
104
|
buildTiltUpArgs([], {});
|
|
105
105
|
expect(findProjectRoot).toHaveBeenCalledTimes(1);
|
|
106
106
|
});
|
|
107
|
+
|
|
108
|
+
it("should include -- --focus=<stack> before service names when focusTargets is set", async () => {
|
|
109
|
+
// Regression test: `tdk up <stack>` used to pass only bare positional service
|
|
110
|
+
// names, which the generated Tiltfile's focus filter never reads (it only reads
|
|
111
|
+
// --focus), so it silently fell back to building every stack instead of the one
|
|
112
|
+
// requested. --focus must be present, carry the stack name, and be preceded by
|
|
113
|
+
// `--` (tilt up rejects "--focus" as an unrecognized top-level flag otherwise -
|
|
114
|
+
// `--` is what routes it to the Tiltfile's own config.parse()).
|
|
115
|
+
const { buildTiltUpArgs } = await import("../tilt.js");
|
|
116
|
+
const args = buildTiltUpArgs(["finance-api", "finance-app"], { focusTargets: ["finance"] });
|
|
117
|
+
const dashDashIndex = args.indexOf("--");
|
|
118
|
+
expect(dashDashIndex).toBeGreaterThanOrEqual(0);
|
|
119
|
+
expect(args[dashDashIndex + 1]).toBe("--focus=finance");
|
|
120
|
+
expect(args.indexOf("--focus=finance")).toBeLessThan(args.indexOf("finance-api"));
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should join multiple focusTargets with commas in a single --focus flag", async () => {
|
|
124
|
+
const { buildTiltUpArgs } = await import("../tilt.js");
|
|
125
|
+
const args = buildTiltUpArgs([], { focusTargets: ["finance", "hr"] });
|
|
126
|
+
expect(args).toContain("--focus=finance,hr");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should NOT include --focus when focusTargets is empty or omitted", async () => {
|
|
130
|
+
const { buildTiltUpArgs } = await import("../tilt.js");
|
|
131
|
+
const withEmpty = buildTiltUpArgs(["svc"], { focusTargets: [] });
|
|
132
|
+
const withOmitted = buildTiltUpArgs(["svc"], {});
|
|
133
|
+
expect(withEmpty.some((a) => a.startsWith("--focus"))).toBe(false);
|
|
134
|
+
expect(withOmitted.some((a) => a.startsWith("--focus"))).toBe(false);
|
|
135
|
+
});
|
|
107
136
|
});
|
|
108
137
|
|
|
109
138
|
describe("buildTiltDownArgs", () => {
|
|
@@ -27,6 +27,10 @@ export const REQUIRED_PACKAGE_SCRIPTS: Record<ResourceType, readonly string[]> =
|
|
|
27
27
|
sdk: ["dev", "build"],
|
|
28
28
|
} as const;
|
|
29
29
|
|
|
30
|
+
// Version specifiers that pin nothing at all - a fresh `bun install`/`npm install`
|
|
31
|
+
// can pull a different (possibly breaking) version on every run.
|
|
32
|
+
export const UNPINNED_DEPENDENCY_VERSIONS = new Set(["latest", "*"]);
|
|
33
|
+
|
|
30
34
|
export const OPTIONAL_INFRA_SERVICES = [
|
|
31
35
|
"monitoring",
|
|
32
36
|
"elk",
|
|
@@ -5,6 +5,7 @@ import type { CheckResult } from "../types/index.js";
|
|
|
5
5
|
import { formatCount } from "./formatting.js";
|
|
6
6
|
import { findProjectRoot } from "./paths.js";
|
|
7
7
|
import { getProjectName } from "./service-urls.js";
|
|
8
|
+
import { discoverResources } from "./services.js";
|
|
8
9
|
|
|
9
10
|
const EXEC_TIMEOUT_MS = 10_000;
|
|
10
11
|
const REGISTRY_PROBE_TIMEOUT_MS = 3_000;
|
|
@@ -88,12 +89,41 @@ interface TiltUiResourceItem {
|
|
|
88
89
|
};
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Resource names with `sablier: {enable: true, deferStart: true}` in their
|
|
94
|
+
* manifest (openspec/changes/prioritized-cold-start): these are expected to
|
|
95
|
+
* sit at `runtimeStatus: none` until their first request, not a sign of
|
|
96
|
+
* anything stuck. Best-effort — returns an empty set if discovery fails
|
|
97
|
+
* (e.g. not run from inside a project), so callers degrade to today's
|
|
98
|
+
* behavior rather than erroring.
|
|
99
|
+
*/
|
|
100
|
+
export function getDeferredResourceNames(): Set<string> {
|
|
101
|
+
try {
|
|
102
|
+
return new Set(
|
|
103
|
+
discoverResources()
|
|
104
|
+
.filter((svc) => svc.config?.sablier?.enable && svc.config?.sablier?.deferStart)
|
|
105
|
+
.map((svc) => svc.name),
|
|
106
|
+
);
|
|
107
|
+
} catch {
|
|
108
|
+
return new Set();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
91
112
|
/**
|
|
92
113
|
* Extract failing UIResources from `tilt get uiresources -o json`.
|
|
114
|
+
*
|
|
115
|
+
* `deferredNames` (typically from getDeferredResourceNames()) separates a
|
|
116
|
+
* `sablier.deferStart` resource's expected `none`/`pending` status into its
|
|
117
|
+
* own count so it's never described as "blocked" or otherwise lumped in with
|
|
118
|
+
* genuinely stuck resources.
|
|
93
119
|
*/
|
|
94
|
-
export function parseTiltResourceFailures(
|
|
120
|
+
export function parseTiltResourceFailures(
|
|
121
|
+
jsonText: string,
|
|
122
|
+
deferredNames: Set<string> = new Set(),
|
|
123
|
+
): {
|
|
95
124
|
failures: TiltResourceFailure[];
|
|
96
125
|
pendingCount: number;
|
|
126
|
+
deferredCount: number;
|
|
97
127
|
okCount: number;
|
|
98
128
|
total: number;
|
|
99
129
|
} {
|
|
@@ -101,6 +131,7 @@ export function parseTiltResourceFailures(jsonText: string): {
|
|
|
101
131
|
const items = parsed.items ?? [];
|
|
102
132
|
const failures: TiltResourceFailure[] = [];
|
|
103
133
|
let pendingCount = 0;
|
|
134
|
+
let deferredCount = 0;
|
|
104
135
|
let okCount = 0;
|
|
105
136
|
|
|
106
137
|
for (const item of items) {
|
|
@@ -133,11 +164,15 @@ export function parseTiltResourceFailures(jsonText: string): {
|
|
|
133
164
|
updateStatus === "none" ||
|
|
134
165
|
runtimeStatus === "none")
|
|
135
166
|
) {
|
|
136
|
-
|
|
167
|
+
if (deferredNames.has(name)) {
|
|
168
|
+
deferredCount += 1;
|
|
169
|
+
} else {
|
|
170
|
+
pendingCount += 1;
|
|
171
|
+
}
|
|
137
172
|
}
|
|
138
173
|
}
|
|
139
174
|
|
|
140
|
-
return { failures, pendingCount, okCount, total: items.length };
|
|
175
|
+
return { failures, pendingCount, deferredCount, okCount, total: items.length };
|
|
141
176
|
}
|
|
142
177
|
|
|
143
178
|
/**
|
|
@@ -156,9 +191,7 @@ export function summarizeTiltBuildError(error: string): string {
|
|
|
156
191
|
}
|
|
157
192
|
|
|
158
193
|
const refusedPackages = [
|
|
159
|
-
...normalized.matchAll(
|
|
160
|
-
/ConnectionRefused downloading package manifest (@[A-Za-z0-9._/-]+)/gi,
|
|
161
|
-
),
|
|
194
|
+
...normalized.matchAll(/ConnectionRefused downloading package manifest (@[A-Za-z0-9._/-]+)/gi),
|
|
162
195
|
].map((match) => match[1]);
|
|
163
196
|
if (refusedPackages.length > 0) {
|
|
164
197
|
const unique = [...new Set(refusedPackages)];
|
|
@@ -167,7 +200,11 @@ export function summarizeTiltBuildError(error: string): string {
|
|
|
167
200
|
return `private registry ConnectionRefused for ${shown}${more} — Verdaccio (:4873) is down or unreachable from the build`;
|
|
168
201
|
}
|
|
169
202
|
|
|
170
|
-
if (
|
|
203
|
+
if (
|
|
204
|
+
/failed to resolve source metadata for docker\.io\/library\/(beauty-crm-[^:\s]+)/i.test(
|
|
205
|
+
normalized,
|
|
206
|
+
)
|
|
207
|
+
) {
|
|
171
208
|
const image = normalized.match(
|
|
172
209
|
/failed to resolve source metadata for docker\.io\/library\/(beauty-crm-[^:\s]+)/i,
|
|
173
210
|
)?.[1];
|
|
@@ -186,10 +223,12 @@ export function summarizeTiltBuildError(error: string): string {
|
|
|
186
223
|
normalized.match(/No such image:\s*([^\s]+)/i)?.[1] ??
|
|
187
224
|
normalized.match(/pull access denied for ([^,:\s]+)/i)?.[1] ??
|
|
188
225
|
undefined;
|
|
189
|
-
const composeService = normalized.match(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
226
|
+
const composeService = normalized.match(/\bup\s+-d\s+--no-build\s+([A-Za-z0-9._-]+)/i)?.[1];
|
|
227
|
+
if (
|
|
228
|
+
missingImage ||
|
|
229
|
+
composeService ||
|
|
230
|
+
/docker compose .*\bup\s+-d\s+--no-build\b/i.test(normalized)
|
|
231
|
+
) {
|
|
193
232
|
const imageLabel = missingImage ?? `${composeService ?? "service"}:dev`;
|
|
194
233
|
const serviceHint = composeService ? ` for ${composeService}` : "";
|
|
195
234
|
return `compose run failed because image is missing (${imageLabel})${serviceHint} — wait for the ImageBuild resource to finish, then re-trigger the *-run-only resource`;
|
|
@@ -212,7 +251,9 @@ export function isRegistryRelatedBuildError(error: string): boolean {
|
|
|
212
251
|
);
|
|
213
252
|
}
|
|
214
253
|
|
|
215
|
-
export function projectExpectsVerdaccio(
|
|
254
|
+
export function projectExpectsVerdaccio(
|
|
255
|
+
projectRoot: string = findProjectRoot() ?? process.cwd(),
|
|
256
|
+
): boolean {
|
|
216
257
|
const projectJsonPath = join(projectRoot, ".tdk", "project.json");
|
|
217
258
|
if (existsSync(projectJsonPath)) {
|
|
218
259
|
try {
|
|
@@ -236,10 +277,7 @@ export function projectExpectsVerdaccio(projectRoot: string = findProjectRoot()
|
|
|
236
277
|
}
|
|
237
278
|
|
|
238
279
|
// Heuristic: scoped private registry pointed at :4873
|
|
239
|
-
for (const candidate of [
|
|
240
|
-
join(projectRoot, ".npmrc"),
|
|
241
|
-
join(projectRoot, "package.json"),
|
|
242
|
-
]) {
|
|
280
|
+
for (const candidate of [join(projectRoot, ".npmrc"), join(projectRoot, "package.json")]) {
|
|
243
281
|
if (!existsSync(candidate)) continue;
|
|
244
282
|
try {
|
|
245
283
|
const text = readFileSync(candidate, "utf-8");
|
|
@@ -419,7 +457,7 @@ export function checkTiltResourceHealth(exec: typeof execSync = execSync): Check
|
|
|
419
457
|
|
|
420
458
|
let parsed: ReturnType<typeof parseTiltResourceFailures>;
|
|
421
459
|
try {
|
|
422
|
-
parsed = parseTiltResourceFailures(jsonText);
|
|
460
|
+
parsed = parseTiltResourceFailures(jsonText, getDeferredResourceNames());
|
|
423
461
|
} catch {
|
|
424
462
|
return {
|
|
425
463
|
name: "Tilt Resources",
|
|
@@ -439,10 +477,11 @@ export function checkTiltResourceHealth(exec: typeof execSync = execSync): Check
|
|
|
439
477
|
}
|
|
440
478
|
|
|
441
479
|
if (parsed.failures.length === 0) {
|
|
480
|
+
const deferredNote = parsed.deferredCount > 0 ? `, ${parsed.deferredCount} deferred` : "";
|
|
442
481
|
return {
|
|
443
482
|
name: "Tilt Resources",
|
|
444
483
|
didPass: true,
|
|
445
|
-
message: `Tilt resources healthy (${parsed.okCount} ok, ${parsed.pendingCount} pending)`,
|
|
484
|
+
message: `Tilt resources healthy (${parsed.okCount} ok, ${parsed.pendingCount} pending${deferredNote})`,
|
|
446
485
|
};
|
|
447
486
|
}
|
|
448
487
|
|
|
@@ -458,9 +497,7 @@ export function checkTiltResourceHealth(exec: typeof execSync = execSync): Check
|
|
|
458
497
|
failure,
|
|
459
498
|
why: describeTiltFailure(failure, exec),
|
|
460
499
|
}));
|
|
461
|
-
const details = described
|
|
462
|
-
.map(({ failure, why }) => `${failure.name}: ${why}`)
|
|
463
|
-
.join("\n ");
|
|
500
|
+
const details = described.map(({ failure, why }) => `${failure.name}: ${why}`).join("\n ");
|
|
464
501
|
const omittedNote =
|
|
465
502
|
omitted > 0 ? `\n …and ${formatCount(omitted, "more failed resource")}` : "";
|
|
466
503
|
|
|
@@ -468,6 +505,10 @@ export function checkTiltResourceHealth(exec: typeof execSync = execSync): Check
|
|
|
468
505
|
parsed.pendingCount > 0
|
|
469
506
|
? `\n ${formatCount(parsed.pendingCount, "resource")} still pending/not started — often blocked by the failures above.`
|
|
470
507
|
: "";
|
|
508
|
+
const deferredNote =
|
|
509
|
+
parsed.deferredCount > 0
|
|
510
|
+
? `\n ${formatCount(parsed.deferredCount, "resource")} intentionally deferred (sablier.deferStart) — not started until first request, unrelated to the failures above.`
|
|
511
|
+
: "";
|
|
471
512
|
|
|
472
513
|
const hasPortConflict = ordered.some((failure) =>
|
|
473
514
|
/port is already allocated/i.test(failure.error),
|
|
@@ -497,13 +538,12 @@ export function checkTiltResourceHealth(exec: typeof execSync = execSync): Check
|
|
|
497
538
|
return {
|
|
498
539
|
name: "Tilt Resources",
|
|
499
540
|
didPass: false,
|
|
500
|
-
message: `Tilt reports ${formatCount(parsed.failures.length, "failed resource")}:\n ${details}${omittedNote}${pendingNote}`,
|
|
541
|
+
message: `Tilt reports ${formatCount(parsed.failures.length, "failed resource")}:\n ${details}${omittedNote}${pendingNote}${deferredNote}`,
|
|
501
542
|
fix,
|
|
502
543
|
};
|
|
503
544
|
}
|
|
504
545
|
|
|
505
|
-
const INFRA_FAILURE_NAME =
|
|
506
|
-
/^(traefik|postgres|nats|infisical|verdaccio|proxy)([.-]|$)/i;
|
|
546
|
+
const INFRA_FAILURE_NAME = /^(traefik|postgres|nats|infisical|verdaccio|proxy)([.-]|$)/i;
|
|
507
547
|
|
|
508
548
|
export function isInfraTiltFailure(name: string): boolean {
|
|
509
549
|
return INFRA_FAILURE_NAME.test(name);
|
|
@@ -563,9 +603,9 @@ export function probeContainerRuntimeError(
|
|
|
563
603
|
|
|
564
604
|
// Zod env/config validation: pull required field paths from the dump.
|
|
565
605
|
if (/ZodError/i.test(logs)) {
|
|
566
|
-
const requiredPaths = [
|
|
567
|
-
|
|
568
|
-
|
|
606
|
+
const requiredPaths = [...logs.matchAll(/"path"\s*:\s*\[\s*"([^"]+)"\s*\]/g)].map(
|
|
607
|
+
(match) => match[1],
|
|
608
|
+
);
|
|
569
609
|
const uniquePaths = [...new Set(requiredPaths)].slice(0, 6);
|
|
570
610
|
if (uniquePaths.length > 0) {
|
|
571
611
|
return `runtime crash: ZodError missing required config/env: ${uniquePaths.join(", ")}`;
|
|
@@ -52,7 +52,7 @@ function generateDbPassword(): string {
|
|
|
52
52
|
return randomBytes(16).toString("hex");
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export function generateEnvFile(
|
|
55
|
+
export function generateEnvFile(_projectRoot: string): string {
|
|
56
56
|
const lines: string[] = [
|
|
57
57
|
"# 🔧 TDK Environment Configuration",
|
|
58
58
|
"# Auto-generated by TDK CLI - edit as needed",
|
|
@@ -107,7 +107,7 @@ export function validateEnvFile(projectRoot: string): {
|
|
|
107
107
|
.split("\n")
|
|
108
108
|
.filter((line) => !line.startsWith("#") && line.includes("="))
|
|
109
109
|
.map((line) => line.split("=")[0])
|
|
110
|
-
.filter((name) => name.length > 0)
|
|
110
|
+
.filter((name) => name.length > 0),
|
|
111
111
|
);
|
|
112
112
|
|
|
113
113
|
for (const envVar of REQUIRED_ENV_VARS) {
|
|
@@ -11,7 +11,8 @@ export const PROJECT_TEMPLATES: Record<string, ProjectTemplate> = {
|
|
|
11
11
|
},
|
|
12
12
|
saas: {
|
|
13
13
|
repo: "https://github.com/tdk-landscape/tdk-saas-starter.git",
|
|
14
|
-
description:
|
|
14
|
+
description:
|
|
15
|
+
"SaaS starter: account dashboard and a working checkout button (Hono + Vite + Bun)",
|
|
15
16
|
},
|
|
16
17
|
erp: {
|
|
17
18
|
repo: "https://github.com/tdk-landscape/tdk-erp-system.git",
|
package/cli/src/utils/prompt.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { createInterface } from "node:readline/promises";
|
|
2
1
|
import { stdin as input, stdout as output } from "node:process";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
3
|
|
|
4
4
|
type ValidationResult = true | string;
|
|
5
5
|
|
|
@@ -67,9 +67,7 @@ export async function promptConfirm(options: ConfirmPromptOptions): Promise<bool
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
export async function promptSelect<T extends string>(
|
|
71
|
-
options: ChoicePromptOptions<T>,
|
|
72
|
-
): Promise<T> {
|
|
70
|
+
export async function promptSelect<T extends string>(options: ChoicePromptOptions<T>): Promise<T> {
|
|
73
71
|
console.log(options.message);
|
|
74
72
|
options.choices.forEach((choice, index) => {
|
|
75
73
|
console.log(` ${index + 1}. ${choice.title}`);
|
|
@@ -92,7 +92,8 @@ export const RESOURCE_FEATURES: Record<string, ResourceFeature> = {
|
|
|
92
92
|
description: "Docker container configuration for production builds",
|
|
93
93
|
applies_to: ["frontend", "backend", "worker", "migrator", "sdk"],
|
|
94
94
|
enabled_by_default: true, // Always enabled
|
|
95
|
-
generator_file:
|
|
95
|
+
generator_file:
|
|
96
|
+
"engine/topologies/tilt/resources/orchestrator/generators/manifest_resource.star",
|
|
96
97
|
output_file: ".autogenerated/Dockerfile.app",
|
|
97
98
|
},
|
|
98
99
|
|
|
@@ -169,15 +170,20 @@ export const RESOURCE_FEATURES: Record<string, ResourceFeature> = {
|
|
|
169
170
|
name: "sablier",
|
|
170
171
|
description:
|
|
171
172
|
"On-demand scaling: stop this resource when idle, wake it on the next request via a " +
|
|
172
|
-
"`sablier: {enable: true}` manifest block (not the featuresEnabled array) " +
|
|
173
|
+
"`sablier: {enable: true}` manifest block (not the featuresEnabled array). Add " +
|
|
174
|
+
"`deferStart: true` to also skip this resource during `tdk up` entirely — no container " +
|
|
175
|
+
"is created or started, and a Traefik static route wakes it on the first request instead " +
|
|
176
|
+
"(backends only for now), so it doesn't wait behind the rest of a large landscape's " +
|
|
177
|
+
"bring-up. Requires `enable: true`. " +
|
|
173
178
|
"(Premium - requires TDK_LICENSE_KEY; free tier writes a disabled stub; see " +
|
|
174
179
|
"hasSablierLicense() in extension-fetch.ts KNOWN_RESOURCES/PREMIUM_PATH_MAP)",
|
|
175
180
|
applies_to: ["backend", "frontend"],
|
|
176
181
|
enabled_by_default: false,
|
|
177
182
|
category: "premium",
|
|
178
183
|
generator_file:
|
|
179
|
-
"engine/topologies/platform/docker/networking/sablier_container_cycle.star (stub; real generator in tdk-cli-extensions/premium/networking/), engine/topologies/platform/docker/networking/traefik.star",
|
|
180
|
-
output_file:
|
|
184
|
+
"engine/topologies/platform/docker/networking/sablier_container_cycle.star (stub; real generator in tdk-cli-extensions/premium/networking/), engine/topologies/platform/docker/networking/traefik.star, engine/topologies/platform/docker/compose/traefik_standalone.star (deferStart wake gateway + static route), engine/topologies/platform/docker/compose/compose.star (deferStart depends_on)",
|
|
185
|
+
output_file:
|
|
186
|
+
"(Traefik + Sablier Docker labels on the resource's own compose service; with deferStart, also a generated Traefik file-provider dynamic-config entry and a depends_on block)",
|
|
181
187
|
},
|
|
182
188
|
};
|
|
183
189
|
|
|
@@ -73,7 +73,8 @@ function parseResource(serviceJsonPath: string): DiscoveredResource {
|
|
|
73
73
|
const parsedConfig = parsed as unknown as ResourceConfig;
|
|
74
74
|
const config: ResourceConfig = {
|
|
75
75
|
...parsedConfig,
|
|
76
|
-
appType: (parsedConfig.appType ||
|
|
76
|
+
appType: (parsedConfig.appType ||
|
|
77
|
+
(parsedConfig as unknown as { type?: string }).type) as ResourceConfig["appType"],
|
|
77
78
|
};
|
|
78
79
|
const resourceDir = dirname(serviceJsonPath);
|
|
79
80
|
|
package/cli/src/utils/tilt.ts
CHANGED
|
@@ -84,10 +84,30 @@ export function buildTiltUpArgs(
|
|
|
84
84
|
quiet?: boolean;
|
|
85
85
|
force?: boolean;
|
|
86
86
|
watch?: boolean;
|
|
87
|
+
focusTargets?: string[];
|
|
87
88
|
} = {},
|
|
88
89
|
): string[] {
|
|
89
90
|
const args: string[] = [];
|
|
90
91
|
addTiltfilePath(args);
|
|
92
|
+
|
|
93
|
+
// Bare positional resource names only scope what this `tilt up` invocation waits
|
|
94
|
+
// on - they do NOT limit which resources the generated Tiltfile enables. Its focus
|
|
95
|
+
// filter (Config.apply_focus / apply_focus_filter) only reads the `--focus` flag;
|
|
96
|
+
// the positional-args catch-all it also defines (`cfg['args']`) is never read. So
|
|
97
|
+
// without `--focus`, every `tdk up <stack>` silently fell back to Tilt's default
|
|
98
|
+
// "pre-alpha" phase and built every stack in that phase, not just the one asked
|
|
99
|
+
// for. Passing the stack name via `--focus` reuses the Tiltfile's own (already
|
|
100
|
+
// dependency-aware) domain expansion instead of re-deriving it here.
|
|
101
|
+
//
|
|
102
|
+
// `--focus=...` needs a `--` before it: Tilt's own `tilt up` only accepts its own
|
|
103
|
+
// fixed flag set directly (see `tilt up --help`) and rejects anything else with
|
|
104
|
+
// "unknown flag" - `--` is what routes a token through to the Tiltfile's own
|
|
105
|
+
// config.parse(). Bare words like a service name aren't flag-shaped, so they don't
|
|
106
|
+
// need `--` to reach the Tiltfile's positional `args` catch-all.
|
|
107
|
+
if (options.focusTargets && options.focusTargets.length > 0) {
|
|
108
|
+
args.push("--", `--focus=${options.focusTargets.join(",")}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
91
111
|
args.push(...serviceNames);
|
|
92
112
|
|
|
93
113
|
if (options.verbose && !options.quiet) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Wake gateway (see openspec/changes/prioritized-cold-start): fronts every
|
|
2
|
+
# `sablier.deferStart` resource's Traefik static route, triggers cold
|
|
3
|
+
# containers via `tilt trigger` and/or Sablier, then holds the request until
|
|
4
|
+
# healthy. Needs the Docker CLI (client only, talks to the mounted
|
|
5
|
+
# /var/run/docker.sock) and the real `tilt` CLI (D3: shells out to the
|
|
6
|
+
# documented `tilt trigger` command rather than depending on Tilt's
|
|
7
|
+
# undocumented internal HTTP API).
|
|
8
|
+
|
|
9
|
+
FROM oven/bun:1.3.11-alpine AS base
|
|
10
|
+
|
|
11
|
+
# Pin to the same Tilt version this workspace runs (v0.37.7) so `tilt trigger`
|
|
12
|
+
# talks the same protocol as the real daemon it's connecting to. Installed
|
|
13
|
+
# directly (not via tilt-dev's install.sh, which shells out to `sudo` -- not
|
|
14
|
+
# present in this minimal image, and unnecessary since we're already root).
|
|
15
|
+
ARG TILT_VERSION=0.37.7
|
|
16
|
+
RUN apk add --no-cache docker-cli curl gcompat \
|
|
17
|
+
&& arch="$(uname -m)" \
|
|
18
|
+
&& case "$arch" in aarch64) tilt_arch=arm64 ;; x86_64) tilt_arch=x86_64 ;; *) echo "unsupported arch: $arch" >&2; exit 1 ;; esac \
|
|
19
|
+
&& curl -fsSL "https://github.com/tilt-dev/tilt/releases/download/v${TILT_VERSION}/tilt.${TILT_VERSION}.linux.${tilt_arch}.tar.gz" \
|
|
20
|
+
| tar -xz -C /usr/local/bin tilt
|
|
21
|
+
|
|
22
|
+
WORKDIR /app
|
|
23
|
+
COPY decide.ts server.ts ./
|
|
24
|
+
|
|
25
|
+
EXPOSE 9999
|
|
26
|
+
CMD ["bun", "run", "server.ts"]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { decideWakeAction, evaluateReadiness } from "./decide";
|
|
3
|
+
|
|
4
|
+
describe("decideWakeAction", () => {
|
|
5
|
+
test("never-created resource: tilt trigger only, no Sablier start", () => {
|
|
6
|
+
const action = decideWakeAction({
|
|
7
|
+
resourceContainerName: "data-governance-api",
|
|
8
|
+
resourceState: "absent",
|
|
9
|
+
dependencies: [],
|
|
10
|
+
});
|
|
11
|
+
expect(action.tiltTriggerNames).toEqual(["data-governance-api"]);
|
|
12
|
+
expect(action.useSablierStart).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("previously-run, now-exited resource: both tilt trigger and Sablier start", () => {
|
|
16
|
+
const action = decideWakeAction({
|
|
17
|
+
resourceContainerName: "data-governance-api",
|
|
18
|
+
resourceState: "exited",
|
|
19
|
+
dependencies: [],
|
|
20
|
+
});
|
|
21
|
+
expect(action.tiltTriggerNames).toEqual(["data-governance-api"]);
|
|
22
|
+
expect(action.useSablierStart).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("created-but-not-started resource: both paths apply", () => {
|
|
26
|
+
const action = decideWakeAction({
|
|
27
|
+
resourceContainerName: "data-governance-api",
|
|
28
|
+
resourceState: "created",
|
|
29
|
+
dependencies: [],
|
|
30
|
+
});
|
|
31
|
+
expect(action.useSablierStart).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("already running: no trigger needed", () => {
|
|
35
|
+
const action = decideWakeAction({
|
|
36
|
+
resourceContainerName: "data-governance-api",
|
|
37
|
+
resourceState: "running",
|
|
38
|
+
dependencies: [],
|
|
39
|
+
});
|
|
40
|
+
expect(action.tiltTriggerNames).toEqual([]);
|
|
41
|
+
expect(action.useSablierStart).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("cold dependency is triggered alongside the resource (task 1.5 finding)", () => {
|
|
45
|
+
const action = decideWakeAction({
|
|
46
|
+
resourceContainerName: "data-governance-api",
|
|
47
|
+
resourceState: "absent",
|
|
48
|
+
dependencies: [
|
|
49
|
+
{ name: "postgres", state: "running" },
|
|
50
|
+
{ name: "data-governance-api-db-migrator", state: "absent" },
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
expect(action.tiltTriggerNames).toEqual([
|
|
54
|
+
"data-governance-api",
|
|
55
|
+
"data-governance-api-db-migrator",
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("running dependency is not re-triggered", () => {
|
|
60
|
+
const action = decideWakeAction({
|
|
61
|
+
resourceContainerName: "data-governance-api",
|
|
62
|
+
resourceState: "running",
|
|
63
|
+
dependencies: [{ name: "postgres", state: "running" }],
|
|
64
|
+
});
|
|
65
|
+
expect(action.tiltTriggerNames).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("evaluateReadiness", () => {
|
|
70
|
+
test("ready once every required name reports healthy", () => {
|
|
71
|
+
const outcome = evaluateReadiness(["a", "b"], { a: true, b: true });
|
|
72
|
+
expect(outcome.ready).toBe(true);
|
|
73
|
+
expect(outcome.pending).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("not ready while some names are still unhealthy", () => {
|
|
77
|
+
const outcome = evaluateReadiness(["a", "b"], { a: true, b: false });
|
|
78
|
+
expect(outcome.ready).toBe(false);
|
|
79
|
+
expect(outcome.pending).toEqual(["b"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("missing entries count as not healthy", () => {
|
|
83
|
+
const outcome = evaluateReadiness(["a", "b"], { a: true });
|
|
84
|
+
expect(outcome.ready).toBe(false);
|
|
85
|
+
expect(outcome.pending).toEqual(["b"]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision logic for the wake gateway (see openspec/changes/prioritized-cold-start).
|
|
3
|
+
*
|
|
4
|
+
* The gateway sits behind a Traefik static (file-provider) route for every
|
|
5
|
+
* `sablier: {enable: true, deferStart: true}` resource. This module decides
|
|
6
|
+
* what to *do* given a snapshot of container state; the actual Docker/Tilt
|
|
7
|
+
* I/O and the healthcheck-polling loop live in server.ts, which is a thin
|
|
8
|
+
* wrapper around this so the decision itself stays testable without a live
|
|
9
|
+
* Docker daemon or Tilt instance.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type ContainerState = "absent" | "created" | "running" | "exited";
|
|
13
|
+
|
|
14
|
+
export interface DependencySnapshot {
|
|
15
|
+
name: string;
|
|
16
|
+
state: ContainerState;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface WakeRequestSnapshot {
|
|
20
|
+
resourceContainerName: string;
|
|
21
|
+
resourceState: ContainerState;
|
|
22
|
+
dependencies: DependencySnapshot[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface WakeAction {
|
|
26
|
+
/** Resources to `tilt trigger` (task 1.5: Tilt never cascades this on its own). */
|
|
27
|
+
tiltTriggerNames: string[];
|
|
28
|
+
/**
|
|
29
|
+
* Whether to also try Sablier's own Docker-level start for the resource.
|
|
30
|
+
* Only meaningful when the resource's container already exists (D2/D3):
|
|
31
|
+
* Sablier has nothing to manage for a container that was never created.
|
|
32
|
+
*/
|
|
33
|
+
useSablierStart: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Decide which resources need `tilt trigger` and whether Sablier's Docker-level
|
|
38
|
+
* start also applies, given one point-in-time snapshot (D2/D3/D5).
|
|
39
|
+
*/
|
|
40
|
+
export function decideWakeAction(snapshot: WakeRequestSnapshot): WakeAction {
|
|
41
|
+
const tiltTriggerNames: string[] = [];
|
|
42
|
+
|
|
43
|
+
if (snapshot.resourceState !== "running") {
|
|
44
|
+
tiltTriggerNames.push(snapshot.resourceContainerName);
|
|
45
|
+
}
|
|
46
|
+
for (const dep of snapshot.dependencies) {
|
|
47
|
+
if (dep.state !== "running") {
|
|
48
|
+
tiltTriggerNames.push(dep.name);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const useSablierStart = snapshot.resourceState === "exited" || snapshot.resourceState === "created";
|
|
53
|
+
|
|
54
|
+
return { tiltTriggerNames, useSablierStart };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface WaitOutcome {
|
|
58
|
+
ready: boolean;
|
|
59
|
+
/** Names still not running/healthy when the wait ended (empty when ready). */
|
|
60
|
+
pending: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Decide whether a set of health polls (resource + dependencies) means the
|
|
65
|
+
* request can be let through yet, or should keep waiting / time out.
|
|
66
|
+
* `healthy` maps each name from a WakeAction's tiltTriggerNames (plus the
|
|
67
|
+
* resource itself) to its latest observed health state.
|
|
68
|
+
*/
|
|
69
|
+
export function evaluateReadiness(
|
|
70
|
+
requiredNames: string[],
|
|
71
|
+
healthy: Record<string, boolean>,
|
|
72
|
+
): WaitOutcome {
|
|
73
|
+
const pending = requiredNames.filter((name) => !healthy[name]);
|
|
74
|
+
return { ready: pending.length === 0, pending };
|
|
75
|
+
}
|