@tdk-landscape/tdk-cli-core 1.3.123 → 1.3.125
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 +284 -18
- package/README.md +20 -2
- package/cli/dist/commands/doctor.js +5 -133
- package/cli/dist/commands/down.js +8 -1
- package/cli/dist/commands/eject.js +2 -2
- package/cli/dist/commands/logs.js +16 -10
- package/cli/dist/commands/networks.js +10 -10
- package/cli/dist/commands/resource.js +7 -126
- package/cli/dist/commands/ui.js +35 -14
- package/cli/dist/commands/up.js +8 -0
- package/cli/dist/commands/upgrade.js +39 -52
- package/cli/dist/components/TabBar.js +2 -7
- package/cli/dist/generator/resource-templates.js +127 -0
- package/cli/dist/utils/bun-requirement.js +11 -0
- package/cli/dist/utils/cold-preflight.js +14 -9
- package/cli/dist/utils/doctor-starlark.js +131 -0
- package/cli/dist/utils/env-validator.js +97 -29
- package/cli/dist/utils/schema-version-gate.js +39 -0
- package/cli/dist/utils/service-manifest.js +11 -0
- package/cli/dist/utils/services.js +7 -1
- package/cli/dist/utils/tilt.js +2 -1
- package/cli/dist/utils/ui-keymap.js +118 -0
- package/cli/package.json +1 -1
- package/engine/topologies/tilt/common/utils.star +3 -3
- package/engine/topologies/tilt/common/utils_debug.star +1 -1
- package/engine/topologies/tilt/generators/typescript/frontend_tsconfig.star +4 -6
- package/engine/topologies/tilt/resources/infra-loader.star +12 -11
- package/engine/topologies/tilt/resources/orchestrator/apply.star +2 -1
- package/engine/topologies/tilt/resources/orchestrator/apply_compose_resource_registration.star +19 -18
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { checkDockerCompose, checkDockerRuntime, checkDockerVersions, checkTilt, } from "../commands/doctor.js";
|
|
5
5
|
import { BUN_FLOOR_LABEL, bunMeetsFloor } from "./bun-floor.js";
|
|
6
|
+
import { projectNeedsBun } from "./bun-requirement.js";
|
|
6
7
|
import { checkNatsBroker } from "./doctor-wiring.js";
|
|
7
8
|
import { getHostPortPlan } from "./host-port-config.js";
|
|
8
9
|
import { findProjectRoot } from "./paths.js";
|
|
@@ -79,8 +80,10 @@ export async function runColdPreflight(opts = {}) {
|
|
|
79
80
|
return requireVerifiedDockerVersions(versions);
|
|
80
81
|
}, "Docker Engine or Compose version check failed"));
|
|
81
82
|
items.push(await safeCheck("tilt", checkTilt, "Tilt CLI not found"));
|
|
83
|
+
// Outside a project Bun stays required: `tdk project` is about to create services that need it.
|
|
84
|
+
const bunNeeded = projectRoot ? projectNeedsBun(discoverResourcesFromRoot(projectRoot)) : true;
|
|
82
85
|
let bun = false;
|
|
83
|
-
const bunPath = findOnPath("bun");
|
|
86
|
+
const bunPath = bunNeeded ? findOnPath("bun") : undefined;
|
|
84
87
|
if (bunPath) {
|
|
85
88
|
try {
|
|
86
89
|
const version = execFileSync(bunPath, ["--version"], {
|
|
@@ -93,14 +96,16 @@ export async function runColdPreflight(opts = {}) {
|
|
|
93
96
|
/* reported as a failed runtime check */
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
|
-
items.push(
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
items.push(bunNeeded
|
|
100
|
+
? {
|
|
101
|
+
id: "bun",
|
|
102
|
+
ok: bun,
|
|
103
|
+
message: bun
|
|
104
|
+
? `Bun ${BUN_FLOOR_LABEL} is available on PATH`
|
|
105
|
+
: `Bun ${BUN_FLOOR_LABEL} not found on PATH`,
|
|
106
|
+
fix: bun ? undefined : "curl -fsSL https://bun.sh/install | bash",
|
|
107
|
+
}
|
|
108
|
+
: { id: "bun", ok: true, message: "Bun not needed: no generated JS services" });
|
|
104
109
|
try {
|
|
105
110
|
const plan = await getHostPortPlan(projectRoot ?? cwd);
|
|
106
111
|
items.push({
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, normalize } from "node:path";
|
|
3
|
+
import { formatCount } from "./formatting.js";
|
|
4
|
+
import { findProjectRoot } from "./paths.js";
|
|
5
|
+
export function findPrivateStarlarkLoadExports(content) {
|
|
6
|
+
const privateExports = new Set();
|
|
7
|
+
const loadCallPattern = /^load\s*\(([\s\S]*?)\)/gm;
|
|
8
|
+
for (const loadMatch of content.matchAll(loadCallPattern)) {
|
|
9
|
+
const args = loadMatch[1] ?? "";
|
|
10
|
+
const quotedValuePattern = /["']([^"']+)["']/g;
|
|
11
|
+
const quotedValues = Array.from(args.matchAll(quotedValuePattern), (match) => match[1]);
|
|
12
|
+
// First quoted arg is the module path. Later quoted args are exported names
|
|
13
|
+
// or alias targets, and Starlark refuses to export names beginning with "_".
|
|
14
|
+
for (const value of quotedValues.slice(1)) {
|
|
15
|
+
if (value.startsWith("_")) {
|
|
16
|
+
privateExports.add(value);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return Array.from(privateExports).sort();
|
|
21
|
+
}
|
|
22
|
+
export function resolveStarlarkLoadTarget(file, modulePath, projectRoot) {
|
|
23
|
+
if (modulePath === "ext://tdk-cli") {
|
|
24
|
+
return join(projectRoot, ".tdk", ".tdk-out", "tdk-cli-ext", "Tiltfile");
|
|
25
|
+
}
|
|
26
|
+
if (modulePath.startsWith(".") || (!modulePath.includes("://") && !modulePath.startsWith("@"))) {
|
|
27
|
+
return normalize(join(dirname(file), modulePath));
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
export function findMissingRelativeStarlarkLoads(file, content, projectRoot) {
|
|
32
|
+
const missing = [];
|
|
33
|
+
const loadCallPattern = /^load\s*\(([\s\S]*?)\)/gm;
|
|
34
|
+
for (const loadMatch of content.matchAll(loadCallPattern)) {
|
|
35
|
+
const args = loadMatch[1] ?? "";
|
|
36
|
+
const moduleMatch = args.match(/["']([^"']+)["']/);
|
|
37
|
+
const modulePath = moduleMatch?.[1];
|
|
38
|
+
if (!modulePath) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const resolved = resolveStarlarkLoadTarget(file, modulePath, projectRoot);
|
|
42
|
+
if (!resolved) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (!existsSync(resolved)) {
|
|
46
|
+
missing.push(`${file}: ${modulePath} -> ${resolved}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return missing;
|
|
50
|
+
}
|
|
51
|
+
export function findRelativeStarlarkLoadTargets(file, content, projectRoot) {
|
|
52
|
+
const targets = [];
|
|
53
|
+
const loadCallPattern = /^load\s*\(([\s\S]*?)\)/gm;
|
|
54
|
+
for (const loadMatch of content.matchAll(loadCallPattern)) {
|
|
55
|
+
const args = loadMatch[1] ?? "";
|
|
56
|
+
const moduleMatch = args.match(/["']([^"']+)["']/);
|
|
57
|
+
const modulePath = moduleMatch?.[1];
|
|
58
|
+
if (modulePath) {
|
|
59
|
+
const target = resolveStarlarkLoadTarget(file, modulePath, projectRoot);
|
|
60
|
+
if (target) {
|
|
61
|
+
targets.push(target);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return targets;
|
|
66
|
+
}
|
|
67
|
+
export function collectReachableStarlarkFiles(entryFile, projectRoot) {
|
|
68
|
+
const visited = new Set();
|
|
69
|
+
const pending = [entryFile];
|
|
70
|
+
while (pending.length > 0) {
|
|
71
|
+
const file = pending.pop();
|
|
72
|
+
if (!file || visited.has(file) || !existsSync(file)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
visited.add(file);
|
|
76
|
+
const content = readFileSync(file, "utf-8");
|
|
77
|
+
for (const target of findRelativeStarlarkLoadTargets(file, content, projectRoot)) {
|
|
78
|
+
if (!visited.has(target)) {
|
|
79
|
+
pending.push(target);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return Array.from(visited).sort();
|
|
84
|
+
}
|
|
85
|
+
export function checkStarlarkLoadExports() {
|
|
86
|
+
const projectRoot = findProjectRoot() ?? process.cwd();
|
|
87
|
+
const entryFile = join(projectRoot, ".tdk", ".tdk-out", "Tiltfile");
|
|
88
|
+
if (!existsSync(entryFile)) {
|
|
89
|
+
return {
|
|
90
|
+
name: "Starlark Load Exports",
|
|
91
|
+
didPass: true,
|
|
92
|
+
isSkipped: true,
|
|
93
|
+
message: "No generated Tiltfile to check",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const privateExportProblems = [];
|
|
97
|
+
const missingLoadProblems = [];
|
|
98
|
+
const reachableFiles = collectReachableStarlarkFiles(entryFile, projectRoot);
|
|
99
|
+
for (const file of reachableFiles) {
|
|
100
|
+
const content = readFileSync(file, "utf-8");
|
|
101
|
+
const privateExports = findPrivateStarlarkLoadExports(content);
|
|
102
|
+
if (privateExports.length > 0) {
|
|
103
|
+
privateExportProblems.push(`${file}: ${privateExports.join(", ")}`);
|
|
104
|
+
}
|
|
105
|
+
for (const missingLoad of findMissingRelativeStarlarkLoads(file, content, projectRoot)) {
|
|
106
|
+
missingLoadProblems.push(missingLoad);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (privateExportProblems.length === 0 && missingLoadProblems.length === 0) {
|
|
110
|
+
const fileCount = formatCount(reachableFiles.length, "reachable Starlark file");
|
|
111
|
+
return {
|
|
112
|
+
name: "Starlark Load Exports",
|
|
113
|
+
didPass: true,
|
|
114
|
+
message: `${fileCount} ${reachableFiles.length === 1 ? "has" : "have"} valid load exports and paths`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const sections = [];
|
|
118
|
+
if (privateExportProblems.length > 0) {
|
|
119
|
+
sections.push(`Private exported symbols:\n ${privateExportProblems.join("\n ")}`);
|
|
120
|
+
}
|
|
121
|
+
if (missingLoadProblems.length > 0) {
|
|
122
|
+
sections.push(`Missing relative load targets:\n ${missingLoadProblems.join("\n ")}`);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
name: "Starlark Load Exports",
|
|
126
|
+
didPass: false,
|
|
127
|
+
message: `Starlark load() issues will stop Tilt before services start:\n ${sections.join("\n\n ")}`,
|
|
128
|
+
fix: "Regenerate with a current TDK (`tdk config regenerate`). If the issue remains, rename loaded symbols without leading underscores and update stale load paths.",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=doctor-starlark.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { writeTextFileAtomic } from "./atomic-write.js";
|
|
4
5
|
const REQUIRED_ENV_VARS = [
|
|
5
6
|
{
|
|
6
7
|
name: "VERDACCIO_URL_DOCKER",
|
|
@@ -59,21 +60,47 @@ function generatedValue(name, password) {
|
|
|
59
60
|
return `postgresql://postgres:${password}@postgres:5432/app_dev`;
|
|
60
61
|
return REQUIRED_ENV_VARS.find((v) => v.name === name)?.default ?? "";
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
-
function unquote(raw) {
|
|
63
|
+
function parseEnvValue(raw) {
|
|
64
64
|
const value = raw.trim();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
const quote = value[0];
|
|
66
|
+
if (quote === '"' || quote === "'") {
|
|
67
|
+
let escaped = false;
|
|
68
|
+
for (let index = 1; index < value.length; index += 1) {
|
|
69
|
+
const char = value[index];
|
|
70
|
+
if (escaped) {
|
|
71
|
+
escaped = false;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (char === "\\") {
|
|
75
|
+
escaped = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (char === quote) {
|
|
79
|
+
const quoted = value.slice(1, index);
|
|
80
|
+
if (quote === '"') {
|
|
81
|
+
const escapes = {
|
|
82
|
+
n: "\n",
|
|
83
|
+
r: "\r",
|
|
84
|
+
t: "\t",
|
|
85
|
+
'"': '"',
|
|
86
|
+
"\\": "\\",
|
|
87
|
+
$: "$",
|
|
88
|
+
};
|
|
89
|
+
return quoted.replace(/\\([nrt"\\$])/g, (_match, escapedChar) => escapes[escapedChar] ?? escapedChar);
|
|
90
|
+
}
|
|
91
|
+
return quoted.replace(/\\'/g, "'");
|
|
92
|
+
}
|
|
69
93
|
}
|
|
94
|
+
return value;
|
|
70
95
|
}
|
|
71
|
-
|
|
96
|
+
const commentIndex = raw.search(/\s#/);
|
|
97
|
+
return (commentIndex === -1 ? raw : raw.slice(0, commentIndex)).trim();
|
|
72
98
|
}
|
|
73
|
-
/** Parses the
|
|
99
|
+
/** Parses the Compose-compatible subset used by TDK for project environment files. */
|
|
74
100
|
export function parseEnv(content) {
|
|
75
101
|
const result = new Map();
|
|
76
|
-
|
|
102
|
+
const normalized = content.startsWith("\uFEFF") ? content.slice(1) : content;
|
|
103
|
+
for (const rawLine of normalized.split(/\r?\n/)) {
|
|
77
104
|
const line = rawLine.trim();
|
|
78
105
|
if (!line || line.startsWith("#"))
|
|
79
106
|
continue;
|
|
@@ -84,7 +111,7 @@ export function parseEnv(content) {
|
|
|
84
111
|
const name = assignment.slice(0, equals).trim();
|
|
85
112
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
86
113
|
continue;
|
|
87
|
-
result.set(name,
|
|
114
|
+
result.set(name, parseEnvValue(assignment.slice(equals + 1)));
|
|
88
115
|
}
|
|
89
116
|
return result;
|
|
90
117
|
}
|
|
@@ -154,10 +181,26 @@ export function validateEnvFile(projectRoot) {
|
|
|
154
181
|
return result;
|
|
155
182
|
}
|
|
156
183
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
184
|
+
* Replaces the value in one parsed assignment while preserving its prefix, inline comment, and line ending.
|
|
185
|
+
* The caller only uses this for assignments whose parsed value is empty or whitespace.
|
|
186
|
+
*/
|
|
187
|
+
function replaceEmptyEnvValue(line, name, value) {
|
|
188
|
+
const parsed = parseEnv(line);
|
|
189
|
+
if (!parsed.has(name))
|
|
190
|
+
return line;
|
|
191
|
+
const equalsIndex = line.indexOf("=");
|
|
192
|
+
if (equalsIndex < 0)
|
|
193
|
+
return line;
|
|
194
|
+
const rawValue = line.slice(equalsIndex + 1);
|
|
195
|
+
const commentIndex = rawValue.search(/\s#/);
|
|
196
|
+
const comment = commentIndex < 0 ? "" : rawValue.slice(commentIndex);
|
|
197
|
+
return line.slice(0, equalsIndex + 1) + value + comment;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Appends missing keys a newer CLI expects and fills empty generated keys in place.
|
|
201
|
+
* Non-empty values are not changed. Creates the file when it is missing.
|
|
159
202
|
*
|
|
160
|
-
* @returns the names that were added, empty when the file was already complete
|
|
203
|
+
* @returns the names that were added or filled in, empty when the file was already complete
|
|
161
204
|
*/
|
|
162
205
|
export function completeEnvFile(projectRoot) {
|
|
163
206
|
const envPath = join(projectRoot, ".env");
|
|
@@ -167,24 +210,49 @@ export function completeEnvFile(projectRoot) {
|
|
|
167
210
|
}
|
|
168
211
|
const existing = readFileSync(envPath, "utf-8");
|
|
169
212
|
const env = parseEnv(existing);
|
|
170
|
-
const
|
|
171
|
-
|
|
213
|
+
const toComplete = REQUIRED_ENV_VARS.filter((v) => {
|
|
214
|
+
if (!v.complete)
|
|
215
|
+
return false;
|
|
216
|
+
if (!env.has(v.name))
|
|
217
|
+
return true;
|
|
218
|
+
return !(env.get(v.name) ?? "").trim();
|
|
219
|
+
});
|
|
220
|
+
if (toComplete.length === 0)
|
|
172
221
|
return [];
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
lines.
|
|
182
|
-
|
|
183
|
-
|
|
222
|
+
const toAdd = toComplete.filter((v) => !env.has(v.name));
|
|
223
|
+
const toRepair = toComplete.filter((v) => env.has(v.name));
|
|
224
|
+
// Anything generated from the password must use the project's existing non-empty value.
|
|
225
|
+
const existingPassword = env.get("DB_PASSWORD");
|
|
226
|
+
const password = existingPassword?.trim() ? existingPassword : generateDbPassword();
|
|
227
|
+
const lines = existing.split(/(\r\n|\n|\r)/);
|
|
228
|
+
for (const envVar of toRepair) {
|
|
229
|
+
let lineIndex = -1;
|
|
230
|
+
for (let index = 0; index < lines.length; index += 2) {
|
|
231
|
+
if (parseEnv(lines[index]).has(envVar.name))
|
|
232
|
+
lineIndex = index;
|
|
233
|
+
}
|
|
234
|
+
if (lineIndex < 0) {
|
|
235
|
+
throw new Error(`Could not locate empty .env assignment for ${envVar.name}`);
|
|
236
|
+
}
|
|
237
|
+
lines[lineIndex] = replaceEmptyEnvValue(lines[lineIndex], envVar.name, generatedValue(envVar.name, password));
|
|
238
|
+
}
|
|
239
|
+
let updated = lines.join("");
|
|
240
|
+
if (toAdd.length > 0) {
|
|
241
|
+
const addedLines = [
|
|
242
|
+
"",
|
|
243
|
+
"# Added by TDK: keys this version expects. Existing values above were not changed.",
|
|
244
|
+
"",
|
|
245
|
+
];
|
|
246
|
+
for (const envVar of toAdd) {
|
|
247
|
+
addedLines.push(...describe(envVar));
|
|
248
|
+
addedLines.push(`${envVar.name}=${generatedValue(envVar.name, password)}`);
|
|
249
|
+
addedLines.push("");
|
|
250
|
+
}
|
|
251
|
+
const separator = updated.endsWith("\n") || updated.endsWith("\r") ? "" : "\n";
|
|
252
|
+
updated += separator + addedLines.join("\n");
|
|
184
253
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return toAdd.map((v) => v.name);
|
|
254
|
+
writeTextFileAtomic(envPath, updated);
|
|
255
|
+
return toComplete.map((v) => v.name);
|
|
188
256
|
}
|
|
189
257
|
export function ensureEnvFile(projectRoot) {
|
|
190
258
|
const envPath = join(projectRoot, ".env");
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { relative } from "node:path";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { formatCount } from "./formatting.js";
|
|
4
|
+
import { SERVICE_MANIFEST_SCHEMA_VERSION } from "./service-manifest.js";
|
|
5
|
+
import { discoverResourcesFromRoot } from "./services.js";
|
|
6
|
+
/**
|
|
7
|
+
* A service.json that declares a schemaVersion this tdk does not know is refused before anything starts: a newer format
|
|
8
|
+
* read as if it were the old one could start the wrong thing. A missing schemaVersion is not refused (see
|
|
9
|
+
* warnSchemaVersions), because every project began without one.
|
|
10
|
+
*/
|
|
11
|
+
export function enforceSchemaVersionGate(projectRoot, options = {}, exit = process.exit) {
|
|
12
|
+
// A root that cannot be read is reported by the checks that run next; this gate only refuses what it can read.
|
|
13
|
+
let resources;
|
|
14
|
+
try {
|
|
15
|
+
resources = discoverResourcesFromRoot(projectRoot);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const unsupported = resources.flatMap((resource) => {
|
|
21
|
+
const version = resource.config?.schemaVersion;
|
|
22
|
+
if (version === undefined || version === SERVICE_MANIFEST_SCHEMA_VERSION)
|
|
23
|
+
return [];
|
|
24
|
+
const file = relative(projectRoot, resource.configPath) || resource.configPath;
|
|
25
|
+
return [
|
|
26
|
+
`${file} schemaVersion ${JSON.stringify(version)}: this tdk supports ${SERVICE_MANIFEST_SCHEMA_VERSION}`,
|
|
27
|
+
];
|
|
28
|
+
});
|
|
29
|
+
if (unsupported.length === 0)
|
|
30
|
+
return;
|
|
31
|
+
const verb = unsupported.length === 1 ? "declares" : "declare";
|
|
32
|
+
const message = `${formatCount(unsupported.length, "service.json file")} ${verb} a schemaVersion this tdk does not support:\n ${unsupported.join("\n ")}`;
|
|
33
|
+
const fix = "Upgrade tdk to a version that supports these files. Changing schemaVersion by hand is not a fix";
|
|
34
|
+
options.onInvalid?.(`${message}\nFix: ${fix}`);
|
|
35
|
+
console.error(chalk.red(`✗ ${message}`));
|
|
36
|
+
console.error(chalk.gray(` Fix: ${fix}`));
|
|
37
|
+
exit(1);
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=schema-version-gate.js.map
|
|
@@ -14,6 +14,7 @@ const DEPRECATED_FIELDS = {
|
|
|
14
14
|
const KNOWN_SERVICE_FIELDS = new Set([
|
|
15
15
|
"$schema",
|
|
16
16
|
"apiBasePath",
|
|
17
|
+
"apiPath",
|
|
17
18
|
"appName",
|
|
18
19
|
"appType",
|
|
19
20
|
"backendName",
|
|
@@ -49,6 +50,13 @@ const KNOWN_SERVICE_FIELDS = new Set([
|
|
|
49
50
|
"traefik",
|
|
50
51
|
"type",
|
|
51
52
|
]);
|
|
53
|
+
const SERVICE_ROUTE_PATH_FIELDS = ["apiPath", "basePath"];
|
|
54
|
+
export function getInvalidServiceRoutePathFields(value) {
|
|
55
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
56
|
+
return [];
|
|
57
|
+
const manifest = value;
|
|
58
|
+
return SERVICE_ROUTE_PATH_FIELDS.filter((field) => field in manifest && typeof manifest[field] !== "string");
|
|
59
|
+
}
|
|
52
60
|
export function validateServiceManifest(value, displayPath) {
|
|
53
61
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
54
62
|
return { errors: [`${displayPath}: expected a JSON object`], warnings: [] };
|
|
@@ -67,6 +75,9 @@ export function validateServiceManifest(value, displayPath) {
|
|
|
67
75
|
if ("schemaVersion" in manifest && manifest.schemaVersion !== SERVICE_MANIFEST_SCHEMA_VERSION) {
|
|
68
76
|
errors.push(`${displayPath}.schemaVersion: unsupported version ${String(manifest.schemaVersion)} (supported: ${SERVICE_MANIFEST_SCHEMA_VERSION})`);
|
|
69
77
|
}
|
|
78
|
+
for (const field of getInvalidServiceRoutePathFields(manifest)) {
|
|
79
|
+
errors.push(`${displayPath}.${field}: expected a string`);
|
|
80
|
+
}
|
|
70
81
|
const dev = manifest.dev;
|
|
71
82
|
const liveReload = dev && typeof dev === "object" && !Array.isArray(dev)
|
|
72
83
|
? dev.liveReload
|
|
@@ -5,7 +5,7 @@ import { AUTOGENERATED_FILE_PATTERNS, SKIP_DIRECTORIES } from "./constants.js";
|
|
|
5
5
|
import { errorFactories } from "./errors.js";
|
|
6
6
|
import { formatCount } from "./formatting.js";
|
|
7
7
|
import { findProjectRoot } from "./paths.js";
|
|
8
|
-
import { validateServiceManifest } from "./service-manifest.js";
|
|
8
|
+
import { getInvalidServiceRoutePathFields, validateServiceManifest } from "./service-manifest.js";
|
|
9
9
|
import { deriveStackStatus } from "./stack-status.js";
|
|
10
10
|
import { includes } from "./validation.js";
|
|
11
11
|
const RESOURCE_JSON_FILENAME = "service.json";
|
|
@@ -79,6 +79,10 @@ function parseResource(serviceJsonPath) {
|
|
|
79
79
|
if (!isValidResourceConfig(parsed)) {
|
|
80
80
|
throw new InvalidServiceJsonError("missing required fields (appName, runtime or appType)");
|
|
81
81
|
}
|
|
82
|
+
const invalidRoutePathFields = getInvalidServiceRoutePathFields(parsed);
|
|
83
|
+
if (invalidRoutePathFields.length > 0) {
|
|
84
|
+
throw new InvalidServiceJsonError(invalidRoutePathFields.map((field) => `${field}: expected a string`).join("; "));
|
|
85
|
+
}
|
|
82
86
|
const parsedConfig = parsed;
|
|
83
87
|
for (const warning of validateServiceManifest(parsed, serviceJsonPath).warnings) {
|
|
84
88
|
// The warning names the file and field and says what is wrong (unknown, deprecated, ignored), so do not prefix it.
|
|
@@ -99,6 +103,8 @@ function parseResource(serviceJsonPath) {
|
|
|
99
103
|
configPath: serviceJsonPath,
|
|
100
104
|
config,
|
|
101
105
|
stack: config.stack,
|
|
106
|
+
port: config.port,
|
|
107
|
+
type: config.appType,
|
|
102
108
|
};
|
|
103
109
|
}
|
|
104
110
|
export function discoverResourcesWithProblems(projectRoot) {
|
package/cli/dist/utils/tilt.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { errorFactories } from "./errors.js";
|
|
3
4
|
import { findProjectRoot } from "./paths.js";
|
|
4
5
|
import { findOnPath } from "./which.js";
|
|
5
6
|
export function runTilt(command, args = [], options = {}) {
|
|
@@ -49,7 +50,7 @@ export async function isTiltAvailable() {
|
|
|
49
50
|
export function getTiltfilePath() {
|
|
50
51
|
const projectRoot = findProjectRoot();
|
|
51
52
|
if (!projectRoot) {
|
|
52
|
-
throw
|
|
53
|
+
throw errorFactories.notInProject();
|
|
53
54
|
}
|
|
54
55
|
return join(projectRoot, ".tdk", ".tdk-out", "Tiltfile");
|
|
55
56
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
export const TABS = [
|
|
2
|
+
{ id: "overview", label: "OVERVIEW", shortcut: "1" },
|
|
3
|
+
{ id: "resources", label: "RESOURCES", shortcut: "2" },
|
|
4
|
+
{ id: "events", label: "EVENTS", shortcut: "3" },
|
|
5
|
+
{ id: "files", label: "FILES", shortcut: "4" },
|
|
6
|
+
{ id: "config", label: "CONFIG", shortcut: "5" },
|
|
7
|
+
];
|
|
8
|
+
const tabRange = `${TABS[0].shortcut}-${TABS[TABS.length - 1].shortcut}`;
|
|
9
|
+
const uiKeymap = [
|
|
10
|
+
{
|
|
11
|
+
id: "list-navigation",
|
|
12
|
+
keys: ["ArrowUp", "ArrowDown", "j", "k"],
|
|
13
|
+
label: "Navigate list items",
|
|
14
|
+
group: "Navigation",
|
|
15
|
+
display: { ascii: "UP/DOWN or k/j", unicode: "↑/↓ or k/j" },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "first-last",
|
|
19
|
+
keys: ["g", "G", "Home", "End"],
|
|
20
|
+
label: "First/last item",
|
|
21
|
+
group: "Navigation",
|
|
22
|
+
display: { ascii: "g/G or Home/End", unicode: "g/G or Home/End" },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "page-list",
|
|
26
|
+
keys: ["PageUp", "PageDown"],
|
|
27
|
+
label: "Move one page",
|
|
28
|
+
group: "Navigation",
|
|
29
|
+
display: { ascii: "PgUp/PgDn", unicode: "PgUp/PgDn" },
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "select",
|
|
33
|
+
keys: ["Enter", "Space"],
|
|
34
|
+
label: "Select item / Open detail",
|
|
35
|
+
group: "Navigation",
|
|
36
|
+
display: { ascii: "Enter or Space", unicode: "Enter or Space" },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "cycle-tabs",
|
|
40
|
+
keys: ["Tab", "Shift+Tab"],
|
|
41
|
+
label: "Next / previous tab",
|
|
42
|
+
group: "Navigation",
|
|
43
|
+
display: { ascii: "Tab/Shift+Tab", unicode: "Tab/Shift+Tab" },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "direct-tabs",
|
|
47
|
+
keys: TABS.map((tab) => tab.shortcut),
|
|
48
|
+
label: "Direct tab access",
|
|
49
|
+
group: "Navigation",
|
|
50
|
+
display: { ascii: tabRange, unicode: tabRange },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "toggle-mouse",
|
|
54
|
+
keys: ["m"],
|
|
55
|
+
label: "Toggle mouse support",
|
|
56
|
+
group: "Actions",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "toggle-tooltips",
|
|
60
|
+
keys: ["t"],
|
|
61
|
+
label: "Toggle tooltips",
|
|
62
|
+
group: "Actions",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "toggle-enabled",
|
|
66
|
+
keys: ["e"],
|
|
67
|
+
label: "Toggle enabled/disabled services",
|
|
68
|
+
group: "Actions",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "refresh",
|
|
72
|
+
keys: ["r"],
|
|
73
|
+
label: "Refresh data",
|
|
74
|
+
group: "Actions",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "search",
|
|
78
|
+
keys: ["/"],
|
|
79
|
+
label: "Search/filter",
|
|
80
|
+
group: "Actions",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: "help",
|
|
84
|
+
keys: ["?"],
|
|
85
|
+
label: "Show this help",
|
|
86
|
+
group: "Actions",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "quit",
|
|
90
|
+
keys: ["q"],
|
|
91
|
+
label: "Quit",
|
|
92
|
+
group: "Actions",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "back",
|
|
96
|
+
keys: ["Esc"],
|
|
97
|
+
label: "Back",
|
|
98
|
+
group: "Actions",
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
export const UI_KEYMAP = uiKeymap;
|
|
102
|
+
function getUiKeymapEntry(id) {
|
|
103
|
+
const entry = UI_KEYMAP.find((shortcut) => shortcut.id === id);
|
|
104
|
+
if (!entry) {
|
|
105
|
+
throw new Error(`Unknown TDK UI shortcut: ${id}`);
|
|
106
|
+
}
|
|
107
|
+
return entry;
|
|
108
|
+
}
|
|
109
|
+
export function formatUiKeyNames(id, ascii = false) {
|
|
110
|
+
const entry = getUiKeymapEntry(id);
|
|
111
|
+
const display = ascii ? entry.display?.ascii : entry.display?.unicode;
|
|
112
|
+
return display ?? entry.keys.join("/");
|
|
113
|
+
}
|
|
114
|
+
export function formatUiKeyHint(id, label, ascii = false) {
|
|
115
|
+
const entry = getUiKeymapEntry(id);
|
|
116
|
+
return `[${formatUiKeyNames(id, ascii)}] ${label ?? entry.label}`;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=ui-keymap.js.map
|
package/cli/package.json
CHANGED
|
@@ -318,11 +318,11 @@ def write_file_if_changed(file_path, content):
|
|
|
318
318
|
existing = str(existing_blob).rstrip()
|
|
319
319
|
|
|
320
320
|
if existing == expected:
|
|
321
|
-
|
|
321
|
+
debug_log("WRITE: Skipping {} (no changes)".format(file_path))
|
|
322
322
|
else:
|
|
323
|
-
|
|
323
|
+
debug_log("WRITE: Writing {} bytes to {}".format(len(expected), file_path))
|
|
324
324
|
_write_generated_file(file_path, expected)
|
|
325
|
-
|
|
325
|
+
debug_log("WRITE: Completed {}".format(file_path))
|
|
326
326
|
|
|
327
327
|
# Keep a generated-content snapshot beside the service outputs. `tdk config verify`
|
|
328
328
|
# compares these snapshots with the files and the source manifest, detecting both
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# 🛠️ TILT SDK - DEBUG UTILITIES
|
|
3
3
|
# =============================================================================
|
|
4
4
|
|
|
5
|
-
DEBUG_MODE = os.environ.get('TILT_DEBUG', 'false').lower() in ['true', '1', 'yes']
|
|
5
|
+
DEBUG_MODE = (os.environ.get('TDK_DEBUG') or os.environ.get('TILT_DEBUG', 'false')).lower() in ['true', '1', 'yes']
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def debug_log(message, data=None):
|
|
@@ -3,6 +3,7 @@ OUTPUT = struct(AUTOGENERATED_FOLDER = ".autogenerated")
|
|
|
3
3
|
# === END INLINED CONSTANTS ===
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
load("../../common/utils_debug.star", "debug_log")
|
|
6
7
|
load("../../../../topologies/tilt/common/utils.star", "Utils")
|
|
7
8
|
load("../../../../topologies/tilt/manifest/constants.star", "GENERATED_CONFIG_FILENAMES")
|
|
8
9
|
|
|
@@ -126,15 +127,12 @@ def generate_frontend_tsconfig(resource_path, write_fn, internal_deps=None, is_d
|
|
|
126
127
|
}
|
|
127
128
|
if is_docker:
|
|
128
129
|
output_path = resource_path + '/' + OUTPUT.AUTOGENERATED_FOLDER + '/' + GENERATED_CONFIG_FILENAMES['tsconfig_docker']
|
|
129
|
-
|
|
130
|
-
print("📝 DEBUG: Writing Docker tsconfig to: {}".format(output_path))
|
|
130
|
+
debug_log("📝 Writing Docker tsconfig to: {}".format(output_path))
|
|
131
131
|
write_fn(output_path, Utils.encode_json(content))
|
|
132
|
-
|
|
133
|
-
print("📝 DEBUG: Docker tsconfig write complete")
|
|
132
|
+
debug_log("📝 Docker tsconfig write complete")
|
|
134
133
|
else:
|
|
135
134
|
output_path = resource_path + '/' + OUTPUT.AUTOGENERATED_FOLDER + '/' + GENERATED_CONFIG_FILENAMES['tsconfig_local']
|
|
136
|
-
|
|
137
|
-
print("📝 DEBUG: Writing Local tsconfig to: {}".format(output_path))
|
|
135
|
+
debug_log("📝 Writing Local tsconfig to: {}".format(output_path))
|
|
138
136
|
write_fn(output_path, Utils.encode_json(content))
|
|
139
137
|
|
|
140
138
|
DEBUG = {}
|