@tdk-landscape/tdk-cli-core 1.3.104 → 1.3.106
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/CHANGELOG.md +1 -0
- package/cli/dist/cli.js +4 -0
- package/cli/dist/commands/doctor.js +28 -3
- package/cli/dist/commands/down.js +13 -4
- package/cli/dist/commands/logs.js +85 -0
- package/cli/dist/commands/mcp.js +24 -0
- package/cli/dist/commands/projects.js +37 -6
- package/cli/dist/commands/resources.js +3 -2
- package/cli/dist/commands/stacks.js +30 -3
- package/cli/dist/commands/status.js +31 -0
- package/cli/dist/commands/ui.js +1 -1
- package/cli/dist/commands/up.js +149 -17
- package/cli/dist/mcp/server.js +100 -0
- package/cli/dist/mcp/tools.js +273 -0
- package/cli/dist/utils/agent-host.js +35 -0
- package/cli/dist/utils/discovery-context.js +9 -3
- package/cli/dist/utils/doctor-report.js +14 -1
- package/cli/dist/utils/doctor-wiring.js +88 -0
- package/cli/dist/utils/json-output.js +27 -0
- package/cli/dist/utils/services.js +65 -7
- package/cli/dist/utils/status-ports.js +32 -0
- package/cli/dist/utils/tilt-logs.js +62 -0
- package/cli/dist/utils/up-only.js +29 -0
- package/cli/dist/utils/up-readiness.js +115 -0
- package/cli/package.json +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createCacheValidator } from "./cache.js";
|
|
2
|
-
import {
|
|
2
|
+
import { findProjectRoot } from "./paths.js";
|
|
3
|
+
import { discoverResources, discoverResourcesWithProblems, discoverStacks, getAllStacks, warnAboutSkippedFiles, } from "./services.js";
|
|
3
4
|
let cachedContext = null;
|
|
4
5
|
const cacheValidator = createCacheValidator(1000);
|
|
5
6
|
export function clearDiscoveryCache() {
|
|
@@ -13,9 +14,13 @@ export function createDiscoveryContext(forceRefresh = false) {
|
|
|
13
14
|
if (!forceRefresh && isCacheValid() && cachedContext) {
|
|
14
15
|
return cachedContext;
|
|
15
16
|
}
|
|
16
|
-
const
|
|
17
|
-
const
|
|
17
|
+
const projectRoot = findProjectRoot();
|
|
18
|
+
const { resources, problems } = projectRoot
|
|
19
|
+
? discoverResourcesWithProblems(projectRoot)
|
|
20
|
+
: { resources: discoverResources(), problems: [] };
|
|
21
|
+
const stacks = discoverStacks(resources);
|
|
18
22
|
const stackNames = getAllStacks(resources);
|
|
23
|
+
warnAboutSkippedFiles(problems);
|
|
19
24
|
const unassignedResources = resources.filter((r) => !r.stack);
|
|
20
25
|
const resourcesByStack = new Map();
|
|
21
26
|
for (const resource of resources) {
|
|
@@ -27,6 +32,7 @@ export function createDiscoveryContext(forceRefresh = false) {
|
|
|
27
32
|
}
|
|
28
33
|
const context = {
|
|
29
34
|
resources,
|
|
35
|
+
problems,
|
|
30
36
|
stacks,
|
|
31
37
|
stackNames,
|
|
32
38
|
unassignedResources,
|
|
@@ -20,8 +20,12 @@ export async function collectDoctorChecks(machineChecks, projectChecks) {
|
|
|
20
20
|
}
|
|
21
21
|
return { checks, errors };
|
|
22
22
|
}
|
|
23
|
+
function containerRuntimeReachable(checks) {
|
|
24
|
+
const runtime = checks.find((check) => check.name === "Container Runtime");
|
|
25
|
+
return runtime ? runtime.didPass : null;
|
|
26
|
+
}
|
|
23
27
|
/** Produce the same readiness decision for human and machine consumers. */
|
|
24
|
-
export function createDoctorReport(checks, inProject, errors = [], portPlan) {
|
|
28
|
+
export function createDoctorReport(checks, inProject, errors = [], portPlan, host) {
|
|
25
29
|
return {
|
|
26
30
|
schemaVersion: 1,
|
|
27
31
|
data: {
|
|
@@ -29,6 +33,15 @@ export function createDoctorReport(checks, inProject, errors = [], portPlan) {
|
|
|
29
33
|
checks.every((check) => check.didPass || check.isSkipped || check.isWarning),
|
|
30
34
|
inProject,
|
|
31
35
|
checks,
|
|
36
|
+
...(host
|
|
37
|
+
? {
|
|
38
|
+
host: {
|
|
39
|
+
...host,
|
|
40
|
+
containerRuntimeReachable: containerRuntimeReachable(checks),
|
|
41
|
+
canUp: host.canUp && containerRuntimeReachable(checks) !== false,
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
: {}),
|
|
32
45
|
...(portPlan !== undefined
|
|
33
46
|
? {
|
|
34
47
|
ports: {
|
|
@@ -165,6 +165,94 @@ export function checkFrontendBackendUrls(projectRoot = findProjectRoot() ?? proc
|
|
|
165
165
|
fix: `Backends are only reachable through Traefik: http://api.${project}.localhost/api/<name without -api>/... (for example catalog-api is /api/catalog). Use that URL, or the generated .autogenerated/env.ts`,
|
|
166
166
|
};
|
|
167
167
|
}
|
|
168
|
+
/** The common ways to apply schema changes from a command line: Prisma, Drizzle, Knex, TypeORM, MikroORM, Sequelize, dbmate, Atlas, Alembic. */
|
|
169
|
+
const MIGRATION_COMMAND = /\b(?:prisma\s+(?:migrate\s+(?:deploy|dev|resolve)|db\s+push)|drizzle-kit\s+(?:migrate|push)|knex\s+migrate:\w+|typeorm\s+migration:run|mikro-orm\s+migration:up|sequelize(?:-cli)?\s+db:migrate|dbmate\s+(?:up|migrate)|atlas\s+migrate\s+apply|alembic\s+upgrade)\b/;
|
|
170
|
+
const START_SCRIPTS = new Set(["start", "dev", "serve", "prestart", "predev", "preserve"]);
|
|
171
|
+
const SHELL_ENTRYPOINT = /\.(?:sh|bash)$/;
|
|
172
|
+
const DOCKERFILE_START = /^\s*(?:CMD|ENTRYPOINT)\b/i;
|
|
173
|
+
function readText(path) {
|
|
174
|
+
try {
|
|
175
|
+
return readFileSync(path, "utf-8");
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function startScriptMigrations(resourceDir) {
|
|
182
|
+
const text = readText(join(resourceDir, "package.json"));
|
|
183
|
+
if (text === undefined)
|
|
184
|
+
return [];
|
|
185
|
+
let scripts = {};
|
|
186
|
+
try {
|
|
187
|
+
scripts = JSON.parse(text)?.scripts ?? {};
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
return Object.entries(scripts)
|
|
193
|
+
.filter(([name, command]) => START_SCRIPTS.has(name) && typeof command === "string" && MIGRATION_COMMAND.test(command))
|
|
194
|
+
.map(([name]) => `package.json "${name}" script`);
|
|
195
|
+
}
|
|
196
|
+
function fileMigrations(path, onlyStartLines) {
|
|
197
|
+
const text = readText(path);
|
|
198
|
+
if (text === undefined)
|
|
199
|
+
return false;
|
|
200
|
+
return text.split("\n").some((line) =>
|
|
201
|
+
// `["prisma", "migrate", "deploy"]` is the same command as `prisma migrate deploy`.
|
|
202
|
+
MIGRATION_COMMAND.test(line.replace(/["'`,[\]]+/g, " ")) &&
|
|
203
|
+
!COMMENT_LINE.test(line) &&
|
|
204
|
+
(!onlyStartLines || DOCKERFILE_START.test(line)));
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* A backend or worker that applies migrations from its own start-up (a `start` script, a Dockerfile CMD,
|
|
208
|
+
* an entrypoint script or its source) races its siblings and runs again on every restart. TDK has a
|
|
209
|
+
* `migrator` app type for this: it runs once, before the services that depend on it. This is a warning,
|
|
210
|
+
* not a failure: such services work, and the opt-in Infisical entrypoint is one of them.
|
|
211
|
+
*/
|
|
212
|
+
export function checkMigrationsInApi(projectRoot = findProjectRoot() ?? process.cwd()) {
|
|
213
|
+
const findings = [];
|
|
214
|
+
for (const resource of discoverResourcesFromRoot(projectRoot)) {
|
|
215
|
+
const appType = resource.config?.appType ?? resource.type;
|
|
216
|
+
if (appType !== "backend" && appType !== "worker")
|
|
217
|
+
continue;
|
|
218
|
+
const dir = resource.path;
|
|
219
|
+
for (const finding of startScriptMigrations(dir))
|
|
220
|
+
findings.push(`${resource.name}: ${finding}`);
|
|
221
|
+
const dockerfile = join(dir, resource.config?.dockerfile ?? "Dockerfile");
|
|
222
|
+
if (fileMigrations(dockerfile, true)) {
|
|
223
|
+
findings.push(`${resource.name}: ${relative(projectRoot, dockerfile)} CMD or ENTRYPOINT`);
|
|
224
|
+
}
|
|
225
|
+
let entries = [];
|
|
226
|
+
try {
|
|
227
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
228
|
+
}
|
|
229
|
+
catch { }
|
|
230
|
+
for (const entry of entries) {
|
|
231
|
+
const path = join(dir, entry.name);
|
|
232
|
+
if (entry.isFile() && SHELL_ENTRYPOINT.test(entry.name) && fileMigrations(path, false)) {
|
|
233
|
+
findings.push(`${resource.name}: ${relative(projectRoot, path)}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
for (const path of listSourceFiles(join(dir, "src"))) {
|
|
237
|
+
if (fileMigrations(path, false))
|
|
238
|
+
findings.push(`${resource.name}: ${relative(projectRoot, path)}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (findings.length === 0) {
|
|
242
|
+
return {
|
|
243
|
+
name: "Migrations outside the API",
|
|
244
|
+
didPass: true,
|
|
245
|
+
message: "No backend or worker runs a migration command from its own start-up",
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
name: "Migrations outside the API",
|
|
250
|
+
didPass: false,
|
|
251
|
+
isWarning: true,
|
|
252
|
+
message: `${formatCount(findings.length, "migration command")} run from a backend or worker's own start-up:\n ${findings.join("\n ")}`,
|
|
253
|
+
fix: "Move the migration into a `migrator` resource (appType migrator) that the service depends on, so it runs once before the service starts instead of on every start and restart",
|
|
254
|
+
};
|
|
255
|
+
}
|
|
168
256
|
export function checkNatsBroker(projectRoot = findProjectRoot() ?? process.cwd()) {
|
|
169
257
|
const users = discoverResourcesFromRoot(projectRoot)
|
|
170
258
|
.filter((resource) => resource.config?.featuresEnabled?.includes("nats"))
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { writeSync } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Returns an emitter that writes at most one `{schemaVersion, data, errors}` object to stdout. If the process
|
|
4
|
+
* exits before anything was emitted (a preflight `process.exit`, Tilt ending early), a failure object is written
|
|
5
|
+
* so a JSON consumer never sees an empty stdout. Uses writeSync because `process.exit` does not flush async writes.
|
|
6
|
+
*/
|
|
7
|
+
export function createJsonEmitter(failureCode, command) {
|
|
8
|
+
let emitted = false;
|
|
9
|
+
const emit = (data, errors = []) => {
|
|
10
|
+
if (emitted)
|
|
11
|
+
return;
|
|
12
|
+
emitted = true;
|
|
13
|
+
writeSync(1, `${JSON.stringify({ schemaVersion: 1, data, errors })}\n`);
|
|
14
|
+
};
|
|
15
|
+
process.once("exit", (code) => {
|
|
16
|
+
emit({ ok: false }, [
|
|
17
|
+
{
|
|
18
|
+
code: failureCode,
|
|
19
|
+
message: code === 0
|
|
20
|
+
? `${command} exited before reporting a result`
|
|
21
|
+
: `${command} exited with code ${code}`,
|
|
22
|
+
},
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
return emit;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=json-output.js.map
|
|
@@ -48,11 +48,34 @@ const printedWarnings = new Set();
|
|
|
48
48
|
export function resetPrintedServiceWarnings() {
|
|
49
49
|
printedWarnings.clear();
|
|
50
50
|
}
|
|
51
|
+
class InvalidServiceJsonError extends Error {
|
|
52
|
+
reason;
|
|
53
|
+
constructor(reason) {
|
|
54
|
+
super(reason);
|
|
55
|
+
this.reason = reason;
|
|
56
|
+
this.name = "InvalidServiceJsonError";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function errorMessage(error) {
|
|
60
|
+
return error instanceof Error ? error.message : String(error);
|
|
61
|
+
}
|
|
51
62
|
function parseResource(serviceJsonPath) {
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
let content;
|
|
64
|
+
try {
|
|
65
|
+
content = readFileSync(serviceJsonPath, "utf-8");
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new InvalidServiceJsonError(`could not read file: ${errorMessage(error)}`);
|
|
69
|
+
}
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(content);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
throw new InvalidServiceJsonError(`invalid JSON: ${errorMessage(error)}`);
|
|
76
|
+
}
|
|
54
77
|
if (!isValidResourceConfig(parsed)) {
|
|
55
|
-
throw new
|
|
78
|
+
throw new InvalidServiceJsonError("missing required fields (appName, runtime or appType)");
|
|
56
79
|
}
|
|
57
80
|
const parsedConfig = parsed;
|
|
58
81
|
for (const warning of validateServiceManifest(parsed, serviceJsonPath).warnings) {
|
|
@@ -76,12 +99,35 @@ function parseResource(serviceJsonPath) {
|
|
|
76
99
|
stack: config.stack,
|
|
77
100
|
};
|
|
78
101
|
}
|
|
79
|
-
export function
|
|
102
|
+
export function discoverResourcesWithProblems(projectRoot) {
|
|
80
103
|
const serviceJsonPaths = findServiceJsonFiles(projectRoot);
|
|
81
104
|
const resources = [];
|
|
105
|
+
const problems = [];
|
|
82
106
|
for (const path of serviceJsonPaths) {
|
|
83
|
-
|
|
107
|
+
try {
|
|
108
|
+
resources.push(parseResource(path));
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
problems.push({
|
|
112
|
+
path,
|
|
113
|
+
reason: error instanceof InvalidServiceJsonError ? error.reason : errorMessage(error),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { resources, problems };
|
|
118
|
+
}
|
|
119
|
+
export function warnAboutSkippedFiles(problems) {
|
|
120
|
+
for (const problem of problems) {
|
|
121
|
+
const warning = `skipped ${problem.path}: ${problem.reason}`;
|
|
122
|
+
if (printedWarnings.has(warning))
|
|
123
|
+
continue;
|
|
124
|
+
printedWarnings.add(warning);
|
|
125
|
+
console.warn(`⚠️ ${warning}`);
|
|
84
126
|
}
|
|
127
|
+
}
|
|
128
|
+
export function discoverResourcesFromRoot(projectRoot) {
|
|
129
|
+
const { resources, problems } = discoverResourcesWithProblems(projectRoot);
|
|
130
|
+
warnAboutSkippedFiles(problems);
|
|
85
131
|
return resources;
|
|
86
132
|
}
|
|
87
133
|
export function discoverResources() {
|
|
@@ -91,6 +137,19 @@ export function discoverResources() {
|
|
|
91
137
|
}
|
|
92
138
|
return discoverResourcesFromRoot(projectRoot);
|
|
93
139
|
}
|
|
140
|
+
export function discoverResourcesStrict() {
|
|
141
|
+
const projectRoot = findProjectRoot();
|
|
142
|
+
if (!projectRoot) {
|
|
143
|
+
throw new Error("Could not find project root (no .tdk/project.json found). Run `tdk project --yes` first.");
|
|
144
|
+
}
|
|
145
|
+
const { resources, problems } = discoverResourcesWithProblems(projectRoot);
|
|
146
|
+
if (problems.length > 0) {
|
|
147
|
+
throw new Error(problems
|
|
148
|
+
.map((problem) => `Invalid service.json at ${problem.path}: ${problem.reason}`)
|
|
149
|
+
.join("\n"));
|
|
150
|
+
}
|
|
151
|
+
return resources;
|
|
152
|
+
}
|
|
94
153
|
/**
|
|
95
154
|
* Unique, sorted `stack` values across discovered service.json files under `projectRoot`.
|
|
96
155
|
* Tilt's discovery groups services by this same field (see tdk-cli-extensions
|
|
@@ -117,8 +176,7 @@ export function getAllStacks(resources) {
|
|
|
117
176
|
}
|
|
118
177
|
return Array.from(stacks).sort();
|
|
119
178
|
}
|
|
120
|
-
export function discoverStacks() {
|
|
121
|
-
const resources = discoverResources();
|
|
179
|
+
export function discoverStacks(resources = discoverResources()) {
|
|
122
180
|
const stackMap = new Map();
|
|
123
181
|
for (const resource of resources) {
|
|
124
182
|
if (resource.stack) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { STANDARD_PORTS } from "./constants.js";
|
|
2
|
+
import { isApiServiceType } from "./resource-kind.js";
|
|
3
|
+
import { resolveServicePath, resolveSubdomainBases } from "./service-urls.js";
|
|
4
|
+
export function buildServicePorts(resource, ingressHttp) {
|
|
5
|
+
const appType = resource.config?.appType;
|
|
6
|
+
const routable = appType === "frontend" || isApiServiceType(appType);
|
|
7
|
+
let url = null;
|
|
8
|
+
if (routable) {
|
|
9
|
+
const { appBase, apiBase } = resolveSubdomainBases(ingressHttp);
|
|
10
|
+
url = `${isApiServiceType(appType) ? apiBase : appBase}${resolveServicePath(resource)}`;
|
|
11
|
+
}
|
|
12
|
+
return { url, containerPort: resource.port ?? resource.config?.port ?? null, hostPort: null };
|
|
13
|
+
}
|
|
14
|
+
/** Ports a client should forward or open: ingress, Tilt UI, and the published datastore. Not one per service. */
|
|
15
|
+
export function buildStackPorts(plan, tiltPort) {
|
|
16
|
+
const ports = [];
|
|
17
|
+
if (plan) {
|
|
18
|
+
ports.push({ name: "ingress-http", hostPort: plan.ingressHttp, purpose: "Traefik HTTP ingress" }, { name: "ingress-https", hostPort: plan.ingressHttps, purpose: "Traefik HTTPS ingress" });
|
|
19
|
+
}
|
|
20
|
+
// `tdk up` may move Tilt off 10350 and records that only in its own process, so this is the default or TILT_PORT.
|
|
21
|
+
const validTiltPort = tiltPort !== undefined && Number.isInteger(tiltPort) && tiltPort > 0;
|
|
22
|
+
ports.push({
|
|
23
|
+
name: "tilt-ui",
|
|
24
|
+
hostPort: validTiltPort ? tiltPort : STANDARD_PORTS.tiltUi,
|
|
25
|
+
purpose: "Tilt UI",
|
|
26
|
+
});
|
|
27
|
+
if (plan) {
|
|
28
|
+
ports.push({ name: "postgres", hostPort: plan.postgres, purpose: "Published Postgres" });
|
|
29
|
+
}
|
|
30
|
+
return ports;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=status-ports.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const DEFAULT_LOG_TAIL = 200;
|
|
2
|
+
/** Upper bound for --tail so a snapshot stays small enough for an agent to read. */
|
|
3
|
+
export const MAX_LOG_TAIL = 10_000;
|
|
4
|
+
const NANOSECONDS = {
|
|
5
|
+
ns: 1,
|
|
6
|
+
us: 1e3,
|
|
7
|
+
µs: 1e3,
|
|
8
|
+
ms: 1e6,
|
|
9
|
+
s: 1e9,
|
|
10
|
+
m: 6e10,
|
|
11
|
+
h: 3.6e12,
|
|
12
|
+
};
|
|
13
|
+
// Go's time.Duration is an int64 of nanoseconds, so anything beyond this is rejected by `tilt logs`.
|
|
14
|
+
const MAX_DURATION_NANOSECONDS = 2 ** 63 - 1;
|
|
15
|
+
/**
|
|
16
|
+
* Go-style duration such as 30s, 5m, 1h or 1h30m, within roughly the range Go's ParseDuration accepts.
|
|
17
|
+
* The sum is a JS number, so values within a few nanoseconds of the int64 limit can disagree with Go; this only needs to
|
|
18
|
+
* reject absurd input, and Tilt remains the final judge (its rejection is reported as TILT_LOGS_FAILED).
|
|
19
|
+
*/
|
|
20
|
+
export function isValidSince(value) {
|
|
21
|
+
if (!/^(\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$/.test(value))
|
|
22
|
+
return false;
|
|
23
|
+
let total = 0;
|
|
24
|
+
for (const [, amount, unit] of value.matchAll(/(\d+(?:\.\d+)?)(ns|us|µs|ms|s|m|h)/g)) {
|
|
25
|
+
total += Number(amount) * (NANOSECONDS[unit] ?? 0);
|
|
26
|
+
}
|
|
27
|
+
return total <= MAX_DURATION_NANOSECONDS;
|
|
28
|
+
}
|
|
29
|
+
export function isValidPort(value) {
|
|
30
|
+
return /^[1-9]\d{0,4}$/.test(value) && Number(value) <= 65535;
|
|
31
|
+
}
|
|
32
|
+
export function isValidTail(value) {
|
|
33
|
+
return /^[1-9]\d*$/.test(value) && Number(value) <= MAX_LOG_TAIL;
|
|
34
|
+
}
|
|
35
|
+
/** Parse `tilt logs --json` (JSON Lines) into normalized lines, keeping at most the last `limit`. Unparseable lines are skipped. */
|
|
36
|
+
export function parseTiltLogLines(output, limit) {
|
|
37
|
+
const lines = [];
|
|
38
|
+
for (const raw of output.split("\n")) {
|
|
39
|
+
if (!raw.trim())
|
|
40
|
+
continue;
|
|
41
|
+
try {
|
|
42
|
+
const entry = JSON.parse(raw);
|
|
43
|
+
const str = (value) => typeof value === "string" && value !== "" ? value : null;
|
|
44
|
+
lines.push({
|
|
45
|
+
time: str(entry.time),
|
|
46
|
+
resource: str(entry.resource),
|
|
47
|
+
level: str(entry.level),
|
|
48
|
+
source: str(entry.source),
|
|
49
|
+
text: typeof entry.message === "string" ? entry.message : "",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Tilt can print a non-JSON notice before the stream; it is not a log line.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return lines.slice(-limit);
|
|
57
|
+
}
|
|
58
|
+
/** True when `tilt logs` failed because no Tilt server answered, as opposed to rejecting its arguments. */
|
|
59
|
+
export function isTiltConnectionFailure(stderr) {
|
|
60
|
+
return /connection refused|connecting to Tilt|no such host|dial tcp|websocket_token/i.test(stderr);
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=tilt-logs.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Requested names that match no discovered service (the Tiltfile silently enables everything for an unknown focus name). */
|
|
2
|
+
export function findUnknownServices(requested, candidates) {
|
|
3
|
+
const known = new Set(candidates.map((resource) => resource.name));
|
|
4
|
+
return requested.filter((name) => !known.has(name));
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The requested services plus their `dependsOn` closure. This mirrors what the generated Tiltfile enables for
|
|
8
|
+
* `--focus=<service>` (see get_all_needed_services in engine/topologies/tilt/config/profiles.star) so the CLI can report
|
|
9
|
+
* the same set. Shared infrastructure (Postgres, Traefik, the golden images) is always enabled by the Tiltfile and is not listed here.
|
|
10
|
+
* A dependsOn entry that is not a discovered service is ignored.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveOnlySelection(requested, all) {
|
|
13
|
+
const byName = new Map(all.map((resource) => [resource.name, resource]));
|
|
14
|
+
const wanted = new Set();
|
|
15
|
+
const visit = (name) => {
|
|
16
|
+
if (wanted.has(name) || !byName.has(name))
|
|
17
|
+
return;
|
|
18
|
+
wanted.add(name);
|
|
19
|
+
for (const dep of byName.get(name)?.config?.dependsOn ?? [])
|
|
20
|
+
visit(dep);
|
|
21
|
+
};
|
|
22
|
+
for (const name of requested)
|
|
23
|
+
visit(name);
|
|
24
|
+
return {
|
|
25
|
+
selected: all.filter((resource) => wanted.has(resource.name)),
|
|
26
|
+
dependencies: all.map((r) => r.name).filter((n) => wanted.has(n) && !requested.includes(n)),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=up-only.js.map
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { getDeferredResourceNames, parseTiltResourceFailures } from "./doctor-runtime.js";
|
|
3
|
+
import { findOnPath } from "./which.js";
|
|
4
|
+
export function tiltGetUiResources(port) {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
execFile(findOnPath("tilt") ?? "tilt", ["get", "uiresources", "-o", "json", "--port", String(port)], { timeout: 15_000, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => resolve(error ? null : stdout));
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Tilt lists resources outside the current focus (the other release phases, or everything but a `--only` selection) with
|
|
11
|
+
* `disableStatus.state: Disabled` and both statuses `none`, which would read as pending forever. A resource whose update
|
|
12
|
+
* status is `not_applicable` (a serve-only local resource) has nothing to build, so it counts as built.
|
|
13
|
+
*/
|
|
14
|
+
export function onlyEnabledResources(jsonText) {
|
|
15
|
+
const parsed = JSON.parse(jsonText);
|
|
16
|
+
const items = (parsed.items ?? [])
|
|
17
|
+
.filter((item) => item.status?.disableStatus?.state !== "Disabled")
|
|
18
|
+
.map((item) => item.status?.updateStatus === "not_applicable"
|
|
19
|
+
? { ...item, status: { ...item.status, updateStatus: "ok" } }
|
|
20
|
+
: item);
|
|
21
|
+
return JSON.stringify({ items });
|
|
22
|
+
}
|
|
23
|
+
function enabledNames(jsonText) {
|
|
24
|
+
return (JSON.parse(jsonText).items ?? [])
|
|
25
|
+
.filter((item) => item.status?.disableStatus?.state !== "Disabled")
|
|
26
|
+
.flatMap((item) => (item.metadata?.name ? [item.metadata.name] : []));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Services the caller asked for must not vanish into the disabled filter: one that Tilt lists as disabled (it can be
|
|
30
|
+
* disabled while running) is a failure, and one Tilt does not list yet is still pending.
|
|
31
|
+
*/
|
|
32
|
+
export function checkExpectedResources(jsonText, expected) {
|
|
33
|
+
const items = JSON.parse(jsonText).items ?? [];
|
|
34
|
+
const byName = new Map(items.map((item) => [item.metadata?.name, item]));
|
|
35
|
+
return {
|
|
36
|
+
missing: expected.filter((name) => !byName.has(name)),
|
|
37
|
+
disabled: expected.filter((name) => byName.get(name)?.status?.disableStatus?.state === "Disabled"),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One look at `tilt get uiresources -o json`. `settled` means polling can stop: everything is ready, or the rest has
|
|
42
|
+
* finished with a failure (an errored resource, or an expected service Tilt has disabled).
|
|
43
|
+
*/
|
|
44
|
+
export function evaluateTiltReadiness(jsonText, deferred, expected) {
|
|
45
|
+
const parsed = parseTiltResourceFailures(onlyEnabledResources(jsonText), deferred);
|
|
46
|
+
let result = {
|
|
47
|
+
ready: parsed.failures.length === 0 && parsed.pendingCount === 0 && parsed.okCount > 0,
|
|
48
|
+
failures: parsed.failures.map((failure) => ({
|
|
49
|
+
name: failure.name,
|
|
50
|
+
message: failure.error || `${failure.updateStatus}/${failure.runtimeStatus}`,
|
|
51
|
+
})),
|
|
52
|
+
pending: parsed.pendingCount,
|
|
53
|
+
timedOut: false,
|
|
54
|
+
enabled: enabledNames(jsonText),
|
|
55
|
+
};
|
|
56
|
+
if (expected && expected.length > 0) {
|
|
57
|
+
const { missing, disabled } = checkExpectedResources(jsonText, expected);
|
|
58
|
+
if (missing.length > 0) {
|
|
59
|
+
result = { ...result, ready: false, pending: result.pending + missing.length };
|
|
60
|
+
}
|
|
61
|
+
if (disabled.length > 0) {
|
|
62
|
+
return {
|
|
63
|
+
settled: true,
|
|
64
|
+
result: {
|
|
65
|
+
...result,
|
|
66
|
+
ready: false,
|
|
67
|
+
failures: [
|
|
68
|
+
...result.failures,
|
|
69
|
+
...disabled.map((name) => ({ name, message: "disabled in Tilt" })),
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
result,
|
|
77
|
+
settled: result.ready || (result.failures.length > 0 && result.pending === 0),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Waits until every non-deferred Tilt resource is built and running. Resolves not-ready as soon as the rest has settled
|
|
82
|
+
* with an errored resource, or at the deadline. Sablier-deferred resources are excluded: they stay idle by design.
|
|
83
|
+
*/
|
|
84
|
+
export async function waitForTiltResourcesReady(port, options = {}) {
|
|
85
|
+
const timeoutMs = options.timeoutMs ?? Number(process.env.TDK_UP_READY_TIMEOUT_MS ?? 900_000);
|
|
86
|
+
const intervalMs = options.intervalMs ?? 2_000;
|
|
87
|
+
const fetchJson = options.fetchJson ?? tiltGetUiResources;
|
|
88
|
+
const deferred = options.deferred ?? getDeferredResourceNames();
|
|
89
|
+
const deadline = Date.now() + timeoutMs;
|
|
90
|
+
let last = {
|
|
91
|
+
ready: false,
|
|
92
|
+
failures: [],
|
|
93
|
+
pending: 0,
|
|
94
|
+
timedOut: false,
|
|
95
|
+
enabled: [],
|
|
96
|
+
};
|
|
97
|
+
for (;;) {
|
|
98
|
+
const text = await fetchJson(port);
|
|
99
|
+
if (text) {
|
|
100
|
+
try {
|
|
101
|
+
const evaluation = evaluateTiltReadiness(text, deferred, options.expected);
|
|
102
|
+
last = evaluation.result;
|
|
103
|
+
if (evaluation.settled)
|
|
104
|
+
return last;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* Tilt may emit partial output while starting; retry. */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (Date.now() >= deadline)
|
|
111
|
+
return { ...last, ready: false, timedOut: true };
|
|
112
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=up-readiness.js.map
|
package/cli/package.json
CHANGED