@tdk-landscape/tdk-cli-core 1.3.78 → 1.3.80

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.
@@ -6,6 +6,7 @@ import { getStackEmoji } from "../utils/constants.js";
6
6
  import { createDiscoveryContext } from "../utils/discovery-context.js";
7
7
  import { logVerbose, requireProjectRoot } from "../utils/errors.js";
8
8
  import { colorizeByStatus, DEFAULT_BOX_WIDTH, formatBoxLine, formatPadded, getStatusIcon, printBoxedHeader, } from "../utils/formatting.js";
9
+ import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
9
10
  import { findProjectRoot } from "../utils/paths.js";
10
11
  import { checkPortStatus } from "../utils/port-assignment.js";
11
12
  import { isValidPort, sanitizeForShell } from "../utils/validation.js";
@@ -156,118 +157,137 @@ export const networksCommand = new Command("networks")
156
157
  .alias("urls")
157
158
  .alias("traefik")
158
159
  .option("-s, --stack <stack>", "Filter by stack name")
159
- .option("--json", "Output as JSON")
160
+ .option("--json", "Output a versioned JSON response")
161
+ .option("--json-legacy", "Output the legacy JSON array (deprecated)")
160
162
  .option("--raw", "Output raw URLs only")
161
163
  .action(async (options) => {
162
- const projectRoot = requireProjectRoot();
163
- const discovery = createDiscoveryContext();
164
- const baseDomain = determineDefaultDomain();
165
- const bareDomain = baseDomain.replace(/^(app|api)\./, "");
166
- const appDomain = `app.${bareDomain}`;
167
- const apiDomain = `api.${bareDomain}`;
168
- const services = discovery.resources;
169
- const servicesWithUrls = await Promise.all(services
170
- .filter((s) => typeof s.config?.basePath === "string")
171
- .map(async (s) => {
172
- const basePath = s.config.basePath.replace(/^\//, "");
173
- const isBackend = s.config?.appType === "backend";
174
- const host = isBackend ? apiDomain : appDomain;
175
- const url = `http://${host}/${basePath}`;
176
- const port = s.config.port;
177
- const status = await checkServiceStatus(s.name, port, url);
178
- return {
179
- name: s.name,
180
- stack: s.stack,
181
- basePath: s.config.basePath,
182
- url,
183
- ...(process.platform === "win32"
184
- ? { loopbackUrl: `http://127.0.0.1:80/${basePath}`.replace(/\/$/, "") }
185
- : {}),
186
- port,
187
- status,
188
- };
189
- }));
190
- const filteredServices = options.stack
191
- ? servicesWithUrls.filter((s) => s.stack === options.stack)
192
- : servicesWithUrls;
193
- if (filteredServices.length === 0) {
194
- if (options.stack) {
195
- console.log(chalk.yellow(`⚠️ No services with basePath found in stack "${options.stack}"`));
164
+ const action = async () => {
165
+ const projectRoot = options.json || options.jsonLegacy ? findProjectRoot() : requireProjectRoot();
166
+ if (!projectRoot) {
167
+ throw new Error("Could not find project root (no .tdk/project.json found)");
196
168
  }
197
- else {
198
- console.log(chalk.yellow("⚠️ No services with basePath found"));
199
- console.log(chalk.gray("\nAdd basePath to your service.json:"));
200
- console.log(chalk.gray(' "basePath": "/my-service"'));
169
+ const discovery = createDiscoveryContext();
170
+ const baseDomain = determineDefaultDomain();
171
+ const bareDomain = baseDomain.replace(/^(app|api)\./, "");
172
+ const appDomain = `app.${bareDomain}`;
173
+ const apiDomain = `api.${bareDomain}`;
174
+ const services = discovery.resources;
175
+ const servicesWithUrls = await Promise.all(services
176
+ .filter((s) => typeof s.config?.basePath === "string")
177
+ .map(async (s) => {
178
+ const basePath = s.config.basePath.replace(/^\//, "");
179
+ const isBackend = s.config?.appType === "backend";
180
+ const host = isBackend ? apiDomain : appDomain;
181
+ const url = `http://${host}/${basePath}`;
182
+ const port = s.config.port;
183
+ const status = await checkServiceStatus(s.name, port, url);
184
+ return {
185
+ name: s.name,
186
+ stack: s.stack,
187
+ basePath: s.config.basePath,
188
+ url,
189
+ ...(process.platform === "win32"
190
+ ? { loopbackUrl: `http://127.0.0.1:80/${basePath}`.replace(/\/$/, "") }
191
+ : {}),
192
+ port,
193
+ status,
194
+ };
195
+ }));
196
+ const filteredServices = options.stack
197
+ ? servicesWithUrls.filter((s) => s.stack === options.stack)
198
+ : servicesWithUrls;
199
+ if (options.json || options.jsonLegacy) {
200
+ const output = options.jsonLegacy
201
+ ? filteredServices
202
+ : createMachineEnvelope({ services: filteredServices });
203
+ console.log(JSON.stringify(output, null, 2));
204
+ return;
201
205
  }
202
- process.exit(0);
203
- }
204
- if (options.json) {
205
- console.log(JSON.stringify(filteredServices, null, 2));
206
- process.exit(0);
207
- }
208
- if (options.raw) {
209
- for (const service of filteredServices) {
210
- console.log(service.url);
211
- if (service.loopbackUrl)
212
- console.log(service.loopbackUrl);
206
+ if (filteredServices.length === 0) {
207
+ if (options.stack) {
208
+ console.log(chalk.yellow(`⚠️ No services with basePath found in stack "${options.stack}"`));
209
+ }
210
+ else {
211
+ console.log(chalk.yellow("⚠️ No services with basePath found"));
212
+ console.log(chalk.gray("\nAdd basePath to your service.json:"));
213
+ console.log(chalk.gray(' "basePath": "/my-service"'));
214
+ }
215
+ process.exit(0);
213
216
  }
214
- process.exit(0);
215
- }
216
- printBoxedHeader("🌐 TRAEFIK NETWORKS", `Domain: http://${baseDomain}`, DEFAULT_BOX_WIDTH);
217
- // Group services by stack using discovery context's stack names for consistent ordering
218
- const stacks = new Map();
219
- for (const stackName of discovery.stackNames) {
220
- const stackServices = filteredServices.filter((s) => s.stack === stackName);
221
- if (stackServices.length > 0) {
222
- stacks.set(stackName, stackServices);
217
+ if (options.raw) {
218
+ for (const service of filteredServices) {
219
+ console.log(service.url);
220
+ if (service.loopbackUrl)
221
+ console.log(service.loopbackUrl);
222
+ }
223
+ process.exit(0);
223
224
  }
224
- }
225
- // Add unstacked services to 'default' group
226
- const unstackedServices = filteredServices.filter((s) => !s.stack);
227
- if (unstackedServices.length > 0) {
228
- stacks.set("default", unstackedServices);
229
- }
230
- let isFirstStack = true;
231
- for (const [stackName, stackServices] of stacks) {
232
- if (!isFirstStack) {
225
+ printBoxedHeader("🌐 TRAEFIK NETWORKS", `Domain: http://${baseDomain}`, DEFAULT_BOX_WIDTH);
226
+ // Group services by stack using discovery context's stack names for consistent ordering
227
+ const stacks = new Map();
228
+ for (const stackName of discovery.stackNames) {
229
+ const stackServices = filteredServices.filter((s) => s.stack === stackName);
230
+ if (stackServices.length > 0) {
231
+ stacks.set(stackName, stackServices);
232
+ }
233
+ }
234
+ // Add unstacked services to 'default' group
235
+ const unstackedServices = filteredServices.filter((s) => !s.stack);
236
+ if (unstackedServices.length > 0) {
237
+ stacks.set("default", unstackedServices);
238
+ }
239
+ let isFirstStack = true;
240
+ for (const [stackName, stackServices] of stacks) {
241
+ if (!isFirstStack) {
242
+ console.log();
243
+ }
244
+ isFirstStack = false;
245
+ const emoji = getStackEmoji(stackName);
246
+ const stackTitle = `${emoji} ${stackName.toUpperCase()} STACK`;
233
247
  console.log();
248
+ console.log(chalk.bold.white(stackTitle));
249
+ console.log(chalk.gray(formatBoxLine("━", DEFAULT_BOX_WIDTH - 4)));
250
+ for (const service of stackServices) {
251
+ const statusSymbol = getStatusIcon(service.status);
252
+ const statusEmoji = colorizeByStatus(statusSymbol, service.status);
253
+ const namePart = formatPadded(service.name, 22);
254
+ const urlPart = service.status === "running"
255
+ ? chalk.cyan.underline(service.url)
256
+ : chalk.gray(service.url); // Gray out URL if stopped
257
+ const statusLabel = service.status !== "running" ? chalk.gray(` [${service.status}]`) : "";
258
+ console.log(` ${statusEmoji} ${chalk.white(namePart)} ${urlPart}${statusLabel}`);
259
+ if (service.loopbackUrl) {
260
+ console.log(` ${chalk.gray(`Loopback: ${service.loopbackUrl} (Host: ${new URL(service.url).hostname})`)}`);
261
+ }
262
+ }
234
263
  }
235
- isFirstStack = false;
236
- const emoji = getStackEmoji(stackName);
237
- const stackTitle = `${emoji} ${stackName.toUpperCase()} STACK`;
238
264
  console.log();
239
- console.log(chalk.bold.white(stackTitle));
240
- console.log(chalk.gray(formatBoxLine("━", DEFAULT_BOX_WIDTH - 4)));
241
- for (const service of stackServices) {
242
- const statusSymbol = getStatusIcon(service.status);
243
- const statusEmoji = colorizeByStatus(statusSymbol, service.status);
244
- const namePart = formatPadded(service.name, 22);
245
- const urlPart = service.status === "running"
246
- ? chalk.cyan.underline(service.url)
247
- : chalk.gray(service.url); // Gray out URL if stopped
248
- const statusLabel = service.status !== "running" ? chalk.gray(` [${service.status}]`) : "";
249
- console.log(` ${statusEmoji} ${chalk.white(namePart)} ${urlPart}${statusLabel}`);
250
- if (service.loopbackUrl) {
251
- console.log(` ${chalk.gray(`Loopback: ${service.loopbackUrl} (Host: ${new URL(service.url).hostname})`)}`);
252
- }
265
+ console.log(chalk.gray(formatBoxLine("─", DEFAULT_BOX_WIDTH - 2)));
266
+ console.log(chalk.gray("🖱️ Click any URL above to open in browser"));
267
+ console.log(chalk.gray("📊 Status: ") +
268
+ chalk.green("✓ Running") +
269
+ " | " +
270
+ chalk.red("✗ Stopped") +
271
+ " | " +
272
+ chalk.gray("? Unknown"));
273
+ if (baseDomain === "localhost") {
274
+ console.log();
275
+ const projectConfig = readProjectConfig(projectRoot);
276
+ const projectName = projectConfig.project.name;
277
+ console.log(chalk.yellow("💡 Tip: Set custom domain with:"));
278
+ console.log(chalk.cyan(` export TDK_PUBLIC_HOST=${projectName}.localhost`));
253
279
  }
254
- }
255
- console.log();
256
- console.log(chalk.gray(formatBoxLine("─", DEFAULT_BOX_WIDTH - 2)));
257
- console.log(chalk.gray("🖱️ Click any URL above to open in browser"));
258
- console.log(chalk.gray("📊 Status: ") +
259
- chalk.green("✓ Running") +
260
- " | " +
261
- chalk.red("✗ Stopped") +
262
- " | " +
263
- chalk.gray("? Unknown"));
264
- if (baseDomain === "localhost") {
265
280
  console.log();
266
- const projectConfig = readProjectConfig(projectRoot);
267
- const projectName = projectConfig.project.name;
268
- console.log(chalk.yellow("💡 Tip: Set custom domain with:"));
269
- console.log(chalk.cyan(` export TDK_PUBLIC_HOST=${projectName}.localhost`));
281
+ };
282
+ if (options.json || options.jsonLegacy) {
283
+ try {
284
+ await action();
285
+ }
286
+ catch (error) {
287
+ writeMachineError(error);
288
+ }
289
+ return;
270
290
  }
271
- console.log();
291
+ await action();
272
292
  });
273
293
  //# sourceMappingURL=networks.js.map
@@ -13,7 +13,7 @@ import { showCommandHeader } from "../utils/formatting.js";
13
13
  import { assignPort } from "../utils/port-assignment.js";
14
14
  import { promptSelect, promptText } from "../utils/prompt.js";
15
15
  import { getDefaultFeaturesForResourceType } from "../utils/resource-features.js";
16
- import { SERVICE_MANIFEST_SCHEMA_VERSION } from "../utils/service-manifest.js";
16
+ import { SERVICE_MANIFEST_SCHEMA_URL, SERVICE_MANIFEST_SCHEMA_VERSION, } from "../utils/service-manifest.js";
17
17
  import { discoverResources } from "../utils/services.js";
18
18
  import { createKebabCaseValidator, isPathSafe, validateResourceName } from "../utils/validation.js";
19
19
  export const BASE_TEMPLATE = {
@@ -77,6 +77,7 @@ export function createServiceJson(name, type, stack, port, extraFeatures = [], f
77
77
  }
78
78
  return {
79
79
  ...base,
80
+ $schema: SERVICE_MANIFEST_SCHEMA_URL,
80
81
  schemaVersion: SERVICE_MANIFEST_SCHEMA_VERSION,
81
82
  appName: name,
82
83
  appType: type,
@@ -494,6 +495,7 @@ export const resourceCommand = new Command("resource")
494
495
  // Update with new values while preserving existing fields
495
496
  const updatedServiceJson = {
496
497
  ...existingServiceJson,
498
+ $schema: existingServiceJson.$schema ?? SERVICE_MANIFEST_SCHEMA_URL,
497
499
  schemaVersion: existingServiceJson.schemaVersion ?? SERVICE_MANIFEST_SCHEMA_VERSION,
498
500
  appName: resourceName,
499
501
  appType: resourceType === "sdk" ? "sdk" : existingServiceJson.appType || resourceType,
@@ -525,6 +527,7 @@ export const resourceCommand = new Command("resource")
525
527
  mkdirSync(fullPath, { recursive: true });
526
528
  // Create service.json
527
529
  const byoServiceJson = {
530
+ $schema: SERVICE_MANIFEST_SCHEMA_URL,
528
531
  schemaVersion: SERVICE_MANIFEST_SCHEMA_VERSION,
529
532
  appName: resourceName,
530
533
  appType: "bring-your-own",
@@ -3,35 +3,56 @@ import { Command } from "commander";
3
3
  import { createDiscoveryContext } from "../utils/discovery-context.js";
4
4
  import { requireProjectRoot, runCommand } from "../utils/errors.js";
5
5
  import { formatCount, showAllSatisfyCondition, showDetail, showEmptyState, showStep, } from "../utils/formatting.js";
6
+ import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
7
+ import { findProjectRoot } from "../utils/paths.js";
6
8
  export const resourcesCommand = new Command("resources")
7
9
  .description("List all resources (services) in the project")
8
10
  .option("-v, --verbose", "Show detailed information about each resource", false)
9
11
  .option("-s, --stack <stack>", "Filter resources by stack name")
10
12
  .option("--no-stack", "Show only resources without a stack")
11
13
  .option("--ports", "Show port assignments", false)
14
+ .option("--json", "Output a versioned JSON resource report", false)
12
15
  .action(async (options) => {
13
- await runCommand(async () => {
14
- requireProjectRoot();
15
- const discovery = createDiscoveryContext();
16
- if (discovery.resources.length === 0) {
17
- showEmptyState("resources");
18
- return;
16
+ const action = async () => {
17
+ if (options.json) {
18
+ if (!findProjectRoot()) {
19
+ writeMachineError(new Error("Could not find project root (no .tdk/project.json found)"));
20
+ }
19
21
  }
22
+ else {
23
+ requireProjectRoot();
24
+ }
25
+ const discovery = createDiscoveryContext();
20
26
  let resources = discovery.resources;
21
27
  if (options.stack) {
22
28
  resources = discovery.resourcesByStack.get(options.stack) || [];
23
- if (resources.length === 0) {
29
+ if (resources.length === 0 && !options.json) {
24
30
  showEmptyState("stack-services", ` in stack "${options.stack}"`);
25
31
  return;
26
32
  }
27
33
  }
28
34
  if (options.noStack) {
29
35
  resources = discovery.unassignedResources;
30
- if (resources.length === 0) {
36
+ if (resources.length === 0 && !options.json) {
31
37
  showAllSatisfyCondition("resources", "assigned to a stack");
32
38
  return;
33
39
  }
34
40
  }
41
+ if (options.json) {
42
+ const data = resources.map((resource) => ({
43
+ name: resource.name,
44
+ stack: resource.stack ?? null,
45
+ type: resource.config?.appType ?? resource.type ?? "unknown",
46
+ port: resource.port ?? null,
47
+ path: resource.configPath,
48
+ }));
49
+ console.log(JSON.stringify(createMachineEnvelope({ resources: data })));
50
+ return;
51
+ }
52
+ if (resources.length === 0) {
53
+ showEmptyState(options.stack ? "stack-services" : "resources", options.stack ? ` in stack "${options.stack}"` : "");
54
+ return;
55
+ }
35
56
  showStep(`Found ${formatCount(resources.length, "resource")}:\n`);
36
57
  if (options.verbose || options.ports) {
37
58
  for (const resource of resources) {
@@ -68,6 +89,16 @@ export const resourcesCommand = new Command("resources")
68
89
  console.log(chalk.yellow(`\n${formatCount(withoutStackCount, "resource")} not assigned to any stack.`));
69
90
  showDetail('Run "tdk resources --no-stack" to see them, or "tdk stack" to assign them.');
70
91
  }
71
- });
92
+ };
93
+ if (options.json) {
94
+ try {
95
+ await action();
96
+ }
97
+ catch (error) {
98
+ writeMachineError(error);
99
+ }
100
+ return;
101
+ }
102
+ await runCommand(action);
72
103
  });
73
104
  //# sourceMappingURL=resources.js.map
@@ -3,15 +3,17 @@ import { Command } from "commander";
3
3
  import { createDiscoveryContext } from "../utils/discovery-context.js";
4
4
  import { runCommand } from "../utils/errors.js";
5
5
  import { formatCount, showDetail, showStep } from "../utils/formatting.js";
6
+ import { createMachineEnvelope, writeMachineError } from "../utils/machine-output.js";
6
7
  import { getTiltfilePath, isTiltAvailable, runTilt } from "../utils/tilt.js";
7
8
  export const statusCommand = new Command("status")
8
9
  .description("Show status of resources and stacks")
10
+ .option("--json", "Output a versioned JSON status report", false)
9
11
  .option("-v, --verbose", "Show detailed information", false)
10
12
  .option("--stacks", "Show stack information (default)", true)
11
13
  .option("--resources", "Show all discovered resources", false)
12
14
  .option("--tilt", "Show tilt resource status", false)
13
15
  .action(async (options) => {
14
- await runCommand(async () => {
16
+ const action = async () => {
15
17
  let tiltAvailable = false;
16
18
  try {
17
19
  tiltAvailable = await isTiltAvailable();
@@ -20,13 +22,59 @@ export const statusCommand = new Command("status")
20
22
  // Tilt not available (spawn error)
21
23
  tiltAvailable = false;
22
24
  }
23
- showStep("TDK Status\n");
24
- console.log(chalk.bold("Tilt:"), tiltAvailable ? chalk.green("available") : chalk.red("not found"));
25
- if (!tiltAvailable) {
26
- showDetail("Install Tilt: https://docs.tilt.dev/install.html");
25
+ if (!options.json) {
26
+ showStep("TDK Status\n");
27
+ console.log(chalk.bold("Tilt:"), tiltAvailable ? chalk.green("available") : chalk.red("not found"));
28
+ if (!tiltAvailable) {
29
+ showDetail("Install Tilt: https://docs.tilt.dev/install.html");
30
+ }
31
+ console.log();
27
32
  }
28
- console.log();
29
33
  const discovery = createDiscoveryContext();
34
+ let tiltResources = null;
35
+ let queryError = null;
36
+ if (options.json && options.tilt && tiltAvailable) {
37
+ const result = await runTilt("get", ["-f", getTiltfilePath(), "resources", "--output=json"], {
38
+ inheritStdio: false,
39
+ });
40
+ if (result.exitCode === 0) {
41
+ try {
42
+ tiltResources = JSON.parse(result.stdout);
43
+ }
44
+ catch (error) {
45
+ queryError = `Tilt returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`;
46
+ }
47
+ }
48
+ else {
49
+ queryError = result.stderr || "Could not retrieve Tilt resource status";
50
+ }
51
+ }
52
+ if (options.json) {
53
+ const errors = queryError ? [{ code: "TILT_STATUS_UNAVAILABLE", message: queryError }] : [];
54
+ const data = {
55
+ tilt: {
56
+ available: tiltAvailable,
57
+ resourcesQueried: options.tilt && tiltAvailable,
58
+ resources: tiltResources,
59
+ },
60
+ resources: discovery.resources.map((resource) => ({
61
+ name: resource.name,
62
+ stack: resource.stack ?? null,
63
+ type: resource.type ?? "unknown",
64
+ port: resource.port ?? null,
65
+ })),
66
+ stacks: discovery.stacks.map((stack) => ({
67
+ name: stack.name,
68
+ resourceCount: stack.resourceCount,
69
+ })),
70
+ };
71
+ console.log(JSON.stringify(createMachineEnvelope(data, errors)));
72
+ if (errors.length > 0) {
73
+ console.error(queryError);
74
+ process.exit(1);
75
+ }
76
+ return;
77
+ }
30
78
  console.log(chalk.bold("Resources:"), `${discovery.resources.length} discovered`);
31
79
  console.log(chalk.bold("Stacks:"), `${discovery.stacks.length} defined`);
32
80
  if (discovery.stacks.length > 0) {
@@ -66,6 +114,16 @@ export const statusCommand = new Command("status")
66
114
  console.log();
67
115
  showDetail('Run "tdk list-stacks" to see all stacks.');
68
116
  showDetail('Run "tdk up <stack-name>" to start a stack.');
69
- });
117
+ };
118
+ if (options.json) {
119
+ try {
120
+ await action();
121
+ }
122
+ catch (error) {
123
+ writeMachineError(error);
124
+ }
125
+ return;
126
+ }
127
+ await runCommand(action);
70
128
  });
71
129
  //# sourceMappingURL=status.js.map
@@ -109,6 +109,7 @@ async function fetchPremium(key, resource, projectId) {
109
109
  return fetch(`${url}&key=${encodeURIComponent(key)}`);
110
110
  }
111
111
  async function fetchPremiumBundle(key, projectId) {
112
+ const attempts = [];
112
113
  for (const resource of KNOWN_RESOURCES) {
113
114
  let res;
114
115
  try {
@@ -119,18 +120,23 @@ async function fetchPremiumBundle(key, projectId) {
119
120
  return null;
120
121
  }
121
122
  if (res.ok) {
122
- return Buffer.from(await res.arrayBuffer());
123
+ const bundle = Buffer.from(await res.arrayBuffer());
124
+ console.info(`🔎 Premium entitlement probe: granted resource=${resource} (HTTP ${res.status}); prior denials=${attempts.join(", ") || "none"}`);
125
+ console.info(`🔎 Premium bundle download: ${bundle.byteLength} bytes`);
126
+ return bundle;
123
127
  }
124
128
  if (res.status === 401 || res.status === 403) {
125
129
  // Not granted for this specific resource name - try the next one
126
130
  // before concluding the key has nothing to offer.
131
+ attempts.push(`${resource}=${res.status}`);
127
132
  continue;
128
133
  }
129
134
  // Anything else (404 missing bundle release, 502 upstream failure,
130
135
  // etc.) won't be fixed by trying a different resource name.
131
- console.warn(`⚠️ Premium fetch failed: HTTP ${res.status}`);
136
+ console.warn(`⚠️ Premium fetch failed: ${resource} returned HTTP ${res.status}; prior entitlement probes: ${attempts.join(", ") || "none"}`);
132
137
  return null;
133
138
  }
139
+ console.warn(`⚠️ Premium entitlement probes denied: ${attempts.join(", ")}`);
134
140
  return null;
135
141
  }
136
142
  /**
@@ -146,6 +152,7 @@ export async function applyPremiumOverlay(projectRoot, destDir) {
146
152
  return false;
147
153
  const tarballPath = cacheTarballPath(key);
148
154
  if (!isCacheFresh(tarballPath)) {
155
+ console.info("🔎 Premium bundle cache: miss; requesting bundle from distribution service");
149
156
  const projectId = getOrCreateProjectId(projectRoot);
150
157
  const bundle = await fetchPremiumBundle(key, projectId);
151
158
  if (!bundle) {
@@ -155,20 +162,35 @@ export async function applyPremiumOverlay(projectRoot, destDir) {
155
162
  mkdirSync(join(tarballPath, ".."), { recursive: true });
156
163
  writeFileSync(tarballPath, bundle);
157
164
  }
165
+ else {
166
+ console.info("🔎 Premium bundle cache: hit (cached bundle reused)");
167
+ }
158
168
  try {
159
169
  const extractDir = join(tarballPath, "..", "extracted");
160
170
  mkdirSync(extractDir, { recursive: true });
161
171
  extractTarball(readFileSync(tarballPath), extractDir);
162
172
  let applied = 0;
173
+ const missing = [];
163
174
  for (const [src, dest] of Object.entries(PREMIUM_PATH_MAP)) {
164
175
  const from = join(extractDir, src);
165
- if (!existsSync(from))
176
+ if (!existsSync(from)) {
177
+ missing.push(src);
166
178
  continue;
179
+ }
167
180
  const to = join(destDir, dest);
168
181
  mkdirSync(join(to, ".."), { recursive: true });
169
182
  writeFileSync(to, readFileSync(from));
170
183
  applied++;
171
184
  }
185
+ const sablierPath = join(extractDir, "networking/sablier_container_cycle.star");
186
+ console.info(`🔎 Premium bundle contents: mapped=${applied}/${Object.keys(PREMIUM_PATH_MAP).length}; sablier_overlay=${existsSync(sablierPath) ? "present" : "MISSING"}`);
187
+ if (missing.length > 0) {
188
+ console.info(`🔎 Premium bundle missing mapped files: ${missing.join(", ")}`);
189
+ }
190
+ if (!existsSync(sablierPath) && process.env.TDK_PREMIUM_DIAGNOSTICS === "1") {
191
+ const sablierGranted = await hasSablierLicense(projectRoot);
192
+ console.info(`🔎 Sablier entitlement check: ${sablierGranted ? "granted" : "not confirmed"}; compare with sablier_overlay above to distinguish license access from bundle contents`);
193
+ }
172
194
  if (applied > 0) {
173
195
  console.log(`✓ Premium resources unlocked (${applied} file${applied === 1 ? "" : "s"})`);
174
196
  return true;
@@ -211,6 +233,7 @@ async function hasLiveResourceLicense(projectRoot, resource) {
211
233
  if (isCacheFresh(cachePath)) {
212
234
  try {
213
235
  const cached = JSON.parse(readFileSync(cachePath, "utf-8"));
236
+ console.info(`🔎 ${resource} entitlement probe: cache=${cached.granted === true ? "granted" : "denied"}`);
214
237
  return cached.granted === true;
215
238
  }
216
239
  catch {
@@ -222,6 +245,7 @@ async function hasLiveResourceLicense(projectRoot, resource) {
222
245
  try {
223
246
  const res = await fetchPremium(key, resource, projectId);
224
247
  granted = res.ok;
248
+ console.info(`🔎 ${resource} entitlement probe: HTTP ${res.status} (${granted ? "granted" : "denied"})`);
225
249
  // Drain the body (a full premium.tar.gz on success) so we don't leave
226
250
  // it dangling - we only need the status, not the content.
227
251
  if (res.body) {
@@ -603,6 +603,16 @@ async function vendorTdkExtension(projectRoot) {
603
603
  export function verifyMasterConfigs(projectRoot) {
604
604
  const errors = [];
605
605
  const warnings = [];
606
+ const diffs = [];
607
+ const addDrift = (displayPath, actual, expected) => {
608
+ if (actual === expected)
609
+ return;
610
+ errors.push(`${actual === undefined ? "Missing file" : "Out of sync"}: ${displayPath} (run 'tdk config regenerate')`);
611
+ diffs.push({
612
+ file: displayPath,
613
+ diff: createUnifiedDiff(displayPath, actual ?? "", expected),
614
+ });
615
+ };
606
616
  const projectConfig = readProjectConfig(projectRoot);
607
617
  const engine = new TemplateEngine();
608
618
  const expectedFiles = engine.generateAll(projectConfig);
@@ -610,22 +620,10 @@ export function verifyMasterConfigs(projectRoot) {
610
620
  for (const filename of ALL_GENERATED_FILES) {
611
621
  const expectedContent = expectedFiles[filename];
612
622
  const filePath = path.join(outputDir, filename);
613
- if (!fs.existsSync(filePath)) {
614
- errors.push(`Missing file: .tdk/.tdk-out/${filename}`);
615
- continue;
616
- }
617
- const actualContent = fs.readFileSync(filePath, "utf-8");
618
- if (actualContent !== expectedContent) {
619
- errors.push(`Out of sync: .tdk/.tdk-out/${filename} (run 'tdk config regenerate')`);
620
- }
623
+ addDrift(`.tdk/.tdk-out/${filename}`, fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : undefined, expectedContent);
621
624
  }
622
625
  const rootTiltIgnore = path.join(projectRoot, ".tiltignore");
623
- if (!fs.existsSync(rootTiltIgnore)) {
624
- errors.push("Missing file: .tiltignore");
625
- }
626
- else if (fs.readFileSync(rootTiltIgnore, "utf-8") !== expectedFiles[".tiltignore"]) {
627
- errors.push("Out of sync: .tiltignore (run 'tdk config regenerate')");
628
- }
626
+ addDrift(".tiltignore", fs.existsSync(rootTiltIgnore) ? fs.readFileSync(rootTiltIgnore, "utf-8") : undefined, expectedFiles[".tiltignore"]);
629
627
  for (const manifestPath of discoverServiceManifestPaths(projectRoot)) {
630
628
  const displayPath = path.relative(projectRoot, manifestPath).split(path.sep).join("/");
631
629
  const validation = validateServiceManifestFile(manifestPath, displayPath);
@@ -633,7 +631,21 @@ export function verifyMasterConfigs(projectRoot) {
633
631
  warnings.push(...validation.warnings);
634
632
  errors.push(...verifyGeneratedResourceFiles(projectRoot, manifestPath));
635
633
  }
636
- return { valid: errors.length === 0, errors, warnings };
634
+ return { valid: errors.length === 0, errors, warnings, diffs };
635
+ }
636
+ function createUnifiedDiff(file, actual, expected) {
637
+ const oldLines = actual.replace(/\n$/, "").split("\n");
638
+ const newLines = expected.replace(/\n$/, "").split("\n");
639
+ const lines = [
640
+ `--- a/${file}`,
641
+ `+++ b/${file}`,
642
+ `@@ -1,${oldLines.length} +1,${newLines.length} @@`,
643
+ ];
644
+ for (const line of oldLines)
645
+ lines.push(`-${line}`);
646
+ for (const line of newLines)
647
+ lines.push(`+${line}`);
648
+ return lines.join("\n");
637
649
  }
638
650
  function verifyGeneratedResourceFiles(projectRoot, serviceJsonPath) {
639
651
  const serviceDir = path.dirname(serviceJsonPath);