@tailor-platform/sdk 1.56.0 → 1.57.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 (32) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +0 -23
  3. package/dist/{application-YHZIkjdy.mjs → application-CdkoGX27.mjs} +37 -4
  4. package/dist/application-CdkoGX27.mjs.map +1 -0
  5. package/dist/application-x_mURdR0.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 +686 -345
  11. package/dist/cli/index.mjs.map +1 -1
  12. package/dist/cli/lib.d.mts +7 -2
  13. package/dist/cli/lib.mjs +2 -2
  14. package/dist/client-DLPEPJ_s.mjs.map +1 -1
  15. package/dist/configure/index.d.mts +2 -2
  16. package/dist/configure/index.mjs +1 -1
  17. package/dist/configure/index.mjs.map +1 -1
  18. package/dist/{index-BW3v5XYC.d.mts → index-B61gFI9a.d.mts} +7 -2
  19. package/dist/{runtime-B8F1nklz.mjs → runtime-1YuaoNr8.mjs} +57 -63
  20. package/dist/runtime-1YuaoNr8.mjs.map +1 -0
  21. package/dist/{types-BinLwXM9.mjs → types-BwGth3a1.mjs} +57 -28
  22. package/dist/types-BwGth3a1.mjs.map +1 -0
  23. package/dist/{types-UeXbHFXW.mjs → types-Ccwchyj5.mjs} +1 -1
  24. package/dist/utils/test/index.d.mts +2 -2
  25. package/dist/{workflow.generated-BHdBzgx6.d.mts → workflow.generated-Kz-nQrTf.d.mts} +10 -1
  26. package/docs/cli/tailordb.md +31 -26
  27. package/docs/cli-reference.md +2 -2
  28. package/package.json +1 -3
  29. package/dist/application-C9-t0qQb.mjs +0 -4
  30. package/dist/application-YHZIkjdy.mjs.map +0 -1
  31. package/dist/runtime-B8F1nklz.mjs.map +0 -1
  32. package/dist/types-BinLwXM9.mjs.map +0 -1
@@ -2,13 +2,13 @@
2
2
 
3
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";
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-B8F1nklz.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-YHZIkjdy.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 trnPrefix, 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 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 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-1YuaoNr8.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-CdkoGX27.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
11
  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";
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";
@@ -1207,7 +1209,7 @@ async function detectFunctionType(options) {
1207
1209
  const rawInput = module.default.input;
1208
1210
  let inputSchema;
1209
1211
  if (rawInput) {
1210
- const { t } = await import("../types-UeXbHFXW.mjs");
1212
+ const { t } = await import("../types-Ccwchyj5.mjs");
1211
1213
  inputSchema = t.object(rawInput);
1212
1214
  }
1213
1215
  return {
@@ -3105,378 +3107,430 @@ const staticwebsiteCommand = defineCommand({
3105
3107
  });
3106
3108
 
3107
3109
  //#endregion
3108
- //#region src/cli/shared/resolve-cli-bin.ts
3110
+ //#region src/cli/commands/tailordb/erd/local-schema.ts
3109
3111
  /**
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.
3112
+ * Resolve TailorDB namespaces that need local type loading for ERD generation.
3113
+ * @param config - Loaded Tailor config.
3114
+ * @param options - Namespace selection options.
3115
+ * @returns Namespace names to load, or undefined to load all owned namespaces.
3113
3116
  */
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}\`.`);
3117
+ function resolveLocalErdSchemaNamespaces(config, options) {
3118
+ if (options.namespaces) return options.namespaces;
3119
+ if (!options.requireErdSite) return;
3120
+ return Object.entries(config.db ?? {}).flatMap(([namespace, dbConfig]) => "external" in dbConfig || !dbConfig.erdSite ? [] : [namespace]);
3121
+ }
3122
+ /**
3123
+ * Load local TailorDB namespaces exactly as SDK generation/deploy sees them.
3124
+ * @param options - Local schema loading options.
3125
+ * @returns Loaded TailorDB namespace data.
3126
+ */
3127
+ async function loadLocalErdSchema(options) {
3128
+ const { config, plugins } = await loadConfig(options.configPath);
3129
+ await generateUserTypes({
3130
+ config,
3131
+ configPath: config.path
3132
+ });
3133
+ const application = defineApplication({
3134
+ config,
3135
+ pluginManager: plugins.length > 0 ? new PluginManager(plugins) : void 0
3136
+ });
3137
+ const namespaceNames = resolveLocalErdSchemaNamespaces(config, {
3138
+ namespaces: options.namespaces,
3139
+ requireErdSite: options.requireErdSite
3140
+ });
3141
+ const namespaceFilter = namespaceNames ? new Set(namespaceNames) : void 0;
3142
+ const services = namespaceFilter ? application.tailorDBServices.filter((db) => namespaceFilter.has(db.namespace)) : application.tailorDBServices;
3143
+ if (namespaceFilter && services.length !== namespaceFilter.size) {
3144
+ const available = application.tailorDBServices.map((db) => db.namespace).join(", ");
3145
+ const requested = [...namespaceFilter].join(", ");
3146
+ throw new Error(`TailorDB namespace "${requested}" not found in local config.db.` + (available ? ` Available owned namespaces: ${available}` : ""));
3122
3147
  }
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);
3148
+ const namespaces = [];
3149
+ for (const db of services) {
3150
+ await db.loadTypes();
3151
+ await db.processNamespacePlugins();
3152
+ namespaces.push({
3153
+ namespace: db.namespace,
3154
+ types: { ...db.types },
3155
+ sourceInfo: new Map(Object.entries(db.typeSourceInfo)),
3156
+ pluginAttachments: db.pluginAttachments
3157
+ });
3158
+ }
3159
+ return {
3160
+ config,
3161
+ namespaces
3162
+ };
3126
3163
  }
3127
3164
 
3128
3165
  //#endregion
3129
3166
  //#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";
3167
+ const CLEAN_ROOM_NOTES = [
3168
+ "Generated by a TailorDB-specific viewer implementation.",
3169
+ "The implementation is based on TailorDB schema contracts, public Liam documentation, Liam CLI help, and black-box generated-output observation.",
3170
+ "It does not copy Liam source code, generated JavaScript/CSS, parser internals, or layout internals."
3171
+ ];
3172
+ function buildRevision(schema) {
3173
+ return hashContent(JSON.stringify(schema)).slice(0, 16);
3174
+ }
3175
+ function toTypeSource(source) {
3176
+ if (!source) return void 0;
3177
+ if (isPluginGeneratedType(source)) return {
3178
+ kind: "plugin",
3179
+ exportName: source.exportName,
3180
+ pluginId: source.pluginId,
3181
+ pluginImportPath: source.pluginImportPath,
3182
+ originalExportName: source.originalExportName,
3183
+ generatedTypeKind: source.generatedTypeKind,
3184
+ namespace: source.namespace
3185
+ };
3186
+ return {
3187
+ kind: "user",
3188
+ exportName: source.exportName
3189
+ };
3190
+ }
3191
+ function toRelationships(relationships) {
3192
+ return Object.entries(relationships).sort(([a], [b]) => a.localeCompare(b)).map(([name, relationship]) => ({
3193
+ name,
3194
+ targetType: relationship.targetType,
3195
+ targetField: relationship.targetField,
3196
+ sourceField: relationship.sourceField,
3197
+ isArray: relationship.isArray,
3198
+ ...relationship.description && { description: relationship.description }
3199
+ }));
3200
+ }
3201
+ function toIndexes(type) {
3202
+ return Object.entries(type.indexes ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([name, index]) => ({
3203
+ name,
3204
+ fields: [...index.fields],
3205
+ unique: index.unique === true
3206
+ }));
3207
+ }
3208
+ function toColumnRelation(options) {
3209
+ const { fieldConfig, parsedField } = options;
3210
+ if (!fieldConfig.foreignKey || !fieldConfig.foreignKeyType) return;
3211
+ const required = fieldConfig.required !== false;
3212
+ const kind = parsedField?.relation || fieldConfig.rawRelation ? "relation" : "foreignKey";
3213
+ return {
3214
+ targetTable: fieldConfig.foreignKeyType,
3215
+ targetColumn: fieldConfig.foreignKeyField || "id",
3216
+ kind,
3217
+ required,
3218
+ ...fieldConfig.rawRelation?.type && { relationType: fieldConfig.rawRelation.type },
3219
+ ...parsedField?.relation?.forwardName && { forwardName: parsedField.relation.forwardName },
3220
+ ...parsedField?.relation?.backwardName && { backwardName: parsedField.relation.backwardName }
3221
+ };
3222
+ }
3223
+ function toColumn(options) {
3224
+ const { fieldName, fieldConfig } = options;
3225
+ const indexNames = options.indexEntries.filter(([, index]) => index.fields.includes(fieldName)).map(([name]) => name);
3226
+ const uniqueIndexNames = options.indexEntries.filter(([, index]) => index.unique && index.fields.includes(fieldName)).map(([name]) => name);
3227
+ const enumValues = fieldConfig.allowedValues?.map((value) => value.value) ?? [];
3228
+ const enumValueDescriptions = Object.fromEntries((fieldConfig.allowedValues ?? []).flatMap((value) => value.description ? [[value.value, value.description]] : []));
3229
+ const nestedFields = Object.entries(fieldConfig.fields ?? {}).map(([nestedName, nestedConfig]) => toColumn({
3230
+ fieldName: nestedName,
3231
+ fieldConfig: nestedConfig,
3232
+ indexEntries: []
3233
+ }));
3234
+ const relation = toColumnRelation(options);
3138
3235
  return {
3139
3236
  name: fieldName,
3140
- type: fieldConfig.array ? `${baseType}[]` : baseType,
3141
- nullable: !fieldConfig.required,
3142
- comment: fieldConfig.description ?? ""
3237
+ type: fieldConfig.type || "string",
3238
+ required: fieldConfig.required !== false,
3239
+ array: fieldConfig.array === true,
3240
+ ...fieldConfig.description && { description: fieldConfig.description },
3241
+ ...fieldConfig.unique && { unique: true },
3242
+ ...(fieldConfig.index || indexNames.length > 0) && { index: true },
3243
+ ...indexNames.length > 0 && { indexNames },
3244
+ ...uniqueIndexNames.length > 0 && { uniqueIndexNames },
3245
+ ...enumValues.length > 0 && { enumValues },
3246
+ ...Object.keys(enumValueDescriptions).length > 0 && { enumValueDescriptions },
3247
+ ...fieldConfig.vector && { vector: true },
3248
+ ...fieldConfig.serial && { serial: { ...fieldConfig.serial } },
3249
+ ...fieldConfig.scale !== void 0 && { scale: fieldConfig.scale },
3250
+ ...fieldConfig.validate?.length && { validations: fieldConfig.validate.length },
3251
+ ...fieldConfig.hooks && { hooks: {
3252
+ ...fieldConfig.hooks.create && { create: true },
3253
+ ...fieldConfig.hooks.update && { update: true }
3254
+ } },
3255
+ ...nestedFields.length > 0 && { fields: nestedFields },
3256
+ ...relation && { relation }
3143
3257
  };
3144
3258
  }
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({
3259
+ function toTable(type, source) {
3260
+ const indexes = toIndexes(type);
3261
+ const indexEntries = indexes.map((index) => [index.name, index]);
3262
+ const fieldColumns = Object.entries(type.fields).filter(([fieldName]) => fieldName !== "id").map(([fieldName, field]) => toColumn({
3263
+ fieldName,
3264
+ fieldConfig: field.config,
3265
+ parsedField: field,
3266
+ indexEntries
3267
+ }));
3268
+ const typeSource = toTypeSource(source);
3269
+ return {
3270
+ name: type.name,
3271
+ pluralForm: type.pluralForm,
3272
+ ...type.description && { description: type.description },
3273
+ ...typeSource && { source: typeSource },
3274
+ columns: [{
3163
3275
  name: "id",
3164
3276
  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)
3277
+ required: true,
3278
+ array: false,
3279
+ primaryKey: true,
3280
+ unique: true
3281
+ }, ...fieldColumns],
3282
+ indexes,
3283
+ forwardRelationships: toRelationships(type.forwardRelationships),
3284
+ backwardRelationships: toRelationships(type.backwardRelationships)
3285
+ };
3286
+ }
3287
+ function toRelation(sourceTable, field) {
3288
+ const relation = toColumnRelation({
3289
+ fieldName: field.name,
3290
+ fieldConfig: field.config,
3291
+ parsedField: field,
3292
+ indexEntries: []
3233
3293
  });
3294
+ if (!relation) return void 0;
3234
3295
  return {
3235
- name: namespace,
3236
- tables,
3237
- relations,
3238
- enums
3296
+ name: `${sourceTable}.${field.name}->${relation.targetTable}.${relation.targetColumn}`,
3297
+ sourceTable,
3298
+ sourceColumns: [field.name],
3299
+ targetTable: relation.targetTable,
3300
+ targetColumns: [relation.targetColumn],
3301
+ required: relation.required,
3302
+ unique: field.config.unique === true || field.relation?.unique === true,
3303
+ kind: relation.kind,
3304
+ ...relation.relationType && { relationType: relation.relationType },
3305
+ ...relation.forwardName && { forwardName: relation.forwardName },
3306
+ ...relation.backwardName && { backwardName: relation.backwardName }
3239
3307
  };
3240
3308
  }
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);
3309
+ function buildRelations(types) {
3310
+ const relations = [];
3311
+ for (const type of Object.values(types)) for (const field of Object.values(type.fields)) {
3312
+ const relation = toRelation(type.name, field);
3313
+ if (relation) relations.push(relation);
3314
+ }
3315
+ return relations.sort((a, b) => a.name.localeCompare(b.name));
3264
3316
  }
3265
3317
  /**
3266
- * Writes the TailorDB schema to a file in tbls-compatible JSON format.
3267
- * @param options - The options for writing the schema file.
3318
+ * Build the TailorDB ERD viewer schema for one namespace.
3319
+ * @param options - Schema build options.
3320
+ * @returns TailorDB ERD viewer schema.
3268
3321
  */
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}`);
3322
+ function buildTailorDbErdSchema(options) {
3323
+ const { namespaceData } = options;
3324
+ const tables = Object.values(namespaceData.types).sort((a, b) => a.name.localeCompare(b.name)).map((type) => toTable(type, namespaceData.sourceInfo.get(type.name)));
3325
+ const schemaWithoutRevision = {
3326
+ version: 1,
3327
+ namespace: namespaceData.namespace,
3328
+ source: options.source ?? "local",
3329
+ cleanRoom: {
3330
+ implementation: "tailor-sdk",
3331
+ notes: CLEAN_ROOM_NOTES
3332
+ },
3333
+ tables,
3334
+ relations: buildRelations(namespaceData.types)
3335
+ };
3336
+ return {
3337
+ ...schemaWithoutRevision,
3338
+ generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3339
+ revision: buildRevision(schemaWithoutRevision)
3340
+ };
3276
3341
  }
3277
3342
 
3278
3343
  //#endregion
3279
3344
  //#region src/cli/commands/tailordb/erd/utils.ts
3280
3345
  /**
3281
- * Initialize shared ERD command context.
3282
- * @param args - CLI arguments.
3283
- * @returns Initialized context.
3346
+ * Initialize shared ERD command behavior.
3284
3347
  */
3285
- async function initErdContext(args) {
3348
+ function initErdCommand() {
3286
3349
  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);
3350
+ }
3351
+ /**
3352
+ * Initialize platform context for ERD deployment.
3353
+ * @param args - CLI arguments.
3354
+ * @returns Initialized deploy context.
3355
+ */
3356
+ async function initErdDeployContext(args) {
3357
+ initErdCommand();
3296
3358
  return {
3297
- client,
3298
- workspaceId,
3299
- config
3359
+ client: await initOperatorClient(await loadAccessToken({
3360
+ useProfile: true,
3361
+ profile: args.profile
3362
+ })),
3363
+ workspaceId: await loadWorkspaceId({
3364
+ workspaceId: args.workspaceId,
3365
+ profile: args.profile
3366
+ })
3300
3367
  };
3301
3368
  }
3302
3369
 
3303
3370
  //#endregion
3304
- //#region src/cli/commands/tailordb/erd/export.ts
3305
- const DEFAULT_ERD_BASE_DIR = ".tailor-sdk/erd";
3371
+ //#region src/cli/commands/tailordb/erd/viewer.ts
3372
+ const VIEWER_ASSETS_DIR = "erd-viewer-assets";
3373
+ const STYLES_LINK = "<link rel=\"stylesheet\" href=\"./styles.css\" />";
3374
+ const APP_SCRIPT = "<script src=\"./app.js\" type=\"module\"><\/script>";
3375
+ function assetDirCandidates() {
3376
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
3377
+ return [
3378
+ path.join(currentDir, "viewer-assets"),
3379
+ path.join(currentDir, VIEWER_ASSETS_DIR),
3380
+ path.join(currentDir, "commands", "tailordb", "erd", VIEWER_ASSETS_DIR),
3381
+ path.resolve(process.cwd(), "packages/sdk/src/cli/commands/tailordb/erd/viewer-assets")
3382
+ ];
3383
+ }
3306
3384
  /**
3307
- * Resolve TailorDB config and namespace.
3308
- * @param config - Loaded Tailor SDK config.
3309
- * @param explicitNamespace - Namespace override.
3310
- * @returns Resolved namespace and erdSite.
3385
+ * Resolve the packaged ERD viewer asset directory.
3386
+ * @returns Absolute path to the viewer asset directory.
3311
3387
  */
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
- };
3388
+ function resolveViewerAssetsDir() {
3389
+ for (const candidate of assetDirCandidates()) if (fs$1.existsSync(path.join(candidate, "index.html"))) return candidate;
3390
+ throw new Error(`ERD viewer assets not found. Checked: ${assetDirCandidates().join(", ")}`);
3321
3391
  }
3322
3392
  /**
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.
3393
+ * Build the self-contained ERD viewer HTML document. CSS, JS, and the schema
3394
+ * are inlined as separately extractable blocks: a `<style>` element, a
3395
+ * `<script type="module">`, and a `<script type="application/json"
3396
+ * id="erd-schema">` data block. This renders without any sibling asset files
3397
+ * and lets external tooling (e.g. a future ERD diff) pull out the schema via
3398
+ * `JSON.parse`.
3399
+ * @param options - Viewer build options.
3400
+ * @returns The self-contained HTML document.
3327
3401
  */
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;
3402
+ function buildViewerHtml(options) {
3403
+ const assetsDir = resolveViewerAssetsDir();
3404
+ const html = fs$1.readFileSync(path.join(assetsDir, "index.html"), "utf8");
3405
+ const css = fs$1.readFileSync(path.join(assetsDir, "styles.css"), "utf8");
3406
+ const appJs = fs$1.readFileSync(path.join(assetsDir, "app.js"), "utf8");
3407
+ if (!html.includes(STYLES_LINK) || !html.includes(APP_SCRIPT)) throw new Error("ERD viewer index.html is missing expected asset references for inlining.");
3408
+ const embedScript = `<script type="application/json" id="erd-schema">${JSON.stringify(options.schema).replaceAll("<", "\\u003c")}<\/script>`;
3409
+ const inlineScript = `<script type="module">\n${appJs.replace(/<\/script/gi, "<\\/script")}\n<\/script>`;
3410
+ return html.replace(STYLES_LINK, `<style>\n${css}\n</style>`).replace(APP_SCRIPT, `${embedScript}\n ${inlineScript}`);
3335
3411
  }
3336
3412
  /**
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
3413
+ * Write the self-contained TailorDB ERD viewer to `<distDir>/index.html`.
3414
+ * @param options - Viewer dist write options.
3341
3415
  */
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
- });
3416
+ function writeViewerDist(options) {
3417
+ fs$1.rmSync(options.distDir, {
3418
+ recursive: true,
3419
+ force: true
3388
3420
  });
3421
+ fs$1.mkdirSync(options.distDir, { recursive: true });
3422
+ fs$1.writeFileSync(path.join(options.distDir, "index.html"), buildViewerHtml({ schema: options.schema }), "utf8");
3423
+ }
3424
+
3425
+ //#endregion
3426
+ //#region src/cli/commands/tailordb/erd/export.ts
3427
+ const DEFAULT_ERD_BASE_DIR$1 = ".tailor-sdk/erd";
3428
+ function getErdSite(context, namespace) {
3429
+ const dbConfig = context.config.db?.[namespace];
3430
+ if (!dbConfig || "external" in dbConfig) return;
3431
+ return dbConfig.erdSite;
3432
+ }
3433
+ function resolveExplicitTarget(options) {
3434
+ const namespaceData = options.context.namespaces.find((candidate) => candidate.namespace === options.namespace);
3435
+ if (!namespaceData) {
3436
+ const available = options.context.namespaces.map((candidate) => candidate.namespace).join(", ");
3437
+ throw new Error(`TailorDB namespace "${options.namespace}" not found in local config.db.` + (available ? ` Available owned namespaces: ${available}` : ""));
3438
+ }
3439
+ const erdSite = getErdSite(options.context, namespaceData.namespace);
3440
+ 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.`);
3441
+ return toTarget(options.outputDir, namespaceData, erdSite);
3442
+ }
3443
+ function resolveAllTargets(options) {
3444
+ const namespaces = options.context.namespaces.filter((namespaceData) => !options.requireErdSite || getErdSite(options.context, namespaceData.namespace));
3445
+ 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.");
3446
+ logger.info(`Found ${namespaces.length} namespace(s)${options.requireErdSite ? " with erdSite configured" : ""}.`);
3447
+ return namespaces.map((namespaceData) => toTarget(options.outputDir, namespaceData, getErdSite(options.context, namespaceData.namespace)));
3448
+ }
3449
+ function toTarget(outputDir, namespaceData, erdSite) {
3450
+ return {
3451
+ namespaceData,
3452
+ erdSite,
3453
+ distDir: path.join(outputDir, namespaceData.namespace, "dist")
3454
+ };
3455
+ }
3456
+ function resolveTargets(options) {
3457
+ if (options.namespace) return [resolveExplicitTarget(options)];
3458
+ return resolveAllTargets(options);
3459
+ }
3460
+ function prepareErdBuild(target) {
3461
+ writeViewerDist({
3462
+ schema: buildTailorDbErdSchema({ namespaceData: target.namespaceData }),
3463
+ distDir: target.distDir
3464
+ });
3465
+ const relativePath = path.relative(process.cwd(), target.distDir);
3466
+ logger.success(`Built ERD to ${relativePath}`);
3467
+ return {
3468
+ namespace: target.namespaceData.namespace,
3469
+ erdSite: target.erdSite,
3470
+ distDir: target.distDir
3471
+ };
3389
3472
  }
3390
3473
  /**
3391
- * Export TailorDB schema and build ERD artifacts via liam.
3474
+ * Prepare TailorDB ERD static viewer builds for one or more namespaces.
3392
3475
  * @param options - Build options.
3476
+ * @returns Build results by namespace.
3393
3477
  */
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}`);
3478
+ async function prepareErdBuilds(options) {
3479
+ return prepareErdBuildsFromContext({
3480
+ context: await loadLocalErdSchema({
3481
+ configPath: options.configPath,
3482
+ namespaces: options.namespace ? [options.namespace] : void 0,
3483
+ requireErdSite: options.requireErdSite
3484
+ }),
3485
+ namespace: options.namespace,
3486
+ outputDir: options.outputDir,
3487
+ requireErdSite: options.requireErdSite
3488
+ });
3400
3489
  }
3401
3490
  /**
3402
- * Prepare ERD builds for one or more namespaces.
3491
+ * Prepare TailorDB ERD static viewer builds from an already loaded schema context.
3403
3492
  * @param options - Build options.
3404
3493
  * @returns Build results by namespace.
3405
3494
  */
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;
3495
+ function prepareErdBuildsFromContext(options) {
3496
+ const outputDir = path.resolve(process.cwd(), options.outputDir ?? DEFAULT_ERD_BASE_DIR$1);
3497
+ return resolveTargets({
3498
+ context: options.context,
3499
+ namespace: options.namespace,
3500
+ outputDir,
3501
+ requireErdSite: options.requireErdSite
3502
+ }).map((target) => prepareErdBuild(target));
3444
3503
  }
3445
3504
  const erdExportCommand = defineAppCommand({
3446
3505
  name: "export",
3447
- description: "Export Liam ERD dist from applied TailorDB schema.",
3506
+ description: "Export TailorDB ERD static viewer from local TailorDB schema.",
3448
3507
  args: z.object({
3449
- ...deploymentArgs,
3508
+ ...configArg,
3450
3509
  namespace: arg(z.string().optional(), {
3451
3510
  alias: "n",
3452
3511
  description: "TailorDB namespace name (optional if only one namespace is defined in config)"
3453
3512
  }),
3454
- output: arg(z.string().default(DEFAULT_ERD_BASE_DIR), {
3513
+ output: arg(z.string().default(DEFAULT_ERD_BASE_DIR$1), {
3455
3514
  alias: "o",
3456
- description: "Output directory path for tbls-compatible ERD JSON (writes to `<outputDir>/<namespace>/schema.json`)",
3515
+ description: "Output directory path for TailorDB ERD viewer files (writes to `<outputDir>/<namespace>/dist`)",
3457
3516
  completion: { type: "directory" }
3458
3517
  })
3459
3518
  }).strict(),
3460
3519
  run: async (args) => {
3461
- const { client, workspaceId, config } = await initErdContext(args);
3462
- const outputDir = path.resolve(process.cwd(), String(args.output));
3520
+ initErdCommand();
3463
3521
  const results = await prepareErdBuilds({
3464
- client,
3465
- workspaceId,
3466
- config,
3522
+ configPath: args.config,
3467
3523
  namespace: args.namespace,
3468
- outputDir
3524
+ outputDir: args.output
3469
3525
  });
3470
3526
  logger.newline();
3471
3527
  if (args.json) logger.out(results.map((result) => ({
3472
3528
  namespace: result.namespace,
3473
- distDir: result.distDir,
3474
- schemaOutputPath: result.schemaOutputPath
3529
+ distDir: result.distDir
3475
3530
  })));
3476
3531
  else for (const result of results) {
3477
3532
  logger.out(`Exported ERD for namespace "${result.namespace}"`);
3478
- logger.out(` - Liam ERD dist: ${result.distDir}`);
3479
- logger.out(` - tbls schema.json: ${result.schemaOutputPath}`);
3533
+ logger.out(` - ERD viewer: ${path.join(result.distDir, "index.html")}`);
3480
3534
  }
3481
3535
  }
3482
3536
  });
@@ -3495,11 +3549,9 @@ const erdDeployCommand = defineAppCommand({
3495
3549
  }).strict(),
3496
3550
  run: async (args) => {
3497
3551
  await assertWritable({ profile: args.profile });
3498
- const { client, workspaceId, config } = await initErdContext(args);
3552
+ const { client, workspaceId } = await initErdDeployContext(args);
3499
3553
  const buildResults = await prepareErdBuilds({
3500
- client,
3501
- workspaceId,
3502
- config,
3554
+ configPath: args.config,
3503
3555
  namespace: args.namespace,
3504
3556
  requireErdSite: true
3505
3557
  });
@@ -3527,65 +3579,354 @@ const erdDeployCommand = defineAppCommand({
3527
3579
 
3528
3580
  //#endregion
3529
3581
  //#region src/cli/commands/tailordb/erd/serve.ts
3582
+ const DEFAULT_ERD_BASE_DIR = ".tailor-sdk/erd";
3583
+ const LOCAL_HOST = "127.0.0.1";
3584
+ const GLOB_CHARS = /[*?[\]{}()!+@]/;
3530
3585
  function formatServeCommand(namespace) {
3531
3586
  return `tailor-sdk tailordb erd serve --namespace ${namespace}`;
3532
3587
  }
3533
- async function runServeDist(results) {
3534
- if (results.length === 0) throw new Error("No ERD build results found.");
3588
+ function getCacheControl(filePath) {
3589
+ return filePath.endsWith(".html") || filePath.endsWith(".json") ? "no-cache" : "public, max-age=3600";
3590
+ }
3591
+ function resolveRequestPath(distDir, requestUrl) {
3592
+ const url = new URL(requestUrl ?? "/", "http://localhost");
3593
+ let pathname;
3594
+ try {
3595
+ pathname = decodeURIComponent(url.pathname);
3596
+ } catch {
3597
+ return;
3598
+ }
3599
+ if (pathname === "/" || pathname.endsWith("/")) pathname = path.join(pathname, "index.html");
3600
+ const root = path.resolve(distDir);
3601
+ const filePath = path.resolve(root, `.${pathname}`);
3602
+ if (filePath !== root && !filePath.startsWith(`${root}${path.sep}`)) return;
3603
+ return filePath;
3604
+ }
3605
+ function openStaticFile(filePath) {
3606
+ let fd;
3607
+ try {
3608
+ fd = fs$1.openSync(filePath, "r");
3609
+ if (!fs$1.fstatSync(fd).isFile()) {
3610
+ fs$1.closeSync(fd);
3611
+ return;
3612
+ }
3613
+ return {
3614
+ filePath,
3615
+ fd
3616
+ };
3617
+ } catch {
3618
+ if (fd !== void 0) fs$1.closeSync(fd);
3619
+ return;
3620
+ }
3621
+ }
3622
+ function serveFile(distDir, req, res) {
3623
+ const filePath = resolveRequestPath(distDir, req.url);
3624
+ if (!filePath) {
3625
+ res.writeHead(403);
3626
+ res.end("Forbidden");
3627
+ return;
3628
+ }
3629
+ const fallbackPath = path.join(distDir, "index.html");
3630
+ const target = openStaticFile(filePath) ?? openStaticFile(fallbackPath);
3631
+ if (!target) {
3632
+ res.writeHead(503, {
3633
+ "Content-Type": "text/plain; charset=utf-8",
3634
+ "Cache-Control": "no-cache",
3635
+ "Retry-After": "1"
3636
+ });
3637
+ res.end("ERD build is refreshing. Please retry.");
3638
+ return;
3639
+ }
3640
+ const mimeType = lookup(target.filePath) || "application/octet-stream";
3641
+ const stream = fs$1.createReadStream(target.filePath, {
3642
+ fd: target.fd,
3643
+ autoClose: true
3644
+ });
3645
+ stream.on("error", () => {
3646
+ if (!res.headersSent) {
3647
+ res.writeHead(503, {
3648
+ "Content-Type": "text/plain; charset=utf-8",
3649
+ "Cache-Control": "no-cache",
3650
+ "Retry-After": "1"
3651
+ });
3652
+ res.end("ERD build is refreshing. Please retry.");
3653
+ return;
3654
+ }
3655
+ res.destroy();
3656
+ });
3657
+ res.writeHead(200, {
3658
+ "Content-Type": mimeType,
3659
+ "Cache-Control": getCacheControl(target.filePath)
3660
+ });
3661
+ stream.pipe(res);
3662
+ }
3663
+ async function startStaticServer(options) {
3664
+ const server = http.createServer((req, res) => {
3665
+ serveFile(options.distDir, req, res);
3666
+ });
3667
+ return await new Promise((resolve, reject) => {
3668
+ server.once("error", reject);
3669
+ server.listen(options.port, LOCAL_HOST, () => {
3670
+ server.off("error", reject);
3671
+ const address = server.address();
3672
+ if (!address || typeof address === "string") {
3673
+ reject(/* @__PURE__ */ new Error("Failed to determine ERD server address."));
3674
+ return;
3675
+ }
3676
+ resolve({
3677
+ server,
3678
+ url: `http://${LOCAL_HOST}:${address.port}`
3679
+ });
3680
+ });
3681
+ });
3682
+ }
3683
+ function getWatchPatterns(config, results) {
3684
+ const namespaces = new Set(results.map((result) => result.namespace));
3685
+ const patterns = [config.path];
3686
+ for (const namespace of namespaces) {
3687
+ const dbConfig = config.db?.[namespace];
3688
+ if (dbConfig && !("external" in dbConfig)) patterns.push(...dbConfig.files);
3689
+ }
3690
+ return [...new Set(patterns)];
3691
+ }
3692
+ function hasGlobPattern(pattern) {
3693
+ return GLOB_CHARS.test(pattern);
3694
+ }
3695
+ function globBaseDir(pattern) {
3696
+ const absolutePattern = path.resolve(pattern);
3697
+ const parsed = path.parse(absolutePattern);
3698
+ const relativePattern = absolutePattern.slice(parsed.root.length);
3699
+ const literalParts = [];
3700
+ for (const part of relativePattern.split(path.sep)) {
3701
+ if (!part || GLOB_CHARS.test(part)) break;
3702
+ literalParts.push(part);
3703
+ }
3704
+ const literalPath = literalParts.length > 0 ? path.join(parsed.root, ...literalParts) : parsed.root;
3705
+ if (!literalPath || literalPath === parsed.root) return parsed.root || process.cwd();
3706
+ if (!fs$1.existsSync(literalPath)) return path.dirname(literalPath);
3707
+ return fs$1.statSync(literalPath).isDirectory() ? literalPath : path.dirname(literalPath);
3708
+ }
3709
+ async function expandWatchPattern(pattern) {
3710
+ if (!hasGlobPattern(pattern)) return [path.resolve(pattern)];
3711
+ const paths = /* @__PURE__ */ new Set();
3712
+ for await (const file of glob(pattern)) paths.add(path.resolve(file));
3713
+ const baseDir = globBaseDir(pattern);
3714
+ if (fs$1.existsSync(baseDir) && fs$1.statSync(baseDir).isDirectory()) paths.add(baseDir);
3715
+ return [...paths];
3716
+ }
3717
+ async function resolveWatchPathsFromConfig(config, results) {
3718
+ const paths = /* @__PURE__ */ new Set();
3719
+ for (const pattern of getWatchPatterns(config, results)) for (const watchPath of await expandWatchPattern(pattern)) paths.add(watchPath);
3720
+ return [...paths];
3721
+ }
3722
+ async function resolveWatchPaths(context, results) {
3723
+ return await resolveWatchPathsFromConfig(context.config, results);
3724
+ }
3725
+ function parseFreshErdExportResults(stdout) {
3726
+ const lines = stdout.trim().split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
3727
+ for (const line of lines.toReversed()) try {
3728
+ const parsed = JSON.parse(line);
3729
+ if (!Array.isArray(parsed)) continue;
3730
+ return parsed.map((entry) => {
3731
+ const result = entry;
3732
+ if (typeof result.namespace !== "string" || typeof result.distDir !== "string") throw new Error("Invalid ERD export JSON output.");
3733
+ return {
3734
+ namespace: result.namespace,
3735
+ distDir: result.distDir
3736
+ };
3737
+ });
3738
+ } catch {
3739
+ continue;
3740
+ }
3741
+ throw new Error("Failed to parse ERD export JSON output.");
3742
+ }
3743
+ function freshErdExportArgs(options) {
3744
+ const cliEntry = process.argv[1];
3745
+ if (!cliEntry) throw new Error("Cannot rebuild ERD schema in a fresh process: CLI entrypoint is unavailable.");
3746
+ const args = [
3747
+ cliEntry,
3748
+ "tailordb",
3749
+ "erd",
3750
+ "export",
3751
+ "--output",
3752
+ options.outputDir,
3753
+ "--json"
3754
+ ];
3755
+ if (options.configPath) args.push("--config", options.configPath);
3756
+ if (options.namespace) args.push("--namespace", options.namespace);
3757
+ return args;
3758
+ }
3759
+ async function runFreshErdExport(options) {
3760
+ return await new Promise((resolve, reject) => {
3761
+ const child = spawn(process.execPath, freshErdExportArgs(options), {
3762
+ cwd: process.cwd(),
3763
+ env: process.env,
3764
+ stdio: [
3765
+ "ignore",
3766
+ "pipe",
3767
+ "pipe"
3768
+ ]
3769
+ });
3770
+ let stdout = "";
3771
+ let stderr = "";
3772
+ child.stdout.setEncoding("utf8");
3773
+ child.stderr.setEncoding("utf8");
3774
+ child.stdout.on("data", (chunk) => {
3775
+ stdout += chunk;
3776
+ });
3777
+ child.stderr.on("data", (chunk) => {
3778
+ stderr += chunk;
3779
+ });
3780
+ child.on("error", reject);
3781
+ child.on("close", (code, signal) => {
3782
+ if (code !== 0) {
3783
+ const detail = stderr.trim() || signal || `exit code ${code}`;
3784
+ reject(/* @__PURE__ */ new Error(`Fresh ERD export failed: ${detail}`));
3785
+ return;
3786
+ }
3787
+ try {
3788
+ resolve(parseFreshErdExportResults(stdout));
3789
+ } catch (error) {
3790
+ reject(error);
3791
+ }
3792
+ });
3793
+ });
3794
+ }
3795
+ function selectPrimaryResult(results) {
3535
3796
  const [primary, ...rest] = results;
3797
+ if (!primary) throw new Error("No ERD build results found.");
3536
3798
  logger.info(`Serving ERD for namespace "${primary.namespace}".`);
3537
3799
  if (rest.length > 0) {
3538
3800
  const commands = rest.map((result) => ` - ${formatServeCommand(result.namespace)}`).join("\n");
3539
3801
  logger.warn(`Multiple namespaces found. To serve another namespace, run:\n${commands}`);
3540
3802
  }
3541
- fs$1.mkdirSync(primary.erdDir, { recursive: true });
3542
- return await new Promise((resolve, reject) => {
3543
- let serveBinPath;
3803
+ return primary;
3804
+ }
3805
+ async function createErdWatcher(options) {
3806
+ let rebuilding = false;
3807
+ let pending = false;
3808
+ let watchPaths = await resolveWatchPaths(options.initialContext, options.initialResults);
3809
+ let importNonce = 0;
3810
+ const watcher = watch(watchPaths, {
3811
+ ignored: /node_modules/,
3812
+ ignoreInitial: true,
3813
+ awaitWriteFinish: {
3814
+ stabilityThreshold: 100,
3815
+ pollInterval: 100
3816
+ }
3817
+ });
3818
+ async function rebuild() {
3819
+ if (rebuilding) {
3820
+ pending = true;
3821
+ return;
3822
+ }
3823
+ rebuilding = true;
3544
3824
  try {
3545
- serveBinPath = resolveCliBinPath({
3546
- packageName: "serve",
3547
- binName: "serve"
3825
+ const results = await runFreshErdExport({
3826
+ configPath: options.configPath,
3827
+ namespace: options.namespace,
3828
+ outputDir: options.outputDir
3548
3829
  });
3830
+ const { config } = await loadConfig(options.configPath, { importNonce: String(importNonce += 1) });
3831
+ const nextWatchPaths = await resolveWatchPathsFromConfig(config, results);
3832
+ watcher.unwatch(watchPaths);
3833
+ watcher.add(nextWatchPaths);
3834
+ watchPaths = nextWatchPaths;
3835
+ logger.success(`Rebuilt ERD schema (${results.map((result) => result.namespace).join(", ")})`, { mode: "stream" });
3549
3836
  } catch (error) {
3837
+ logger.error("Failed to rebuild ERD schema. Serving the last successful build.", { mode: "stream" });
3550
3838
  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}`));
3839
+ } finally {
3840
+ rebuilding = false;
3841
+ if (pending) {
3842
+ pending = false;
3843
+ await rebuild();
3567
3844
  }
3568
- });
3845
+ }
3846
+ }
3847
+ let debounceTimer;
3848
+ const scheduleRebuild = (changedPath) => {
3849
+ logger.info(`Schema source changed: ${path.relative(process.cwd(), changedPath)}`, { mode: "stream" });
3850
+ if (debounceTimer) clearTimeout(debounceTimer);
3851
+ debounceTimer = setTimeout(() => {
3852
+ rebuild();
3853
+ }, 150);
3854
+ };
3855
+ watcher.on("add", scheduleRebuild);
3856
+ watcher.on("change", scheduleRebuild);
3857
+ watcher.on("unlink", scheduleRebuild);
3858
+ watcher.on("error", (error) => {
3859
+ logger.error(`ERD watcher error: ${String(error)}`, { mode: "stream" });
3860
+ });
3861
+ return watcher;
3862
+ }
3863
+ async function waitForShutdown(server, watcher) {
3864
+ return await new Promise((resolve) => {
3865
+ const shutdown = () => {
3866
+ watcher.close().finally(() => {
3867
+ server.close(() => {
3868
+ logger.info("ERD server stopped.");
3869
+ resolve();
3870
+ });
3871
+ });
3872
+ };
3873
+ process.once("SIGINT", shutdown);
3874
+ process.once("SIGTERM", shutdown);
3569
3875
  });
3570
3876
  }
3571
3877
  const erdServeCommand = defineAppCommand({
3572
3878
  name: "serve",
3573
- description: "Generate and serve ERD locally (liam build + serve dist). (beta)",
3879
+ description: "Generate and serve TailorDB ERD locally with watch reload. (beta)",
3574
3880
  args: z.object({
3575
- ...deploymentArgs,
3881
+ ...configArg,
3576
3882
  namespace: arg(z.string().optional(), {
3577
3883
  alias: "n",
3578
3884
  description: "TailorDB namespace name (uses first namespace in config if not specified)"
3579
- })
3885
+ }),
3886
+ port: arg(z.coerce.number().int().min(0).max(65535).default(0), { description: "Local server port (0 selects a free port)" }),
3887
+ open: arg(z.boolean().default(false), { description: "Open the ERD viewer in the default browser" })
3580
3888
  }).strict(),
3581
3889
  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
- }));
3890
+ initErdCommand();
3891
+ const outputDir = path.resolve(process.cwd(), DEFAULT_ERD_BASE_DIR);
3892
+ const context = await loadLocalErdSchema({
3893
+ configPath: args.config,
3894
+ namespaces: args.namespace ? [args.namespace] : void 0
3895
+ });
3896
+ const results = prepareErdBuildsFromContext({
3897
+ context,
3898
+ namespace: args.namespace,
3899
+ outputDir
3900
+ });
3901
+ const primary = selectPrimaryResult(results);
3902
+ const { server, url } = await startStaticServer({
3903
+ distDir: primary.distDir,
3904
+ port: args.port
3905
+ });
3906
+ const watchUrl = `${url}/?watch=1`;
3907
+ const watcher = await createErdWatcher({
3908
+ configPath: args.config,
3909
+ namespace: args.namespace,
3910
+ outputDir,
3911
+ initialContext: context,
3912
+ initialResults: results
3913
+ });
3914
+ logger.newline();
3915
+ if (args.json) logger.out({
3916
+ namespace: primary.namespace,
3917
+ url: watchUrl,
3918
+ distDir: primary.distDir
3919
+ });
3920
+ else {
3921
+ logger.success("ERD server started.");
3922
+ logger.out(watchUrl);
3923
+ }
3924
+ if (args.open) try {
3925
+ await open(watchUrl);
3926
+ } catch {
3927
+ logger.warn("Failed to open browser automatically. Please open the URL above manually.");
3928
+ }
3929
+ await waitForShutdown(server, watcher);
3589
3930
  }
3590
3931
  });
3591
3932
 
@@ -3593,7 +3934,7 @@ const erdServeCommand = defineAppCommand({
3593
3934
  //#region src/cli/commands/tailordb/erd/index.ts
3594
3935
  const erdCommand = defineCommand({
3595
3936
  name: "erd",
3596
- description: "Generate ERD artifacts for TailorDB namespaces using Liam ERD. (beta)",
3937
+ description: "Generate TailorDB ERD viewer artifacts from local TailorDB schema. (beta)",
3597
3938
  subCommands: {
3598
3939
  export: erdExportCommand,
3599
3940
  serve: erdServeCommand,