@tailor-platform/sdk 2.0.0-next.5 → 2.0.0-next.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/dist/{application-Bf8wxOi6.mjs → application-BJXRpQj5.mjs} +190 -1258
- package/dist/application-BJXRpQj5.mjs.map +1 -0
- package/dist/application-BV-AXawv.mjs +3 -0
- package/dist/cli/commands/deploy/deploy.d.mts +8 -3
- package/dist/cli/commands/generate/options.d.mts +0 -1
- package/dist/cli/commands/generate/service.d.mts +1 -1
- package/dist/cli/commands/staticwebsite/deploy.d.mts +19 -0
- package/dist/cli/commands/workspace/create.d.mts +3 -0
- package/dist/cli/commands/workspace/list.d.mts +1 -0
- package/dist/cli/commands/workspace/transform.d.mts +2 -0
- package/dist/cli/index.mjs +776 -1551
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/lib.d.mts +7 -3
- package/dist/cli/lib.mjs +49 -3
- package/dist/cli/lib.mjs.map +1 -1
- package/dist/cli/shared/config-loader.d.mts +1 -1
- package/dist/cli/shared/readonly-guard.d.mts +25 -0
- package/dist/cli/shared/tailordb-namespaces.d.mts +37 -0
- package/dist/cli/shared/workspace-name.d.mts +1 -0
- package/dist/completion/zsh-worker.zsh +15 -303
- package/dist/configure/index.mjs +7 -6
- package/dist/configure/index.mjs.map +1 -1
- package/dist/configure/services/executor/executor.d.mts +2 -13
- package/dist/configure/services/executor/operation.d.mts +7 -2
- package/dist/configure/services/tailordb/schema.d.mts +1 -7
- package/dist/configure/services/tailordb/types.d.mts +1 -1
- package/dist/configure/services/workflow/execution-policy.d.mts +2 -2
- package/dist/configure/types/type.d.mts +1 -9
- package/dist/{crashreport-D3RjuSYb.mjs → crashreport-0ioNRZ4K.mjs} +1 -1
- package/dist/{crashreport-Cg-pqRJV.mjs → crashreport-CxHne11E.mjs} +4 -3
- package/dist/{crashreport-Cg-pqRJV.mjs.map → crashreport-CxHne11E.mjs.map} +1 -1
- package/dist/errors-DfnF4ibq.mjs +100 -0
- package/dist/errors-DfnF4ibq.mjs.map +1 -0
- package/dist/{globals-BjhJJ2dN.mjs → globals-D-YbJKW-.mjs} +16 -10
- package/dist/globals-D-YbJKW-.mjs.map +1 -0
- package/dist/{logger-BEiZZ3qT.mjs → logger-BwS4ppwO.mjs} +3 -3
- package/dist/logger-BwS4ppwO.mjs.map +1 -0
- package/dist/parser/service/tailordb/type-source.d.mts +10 -0
- package/dist/{register-ts-hook-BhzVkej0.mjs → register-ts-hook-DL31O2W9.mjs} +2623 -1645
- package/dist/register-ts-hook-DL31O2W9.mjs.map +1 -0
- package/dist/{registry-BSbq6SPo.mjs → registry-CC3CbQiF.mjs} +3 -3
- package/dist/registry-CC3CbQiF.mjs.map +1 -0
- package/dist/runtime/field-parse.d.mts +10 -0
- package/dist/runtime/globals.d.mts +5 -2
- package/dist/runtime/index.mjs +1 -1
- package/dist/runtime/workflow.d.mts +70 -10
- package/dist/runtime/workflow.mjs +1 -1
- package/dist/{schema-iCd1ugGp.mjs → schema--xYWRGfe.mjs} +42 -24
- package/dist/schema--xYWRGfe.mjs.map +1 -0
- package/dist/{secret-file-aoNXJmKa.mjs → secret-file-C3J4Gy8r.mjs} +2 -27
- package/dist/secret-file-C3J4Gy8r.mjs.map +1 -0
- package/dist/{secretmanager-Bd45j7an.mjs → secretmanager-IY4UvinW.mjs} +42 -9
- package/dist/secretmanager-IY4UvinW.mjs.map +1 -0
- package/dist/{service-CzZEVrp9.mjs → service-CnHz9rwz.mjs} +34 -12
- package/dist/service-CnHz9rwz.mjs.map +1 -0
- package/dist/{service-DeZeYa06.mjs → service-DaeJlBoH.mjs} +3 -3
- package/dist/{service-DeZeYa06.mjs.map → service-DaeJlBoH.mjs.map} +1 -1
- package/dist/service-nU6ITOHL.mjs +3 -0
- package/dist/service_pb-BoLM0X4K.mjs +167 -0
- package/dist/service_pb-BoLM0X4K.mjs.map +1 -0
- package/dist/{service_pb-DZkfjB_6.mjs → service_pb-r1T7kpJc.mjs} +1 -1
- package/dist/types/executor.generated.d.mts +60 -42
- package/dist/user-agent-Bgsszb5I.mjs +28 -0
- package/dist/user-agent-Bgsszb5I.mjs.map +1 -0
- package/dist/vitest/environment.mjs +1 -1
- package/dist/vitest/index.d.mts +8 -8
- package/dist/vitest/index.mjs +591 -173
- package/dist/vitest/index.mjs.map +1 -1
- package/dist/vitest/mock.d.mts +8 -8
- package/dist/vitest/mocks/aigateway.d.mts +14 -4
- package/dist/vitest/mocks/authconnection.d.mts +14 -4
- package/dist/vitest/mocks/file.d.mts +23 -6
- package/dist/vitest/mocks/iconv.d.mts +28 -4
- package/dist/vitest/mocks/idp.d.mts +25 -9
- package/dist/vitest/mocks/secretmanager.d.mts +13 -4
- package/dist/vitest/mocks/tailordb.d.mts +48 -7
- package/dist/vitest/mocks/workflow.d.mts +68 -22
- package/dist/vitest/setup.mjs +2 -2
- package/dist/workflow-9kHGKxF2.mjs +64 -0
- package/dist/workflow-9kHGKxF2.mjs.map +1 -0
- package/dist/{service_pb-Bk7dL-yT.mjs → workspace_resource_pb-Db3fv68L.mjs} +47 -165
- package/dist/workspace_resource_pb-Db3fv68L.mjs.map +1 -0
- package/docs/cli/application.md +61 -15
- package/docs/cli/tailordb.md +8 -140
- package/docs/cli/workspace.md +6 -4
- package/docs/cli-reference.md +11 -8
- package/docs/github-actions.md +8 -0
- package/docs/migration/v2.md +41 -0
- package/docs/runtime.md +5 -6
- package/docs/services/executor.md +5 -0
- package/docs/services/tailordb.md +44 -0
- package/docs/services/workflow.md +2 -2
- package/docs/testing.md +105 -89
- package/package.json +2 -5
- package/dist/application-Bf8wxOi6.mjs.map +0 -1
- package/dist/application-CUkjzfij.mjs +0 -3
- package/dist/cli/erd-viewer-assets/app.js +0 -1424
- package/dist/cli/erd-viewer-assets/index.html +0 -77
- package/dist/cli/erd-viewer-assets/serve.json +0 -13
- package/dist/cli/erd-viewer-assets/styles.css +0 -1036
- package/dist/configure/types/field-runtime.d.mts +0 -16
- package/dist/errors-118wUDRr.mjs +0 -42
- package/dist/errors-118wUDRr.mjs.map +0 -1
- package/dist/globals-BjhJJ2dN.mjs.map +0 -1
- package/dist/logger-BEiZZ3qT.mjs.map +0 -1
- package/dist/register-ts-hook-BhzVkej0.mjs.map +0 -1
- package/dist/registry-BSbq6SPo.mjs.map +0 -1
- package/dist/resource_pb-BRv7AMXK.mjs +0 -49
- package/dist/resource_pb-BRv7AMXK.mjs.map +0 -1
- package/dist/schema-iCd1ugGp.mjs.map +0 -1
- package/dist/secret-file-aoNXJmKa.mjs.map +0 -1
- package/dist/secretmanager-Bd45j7an.mjs.map +0 -1
- package/dist/service-BT0GPd01.mjs +0 -3
- package/dist/service-CzZEVrp9.mjs.map +0 -1
- package/dist/service_pb-Bk7dL-yT.mjs.map +0 -1
- package/dist/workflow-GNuSnsTx.mjs +0 -22
- package/dist/workflow-GNuSnsTx.mjs.map +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { r as parseInputFields } from "../schema--xYWRGfe.mjs";
|
|
3
|
+
import { St as AuthInvokerSchema, T as CustomDomainStatus, U as FunctionExecution_Type, kt as PATScope } from "../workspace_resource_pb-Db3fv68L.mjs";
|
|
2
4
|
import { t as assertDefined } from "../assert-DBxo8jPo.mjs";
|
|
3
|
-
import { n as logger, r as styles } from "../logger-
|
|
4
|
-
import { $ as
|
|
5
|
-
import {
|
|
6
|
-
import { i as ExecutorSchema, r as stripExecutorTriggerArgs } from "../service-
|
|
5
|
+
import { n as logger, r as styles } from "../logger-BwS4ppwO.mjs";
|
|
6
|
+
import { $n as saveUserTokens, An as sdkNameLabelKey, B as extractOwnedNamespaces, Bn as hasUserTokenEntry, Bt as getCommand$1, Cn as formatMigrationNumber, Ct as triggerCommand, Dn as hasChanges, E as listCommand$14, En as formatMigrationDiff, F as openInConfiguredEditor, Fn as assertWritable, Gn as loadPlatformClientConfig, Hn as loadConfigPath, I as deployCommand$1, In as loadConfig, It as executionsCommand, J as listCommand$13, Jn as platformConfigFromProfile, Jt as formatKeyValueTable, Kn as loadStoredUserTokens, Kt as createCommand$4, Ln as deleteUserTokens, M as generateMigrationScript, Mt as startCommand, N as writeDbTypesFile, Nn as prompt, On as getNamespacesWithMigrations, P as getConfiguredEditorCommand, Pn as apiCall, Pt as getCommand$6, Q as updateCommand$2, Qn as resolveUserTokenKey, Rn as fetchLatestToken, S as waitCommand, Sn as reconstructSnapshotFromMigrations, St as webhookCommand, Tt as listCommand$8, U as removeCommand$1, Un as loadConsoleBaseUrl, Ut as deployFromCLI, V as logBetaWarning, Vn as loadAccessToken, W as updateCommand$3, Wn as loadMachineUserName, Wt as listCommand$17, X as getCommand$5, Xn as removeLegacyUserAlias, Xt as ensureConfigId, Yn as readPlatformConfig, Yt as workspaceNameSchema, Zn as resolveConfigUser, Zt as executeScript, _n as getMigrationFilePath, _t as listCommand$9, an as protoGqlPermission, ar as deploymentArgs, bn as isValidMigrationNumber, c as listCommand$16, cn as INITIAL_SCHEMA_NUMBER, cr as pagedLogArgs, ct as listCommand$11, dn as assertValidMigrationFiles, dr as toPageDirection, en as MIGRATION_LABEL_KEY, er as writePlatformConfig, et as listCommand$12, f as restoreCommand, fn as compareLocalTypesWithSnapshot, fr as workspaceArgs, g as deleteCommand$5, gt as generate, hn as getLatestMigrationNumber, i as updateCommand$4, in as generateAllTypeManifestsFromSnapshot, ir as confirmationArgs, it as deleteCommand$4, j as generateCommand$1, jn as PluginManager, k as truncateCommand, kn as resourceTrn, kt as jobsCommand, lr as paginationArgs, m as getCommand$7, mn as createSnapshotFromLocalTypes, mt as listCommand$10, nn as parseMigrationLabelNumber, nr as commonArgs, nt as getCommand$4, o as removeCommand, or as isVerbose, ot as createCommand$3, pt as tokenCommand$1, q as treeCommand, qn as loadWorkspaceId, r as queryCommand, rn as compareSnapshotWithRemote, rr as configArg, sr as multiConfigArg, t as registerTsHook, tn as handleOptionalToRequiredError, tr as defineAppCommand, u as inviteCommand, ur as resolveMachineUserInputSource, ut as getCommand$3, vn as getMigrationFiles, w as resumeCommand, wn as parseMigrationNumberArg, x as healthCommand, xn as loadDiff, y as listCommand$15, yt as getCommand$2, z as showCommand, zn as hasAnyUserTokenEntry, zt as functionExecutionStatusToString } from "../register-ts-hook-DL31O2W9.mjs";
|
|
7
|
+
import { A as fetchAllTolerant, B as initOperatorClient, D as closeConnectionPool, I as getOAuth2ClientId, L as getOrNull, M as fetchPaged, N as fetchPlatformMachineUserToken, O as defaultPlatformBaseUrl, P as fetchUserInfo, R as getPlatformBaseUrl, V as isDefaultPlatform, _ as platformBundleDefinePlugin, a as resolveInlineSourcemap, b as resolveBundleLogLevel, c as WorkflowJobSchema, d as INVOKER_EXPR, k as fetchAll, l as ResolverSchema, v as composeFunctionTreeshakeOptions, w as getDistDir, y as createLogLevelTreeshakeOptions, z as initOAuth2Client } from "../application-BJXRpQj5.mjs";
|
|
8
|
+
import { i as ExecutorSchema, r as stripExecutorTriggerArgs } from "../service-CnHz9rwz.mjs";
|
|
7
9
|
import { t as multiline } from "../multiline-sfHpTZZK.mjs";
|
|
8
10
|
import { t as readPackageJson } from "../package-json-8b0O9TlX.mjs";
|
|
9
|
-
import {
|
|
10
|
-
import { n as isCLIError } from "../errors-
|
|
11
|
-
import {
|
|
12
|
-
import { a as JSON_FOOTER_MARKER, i as CRASH_LOG_EXTENSION, o as parseCrashReportConfig, r as sendCrashReport, t as initCrashReporting } from "../crashreport-
|
|
13
|
-
import { t as isPluginGeneratedType } from "../type-source-DH_LH20p.mjs";
|
|
11
|
+
import { t as userAgent } from "../user-agent-Bgsszb5I.mjs";
|
|
12
|
+
import { n as errorToJson, r as isCLIError, t as createCLIError } from "../errors-DfnF4ibq.mjs";
|
|
13
|
+
import { t as OperatorService } from "../service_pb-BoLM0X4K.mjs";
|
|
14
|
+
import { a as JSON_FOOTER_MARKER, i as CRASH_LOG_EXTENSION, o as parseCrashReportConfig, r as sendCrashReport, t as initCrashReporting } from "../crashreport-CxHne11E.mjs";
|
|
14
15
|
import * as path from "pathe";
|
|
15
16
|
import { dirname, resolve } from "pathe";
|
|
16
17
|
import { resolvePackageJSON, resolveTSConfig } from "pkg-types";
|
|
@@ -18,29 +19,578 @@ import { arg, defineCommand, runCommand, runMain } from "politty";
|
|
|
18
19
|
import { withCompletionCommand } from "politty/completion";
|
|
19
20
|
import { withSkillCommand } from "politty/skill";
|
|
20
21
|
import { z } from "zod";
|
|
21
|
-
import { create } from "@bufbuild/protobuf";
|
|
22
|
-
import * as fs
|
|
22
|
+
import { ScalarType, create } from "@bufbuild/protobuf";
|
|
23
|
+
import * as fs from "node:fs";
|
|
23
24
|
import { accessSync, constants, readdirSync } from "node:fs";
|
|
24
25
|
import { timestampDate } from "@bufbuild/protobuf/wkt";
|
|
25
|
-
import {
|
|
26
|
+
import { pathToFileURL } from "node:url";
|
|
26
27
|
import { generateCodeVerifier } from "@badgateway/oauth2-client";
|
|
27
28
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
28
|
-
import pLimit from "p-limit";
|
|
29
29
|
import * as crypto from "node:crypto";
|
|
30
30
|
import { createHash } from "node:crypto";
|
|
31
31
|
import * as http from "node:http";
|
|
32
32
|
import open from "open";
|
|
33
33
|
import * as rolldown from "rolldown";
|
|
34
|
-
import * as fsPromises from "node:fs/promises";
|
|
35
|
-
import { glob } from "node:fs/promises";
|
|
36
34
|
import * as os from "node:os";
|
|
37
|
-
import
|
|
35
|
+
import * as fsPromises from "node:fs/promises";
|
|
38
36
|
import { TraceMap, generatedPositionFor, originalPositionFor } from "@jridgewell/trace-mapping";
|
|
39
37
|
import { spawn, spawnSync } from "node:child_process";
|
|
40
|
-
import { watch } from "chokidar";
|
|
41
|
-
import * as fs from "fs";
|
|
42
|
-
import { lookup } from "mime-types";
|
|
43
38
|
|
|
39
|
+
//#region src/cli/commands/api/proto-reflect.ts
|
|
40
|
+
const UNARY_METHODS = OperatorService.methods.filter((m) => m.methodKind === "unary");
|
|
41
|
+
function listMethodNames() {
|
|
42
|
+
return UNARY_METHODS.map((m) => m.name).toSorted();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns every accepted form of the `endpoint` positional — bare method name
|
|
46
|
+
* and fully-qualified `service/Method`. politty's expand completion keys its
|
|
47
|
+
* static table by the literal endpoint string, so both forms must appear in
|
|
48
|
+
* `choices` or the user typing the fully-qualified name gets no `--field`
|
|
49
|
+
* completion candidates back at TAB time.
|
|
50
|
+
* @returns Sorted list of accepted endpoint values
|
|
51
|
+
*/
|
|
52
|
+
function listMethodChoices() {
|
|
53
|
+
return UNARY_METHODS.flatMap((m) => [m.name, `${OperatorService.typeName}/${m.name}`]).toSorted();
|
|
54
|
+
}
|
|
55
|
+
function getMethodDescriptor(methodName) {
|
|
56
|
+
return UNARY_METHODS.find((m) => m.name === methodName);
|
|
57
|
+
}
|
|
58
|
+
function extractMethodName(endpoint) {
|
|
59
|
+
if (!endpoint.includes("/")) return endpoint;
|
|
60
|
+
return endpoint.split("/").pop() ?? endpoint;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Returns the nested message descriptor when the field is a message, a list of
|
|
64
|
+
* messages, or a map with message values. Otherwise returns undefined.
|
|
65
|
+
* @param field - The proto field descriptor to inspect
|
|
66
|
+
* @returns The nested message descriptor, or undefined when the field isn't message-shaped
|
|
67
|
+
*/
|
|
68
|
+
function nestedMessage(field) {
|
|
69
|
+
if (field.fieldKind === "message") return field.message;
|
|
70
|
+
if (field.fieldKind === "list" && field.listKind === "message") return field.message;
|
|
71
|
+
if (field.fieldKind === "map" && field.mapKind === "message") return field.message;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Most `google.protobuf.*` well-known types serialize as scalars in proto JSON
|
|
75
|
+
* (Timestamp/Duration → RFC3339/duration string, FieldMask → comma-joined
|
|
76
|
+
* string, *Value wrappers → underlying primitive, etc.), so we must not let
|
|
77
|
+
* `--field` drill into their internal fields. Treating them as leaves keeps
|
|
78
|
+
* the completion and the body shape consistent with what the server expects.
|
|
79
|
+
* @param message - The proto message descriptor to inspect
|
|
80
|
+
* @returns True when the message is a well-known type
|
|
81
|
+
*/
|
|
82
|
+
function isWellKnownType(message) {
|
|
83
|
+
return message.typeName.startsWith("google.protobuf.");
|
|
84
|
+
}
|
|
85
|
+
const UNREPRESENTABLE_WELL_KNOWN_TYPES = /* @__PURE__ */ new Set([
|
|
86
|
+
"google.protobuf.Struct",
|
|
87
|
+
"google.protobuf.Value",
|
|
88
|
+
"google.protobuf.ListValue",
|
|
89
|
+
"google.protobuf.NullValue",
|
|
90
|
+
"google.protobuf.Any",
|
|
91
|
+
"google.protobuf.Empty"
|
|
92
|
+
]);
|
|
93
|
+
/**
|
|
94
|
+
* @param message - The proto message descriptor to inspect
|
|
95
|
+
* @returns True when the message is a well-known type that `--field` cannot represent
|
|
96
|
+
*/
|
|
97
|
+
function isUnrepresentableWellKnownType(message) {
|
|
98
|
+
return UNREPRESENTABLE_WELL_KNOWN_TYPES.has(message.typeName);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Pre-enumerates every `--field` completion candidate for `methodName`,
|
|
102
|
+
* walking the input message tree to produce a flat list that the shell can
|
|
103
|
+
* prefix-filter at TAB time. Used by the `expand` completion variant so no
|
|
104
|
+
* Node process is spawned per keystroke.
|
|
105
|
+
*
|
|
106
|
+
* For each leaf field, emits `key=` (key completion). Enum leaves additionally
|
|
107
|
+
* emit `key=ENUM_VALUE` per value, bool leaves emit `key=true` and
|
|
108
|
+
* `key=false`. For nested messages, emits `key.` (drill-down) and recurses.
|
|
109
|
+
*
|
|
110
|
+
* Returns an empty array when `methodName` is unknown so the `expand`
|
|
111
|
+
* generator stays exception-free at script-generation time.
|
|
112
|
+
* @param methodName - Name of the unary RPC whose input message is being walked
|
|
113
|
+
* @returns Flat list of `{ value, description }` candidates
|
|
114
|
+
*/
|
|
115
|
+
function enumerateAllFieldCompletions(methodName) {
|
|
116
|
+
const method = getMethodDescriptor(methodName);
|
|
117
|
+
if (!method) return [];
|
|
118
|
+
const candidates = [];
|
|
119
|
+
const visited = /* @__PURE__ */ new Set();
|
|
120
|
+
function walk(message, prefix) {
|
|
121
|
+
visited.add(message);
|
|
122
|
+
for (const field of message.fields) {
|
|
123
|
+
if (field.fieldKind === "list" || field.fieldKind === "map") continue;
|
|
124
|
+
const fullKey = prefix + field.localName;
|
|
125
|
+
if (field.fieldKind === "message") {
|
|
126
|
+
if (isUnrepresentableWellKnownType(field.message)) continue;
|
|
127
|
+
if (!isWellKnownType(field.message)) {
|
|
128
|
+
const nested = field.message;
|
|
129
|
+
candidates.push({
|
|
130
|
+
value: `${fullKey}.`,
|
|
131
|
+
description: `${fullKey} (message)`
|
|
132
|
+
});
|
|
133
|
+
if (!visited.has(nested)) walk(nested, `${fullKey}.`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
candidates.push({
|
|
138
|
+
value: `${fullKey}=`,
|
|
139
|
+
description: `Set ${fullKey}`
|
|
140
|
+
});
|
|
141
|
+
if (field.fieldKind === "enum") for (const v of field.enum.values) candidates.push({
|
|
142
|
+
value: `${fullKey}=${v.name}`,
|
|
143
|
+
description: v.name
|
|
144
|
+
});
|
|
145
|
+
else if (field.fieldKind === "scalar" && field.scalar === ScalarType.BOOL) {
|
|
146
|
+
candidates.push({
|
|
147
|
+
value: `${fullKey}=true`,
|
|
148
|
+
description: "true"
|
|
149
|
+
});
|
|
150
|
+
candidates.push({
|
|
151
|
+
value: `${fullKey}=false`,
|
|
152
|
+
description: "false"
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
visited.delete(message);
|
|
157
|
+
}
|
|
158
|
+
walk(method.input, "");
|
|
159
|
+
return candidates;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Resolves the leaf `DescField` at a dotted `--field` path within an input
|
|
163
|
+
* message tree. Returns undefined when any segment is missing or when a
|
|
164
|
+
* non-leaf segment isn't a singular message — callers fall back to treating
|
|
165
|
+
* the value as a string in that case.
|
|
166
|
+
* @param input - The RPC input message descriptor
|
|
167
|
+
* @param path - Dot-split path segments to follow (e.g. ["application", "name"])
|
|
168
|
+
* @returns The leaf field descriptor, or undefined when the path doesn't resolve
|
|
169
|
+
*/
|
|
170
|
+
function resolveLeafField(input, path) {
|
|
171
|
+
let message = input;
|
|
172
|
+
for (let i = 0; i < path.length; i++) {
|
|
173
|
+
const field = message.fields.find((f) => f.localName === path[i]);
|
|
174
|
+
if (!field) return void 0;
|
|
175
|
+
if (i === path.length - 1) {
|
|
176
|
+
if (field.fieldKind === "message" && isUnrepresentableWellKnownType(field.message)) return;
|
|
177
|
+
return field;
|
|
178
|
+
}
|
|
179
|
+
if (field.fieldKind !== "message") return void 0;
|
|
180
|
+
if (isWellKnownType(field.message)) return void 0;
|
|
181
|
+
message = field.message;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/cli/commands/api/render.ts
|
|
187
|
+
const SCALAR_LABEL = {
|
|
188
|
+
[ScalarType.DOUBLE]: "double",
|
|
189
|
+
[ScalarType.FLOAT]: "float",
|
|
190
|
+
[ScalarType.INT64]: "int64",
|
|
191
|
+
[ScalarType.UINT64]: "uint64",
|
|
192
|
+
[ScalarType.INT32]: "int32",
|
|
193
|
+
[ScalarType.FIXED64]: "fixed64",
|
|
194
|
+
[ScalarType.FIXED32]: "fixed32",
|
|
195
|
+
[ScalarType.BOOL]: "bool",
|
|
196
|
+
[ScalarType.STRING]: "string",
|
|
197
|
+
[ScalarType.BYTES]: "bytes",
|
|
198
|
+
[ScalarType.UINT32]: "uint32",
|
|
199
|
+
[ScalarType.SFIXED32]: "sfixed32",
|
|
200
|
+
[ScalarType.SFIXED64]: "sfixed64",
|
|
201
|
+
[ScalarType.SINT32]: "sint32",
|
|
202
|
+
[ScalarType.SINT64]: "sint64"
|
|
203
|
+
};
|
|
204
|
+
function shortName(typeName) {
|
|
205
|
+
const dot = typeName.lastIndexOf(".");
|
|
206
|
+
return dot < 0 ? typeName : typeName.slice(dot + 1);
|
|
207
|
+
}
|
|
208
|
+
function scalarLabel(scalar) {
|
|
209
|
+
return SCALAR_LABEL[scalar] ?? `scalar(${scalar})`;
|
|
210
|
+
}
|
|
211
|
+
function enumLabel(enumDesc) {
|
|
212
|
+
return `enum ${shortName(enumDesc.typeName)}`;
|
|
213
|
+
}
|
|
214
|
+
function enumOf(field) {
|
|
215
|
+
if (field.fieldKind === "enum") return field.enum;
|
|
216
|
+
if (field.fieldKind === "list" && field.listKind === "enum") return field.enum;
|
|
217
|
+
}
|
|
218
|
+
function describeFieldType(field) {
|
|
219
|
+
switch (field.fieldKind) {
|
|
220
|
+
case "scalar": return scalarLabel(field.scalar);
|
|
221
|
+
case "enum": return enumLabel(field.enum);
|
|
222
|
+
case "message": return shortName(field.message.typeName);
|
|
223
|
+
case "list": {
|
|
224
|
+
let inner;
|
|
225
|
+
if (field.listKind === "scalar") inner = scalarLabel(field.scalar);
|
|
226
|
+
else if (field.listKind === "enum") inner = enumLabel(field.enum);
|
|
227
|
+
else inner = shortName(field.message.typeName);
|
|
228
|
+
return `repeated ${inner}`;
|
|
229
|
+
}
|
|
230
|
+
case "map": {
|
|
231
|
+
const keyType = scalarLabel(field.mapKey);
|
|
232
|
+
let valType;
|
|
233
|
+
if (field.mapKind === "scalar") valType = scalarLabel(field.scalar);
|
|
234
|
+
else if (field.mapKind === "enum") valType = enumLabel(field.enum);
|
|
235
|
+
else valType = shortName(field.message.typeName);
|
|
236
|
+
return `map<${keyType}, ${valType}>`;
|
|
237
|
+
}
|
|
238
|
+
default: return "<unknown>";
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function fieldToJson(field, visited) {
|
|
242
|
+
const json = {
|
|
243
|
+
name: field.localName,
|
|
244
|
+
protoName: field.name,
|
|
245
|
+
type: describeFieldType(field),
|
|
246
|
+
fieldKind: field.fieldKind,
|
|
247
|
+
repeated: field.fieldKind === "list"
|
|
248
|
+
};
|
|
249
|
+
if (field.oneof) json.oneof = field.oneof.name;
|
|
250
|
+
const fieldEnum = enumOf(field);
|
|
251
|
+
if (fieldEnum) json.enumValues = fieldEnum.values.map((v) => v.name);
|
|
252
|
+
const nested = nestedMessage(field);
|
|
253
|
+
if (nested && !visited.has(nested)) {
|
|
254
|
+
visited.add(nested);
|
|
255
|
+
json.message = {
|
|
256
|
+
typeName: nested.typeName,
|
|
257
|
+
fields: nested.fields.map((f) => fieldToJson(f, visited))
|
|
258
|
+
};
|
|
259
|
+
visited.delete(nested);
|
|
260
|
+
} else if (nested) json.message = {
|
|
261
|
+
typeName: nested.typeName,
|
|
262
|
+
fields: [],
|
|
263
|
+
recursive: true
|
|
264
|
+
};
|
|
265
|
+
return json;
|
|
266
|
+
}
|
|
267
|
+
function renderInspectJson(method) {
|
|
268
|
+
const visited = /* @__PURE__ */ new Set([method.input]);
|
|
269
|
+
return {
|
|
270
|
+
method: method.name,
|
|
271
|
+
input: {
|
|
272
|
+
typeName: method.input.typeName,
|
|
273
|
+
fields: method.input.fields.map((f) => fieldToJson(f, visited))
|
|
274
|
+
},
|
|
275
|
+
output: { typeName: method.output.typeName }
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function renderFieldText(field, indent, visited) {
|
|
279
|
+
const lines = [];
|
|
280
|
+
const oneofTag = field.oneof ? ` (oneof ${field.oneof.name})` : "";
|
|
281
|
+
lines.push(`${indent}${field.localName}: ${describeFieldType(field)}${oneofTag}`);
|
|
282
|
+
const fieldEnum = enumOf(field);
|
|
283
|
+
if (fieldEnum) {
|
|
284
|
+
const values = fieldEnum.values.map((v) => v.name).join(", ");
|
|
285
|
+
lines.push(`${indent} values: ${values}`);
|
|
286
|
+
}
|
|
287
|
+
const nested = nestedMessage(field);
|
|
288
|
+
if (nested && !visited.has(nested)) {
|
|
289
|
+
visited.add(nested);
|
|
290
|
+
for (const sub of nested.fields) lines.push(...renderFieldText(sub, `${indent} `, visited));
|
|
291
|
+
visited.delete(nested);
|
|
292
|
+
} else if (nested) lines.push(`${indent} …(recursive ${nested.typeName})`);
|
|
293
|
+
return lines;
|
|
294
|
+
}
|
|
295
|
+
function renderInspectText(method) {
|
|
296
|
+
const lines = [];
|
|
297
|
+
lines.push(`${method.name}`);
|
|
298
|
+
lines.push(` request: ${method.input.typeName}`);
|
|
299
|
+
const visited = /* @__PURE__ */ new Set([method.input]);
|
|
300
|
+
for (const f of method.input.fields) lines.push(...renderFieldText(f, " ", visited));
|
|
301
|
+
lines.push(` response: ${method.output.typeName}`);
|
|
302
|
+
return lines.join("\n");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
//#endregion
|
|
306
|
+
//#region src/cli/commands/api/inspect.ts
|
|
307
|
+
const inspectCommand = defineAppCommand({
|
|
308
|
+
name: "inspect",
|
|
309
|
+
description: "Print the input message tree of an OperatorService endpoint.",
|
|
310
|
+
notes: "Combine with the global `--json` flag for a machine-readable descriptor. Recursive type references and `oneof` membership are annotated. Use `tailor api list` to discover endpoint names.",
|
|
311
|
+
examples: [{
|
|
312
|
+
cmd: "GetApplication",
|
|
313
|
+
desc: "Show fields of GetApplicationRequest."
|
|
314
|
+
}, {
|
|
315
|
+
cmd: "CreateExecutorExecutor",
|
|
316
|
+
desc: "Inspect a deeply nested input with `(oneof config)` annotations."
|
|
317
|
+
}],
|
|
318
|
+
args: z.strictObject({ endpoint: arg(z.string(), {
|
|
319
|
+
positional: true,
|
|
320
|
+
description: "API endpoint to inspect (e.g., 'GetApplication' or 'tailor.v1.OperatorService/GetApplication').",
|
|
321
|
+
completion: { custom: { choices: listMethodNames() } }
|
|
322
|
+
}) }),
|
|
323
|
+
run: (args) => {
|
|
324
|
+
const methodName = extractMethodName(args.endpoint);
|
|
325
|
+
const method = getMethodDescriptor(methodName);
|
|
326
|
+
if (!method) throw createCLIError({
|
|
327
|
+
message: `unknown method: ${methodName}`,
|
|
328
|
+
suggestion: "Run `tailor api list` to see available methods.",
|
|
329
|
+
command: "api inspect"
|
|
330
|
+
});
|
|
331
|
+
if (logger.jsonMode) logger.out(renderInspectJson(method));
|
|
332
|
+
else logger.out(renderInspectText(method));
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/cli/commands/api/list.ts
|
|
338
|
+
const listCommand$7 = defineAppCommand({
|
|
339
|
+
name: "list",
|
|
340
|
+
description: "List all invocable OperatorService methods.",
|
|
341
|
+
notes: "Only single-request (non-streaming) methods are listed, because the CLI issues a single JSON request and reads one JSON response.",
|
|
342
|
+
args: z.strictObject({}),
|
|
343
|
+
run: () => {
|
|
344
|
+
const names = listMethodNames();
|
|
345
|
+
if (logger.jsonMode) logger.out(names);
|
|
346
|
+
else for (const name of names) logger.out(name);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
//#endregion
|
|
351
|
+
//#region src/cli/commands/api/index.ts
|
|
352
|
+
function resolveNamespaceName(methodName, config) {
|
|
353
|
+
if (/Auth|Tenant|UserProfile/.test(methodName)) return config.auth?.name;
|
|
354
|
+
if (/IdP/.test(methodName)) {
|
|
355
|
+
if (config.idp?.length === 1) return assertDefined(config.idp[0], "idp config missing").name;
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (/TailorDB/.test(methodName)) {
|
|
359
|
+
const keys = Object.keys(config.db ?? {});
|
|
360
|
+
if (keys.length === 1) return keys[0];
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (/Pipeline/.test(methodName)) {
|
|
364
|
+
const keys = Object.keys(config.resolver ?? {});
|
|
365
|
+
if (keys.length === 1) return keys[0];
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Parse a JSON body string as a plain object for field injection.
|
|
371
|
+
* Returns undefined for invalid JSON or non-object values (null, arrays, primitives),
|
|
372
|
+
* so callers can fall back to sending the raw string unchanged.
|
|
373
|
+
* @param body - Raw body string
|
|
374
|
+
* @returns Parsed object or undefined
|
|
375
|
+
*/
|
|
376
|
+
function parseBodyAsObject(body) {
|
|
377
|
+
let parsed;
|
|
378
|
+
try {
|
|
379
|
+
parsed = JSON.parse(body);
|
|
380
|
+
} catch {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return;
|
|
384
|
+
return parsed;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Set a dotted path on a body object, replacing non-object intermediates as
|
|
388
|
+
* needed. `--field` takes precedence over `--body`, so collisions overwrite.
|
|
389
|
+
* @param obj - The body object to mutate
|
|
390
|
+
* @param path - Dot-split path segments (e.g. ["application", "name"])
|
|
391
|
+
* @param value - Value to assign at the leaf
|
|
392
|
+
*/
|
|
393
|
+
function setNestedPath(obj, path, value) {
|
|
394
|
+
let cursor = obj;
|
|
395
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
396
|
+
const key = assertDefined(path[i], "path segment missing");
|
|
397
|
+
const next = cursor[key];
|
|
398
|
+
if (typeof next !== "object" || next === null || Array.isArray(next)) cursor[key] = {};
|
|
399
|
+
cursor = cursor[key];
|
|
400
|
+
}
|
|
401
|
+
cursor[assertDefined(path[path.length - 1], "path last segment missing")] = value;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Coerces a raw `--field` string value to match the leaf proto type so the
|
|
405
|
+
* JSON body sends a properly typed value. Today we only coerce bool — the
|
|
406
|
+
* completion candidates explicitly suggest `=true`/`=false`, so sending the
|
|
407
|
+
* literal string "true" would be a visible mismatch. Other scalars are left
|
|
408
|
+
* as strings: proto JSON accepts string forms for ints/floats/int64/etc.,
|
|
409
|
+
* and we'd rather pass through what the user typed than silently coerce.
|
|
410
|
+
* @param field - The resolved leaf field descriptor, or undefined when the path didn't resolve
|
|
411
|
+
* @param raw - The raw string value after `=`
|
|
412
|
+
* @returns The value to write into the body object
|
|
413
|
+
*/
|
|
414
|
+
function coerceFieldValue(field, raw) {
|
|
415
|
+
if (field && field.fieldKind === "scalar" && field.scalar === ScalarType.BOOL) {
|
|
416
|
+
if (raw === "true") return true;
|
|
417
|
+
if (raw === "false") return false;
|
|
418
|
+
throw new Error(`Invalid value for bool field: '${raw}'. Expected 'true' or 'false'.`);
|
|
419
|
+
}
|
|
420
|
+
return raw;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Collapse top-level body keys that address a known input field by its
|
|
424
|
+
* protobuf name (snake_case) or JSON name to the field's `localName`. protojson
|
|
425
|
+
* accepts every alias, so converging them here lets the presence checks below
|
|
426
|
+
* reason in a single namespace — otherwise a `--body` written as
|
|
427
|
+
* `{"workspace_id": ...}` slips past the camelCase check and we inject a second
|
|
428
|
+
* `workspaceId`, which the server rejects as a duplicate field. When both forms
|
|
429
|
+
* are present the canonical key wins and the alias is dropped.
|
|
430
|
+
* @param body - The parsed body object to mutate
|
|
431
|
+
* @param fields - The target endpoint's input fields
|
|
432
|
+
* @returns Whether any key was rewritten
|
|
433
|
+
*/
|
|
434
|
+
function normalizeBodyFieldKeys(body, fields) {
|
|
435
|
+
const aliasToLocal = /* @__PURE__ */ new Map();
|
|
436
|
+
for (const f of fields) {
|
|
437
|
+
aliasToLocal.set(f.name, f.localName);
|
|
438
|
+
aliasToLocal.set(f.jsonName, f.localName);
|
|
439
|
+
}
|
|
440
|
+
let changed = false;
|
|
441
|
+
for (const key of Object.keys(body)) {
|
|
442
|
+
const local = aliasToLocal.get(key);
|
|
443
|
+
if (!local || local === key) continue;
|
|
444
|
+
if (!Object.hasOwn(body, local)) body[local] = body[key];
|
|
445
|
+
delete body[key];
|
|
446
|
+
changed = true;
|
|
447
|
+
}
|
|
448
|
+
return changed;
|
|
449
|
+
}
|
|
450
|
+
const FORBIDDEN_SEGMENTS = /* @__PURE__ */ new Set([
|
|
451
|
+
"__proto__",
|
|
452
|
+
"constructor",
|
|
453
|
+
"prototype"
|
|
454
|
+
]);
|
|
455
|
+
const fieldArg = z.string().transform((val, ctx) => {
|
|
456
|
+
const eq = val.indexOf("=");
|
|
457
|
+
if (eq < 0) {
|
|
458
|
+
ctx.addIssue({
|
|
459
|
+
code: z.ZodIssueCode.custom,
|
|
460
|
+
message: `Invalid field format: '${val}'. Expected format: 'key=value' or 'a.b.c=value'`
|
|
461
|
+
});
|
|
462
|
+
return z.NEVER;
|
|
463
|
+
}
|
|
464
|
+
const key = val.slice(0, eq);
|
|
465
|
+
if (key.length === 0) {
|
|
466
|
+
ctx.addIssue({
|
|
467
|
+
code: z.ZodIssueCode.custom,
|
|
468
|
+
message: "Field key cannot be empty"
|
|
469
|
+
});
|
|
470
|
+
return z.NEVER;
|
|
471
|
+
}
|
|
472
|
+
const segments = key.split(".");
|
|
473
|
+
if (segments.some((seg) => seg.length === 0)) {
|
|
474
|
+
ctx.addIssue({
|
|
475
|
+
code: z.ZodIssueCode.custom,
|
|
476
|
+
message: `Invalid field key: '${key}'. Dotted segments cannot be empty`
|
|
477
|
+
});
|
|
478
|
+
return z.NEVER;
|
|
479
|
+
}
|
|
480
|
+
const forbidden = segments.find((seg) => FORBIDDEN_SEGMENTS.has(seg));
|
|
481
|
+
if (forbidden) {
|
|
482
|
+
ctx.addIssue({
|
|
483
|
+
code: z.ZodIssueCode.custom,
|
|
484
|
+
message: `Invalid field key: '${key}'. Segment '${forbidden}' is not allowed.`
|
|
485
|
+
});
|
|
486
|
+
return z.NEVER;
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
path: segments,
|
|
490
|
+
value: val.slice(eq + 1)
|
|
491
|
+
};
|
|
492
|
+
});
|
|
493
|
+
const apiCommand = defineAppCommand({
|
|
494
|
+
name: "api",
|
|
495
|
+
description: "Call Tailor Platform API endpoints directly.",
|
|
496
|
+
notes: `Use \`tailor api list\` to enumerate invocable methods and \`tailor api inspect <endpoint>\` to print an endpoint's input message tree (combine with \`--json\` for machine-readable output).
|
|
497
|
+
|
|
498
|
+
The request body is inferred from the target endpoint's request schema, and commonly required fields are auto-injected so they can be omitted from \`--body\`:
|
|
499
|
+
|
|
500
|
+
- \`workspaceId\` — resolved from \`-w\` / \`TAILOR_PLATFORM_WORKSPACE_ID\` / the selected profile.
|
|
501
|
+
- \`namespaceName\` — resolved from \`tailor.config.ts\` based on the endpoint's service:
|
|
502
|
+
- Auth / Tenant / UserProfile endpoints use \`auth.name\`.
|
|
503
|
+
- IdP / TailorDB / Pipeline endpoints use the sole configured namespace when exactly one is defined.
|
|
504
|
+
|
|
505
|
+
Values already present in \`--body\` are never overridden. If a value cannot be resolved (e.g. no config found), injection is silently skipped and the server-side validation error takes precedence.
|
|
506
|
+
|
|
507
|
+
Use \`--field key=value\` (repeatable) to set request body fields without writing JSON. Dotted keys (e.g. \`application.name=foo\`) build nested objects. \`--field\` overrides matching fields in \`--body\` and tab-completes from the endpoint's request schema.`,
|
|
508
|
+
examples: [
|
|
509
|
+
{
|
|
510
|
+
cmd: "GetApplication -b '{\"applicationName\":\"app-1\"}'",
|
|
511
|
+
desc: "Call an endpoint; workspaceId is auto-injected."
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
cmd: "GetApplication -f applicationName=app-1",
|
|
515
|
+
desc: "Same as above, using --field instead of --body."
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
cmd: "list",
|
|
519
|
+
desc: "List all invocable OperatorService methods."
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
cmd: "inspect GetApplication",
|
|
523
|
+
desc: "Show the input message tree for an endpoint."
|
|
524
|
+
}
|
|
525
|
+
],
|
|
526
|
+
subCommands: {
|
|
527
|
+
list: listCommand$7,
|
|
528
|
+
inspect: inspectCommand
|
|
529
|
+
},
|
|
530
|
+
args: z.strictObject({
|
|
531
|
+
...workspaceArgs,
|
|
532
|
+
...configArg,
|
|
533
|
+
body: arg(z.string().default("{}"), {
|
|
534
|
+
alias: "b",
|
|
535
|
+
description: "Request body as JSON."
|
|
536
|
+
}),
|
|
537
|
+
field: arg(fieldArg.array().optional(), {
|
|
538
|
+
alias: "f",
|
|
539
|
+
description: "Set a body field as `key=value` (repeatable; dotted keys nest). Overrides --body.",
|
|
540
|
+
completion: { custom: { expand: {
|
|
541
|
+
dependsOn: ["endpoint"],
|
|
542
|
+
enumerate: ({ endpoint }) => enumerateAllFieldCompletions(extractMethodName(endpoint ?? ""))
|
|
543
|
+
} } }
|
|
544
|
+
}),
|
|
545
|
+
endpoint: arg(z.string(), {
|
|
546
|
+
positional: true,
|
|
547
|
+
description: "API endpoint to call (e.g., 'GetApplication' or 'tailor.v1.OperatorService/GetApplication').",
|
|
548
|
+
completion: { custom: { choices: listMethodChoices() } }
|
|
549
|
+
})
|
|
550
|
+
}),
|
|
551
|
+
run: async (args) => {
|
|
552
|
+
await assertWritable({ profile: args.profile });
|
|
553
|
+
const methodName = extractMethodName(args.endpoint);
|
|
554
|
+
const method = getMethodDescriptor(methodName);
|
|
555
|
+
const parsedBody = parseBodyAsObject(args.body);
|
|
556
|
+
let mutated = false;
|
|
557
|
+
if (args.field && args.field.length > 0) {
|
|
558
|
+
if (!parsedBody) throw new Error("--field requires --body to be a JSON object (or omitted).");
|
|
559
|
+
for (const f of args.field) {
|
|
560
|
+
const leaf = method ? resolveLeafField(method.input, f.path) : void 0;
|
|
561
|
+
setNestedPath(parsedBody, f.path, coerceFieldValue(leaf, f.value));
|
|
562
|
+
}
|
|
563
|
+
mutated = true;
|
|
564
|
+
}
|
|
565
|
+
if (parsedBody && method) {
|
|
566
|
+
if (normalizeBodyFieldKeys(parsedBody, method.input.fields)) mutated = true;
|
|
567
|
+
const fieldNames = method.input.fields.map((f) => f.localName);
|
|
568
|
+
if (fieldNames.includes("workspaceId") && !Object.hasOwn(parsedBody, "workspaceId")) try {
|
|
569
|
+
parsedBody.workspaceId = await loadWorkspaceId({
|
|
570
|
+
workspaceId: args["workspace-id"],
|
|
571
|
+
profile: args.profile
|
|
572
|
+
});
|
|
573
|
+
mutated = true;
|
|
574
|
+
} catch {}
|
|
575
|
+
if (fieldNames.includes("namespaceName") && !Object.hasOwn(parsedBody, "namespaceName")) try {
|
|
576
|
+
const { config } = await loadConfig(args.config);
|
|
577
|
+
const ns = resolveNamespaceName(methodName, config);
|
|
578
|
+
if (ns) {
|
|
579
|
+
parsedBody.namespaceName = ns;
|
|
580
|
+
mutated = true;
|
|
581
|
+
}
|
|
582
|
+
} catch {}
|
|
583
|
+
}
|
|
584
|
+
const result = await apiCall({
|
|
585
|
+
profile: args.profile,
|
|
586
|
+
endpoint: args.endpoint,
|
|
587
|
+
body: mutated && parsedBody ? JSON.stringify(parsedBody) : args.body
|
|
588
|
+
});
|
|
589
|
+
logger.out(JSON.stringify(result.data, null, 2));
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
//#endregion
|
|
44
594
|
//#region src/cli/commands/auth/token.ts
|
|
45
595
|
const tokenCommand = defineAppCommand({
|
|
46
596
|
name: "token",
|
|
@@ -396,7 +946,7 @@ const listCommand$6 = defineAppCommand({
|
|
|
396
946
|
}
|
|
397
947
|
let entries;
|
|
398
948
|
try {
|
|
399
|
-
entries = fs
|
|
949
|
+
entries = fs.readdirSync(config.localDir);
|
|
400
950
|
} catch {
|
|
401
951
|
logger.info("No crash reports found.");
|
|
402
952
|
if (jsonOutput) logger.out([]);
|
|
@@ -436,7 +986,7 @@ const sendCommand = defineAppCommand({
|
|
|
436
986
|
run: async (args) => {
|
|
437
987
|
let content;
|
|
438
988
|
try {
|
|
439
|
-
content = fs
|
|
989
|
+
content = fs.readFileSync(args.file, "utf-8");
|
|
440
990
|
} catch {
|
|
441
991
|
logger.error(`Crash report file not found: ${args.file}`);
|
|
442
992
|
process.exit(1);
|
|
@@ -489,44 +1039,26 @@ const crashReportCommand = defineCommand({
|
|
|
489
1039
|
}
|
|
490
1040
|
});
|
|
491
1041
|
|
|
492
|
-
//#endregion
|
|
493
|
-
//#region src/parser/service/resolver/local-input-parser.ts
|
|
494
|
-
/**
|
|
495
|
-
* Build a local, in-process validator for a resolver's declared input fields.
|
|
496
|
-
* Each field already carries its own bound `_parseInternal` (attached when the
|
|
497
|
-
* user's resolver module was loaded via dynamic import), so this only needs to
|
|
498
|
-
* orchestrate calling every field and collecting issues.
|
|
499
|
-
* @param fields - Resolver's raw input field definitions (already-built TailorField instances)
|
|
500
|
-
* @returns An object exposing a `.parse()` method for local format detection
|
|
501
|
-
*/
|
|
502
|
-
function buildLocalInputParser(fields) {
|
|
503
|
-
return { parse(args) {
|
|
504
|
-
const { value, data, invoker } = args;
|
|
505
|
-
if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date) return { issues: [{ message: `Expected an object: received ${String(value)}` }] };
|
|
506
|
-
const record = value;
|
|
507
|
-
const issues = [];
|
|
508
|
-
for (const [name, field] of Object.entries(fields)) {
|
|
509
|
-
const result = field._parseInternal({
|
|
510
|
-
value: record[name],
|
|
511
|
-
data,
|
|
512
|
-
invoker,
|
|
513
|
-
pathArray: [name]
|
|
514
|
-
});
|
|
515
|
-
if (result.issues) issues.push(...result.issues);
|
|
516
|
-
}
|
|
517
|
-
return issues.length > 0 ? { issues } : {};
|
|
518
|
-
} };
|
|
519
|
-
}
|
|
520
|
-
|
|
521
1042
|
//#endregion
|
|
522
1043
|
//#region src/cli/commands/deploy/index.ts
|
|
523
|
-
const deployCommand
|
|
1044
|
+
const deployCommand = defineAppCommand({
|
|
524
1045
|
name: "deploy",
|
|
525
1046
|
description: "Deploy your application by applying the Tailor configuration.",
|
|
526
1047
|
args: z.strictObject({
|
|
527
1048
|
...workspaceArgs,
|
|
528
1049
|
...multiConfigArg,
|
|
529
1050
|
...confirmationArgs,
|
|
1051
|
+
"create-workspace": arg(z.boolean().optional(), { description: "Create a workspace when the account has none" }),
|
|
1052
|
+
"workspace-name": arg(z.string().optional(), { description: "Name for a workspace created during deploy" }),
|
|
1053
|
+
"workspace-region": arg(z.string().optional(), { description: "Region for a workspace created during deploy" }),
|
|
1054
|
+
"organization-id": arg(z.string().optional(), {
|
|
1055
|
+
description: "Organization ID for a workspace created during deploy",
|
|
1056
|
+
env: "TAILOR_PLATFORM_ORGANIZATION_ID"
|
|
1057
|
+
}),
|
|
1058
|
+
"folder-id": arg(z.string().optional(), {
|
|
1059
|
+
description: "Folder ID for a workspace created during deploy",
|
|
1060
|
+
env: "TAILOR_PLATFORM_FOLDER_ID"
|
|
1061
|
+
}),
|
|
530
1062
|
"dry-run": arg(z.boolean().optional(), {
|
|
531
1063
|
alias: "d",
|
|
532
1064
|
description: "Run the command without making any changes"
|
|
@@ -540,16 +1072,26 @@ const deployCommand$1 = defineAppCommand({
|
|
|
540
1072
|
await assertWritable({ profile: args.profile });
|
|
541
1073
|
const { initTelemetry } = await import("../telemetry-CkbkeJxl.mjs");
|
|
542
1074
|
await initTelemetry();
|
|
543
|
-
await
|
|
1075
|
+
await deployFromCLI({
|
|
544
1076
|
workspaceId: args["workspace-id"],
|
|
545
1077
|
profile: args.profile,
|
|
546
1078
|
configPath: args.config,
|
|
547
1079
|
dryRun: args["dry-run"],
|
|
548
1080
|
yes: args.yes,
|
|
1081
|
+
createWorkspace: args["create-workspace"],
|
|
1082
|
+
workspaceName: args["workspace-name"],
|
|
1083
|
+
workspaceRegion: args["workspace-region"],
|
|
1084
|
+
organizationId: args["organization-id"],
|
|
1085
|
+
folderId: args["folder-id"],
|
|
549
1086
|
noSchemaCheck: args["no-schema-check"],
|
|
550
1087
|
noValidate: args["no-validate"],
|
|
551
1088
|
noCache: args["no-cache"],
|
|
552
1089
|
cleanCache: args["clean-cache"]
|
|
1090
|
+
}, {
|
|
1091
|
+
envFile: args["env-file"],
|
|
1092
|
+
envFileIfExists: args["env-file-if-exists"],
|
|
1093
|
+
verbose: args.verbose,
|
|
1094
|
+
json: args.json
|
|
553
1095
|
});
|
|
554
1096
|
}
|
|
555
1097
|
});
|
|
@@ -562,12 +1104,12 @@ const executorCommand = defineCommand({
|
|
|
562
1104
|
subCommands: {
|
|
563
1105
|
trigger: triggerCommand,
|
|
564
1106
|
jobs: jobsCommand,
|
|
565
|
-
list: listCommand$
|
|
1107
|
+
list: listCommand$8,
|
|
566
1108
|
get: getCommand$1,
|
|
567
1109
|
webhook: webhookCommand
|
|
568
1110
|
},
|
|
569
1111
|
async run() {
|
|
570
|
-
await runCommand(listCommand$
|
|
1112
|
+
await runCommand(listCommand$8, []);
|
|
571
1113
|
}
|
|
572
1114
|
});
|
|
573
1115
|
|
|
@@ -1148,12 +1690,12 @@ async function bundleForTestRun(options) {
|
|
|
1148
1690
|
const inlineSourcemap = resolveInlineSourcemap(options.inlineSourcemap);
|
|
1149
1691
|
const bundleLogLevel = resolveBundleLogLevel(options.logLevel);
|
|
1150
1692
|
const outputDir = path.resolve(getDistDir(), "test-run");
|
|
1151
|
-
fs
|
|
1693
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1152
1694
|
const baseName = `test-run--${detected.name}`;
|
|
1153
1695
|
const scriptName = `${baseName}.js`;
|
|
1154
1696
|
const entryPath = path.join(outputDir, `${baseName}.entry.js`);
|
|
1155
1697
|
const entryContent = generateEntry(detected, sourceFile, env, machineUser, workspaceId);
|
|
1156
|
-
fs
|
|
1698
|
+
fs.writeFileSync(entryPath, entryContent);
|
|
1157
1699
|
let tsconfig;
|
|
1158
1700
|
try {
|
|
1159
1701
|
tsconfig = await resolveTSConfig();
|
|
@@ -1301,7 +1843,13 @@ async function detectFunctionType(options) {
|
|
|
1301
1843
|
if (resolverResult.success) {
|
|
1302
1844
|
const rawInput = module.default.input;
|
|
1303
1845
|
let inputSchema;
|
|
1304
|
-
if (rawInput)
|
|
1846
|
+
if (rawInput) {
|
|
1847
|
+
const fields = rawInput;
|
|
1848
|
+
inputSchema = { parse: (args) => parseInputFields({
|
|
1849
|
+
...args,
|
|
1850
|
+
fields
|
|
1851
|
+
}) };
|
|
1852
|
+
}
|
|
1305
1853
|
return {
|
|
1306
1854
|
type: "resolver",
|
|
1307
1855
|
name: resolverResult.data.name,
|
|
@@ -1446,7 +1994,7 @@ When a \`.js\` file is provided, detection and bundling are skipped and the file
|
|
|
1446
1994
|
run: async (args) => {
|
|
1447
1995
|
const jsonOutput = logger.jsonMode;
|
|
1448
1996
|
const filePath = path.resolve(args.file);
|
|
1449
|
-
if (!fs
|
|
1997
|
+
if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
1450
1998
|
const { config } = await loadConfig(args.config);
|
|
1451
1999
|
const authNamespace = resolveAuthNamespace(config.auth);
|
|
1452
2000
|
const machineUserName = await resolveMachineUserName({
|
|
@@ -1475,7 +2023,7 @@ When a \`.js\` file is provided, detection and bundling are skipped and the file
|
|
|
1475
2023
|
let functionName;
|
|
1476
2024
|
if (isPreBundled) {
|
|
1477
2025
|
scriptName = path.basename(filePath);
|
|
1478
|
-
bundledCode = fs
|
|
2026
|
+
bundledCode = fs.readFileSync(filePath, "utf-8");
|
|
1479
2027
|
logger.info(`Using pre-bundled script ${styles.bold(scriptName)}`);
|
|
1480
2028
|
} else {
|
|
1481
2029
|
logger.info(`Detecting function type from ${styles.path(relativePath)}`);
|
|
@@ -1629,12 +2177,12 @@ const functionCommand = defineCommand({
|
|
|
1629
2177
|
description: "Manage functions",
|
|
1630
2178
|
subCommands: {
|
|
1631
2179
|
get: getCommand$2,
|
|
1632
|
-
list: listCommand$
|
|
2180
|
+
list: listCommand$9,
|
|
1633
2181
|
logs: logsCommand,
|
|
1634
2182
|
"test-run": testRunCommand
|
|
1635
2183
|
},
|
|
1636
2184
|
async run() {
|
|
1637
|
-
await runCommand(listCommand$
|
|
2185
|
+
await runCommand(listCommand$9, []);
|
|
1638
2186
|
}
|
|
1639
2187
|
});
|
|
1640
2188
|
|
|
@@ -1643,23 +2191,14 @@ const functionCommand = defineCommand({
|
|
|
1643
2191
|
const generateCommand = defineAppCommand({
|
|
1644
2192
|
name: "generate",
|
|
1645
2193
|
description: "Generate files using Tailor configuration.",
|
|
1646
|
-
args: z.strictObject({
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
}),
|
|
1651
|
-
watch: arg(z.boolean().default(false), {
|
|
1652
|
-
alias: "W",
|
|
1653
|
-
description: "Watch for type/resolver changes and regenerate"
|
|
1654
|
-
})
|
|
1655
|
-
}),
|
|
2194
|
+
args: z.strictObject({ config: arg(z.string().default("tailor.config.ts"), {
|
|
2195
|
+
alias: "c",
|
|
2196
|
+
description: "Path to SDK config file"
|
|
2197
|
+
}) }),
|
|
1656
2198
|
run: async (args) => {
|
|
1657
2199
|
const { initTelemetry } = await import("../telemetry-CkbkeJxl.mjs");
|
|
1658
2200
|
await initTelemetry();
|
|
1659
|
-
await generate({
|
|
1660
|
-
configPath: args.config,
|
|
1661
|
-
watch: args.watch
|
|
1662
|
-
});
|
|
2201
|
+
await generate({ configPath: args.config });
|
|
1663
2202
|
}
|
|
1664
2203
|
});
|
|
1665
2204
|
|
|
@@ -1970,11 +2509,11 @@ const machineuserCommand = defineCommand({
|
|
|
1970
2509
|
name: "machineuser",
|
|
1971
2510
|
description: "Manage machine users in your Tailor Platform application.",
|
|
1972
2511
|
subCommands: {
|
|
1973
|
-
list: listCommand$
|
|
2512
|
+
list: listCommand$10,
|
|
1974
2513
|
token: tokenCommand$1
|
|
1975
2514
|
},
|
|
1976
2515
|
async run() {
|
|
1977
|
-
await runCommand(listCommand$
|
|
2516
|
+
await runCommand(listCommand$10, []);
|
|
1978
2517
|
}
|
|
1979
2518
|
});
|
|
1980
2519
|
|
|
@@ -1984,11 +2523,11 @@ const oauth2clientCommand = defineCommand({
|
|
|
1984
2523
|
name: "oauth2client",
|
|
1985
2524
|
description: "Manage OAuth2 clients in your Tailor Platform application.",
|
|
1986
2525
|
subCommands: {
|
|
1987
|
-
list: listCommand$
|
|
2526
|
+
list: listCommand$11,
|
|
1988
2527
|
get: getCommand$3
|
|
1989
2528
|
},
|
|
1990
2529
|
async run() {
|
|
1991
|
-
await runCommand(listCommand$
|
|
2530
|
+
await runCommand(listCommand$11, []);
|
|
1992
2531
|
}
|
|
1993
2532
|
});
|
|
1994
2533
|
|
|
@@ -2045,7 +2584,7 @@ const folderCommand = defineCommand({
|
|
|
2045
2584
|
create: createCommand$3,
|
|
2046
2585
|
delete: deleteCommand$4,
|
|
2047
2586
|
get: getCommand$4,
|
|
2048
|
-
list: listCommand$
|
|
2587
|
+
list: listCommand$12,
|
|
2049
2588
|
update: updateCommand$2
|
|
2050
2589
|
}
|
|
2051
2590
|
});
|
|
@@ -2058,12 +2597,12 @@ const organizationCommand = defineCommand({
|
|
|
2058
2597
|
subCommands: {
|
|
2059
2598
|
folder: folderCommand,
|
|
2060
2599
|
get: getCommand$5,
|
|
2061
|
-
list: listCommand$
|
|
2600
|
+
list: listCommand$13,
|
|
2062
2601
|
tree: treeCommand,
|
|
2063
2602
|
update: updateCommand$3
|
|
2064
2603
|
},
|
|
2065
2604
|
async run() {
|
|
2066
|
-
await runCommand(listCommand$
|
|
2605
|
+
await runCommand(listCommand$13, []);
|
|
2067
2606
|
}
|
|
2068
2607
|
});
|
|
2069
2608
|
|
|
@@ -2111,6 +2650,12 @@ const BUILTIN_COMMAND_NAMES = [
|
|
|
2111
2650
|
|
|
2112
2651
|
//#endregion
|
|
2113
2652
|
//#region src/cli/shared/plugin.ts
|
|
2653
|
+
/**
|
|
2654
|
+
* npm packages known to provide a plugin slug, keyed by the slug the
|
|
2655
|
+
* dispatcher resolves (command path joined with `-`). Used to suggest an
|
|
2656
|
+
* install command when the plugin executable is not found.
|
|
2657
|
+
*/
|
|
2658
|
+
const KNOWN_PLUGIN_PACKAGES = { "tailordb-erd": "@tailor-platform/sdk-tailordb-erd-plugin" };
|
|
2114
2659
|
/** PATH entry separator (`;` on Windows, `:` elsewhere). */
|
|
2115
2660
|
const pathDelimiter = process.platform === "win32" ? ";" : ":";
|
|
2116
2661
|
/**
|
|
@@ -2288,17 +2833,7 @@ async function resolveActiveUser(profile) {
|
|
|
2288
2833
|
*/
|
|
2289
2834
|
async function buildPluginEnv(options = {}) {
|
|
2290
2835
|
const { profile } = options;
|
|
2291
|
-
|
|
2292
|
-
try {
|
|
2293
|
-
platformConfig = await loadPlatformClientConfig({
|
|
2294
|
-
profile,
|
|
2295
|
-
allowMissingProfile: true
|
|
2296
|
-
});
|
|
2297
|
-
} catch {}
|
|
2298
|
-
const env = {
|
|
2299
|
-
TAILOR_PLATFORM_URL: getPlatformBaseUrl(platformConfig),
|
|
2300
|
-
TAILOR_PLATFORM_OAUTH2_CLIENT_ID: getOAuth2ClientId(platformConfig)
|
|
2301
|
-
};
|
|
2836
|
+
const env = {};
|
|
2302
2837
|
const binPath = process.argv[1];
|
|
2303
2838
|
if (binPath) env.TAILOR_BIN = binPath;
|
|
2304
2839
|
try {
|
|
@@ -2307,6 +2842,16 @@ async function buildPluginEnv(options = {}) {
|
|
|
2307
2842
|
} catch {}
|
|
2308
2843
|
const configPath = loadConfigPath();
|
|
2309
2844
|
if (configPath) env.TAILOR_CONFIG_PATH = configPath;
|
|
2845
|
+
if (options.skipPlatformContext) return env;
|
|
2846
|
+
let platformConfig;
|
|
2847
|
+
try {
|
|
2848
|
+
platformConfig = await loadPlatformClientConfig({
|
|
2849
|
+
profile,
|
|
2850
|
+
allowMissingProfile: true
|
|
2851
|
+
});
|
|
2852
|
+
} catch {}
|
|
2853
|
+
env.TAILOR_PLATFORM_URL = getPlatformBaseUrl(platformConfig);
|
|
2854
|
+
env.TAILOR_PLATFORM_OAUTH2_CLIENT_ID = getOAuth2ClientId(platformConfig);
|
|
2310
2855
|
try {
|
|
2311
2856
|
env.TAILOR_PLATFORM_WORKSPACE_ID = await loadWorkspaceId({ profile });
|
|
2312
2857
|
} catch {}
|
|
@@ -2378,21 +2923,72 @@ function buildSpawnTarget(pluginPath, args) {
|
|
|
2378
2923
|
};
|
|
2379
2924
|
}
|
|
2380
2925
|
/**
|
|
2926
|
+
* Check whether args forwarded to a plugin carry an explicit env-file flag.
|
|
2927
|
+
* Scanning stops at a `--` terminator. The check is presence-only:
|
|
2928
|
+
* `--env-file-if-exists` counts even when the named file does not exist, so
|
|
2929
|
+
* no platform context is injected in that case either.
|
|
2930
|
+
* @param args - Args forwarded to the plugin
|
|
2931
|
+
* @returns Whether an `--env-file`/`-e`/`--env-file-if-exists` flag is present
|
|
2932
|
+
*/
|
|
2933
|
+
function hasEnvFileFlag(args) {
|
|
2934
|
+
for (const token of args) {
|
|
2935
|
+
if (token === "--") return false;
|
|
2936
|
+
if (token === "--env-file" || token === "-e" || token === "--env-file-if-exists") return true;
|
|
2937
|
+
if (token.startsWith("--env-file=") || token.startsWith("-e=") || token.startsWith("--env-file-if-exists=")) return true;
|
|
2938
|
+
}
|
|
2939
|
+
return false;
|
|
2940
|
+
}
|
|
2941
|
+
/**
|
|
2942
|
+
* Extract an explicit `--profile`/`-p` value from args forwarded to a plugin,
|
|
2943
|
+
* so the injected platform context matches the profile the plugin will use.
|
|
2944
|
+
* Scanning stops at a `--` terminator; the last occurrence wins.
|
|
2945
|
+
* @param args - Args forwarded to the plugin
|
|
2946
|
+
* @returns The explicit profile value, or undefined when not present
|
|
2947
|
+
*/
|
|
2948
|
+
function explicitProfileFromArgs(args) {
|
|
2949
|
+
let profile;
|
|
2950
|
+
for (let i = 0; i < args.length; i++) {
|
|
2951
|
+
const token = args[i];
|
|
2952
|
+
if (token === void 0 || token === "--") break;
|
|
2953
|
+
if (token === "--profile" || token === "-p") {
|
|
2954
|
+
const next = args[i + 1];
|
|
2955
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
2956
|
+
profile = next;
|
|
2957
|
+
i++;
|
|
2958
|
+
}
|
|
2959
|
+
continue;
|
|
2960
|
+
}
|
|
2961
|
+
if (token.startsWith("--profile=")) profile = token.slice(10);
|
|
2962
|
+
else if (token.startsWith("-p=")) profile = token.slice(3);
|
|
2963
|
+
}
|
|
2964
|
+
return profile || void 0;
|
|
2965
|
+
}
|
|
2966
|
+
/**
|
|
2967
|
+
* Build the plugin slug from the known command path plus the unknown name,
|
|
2968
|
+
* so `tailor tailordb erd` resolves `tailor-tailordb-erd`.
|
|
2969
|
+
* @param commandPath - Parent command path preceding the unknown subcommand
|
|
2970
|
+
* @param name - Unknown subcommand name
|
|
2971
|
+
* @returns The plugin slug
|
|
2972
|
+
*/
|
|
2973
|
+
function pluginSlug(commandPath, name) {
|
|
2974
|
+
return [...commandPath ?? [], name].join("-");
|
|
2975
|
+
}
|
|
2976
|
+
/**
|
|
2381
2977
|
* Resolve and execute a plugin, forwarding stdio and propagating its exit code.
|
|
2382
2978
|
* @param params - Dispatch parameters
|
|
2383
|
-
* @param params.name - Plugin name (without the `<cli>-` prefix)
|
|
2384
|
-
* @param params.args - Args to forward to the plugin
|
|
2385
|
-
* @param params.cliName - The host CLI name
|
|
2386
|
-
* @param params.profile - Active profile name, if any
|
|
2387
2979
|
* @returns The plugin's exit code, or undefined when no matching plugin exists
|
|
2388
2980
|
*/
|
|
2389
2981
|
async function dispatchPlugin(params) {
|
|
2390
|
-
const slug =
|
|
2982
|
+
const slug = pluginSlug(params.commandPath, params.name);
|
|
2391
2983
|
const plugin = resolvePlugin(slug, params.cliName);
|
|
2392
2984
|
if (!plugin) return;
|
|
2985
|
+
const profile = explicitProfileFromArgs(params.args) ?? params.profile;
|
|
2393
2986
|
const env = {
|
|
2394
2987
|
...process.env,
|
|
2395
|
-
...await buildPluginEnv({
|
|
2988
|
+
...await buildPluginEnv({
|
|
2989
|
+
profile,
|
|
2990
|
+
skipPlatformContext: hasEnvFileFlag(params.args)
|
|
2991
|
+
})
|
|
2396
2992
|
};
|
|
2397
2993
|
const { command, args, shell } = buildSpawnTarget(plugin.path, params.args);
|
|
2398
2994
|
return await new Promise((resolve) => {
|
|
@@ -2411,6 +3007,27 @@ async function dispatchPlugin(params) {
|
|
|
2411
3007
|
});
|
|
2412
3008
|
});
|
|
2413
3009
|
}
|
|
3010
|
+
/**
|
|
3011
|
+
* Dispatch a plugin, printing an install hint when the subcommand maps to a
|
|
3012
|
+
* known plugin package that is not installed.
|
|
3013
|
+
* @param params - Dispatch parameters
|
|
3014
|
+
* @returns The plugin's exit code (1 after an install hint), or undefined when
|
|
3015
|
+
* the subcommand matches no plugin and no known package
|
|
3016
|
+
*/
|
|
3017
|
+
async function dispatchPluginWithInstallHint(params) {
|
|
3018
|
+
const exitCode = await dispatchPlugin(params);
|
|
3019
|
+
if (exitCode !== void 0) return exitCode;
|
|
3020
|
+
const knownPackage = KNOWN_PLUGIN_PACKAGES[pluginSlug(params.commandPath, params.name)];
|
|
3021
|
+
if (!knownPackage) return;
|
|
3022
|
+
const fullCommand = [
|
|
3023
|
+
params.cliName,
|
|
3024
|
+
...params.commandPath ?? [],
|
|
3025
|
+
params.name
|
|
3026
|
+
].join(" ");
|
|
3027
|
+
logger.error(`"${fullCommand}" is provided by the ${knownPackage} CLI plugin, which is not installed.`);
|
|
3028
|
+
logger.info(`Install it with: npm install -D ${knownPackage}`);
|
|
3029
|
+
return 1;
|
|
3030
|
+
}
|
|
2414
3031
|
|
|
2415
3032
|
//#endregion
|
|
2416
3033
|
//#region src/cli/commands/plugin/list.ts
|
|
@@ -3213,10 +3830,10 @@ function lockPath(outputDir) {
|
|
|
3213
3830
|
*/
|
|
3214
3831
|
function readLock(outputDir) {
|
|
3215
3832
|
const file = lockPath(outputDir);
|
|
3216
|
-
if (!fs
|
|
3833
|
+
if (!fs.existsSync(file)) return null;
|
|
3217
3834
|
let parsed;
|
|
3218
3835
|
try {
|
|
3219
|
-
parsed = JSON.parse(fs
|
|
3836
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
3220
3837
|
} catch (cause) {
|
|
3221
3838
|
throw new Error(`${LOCK_FILENAME} is not valid JSON. The lock file is machine-owned; restore it from git (git checkout -- .github/tailor.lock) and re-run setup.`, { cause });
|
|
3222
3839
|
}
|
|
@@ -3232,8 +3849,8 @@ function readLock(outputDir) {
|
|
|
3232
3849
|
*/
|
|
3233
3850
|
function writeLock(outputDir, lock) {
|
|
3234
3851
|
const file = lockPath(outputDir);
|
|
3235
|
-
fs
|
|
3236
|
-
fs
|
|
3852
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
3853
|
+
fs.writeFileSync(file, `${JSON.stringify(lock, null, 2)}\n`, "utf-8");
|
|
3237
3854
|
}
|
|
3238
3855
|
/**
|
|
3239
3856
|
* Find a lock target by identity. Targets are identified by (kind,
|
|
@@ -3284,10 +3901,10 @@ const HEADER = `# Generated by \`tailor setup\` — managed by the Tailor SDK.
|
|
|
3284
3901
|
* @returns Detected package manager, defaults to npm
|
|
3285
3902
|
*/
|
|
3286
3903
|
function detectPackageManager(dir) {
|
|
3287
|
-
if (fs
|
|
3288
|
-
if (fs
|
|
3289
|
-
if (fs
|
|
3290
|
-
if (fs
|
|
3904
|
+
if (fs.existsSync(path.join(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
3905
|
+
if (fs.existsSync(path.join(dir, "yarn.lock"))) return "yarn";
|
|
3906
|
+
if (fs.existsSync(path.join(dir, "bun.lockb")) || fs.existsSync(path.join(dir, "bun.lock"))) return "bun";
|
|
3907
|
+
if (fs.existsSync(path.join(dir, "package-lock.json"))) return "npm";
|
|
3291
3908
|
return "npm";
|
|
3292
3909
|
}
|
|
3293
3910
|
function block(content, name, keep) {
|
|
@@ -3673,9 +4290,8 @@ async function defaultLoadHasStaticWebsites$1(configPath) {
|
|
|
3673
4290
|
const { config } = await loadConfig(configPath);
|
|
3674
4291
|
return (config.staticWebsites?.length ?? 0) > 0;
|
|
3675
4292
|
}
|
|
3676
|
-
const WORKSPACE_NAME_RE = /^[a-z0-9-]+$/;
|
|
3677
4293
|
function validateWorkspaceName(name) {
|
|
3678
|
-
if (
|
|
4294
|
+
if (!workspaceNameSchema.safeParse(name).success) throw new Error(`Invalid workspace name "${name}". Names must be 3-63 characters of lowercase letters, numbers, and hyphens, and cannot start or end with a hyphen. Pass a valid name with --name.`);
|
|
3679
4295
|
}
|
|
3680
4296
|
const BRANCH_RE = /^[A-Za-z0-9._/-]+$/;
|
|
3681
4297
|
const TAG_PATTERN_RE = /^[A-Za-z0-9._/*?![\]-]+$/;
|
|
@@ -3718,13 +4334,13 @@ function resolveConfigPath(outputDir, dir) {
|
|
|
3718
4334
|
const appDir = path.resolve(outputDir, dir);
|
|
3719
4335
|
const rel = path.relative(outputDir, appDir);
|
|
3720
4336
|
if (path.isAbsolute(dir) || escapesRoot$1(rel)) throw new Error(`--dir must be a relative path inside the repository (got "${dir}").`);
|
|
3721
|
-
if (fs
|
|
3722
|
-
const realAppDir = path.normalize(fs
|
|
3723
|
-
const realOutputDir = path.normalize(fs
|
|
4337
|
+
if (fs.existsSync(appDir)) {
|
|
4338
|
+
const realAppDir = path.normalize(fs.realpathSync(appDir));
|
|
4339
|
+
const realOutputDir = path.normalize(fs.realpathSync(outputDir));
|
|
3724
4340
|
if (escapesRoot$1(path.relative(realOutputDir, realAppDir))) throw new Error(`--dir must resolve to a directory inside the repository (got "${dir}", which links outside it).`);
|
|
3725
4341
|
}
|
|
3726
4342
|
const configPath = path.join(appDir, "tailor.config.ts");
|
|
3727
|
-
if (!fs
|
|
4343
|
+
if (!fs.existsSync(configPath)) throw new Error(`tailor.config.ts not found at ${configPath}. Run this from your SDK project root, or pass the app directory with --dir.`);
|
|
3728
4344
|
return configPath;
|
|
3729
4345
|
}
|
|
3730
4346
|
/**
|
|
@@ -3960,8 +4576,8 @@ async function setupTarget(options) {
|
|
|
3960
4576
|
file: resolved.file
|
|
3961
4577
|
});
|
|
3962
4578
|
const existing = findTarget(lock, resolved.kind, resolved.workspaceName);
|
|
3963
|
-
const fileExists = fs
|
|
3964
|
-
const currentContent = fileExists ? fs
|
|
4579
|
+
const fileExists = fs.existsSync(absFile);
|
|
4580
|
+
const currentContent = fileExists ? fs.readFileSync(absFile, "utf-8") : null;
|
|
3965
4581
|
const normalize = resolved.kind === "action" ? normalizeActionContent : void 0;
|
|
3966
4582
|
const decision = decideAction({
|
|
3967
4583
|
existing,
|
|
@@ -3979,8 +4595,8 @@ async function setupTarget(options) {
|
|
|
3979
4595
|
const existingBody = extractBuildSiteRunBody(currentContent);
|
|
3980
4596
|
if (existingBody !== null) contentToWrite = injectBuildSiteRunBody(contentToWrite, existingBody);
|
|
3981
4597
|
}
|
|
3982
|
-
fs
|
|
3983
|
-
fs
|
|
4598
|
+
fs.mkdirSync(path.dirname(absFile), { recursive: true });
|
|
4599
|
+
fs.writeFileSync(absFile, contentToWrite, "utf-8");
|
|
3984
4600
|
const newTarget = {
|
|
3985
4601
|
kind: resolved.kind,
|
|
3986
4602
|
workspaceName: resolved.workspaceName,
|
|
@@ -4084,21 +4700,21 @@ async function setupCoordinate(options) {
|
|
|
4084
4700
|
file
|
|
4085
4701
|
});
|
|
4086
4702
|
const existing = findTarget(lock, "coordinate", coordinatorName);
|
|
4087
|
-
const fileExists = fs
|
|
4703
|
+
const fileExists = fs.existsSync(absFile);
|
|
4088
4704
|
const decision = decideAction({
|
|
4089
4705
|
existing,
|
|
4090
4706
|
fileExists,
|
|
4091
|
-
currentContent: fileExists ? fs
|
|
4707
|
+
currentContent: fileExists ? fs.readFileSync(absFile, "utf-8") : null,
|
|
4092
4708
|
force
|
|
4093
4709
|
});
|
|
4094
4710
|
if (decision.action === "conflict") throw new Error(`${file}: ${decision.reason}`);
|
|
4095
|
-
fs
|
|
4096
|
-
fs
|
|
4711
|
+
fs.mkdirSync(path.dirname(absFile), { recursive: true });
|
|
4712
|
+
fs.writeFileSync(absFile, render.content, "utf-8");
|
|
4097
4713
|
const tailorSetupFile = ".github/actions/tailor-setup/action.yml";
|
|
4098
4714
|
const absTailorSetupFile = path.join(outputDir, tailorSetupFile);
|
|
4099
|
-
if (!fs
|
|
4100
|
-
fs
|
|
4101
|
-
fs
|
|
4715
|
+
if (!fs.existsSync(absTailorSetupFile)) {
|
|
4716
|
+
fs.mkdirSync(path.dirname(absTailorSetupFile), { recursive: true });
|
|
4717
|
+
fs.writeFileSync(absTailorSetupFile, renderTailorSetupAction({ packageManager }), "utf-8");
|
|
4102
4718
|
logger.success(`Generated ${styles.path(tailorSetupFile)}`);
|
|
4103
4719
|
}
|
|
4104
4720
|
const newTarget = {
|
|
@@ -4225,13 +4841,13 @@ function resolveWithinRoot(outputDir, relPath) {
|
|
|
4225
4841
|
const abs = path.join(outputDir, relPath);
|
|
4226
4842
|
if (escapesRoot(path.relative(outputDir, abs))) return null;
|
|
4227
4843
|
try {
|
|
4228
|
-
if (escapesRoot(path.relative(fs
|
|
4844
|
+
if (escapesRoot(path.relative(fs.realpathSync(outputDir), fs.realpathSync(abs)))) return null;
|
|
4229
4845
|
} catch {}
|
|
4230
4846
|
return abs;
|
|
4231
4847
|
}
|
|
4232
4848
|
function readHash(absFile, normalize) {
|
|
4233
4849
|
try {
|
|
4234
|
-
const content = fs
|
|
4850
|
+
const content = fs.readFileSync(absFile, "utf-8");
|
|
4235
4851
|
return hashContent(normalize ? normalize(content) : content);
|
|
4236
4852
|
} catch {
|
|
4237
4853
|
return null;
|
|
@@ -4272,7 +4888,7 @@ async function checkGitHub(options) {
|
|
|
4272
4888
|
}
|
|
4273
4889
|
const slackToken = Boolean(process.env["TAILOR_SLACK_BOT_TOKEN"]);
|
|
4274
4890
|
if (slackToken !== Boolean(process.env["TAILOR_SLACK_CHANNEL_ID"])) throw new Error(`Slack is partially configured: ${slackToken ? "TAILOR_SLACK_BOT_TOKEN is set" : "TAILOR_SLACK_CHANNEL_ID is set"} but not both. TAILOR_SLACK_BOT_TOKEN (secret) and TAILOR_SLACK_CHANNEL_ID (variable) must be set together, or neither should be set.`);
|
|
4275
|
-
const exists = options.configExistsAt ?? ((p) => fs
|
|
4891
|
+
const exists = options.configExistsAt ?? ((p) => fs.existsSync(p));
|
|
4276
4892
|
const defaultBranch = detectDefaultBranchSafe(outputDir, options.gitRunner);
|
|
4277
4893
|
const loadErdNamespaces = options.loadErdNamespaces ?? defaultLoadErdNamespaces;
|
|
4278
4894
|
const loadHasMigrations = options.loadHasMigrations ?? defaultLoadHasMigrations;
|
|
@@ -4361,10 +4977,10 @@ async function setupDelete(options) {
|
|
|
4361
4977
|
const remaining = [...lock.targets];
|
|
4362
4978
|
try {
|
|
4363
4979
|
for (const { relPath, absPath, target } of matches) {
|
|
4364
|
-
fs
|
|
4980
|
+
fs.rmSync(absPath, { force: true });
|
|
4365
4981
|
if (target.kind === "action") {
|
|
4366
4982
|
const dir = path.dirname(absPath);
|
|
4367
|
-
if (fs
|
|
4983
|
+
if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
|
|
4368
4984
|
}
|
|
4369
4985
|
const index = remaining.findIndex((t) => targetKey(t) === targetKey(target));
|
|
4370
4986
|
if (index !== -1) remaining.splice(index, 1);
|
|
@@ -4569,212 +5185,15 @@ const setupCommand = defineCommand({
|
|
|
4569
5185
|
});
|
|
4570
5186
|
|
|
4571
5187
|
//#endregion
|
|
4572
|
-
//#region src/cli/
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
const update = () => {
|
|
4582
|
-
current += 1;
|
|
4583
|
-
const percent = Math.round(current / total * 100);
|
|
4584
|
-
process.stderr.write(`\r${label} ${current}/${total} (${percent}%)`);
|
|
4585
|
-
};
|
|
4586
|
-
const finish = () => {
|
|
4587
|
-
process.stderr.write("\n");
|
|
4588
|
-
};
|
|
4589
|
-
return {
|
|
4590
|
-
update,
|
|
4591
|
-
finish
|
|
4592
|
-
};
|
|
4593
|
-
}
|
|
4594
|
-
/**
|
|
4595
|
-
* Wrap a promise with a timeout, rejecting if the timeout elapses first.
|
|
4596
|
-
* @template T
|
|
4597
|
-
* @param p - Promise to await
|
|
4598
|
-
* @param ms - Timeout in milliseconds
|
|
4599
|
-
* @param message - Error message on timeout
|
|
4600
|
-
* @returns Result of the original promise if it completes in time
|
|
4601
|
-
*/
|
|
4602
|
-
async function withTimeout(p, ms, message) {
|
|
4603
|
-
return await Promise.race([p, setTimeout$1(ms).then(() => {
|
|
4604
|
-
throw new Error(message);
|
|
4605
|
-
})]);
|
|
4606
|
-
}
|
|
4607
|
-
|
|
4608
|
-
//#endregion
|
|
4609
|
-
//#region src/cli/commands/staticwebsite/deploy.ts
|
|
4610
|
-
const CHUNK_SIZE = 64 * 1024;
|
|
4611
|
-
const IGNORED_FILES = /* @__PURE__ */ new Set([
|
|
4612
|
-
".DS_Store",
|
|
4613
|
-
"thumbs.db",
|
|
4614
|
-
"desktop.ini"
|
|
4615
|
-
]);
|
|
4616
|
-
function shouldIgnoreFile(filePath) {
|
|
4617
|
-
const fileName = path.basename(filePath).toLowerCase();
|
|
4618
|
-
return IGNORED_FILES.has(fileName);
|
|
4619
|
-
}
|
|
4620
|
-
/**
|
|
4621
|
-
* Deploy a static website by creating a deployment, uploading files, and publishing it.
|
|
4622
|
-
* @param client - Operator client instance
|
|
4623
|
-
* @param workspaceId - Workspace ID
|
|
4624
|
-
* @param name - Static website name
|
|
4625
|
-
* @param distDir - Directory containing static site files
|
|
4626
|
-
* @param showProgress - Whether to show upload progress
|
|
4627
|
-
* @returns Deployment result with URL and skipped files
|
|
4628
|
-
*/
|
|
4629
|
-
async function deployStaticWebsite(client, workspaceId, name, distDir, showProgress = true) {
|
|
4630
|
-
const { deploymentId } = await client.createDeployment({
|
|
4631
|
-
workspaceId,
|
|
4632
|
-
name
|
|
4633
|
-
});
|
|
4634
|
-
if (!deploymentId) throw new Error("createDeployment returned empty deploymentId");
|
|
4635
|
-
const skippedFiles = await uploadDirectory(client, workspaceId, deploymentId, distDir, showProgress);
|
|
4636
|
-
const { url } = await client.publishDeployment({
|
|
4637
|
-
workspaceId,
|
|
4638
|
-
deploymentId
|
|
4639
|
-
});
|
|
4640
|
-
if (!url) throw new Error("publishDeployment returned empty url");
|
|
4641
|
-
return {
|
|
4642
|
-
url,
|
|
4643
|
-
skippedFiles
|
|
4644
|
-
};
|
|
4645
|
-
}
|
|
4646
|
-
async function uploadDirectory(client, workspaceId, deploymentId, rootDir, showProgress) {
|
|
4647
|
-
const files = await collectFiles(rootDir);
|
|
4648
|
-
if (files.length === 0) {
|
|
4649
|
-
logger.warn(`No files found under ${rootDir}`);
|
|
4650
|
-
return [];
|
|
4651
|
-
}
|
|
4652
|
-
const limit = pLimit(5);
|
|
4653
|
-
const total = files.length;
|
|
4654
|
-
const progress = showProgress ? createProgress("Uploading files", total) : void 0;
|
|
4655
|
-
const skippedFiles = [];
|
|
4656
|
-
await Promise.all(files.map((relativePath) => limit(async () => {
|
|
4657
|
-
await uploadSingleFile(client, workspaceId, deploymentId, rootDir, relativePath, skippedFiles);
|
|
4658
|
-
if (progress) progress.update();
|
|
4659
|
-
})));
|
|
4660
|
-
if (progress) progress.finish();
|
|
4661
|
-
return skippedFiles;
|
|
4662
|
-
}
|
|
4663
|
-
/**
|
|
4664
|
-
* Recursively collect all deployable files under the given directory.
|
|
4665
|
-
* @param rootDir - Root directory to scan
|
|
4666
|
-
* @param currentDir - Current relative directory (for recursion)
|
|
4667
|
-
* @returns List of file paths relative to rootDir
|
|
4668
|
-
*/
|
|
4669
|
-
async function collectFiles(rootDir, currentDir = "") {
|
|
4670
|
-
const dirPath = path.join(rootDir, currentDir);
|
|
4671
|
-
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
4672
|
-
const files = [];
|
|
4673
|
-
for (const entry of entries) {
|
|
4674
|
-
const rel = path.join(currentDir, entry.name);
|
|
4675
|
-
if (entry.isDirectory()) {
|
|
4676
|
-
const sub = await collectFiles(rootDir, rel);
|
|
4677
|
-
files.push(...sub);
|
|
4678
|
-
} else if (entry.isFile() && !entry.isSymbolicLink() && !shouldIgnoreFile(rel)) files.push(rel);
|
|
4679
|
-
}
|
|
4680
|
-
return files;
|
|
4681
|
-
}
|
|
4682
|
-
async function uploadSingleFile(client, workspaceId, deploymentId, rootDir, filePath, skippedFiles) {
|
|
4683
|
-
const absPath = path.join(rootDir, filePath);
|
|
4684
|
-
const mime = lookup(filePath);
|
|
4685
|
-
if (!mime) {
|
|
4686
|
-
skippedFiles.push(`${filePath} (unsupported content type; no MIME mapping found)`);
|
|
4687
|
-
return;
|
|
4688
|
-
}
|
|
4689
|
-
const contentType = mime;
|
|
4690
|
-
const readStream = fs.createReadStream(absPath, { highWaterMark: CHUNK_SIZE });
|
|
4691
|
-
async function* requestStream() {
|
|
4692
|
-
yield { payload: {
|
|
4693
|
-
case: "initialMetadata",
|
|
4694
|
-
value: {
|
|
4695
|
-
workspaceId,
|
|
4696
|
-
deploymentId,
|
|
4697
|
-
filePath,
|
|
4698
|
-
contentType
|
|
4699
|
-
}
|
|
4700
|
-
} };
|
|
4701
|
-
for await (const chunk of readStream) yield { payload: {
|
|
4702
|
-
case: "chunkData",
|
|
4703
|
-
value: chunk
|
|
4704
|
-
} };
|
|
4705
|
-
}
|
|
4706
|
-
async function uploadWithLogging() {
|
|
4707
|
-
try {
|
|
4708
|
-
await client.uploadFile(requestStream());
|
|
4709
|
-
} catch (error) {
|
|
4710
|
-
if (error instanceof ConnectError && error.code === Code.InvalidArgument) {
|
|
4711
|
-
skippedFiles.push(`${filePath} (server rejected file as invalid: ${error.message})`);
|
|
4712
|
-
return;
|
|
4713
|
-
}
|
|
4714
|
-
throw error;
|
|
4715
|
-
}
|
|
4716
|
-
}
|
|
4717
|
-
await withTimeout(uploadWithLogging(), 2 * 6e4, `Upload timed out for "${filePath}"`);
|
|
4718
|
-
}
|
|
4719
|
-
/**
|
|
4720
|
-
* Log skipped files after a deployment, including reasons for skipping.
|
|
4721
|
-
* @param skippedFiles - List of skipped file descriptions
|
|
4722
|
-
*/
|
|
4723
|
-
function logSkippedFiles(skippedFiles) {
|
|
4724
|
-
if (skippedFiles.length === 0) return;
|
|
4725
|
-
logger.warn("Deployment completed, but some files failed to upload. These files may have unsupported content types or other validation issues. Please review the list below:");
|
|
4726
|
-
for (const file of skippedFiles) logger.log(` - ${file}`);
|
|
4727
|
-
}
|
|
4728
|
-
const deployCommand = defineAppCommand({
|
|
4729
|
-
name: "deploy",
|
|
4730
|
-
description: "Deploy a static website from a local build directory.",
|
|
4731
|
-
args: z.strictObject({
|
|
4732
|
-
...workspaceArgs,
|
|
4733
|
-
name: arg(z.string(), {
|
|
4734
|
-
alias: "n",
|
|
4735
|
-
description: "Static website name"
|
|
4736
|
-
}),
|
|
4737
|
-
dir: arg(z.string(), {
|
|
4738
|
-
alias: "d",
|
|
4739
|
-
description: "Path to the static website files",
|
|
4740
|
-
completion: { type: "directory" }
|
|
4741
|
-
})
|
|
4742
|
-
}),
|
|
4743
|
-
run: async (args) => {
|
|
4744
|
-
await assertWritable({ profile: args.profile });
|
|
4745
|
-
logger.info(`Deploying static website "${args.name}" from directory: ${args.dir}`);
|
|
4746
|
-
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
4747
|
-
const name = args.name;
|
|
4748
|
-
const dir = path.resolve(process.cwd(), args.dir);
|
|
4749
|
-
const workspaceId = await loadWorkspaceId({
|
|
4750
|
-
workspaceId: args["workspace-id"],
|
|
4751
|
-
profile: args.profile
|
|
4752
|
-
});
|
|
4753
|
-
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) throw new Error(`Directory not found or not a directory: ${dir}`);
|
|
4754
|
-
const { url, skippedFiles } = await withTimeout(deployStaticWebsite(client, workspaceId, name, dir, !args.json), 10 * 6e4, "Deployment timed out after 10 minutes.");
|
|
4755
|
-
if (args.json) logger.out({
|
|
4756
|
-
name,
|
|
4757
|
-
workspaceId,
|
|
4758
|
-
url,
|
|
4759
|
-
skippedFiles
|
|
4760
|
-
});
|
|
4761
|
-
else {
|
|
4762
|
-
logger.success(`Static website "${name}" deployed successfully. URL: ${url}`);
|
|
4763
|
-
logSkippedFiles(skippedFiles);
|
|
4764
|
-
}
|
|
4765
|
-
}
|
|
4766
|
-
});
|
|
4767
|
-
|
|
4768
|
-
//#endregion
|
|
4769
|
-
//#region src/cli/commands/staticwebsite/domain/status.ts
|
|
4770
|
-
const statusLabels = {
|
|
4771
|
-
[CustomDomainStatus.UNSPECIFIED]: "unspecified",
|
|
4772
|
-
[CustomDomainStatus.PENDING]: "pending",
|
|
4773
|
-
[CustomDomainStatus.VERIFYING]: "verifying",
|
|
4774
|
-
[CustomDomainStatus.CERT_ISSUED]: "cert_issued",
|
|
4775
|
-
[CustomDomainStatus.ACTIVE]: "active",
|
|
4776
|
-
[CustomDomainStatus.FAILED]: "failed"
|
|
4777
|
-
};
|
|
5188
|
+
//#region src/cli/commands/staticwebsite/domain/status.ts
|
|
5189
|
+
const statusLabels = {
|
|
5190
|
+
[CustomDomainStatus.UNSPECIFIED]: "unspecified",
|
|
5191
|
+
[CustomDomainStatus.PENDING]: "pending",
|
|
5192
|
+
[CustomDomainStatus.VERIFYING]: "verifying",
|
|
5193
|
+
[CustomDomainStatus.CERT_ISSUED]: "cert_issued",
|
|
5194
|
+
[CustomDomainStatus.ACTIVE]: "active",
|
|
5195
|
+
[CustomDomainStatus.FAILED]: "failed"
|
|
5196
|
+
};
|
|
4778
5197
|
|
|
4779
5198
|
//#endregion
|
|
4780
5199
|
//#region src/cli/commands/staticwebsite/domain/get.ts
|
|
@@ -4975,7 +5394,7 @@ const staticwebsiteCommand = defineCommand({
|
|
|
4975
5394
|
name: "staticwebsite",
|
|
4976
5395
|
description: "Manage static websites in your workspace.",
|
|
4977
5396
|
subCommands: {
|
|
4978
|
-
deploy: deployCommand,
|
|
5397
|
+
deploy: deployCommand$1,
|
|
4979
5398
|
domain: domainCommand,
|
|
4980
5399
|
list: listCommand$2,
|
|
4981
5400
|
get: getCommand
|
|
@@ -4985,1201 +5404,6 @@ const staticwebsiteCommand = defineCommand({
|
|
|
4985
5404
|
}
|
|
4986
5405
|
});
|
|
4987
5406
|
|
|
4988
|
-
//#endregion
|
|
4989
|
-
//#region src/cli/commands/tailordb/erd/local-schema.ts
|
|
4990
|
-
/**
|
|
4991
|
-
* Resolve TailorDB namespaces that need local type loading for ERD generation.
|
|
4992
|
-
* @param config - Loaded Tailor config.
|
|
4993
|
-
* @param options - Namespace selection options.
|
|
4994
|
-
* @returns Namespace names to load, or undefined to load all owned namespaces.
|
|
4995
|
-
*/
|
|
4996
|
-
function resolveLocalErdSchemaNamespaces(config, options) {
|
|
4997
|
-
if (options.namespaces) return options.namespaces;
|
|
4998
|
-
if (!options.requireErdSite) return;
|
|
4999
|
-
return Object.entries(config.db ?? {}).flatMap(([namespace, dbConfig]) => "external" in dbConfig || !dbConfig.erdSite ? [] : [namespace]);
|
|
5000
|
-
}
|
|
5001
|
-
/**
|
|
5002
|
-
* Load local TailorDB namespaces exactly as SDK generation/deploy sees them.
|
|
5003
|
-
* @param options - Local schema loading options.
|
|
5004
|
-
* @returns Loaded TailorDB namespace data.
|
|
5005
|
-
*/
|
|
5006
|
-
async function loadLocalErdSchema(options) {
|
|
5007
|
-
const { config, plugins } = await loadConfig(options.configPath);
|
|
5008
|
-
await generateUserTypes({
|
|
5009
|
-
config,
|
|
5010
|
-
configPath: config.path
|
|
5011
|
-
});
|
|
5012
|
-
const application = defineApplication({
|
|
5013
|
-
config,
|
|
5014
|
-
pluginManager: plugins.length > 0 ? new PluginManager(plugins) : void 0
|
|
5015
|
-
});
|
|
5016
|
-
const namespaceNames = resolveLocalErdSchemaNamespaces(config, {
|
|
5017
|
-
namespaces: options.namespaces,
|
|
5018
|
-
requireErdSite: options.requireErdSite
|
|
5019
|
-
});
|
|
5020
|
-
const namespaceFilter = namespaceNames ? new Set(namespaceNames) : void 0;
|
|
5021
|
-
const services = namespaceFilter ? application.tailorDBServices.filter((db) => namespaceFilter.has(db.namespace)) : application.tailorDBServices;
|
|
5022
|
-
if (namespaceFilter && services.length !== namespaceFilter.size) {
|
|
5023
|
-
const available = application.tailorDBServices.map((db) => db.namespace).join(", ");
|
|
5024
|
-
const requested = [...namespaceFilter].join(", ");
|
|
5025
|
-
throw new Error(`TailorDB namespace "${requested}" not found in local config.db.` + (available ? ` Available owned namespaces: ${available}` : ""));
|
|
5026
|
-
}
|
|
5027
|
-
const namespaces = [];
|
|
5028
|
-
for (const db of services) {
|
|
5029
|
-
await db.loadTypes();
|
|
5030
|
-
await db.processNamespacePlugins();
|
|
5031
|
-
namespaces.push({
|
|
5032
|
-
namespace: db.namespace,
|
|
5033
|
-
types: { ...db.types },
|
|
5034
|
-
sourceInfo: new Map(Object.entries(db.typeSourceInfo)),
|
|
5035
|
-
pluginAttachments: db.pluginAttachments
|
|
5036
|
-
});
|
|
5037
|
-
}
|
|
5038
|
-
return {
|
|
5039
|
-
config,
|
|
5040
|
-
namespaces
|
|
5041
|
-
};
|
|
5042
|
-
}
|
|
5043
|
-
|
|
5044
|
-
//#endregion
|
|
5045
|
-
//#region src/cli/commands/tailordb/erd/schema.ts
|
|
5046
|
-
const CLEAN_ROOM_NOTES = [
|
|
5047
|
-
"Generated by a TailorDB-specific viewer implementation.",
|
|
5048
|
-
"The implementation is based on TailorDB schema contracts, public Liam documentation, Liam CLI help, and black-box generated-output observation.",
|
|
5049
|
-
"It does not copy Liam source code, generated JavaScript/CSS, parser internals, or layout internals."
|
|
5050
|
-
];
|
|
5051
|
-
function buildRevision(schema) {
|
|
5052
|
-
return hashContent$1(JSON.stringify(schema)).slice(0, 16);
|
|
5053
|
-
}
|
|
5054
|
-
function toTypeSource(source) {
|
|
5055
|
-
if (!source) return void 0;
|
|
5056
|
-
if (isPluginGeneratedType(source)) return {
|
|
5057
|
-
kind: "plugin",
|
|
5058
|
-
exportName: source.exportName,
|
|
5059
|
-
pluginId: source.pluginId,
|
|
5060
|
-
pluginImportPath: source.pluginImportPath,
|
|
5061
|
-
originalExportName: source.originalExportName,
|
|
5062
|
-
generatedTypeKind: source.generatedTypeKind,
|
|
5063
|
-
namespace: source.namespace
|
|
5064
|
-
};
|
|
5065
|
-
return {
|
|
5066
|
-
kind: "user",
|
|
5067
|
-
exportName: source.exportName
|
|
5068
|
-
};
|
|
5069
|
-
}
|
|
5070
|
-
function toRelationships(relationships) {
|
|
5071
|
-
return Object.entries(relationships).toSorted(([a], [b]) => a.localeCompare(b)).map(([name, relationship]) => ({
|
|
5072
|
-
name,
|
|
5073
|
-
targetType: relationship.targetType,
|
|
5074
|
-
targetField: relationship.targetField,
|
|
5075
|
-
sourceField: relationship.sourceField,
|
|
5076
|
-
isArray: relationship.isArray,
|
|
5077
|
-
...relationship.description && { description: relationship.description }
|
|
5078
|
-
}));
|
|
5079
|
-
}
|
|
5080
|
-
function toIndexes(type) {
|
|
5081
|
-
return Object.entries(type.indexes ?? {}).toSorted(([a], [b]) => a.localeCompare(b)).map(([name, index]) => ({
|
|
5082
|
-
name,
|
|
5083
|
-
fields: [...index.fields],
|
|
5084
|
-
unique: index.unique === true
|
|
5085
|
-
}));
|
|
5086
|
-
}
|
|
5087
|
-
function toColumnRelation(options) {
|
|
5088
|
-
const { fieldConfig, parsedField } = options;
|
|
5089
|
-
if (!fieldConfig.foreignKey || !fieldConfig.foreignKeyType) return;
|
|
5090
|
-
const required = fieldConfig.required !== false;
|
|
5091
|
-
const kind = parsedField?.relation || fieldConfig.rawRelation ? "relation" : "foreignKey";
|
|
5092
|
-
return {
|
|
5093
|
-
targetTable: fieldConfig.foreignKeyType,
|
|
5094
|
-
targetColumn: fieldConfig.foreignKeyField || "id",
|
|
5095
|
-
kind,
|
|
5096
|
-
required,
|
|
5097
|
-
...fieldConfig.rawRelation?.type && { relationType: fieldConfig.rawRelation.type },
|
|
5098
|
-
...parsedField?.relation?.forwardName && { forwardName: parsedField.relation.forwardName },
|
|
5099
|
-
...parsedField?.relation?.backwardName && { backwardName: parsedField.relation.backwardName }
|
|
5100
|
-
};
|
|
5101
|
-
}
|
|
5102
|
-
function toColumn(options) {
|
|
5103
|
-
const { fieldName, fieldConfig } = options;
|
|
5104
|
-
const indexNames = options.indexEntries.filter(([, index]) => index.fields.includes(fieldName)).map(([name]) => name);
|
|
5105
|
-
const uniqueIndexNames = options.indexEntries.filter(([, index]) => index.unique && index.fields.includes(fieldName)).map(([name]) => name);
|
|
5106
|
-
const enumValues = fieldConfig.allowedValues?.map((value) => value.value) ?? [];
|
|
5107
|
-
const enumValueDescriptions = Object.fromEntries((fieldConfig.allowedValues ?? []).flatMap((value) => value.description ? [[value.value, value.description]] : []));
|
|
5108
|
-
const nestedFields = Object.entries(fieldConfig.fields ?? {}).map(([nestedName, nestedConfig]) => toColumn({
|
|
5109
|
-
fieldName: nestedName,
|
|
5110
|
-
fieldConfig: nestedConfig,
|
|
5111
|
-
indexEntries: []
|
|
5112
|
-
}));
|
|
5113
|
-
const relation = toColumnRelation(options);
|
|
5114
|
-
return {
|
|
5115
|
-
name: fieldName,
|
|
5116
|
-
type: fieldConfig.type || "string",
|
|
5117
|
-
required: fieldConfig.required !== false,
|
|
5118
|
-
array: fieldConfig.array === true,
|
|
5119
|
-
...fieldConfig.description && { description: fieldConfig.description },
|
|
5120
|
-
...fieldConfig.unique && { unique: true },
|
|
5121
|
-
...(fieldConfig.index || indexNames.length > 0) && { index: true },
|
|
5122
|
-
...indexNames.length > 0 && { indexNames },
|
|
5123
|
-
...uniqueIndexNames.length > 0 && { uniqueIndexNames },
|
|
5124
|
-
...enumValues.length > 0 && { enumValues },
|
|
5125
|
-
...Object.keys(enumValueDescriptions).length > 0 && { enumValueDescriptions },
|
|
5126
|
-
...fieldConfig.vector && { vector: true },
|
|
5127
|
-
...fieldConfig.serial && { serial: { ...fieldConfig.serial } },
|
|
5128
|
-
...fieldConfig.scale !== void 0 && { scale: fieldConfig.scale },
|
|
5129
|
-
...fieldConfig.validate?.length && { validations: fieldConfig.validate.length },
|
|
5130
|
-
...fieldConfig.hooks && { hooks: {
|
|
5131
|
-
...fieldConfig.hooks.create && { create: true },
|
|
5132
|
-
...fieldConfig.hooks.update && { update: true }
|
|
5133
|
-
} },
|
|
5134
|
-
...nestedFields.length > 0 && { fields: nestedFields },
|
|
5135
|
-
...relation && { relation }
|
|
5136
|
-
};
|
|
5137
|
-
}
|
|
5138
|
-
function toTable(type, source) {
|
|
5139
|
-
const indexes = toIndexes(type);
|
|
5140
|
-
const indexEntries = indexes.map((index) => [index.name, index]);
|
|
5141
|
-
const fieldColumns = Object.entries(type.fields).filter(([fieldName]) => fieldName !== "id").map(([fieldName, field]) => toColumn({
|
|
5142
|
-
fieldName,
|
|
5143
|
-
fieldConfig: field.config,
|
|
5144
|
-
parsedField: field,
|
|
5145
|
-
indexEntries
|
|
5146
|
-
}));
|
|
5147
|
-
const typeSource = toTypeSource(source);
|
|
5148
|
-
return {
|
|
5149
|
-
name: type.name,
|
|
5150
|
-
pluralForm: type.pluralForm,
|
|
5151
|
-
...type.description && { description: type.description },
|
|
5152
|
-
...typeSource && { source: typeSource },
|
|
5153
|
-
columns: [{
|
|
5154
|
-
name: "id",
|
|
5155
|
-
type: "uuid",
|
|
5156
|
-
required: true,
|
|
5157
|
-
array: false,
|
|
5158
|
-
primaryKey: true,
|
|
5159
|
-
unique: true
|
|
5160
|
-
}, ...fieldColumns],
|
|
5161
|
-
indexes,
|
|
5162
|
-
forwardRelationships: toRelationships(type.forwardRelationships),
|
|
5163
|
-
backwardRelationships: toRelationships(type.backwardRelationships)
|
|
5164
|
-
};
|
|
5165
|
-
}
|
|
5166
|
-
function toRelation(sourceTable, field) {
|
|
5167
|
-
const relation = toColumnRelation({
|
|
5168
|
-
fieldName: field.name,
|
|
5169
|
-
fieldConfig: field.config,
|
|
5170
|
-
parsedField: field,
|
|
5171
|
-
indexEntries: []
|
|
5172
|
-
});
|
|
5173
|
-
if (!relation) return void 0;
|
|
5174
|
-
return {
|
|
5175
|
-
name: `${sourceTable}.${field.name}->${relation.targetTable}.${relation.targetColumn}`,
|
|
5176
|
-
sourceTable,
|
|
5177
|
-
sourceColumns: [field.name],
|
|
5178
|
-
targetTable: relation.targetTable,
|
|
5179
|
-
targetColumns: [relation.targetColumn],
|
|
5180
|
-
required: relation.required,
|
|
5181
|
-
unique: field.config.unique === true || field.relation?.unique === true,
|
|
5182
|
-
kind: relation.kind,
|
|
5183
|
-
...relation.relationType && { relationType: relation.relationType },
|
|
5184
|
-
...relation.forwardName && { forwardName: relation.forwardName },
|
|
5185
|
-
...relation.backwardName && { backwardName: relation.backwardName }
|
|
5186
|
-
};
|
|
5187
|
-
}
|
|
5188
|
-
function buildRelations(types) {
|
|
5189
|
-
const relations = [];
|
|
5190
|
-
for (const type of Object.values(types)) for (const field of Object.values(type.fields)) {
|
|
5191
|
-
const relation = toRelation(type.name, field);
|
|
5192
|
-
if (relation) relations.push(relation);
|
|
5193
|
-
}
|
|
5194
|
-
return relations.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
5195
|
-
}
|
|
5196
|
-
/**
|
|
5197
|
-
* Build the TailorDB ERD viewer schema for one namespace.
|
|
5198
|
-
* @param options - Schema build options.
|
|
5199
|
-
* @returns TailorDB ERD viewer schema.
|
|
5200
|
-
*/
|
|
5201
|
-
function buildTailorDbErdSchema(options) {
|
|
5202
|
-
const { namespaceData } = options;
|
|
5203
|
-
const tables = Object.values(namespaceData.types).toSorted((a, b) => a.name.localeCompare(b.name)).map((type) => toTable(type, namespaceData.sourceInfo.get(type.name)));
|
|
5204
|
-
const schemaWithoutRevision = {
|
|
5205
|
-
version: 1,
|
|
5206
|
-
namespace: namespaceData.namespace,
|
|
5207
|
-
source: options.source ?? "local",
|
|
5208
|
-
cleanRoom: {
|
|
5209
|
-
implementation: "tailor",
|
|
5210
|
-
notes: CLEAN_ROOM_NOTES
|
|
5211
|
-
},
|
|
5212
|
-
tables,
|
|
5213
|
-
relations: buildRelations(namespaceData.types)
|
|
5214
|
-
};
|
|
5215
|
-
return {
|
|
5216
|
-
...schemaWithoutRevision,
|
|
5217
|
-
generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
5218
|
-
revision: buildRevision(schemaWithoutRevision)
|
|
5219
|
-
};
|
|
5220
|
-
}
|
|
5221
|
-
|
|
5222
|
-
//#endregion
|
|
5223
|
-
//#region src/cli/commands/tailordb/erd/utils.ts
|
|
5224
|
-
/**
|
|
5225
|
-
* Initialize shared ERD command behavior.
|
|
5226
|
-
*/
|
|
5227
|
-
function initErdCommand() {
|
|
5228
|
-
logBetaWarning("tailordb erd");
|
|
5229
|
-
}
|
|
5230
|
-
/**
|
|
5231
|
-
* Initialize platform context for ERD deployment.
|
|
5232
|
-
* @param args - CLI arguments.
|
|
5233
|
-
* @returns Initialized deploy context.
|
|
5234
|
-
*/
|
|
5235
|
-
async function initErdDeployContext(args) {
|
|
5236
|
-
initErdCommand();
|
|
5237
|
-
return {
|
|
5238
|
-
client: await initOperatorClient(await loadAccessToken({ profile: args.profile })),
|
|
5239
|
-
workspaceId: await loadWorkspaceId({
|
|
5240
|
-
workspaceId: args.workspaceId,
|
|
5241
|
-
profile: args.profile
|
|
5242
|
-
})
|
|
5243
|
-
};
|
|
5244
|
-
}
|
|
5245
|
-
|
|
5246
|
-
//#endregion
|
|
5247
|
-
//#region src/cli/commands/tailordb/erd/viewer.ts
|
|
5248
|
-
const VIEWER_ASSETS_DIR = "erd-viewer-assets";
|
|
5249
|
-
const STYLES_LINK = "<link rel=\"stylesheet\" href=\"./styles.css\" />";
|
|
5250
|
-
const APP_SCRIPT = "<script src=\"./app.js\" type=\"module\"><\/script>";
|
|
5251
|
-
function escapeHtml(value) {
|
|
5252
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """);
|
|
5253
|
-
}
|
|
5254
|
-
function assetDirCandidates() {
|
|
5255
|
-
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
5256
|
-
return [
|
|
5257
|
-
path.join(currentDir, "viewer-assets"),
|
|
5258
|
-
path.join(currentDir, VIEWER_ASSETS_DIR),
|
|
5259
|
-
path.join(currentDir, "commands", "tailordb", "erd", VIEWER_ASSETS_DIR),
|
|
5260
|
-
path.resolve(process.cwd(), "packages/sdk/src/cli/commands/tailordb/erd/viewer-assets")
|
|
5261
|
-
];
|
|
5262
|
-
}
|
|
5263
|
-
/**
|
|
5264
|
-
* Resolve the packaged ERD viewer asset directory.
|
|
5265
|
-
* @returns Absolute path to the viewer asset directory.
|
|
5266
|
-
*/
|
|
5267
|
-
function resolveViewerAssetsDir() {
|
|
5268
|
-
for (const candidate of assetDirCandidates()) if (fs$1.existsSync(path.join(candidate, "index.html"))) return candidate;
|
|
5269
|
-
throw new Error(`ERD viewer assets not found. Checked: ${assetDirCandidates().join(", ")}`);
|
|
5270
|
-
}
|
|
5271
|
-
/**
|
|
5272
|
-
* Build the self-contained ERD viewer HTML document. CSS, JS, and the schema
|
|
5273
|
-
* are inlined as separately extractable blocks: a `<style>` element, a
|
|
5274
|
-
* `<script type="module">`, and a `<script type="application/json"
|
|
5275
|
-
* id="erd-schema">` data block. This renders without any sibling asset files
|
|
5276
|
-
* and lets external tooling (including the ERD diff command) pull out the
|
|
5277
|
-
* schema via `JSON.parse`.
|
|
5278
|
-
* @param options - Viewer build options.
|
|
5279
|
-
* @returns The self-contained HTML document.
|
|
5280
|
-
*/
|
|
5281
|
-
function buildViewerHtml(options) {
|
|
5282
|
-
const assetsDir = resolveViewerAssetsDir();
|
|
5283
|
-
const html = fs$1.readFileSync(path.join(assetsDir, "index.html"), "utf8");
|
|
5284
|
-
const css = fs$1.readFileSync(path.join(assetsDir, "styles.css"), "utf8");
|
|
5285
|
-
const appJs = fs$1.readFileSync(path.join(assetsDir, "app.js"), "utf8");
|
|
5286
|
-
if (!html.includes(STYLES_LINK) || !html.includes(APP_SCRIPT)) throw new Error("ERD viewer index.html is missing expected asset references for inlining.");
|
|
5287
|
-
const embedScript = `<script type="application/json" id="erd-schema">${JSON.stringify(options.schema).replaceAll("<", "\\u003c")}<\/script>`;
|
|
5288
|
-
const currentSchemaScript = options.currentSchema === void 0 ? "" : `\n <script type="application/json" id="erd-current-schema">${JSON.stringify(options.currentSchema).replaceAll("<", "\\u003c")}<\/script>`;
|
|
5289
|
-
const diffScript = options.diff === void 0 ? "" : `\n <script type="application/json" id="erd-diff">${JSON.stringify(options.diff).replaceAll("<", "\\u003c")}<\/script>`;
|
|
5290
|
-
const inlineScript = `<script type="module">\n${appJs.replace(/<\/script/gi, "<\\/script")}\n<\/script>`;
|
|
5291
|
-
return html.replace("<title>TailorDB ERD</title>", () => `<title>${escapeHtml(options.title ?? "TailorDB ERD")}</title>`).replace(STYLES_LINK, () => `<style>\n${css}\n</style>`).replace(APP_SCRIPT, () => `${embedScript}${currentSchemaScript}${diffScript}\n ${inlineScript}`);
|
|
5292
|
-
}
|
|
5293
|
-
/**
|
|
5294
|
-
* Write the self-contained TailorDB ERD viewer to `<distDir>/index.html`.
|
|
5295
|
-
* @param options - Viewer dist write options.
|
|
5296
|
-
*/
|
|
5297
|
-
function writeViewerDist(options) {
|
|
5298
|
-
fs$1.rmSync(options.distDir, {
|
|
5299
|
-
recursive: true,
|
|
5300
|
-
force: true
|
|
5301
|
-
});
|
|
5302
|
-
fs$1.mkdirSync(options.distDir, { recursive: true });
|
|
5303
|
-
fs$1.writeFileSync(path.join(options.distDir, "index.html"), buildViewerHtml({ schema: options.schema }), "utf8");
|
|
5304
|
-
}
|
|
5305
|
-
|
|
5306
|
-
//#endregion
|
|
5307
|
-
//#region src/cli/commands/tailordb/erd/export.ts
|
|
5308
|
-
const DEFAULT_ERD_BASE_DIR$1 = ".tailor/erd";
|
|
5309
|
-
function getErdSite(context, namespace) {
|
|
5310
|
-
const dbConfig = context.config.db?.[namespace];
|
|
5311
|
-
if (!dbConfig || "external" in dbConfig) return;
|
|
5312
|
-
return dbConfig.erdSite;
|
|
5313
|
-
}
|
|
5314
|
-
function resolveExplicitTarget(options) {
|
|
5315
|
-
const namespaceData = options.context.namespaces.find((candidate) => candidate.namespace === options.namespace);
|
|
5316
|
-
if (!namespaceData) {
|
|
5317
|
-
const available = options.context.namespaces.map((candidate) => candidate.namespace).join(", ");
|
|
5318
|
-
throw new Error(`TailorDB namespace "${options.namespace}" not found in local config.db.` + (available ? ` Available owned namespaces: ${available}` : ""));
|
|
5319
|
-
}
|
|
5320
|
-
const erdSite = getErdSite(options.context, namespaceData.namespace);
|
|
5321
|
-
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.`);
|
|
5322
|
-
return toTarget(options.outputDir, namespaceData, erdSite);
|
|
5323
|
-
}
|
|
5324
|
-
function resolveAllTargets(options) {
|
|
5325
|
-
const namespaces = options.context.namespaces.filter((namespaceData) => !options.requireErdSite || getErdSite(options.context, namespaceData.namespace));
|
|
5326
|
-
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.");
|
|
5327
|
-
logger.info(`Found ${namespaces.length} namespace(s)${options.requireErdSite ? " with erdSite configured" : ""}.`);
|
|
5328
|
-
return namespaces.map((namespaceData) => toTarget(options.outputDir, namespaceData, getErdSite(options.context, namespaceData.namespace)));
|
|
5329
|
-
}
|
|
5330
|
-
function toTarget(outputDir, namespaceData, erdSite) {
|
|
5331
|
-
return {
|
|
5332
|
-
namespaceData,
|
|
5333
|
-
erdSite,
|
|
5334
|
-
distDir: path.join(outputDir, namespaceData.namespace, "dist")
|
|
5335
|
-
};
|
|
5336
|
-
}
|
|
5337
|
-
function resolveTargets(options) {
|
|
5338
|
-
if (options.namespace) return [resolveExplicitTarget(options)];
|
|
5339
|
-
return resolveAllTargets(options);
|
|
5340
|
-
}
|
|
5341
|
-
function prepareErdBuild(target) {
|
|
5342
|
-
writeViewerDist({
|
|
5343
|
-
schema: buildTailorDbErdSchema({ namespaceData: target.namespaceData }),
|
|
5344
|
-
distDir: target.distDir
|
|
5345
|
-
});
|
|
5346
|
-
const relativePath = path.relative(process.cwd(), target.distDir);
|
|
5347
|
-
logger.success(`Built ERD to ${relativePath}`);
|
|
5348
|
-
return {
|
|
5349
|
-
namespace: target.namespaceData.namespace,
|
|
5350
|
-
erdSite: target.erdSite,
|
|
5351
|
-
distDir: target.distDir
|
|
5352
|
-
};
|
|
5353
|
-
}
|
|
5354
|
-
/**
|
|
5355
|
-
* Prepare TailorDB ERD static viewer builds for one or more namespaces.
|
|
5356
|
-
* @param options - Build options.
|
|
5357
|
-
* @returns Build results by namespace.
|
|
5358
|
-
*/
|
|
5359
|
-
async function prepareErdBuilds(options) {
|
|
5360
|
-
return prepareErdBuildsFromContext({
|
|
5361
|
-
context: await loadLocalErdSchema({
|
|
5362
|
-
configPath: options.configPath,
|
|
5363
|
-
namespaces: options.namespace ? [options.namespace] : void 0,
|
|
5364
|
-
requireErdSite: options.requireErdSite
|
|
5365
|
-
}),
|
|
5366
|
-
namespace: options.namespace,
|
|
5367
|
-
outputDir: options.outputDir,
|
|
5368
|
-
requireErdSite: options.requireErdSite
|
|
5369
|
-
});
|
|
5370
|
-
}
|
|
5371
|
-
/**
|
|
5372
|
-
* Prepare TailorDB ERD static viewer builds from an already loaded schema context.
|
|
5373
|
-
* @param options - Build options.
|
|
5374
|
-
* @returns Build results by namespace.
|
|
5375
|
-
*/
|
|
5376
|
-
function prepareErdBuildsFromContext(options) {
|
|
5377
|
-
const outputDir = path.resolve(process.cwd(), options.outputDir ?? DEFAULT_ERD_BASE_DIR$1);
|
|
5378
|
-
return resolveTargets({
|
|
5379
|
-
context: options.context,
|
|
5380
|
-
namespace: options.namespace,
|
|
5381
|
-
outputDir,
|
|
5382
|
-
requireErdSite: options.requireErdSite
|
|
5383
|
-
}).map((target) => prepareErdBuild(target));
|
|
5384
|
-
}
|
|
5385
|
-
const erdExportCommand = defineAppCommand({
|
|
5386
|
-
name: "export",
|
|
5387
|
-
description: "Export TailorDB ERD static viewer from local TailorDB schema.",
|
|
5388
|
-
args: z.strictObject({
|
|
5389
|
-
...configArg,
|
|
5390
|
-
namespace: arg(z.string().optional(), {
|
|
5391
|
-
alias: "n",
|
|
5392
|
-
description: "TailorDB namespace name (optional if only one namespace is defined in config)"
|
|
5393
|
-
}),
|
|
5394
|
-
output: arg(z.string().default(DEFAULT_ERD_BASE_DIR$1), {
|
|
5395
|
-
alias: "o",
|
|
5396
|
-
description: "Output directory path for TailorDB ERD viewer files (writes to `<outputDir>/<namespace>/dist`)",
|
|
5397
|
-
completion: { type: "directory" }
|
|
5398
|
-
})
|
|
5399
|
-
}),
|
|
5400
|
-
run: async (args) => {
|
|
5401
|
-
initErdCommand();
|
|
5402
|
-
const results = await prepareErdBuilds({
|
|
5403
|
-
configPath: args.config,
|
|
5404
|
-
namespace: args.namespace,
|
|
5405
|
-
outputDir: args.output
|
|
5406
|
-
});
|
|
5407
|
-
logger.newline();
|
|
5408
|
-
if (args.json) logger.out(results.map((result) => ({
|
|
5409
|
-
namespace: result.namespace,
|
|
5410
|
-
distDir: result.distDir
|
|
5411
|
-
})));
|
|
5412
|
-
else for (const result of results) {
|
|
5413
|
-
logger.out(`Exported ERD for namespace "${result.namespace}"`);
|
|
5414
|
-
logger.out(` - ERD viewer: ${path.join(result.distDir, "index.html")}`);
|
|
5415
|
-
}
|
|
5416
|
-
}
|
|
5417
|
-
});
|
|
5418
|
-
|
|
5419
|
-
//#endregion
|
|
5420
|
-
//#region src/cli/commands/tailordb/erd/deploy.ts
|
|
5421
|
-
const erdDeployCommand = defineAppCommand({
|
|
5422
|
-
name: "deploy",
|
|
5423
|
-
description: "Deploy ERD static website for TailorDB namespace(s).",
|
|
5424
|
-
args: z.strictObject({
|
|
5425
|
-
...deploymentArgs,
|
|
5426
|
-
namespace: arg(z.string().optional(), {
|
|
5427
|
-
alias: "n",
|
|
5428
|
-
description: "TailorDB namespace name (optional - deploys all namespaces with erdSite if omitted)"
|
|
5429
|
-
})
|
|
5430
|
-
}),
|
|
5431
|
-
run: async (args) => {
|
|
5432
|
-
await assertWritable({ profile: args.profile });
|
|
5433
|
-
const { client, workspaceId } = await initErdDeployContext(args);
|
|
5434
|
-
const buildResults = await prepareErdBuilds({
|
|
5435
|
-
configPath: args.config,
|
|
5436
|
-
namespace: args.namespace,
|
|
5437
|
-
requireErdSite: true
|
|
5438
|
-
});
|
|
5439
|
-
const deployResults = await Promise.all(buildResults.map(async (result) => {
|
|
5440
|
-
if (!result.erdSite) throw new Error(`No erdSite configured for namespace "${result.namespace}". Add erdSite: "<static-website-name>" to db.${result.namespace} in tailor.config.ts.`);
|
|
5441
|
-
if (!args.json) logger.info(`Deploying ERD for namespace "${result.namespace}" to site "${result.erdSite}"...`);
|
|
5442
|
-
const { url, skippedFiles } = await deployStaticWebsite(client, workspaceId, result.erdSite, result.distDir, !args.json);
|
|
5443
|
-
return {
|
|
5444
|
-
namespace: result.namespace,
|
|
5445
|
-
erdSite: result.erdSite,
|
|
5446
|
-
url,
|
|
5447
|
-
skippedFiles
|
|
5448
|
-
};
|
|
5449
|
-
}));
|
|
5450
|
-
logger.newline();
|
|
5451
|
-
if (args.json) logger.out(deployResults);
|
|
5452
|
-
else for (const result of deployResults) {
|
|
5453
|
-
logSkippedFiles(result.skippedFiles);
|
|
5454
|
-
logger.newline();
|
|
5455
|
-
logger.success(`ERD site "${result.erdSite}" deployed successfully.`);
|
|
5456
|
-
logger.out(result.url);
|
|
5457
|
-
}
|
|
5458
|
-
}
|
|
5459
|
-
});
|
|
5460
|
-
|
|
5461
|
-
//#endregion
|
|
5462
|
-
//#region src/cli/commands/tailordb/erd/diff.ts
|
|
5463
|
-
const SCHEMA_BLOCK_PATTERN = /<script type="application\/json" id="erd-schema">([\s\S]*?)<\/script>/;
|
|
5464
|
-
function stableValue(value) {
|
|
5465
|
-
if (Array.isArray(value)) return value.map((item) => stableValue(item));
|
|
5466
|
-
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, stableValue(item)]));
|
|
5467
|
-
return value;
|
|
5468
|
-
}
|
|
5469
|
-
function stableJson(value) {
|
|
5470
|
-
return JSON.stringify(stableValue(value));
|
|
5471
|
-
}
|
|
5472
|
-
function changedFields(base, head) {
|
|
5473
|
-
return [.../* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(head)])].toSorted((a, b) => a.localeCompare(b)).filter((key) => stableJson(base[key]) !== stableJson(head[key]));
|
|
5474
|
-
}
|
|
5475
|
-
function detailForChangedFields(fields) {
|
|
5476
|
-
return `Changed fields: ${fields.join(", ")}`;
|
|
5477
|
-
}
|
|
5478
|
-
function mapByName(items) {
|
|
5479
|
-
return new Map(items.map((item) => [item.name, item]));
|
|
5480
|
-
}
|
|
5481
|
-
function compareCommonNamed(baseItems, headItems, addChange) {
|
|
5482
|
-
const baseByName = mapByName(baseItems);
|
|
5483
|
-
const headByName = mapByName(headItems);
|
|
5484
|
-
const commonNames = [...baseByName.keys()].filter((name) => headByName.has(name)).toSorted((a, b) => a.localeCompare(b));
|
|
5485
|
-
for (const name of commonNames) {
|
|
5486
|
-
const base = baseByName.get(name);
|
|
5487
|
-
const head = headByName.get(name);
|
|
5488
|
-
if (!base || !head) continue;
|
|
5489
|
-
const fields = changedFields(base, head);
|
|
5490
|
-
if (fields.length > 0) addChange(name, fields);
|
|
5491
|
-
}
|
|
5492
|
-
}
|
|
5493
|
-
function removedNames(baseItems, headItems) {
|
|
5494
|
-
const headByName = mapByName(headItems);
|
|
5495
|
-
return baseItems.map((item) => item.name).filter((name) => !headByName.has(name)).toSorted((a, b) => a.localeCompare(b));
|
|
5496
|
-
}
|
|
5497
|
-
function addedNames(baseItems, headItems) {
|
|
5498
|
-
const baseByName = mapByName(baseItems);
|
|
5499
|
-
return headItems.map((item) => item.name).filter((name) => !baseByName.has(name)).toSorted((a, b) => a.localeCompare(b));
|
|
5500
|
-
}
|
|
5501
|
-
function tableMetadata(table) {
|
|
5502
|
-
return {
|
|
5503
|
-
backwardRelationships: table.backwardRelationships,
|
|
5504
|
-
description: table.description,
|
|
5505
|
-
forwardRelationships: table.forwardRelationships,
|
|
5506
|
-
pluralForm: table.pluralForm,
|
|
5507
|
-
source: table.source
|
|
5508
|
-
};
|
|
5509
|
-
}
|
|
5510
|
-
function pushChange(changes, change) {
|
|
5511
|
-
changes.push(change);
|
|
5512
|
-
}
|
|
5513
|
-
function diffColumns(changes, tableName, baseColumns, headColumns) {
|
|
5514
|
-
compareCommonNamed(baseColumns, headColumns, (name, fields) => {
|
|
5515
|
-
pushChange(changes, {
|
|
5516
|
-
action: "changed",
|
|
5517
|
-
entity: "column",
|
|
5518
|
-
path: `${tableName}.${name}`,
|
|
5519
|
-
detail: detailForChangedFields(fields)
|
|
5520
|
-
});
|
|
5521
|
-
});
|
|
5522
|
-
for (const name of removedNames(baseColumns, headColumns)) pushChange(changes, {
|
|
5523
|
-
action: "removed",
|
|
5524
|
-
entity: "column",
|
|
5525
|
-
path: `${tableName}.${name}`,
|
|
5526
|
-
detail: "Column removed"
|
|
5527
|
-
});
|
|
5528
|
-
for (const name of addedNames(baseColumns, headColumns)) pushChange(changes, {
|
|
5529
|
-
action: "added",
|
|
5530
|
-
entity: "column",
|
|
5531
|
-
path: `${tableName}.${name}`,
|
|
5532
|
-
detail: "Column added"
|
|
5533
|
-
});
|
|
5534
|
-
}
|
|
5535
|
-
function diffIndexes(changes, tableName, baseIndexes, headIndexes) {
|
|
5536
|
-
compareCommonNamed(baseIndexes, headIndexes, (name, fields) => {
|
|
5537
|
-
pushChange(changes, {
|
|
5538
|
-
action: "changed",
|
|
5539
|
-
entity: "index",
|
|
5540
|
-
path: `${tableName}.${name}`,
|
|
5541
|
-
detail: detailForChangedFields(fields)
|
|
5542
|
-
});
|
|
5543
|
-
});
|
|
5544
|
-
for (const name of removedNames(baseIndexes, headIndexes)) pushChange(changes, {
|
|
5545
|
-
action: "removed",
|
|
5546
|
-
entity: "index",
|
|
5547
|
-
path: `${tableName}.${name}`,
|
|
5548
|
-
detail: "Index removed"
|
|
5549
|
-
});
|
|
5550
|
-
for (const name of addedNames(baseIndexes, headIndexes)) pushChange(changes, {
|
|
5551
|
-
action: "added",
|
|
5552
|
-
entity: "index",
|
|
5553
|
-
path: `${tableName}.${name}`,
|
|
5554
|
-
detail: "Index added"
|
|
5555
|
-
});
|
|
5556
|
-
}
|
|
5557
|
-
function diffTables(changes, baseTables, headTables) {
|
|
5558
|
-
const baseByName = mapByName(baseTables);
|
|
5559
|
-
const headByName = mapByName(headTables);
|
|
5560
|
-
for (const name of removedNames(baseTables, headTables)) pushChange(changes, {
|
|
5561
|
-
action: "removed",
|
|
5562
|
-
entity: "table",
|
|
5563
|
-
path: name,
|
|
5564
|
-
detail: "Table removed"
|
|
5565
|
-
});
|
|
5566
|
-
for (const name of addedNames(baseTables, headTables)) pushChange(changes, {
|
|
5567
|
-
action: "added",
|
|
5568
|
-
entity: "table",
|
|
5569
|
-
path: name,
|
|
5570
|
-
detail: "Table added"
|
|
5571
|
-
});
|
|
5572
|
-
const commonNames = [...baseByName.keys()].filter((name) => headByName.has(name)).toSorted((a, b) => a.localeCompare(b));
|
|
5573
|
-
for (const name of commonNames) {
|
|
5574
|
-
const base = baseByName.get(name);
|
|
5575
|
-
const head = headByName.get(name);
|
|
5576
|
-
if (!base || !head) continue;
|
|
5577
|
-
const tableFields = changedFields(tableMetadata(base), tableMetadata(head));
|
|
5578
|
-
if (tableFields.length > 0) pushChange(changes, {
|
|
5579
|
-
action: "changed",
|
|
5580
|
-
entity: "table",
|
|
5581
|
-
path: name,
|
|
5582
|
-
detail: detailForChangedFields(tableFields)
|
|
5583
|
-
});
|
|
5584
|
-
diffColumns(changes, name, base.columns, head.columns);
|
|
5585
|
-
diffIndexes(changes, name, base.indexes, head.indexes);
|
|
5586
|
-
}
|
|
5587
|
-
}
|
|
5588
|
-
function diffRelations(changes, baseRelations, headRelations) {
|
|
5589
|
-
compareCommonNamed(baseRelations, headRelations, (name, fields) => {
|
|
5590
|
-
pushChange(changes, {
|
|
5591
|
-
action: "changed",
|
|
5592
|
-
entity: "relation",
|
|
5593
|
-
path: name,
|
|
5594
|
-
detail: detailForChangedFields(fields)
|
|
5595
|
-
});
|
|
5596
|
-
});
|
|
5597
|
-
for (const name of removedNames(baseRelations, headRelations)) pushChange(changes, {
|
|
5598
|
-
action: "removed",
|
|
5599
|
-
entity: "relation",
|
|
5600
|
-
path: name,
|
|
5601
|
-
detail: "Relation removed"
|
|
5602
|
-
});
|
|
5603
|
-
for (const name of addedNames(baseRelations, headRelations)) pushChange(changes, {
|
|
5604
|
-
action: "added",
|
|
5605
|
-
entity: "relation",
|
|
5606
|
-
path: name,
|
|
5607
|
-
detail: "Relation added"
|
|
5608
|
-
});
|
|
5609
|
-
}
|
|
5610
|
-
function summarize(changes) {
|
|
5611
|
-
return {
|
|
5612
|
-
added: changes.filter((change) => change.action === "added").length,
|
|
5613
|
-
changed: changes.filter((change) => change.action === "changed").length,
|
|
5614
|
-
removed: changes.filter((change) => change.action === "removed").length
|
|
5615
|
-
};
|
|
5616
|
-
}
|
|
5617
|
-
function extractEmbeddedErdSchema(html) {
|
|
5618
|
-
const match = SCHEMA_BLOCK_PATTERN.exec(html);
|
|
5619
|
-
if (!match?.[1]) throw new Error("ERD schema block not found.");
|
|
5620
|
-
try {
|
|
5621
|
-
return JSON.parse(match[1]);
|
|
5622
|
-
} catch (error) {
|
|
5623
|
-
throw new Error(`Failed to parse ERD schema block: ${String(error)}`, { cause: error });
|
|
5624
|
-
}
|
|
5625
|
-
}
|
|
5626
|
-
function createEmptyErdSchema(options) {
|
|
5627
|
-
return {
|
|
5628
|
-
version: 1,
|
|
5629
|
-
namespace: options.namespace,
|
|
5630
|
-
generatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
5631
|
-
revision: options.revision,
|
|
5632
|
-
source: "local",
|
|
5633
|
-
cleanRoom: {
|
|
5634
|
-
implementation: "tailor",
|
|
5635
|
-
notes: ["Synthetic empty schema used for ERD diff generation."]
|
|
5636
|
-
},
|
|
5637
|
-
tables: [],
|
|
5638
|
-
relations: []
|
|
5639
|
-
};
|
|
5640
|
-
}
|
|
5641
|
-
function buildErdSchemaDiff(options) {
|
|
5642
|
-
const { base, head } = options;
|
|
5643
|
-
if (base.namespace !== head.namespace) throw new Error(`Cannot diff ERD schemas from different namespaces: ${base.namespace}, ${head.namespace}`);
|
|
5644
|
-
const changes = [];
|
|
5645
|
-
diffTables(changes, base.tables, head.tables);
|
|
5646
|
-
diffRelations(changes, base.relations, head.relations);
|
|
5647
|
-
const summary = summarize(changes);
|
|
5648
|
-
return {
|
|
5649
|
-
namespace: head.namespace,
|
|
5650
|
-
baseRevision: base.revision,
|
|
5651
|
-
headRevision: head.revision,
|
|
5652
|
-
changed: changes.length > 0,
|
|
5653
|
-
summary,
|
|
5654
|
-
changes
|
|
5655
|
-
};
|
|
5656
|
-
}
|
|
5657
|
-
function mergeNamedByHead(baseItems, headItems) {
|
|
5658
|
-
const headByName = mapByName(headItems);
|
|
5659
|
-
const removedItems = baseItems.filter((item) => !headByName.has(item.name)).toSorted((a, b) => a.name.localeCompare(b.name));
|
|
5660
|
-
return [...headItems, ...removedItems];
|
|
5661
|
-
}
|
|
5662
|
-
function mergeDiffViewerTable(baseTable, headTable) {
|
|
5663
|
-
return {
|
|
5664
|
-
...headTable,
|
|
5665
|
-
columns: mergeNamedByHead(baseTable.columns, headTable.columns),
|
|
5666
|
-
indexes: mergeNamedByHead(baseTable.indexes, headTable.indexes)
|
|
5667
|
-
};
|
|
5668
|
-
}
|
|
5669
|
-
function mergeDiffViewerTables(baseTables, headTables) {
|
|
5670
|
-
const baseByName = mapByName(baseTables);
|
|
5671
|
-
const headByName = mapByName(headTables);
|
|
5672
|
-
const merged = headTables.map((headTable) => {
|
|
5673
|
-
const baseTable = baseByName.get(headTable.name);
|
|
5674
|
-
return baseTable ? mergeDiffViewerTable(baseTable, headTable) : headTable;
|
|
5675
|
-
});
|
|
5676
|
-
const removedTables = baseTables.filter((table) => !headByName.has(table.name)).toSorted((a, b) => a.name.localeCompare(b.name));
|
|
5677
|
-
return [...merged, ...removedTables];
|
|
5678
|
-
}
|
|
5679
|
-
/**
|
|
5680
|
-
* Build the schema rendered by the diff viewer. The ordinary ERD viewer only
|
|
5681
|
-
* knows how to draw objects present in its schema, so the diff schema keeps
|
|
5682
|
-
* base-only tables, columns, indexes, and relations visible for removal
|
|
5683
|
-
* highlighting while preferring head-side metadata for unchanged objects.
|
|
5684
|
-
* @param options - Base and head schemas to merge for diff rendering.
|
|
5685
|
-
* @returns A TailorDB ERD schema suitable for the visual diff viewer.
|
|
5686
|
-
*/
|
|
5687
|
-
function buildErdDiffViewerSchema(options) {
|
|
5688
|
-
const { base, head } = options;
|
|
5689
|
-
if (base.namespace !== head.namespace) throw new Error(`Cannot diff ERD schemas from different namespaces: ${base.namespace}, ${head.namespace}`);
|
|
5690
|
-
return {
|
|
5691
|
-
...head,
|
|
5692
|
-
tables: mergeDiffViewerTables(base.tables, head.tables),
|
|
5693
|
-
relations: mergeNamedByHead(base.relations, head.relations)
|
|
5694
|
-
};
|
|
5695
|
-
}
|
|
5696
|
-
function renderErdDiffHtml(options) {
|
|
5697
|
-
return buildViewerHtml({
|
|
5698
|
-
schema: options.schema,
|
|
5699
|
-
currentSchema: options.currentSchema,
|
|
5700
|
-
diff: options.diff,
|
|
5701
|
-
title: `TailorDB ERD diff - ${options.diff.namespace}`
|
|
5702
|
-
});
|
|
5703
|
-
}
|
|
5704
|
-
|
|
5705
|
-
//#endregion
|
|
5706
|
-
//#region src/cli/commands/tailordb/erd/diff-command.ts
|
|
5707
|
-
function readSchema(filePath) {
|
|
5708
|
-
if (!filePath) return void 0;
|
|
5709
|
-
return extractEmbeddedErdSchema(fs$1.readFileSync(filePath, "utf8"));
|
|
5710
|
-
}
|
|
5711
|
-
function resolveNamespace(options) {
|
|
5712
|
-
const namespace = options.namespace ?? options.head?.namespace ?? options.base?.namespace;
|
|
5713
|
-
if (!namespace) throw new Error("Missing --namespace when one side of the ERD diff is omitted.");
|
|
5714
|
-
if (options.base && options.base.namespace !== namespace) throw new Error(`Base ERD namespace "${options.base.namespace}" does not match "${namespace}".`);
|
|
5715
|
-
if (options.head && options.head.namespace !== namespace) throw new Error(`Head ERD namespace "${options.head.namespace}" does not match "${namespace}".`);
|
|
5716
|
-
return namespace;
|
|
5717
|
-
}
|
|
5718
|
-
function writeFile(filePath, content) {
|
|
5719
|
-
fs$1.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
5720
|
-
fs$1.writeFileSync(filePath, content, "utf8");
|
|
5721
|
-
}
|
|
5722
|
-
function writeErdDiff(options) {
|
|
5723
|
-
const base = readSchema(options.baseHtml);
|
|
5724
|
-
const head = readSchema(options.headHtml);
|
|
5725
|
-
if (!base && !head) throw new Error("At least one of --base-html or --head-html is required.");
|
|
5726
|
-
const namespace = resolveNamespace({
|
|
5727
|
-
namespace: options.namespace,
|
|
5728
|
-
base,
|
|
5729
|
-
head
|
|
5730
|
-
});
|
|
5731
|
-
const baseSchema = base ?? createEmptyErdSchema({
|
|
5732
|
-
namespace,
|
|
5733
|
-
revision: "missing-base"
|
|
5734
|
-
});
|
|
5735
|
-
const headSchema = head ?? createEmptyErdSchema({
|
|
5736
|
-
namespace,
|
|
5737
|
-
revision: "missing-head"
|
|
5738
|
-
});
|
|
5739
|
-
const diff = buildErdSchemaDiff({
|
|
5740
|
-
base: baseSchema,
|
|
5741
|
-
head: headSchema
|
|
5742
|
-
});
|
|
5743
|
-
const viewerSchema = buildErdDiffViewerSchema({
|
|
5744
|
-
base: baseSchema,
|
|
5745
|
-
head: headSchema
|
|
5746
|
-
});
|
|
5747
|
-
writeFile(options.outputHtml, renderErdDiffHtml({
|
|
5748
|
-
schema: viewerSchema,
|
|
5749
|
-
currentSchema: headSchema,
|
|
5750
|
-
diff
|
|
5751
|
-
}));
|
|
5752
|
-
if (options.outputJson) writeFile(options.outputJson, `${JSON.stringify(diff, null, 2)}\n`);
|
|
5753
|
-
return {
|
|
5754
|
-
namespace,
|
|
5755
|
-
outputHtml: options.outputHtml,
|
|
5756
|
-
outputJson: options.outputJson,
|
|
5757
|
-
diff
|
|
5758
|
-
};
|
|
5759
|
-
}
|
|
5760
|
-
const erdDiffCommand = defineAppCommand({
|
|
5761
|
-
name: "diff",
|
|
5762
|
-
description: "Render TailorDB ERD schema diff HTML from exported ERD viewers.",
|
|
5763
|
-
args: z.strictObject({
|
|
5764
|
-
"base-html": arg(z.string().optional(), {
|
|
5765
|
-
description: "Base ERD viewer HTML file",
|
|
5766
|
-
completion: {
|
|
5767
|
-
type: "file",
|
|
5768
|
-
matcher: [".html"]
|
|
5769
|
-
}
|
|
5770
|
-
}),
|
|
5771
|
-
"head-html": arg(z.string().optional(), {
|
|
5772
|
-
description: "Head ERD viewer HTML file",
|
|
5773
|
-
completion: {
|
|
5774
|
-
type: "file",
|
|
5775
|
-
matcher: [".html"]
|
|
5776
|
-
}
|
|
5777
|
-
}),
|
|
5778
|
-
namespace: arg(z.string().optional(), {
|
|
5779
|
-
alias: "n",
|
|
5780
|
-
description: "TailorDB namespace name (defaults to the provided ERD schema namespace)"
|
|
5781
|
-
}),
|
|
5782
|
-
output: arg(z.string().min(1), {
|
|
5783
|
-
alias: "o",
|
|
5784
|
-
description: "Output ERD diff HTML file",
|
|
5785
|
-
completion: {
|
|
5786
|
-
type: "file",
|
|
5787
|
-
matcher: [".html"]
|
|
5788
|
-
}
|
|
5789
|
-
}),
|
|
5790
|
-
"output-json": arg(z.string().optional(), {
|
|
5791
|
-
description: "Optional output JSON file for the computed diff",
|
|
5792
|
-
completion: {
|
|
5793
|
-
type: "file",
|
|
5794
|
-
matcher: [".json"]
|
|
5795
|
-
}
|
|
5796
|
-
})
|
|
5797
|
-
}),
|
|
5798
|
-
run: (args) => {
|
|
5799
|
-
initErdCommand();
|
|
5800
|
-
const result = writeErdDiff({
|
|
5801
|
-
baseHtml: args["base-html"],
|
|
5802
|
-
headHtml: args["head-html"],
|
|
5803
|
-
namespace: args.namespace,
|
|
5804
|
-
outputHtml: args.output,
|
|
5805
|
-
outputJson: args["output-json"]
|
|
5806
|
-
});
|
|
5807
|
-
if (args.json) logger.out({
|
|
5808
|
-
namespace: result.namespace,
|
|
5809
|
-
outputHtml: result.outputHtml,
|
|
5810
|
-
outputJson: result.outputJson,
|
|
5811
|
-
summary: result.diff.summary
|
|
5812
|
-
});
|
|
5813
|
-
else logger.success(`Wrote ERD diff to ${path.relative(process.cwd(), result.outputHtml)}`);
|
|
5814
|
-
}
|
|
5815
|
-
});
|
|
5816
|
-
|
|
5817
|
-
//#endregion
|
|
5818
|
-
//#region src/cli/commands/tailordb/erd/serve.ts
|
|
5819
|
-
const DEFAULT_ERD_BASE_DIR = ".tailor/erd";
|
|
5820
|
-
const LOCAL_HOST = "127.0.0.1";
|
|
5821
|
-
const GLOB_CHARS = /[*?[\]{}()!+@]/;
|
|
5822
|
-
function formatServeCommand(namespace) {
|
|
5823
|
-
return `tailor tailordb erd serve --namespace ${namespace}`;
|
|
5824
|
-
}
|
|
5825
|
-
function getCacheControl(filePath) {
|
|
5826
|
-
return filePath.endsWith(".html") || filePath.endsWith(".json") ? "no-cache" : "public, max-age=3600";
|
|
5827
|
-
}
|
|
5828
|
-
function resolveRequestPath(distDir, requestUrl) {
|
|
5829
|
-
const url = new URL(requestUrl ?? "/", "http://localhost");
|
|
5830
|
-
let pathname;
|
|
5831
|
-
try {
|
|
5832
|
-
pathname = decodeURIComponent(url.pathname);
|
|
5833
|
-
} catch {
|
|
5834
|
-
return;
|
|
5835
|
-
}
|
|
5836
|
-
if (pathname === "/" || pathname.endsWith("/")) pathname = path.join(pathname, "index.html");
|
|
5837
|
-
const root = path.resolve(distDir);
|
|
5838
|
-
const filePath = path.resolve(root, `.${pathname}`);
|
|
5839
|
-
if (filePath !== root && !filePath.startsWith(`${root}${path.sep}`)) return;
|
|
5840
|
-
return filePath;
|
|
5841
|
-
}
|
|
5842
|
-
function openStaticFile(filePath) {
|
|
5843
|
-
let fd;
|
|
5844
|
-
try {
|
|
5845
|
-
fd = fs$1.openSync(filePath, "r");
|
|
5846
|
-
if (!fs$1.fstatSync(fd).isFile()) {
|
|
5847
|
-
fs$1.closeSync(fd);
|
|
5848
|
-
return;
|
|
5849
|
-
}
|
|
5850
|
-
return {
|
|
5851
|
-
filePath,
|
|
5852
|
-
fd
|
|
5853
|
-
};
|
|
5854
|
-
} catch {
|
|
5855
|
-
if (fd !== void 0) fs$1.closeSync(fd);
|
|
5856
|
-
return;
|
|
5857
|
-
}
|
|
5858
|
-
}
|
|
5859
|
-
function serveFile(distDir, req, res) {
|
|
5860
|
-
const filePath = resolveRequestPath(distDir, req.url);
|
|
5861
|
-
if (!filePath) {
|
|
5862
|
-
res.writeHead(403);
|
|
5863
|
-
res.end("Forbidden");
|
|
5864
|
-
return;
|
|
5865
|
-
}
|
|
5866
|
-
const fallbackPath = path.join(distDir, "index.html");
|
|
5867
|
-
const target = openStaticFile(filePath) ?? openStaticFile(fallbackPath);
|
|
5868
|
-
if (!target) {
|
|
5869
|
-
res.writeHead(503, {
|
|
5870
|
-
"Content-Type": "text/plain; charset=utf-8",
|
|
5871
|
-
"Cache-Control": "no-cache",
|
|
5872
|
-
"Retry-After": "1"
|
|
5873
|
-
});
|
|
5874
|
-
res.end("ERD build is refreshing. Please retry.");
|
|
5875
|
-
return;
|
|
5876
|
-
}
|
|
5877
|
-
const mimeType = lookup(target.filePath) || "application/octet-stream";
|
|
5878
|
-
const stream = fs$1.createReadStream(target.filePath, {
|
|
5879
|
-
fd: target.fd,
|
|
5880
|
-
autoClose: true
|
|
5881
|
-
});
|
|
5882
|
-
stream.on("error", () => {
|
|
5883
|
-
if (!res.headersSent) {
|
|
5884
|
-
res.writeHead(503, {
|
|
5885
|
-
"Content-Type": "text/plain; charset=utf-8",
|
|
5886
|
-
"Cache-Control": "no-cache",
|
|
5887
|
-
"Retry-After": "1"
|
|
5888
|
-
});
|
|
5889
|
-
res.end("ERD build is refreshing. Please retry.");
|
|
5890
|
-
return;
|
|
5891
|
-
}
|
|
5892
|
-
res.destroy();
|
|
5893
|
-
});
|
|
5894
|
-
res.writeHead(200, {
|
|
5895
|
-
"Content-Type": mimeType,
|
|
5896
|
-
"Cache-Control": getCacheControl(target.filePath)
|
|
5897
|
-
});
|
|
5898
|
-
stream.pipe(res);
|
|
5899
|
-
}
|
|
5900
|
-
async function startStaticServer(options) {
|
|
5901
|
-
const server = http.createServer((req, res) => {
|
|
5902
|
-
serveFile(options.distDir, req, res);
|
|
5903
|
-
});
|
|
5904
|
-
return await new Promise((resolve, reject) => {
|
|
5905
|
-
server.once("error", reject);
|
|
5906
|
-
server.listen(options.port, LOCAL_HOST, () => {
|
|
5907
|
-
server.off("error", reject);
|
|
5908
|
-
const address = server.address();
|
|
5909
|
-
if (!address || typeof address === "string") {
|
|
5910
|
-
reject(/* @__PURE__ */ new Error("Failed to determine ERD server address."));
|
|
5911
|
-
return;
|
|
5912
|
-
}
|
|
5913
|
-
resolve({
|
|
5914
|
-
server,
|
|
5915
|
-
url: `http://${LOCAL_HOST}:${address.port}`
|
|
5916
|
-
});
|
|
5917
|
-
});
|
|
5918
|
-
});
|
|
5919
|
-
}
|
|
5920
|
-
function getWatchPatterns(config, results) {
|
|
5921
|
-
const namespaces = new Set(results.map((result) => result.namespace));
|
|
5922
|
-
const patterns = [config.path];
|
|
5923
|
-
for (const namespace of namespaces) {
|
|
5924
|
-
const dbConfig = config.db?.[namespace];
|
|
5925
|
-
if (dbConfig && !("external" in dbConfig)) patterns.push(...dbConfig.files);
|
|
5926
|
-
}
|
|
5927
|
-
return [...new Set(patterns)];
|
|
5928
|
-
}
|
|
5929
|
-
function hasGlobPattern(pattern) {
|
|
5930
|
-
return GLOB_CHARS.test(pattern);
|
|
5931
|
-
}
|
|
5932
|
-
function globBaseDir(pattern) {
|
|
5933
|
-
const absolutePattern = path.resolve(pattern);
|
|
5934
|
-
const parsed = path.parse(absolutePattern);
|
|
5935
|
-
const relativePattern = absolutePattern.slice(parsed.root.length);
|
|
5936
|
-
const literalParts = [];
|
|
5937
|
-
for (const part of relativePattern.split(path.sep)) {
|
|
5938
|
-
if (!part || GLOB_CHARS.test(part)) break;
|
|
5939
|
-
literalParts.push(part);
|
|
5940
|
-
}
|
|
5941
|
-
const literalPath = literalParts.length > 0 ? path.join(parsed.root, ...literalParts) : parsed.root;
|
|
5942
|
-
if (!literalPath || literalPath === parsed.root) return parsed.root || process.cwd();
|
|
5943
|
-
if (!fs$1.existsSync(literalPath)) return path.dirname(literalPath);
|
|
5944
|
-
return fs$1.statSync(literalPath).isDirectory() ? literalPath : path.dirname(literalPath);
|
|
5945
|
-
}
|
|
5946
|
-
async function expandWatchPattern(pattern) {
|
|
5947
|
-
if (!hasGlobPattern(pattern)) return [path.resolve(pattern)];
|
|
5948
|
-
const paths = /* @__PURE__ */ new Set();
|
|
5949
|
-
for await (const file of glob(pattern)) paths.add(path.resolve(file));
|
|
5950
|
-
const baseDir = globBaseDir(pattern);
|
|
5951
|
-
if (fs$1.existsSync(baseDir) && fs$1.statSync(baseDir).isDirectory()) paths.add(baseDir);
|
|
5952
|
-
return [...paths];
|
|
5953
|
-
}
|
|
5954
|
-
async function resolveWatchPathsFromConfig(config, results) {
|
|
5955
|
-
const paths = /* @__PURE__ */ new Set();
|
|
5956
|
-
for (const pattern of getWatchPatterns(config, results)) for (const watchPath of await expandWatchPattern(pattern)) paths.add(watchPath);
|
|
5957
|
-
return [...paths];
|
|
5958
|
-
}
|
|
5959
|
-
async function resolveWatchPaths(context, results) {
|
|
5960
|
-
return await resolveWatchPathsFromConfig(context.config, results);
|
|
5961
|
-
}
|
|
5962
|
-
function parseFreshErdExportResults(stdout) {
|
|
5963
|
-
const lines = stdout.trim().split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
5964
|
-
for (const line of lines.toReversed()) try {
|
|
5965
|
-
const parsed = JSON.parse(line);
|
|
5966
|
-
if (!Array.isArray(parsed)) continue;
|
|
5967
|
-
return parsed.map((entry) => {
|
|
5968
|
-
const result = entry;
|
|
5969
|
-
if (typeof result.namespace !== "string" || typeof result.distDir !== "string") throw new Error("Invalid ERD export JSON output.");
|
|
5970
|
-
return {
|
|
5971
|
-
namespace: result.namespace,
|
|
5972
|
-
distDir: result.distDir
|
|
5973
|
-
};
|
|
5974
|
-
});
|
|
5975
|
-
} catch {
|
|
5976
|
-
continue;
|
|
5977
|
-
}
|
|
5978
|
-
throw new Error("Failed to parse ERD export JSON output.");
|
|
5979
|
-
}
|
|
5980
|
-
function freshErdExportArgs(options) {
|
|
5981
|
-
const cliEntry = process.argv[1];
|
|
5982
|
-
if (!cliEntry) throw new Error("Cannot rebuild ERD schema in a fresh process: CLI entrypoint is unavailable.");
|
|
5983
|
-
const args = [
|
|
5984
|
-
cliEntry,
|
|
5985
|
-
"tailordb",
|
|
5986
|
-
"erd",
|
|
5987
|
-
"export",
|
|
5988
|
-
"--output",
|
|
5989
|
-
options.outputDir,
|
|
5990
|
-
"--json"
|
|
5991
|
-
];
|
|
5992
|
-
if (options.configPath) args.push("--config", options.configPath);
|
|
5993
|
-
if (options.namespace) args.push("--namespace", options.namespace);
|
|
5994
|
-
return args;
|
|
5995
|
-
}
|
|
5996
|
-
async function runFreshErdExport(options) {
|
|
5997
|
-
return await new Promise((resolve, reject) => {
|
|
5998
|
-
const child = spawn(process.execPath, freshErdExportArgs(options), {
|
|
5999
|
-
cwd: process.cwd(),
|
|
6000
|
-
env: process.env,
|
|
6001
|
-
stdio: [
|
|
6002
|
-
"ignore",
|
|
6003
|
-
"pipe",
|
|
6004
|
-
"pipe"
|
|
6005
|
-
]
|
|
6006
|
-
});
|
|
6007
|
-
let stdout = "";
|
|
6008
|
-
let stderr = "";
|
|
6009
|
-
child.stdout.setEncoding("utf8");
|
|
6010
|
-
child.stderr.setEncoding("utf8");
|
|
6011
|
-
child.stdout.on("data", (chunk) => {
|
|
6012
|
-
stdout += chunk;
|
|
6013
|
-
});
|
|
6014
|
-
child.stderr.on("data", (chunk) => {
|
|
6015
|
-
stderr += chunk;
|
|
6016
|
-
});
|
|
6017
|
-
child.on("error", reject);
|
|
6018
|
-
child.on("close", (code, signal) => {
|
|
6019
|
-
if (code !== 0) {
|
|
6020
|
-
const detail = stderr.trim() || signal || `exit code ${code}`;
|
|
6021
|
-
reject(/* @__PURE__ */ new Error(`Fresh ERD export failed: ${detail}`));
|
|
6022
|
-
return;
|
|
6023
|
-
}
|
|
6024
|
-
try {
|
|
6025
|
-
resolve(parseFreshErdExportResults(stdout));
|
|
6026
|
-
} catch (error) {
|
|
6027
|
-
reject(error);
|
|
6028
|
-
}
|
|
6029
|
-
});
|
|
6030
|
-
});
|
|
6031
|
-
}
|
|
6032
|
-
function selectPrimaryResult(results) {
|
|
6033
|
-
const [primary, ...rest] = results;
|
|
6034
|
-
if (!primary) throw new Error("No ERD build results found.");
|
|
6035
|
-
logger.info(`Serving ERD for namespace "${primary.namespace}".`);
|
|
6036
|
-
if (rest.length > 0) {
|
|
6037
|
-
const commands = rest.map((result) => ` - ${formatServeCommand(result.namespace)}`).join("\n");
|
|
6038
|
-
logger.warn(`Multiple namespaces found. To serve another namespace, run:\n${commands}`);
|
|
6039
|
-
}
|
|
6040
|
-
return primary;
|
|
6041
|
-
}
|
|
6042
|
-
async function createErdWatcher(options) {
|
|
6043
|
-
let rebuilding = false;
|
|
6044
|
-
let pending = false;
|
|
6045
|
-
let watchPaths = await resolveWatchPaths(options.initialContext, options.initialResults);
|
|
6046
|
-
let importNonce = 0;
|
|
6047
|
-
const watcher = watch(watchPaths, {
|
|
6048
|
-
ignored: /node_modules/,
|
|
6049
|
-
ignoreInitial: true,
|
|
6050
|
-
awaitWriteFinish: {
|
|
6051
|
-
stabilityThreshold: 100,
|
|
6052
|
-
pollInterval: 100
|
|
6053
|
-
}
|
|
6054
|
-
});
|
|
6055
|
-
async function rebuild() {
|
|
6056
|
-
if (rebuilding) {
|
|
6057
|
-
pending = true;
|
|
6058
|
-
return;
|
|
6059
|
-
}
|
|
6060
|
-
rebuilding = true;
|
|
6061
|
-
try {
|
|
6062
|
-
const results = await runFreshErdExport({
|
|
6063
|
-
configPath: options.configPath,
|
|
6064
|
-
namespace: options.namespace,
|
|
6065
|
-
outputDir: options.outputDir
|
|
6066
|
-
});
|
|
6067
|
-
const { config } = await loadConfig(options.configPath, { importNonce: String(importNonce += 1) });
|
|
6068
|
-
const nextWatchPaths = await resolveWatchPathsFromConfig(config, results);
|
|
6069
|
-
watcher.unwatch(watchPaths);
|
|
6070
|
-
watcher.add(nextWatchPaths);
|
|
6071
|
-
watchPaths = nextWatchPaths;
|
|
6072
|
-
logger.success(`Rebuilt ERD schema (${results.map((result) => result.namespace).join(", ")})`, { mode: "stream" });
|
|
6073
|
-
} catch (error) {
|
|
6074
|
-
logger.error("Failed to rebuild ERD schema. Serving the last successful build.", { mode: "stream" });
|
|
6075
|
-
logger.error(String(error));
|
|
6076
|
-
} finally {
|
|
6077
|
-
rebuilding = false;
|
|
6078
|
-
if (pending) {
|
|
6079
|
-
pending = false;
|
|
6080
|
-
await rebuild();
|
|
6081
|
-
}
|
|
6082
|
-
}
|
|
6083
|
-
}
|
|
6084
|
-
let debounceTimer;
|
|
6085
|
-
const scheduleRebuild = (changedPath) => {
|
|
6086
|
-
logger.info(`Schema source changed: ${path.relative(process.cwd(), changedPath)}`, { mode: "stream" });
|
|
6087
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
6088
|
-
debounceTimer = setTimeout(() => {
|
|
6089
|
-
rebuild();
|
|
6090
|
-
}, 150);
|
|
6091
|
-
};
|
|
6092
|
-
watcher.on("add", scheduleRebuild);
|
|
6093
|
-
watcher.on("change", scheduleRebuild);
|
|
6094
|
-
watcher.on("unlink", scheduleRebuild);
|
|
6095
|
-
watcher.on("error", (error) => {
|
|
6096
|
-
logger.error(`ERD watcher error: ${String(error)}`, { mode: "stream" });
|
|
6097
|
-
});
|
|
6098
|
-
return watcher;
|
|
6099
|
-
}
|
|
6100
|
-
async function waitForShutdown(server, watcher) {
|
|
6101
|
-
return await new Promise((resolve) => {
|
|
6102
|
-
const shutdown = () => {
|
|
6103
|
-
watcher.close().finally(() => {
|
|
6104
|
-
server.close(() => {
|
|
6105
|
-
logger.info("ERD server stopped.");
|
|
6106
|
-
resolve();
|
|
6107
|
-
});
|
|
6108
|
-
});
|
|
6109
|
-
};
|
|
6110
|
-
process.once("SIGINT", shutdown);
|
|
6111
|
-
process.once("SIGTERM", shutdown);
|
|
6112
|
-
});
|
|
6113
|
-
}
|
|
6114
|
-
const erdServeCommand = defineAppCommand({
|
|
6115
|
-
name: "serve",
|
|
6116
|
-
description: "Generate and serve TailorDB ERD locally with watch reload. (beta)",
|
|
6117
|
-
args: z.strictObject({
|
|
6118
|
-
...configArg,
|
|
6119
|
-
namespace: arg(z.string().optional(), {
|
|
6120
|
-
alias: "n",
|
|
6121
|
-
description: "TailorDB namespace name (uses first namespace in config if not specified)"
|
|
6122
|
-
}),
|
|
6123
|
-
port: arg(z.coerce.number().int().min(0).max(65535).default(0), { description: "Local server port (0 selects a free port)" }),
|
|
6124
|
-
open: arg(z.boolean().default(false), { description: "Open the ERD viewer in the default browser" })
|
|
6125
|
-
}),
|
|
6126
|
-
run: async (args) => {
|
|
6127
|
-
initErdCommand();
|
|
6128
|
-
const outputDir = path.resolve(process.cwd(), DEFAULT_ERD_BASE_DIR);
|
|
6129
|
-
const context = await loadLocalErdSchema({
|
|
6130
|
-
configPath: args.config,
|
|
6131
|
-
namespaces: args.namespace ? [args.namespace] : void 0
|
|
6132
|
-
});
|
|
6133
|
-
const results = prepareErdBuildsFromContext({
|
|
6134
|
-
context,
|
|
6135
|
-
namespace: args.namespace,
|
|
6136
|
-
outputDir
|
|
6137
|
-
});
|
|
6138
|
-
const primary = selectPrimaryResult(results);
|
|
6139
|
-
const { server, url } = await startStaticServer({
|
|
6140
|
-
distDir: primary.distDir,
|
|
6141
|
-
port: args.port
|
|
6142
|
-
});
|
|
6143
|
-
const watchUrl = `${url}/?watch=1`;
|
|
6144
|
-
const watcher = await createErdWatcher({
|
|
6145
|
-
configPath: args.config,
|
|
6146
|
-
namespace: args.namespace,
|
|
6147
|
-
outputDir,
|
|
6148
|
-
initialContext: context,
|
|
6149
|
-
initialResults: results
|
|
6150
|
-
});
|
|
6151
|
-
logger.newline();
|
|
6152
|
-
if (args.json) logger.out({
|
|
6153
|
-
namespace: primary.namespace,
|
|
6154
|
-
url: watchUrl,
|
|
6155
|
-
distDir: primary.distDir
|
|
6156
|
-
});
|
|
6157
|
-
else {
|
|
6158
|
-
logger.success("ERD server started.");
|
|
6159
|
-
logger.out(watchUrl);
|
|
6160
|
-
}
|
|
6161
|
-
if (args.open) try {
|
|
6162
|
-
await open(watchUrl);
|
|
6163
|
-
} catch {
|
|
6164
|
-
logger.warn("Failed to open browser automatically. Please open the URL above manually.");
|
|
6165
|
-
}
|
|
6166
|
-
await waitForShutdown(server, watcher);
|
|
6167
|
-
}
|
|
6168
|
-
});
|
|
6169
|
-
|
|
6170
|
-
//#endregion
|
|
6171
|
-
//#region src/cli/commands/tailordb/erd/index.ts
|
|
6172
|
-
const erdCommand = defineCommand({
|
|
6173
|
-
name: "erd",
|
|
6174
|
-
description: "Generate TailorDB ERD viewer artifacts from local TailorDB schema. (beta)",
|
|
6175
|
-
subCommands: {
|
|
6176
|
-
export: erdExportCommand,
|
|
6177
|
-
diff: erdDiffCommand,
|
|
6178
|
-
serve: erdServeCommand,
|
|
6179
|
-
deploy: erdDeployCommand
|
|
6180
|
-
}
|
|
6181
|
-
});
|
|
6182
|
-
|
|
6183
5407
|
//#endregion
|
|
6184
5408
|
//#region src/cli/commands/tailordb/migrate/script.ts
|
|
6185
5409
|
/**
|
|
@@ -6204,9 +5428,9 @@ async function script(options) {
|
|
|
6204
5428
|
const targetNamespace = resolveTargetNamespace(namespacesWithMigrations, options.namespace);
|
|
6205
5429
|
const { migrationsDir } = assertDefined(namespacesWithMigrations.find((ns) => ns.namespace === targetNamespace), "namespace with migrations not found");
|
|
6206
5430
|
const diffPath = getMigrationFilePath(migrationsDir, migrationNumber, "diff");
|
|
6207
|
-
if (!fs
|
|
5431
|
+
if (!fs.existsSync(diffPath)) throw new Error(`Migration ${options.number} not found in ${migrationsDir}. Expected ${diffPath}.`);
|
|
6208
5432
|
const migratePath = getMigrationFilePath(migrationsDir, migrationNumber, "migrate");
|
|
6209
|
-
if (fs
|
|
5433
|
+
if (fs.existsSync(migratePath)) throw new Error(`Migration script already exists at ${migratePath}.`);
|
|
6210
5434
|
const diff = loadDiff(diffPath);
|
|
6211
5435
|
const previousSnapshot = reconstructSnapshotFromMigrations(migrationsDir, migrationNumber - 1);
|
|
6212
5436
|
if (!previousSnapshot) throw new Error(`Could not reconstruct previous schema for migration ${options.number}. Make sure migration ${0} exists.`);
|
|
@@ -6391,7 +5615,7 @@ async function collectMigrationStatuses(options) {
|
|
|
6391
5615
|
const pendingMigrations = getMigrationFiles(migrationsDir).map((f) => f.number).filter((n, i, arr) => arr.indexOf(n) === i).toSorted((a, b) => a - b).filter((n) => n > currentMigration).map((num) => {
|
|
6392
5616
|
const diffPath = getMigrationFilePath(migrationsDir, num, "diff");
|
|
6393
5617
|
let description;
|
|
6394
|
-
if (fs
|
|
5618
|
+
if (fs.existsSync(diffPath)) try {
|
|
6395
5619
|
description = loadDiff(diffPath).description;
|
|
6396
5620
|
} catch {}
|
|
6397
5621
|
return {
|
|
@@ -6506,7 +5730,7 @@ async function fetchRemoteTypes(client, workspaceId, namespace) {
|
|
|
6506
5730
|
async function assertMigrationsReproduceLocalTypes(loaded, target) {
|
|
6507
5731
|
const { config, plugins } = loaded;
|
|
6508
5732
|
const pluginManager = plugins.length > 0 ? new PluginManager(plugins) : void 0;
|
|
6509
|
-
const { defineApplication, generatePluginFilesIfNeeded } = await import("../application-
|
|
5733
|
+
const { defineApplication, generatePluginFilesIfNeeded } = await import("../application-BV-AXawv.mjs");
|
|
6510
5734
|
const application = defineApplication({
|
|
6511
5735
|
config,
|
|
6512
5736
|
pluginManager
|
|
@@ -6518,7 +5742,7 @@ async function assertMigrationsReproduceLocalTypes(loaded, target) {
|
|
|
6518
5742
|
await service.processNamespacePlugins();
|
|
6519
5743
|
}
|
|
6520
5744
|
const pluginExecutorFiles = generatePluginFilesIfNeeded(pluginManager, application.tailorDBServices, config.path);
|
|
6521
|
-
const executorService = application.executorService ?? (pluginExecutorFiles.length > 0 ? (await import("../service-
|
|
5745
|
+
const executorService = application.executorService ?? (pluginExecutorFiles.length > 0 ? (await import("../service-nU6ITOHL.mjs")).createExecutorService({ config: { files: [] } }) : void 0);
|
|
6522
5746
|
await executorService?.loadExecutors();
|
|
6523
5747
|
if (pluginExecutorFiles.length > 0) await executorService?.loadPluginExecutorFiles([...pluginExecutorFiles]);
|
|
6524
5748
|
const executorUsedTypes = /* @__PURE__ */ new Set();
|
|
@@ -6763,10 +5987,10 @@ const migrationCommand = defineCommand({
|
|
|
6763
5987
|
const tailordbCommand = defineCommand({
|
|
6764
5988
|
name: "tailordb",
|
|
6765
5989
|
description: "Manage TailorDB tables and data.",
|
|
5990
|
+
notes: "The `tailordb erd` commands are provided by the @tailor-platform/sdk-tailordb-erd-plugin CLI plugin.",
|
|
6766
5991
|
subCommands: {
|
|
6767
5992
|
truncate: truncateCommand,
|
|
6768
|
-
migration: migrationCommand
|
|
6769
|
-
erd: erdCommand
|
|
5993
|
+
migration: migrationCommand
|
|
6770
5994
|
}
|
|
6771
5995
|
});
|
|
6772
5996
|
|
|
@@ -6789,7 +6013,7 @@ const upgradeCommand = defineAppCommand({
|
|
|
6789
6013
|
run: async (args) => {
|
|
6790
6014
|
const { initTelemetry } = await import("../telemetry-CkbkeJxl.mjs");
|
|
6791
6015
|
await initTelemetry();
|
|
6792
|
-
const { upgrade } = await import("../service-
|
|
6016
|
+
const { upgrade } = await import("../service-DaeJlBoH.mjs");
|
|
6793
6017
|
await upgrade({
|
|
6794
6018
|
from: args.from,
|
|
6795
6019
|
dryRun: args["dry-run"],
|
|
@@ -7134,7 +6358,7 @@ const workflowCommand = defineCommand({
|
|
|
7134
6358
|
name: "workflow",
|
|
7135
6359
|
description: "Manage workflows and workflow executions.",
|
|
7136
6360
|
subCommands: {
|
|
7137
|
-
list: listCommand$
|
|
6361
|
+
list: listCommand$14,
|
|
7138
6362
|
get: getCommand$6,
|
|
7139
6363
|
start: startCommand,
|
|
7140
6364
|
wait: waitCommand,
|
|
@@ -7142,7 +6366,7 @@ const workflowCommand = defineCommand({
|
|
|
7142
6366
|
resume: resumeCommand
|
|
7143
6367
|
},
|
|
7144
6368
|
async run() {
|
|
7145
|
-
await runCommand(listCommand$
|
|
6369
|
+
await runCommand(listCommand$14, []);
|
|
7146
6370
|
}
|
|
7147
6371
|
});
|
|
7148
6372
|
|
|
@@ -7153,10 +6377,10 @@ const appCommand = defineCommand({
|
|
|
7153
6377
|
description: "Manage workspace applications",
|
|
7154
6378
|
subCommands: {
|
|
7155
6379
|
health: healthCommand,
|
|
7156
|
-
list: listCommand$
|
|
6380
|
+
list: listCommand$15
|
|
7157
6381
|
},
|
|
7158
6382
|
async run() {
|
|
7159
|
-
await runCommand(listCommand$
|
|
6383
|
+
await runCommand(listCommand$15, []);
|
|
7160
6384
|
}
|
|
7161
6385
|
});
|
|
7162
6386
|
|
|
@@ -7167,12 +6391,12 @@ const userCommand = defineCommand({
|
|
|
7167
6391
|
description: "Manage workspace users",
|
|
7168
6392
|
subCommands: {
|
|
7169
6393
|
invite: inviteCommand,
|
|
7170
|
-
list: listCommand$
|
|
6394
|
+
list: listCommand$16,
|
|
7171
6395
|
remove: removeCommand,
|
|
7172
6396
|
update: updateCommand$4
|
|
7173
6397
|
},
|
|
7174
6398
|
async run() {
|
|
7175
|
-
await runCommand(listCommand$
|
|
6399
|
+
await runCommand(listCommand$16, []);
|
|
7176
6400
|
}
|
|
7177
6401
|
});
|
|
7178
6402
|
|
|
@@ -7186,12 +6410,12 @@ const workspaceCommand = defineCommand({
|
|
|
7186
6410
|
create: createCommand$4,
|
|
7187
6411
|
delete: deleteCommand$5,
|
|
7188
6412
|
get: getCommand$7,
|
|
7189
|
-
list: listCommand$
|
|
6413
|
+
list: listCommand$17,
|
|
7190
6414
|
restore: restoreCommand,
|
|
7191
6415
|
user: userCommand
|
|
7192
6416
|
},
|
|
7193
6417
|
async run() {
|
|
7194
|
-
await runCommand(listCommand$
|
|
6418
|
+
await runCommand(listCommand$17, []);
|
|
7195
6419
|
}
|
|
7196
6420
|
});
|
|
7197
6421
|
|
|
@@ -7247,7 +6471,7 @@ Run \`${cliName} plugin list\` to see which plugins are installed and where they
|
|
|
7247
6471
|
auth: authCommand,
|
|
7248
6472
|
authconnection: authconnectionCommand,
|
|
7249
6473
|
crashreport: crashReportCommand,
|
|
7250
|
-
deploy: deployCommand
|
|
6474
|
+
deploy: deployCommand,
|
|
7251
6475
|
executor: executorCommand,
|
|
7252
6476
|
function: functionCommand,
|
|
7253
6477
|
generate: generateCommand,
|
|
@@ -7295,7 +6519,7 @@ runMain(mainCommand, {
|
|
|
7295
6519
|
version: packageJson.version,
|
|
7296
6520
|
globalArgs: z.object(commonArgs),
|
|
7297
6521
|
displayErrors: false,
|
|
7298
|
-
onUnknownSubcommand: ({ commandPath, name, args }) =>
|
|
6522
|
+
onUnknownSubcommand: ({ commandPath, name, args }) => dispatchPluginWithInstallHint({
|
|
7299
6523
|
commandPath,
|
|
7300
6524
|
name,
|
|
7301
6525
|
args,
|
|
@@ -7304,7 +6528,8 @@ runMain(mainCommand, {
|
|
|
7304
6528
|
}),
|
|
7305
6529
|
cleanup: async ({ error }) => {
|
|
7306
6530
|
if (error) {
|
|
7307
|
-
if (
|
|
6531
|
+
if (logger.jsonMode) logger.log(JSON.stringify(errorToJson(error, { includeStack: isVerbose() })));
|
|
6532
|
+
else if (isCLIError(error)) {
|
|
7308
6533
|
logger.log(error.format());
|
|
7309
6534
|
if (isVerbose() && error.stack) logger.debug(`\nStack trace:\n${error.stack}`);
|
|
7310
6535
|
} else if (error instanceof Error) {
|
|
@@ -7312,7 +6537,7 @@ runMain(mainCommand, {
|
|
|
7312
6537
|
if (isVerbose() && error.stack) logger.debug(`\nStack trace:\n${error.stack}`);
|
|
7313
6538
|
} else logger.error(`Unknown error: ${error}`);
|
|
7314
6539
|
if (!isCLIError(error) && (!(error instanceof Error) || error instanceof TypeError || error instanceof RangeError)) {
|
|
7315
|
-
const { reportCrash } = await import("../crashreport-
|
|
6540
|
+
const { reportCrash } = await import("../crashreport-0ioNRZ4K.mjs");
|
|
7316
6541
|
await reportCrash(error, "handledError");
|
|
7317
6542
|
}
|
|
7318
6543
|
}
|