@tailor-platform/sdk 1.56.1 → 1.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +0 -23
  3. package/dist/{application-DuT_ae02.mjs → application-B59TaTk_.mjs} +10 -6
  4. package/dist/application-B59TaTk_.mjs.map +1 -0
  5. package/dist/application-gO_pa5BO.mjs +4 -0
  6. package/dist/cli/erd-viewer-assets/app.js +1181 -0
  7. package/dist/cli/erd-viewer-assets/index.html +73 -0
  8. package/dist/cli/erd-viewer-assets/serve.json +13 -0
  9. package/dist/cli/erd-viewer-assets/styles.css +789 -0
  10. package/dist/cli/index.mjs +847 -355
  11. package/dist/cli/index.mjs.map +1 -1
  12. package/dist/cli/lib.d.mts +587 -3
  13. package/dist/cli/lib.mjs +3 -3
  14. package/dist/{client-DLPEPJ_s.mjs → client-62B-r3MN.mjs} +39 -10
  15. package/dist/client-62B-r3MN.mjs.map +1 -0
  16. package/dist/{client-DrzwCD1W.mjs → client-BWl3f1XS.mjs} +1 -1
  17. package/dist/configure/index.d.mts +2 -2
  18. package/dist/{crashreport-Bm2mN5tg.mjs → crashreport-CCGpLUlP.mjs} +2 -2
  19. package/dist/{crashreport-Bm2mN5tg.mjs.map → crashreport-CCGpLUlP.mjs.map} +1 -1
  20. package/dist/{crashreport-C5oHvHUC.mjs → crashreport-CXD_Kjk-.mjs} +1 -1
  21. package/dist/{index-B61gFI9a.d.mts → index-BWoHfE-i.d.mts} +3 -2
  22. package/dist/{runtime-745lvg7i.mjs → runtime-BC-FbQkg.mjs} +179 -190
  23. package/dist/runtime-BC-FbQkg.mjs.map +1 -0
  24. package/dist/utils/test/index.d.mts +2 -2
  25. package/dist/{workflow.generated-Kz-nQrTf.d.mts → workflow.generated-CV77NlFp.d.mts} +3 -2
  26. package/docs/cli/application.md +5 -5
  27. package/docs/cli/auth.md +55 -6
  28. package/docs/cli/function.md +2 -2
  29. package/docs/cli/staticwebsite.md +137 -0
  30. package/docs/cli/tailordb.md +31 -26
  31. package/docs/cli-reference.md +19 -16
  32. package/docs/generator/builtin.md +1 -1
  33. package/docs/services/auth.md +0 -11
  34. package/docs/services/staticwebsite.md +13 -0
  35. package/package.json +4 -6
  36. package/dist/application-CC3oaSay.mjs +0 -4
  37. package/dist/application-DuT_ae02.mjs.map +0 -1
  38. package/dist/client-DLPEPJ_s.mjs.map +0 -1
  39. package/dist/runtime-745lvg7i.mjs.map +0 -1
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { G as PATScope, R as AuthInvokerSchema, c as fetchUserInfo, d as initOperatorClient, h as userAgent, i as fetchAll, j as FunctionExecution_Type, n as closeConnectionPool, o as fetchPaged, s as fetchPlatformMachineUserToken, u as initOAuth2Client } from "../client-DLPEPJ_s.mjs";
3
+ import { E as CustomDomainStatus, K as PATScope, M as FunctionExecution_Type, c as fetchUserInfo, d as initOperatorClient, h as userAgent, i as fetchAll, n as closeConnectionPool, o as fetchPaged, s as fetchPlatformMachineUserToken, u as initOAuth2Client, z as AuthInvokerSchema } from "../client-62B-r3MN.mjs";
4
4
  import { n as logger, r as styles } from "../logger-DpJyJvNz.mjs";
5
- import { $ as listCommand$10, An as workspaceArgs, At as startCommand, B as logBetaWarning, C as listCommand$13, Cn as configArg, Dn as pagedLogArgs, Dt as jobsCommand, E as resumeCommand, En as isVerbose, F as writeDbTypesFile, Gt as parseMigrationLabelNumber, H as removeCommand$1, Ht as executeScript, I as getConfiguredEditorCommand, K as treeCommand, L as openInConfiguredEditor, Lt as functionExecutionStatusToString, Mt as getCommand$6, N as generateCommand$1, O as listCommand$12, On as paginationArgs, P as generateMigrationScript, Pt as executionsCommand, Rt as formatKeyValueTable, Sn as commonArgs, St as triggerCommand, T as healthCommand, Tn as deploymentArgs, U as updateCommand$3, Vt as deploy, Y as getCommand$5, Yt as INITIAL_SCHEMA_NUMBER, Z as updateCommand$2, _n as prompt, at as createCommand$3, b as createCommand$4, bn as assertWritable, c as listCommand$14, cn as reconstructSnapshotFromMigrations, f as restoreCommand, ft as tokenCommand, g as getCommand$7, gt as listCommand$7, hn as trnPrefix, ht as generate, i as updateCommand$4, in as getMigrationFiles, j as truncateCommand, kn as toPageDirection, ln as formatMigrationNumber, lt as getCommand$3, m as listCommand$15, mn as sdkNameLabelKey, o as removeCommand, on as isValidMigrationNumber, pn as getNamespacesWithMigrations, pt as listCommand$8, q as listCommand$11, r as queryCommand, rn as getMigrationFilePath, rt as deleteCommand$3, sn as loadDiff, st as listCommand$9, t as isNativeTypeScriptRuntime, tt as getCommand$4, u as inviteCommand, v as deleteCommand$4, vn as apiCommand, vt as getCommand$2, wn as confirmationArgs, wt as listCommand$6, xn as defineAppCommand, xt as webhookCommand, z as showCommand, zt as getCommand$1 } from "../runtime-745lvg7i.mjs";
6
- import { A as saveUserTokens, C as deleteUserTokens, D as loadWorkspaceId, O as readPlatformConfig, S as loadConfig, T as loadAccessToken, _ as createLogLevelTreeshakeOptions, a as WorkflowJobSchema, b as getDistDir, g as composeFunctionTreeshakeOptions, i as resolveInlineSourcemap, j as writePlatformConfig, k as resolveTokens, l as ExecutorSchema, o as ResolverSchema, u as INVOKER_EXPR, v as resolveBundleLogLevel, w as fetchLatestToken } from "../application-DuT_ae02.mjs";
5
+ import { $ as listCommand$10, An as toPageDirection, At as startCommand, B as logBetaWarning, C as listCommand$13, Cn as commonArgs, Dn as isVerbose, Dt as jobsCommand, E as resumeCommand, En as deploymentArgs, F as writeDbTypesFile, Gt as parseMigrationLabelNumber, H as removeCommand$1, Ht as executeScript, I as getConfiguredEditorCommand, K as treeCommand, L as openInConfiguredEditor, Lt as functionExecutionStatusToString, Mt as getCommand$6, N as generateCommand$1, O as listCommand$12, On as pagedLogArgs, P as generateMigrationScript, Pt as executionsCommand, Rt as formatKeyValueTable, Sn as defineAppCommand, St as triggerCommand, T as healthCommand, Tn as confirmationArgs, U as updateCommand$3, Vt as deploy, Y as getCommand$5, Yt as INITIAL_SCHEMA_NUMBER, Z as updateCommand$2, _n as generateUserTypes, at as createCommand$3, b as createCommand$4, c as listCommand$14, cn as reconstructSnapshotFromMigrations, f as restoreCommand, ft as tokenCommand, g as getCommand$7, gn as PluginManager, gt as listCommand$7, hn as sdkNameLabelKey, ht as generate, i as updateCommand$4, in as getMigrationFiles, j as truncateCommand, jn as workspaceArgs, kn as paginationArgs, ln as formatMigrationNumber, lt as getCommand$3, m as listCommand$15, mn as resourceTrn, o as removeCommand, on as isValidMigrationNumber, pn as getNamespacesWithMigrations, pt as listCommand$8, q as listCommand$11, r as queryCommand, rn as getMigrationFilePath, rt as deleteCommand$3, sn as loadDiff, st as listCommand$9, t as isNativeTypeScriptRuntime, tt as getCommand$4, u as inviteCommand, v as deleteCommand$4, vn as prompt, vt as getCommand$2, wn as configArg, wt as listCommand$6, xn as assertWritable, xt as webhookCommand, yn as apiCommand, z as showCommand, zt as getCommand$1 } from "../runtime-BC-FbQkg.mjs";
6
+ import { A as resolveTokens, C as loadConfig, E as loadAccessToken, M as writePlatformConfig, O as loadWorkspaceId, T as fetchLatestToken, _ as createLogLevelTreeshakeOptions, a as WorkflowJobSchema, b as getDistDir, g as composeFunctionTreeshakeOptions, i as resolveInlineSourcemap, j as saveUserTokens, k as readPlatformConfig, l as ExecutorSchema, o as ResolverSchema, t as defineApplication, u as INVOKER_EXPR, v as resolveBundleLogLevel, w as deleteUserTokens, x as hashContent } from "../application-B59TaTk_.mjs";
7
7
  import { t as multiline } from "../multiline-Cf9ODpr1.mjs";
8
+ import { r as isPluginGeneratedType } from "../seed-C0fE2sJB.mjs";
8
9
  import { t as readPackageJson } from "../package-json-DcQApfPQ.mjs";
9
10
  import { n as isCLIError } from "../errors-EsY4XO6O.mjs";
10
- import { a as JSON_FOOTER_MARKER, i as CRASH_LOG_EXTENSION, o as parseCrashReportConfig, r as sendCrashReport, t as initCrashReporting } from "../crashreport-Bm2mN5tg.mjs";
11
- import { createRequire } from "node:module";
11
+ import { a as JSON_FOOTER_MARKER, i as CRASH_LOG_EXTENSION, o as parseCrashReportConfig, r as sendCrashReport, t as initCrashReporting } from "../crashreport-CCGpLUlP.mjs";
12
12
  import { arg, defineCommand, runCommand, runMain } from "politty";
13
13
  import { withCompletionCommand } from "politty/completion";
14
14
  import { z } from "zod";
@@ -17,7 +17,7 @@ import * as fs$1 from "node:fs";
17
17
  import { timestampDate } from "@bufbuild/protobuf/wkt";
18
18
  import * as path from "pathe";
19
19
  import { dirname, resolve } from "pathe";
20
- import { pathToFileURL } from "node:url";
20
+ import { fileURLToPath, pathToFileURL } from "node:url";
21
21
  import { generateCodeVerifier } from "@badgateway/oauth2-client";
22
22
  import { Code, ConnectError } from "@connectrpc/connect";
23
23
  import { resolvePackageJSON, resolveTSConfig } from "pkg-types";
@@ -26,9 +26,11 @@ import * as http from "node:http";
26
26
  import open from "open";
27
27
  import * as rolldown from "rolldown";
28
28
  import * as fsPromises from "node:fs/promises";
29
+ import { glob } from "node:fs/promises";
29
30
  import pLimit from "p-limit";
30
31
  import { TraceMap, generatedPositionFor, originalPositionFor } from "@jridgewell/trace-mapping";
31
32
  import { spawn, spawnSync } from "node:child_process";
33
+ import { watch } from "chokidar";
32
34
  import * as fs from "fs";
33
35
  import { lookup } from "mime-types";
34
36
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
@@ -159,6 +161,45 @@ const authorizeAuthConnectionCommand = defineAppCommand({
159
161
  }
160
162
  });
161
163
 
164
+ //#endregion
165
+ //#region src/cli/commands/authconnection/delete.ts
166
+ const deleteAuthConnectionCommand = defineAppCommand({
167
+ name: "delete",
168
+ description: "Delete an auth connection entirely.",
169
+ args: z.object({
170
+ ...workspaceArgs,
171
+ ...connectionNameArgs,
172
+ ...confirmationArgs
173
+ }).strict(),
174
+ run: async (args) => {
175
+ await assertWritable({ profile: args.profile });
176
+ const client = await initOperatorClient(await loadAccessToken({
177
+ useProfile: true,
178
+ profile: args.profile
179
+ }));
180
+ const workspaceId = await loadWorkspaceId({
181
+ workspaceId: args["workspace-id"],
182
+ profile: args.profile
183
+ });
184
+ if (!args.yes) {
185
+ if (await prompt.text({ message: `Enter the connection name to confirm deletion ("${args.name}"):` }) !== args.name) {
186
+ logger.info("Auth connection deletion cancelled.");
187
+ return;
188
+ }
189
+ }
190
+ try {
191
+ await client.deleteAuthConnection({
192
+ workspaceId,
193
+ connectionName: args.name
194
+ });
195
+ } catch (error) {
196
+ if (error instanceof ConnectError && error.code === Code.NotFound) throw new Error(`Auth connection "${args.name}" not found.`, { cause: error });
197
+ throw error;
198
+ }
199
+ logger.success(`Auth connection "${args.name}" deleted.`);
200
+ }
201
+ });
202
+
162
203
  //#endregion
163
204
  //#region src/cli/commands/authconnection/list.ts
164
205
  function connectionInfo(connection) {
@@ -216,7 +257,8 @@ const listAuthConnectionCommand = defineAppCommand({
216
257
  //#region src/cli/commands/authconnection/revoke.ts
217
258
  const revokeAuthConnectionCommand = defineAppCommand({
218
259
  name: "revoke",
219
- description: "Revoke an auth connection.",
260
+ description: "Revoke an auth connection's tokens (keeps the connection; use 'delete' to remove it).",
261
+ notes: "Revoke invalidates the connection's active session and tokens but keeps the connection and its stored credentials, so it can be re-authorized later. Use `delete` to remove the connection entirely.",
220
262
  args: z.object({
221
263
  ...workspaceArgs,
222
264
  ...connectionNameArgs,
@@ -259,7 +301,8 @@ const authconnectionCommand = defineCommand({
259
301
  subCommands: {
260
302
  authorize: authorizeAuthConnectionCommand,
261
303
  list: listAuthConnectionCommand,
262
- revoke: revokeAuthConnectionCommand
304
+ revoke: revokeAuthConnectionCommand,
305
+ delete: deleteAuthConnectionCommand
263
306
  },
264
307
  async run() {
265
308
  await runCommand(listAuthConnectionCommand, []);
@@ -945,9 +988,9 @@ async function downloadScriptForMapping(options) {
945
988
  const logsCommand = defineAppCommand({
946
989
  name: "logs",
947
990
  description: "List or get function execution logs.",
948
- notes: `When viewing a specific execution that failed, the command displays error details with the stack trace mapped back to original source files via the inline sourcemap (clickable file links and code snippets, matching \`function test-run\` output).
991
+ notes: `When viewing a specific execution that failed, the command displays error details with the stack trace mapped back to your original source files (clickable file links and code snippets, matching \`function test-run\` output).
949
992
 
950
- The download is pinned to the bundle that actually ran using the execution's content hash, so stack traces stay accurate across redeploys when the server retains old bundles. The command falls back to a plain-text error display when the pinned bundle cannot be retrieved, or when the execution was recorded before content hashes started being tracked and the function was redeployed after it ran.`,
993
+ Stack traces stay accurate even after later redeploys, because the trace is resolved against the exact build that produced the execution. If that build is no longer available, the command falls back to a plain-text error display.`,
951
994
  examples: [
952
995
  {
953
996
  cmd: "",
@@ -2082,7 +2125,7 @@ const secretValueArgs = {
2082
2125
  */
2083
2126
  async function checkVaultManaged(params) {
2084
2127
  const { client, workspaceId, vaultName } = params;
2085
- const trn = `${trnPrefix(workspaceId)}:vault:${vaultName}`;
2128
+ const trn = resourceTrn(workspaceId, "vault", vaultName);
2086
2129
  const notManaged = {
2087
2130
  isManaged: false,
2088
2131
  trn,
@@ -2982,6 +3025,115 @@ const deployCommand = defineAppCommand({
2982
3025
  }
2983
3026
  });
2984
3027
 
3028
+ //#endregion
3029
+ //#region src/cli/commands/staticwebsite/domain/status.ts
3030
+ const statusLabels = {
3031
+ [CustomDomainStatus.UNSPECIFIED]: "unspecified",
3032
+ [CustomDomainStatus.PENDING]: "pending",
3033
+ [CustomDomainStatus.VERIFYING]: "verifying",
3034
+ [CustomDomainStatus.CERT_ISSUED]: "cert_issued",
3035
+ [CustomDomainStatus.ACTIVE]: "active",
3036
+ [CustomDomainStatus.FAILED]: "failed"
3037
+ };
3038
+
3039
+ //#endregion
3040
+ //#region src/cli/commands/staticwebsite/domain/get.ts
3041
+ const domainGetCommand = defineAppCommand({
3042
+ name: "get",
3043
+ description: "Get details of a custom domain.",
3044
+ args: z.object({
3045
+ ...workspaceArgs,
3046
+ domain: arg(z.string(), {
3047
+ positional: true,
3048
+ description: "Custom domain name"
3049
+ })
3050
+ }).strict(),
3051
+ run: async (args) => {
3052
+ const client = await initOperatorClient(await loadAccessToken({
3053
+ useProfile: true,
3054
+ profile: args.profile
3055
+ }));
3056
+ const workspaceId = await loadWorkspaceId({
3057
+ workspaceId: args["workspace-id"],
3058
+ profile: args.profile
3059
+ });
3060
+ const notFoundErrorMessage = `Custom domain "${args.domain}" not found.`;
3061
+ try {
3062
+ const { customDomain } = await client.getCustomDomain({
3063
+ workspaceId,
3064
+ domain: args.domain
3065
+ });
3066
+ if (!customDomain) throw new Error(notFoundErrorMessage);
3067
+ const info = {
3068
+ domain: customDomain.domain,
3069
+ status: statusLabels[customDomain.status] ?? "unknown",
3070
+ trafficCnameTarget: customDomain.trafficCnameTarget,
3071
+ certificateCnameTarget: customDomain.certificateCnameTarget,
3072
+ errorMessage: customDomain.errorMessage || void 0
3073
+ };
3074
+ logger.out(info);
3075
+ } catch (error) {
3076
+ if (error instanceof ConnectError && error.code === Code.NotFound) throw new Error(notFoundErrorMessage, { cause: error });
3077
+ throw error;
3078
+ }
3079
+ }
3080
+ });
3081
+
3082
+ //#endregion
3083
+ //#region src/cli/commands/staticwebsite/domain/list.ts
3084
+ const domainListCommand = defineAppCommand({
3085
+ name: "list",
3086
+ description: "List custom domains for a static website.",
3087
+ args: z.object({
3088
+ ...workspaceArgs,
3089
+ name: arg(z.string(), {
3090
+ positional: true,
3091
+ description: "Static website name"
3092
+ })
3093
+ }).strict(),
3094
+ run: async (args) => {
3095
+ const client = await initOperatorClient(await loadAccessToken({
3096
+ useProfile: true,
3097
+ profile: args.profile
3098
+ }));
3099
+ const workspaceId = await loadWorkspaceId({
3100
+ workspaceId: args["workspace-id"],
3101
+ profile: args.profile
3102
+ });
3103
+ try {
3104
+ const { customDomains } = await client.listCustomDomains({
3105
+ workspaceId,
3106
+ staticWebsiteName: args.name
3107
+ });
3108
+ if (customDomains.length === 0) {
3109
+ logger.info("No custom domains found.");
3110
+ return;
3111
+ }
3112
+ const formatted = customDomains.map((d) => ({
3113
+ domain: d.domain,
3114
+ status: statusLabels[d.status] ?? "unknown",
3115
+ trafficCnameTarget: d.trafficCnameTarget,
3116
+ certificateCnameTarget: d.certificateCnameTarget
3117
+ }));
3118
+ logger.out(formatted);
3119
+ } catch (error) {
3120
+ if (error instanceof ConnectError && error.code === Code.NotFound) throw new Error(`Static website "${args.name}" not found.`, { cause: error });
3121
+ throw error;
3122
+ }
3123
+ }
3124
+ });
3125
+
3126
+ //#endregion
3127
+ //#region src/cli/commands/staticwebsite/domain/index.ts
3128
+ const domainCommand = defineCommand({
3129
+ name: "domain",
3130
+ description: "Manage custom domains for static websites.",
3131
+ subCommands: {
3132
+ list: domainListCommand,
3133
+ get: domainGetCommand
3134
+ }
3135
+ });
3136
+
2985
3137
  //#endregion
2986
3138
  //#region src/cli/commands/staticwebsite/get.ts
2987
3139
  const getCommand = defineAppCommand({
@@ -3096,6 +3248,7 @@ const staticwebsiteCommand = defineCommand({
3096
3248
  description: "Manage static websites in your workspace.",
3097
3249
  subCommands: {
3098
3250
  deploy: deployCommand,
3251
+ domain: domainCommand,
3099
3252
  list: listCommand$2,
3100
3253
  get: getCommand
3101
3254
  },
@@ -3105,378 +3258,430 @@ const staticwebsiteCommand = defineCommand({
3105
3258
  });
3106
3259
 
3107
3260
  //#endregion
3108
- //#region src/cli/shared/resolve-cli-bin.ts
3261
+ //#region src/cli/commands/tailordb/erd/local-schema.ts
3109
3262
  /**
3110
- * Resolve a CLI binary path from the SDK's dependencies.
3111
- * @param options - Resolution options for locating the CLI binary.
3112
- * @returns Absolute path to the CLI binary entry.
3263
+ * Resolve TailorDB namespaces that need local type loading for ERD generation.
3264
+ * @param config - Loaded Tailor config.
3265
+ * @param options - Namespace selection options.
3266
+ * @returns Namespace names to load, or undefined to load all owned namespaces.
3113
3267
  */
3114
- function resolveCliBinPath(options) {
3115
- const { packageName, binName } = options;
3116
- const requireFromSdk = createRequire(import.meta.url);
3117
- let pkgJsonPath;
3118
- try {
3119
- pkgJsonPath = requireFromSdk.resolve(`${packageName}/package.json`);
3120
- } catch {
3121
- throw new Error(`Failed to resolve \`${packageName}\`.`);
3268
+ function resolveLocalErdSchemaNamespaces(config, options) {
3269
+ if (options.namespaces) return options.namespaces;
3270
+ if (!options.requireErdSite) return;
3271
+ return Object.entries(config.db ?? {}).flatMap(([namespace, dbConfig]) => "external" in dbConfig || !dbConfig.erdSite ? [] : [namespace]);
3272
+ }
3273
+ /**
3274
+ * Load local TailorDB namespaces exactly as SDK generation/deploy sees them.
3275
+ * @param options - Local schema loading options.
3276
+ * @returns Loaded TailorDB namespace data.
3277
+ */
3278
+ async function loadLocalErdSchema(options) {
3279
+ const { config, plugins } = await loadConfig(options.configPath);
3280
+ await generateUserTypes({
3281
+ config,
3282
+ configPath: config.path
3283
+ });
3284
+ const application = defineApplication({
3285
+ config,
3286
+ pluginManager: plugins.length > 0 ? new PluginManager(plugins) : void 0
3287
+ });
3288
+ const namespaceNames = resolveLocalErdSchemaNamespaces(config, {
3289
+ namespaces: options.namespaces,
3290
+ requireErdSite: options.requireErdSite
3291
+ });
3292
+ const namespaceFilter = namespaceNames ? new Set(namespaceNames) : void 0;
3293
+ const services = namespaceFilter ? application.tailorDBServices.filter((db) => namespaceFilter.has(db.namespace)) : application.tailorDBServices;
3294
+ if (namespaceFilter && services.length !== namespaceFilter.size) {
3295
+ const available = application.tailorDBServices.map((db) => db.namespace).join(", ");
3296
+ const requested = [...namespaceFilter].join(", ");
3297
+ throw new Error(`TailorDB namespace "${requested}" not found in local config.db.` + (available ? ` Available owned namespaces: ${available}` : ""));
3122
3298
  }
3123
- const binRelativePath = JSON.parse(fs$1.readFileSync(pkgJsonPath, "utf8")).bin?.[binName];
3124
- if (!binRelativePath) throw new Error(`\`${packageName}\` does not expose a \`${binName}\` binary entry.`);
3125
- return path.resolve(path.dirname(pkgJsonPath), binRelativePath);
3299
+ const namespaces = [];
3300
+ for (const db of services) {
3301
+ await db.loadTypes();
3302
+ await db.processNamespacePlugins();
3303
+ namespaces.push({
3304
+ namespace: db.namespace,
3305
+ types: { ...db.types },
3306
+ sourceInfo: new Map(Object.entries(db.typeSourceInfo)),
3307
+ pluginAttachments: db.pluginAttachments
3308
+ });
3309
+ }
3310
+ return {
3311
+ config,
3312
+ namespaces
3313
+ };
3126
3314
  }
3127
3315
 
3128
3316
  //#endregion
3129
3317
  //#region src/cli/commands/tailordb/erd/schema.ts
3130
- /**
3131
- * Convert TailorDB field config to tbls column definition.
3132
- * @param fieldName - Field name
3133
- * @param fieldConfig - TailorDB field configuration
3134
- * @returns tbls column definition
3135
- */
3136
- function toTblsColumn(fieldName, fieldConfig) {
3137
- const baseType = fieldConfig.type || "string";
3318
+ const CLEAN_ROOM_NOTES = [
3319
+ "Generated by a TailorDB-specific viewer implementation.",
3320
+ "The implementation is based on TailorDB schema contracts, public Liam documentation, Liam CLI help, and black-box generated-output observation.",
3321
+ "It does not copy Liam source code, generated JavaScript/CSS, parser internals, or layout internals."
3322
+ ];
3323
+ function buildRevision(schema) {
3324
+ return hashContent(JSON.stringify(schema)).slice(0, 16);
3325
+ }
3326
+ function toTypeSource(source) {
3327
+ if (!source) return void 0;
3328
+ if (isPluginGeneratedType(source)) return {
3329
+ kind: "plugin",
3330
+ exportName: source.exportName,
3331
+ pluginId: source.pluginId,
3332
+ pluginImportPath: source.pluginImportPath,
3333
+ originalExportName: source.originalExportName,
3334
+ generatedTypeKind: source.generatedTypeKind,
3335
+ namespace: source.namespace
3336
+ };
3337
+ return {
3338
+ kind: "user",
3339
+ exportName: source.exportName
3340
+ };
3341
+ }
3342
+ function toRelationships(relationships) {
3343
+ return Object.entries(relationships).sort(([a], [b]) => a.localeCompare(b)).map(([name, relationship]) => ({
3344
+ name,
3345
+ targetType: relationship.targetType,
3346
+ targetField: relationship.targetField,
3347
+ sourceField: relationship.sourceField,
3348
+ isArray: relationship.isArray,
3349
+ ...relationship.description && { description: relationship.description }
3350
+ }));
3351
+ }
3352
+ function toIndexes(type) {
3353
+ return Object.entries(type.indexes ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([name, index]) => ({
3354
+ name,
3355
+ fields: [...index.fields],
3356
+ unique: index.unique === true
3357
+ }));
3358
+ }
3359
+ function toColumnRelation(options) {
3360
+ const { fieldConfig, parsedField } = options;
3361
+ if (!fieldConfig.foreignKey || !fieldConfig.foreignKeyType) return;
3362
+ const required = fieldConfig.required !== false;
3363
+ const kind = parsedField?.relation || fieldConfig.rawRelation ? "relation" : "foreignKey";
3364
+ return {
3365
+ targetTable: fieldConfig.foreignKeyType,
3366
+ targetColumn: fieldConfig.foreignKeyField || "id",
3367
+ kind,
3368
+ required,
3369
+ ...fieldConfig.rawRelation?.type && { relationType: fieldConfig.rawRelation.type },
3370
+ ...parsedField?.relation?.forwardName && { forwardName: parsedField.relation.forwardName },
3371
+ ...parsedField?.relation?.backwardName && { backwardName: parsedField.relation.backwardName }
3372
+ };
3373
+ }
3374
+ function toColumn(options) {
3375
+ const { fieldName, fieldConfig } = options;
3376
+ const indexNames = options.indexEntries.filter(([, index]) => index.fields.includes(fieldName)).map(([name]) => name);
3377
+ const uniqueIndexNames = options.indexEntries.filter(([, index]) => index.unique && index.fields.includes(fieldName)).map(([name]) => name);
3378
+ const enumValues = fieldConfig.allowedValues?.map((value) => value.value) ?? [];
3379
+ const enumValueDescriptions = Object.fromEntries((fieldConfig.allowedValues ?? []).flatMap((value) => value.description ? [[value.value, value.description]] : []));
3380
+ const nestedFields = Object.entries(fieldConfig.fields ?? {}).map(([nestedName, nestedConfig]) => toColumn({
3381
+ fieldName: nestedName,
3382
+ fieldConfig: nestedConfig,
3383
+ indexEntries: []
3384
+ }));
3385
+ const relation = toColumnRelation(options);
3138
3386
  return {
3139
3387
  name: fieldName,
3140
- type: fieldConfig.array ? `${baseType}[]` : baseType,
3141
- nullable: !fieldConfig.required,
3142
- comment: fieldConfig.description ?? ""
3388
+ type: fieldConfig.type || "string",
3389
+ required: fieldConfig.required !== false,
3390
+ array: fieldConfig.array === true,
3391
+ ...fieldConfig.description && { description: fieldConfig.description },
3392
+ ...fieldConfig.unique && { unique: true },
3393
+ ...(fieldConfig.index || indexNames.length > 0) && { index: true },
3394
+ ...indexNames.length > 0 && { indexNames },
3395
+ ...uniqueIndexNames.length > 0 && { uniqueIndexNames },
3396
+ ...enumValues.length > 0 && { enumValues },
3397
+ ...Object.keys(enumValueDescriptions).length > 0 && { enumValueDescriptions },
3398
+ ...fieldConfig.vector && { vector: true },
3399
+ ...fieldConfig.serial && { serial: { ...fieldConfig.serial } },
3400
+ ...fieldConfig.scale !== void 0 && { scale: fieldConfig.scale },
3401
+ ...fieldConfig.validate?.length && { validations: fieldConfig.validate.length },
3402
+ ...fieldConfig.hooks && { hooks: {
3403
+ ...fieldConfig.hooks.create && { create: true },
3404
+ ...fieldConfig.hooks.update && { update: true }
3405
+ } },
3406
+ ...nestedFields.length > 0 && { fields: nestedFields },
3407
+ ...relation && { relation }
3143
3408
  };
3144
3409
  }
3145
- /**
3146
- * Build tbls schema JSON from TailorDB types.
3147
- * @param types - TailorDB types fetched from platform
3148
- * @param namespace - TailorDB namespace
3149
- * @returns tbls-compatible schema representation
3150
- */
3151
- function buildTblsSchema(types, namespace) {
3152
- const tables = [];
3153
- const relations = [];
3154
- const referencedByTable = {};
3155
- const constraintsByTable = {};
3156
- const enumsMap = /* @__PURE__ */ new Map();
3157
- for (const type of types) {
3158
- const tableName = type.name;
3159
- const schema = type.schema;
3160
- const columns = [];
3161
- const tableConstraints = [];
3162
- columns.push({
3410
+ function toTable(type, source) {
3411
+ const indexes = toIndexes(type);
3412
+ const indexEntries = indexes.map((index) => [index.name, index]);
3413
+ const fieldColumns = Object.entries(type.fields).filter(([fieldName]) => fieldName !== "id").map(([fieldName, field]) => toColumn({
3414
+ fieldName,
3415
+ fieldConfig: field.config,
3416
+ parsedField: field,
3417
+ indexEntries
3418
+ }));
3419
+ const typeSource = toTypeSource(source);
3420
+ return {
3421
+ name: type.name,
3422
+ pluralForm: type.pluralForm,
3423
+ ...type.description && { description: type.description },
3424
+ ...typeSource && { source: typeSource },
3425
+ columns: [{
3163
3426
  name: "id",
3164
3427
  type: "uuid",
3165
- nullable: false,
3166
- comment: ""
3167
- });
3168
- tableConstraints.push({
3169
- name: `pk_${tableName}`,
3170
- type: "PRIMARY KEY",
3171
- def: "",
3172
- table: tableName,
3173
- columns: ["id"]
3174
- });
3175
- if (schema) for (const [fieldName, fieldConfig] of Object.entries(schema.fields ?? {})) {
3176
- columns.push(toTblsColumn(fieldName, fieldConfig));
3177
- if (fieldConfig.type === "enum" && fieldConfig.allowedValues.length > 0) {
3178
- const enumName = `${tableName}_${fieldName}`;
3179
- let values = enumsMap.get(enumName);
3180
- if (!values) {
3181
- values = /* @__PURE__ */ new Set();
3182
- enumsMap.set(enumName, values);
3183
- }
3184
- for (const value of fieldConfig.allowedValues) values.add(value.value);
3185
- }
3186
- if (fieldConfig.foreignKey && fieldConfig.foreignKeyType) {
3187
- const foreignTable = fieldConfig.foreignKeyType;
3188
- const foreignColumn = fieldConfig.foreignKeyField || "id";
3189
- const childCardinality = fieldConfig.required ? "exactly_one" : "zero_or_one";
3190
- relations.push({
3191
- table: tableName,
3192
- columns: [fieldName],
3193
- parent_table: foreignTable,
3194
- parent_columns: [foreignColumn],
3195
- cardinality: childCardinality,
3196
- parent_cardinality: "zero_or_more",
3197
- def: ""
3198
- });
3199
- tableConstraints.push({
3200
- name: `fk_${tableName}_${fieldName}`,
3201
- type: "FOREIGN KEY",
3202
- def: "",
3203
- table: tableName,
3204
- columns: [fieldName],
3205
- referenced_table: foreignTable,
3206
- referenced_columns: [foreignColumn]
3207
- });
3208
- if (!referencedByTable[tableName]) referencedByTable[tableName] = /* @__PURE__ */ new Set();
3209
- referencedByTable[tableName].add(foreignTable);
3210
- }
3211
- }
3212
- constraintsByTable[tableName] = tableConstraints;
3213
- tables.push({
3214
- name: tableName,
3215
- type: "table",
3216
- comment: schema?.description ?? "",
3217
- columns,
3218
- indexes: [],
3219
- constraints: constraintsByTable[tableName] ?? [],
3220
- triggers: [],
3221
- def: "",
3222
- referenced_tables: []
3223
- });
3224
- }
3225
- for (const table of tables) {
3226
- const referenced = referencedByTable[table.name];
3227
- table.referenced_tables = referenced ? Array.from(referenced) : [];
3228
- }
3229
- const enums = [];
3230
- for (const [name, values] of enumsMap.entries()) enums.push({
3231
- name,
3232
- values: Array.from(values)
3428
+ required: true,
3429
+ array: false,
3430
+ primaryKey: true,
3431
+ unique: true
3432
+ }, ...fieldColumns],
3433
+ indexes,
3434
+ forwardRelationships: toRelationships(type.forwardRelationships),
3435
+ backwardRelationships: toRelationships(type.backwardRelationships)
3436
+ };
3437
+ }
3438
+ function toRelation(sourceTable, field) {
3439
+ const relation = toColumnRelation({
3440
+ fieldName: field.name,
3441
+ fieldConfig: field.config,
3442
+ parsedField: field,
3443
+ indexEntries: []
3233
3444
  });
3445
+ if (!relation) return void 0;
3234
3446
  return {
3235
- name: namespace,
3236
- tables,
3237
- relations,
3238
- enums
3447
+ name: `${sourceTable}.${field.name}->${relation.targetTable}.${relation.targetColumn}`,
3448
+ sourceTable,
3449
+ sourceColumns: [field.name],
3450
+ targetTable: relation.targetTable,
3451
+ targetColumns: [relation.targetColumn],
3452
+ required: relation.required,
3453
+ unique: field.config.unique === true || field.relation?.unique === true,
3454
+ kind: relation.kind,
3455
+ ...relation.relationType && { relationType: relation.relationType },
3456
+ ...relation.forwardName && { forwardName: relation.forwardName },
3457
+ ...relation.backwardName && { backwardName: relation.backwardName }
3239
3458
  };
3240
3459
  }
3241
- /**
3242
- * Export apply-applied TailorDB schema for a namespace as tbls-compatible JSON.
3243
- * @param options - Export options
3244
- * @returns tbls schema representation
3245
- */
3246
- async function exportTailorDBSchema(options) {
3247
- const { client, workspaceId, namespace } = options;
3248
- const types = await fetchAll(async (pageToken, maxPageSize) => {
3249
- try {
3250
- const { tailordbTypes, nextPageToken } = await client.listTailorDBTypes({
3251
- workspaceId,
3252
- namespaceName: namespace,
3253
- pageToken,
3254
- pageSize: maxPageSize
3255
- });
3256
- return [tailordbTypes, nextPageToken];
3257
- } catch (error) {
3258
- if (error instanceof ConnectError && error.code === Code.NotFound) return [[], ""];
3259
- throw error;
3260
- }
3261
- });
3262
- if (types.length === 0) logger.warn(`No TailorDB types found in namespace "${namespace}". Returning empty schema.`);
3263
- return buildTblsSchema(types, namespace);
3460
+ function buildRelations(types) {
3461
+ const relations = [];
3462
+ for (const type of Object.values(types)) for (const field of Object.values(type.fields)) {
3463
+ const relation = toRelation(type.name, field);
3464
+ if (relation) relations.push(relation);
3465
+ }
3466
+ return relations.sort((a, b) => a.name.localeCompare(b.name));
3264
3467
  }
3265
3468
  /**
3266
- * Writes the TailorDB schema to a file in tbls-compatible JSON format.
3267
- * @param options - The options for writing the schema file.
3469
+ * Build the TailorDB ERD viewer schema for one namespace.
3470
+ * @param options - Schema build options.
3471
+ * @returns TailorDB ERD viewer schema.
3268
3472
  */
3269
- async function writeTblsSchemaToFile(options) {
3270
- const schema = await exportTailorDBSchema(options);
3271
- const json = JSON.stringify(schema, null, 2);
3272
- fs$1.mkdirSync(path.dirname(options.outputPath), { recursive: true });
3273
- fs$1.writeFileSync(options.outputPath, json, "utf8");
3274
- const relativePath = path.relative(process.cwd(), options.outputPath);
3275
- logger.success(`Wrote ERD schema to ${relativePath}`);
3473
+ function buildTailorDbErdSchema(options) {
3474
+ const { namespaceData } = options;
3475
+ const tables = Object.values(namespaceData.types).sort((a, b) => a.name.localeCompare(b.name)).map((type) => toTable(type, namespaceData.sourceInfo.get(type.name)));
3476
+ const schemaWithoutRevision = {
3477
+ version: 1,
3478
+ namespace: namespaceData.namespace,
3479
+ source: options.source ?? "local",
3480
+ cleanRoom: {
3481
+ implementation: "tailor-sdk",
3482
+ notes: CLEAN_ROOM_NOTES
3483
+ },
3484
+ tables,
3485
+ relations: buildRelations(namespaceData.types)
3486
+ };
3487
+ return {
3488
+ ...schemaWithoutRevision,
3489
+ generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3490
+ revision: buildRevision(schemaWithoutRevision)
3491
+ };
3276
3492
  }
3277
3493
 
3278
3494
  //#endregion
3279
3495
  //#region src/cli/commands/tailordb/erd/utils.ts
3280
3496
  /**
3281
- * Initialize shared ERD command context.
3282
- * @param args - CLI arguments.
3283
- * @returns Initialized context.
3497
+ * Initialize shared ERD command behavior.
3284
3498
  */
3285
- async function initErdContext(args) {
3499
+ function initErdCommand() {
3286
3500
  logBetaWarning("tailordb erd");
3287
- const client = await initOperatorClient(await loadAccessToken({
3288
- useProfile: true,
3289
- profile: args.profile
3290
- }));
3291
- const workspaceId = await loadWorkspaceId({
3292
- workspaceId: args.workspaceId,
3293
- profile: args.profile
3294
- });
3295
- const { config } = await loadConfig(args.config);
3501
+ }
3502
+ /**
3503
+ * Initialize platform context for ERD deployment.
3504
+ * @param args - CLI arguments.
3505
+ * @returns Initialized deploy context.
3506
+ */
3507
+ async function initErdDeployContext(args) {
3508
+ initErdCommand();
3296
3509
  return {
3297
- client,
3298
- workspaceId,
3299
- config
3510
+ client: await initOperatorClient(await loadAccessToken({
3511
+ useProfile: true,
3512
+ profile: args.profile
3513
+ })),
3514
+ workspaceId: await loadWorkspaceId({
3515
+ workspaceId: args.workspaceId,
3516
+ profile: args.profile
3517
+ })
3300
3518
  };
3301
3519
  }
3302
3520
 
3303
3521
  //#endregion
3304
- //#region src/cli/commands/tailordb/erd/export.ts
3305
- const DEFAULT_ERD_BASE_DIR = ".tailor-sdk/erd";
3522
+ //#region src/cli/commands/tailordb/erd/viewer.ts
3523
+ const VIEWER_ASSETS_DIR = "erd-viewer-assets";
3524
+ const STYLES_LINK = "<link rel=\"stylesheet\" href=\"./styles.css\" />";
3525
+ const APP_SCRIPT = "<script src=\"./app.js\" type=\"module\"><\/script>";
3526
+ function assetDirCandidates() {
3527
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
3528
+ return [
3529
+ path.join(currentDir, "viewer-assets"),
3530
+ path.join(currentDir, VIEWER_ASSETS_DIR),
3531
+ path.join(currentDir, "commands", "tailordb", "erd", VIEWER_ASSETS_DIR),
3532
+ path.resolve(process.cwd(), "packages/sdk/src/cli/commands/tailordb/erd/viewer-assets")
3533
+ ];
3534
+ }
3306
3535
  /**
3307
- * Resolve TailorDB config and namespace.
3308
- * @param config - Loaded Tailor SDK config.
3309
- * @param explicitNamespace - Namespace override.
3310
- * @returns Resolved namespace and erdSite.
3536
+ * Resolve the packaged ERD viewer asset directory.
3537
+ * @returns Absolute path to the viewer asset directory.
3311
3538
  */
3312
- function resolveDbConfig(config, explicitNamespace) {
3313
- const namespace = explicitNamespace ?? Object.keys(config.db ?? {})[0];
3314
- if (!namespace) throw new Error("No TailorDB namespaces found in config. Please define db services in tailor.config.ts or pass --namespace.");
3315
- const dbConfig = config.db?.[namespace];
3316
- if (!dbConfig || typeof dbConfig !== "object" || "external" in dbConfig) throw new Error(`TailorDB namespace "${namespace}" not found in config.db.`);
3317
- return {
3318
- namespace,
3319
- erdSite: dbConfig.erdSite
3320
- };
3539
+ function resolveViewerAssetsDir() {
3540
+ for (const candidate of assetDirCandidates()) if (fs$1.existsSync(path.join(candidate, "index.html"))) return candidate;
3541
+ throw new Error(`ERD viewer assets not found. Checked: ${assetDirCandidates().join(", ")}`);
3321
3542
  }
3322
3543
  /**
3323
- * Get all namespaces from config.
3324
- * @param config - Loaded Tailor SDK config.
3325
- * @param options - Options for filtering namespaces.
3326
- * @returns All namespaces with optional erdSite.
3544
+ * Build the self-contained ERD viewer HTML document. CSS, JS, and the schema
3545
+ * are inlined as separately extractable blocks: a `<style>` element, a
3546
+ * `<script type="module">`, and a `<script type="application/json"
3547
+ * id="erd-schema">` data block. This renders without any sibling asset files
3548
+ * and lets external tooling (e.g. a future ERD diff) pull out the schema via
3549
+ * `JSON.parse`.
3550
+ * @param options - Viewer build options.
3551
+ * @returns The self-contained HTML document.
3327
3552
  */
3328
- function resolveAllNamespaces(config, options) {
3329
- const results = [];
3330
- for (const [namespace, dbConfig] of Object.entries(config.db ?? {})) if (dbConfig && !("external" in dbConfig) && !(options?.requireErdSite && !dbConfig.erdSite)) results.push({
3331
- namespace,
3332
- erdSite: dbConfig.erdSite
3333
- });
3334
- return results;
3553
+ function buildViewerHtml(options) {
3554
+ const assetsDir = resolveViewerAssetsDir();
3555
+ const html = fs$1.readFileSync(path.join(assetsDir, "index.html"), "utf8");
3556
+ const css = fs$1.readFileSync(path.join(assetsDir, "styles.css"), "utf8");
3557
+ const appJs = fs$1.readFileSync(path.join(assetsDir, "app.js"), "utf8");
3558
+ if (!html.includes(STYLES_LINK) || !html.includes(APP_SCRIPT)) throw new Error("ERD viewer index.html is missing expected asset references for inlining.");
3559
+ const embedScript = `<script type="application/json" id="erd-schema">${JSON.stringify(options.schema).replaceAll("<", "\\u003c")}<\/script>`;
3560
+ const inlineScript = `<script type="module">\n${appJs.replace(/<\/script/gi, "<\\/script")}\n<\/script>`;
3561
+ return html.replace(STYLES_LINK, `<style>\n${css}\n</style>`).replace(APP_SCRIPT, `${embedScript}\n ${inlineScript}`);
3335
3562
  }
3336
3563
  /**
3337
- * Run the liam CLI to build an ERD static site from a schema file.
3338
- * @param schemaPath - Path to the ERD schema JSON file
3339
- * @param cwd - Working directory where liam will run (dist is created here)
3340
- * @returns Resolves when the build completes successfully
3564
+ * Write the self-contained TailorDB ERD viewer to `<distDir>/index.html`.
3565
+ * @param options - Viewer dist write options.
3341
3566
  */
3342
- async function runLiamBuild(schemaPath, cwd) {
3343
- fs$1.mkdirSync(cwd, { recursive: true });
3344
- return await new Promise((resolve, reject) => {
3345
- let liamBinPath;
3346
- try {
3347
- liamBinPath = resolveCliBinPath({
3348
- packageName: "@liam-hq/cli",
3349
- binName: "liam"
3350
- });
3351
- } catch (error) {
3352
- logger.error(String(error));
3353
- reject(error);
3354
- return;
3355
- }
3356
- const child = spawn(process.execPath, [
3357
- liamBinPath,
3358
- "erd",
3359
- "build",
3360
- "--format",
3361
- "tbls",
3362
- "--input",
3363
- schemaPath
3364
- ], {
3365
- stdio: [
3366
- "pipe",
3367
- "ignore",
3368
- "pipe"
3369
- ],
3370
- cwd
3371
- });
3372
- let stderrOutput = "";
3373
- child.stderr?.on("data", (data) => {
3374
- stderrOutput += data.toString();
3375
- });
3376
- child.on("error", (error) => {
3377
- logger.error("Failed to run `@liam-hq/cli`.");
3378
- reject(error);
3379
- });
3380
- child.on("close", (code) => {
3381
- if (code === 0) resolve();
3382
- else {
3383
- if (stderrOutput) logger.error(stderrOutput);
3384
- logger.error("liam CLI exited with a non-zero code. Ensure `@liam-hq/cli erd build --format tbls --input schema.json` works in your project.");
3385
- reject(/* @__PURE__ */ new Error(`liam CLI exited with code ${code ?? 1}`));
3386
- }
3387
- });
3567
+ function writeViewerDist(options) {
3568
+ fs$1.rmSync(options.distDir, {
3569
+ recursive: true,
3570
+ force: true
3388
3571
  });
3572
+ fs$1.mkdirSync(options.distDir, { recursive: true });
3573
+ fs$1.writeFileSync(path.join(options.distDir, "index.html"), buildViewerHtml({ schema: options.schema }), "utf8");
3574
+ }
3575
+
3576
+ //#endregion
3577
+ //#region src/cli/commands/tailordb/erd/export.ts
3578
+ const DEFAULT_ERD_BASE_DIR$1 = ".tailor-sdk/erd";
3579
+ function getErdSite(context, namespace) {
3580
+ const dbConfig = context.config.db?.[namespace];
3581
+ if (!dbConfig || "external" in dbConfig) return;
3582
+ return dbConfig.erdSite;
3583
+ }
3584
+ function resolveExplicitTarget(options) {
3585
+ const namespaceData = options.context.namespaces.find((candidate) => candidate.namespace === options.namespace);
3586
+ if (!namespaceData) {
3587
+ const available = options.context.namespaces.map((candidate) => candidate.namespace).join(", ");
3588
+ throw new Error(`TailorDB namespace "${options.namespace}" not found in local config.db.` + (available ? ` Available owned namespaces: ${available}` : ""));
3589
+ }
3590
+ const erdSite = getErdSite(options.context, namespaceData.namespace);
3591
+ if (options.requireErdSite && !erdSite) throw new Error(`No erdSite configured for namespace "${namespaceData.namespace}". Add erdSite: "<static-website-name>" to db.${namespaceData.namespace} in tailor.config.ts.`);
3592
+ return toTarget(options.outputDir, namespaceData, erdSite);
3593
+ }
3594
+ function resolveAllTargets(options) {
3595
+ const namespaces = options.context.namespaces.filter((namespaceData) => !options.requireErdSite || getErdSite(options.context, namespaceData.namespace));
3596
+ if (namespaces.length === 0) throw new Error(options.requireErdSite ? "No namespaces with erdSite configured found. Add erdSite: \"<static-website-name>\" to db.<namespace> in tailor.config.ts." : "No TailorDB namespaces found in config. Please define db services in tailor.config.ts.");
3597
+ logger.info(`Found ${namespaces.length} namespace(s)${options.requireErdSite ? " with erdSite configured" : ""}.`);
3598
+ return namespaces.map((namespaceData) => toTarget(options.outputDir, namespaceData, getErdSite(options.context, namespaceData.namespace)));
3599
+ }
3600
+ function toTarget(outputDir, namespaceData, erdSite) {
3601
+ return {
3602
+ namespaceData,
3603
+ erdSite,
3604
+ distDir: path.join(outputDir, namespaceData.namespace, "dist")
3605
+ };
3606
+ }
3607
+ function resolveTargets(options) {
3608
+ if (options.namespace) return [resolveExplicitTarget(options)];
3609
+ return resolveAllTargets(options);
3610
+ }
3611
+ function prepareErdBuild(target) {
3612
+ writeViewerDist({
3613
+ schema: buildTailorDbErdSchema({ namespaceData: target.namespaceData }),
3614
+ distDir: target.distDir
3615
+ });
3616
+ const relativePath = path.relative(process.cwd(), target.distDir);
3617
+ logger.success(`Built ERD to ${relativePath}`);
3618
+ return {
3619
+ namespace: target.namespaceData.namespace,
3620
+ erdSite: target.erdSite,
3621
+ distDir: target.distDir
3622
+ };
3389
3623
  }
3390
3624
  /**
3391
- * Export TailorDB schema and build ERD artifacts via liam.
3625
+ * Prepare TailorDB ERD static viewer builds for one or more namespaces.
3392
3626
  * @param options - Build options.
3627
+ * @returns Build results by namespace.
3393
3628
  */
3394
- async function prepareErdBuild(options) {
3395
- await writeTblsSchemaToFile(options);
3396
- await runLiamBuild(options.outputPath, options.erdDir);
3397
- const distDir = path.join(options.erdDir, "dist");
3398
- const relativePath = path.relative(process.cwd(), distDir);
3399
- logger.success(`Built ERD to ${relativePath}`);
3629
+ async function prepareErdBuilds(options) {
3630
+ return prepareErdBuildsFromContext({
3631
+ context: await loadLocalErdSchema({
3632
+ configPath: options.configPath,
3633
+ namespaces: options.namespace ? [options.namespace] : void 0,
3634
+ requireErdSite: options.requireErdSite
3635
+ }),
3636
+ namespace: options.namespace,
3637
+ outputDir: options.outputDir,
3638
+ requireErdSite: options.requireErdSite
3639
+ });
3400
3640
  }
3401
3641
  /**
3402
- * Prepare ERD builds for one or more namespaces.
3642
+ * Prepare TailorDB ERD static viewer builds from an already loaded schema context.
3403
3643
  * @param options - Build options.
3404
3644
  * @returns Build results by namespace.
3405
3645
  */
3406
- async function prepareErdBuilds(options) {
3407
- const { client, workspaceId, config } = options;
3408
- const baseDir = options.outputDir ?? path.resolve(process.cwd(), DEFAULT_ERD_BASE_DIR);
3409
- let targets;
3410
- if (options.namespace) {
3411
- const { namespace, erdSite } = resolveDbConfig(config, options.namespace);
3412
- if (options.requireErdSite && !erdSite) throw new Error(`No erdSite configured for namespace "${namespace}". Add erdSite: "<static-website-name>" to db.${namespace} in tailor.config.ts.`);
3413
- const erdDir = path.join(baseDir, namespace);
3414
- targets = [{
3415
- namespace,
3416
- erdSite,
3417
- schemaOutputPath: path.join(erdDir, "schema.json"),
3418
- distDir: path.join(erdDir, "dist"),
3419
- erdDir
3420
- }];
3421
- } else {
3422
- const namespaces = resolveAllNamespaces(config, { requireErdSite: options.requireErdSite });
3423
- if (namespaces.length === 0) throw new Error(options.requireErdSite ? "No namespaces with erdSite configured found. Add erdSite: \"<static-website-name>\" to db.<namespace> in tailor.config.ts." : "No TailorDB namespaces found in config. Please define db services in tailor.config.ts.");
3424
- logger.info(`Found ${namespaces.length} namespace(s)${options.requireErdSite ? " with erdSite configured" : ""}.`);
3425
- targets = namespaces.map(({ namespace, erdSite }) => {
3426
- const erdDir = path.join(baseDir, namespace);
3427
- return {
3428
- namespace,
3429
- erdSite,
3430
- schemaOutputPath: path.join(erdDir, "schema.json"),
3431
- distDir: path.join(erdDir, "dist"),
3432
- erdDir
3433
- };
3434
- });
3435
- }
3436
- await Promise.all(targets.map((target) => prepareErdBuild({
3437
- namespace: target.namespace,
3438
- client,
3439
- workspaceId,
3440
- outputPath: target.schemaOutputPath,
3441
- erdDir: target.erdDir
3442
- })));
3443
- return targets;
3646
+ function prepareErdBuildsFromContext(options) {
3647
+ const outputDir = path.resolve(process.cwd(), options.outputDir ?? DEFAULT_ERD_BASE_DIR$1);
3648
+ return resolveTargets({
3649
+ context: options.context,
3650
+ namespace: options.namespace,
3651
+ outputDir,
3652
+ requireErdSite: options.requireErdSite
3653
+ }).map((target) => prepareErdBuild(target));
3444
3654
  }
3445
3655
  const erdExportCommand = defineAppCommand({
3446
3656
  name: "export",
3447
- description: "Export Liam ERD dist from applied TailorDB schema.",
3657
+ description: "Export TailorDB ERD static viewer from local TailorDB schema.",
3448
3658
  args: z.object({
3449
- ...deploymentArgs,
3659
+ ...configArg,
3450
3660
  namespace: arg(z.string().optional(), {
3451
3661
  alias: "n",
3452
3662
  description: "TailorDB namespace name (optional if only one namespace is defined in config)"
3453
3663
  }),
3454
- output: arg(z.string().default(DEFAULT_ERD_BASE_DIR), {
3664
+ output: arg(z.string().default(DEFAULT_ERD_BASE_DIR$1), {
3455
3665
  alias: "o",
3456
- description: "Output directory path for tbls-compatible ERD JSON (writes to `<outputDir>/<namespace>/schema.json`)",
3666
+ description: "Output directory path for TailorDB ERD viewer files (writes to `<outputDir>/<namespace>/dist`)",
3457
3667
  completion: { type: "directory" }
3458
3668
  })
3459
3669
  }).strict(),
3460
3670
  run: async (args) => {
3461
- const { client, workspaceId, config } = await initErdContext(args);
3462
- const outputDir = path.resolve(process.cwd(), String(args.output));
3671
+ initErdCommand();
3463
3672
  const results = await prepareErdBuilds({
3464
- client,
3465
- workspaceId,
3466
- config,
3673
+ configPath: args.config,
3467
3674
  namespace: args.namespace,
3468
- outputDir
3675
+ outputDir: args.output
3469
3676
  });
3470
3677
  logger.newline();
3471
3678
  if (args.json) logger.out(results.map((result) => ({
3472
3679
  namespace: result.namespace,
3473
- distDir: result.distDir,
3474
- schemaOutputPath: result.schemaOutputPath
3680
+ distDir: result.distDir
3475
3681
  })));
3476
3682
  else for (const result of results) {
3477
3683
  logger.out(`Exported ERD for namespace "${result.namespace}"`);
3478
- logger.out(` - Liam ERD dist: ${result.distDir}`);
3479
- logger.out(` - tbls schema.json: ${result.schemaOutputPath}`);
3684
+ logger.out(` - ERD viewer: ${path.join(result.distDir, "index.html")}`);
3480
3685
  }
3481
3686
  }
3482
3687
  });
@@ -3495,11 +3700,9 @@ const erdDeployCommand = defineAppCommand({
3495
3700
  }).strict(),
3496
3701
  run: async (args) => {
3497
3702
  await assertWritable({ profile: args.profile });
3498
- const { client, workspaceId, config } = await initErdContext(args);
3703
+ const { client, workspaceId } = await initErdDeployContext(args);
3499
3704
  const buildResults = await prepareErdBuilds({
3500
- client,
3501
- workspaceId,
3502
- config,
3705
+ configPath: args.config,
3503
3706
  namespace: args.namespace,
3504
3707
  requireErdSite: true
3505
3708
  });
@@ -3527,65 +3730,354 @@ const erdDeployCommand = defineAppCommand({
3527
3730
 
3528
3731
  //#endregion
3529
3732
  //#region src/cli/commands/tailordb/erd/serve.ts
3733
+ const DEFAULT_ERD_BASE_DIR = ".tailor-sdk/erd";
3734
+ const LOCAL_HOST = "127.0.0.1";
3735
+ const GLOB_CHARS = /[*?[\]{}()!+@]/;
3530
3736
  function formatServeCommand(namespace) {
3531
3737
  return `tailor-sdk tailordb erd serve --namespace ${namespace}`;
3532
3738
  }
3533
- async function runServeDist(results) {
3534
- if (results.length === 0) throw new Error("No ERD build results found.");
3739
+ function getCacheControl(filePath) {
3740
+ return filePath.endsWith(".html") || filePath.endsWith(".json") ? "no-cache" : "public, max-age=3600";
3741
+ }
3742
+ function resolveRequestPath(distDir, requestUrl) {
3743
+ const url = new URL(requestUrl ?? "/", "http://localhost");
3744
+ let pathname;
3745
+ try {
3746
+ pathname = decodeURIComponent(url.pathname);
3747
+ } catch {
3748
+ return;
3749
+ }
3750
+ if (pathname === "/" || pathname.endsWith("/")) pathname = path.join(pathname, "index.html");
3751
+ const root = path.resolve(distDir);
3752
+ const filePath = path.resolve(root, `.${pathname}`);
3753
+ if (filePath !== root && !filePath.startsWith(`${root}${path.sep}`)) return;
3754
+ return filePath;
3755
+ }
3756
+ function openStaticFile(filePath) {
3757
+ let fd;
3758
+ try {
3759
+ fd = fs$1.openSync(filePath, "r");
3760
+ if (!fs$1.fstatSync(fd).isFile()) {
3761
+ fs$1.closeSync(fd);
3762
+ return;
3763
+ }
3764
+ return {
3765
+ filePath,
3766
+ fd
3767
+ };
3768
+ } catch {
3769
+ if (fd !== void 0) fs$1.closeSync(fd);
3770
+ return;
3771
+ }
3772
+ }
3773
+ function serveFile(distDir, req, res) {
3774
+ const filePath = resolveRequestPath(distDir, req.url);
3775
+ if (!filePath) {
3776
+ res.writeHead(403);
3777
+ res.end("Forbidden");
3778
+ return;
3779
+ }
3780
+ const fallbackPath = path.join(distDir, "index.html");
3781
+ const target = openStaticFile(filePath) ?? openStaticFile(fallbackPath);
3782
+ if (!target) {
3783
+ res.writeHead(503, {
3784
+ "Content-Type": "text/plain; charset=utf-8",
3785
+ "Cache-Control": "no-cache",
3786
+ "Retry-After": "1"
3787
+ });
3788
+ res.end("ERD build is refreshing. Please retry.");
3789
+ return;
3790
+ }
3791
+ const mimeType = lookup(target.filePath) || "application/octet-stream";
3792
+ const stream = fs$1.createReadStream(target.filePath, {
3793
+ fd: target.fd,
3794
+ autoClose: true
3795
+ });
3796
+ stream.on("error", () => {
3797
+ if (!res.headersSent) {
3798
+ res.writeHead(503, {
3799
+ "Content-Type": "text/plain; charset=utf-8",
3800
+ "Cache-Control": "no-cache",
3801
+ "Retry-After": "1"
3802
+ });
3803
+ res.end("ERD build is refreshing. Please retry.");
3804
+ return;
3805
+ }
3806
+ res.destroy();
3807
+ });
3808
+ res.writeHead(200, {
3809
+ "Content-Type": mimeType,
3810
+ "Cache-Control": getCacheControl(target.filePath)
3811
+ });
3812
+ stream.pipe(res);
3813
+ }
3814
+ async function startStaticServer(options) {
3815
+ const server = http.createServer((req, res) => {
3816
+ serveFile(options.distDir, req, res);
3817
+ });
3818
+ return await new Promise((resolve, reject) => {
3819
+ server.once("error", reject);
3820
+ server.listen(options.port, LOCAL_HOST, () => {
3821
+ server.off("error", reject);
3822
+ const address = server.address();
3823
+ if (!address || typeof address === "string") {
3824
+ reject(/* @__PURE__ */ new Error("Failed to determine ERD server address."));
3825
+ return;
3826
+ }
3827
+ resolve({
3828
+ server,
3829
+ url: `http://${LOCAL_HOST}:${address.port}`
3830
+ });
3831
+ });
3832
+ });
3833
+ }
3834
+ function getWatchPatterns(config, results) {
3835
+ const namespaces = new Set(results.map((result) => result.namespace));
3836
+ const patterns = [config.path];
3837
+ for (const namespace of namespaces) {
3838
+ const dbConfig = config.db?.[namespace];
3839
+ if (dbConfig && !("external" in dbConfig)) patterns.push(...dbConfig.files);
3840
+ }
3841
+ return [...new Set(patterns)];
3842
+ }
3843
+ function hasGlobPattern(pattern) {
3844
+ return GLOB_CHARS.test(pattern);
3845
+ }
3846
+ function globBaseDir(pattern) {
3847
+ const absolutePattern = path.resolve(pattern);
3848
+ const parsed = path.parse(absolutePattern);
3849
+ const relativePattern = absolutePattern.slice(parsed.root.length);
3850
+ const literalParts = [];
3851
+ for (const part of relativePattern.split(path.sep)) {
3852
+ if (!part || GLOB_CHARS.test(part)) break;
3853
+ literalParts.push(part);
3854
+ }
3855
+ const literalPath = literalParts.length > 0 ? path.join(parsed.root, ...literalParts) : parsed.root;
3856
+ if (!literalPath || literalPath === parsed.root) return parsed.root || process.cwd();
3857
+ if (!fs$1.existsSync(literalPath)) return path.dirname(literalPath);
3858
+ return fs$1.statSync(literalPath).isDirectory() ? literalPath : path.dirname(literalPath);
3859
+ }
3860
+ async function expandWatchPattern(pattern) {
3861
+ if (!hasGlobPattern(pattern)) return [path.resolve(pattern)];
3862
+ const paths = /* @__PURE__ */ new Set();
3863
+ for await (const file of glob(pattern)) paths.add(path.resolve(file));
3864
+ const baseDir = globBaseDir(pattern);
3865
+ if (fs$1.existsSync(baseDir) && fs$1.statSync(baseDir).isDirectory()) paths.add(baseDir);
3866
+ return [...paths];
3867
+ }
3868
+ async function resolveWatchPathsFromConfig(config, results) {
3869
+ const paths = /* @__PURE__ */ new Set();
3870
+ for (const pattern of getWatchPatterns(config, results)) for (const watchPath of await expandWatchPattern(pattern)) paths.add(watchPath);
3871
+ return [...paths];
3872
+ }
3873
+ async function resolveWatchPaths(context, results) {
3874
+ return await resolveWatchPathsFromConfig(context.config, results);
3875
+ }
3876
+ function parseFreshErdExportResults(stdout) {
3877
+ const lines = stdout.trim().split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
3878
+ for (const line of lines.toReversed()) try {
3879
+ const parsed = JSON.parse(line);
3880
+ if (!Array.isArray(parsed)) continue;
3881
+ return parsed.map((entry) => {
3882
+ const result = entry;
3883
+ if (typeof result.namespace !== "string" || typeof result.distDir !== "string") throw new Error("Invalid ERD export JSON output.");
3884
+ return {
3885
+ namespace: result.namespace,
3886
+ distDir: result.distDir
3887
+ };
3888
+ });
3889
+ } catch {
3890
+ continue;
3891
+ }
3892
+ throw new Error("Failed to parse ERD export JSON output.");
3893
+ }
3894
+ function freshErdExportArgs(options) {
3895
+ const cliEntry = process.argv[1];
3896
+ if (!cliEntry) throw new Error("Cannot rebuild ERD schema in a fresh process: CLI entrypoint is unavailable.");
3897
+ const args = [
3898
+ cliEntry,
3899
+ "tailordb",
3900
+ "erd",
3901
+ "export",
3902
+ "--output",
3903
+ options.outputDir,
3904
+ "--json"
3905
+ ];
3906
+ if (options.configPath) args.push("--config", options.configPath);
3907
+ if (options.namespace) args.push("--namespace", options.namespace);
3908
+ return args;
3909
+ }
3910
+ async function runFreshErdExport(options) {
3911
+ return await new Promise((resolve, reject) => {
3912
+ const child = spawn(process.execPath, freshErdExportArgs(options), {
3913
+ cwd: process.cwd(),
3914
+ env: process.env,
3915
+ stdio: [
3916
+ "ignore",
3917
+ "pipe",
3918
+ "pipe"
3919
+ ]
3920
+ });
3921
+ let stdout = "";
3922
+ let stderr = "";
3923
+ child.stdout.setEncoding("utf8");
3924
+ child.stderr.setEncoding("utf8");
3925
+ child.stdout.on("data", (chunk) => {
3926
+ stdout += chunk;
3927
+ });
3928
+ child.stderr.on("data", (chunk) => {
3929
+ stderr += chunk;
3930
+ });
3931
+ child.on("error", reject);
3932
+ child.on("close", (code, signal) => {
3933
+ if (code !== 0) {
3934
+ const detail = stderr.trim() || signal || `exit code ${code}`;
3935
+ reject(/* @__PURE__ */ new Error(`Fresh ERD export failed: ${detail}`));
3936
+ return;
3937
+ }
3938
+ try {
3939
+ resolve(parseFreshErdExportResults(stdout));
3940
+ } catch (error) {
3941
+ reject(error);
3942
+ }
3943
+ });
3944
+ });
3945
+ }
3946
+ function selectPrimaryResult(results) {
3535
3947
  const [primary, ...rest] = results;
3948
+ if (!primary) throw new Error("No ERD build results found.");
3536
3949
  logger.info(`Serving ERD for namespace "${primary.namespace}".`);
3537
3950
  if (rest.length > 0) {
3538
3951
  const commands = rest.map((result) => ` - ${formatServeCommand(result.namespace)}`).join("\n");
3539
3952
  logger.warn(`Multiple namespaces found. To serve another namespace, run:\n${commands}`);
3540
3953
  }
3541
- fs$1.mkdirSync(primary.erdDir, { recursive: true });
3542
- return await new Promise((resolve, reject) => {
3543
- let serveBinPath;
3954
+ return primary;
3955
+ }
3956
+ async function createErdWatcher(options) {
3957
+ let rebuilding = false;
3958
+ let pending = false;
3959
+ let watchPaths = await resolveWatchPaths(options.initialContext, options.initialResults);
3960
+ let importNonce = 0;
3961
+ const watcher = watch(watchPaths, {
3962
+ ignored: /node_modules/,
3963
+ ignoreInitial: true,
3964
+ awaitWriteFinish: {
3965
+ stabilityThreshold: 100,
3966
+ pollInterval: 100
3967
+ }
3968
+ });
3969
+ async function rebuild() {
3970
+ if (rebuilding) {
3971
+ pending = true;
3972
+ return;
3973
+ }
3974
+ rebuilding = true;
3544
3975
  try {
3545
- serveBinPath = resolveCliBinPath({
3546
- packageName: "serve",
3547
- binName: "serve"
3976
+ const results = await runFreshErdExport({
3977
+ configPath: options.configPath,
3978
+ namespace: options.namespace,
3979
+ outputDir: options.outputDir
3548
3980
  });
3981
+ const { config } = await loadConfig(options.configPath, { importNonce: String(importNonce += 1) });
3982
+ const nextWatchPaths = await resolveWatchPathsFromConfig(config, results);
3983
+ watcher.unwatch(watchPaths);
3984
+ watcher.add(nextWatchPaths);
3985
+ watchPaths = nextWatchPaths;
3986
+ logger.success(`Rebuilt ERD schema (${results.map((result) => result.namespace).join(", ")})`, { mode: "stream" });
3549
3987
  } catch (error) {
3988
+ logger.error("Failed to rebuild ERD schema. Serving the last successful build.", { mode: "stream" });
3550
3989
  logger.error(String(error));
3551
- reject(error);
3552
- return;
3553
- }
3554
- const child = spawn(process.execPath, [serveBinPath, "dist"], {
3555
- stdio: "inherit",
3556
- cwd: primary.erdDir
3557
- });
3558
- child.on("error", (error) => {
3559
- logger.error("Failed to run `serve dist`.");
3560
- reject(error);
3561
- });
3562
- child.on("exit", (code) => {
3563
- if (code === 0) resolve();
3564
- else {
3565
- logger.error("serve CLI exited with a non-zero code. Ensure `serve dist` works in your project.");
3566
- reject(/* @__PURE__ */ new Error(`serve CLI exited with code ${code ?? 1}`));
3990
+ } finally {
3991
+ rebuilding = false;
3992
+ if (pending) {
3993
+ pending = false;
3994
+ await rebuild();
3567
3995
  }
3568
- });
3996
+ }
3997
+ }
3998
+ let debounceTimer;
3999
+ const scheduleRebuild = (changedPath) => {
4000
+ logger.info(`Schema source changed: ${path.relative(process.cwd(), changedPath)}`, { mode: "stream" });
4001
+ if (debounceTimer) clearTimeout(debounceTimer);
4002
+ debounceTimer = setTimeout(() => {
4003
+ rebuild();
4004
+ }, 150);
4005
+ };
4006
+ watcher.on("add", scheduleRebuild);
4007
+ watcher.on("change", scheduleRebuild);
4008
+ watcher.on("unlink", scheduleRebuild);
4009
+ watcher.on("error", (error) => {
4010
+ logger.error(`ERD watcher error: ${String(error)}`, { mode: "stream" });
4011
+ });
4012
+ return watcher;
4013
+ }
4014
+ async function waitForShutdown(server, watcher) {
4015
+ return await new Promise((resolve) => {
4016
+ const shutdown = () => {
4017
+ watcher.close().finally(() => {
4018
+ server.close(() => {
4019
+ logger.info("ERD server stopped.");
4020
+ resolve();
4021
+ });
4022
+ });
4023
+ };
4024
+ process.once("SIGINT", shutdown);
4025
+ process.once("SIGTERM", shutdown);
3569
4026
  });
3570
4027
  }
3571
4028
  const erdServeCommand = defineAppCommand({
3572
4029
  name: "serve",
3573
- description: "Generate and serve ERD locally (liam build + serve dist). (beta)",
4030
+ description: "Generate and serve TailorDB ERD locally with watch reload. (beta)",
3574
4031
  args: z.object({
3575
- ...deploymentArgs,
4032
+ ...configArg,
3576
4033
  namespace: arg(z.string().optional(), {
3577
4034
  alias: "n",
3578
4035
  description: "TailorDB namespace name (uses first namespace in config if not specified)"
3579
- })
4036
+ }),
4037
+ port: arg(z.coerce.number().int().min(0).max(65535).default(0), { description: "Local server port (0 selects a free port)" }),
4038
+ open: arg(z.boolean().default(false), { description: "Open the ERD viewer in the default browser" })
3580
4039
  }).strict(),
3581
4040
  run: async (args) => {
3582
- const { client, workspaceId, config } = await initErdContext(args);
3583
- await runServeDist(await prepareErdBuilds({
3584
- client,
3585
- workspaceId,
3586
- config,
3587
- namespace: args.namespace
3588
- }));
4041
+ initErdCommand();
4042
+ const outputDir = path.resolve(process.cwd(), DEFAULT_ERD_BASE_DIR);
4043
+ const context = await loadLocalErdSchema({
4044
+ configPath: args.config,
4045
+ namespaces: args.namespace ? [args.namespace] : void 0
4046
+ });
4047
+ const results = prepareErdBuildsFromContext({
4048
+ context,
4049
+ namespace: args.namespace,
4050
+ outputDir
4051
+ });
4052
+ const primary = selectPrimaryResult(results);
4053
+ const { server, url } = await startStaticServer({
4054
+ distDir: primary.distDir,
4055
+ port: args.port
4056
+ });
4057
+ const watchUrl = `${url}/?watch=1`;
4058
+ const watcher = await createErdWatcher({
4059
+ configPath: args.config,
4060
+ namespace: args.namespace,
4061
+ outputDir,
4062
+ initialContext: context,
4063
+ initialResults: results
4064
+ });
4065
+ logger.newline();
4066
+ if (args.json) logger.out({
4067
+ namespace: primary.namespace,
4068
+ url: watchUrl,
4069
+ distDir: primary.distDir
4070
+ });
4071
+ else {
4072
+ logger.success("ERD server started.");
4073
+ logger.out(watchUrl);
4074
+ }
4075
+ if (args.open) try {
4076
+ await open(watchUrl);
4077
+ } catch {
4078
+ logger.warn("Failed to open browser automatically. Please open the URL above manually.");
4079
+ }
4080
+ await waitForShutdown(server, watcher);
3589
4081
  }
3590
4082
  });
3591
4083
 
@@ -3593,7 +4085,7 @@ const erdServeCommand = defineAppCommand({
3593
4085
  //#region src/cli/commands/tailordb/erd/index.ts
3594
4086
  const erdCommand = defineCommand({
3595
4087
  name: "erd",
3596
- description: "Generate ERD artifacts for TailorDB namespaces using Liam ERD. (beta)",
4088
+ description: "Generate TailorDB ERD viewer artifacts from local TailorDB schema. (beta)",
3597
4089
  subCommands: {
3598
4090
  export: erdExportCommand,
3599
4091
  serve: erdServeCommand,
@@ -3714,10 +4206,10 @@ async function set(options) {
3714
4206
  useProfile: false,
3715
4207
  profile: options.profile
3716
4208
  }));
3717
- const trn = `${trnPrefix(await loadWorkspaceId({
4209
+ const trn = resourceTrn(await loadWorkspaceId({
3718
4210
  workspaceId: options.workspaceId,
3719
4211
  profile: options.profile
3720
- }))}:tailordb:${targetNamespace}`;
4212
+ }), "tailordb", targetNamespace);
3721
4213
  let currentMigration;
3722
4214
  try {
3723
4215
  const { metadata } = await client.getMetadata({ trn });
@@ -3806,7 +4298,7 @@ async function collectMigrationStatuses(options) {
3806
4298
  });
3807
4299
  const statuses = [];
3808
4300
  for (const { namespace, migrationsDir } of targetNamespaces) {
3809
- const trn = `${trnPrefix(workspaceId)}:tailordb:${namespace}`;
4301
+ const trn = resourceTrn(workspaceId, "tailordb", namespace);
3810
4302
  let currentMigration;
3811
4303
  try {
3812
4304
  const { metadata } = await client.getMetadata({ trn });
@@ -4362,7 +4854,7 @@ runMain(mainCommand, {
4362
4854
  if (isVerbose() && error.stack) logger.debug(`\nStack trace:\n${error.stack}`);
4363
4855
  } else logger.error(`Unknown error: ${error}`);
4364
4856
  if (!isCLIError(error) && (!(error instanceof Error) || error instanceof TypeError || error instanceof RangeError)) {
4365
- const { reportCrash } = await import("../crashreport-C5oHvHUC.mjs");
4857
+ const { reportCrash } = await import("../crashreport-CXD_Kjk-.mjs");
4366
4858
  await reportCrash(error, "handledError");
4367
4859
  }
4368
4860
  }