@tailor-platform/sdk 1.60.3 → 1.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/dist/{application-pusdxz35.mjs → application-BezXGbrU.mjs} +73 -509
- package/dist/application-BezXGbrU.mjs.map +1 -0
- package/dist/application-DSXntqnV.mjs +4 -0
- package/dist/assert-CKfwrmCV.mjs +10 -0
- package/dist/assert-CKfwrmCV.mjs.map +1 -0
- package/dist/cli/index.mjs +819 -239
- package/dist/cli/index.mjs.map +1 -1
- package/dist/cli/lib.d.mts +3 -1
- package/dist/cli/lib.mjs +13 -6
- package/dist/cli/lib.mjs.map +1 -1
- package/dist/{client-B-jRdlC_.mjs → client-C68VWo4g.mjs} +1 -1
- package/dist/{client-W5P4NYYX.mjs → client-CobIRHl-.mjs} +207 -2
- package/dist/{client-W5P4NYYX.mjs.map → client-CobIRHl-.mjs.map} +1 -1
- package/dist/configure/index.mjs +2 -2
- package/dist/{crashreport-D3DvAzdg.mjs → crashreport-BhD0y14F.mjs} +2 -2
- package/dist/{crashreport-D3DvAzdg.mjs.map → crashreport-BhD0y14F.mjs.map} +1 -1
- package/dist/{crashreport-lnVTnbB5.mjs → crashreport-D1wKBJ8N.mjs} +1 -1
- package/dist/{mock-Dpu__UeJ.mjs → mock-DMgIygjE.mjs} +3 -2
- package/dist/mock-DMgIygjE.mjs.map +1 -0
- package/dist/plugin/builtin/seed/index.mjs +1 -1
- package/dist/plugin/index.mjs +1 -1
- package/dist/plugin/index.mjs.map +1 -1
- package/dist/registry-D0uB0OrK.mjs.map +1 -1
- package/dist/{repl-editor-Y9QJDL0K.mjs → repl-editor-CJG3sz7A.mjs} +11 -9
- package/dist/repl-editor-CJG3sz7A.mjs.map +1 -0
- package/dist/{runtime-C0_FZWdE.mjs → runtime-CW3jcQCc.mjs} +979 -584
- package/dist/runtime-CW3jcQCc.mjs.map +1 -0
- package/dist/{schema-DYKNTu-n.mjs → schema-1msIhXwA.mjs} +12 -7
- package/dist/schema-1msIhXwA.mjs.map +1 -0
- package/dist/{seed-C0fE2sJB.mjs → seed-BH2FbrPV.mjs} +4 -3
- package/dist/seed-BH2FbrPV.mjs.map +1 -0
- package/dist/service-BHQIerYh.mjs +4 -0
- package/dist/{service-aPT0fx3y.mjs → service-DMohAx8a2.mjs} +3 -3
- package/dist/service-DMohAx8a2.mjs.map +1 -0
- package/dist/service-wI3Hvrgx.mjs +460 -0
- package/dist/service-wI3Hvrgx.mjs.map +1 -0
- package/dist/{types-Ccwchyj5.mjs → types-2Be3wSMc.mjs} +1 -1
- package/dist/{types-BwGth3a1.mjs → types-CmzfQP_m.mjs} +3 -3
- package/dist/types-CmzfQP_m.mjs.map +1 -0
- package/dist/utils/test/index.mjs +1 -1
- package/dist/utils/test/index.mjs.map +1 -1
- package/dist/vitest/index.mjs +7 -4
- package/dist/vitest/index.mjs.map +1 -1
- package/dist/vitest/setup.mjs +1 -1
- package/docs/cli/application.md +11 -10
- package/docs/cli/setup.md +18 -12
- package/docs/cli/tailordb.md +54 -0
- package/docs/cli-reference.md +4 -3
- package/docs/github-actions.md +337 -0
- package/docs/services/tailordb-migration.md +17 -1
- package/package.json +4 -3
- package/dist/application-D4tRNn90.mjs +0 -4
- package/dist/application-pusdxz35.mjs.map +0 -1
- package/dist/mock-Dpu__UeJ.mjs.map +0 -1
- package/dist/repl-editor-Y9QJDL0K.mjs.map +0 -1
- package/dist/runtime-C0_FZWdE.mjs.map +0 -1
- package/dist/schema-DYKNTu-n.mjs.map +0 -1
- package/dist/seed-C0fE2sJB.mjs.map +0 -1
- package/dist/service-aPT0fx3y.mjs.map +0 -1
- package/dist/types-BwGth3a1.mjs.map +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Et as AuthInvokerSchema, L as CustomDomainStatus, Nt as PATScope, Q as FunctionExecution_Type, _ as userAgent, a as fetchAll, c as fetchPlatformMachineUserToken, d as initOAuth2Client, f as initOperatorClient, l as fetchUserInfo, r as closeConnectionPool, s as fetchPaged } from "../client-CobIRHl-.mjs";
|
|
4
|
+
import { t as assertDefined } from "../assert-CKfwrmCV.mjs";
|
|
4
5
|
import { n as logger, r as styles } from "../logger-DpJyJvNz.mjs";
|
|
5
|
-
import { $ as listCommand$10, An as
|
|
6
|
-
import {
|
|
6
|
+
import { $ as listCommand$10, $t as INITIAL_SCHEMA_NUMBER, An as commonArgs, At as startCommand, B as logBetaWarning, C as listCommand$13, Cn as PluginManager, Dt as jobsCommand, E as resumeCommand, En as apiCommand, F as writeDbTypesFile, Fn as pagedLogArgs, Gt as MIGRATION_LABEL_KEY, H as removeCommand$1, Ht as executeScript, I as getConfiguredEditorCommand, In as paginationArgs, Jt as compareSnapshotWithRemote, K as treeCommand, Kt as handleOptionalToRequiredError, L as openInConfiguredEditor, Ln as toPageDirection, Lt as functionExecutionStatusToString, Mn as confirmationArgs, Mt as getCommand$6, N as generateCommand$1, Nn as deploymentArgs, O as listCommand$12, On as assertWritable, P as generateMigrationScript, Pn as isVerbose, Pt as executionsCommand, Rn as workspaceArgs, Rt as formatKeyValueTable, Sn as sdkNameLabelKey, St as triggerCommand, T as healthCommand, Tn as prompt, U as updateCommand$3, Vt as deploy, Xt as protoGqlPermission, Y as getCommand$5, Yt as generateAllTypeManifestsFromSnapshot, Z as updateCommand$2, _n as formatMigrationDiff, an as createSnapshotFromLocalTypes, at as createCommand$3, b as createCommand$4, bn as ensureConfigId, c as listCommand$14, cn as getMigrationFilePath, dn as isValidMigrationNumber, f as restoreCommand, fn as loadDiff, ft as tokenCommand, g as getCommand$7, gt as listCommand$7, hn as parseMigrationNumberArg, ht as generate, i as updateCommand$4, j as truncateCommand, jn as configArg, kn as defineAppCommand, ln as getMigrationFiles, lt as getCommand$3, m as listCommand$15, mn as formatMigrationNumber, nn as assertValidMigrationFiles, o as removeCommand, on as getLatestMigrationNumber, pn as reconstructSnapshotFromMigrations, pt as listCommand$8, q as listCommand$11, qt as parseMigrationLabelNumber, r as queryCommand, rn as compareLocalTypesWithSnapshot, rt as deleteCommand$3, st as listCommand$9, t as isNativeTypeScriptRuntime, tt as getCommand$4, u as inviteCommand, v as deleteCommand$4, vn as hasChanges, vt as getCommand$2, wn as generateUserTypes, wt as listCommand$6, xn as resourceTrn, xt as webhookCommand, yn as getNamespacesWithMigrations, z as showCommand, zt as getCommand$1 } from "../runtime-CW3jcQCc.mjs";
|
|
7
|
+
import { A as resolveTokens, C as loadConfig, E as loadAccessToken, M as writePlatformConfig, O as loadWorkspaceId, T as fetchLatestToken, _ as createLogLevelTreeshakeOptions, a as WorkflowJobSchema, b as getDistDir, c as INVOKER_EXPR, g as composeFunctionTreeshakeOptions, h as platformBundleDefinePlugin, i as resolveInlineSourcemap, j as saveUserTokens, k as readPlatformConfig, o as ResolverSchema, t as defineApplication, v as resolveBundleLogLevel, w as deleteUserTokens, x as hashContent$1 } from "../application-BezXGbrU.mjs";
|
|
8
|
+
import { n as ExecutorSchema } from "../service-wI3Hvrgx.mjs";
|
|
7
9
|
import { t as multiline } from "../multiline-Cf9ODpr1.mjs";
|
|
8
|
-
import { r as isPluginGeneratedType } from "../seed-
|
|
10
|
+
import { r as isPluginGeneratedType } from "../seed-BH2FbrPV.mjs";
|
|
9
11
|
import { t as readPackageJson } from "../package-json-DcQApfPQ.mjs";
|
|
10
12
|
import { n as isCLIError } from "../errors-EsY4XO6O.mjs";
|
|
11
|
-
import { a as JSON_FOOTER_MARKER, i as CRASH_LOG_EXTENSION, o as parseCrashReportConfig, r as sendCrashReport, t as initCrashReporting } from "../crashreport-
|
|
13
|
+
import { a as JSON_FOOTER_MARKER, i as CRASH_LOG_EXTENSION, o as parseCrashReportConfig, r as sendCrashReport, t as initCrashReporting } from "../crashreport-BhD0y14F.mjs";
|
|
12
14
|
import { arg, defineCommand, runCommand, runMain } from "politty";
|
|
13
15
|
import { withCompletionCommand } from "politty/completion";
|
|
14
16
|
import { z } from "zod";
|
|
@@ -22,6 +24,7 @@ import { generateCodeVerifier } from "@badgateway/oauth2-client";
|
|
|
22
24
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
23
25
|
import { resolvePackageJSON, resolveTSConfig } from "pkg-types";
|
|
24
26
|
import * as crypto from "node:crypto";
|
|
27
|
+
import { createHash } from "node:crypto";
|
|
25
28
|
import * as http from "node:http";
|
|
26
29
|
import open from "open";
|
|
27
30
|
import * as rolldown from "rolldown";
|
|
@@ -74,10 +77,7 @@ const authorizeAuthConnectionCommand = defineAppCommand({
|
|
|
74
77
|
}).strict(),
|
|
75
78
|
run: async (args) => {
|
|
76
79
|
await assertWritable({ profile: args.profile });
|
|
77
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
78
|
-
useProfile: true,
|
|
79
|
-
profile: args.profile
|
|
80
|
-
}));
|
|
80
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
81
81
|
const workspaceId = await loadWorkspaceId({
|
|
82
82
|
workspaceId: args["workspace-id"],
|
|
83
83
|
profile: args.profile
|
|
@@ -178,10 +178,7 @@ const deleteAuthConnectionCommand = defineAppCommand({
|
|
|
178
178
|
}).strict(),
|
|
179
179
|
run: async (args) => {
|
|
180
180
|
await assertWritable({ profile: args.profile });
|
|
181
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
182
|
-
useProfile: true,
|
|
183
|
-
profile: args.profile
|
|
184
|
-
}));
|
|
181
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
185
182
|
const workspaceId = await loadWorkspaceId({
|
|
186
183
|
workspaceId: args["workspace-id"],
|
|
187
184
|
profile: args.profile
|
|
@@ -228,10 +225,7 @@ const listAuthConnectionCommand = defineAppCommand({
|
|
|
228
225
|
...paginationArgs()
|
|
229
226
|
}).strict(),
|
|
230
227
|
run: async (args) => {
|
|
231
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
232
|
-
useProfile: true,
|
|
233
|
-
profile: args.profile
|
|
234
|
-
}));
|
|
228
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
235
229
|
const workspaceId = await loadWorkspaceId({
|
|
236
230
|
workspaceId: args["workspace-id"],
|
|
237
231
|
profile: args.profile
|
|
@@ -308,10 +302,7 @@ const revokeAuthConnectionCommand = defineAppCommand({
|
|
|
308
302
|
}).strict(),
|
|
309
303
|
run: async (args) => {
|
|
310
304
|
await assertWritable({ profile: args.profile });
|
|
311
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
312
|
-
useProfile: true,
|
|
313
|
-
profile: args.profile
|
|
314
|
-
}));
|
|
305
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
315
306
|
const workspaceId = await loadWorkspaceId({
|
|
316
307
|
workspaceId: args["workspace-id"],
|
|
317
308
|
profile: args.profile
|
|
@@ -487,6 +478,7 @@ const deployCommand$1 = defineAppCommand({
|
|
|
487
478
|
description: "Run the command without making any changes"
|
|
488
479
|
}),
|
|
489
480
|
"no-schema-check": arg(z.boolean().optional(), { description: "Skip schema diff check against migration snapshots" }),
|
|
481
|
+
"no-validate": arg(z.boolean().optional(), { description: "Skip client-side validation against platform resource constraints" }),
|
|
490
482
|
"no-cache": arg(z.boolean().optional(), { description: "Disable bundle caching for this run" }),
|
|
491
483
|
"clean-cache": arg(z.boolean().optional(), { description: "Clean the bundle cache before building" })
|
|
492
484
|
}).strict(),
|
|
@@ -501,6 +493,7 @@ const deployCommand$1 = defineAppCommand({
|
|
|
501
493
|
dryRun: args["dry-run"],
|
|
502
494
|
yes: args.yes,
|
|
503
495
|
noSchemaCheck: args["no-schema-check"],
|
|
496
|
+
noValidate: args["no-validate"],
|
|
504
497
|
noCache: args["no-cache"],
|
|
505
498
|
cleanCache: args["clean-cache"]
|
|
506
499
|
});
|
|
@@ -644,9 +637,9 @@ function parseStackTrace(error) {
|
|
|
644
637
|
const match = STACK_FRAME_REGEX.exec(line);
|
|
645
638
|
if (match) frames.push({
|
|
646
639
|
functionName: match[1] || "<anonymous>",
|
|
647
|
-
file: match[2],
|
|
648
|
-
line: Number(match[3]),
|
|
649
|
-
column: Number(match[4])
|
|
640
|
+
file: assertDefined(match[2], "stack frame file missing"),
|
|
641
|
+
line: Number(assertDefined(match[3], "stack frame line missing")),
|
|
642
|
+
column: Number(assertDefined(match[4], "stack frame column missing"))
|
|
650
643
|
});
|
|
651
644
|
}
|
|
652
645
|
return {
|
|
@@ -664,7 +657,7 @@ function extractInlineSourcemap(bundledCode) {
|
|
|
664
657
|
const match = INLINE_SOURCEMAP_REGEX.exec(bundledCode);
|
|
665
658
|
if (!match) return null;
|
|
666
659
|
try {
|
|
667
|
-
const decoded = Buffer.from(match[1], "base64").toString("utf-8");
|
|
660
|
+
const decoded = Buffer.from(assertDefined(match[1], "sourcemap base64 data missing"), "base64").toString("utf-8");
|
|
668
661
|
return new TraceMap(JSON.parse(decoded));
|
|
669
662
|
} catch {
|
|
670
663
|
return null;
|
|
@@ -918,7 +911,7 @@ function composeExecutionErrorString(error) {
|
|
|
918
911
|
*/
|
|
919
912
|
function formatExecutionErrorFallback(error) {
|
|
920
913
|
const [headerLine, ...frameLines] = composeExecutionErrorString(error).split("\n");
|
|
921
|
-
return [` ${styles.error(headerLine
|
|
914
|
+
return [` ${styles.error(headerLine)}`, ...frameLines.map((line) => ` ${styles.dim(line)}`)].join("\n");
|
|
922
915
|
}
|
|
923
916
|
/**
|
|
924
917
|
* Format an execution error for display, applying sourcemap mapping
|
|
@@ -1061,10 +1054,7 @@ Stack traces stay accurate even after later redeploys, because the trace is reso
|
|
|
1061
1054
|
})
|
|
1062
1055
|
}).strict(),
|
|
1063
1056
|
run: async (args) => {
|
|
1064
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
1065
|
-
useProfile: true,
|
|
1066
|
-
profile: args.profile
|
|
1067
|
-
}));
|
|
1057
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
1068
1058
|
const workspaceId = await loadWorkspaceId({
|
|
1069
1059
|
workspaceId: args["workspace-id"],
|
|
1070
1060
|
profile: args.profile
|
|
@@ -1228,7 +1218,7 @@ function generateEntry(detected, sourceFile, env, machineUser, workspaceId) {
|
|
|
1228
1218
|
`;
|
|
1229
1219
|
}
|
|
1230
1220
|
case "workflow-job": {
|
|
1231
|
-
const exportName = detected.exportName;
|
|
1221
|
+
const exportName = assertDefined(detected.exportName, "workflow job export name missing");
|
|
1232
1222
|
return multiline`
|
|
1233
1223
|
import { ${exportName} } from "${absoluteSourcePath}";
|
|
1234
1224
|
|
|
@@ -1294,7 +1284,7 @@ async function detectFunctionType(options) {
|
|
|
1294
1284
|
const rawInput = module.default.input;
|
|
1295
1285
|
let inputSchema;
|
|
1296
1286
|
if (rawInput) {
|
|
1297
|
-
const { t } = await import("../types-
|
|
1287
|
+
const { t } = await import("../types-2Be3wSMc.mjs");
|
|
1298
1288
|
inputSchema = t.object(rawInput);
|
|
1299
1289
|
}
|
|
1300
1290
|
return {
|
|
@@ -1362,11 +1352,15 @@ function detectWorkflowJob(module, jobName) {
|
|
|
1362
1352
|
exportName: match.exportName
|
|
1363
1353
|
};
|
|
1364
1354
|
}
|
|
1365
|
-
if (jobs.length === 1)
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1355
|
+
if (jobs.length === 1) {
|
|
1356
|
+
const [jobEntry] = jobs;
|
|
1357
|
+
const job = assertDefined(jobEntry, "workflow job missing");
|
|
1358
|
+
return {
|
|
1359
|
+
type: "workflow-job",
|
|
1360
|
+
name: job.name,
|
|
1361
|
+
exportName: job.exportName
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1370
1364
|
const available = jobs.map((j) => ` - "${j.name}" (export: ${j.exportName})`).join("\n");
|
|
1371
1365
|
throw new Error(`Multiple workflow jobs found. Specify one with --name:\n${available}`);
|
|
1372
1366
|
}
|
|
@@ -1441,10 +1435,7 @@ When a \`.js\` file is provided, detection and bundling are skipped and the file
|
|
|
1441
1435
|
const { config } = await loadConfig(args.config);
|
|
1442
1436
|
const authNamespace = resolveAuthNamespace(config.auth);
|
|
1443
1437
|
const machineUserName = resolveMachineUserName(args["machine-user"], config.auth);
|
|
1444
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
1445
|
-
useProfile: true,
|
|
1446
|
-
profile: args.profile
|
|
1447
|
-
}));
|
|
1438
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
1448
1439
|
const workspaceId = await loadWorkspaceId({
|
|
1449
1440
|
workspaceId: args["workspace-id"],
|
|
1450
1441
|
profile: args.profile
|
|
@@ -1518,7 +1509,7 @@ When a \`.js\` file is provided, detection and bundling are skipped and the file
|
|
|
1518
1509
|
else {
|
|
1519
1510
|
if (result.success) logger.success("Execution succeeded");
|
|
1520
1511
|
else logger.error("Execution failed");
|
|
1521
|
-
if (result.logs
|
|
1512
|
+
if (result.logs.trim()) {
|
|
1522
1513
|
logger.log(styles.bold("\nLogs:"));
|
|
1523
1514
|
for (const line of result.logs.split("\n")) logger.log(` ${line}`);
|
|
1524
1515
|
}
|
|
@@ -1562,7 +1553,7 @@ function resolveMachineUserName(cliMachineUser, authConfig) {
|
|
|
1562
1553
|
const machineUsers = authConfig.machineUsers;
|
|
1563
1554
|
if (machineUsers) {
|
|
1564
1555
|
const keys = Object.keys(machineUsers);
|
|
1565
|
-
if (keys.length > 0) return keys[0];
|
|
1556
|
+
if (keys.length > 0) return assertDefined(keys[0], "machine user key missing");
|
|
1566
1557
|
}
|
|
1567
1558
|
}
|
|
1568
1559
|
throw new Error("Machine user is required. Provide --machine-user or ensure tailor.config.ts has machine users configured.");
|
|
@@ -1617,7 +1608,7 @@ function resolveResolverArg(argStr, inputSchema, machineUser, workspaceId) {
|
|
|
1617
1608
|
type: "machine_user",
|
|
1618
1609
|
workspaceId,
|
|
1619
1610
|
attributes: machineUser.attributes ?? null,
|
|
1620
|
-
attributeList: machineUser.attributeList
|
|
1611
|
+
attributeList: machineUser.attributeList
|
|
1621
1612
|
};
|
|
1622
1613
|
if (!inputSchema.parse({
|
|
1623
1614
|
value: parsed,
|
|
@@ -1751,7 +1742,7 @@ const startAuthServer = async () => {
|
|
|
1751
1742
|
await saveUserTokens(pfConfig, userInfo.email, {
|
|
1752
1743
|
accessToken: tokens.accessToken,
|
|
1753
1744
|
refreshToken: tokens.refreshToken ?? void 0
|
|
1754
|
-
}, new Date(tokens.expiresAt).toISOString());
|
|
1745
|
+
}, new Date(assertDefined(tokens.expiresAt, "token response missing expiresAt")).toISOString());
|
|
1755
1746
|
pfConfig.current_user = userInfo.email;
|
|
1756
1747
|
writePlatformConfig(pfConfig);
|
|
1757
1748
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -1797,7 +1788,7 @@ async function loginAsMachineUser(args) {
|
|
|
1797
1788
|
const clientSecret = args.clientSecret ?? await prompt.password({ message: "Client secret" });
|
|
1798
1789
|
const tokens = await fetchPlatformMachineUserToken(args.clientId, clientSecret);
|
|
1799
1790
|
const pfConfig = await readPlatformConfig();
|
|
1800
|
-
await saveUserTokens(pfConfig, args.clientId, { accessToken: tokens.accessToken }, new Date(tokens.expiresAt).toISOString());
|
|
1791
|
+
await saveUserTokens(pfConfig, args.clientId, { accessToken: tokens.accessToken }, new Date(assertDefined(tokens.expiresAt, "token response missing expiresAt")).toISOString());
|
|
1801
1792
|
pfConfig.current_user = args.clientId;
|
|
1802
1793
|
writePlatformConfig(pfConfig);
|
|
1803
1794
|
}
|
|
@@ -1821,7 +1812,7 @@ const loginCommand = defineAppCommand({
|
|
|
1821
1812
|
})
|
|
1822
1813
|
}).strict().describe("Machine User Login")]),
|
|
1823
1814
|
run: async (args) => {
|
|
1824
|
-
if ("machine-user" in args
|
|
1815
|
+
if ("machine-user" in args) await loginAsMachineUser({
|
|
1825
1816
|
clientId: args.clientId,
|
|
1826
1817
|
clientSecret: args.clientSecret
|
|
1827
1818
|
});
|
|
@@ -1839,13 +1830,14 @@ const logoutCommand = defineAppCommand({
|
|
|
1839
1830
|
args: z.object({}).strict(),
|
|
1840
1831
|
run: async () => {
|
|
1841
1832
|
const pfConfig = await readPlatformConfig();
|
|
1842
|
-
const
|
|
1843
|
-
|
|
1833
|
+
const currentUser = pfConfig.current_user;
|
|
1834
|
+
const userEntry = currentUser ? pfConfig.users[currentUser] : void 0;
|
|
1835
|
+
if (!userEntry || !currentUser) {
|
|
1844
1836
|
logger.info("You are not logged in.");
|
|
1845
1837
|
return;
|
|
1846
1838
|
}
|
|
1847
1839
|
try {
|
|
1848
|
-
const { accessToken, refreshToken } = await resolveTokens(userEntry,
|
|
1840
|
+
const { accessToken, refreshToken } = await resolveTokens(userEntry, currentUser);
|
|
1849
1841
|
const client = initOAuth2Client();
|
|
1850
1842
|
const tokenTypeHint = refreshToken ? "refresh_token" : "access_token";
|
|
1851
1843
|
await client.revoke({
|
|
@@ -1856,8 +1848,8 @@ const logoutCommand = defineAppCommand({
|
|
|
1856
1848
|
} catch (error) {
|
|
1857
1849
|
logger.warn(`Failed to revoke token: ${error instanceof Error ? error.message : error}`);
|
|
1858
1850
|
}
|
|
1859
|
-
await deleteUserTokens(pfConfig,
|
|
1860
|
-
delete pfConfig.users[
|
|
1851
|
+
await deleteUserTokens(pfConfig, currentUser);
|
|
1852
|
+
delete pfConfig.users[currentUser];
|
|
1861
1853
|
pfConfig.current_user = null;
|
|
1862
1854
|
writePlatformConfig(pfConfig);
|
|
1863
1855
|
logger.success("Successfully logged out from Tailor Platform.");
|
|
@@ -2048,12 +2040,15 @@ const listCommand$4 = defineAppCommand({
|
|
|
2048
2040
|
if (jsonOutput) logger.out([]);
|
|
2049
2041
|
return;
|
|
2050
2042
|
}
|
|
2051
|
-
const profileInfos = profiles.map(([name, profile]) =>
|
|
2052
|
-
name
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2043
|
+
const profileInfos = profiles.map(([name, profile]) => {
|
|
2044
|
+
const p = assertDefined(profile, `profile entry "${name}" is undefined`);
|
|
2045
|
+
return {
|
|
2046
|
+
name,
|
|
2047
|
+
user: p.user,
|
|
2048
|
+
workspaceId: p.workspace_id,
|
|
2049
|
+
permission: p.readonly === true ? "read" : "write"
|
|
2050
|
+
};
|
|
2051
|
+
});
|
|
2057
2052
|
logger.out(profileInfos);
|
|
2058
2053
|
}
|
|
2059
2054
|
});
|
|
@@ -2080,9 +2075,9 @@ const updateCommand$1 = defineAppCommand({
|
|
|
2080
2075
|
}).strict(),
|
|
2081
2076
|
run: async (args) => {
|
|
2082
2077
|
const config = await readPlatformConfig();
|
|
2083
|
-
if (!config.profiles[args.name]) throw new Error(`Profile "${args.name}" not found.`);
|
|
2084
|
-
if (!args.user && !args["workspace-id"] && args.permission === void 0) throw new Error("Please provide at least one property to update.");
|
|
2085
2078
|
const profile = config.profiles[args.name];
|
|
2079
|
+
if (!profile) throw new Error(`Profile "${args.name}" not found.`);
|
|
2080
|
+
if (!args.user && !args["workspace-id"] && args.permission === void 0) throw new Error("Please provide at least one property to update.");
|
|
2086
2081
|
const oldUser = profile.user;
|
|
2087
2082
|
const newUser = args.user || oldUser;
|
|
2088
2083
|
const oldWorkspaceId = profile.workspace_id;
|
|
@@ -2222,10 +2217,7 @@ const createSecretCommand = defineAppCommand({
|
|
|
2222
2217
|
}).strict(),
|
|
2223
2218
|
run: async (args) => {
|
|
2224
2219
|
await assertWritable({ profile: args.profile });
|
|
2225
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
2226
|
-
useProfile: true,
|
|
2227
|
-
profile: args.profile
|
|
2228
|
-
}));
|
|
2220
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
2229
2221
|
const workspaceId = await loadWorkspaceId({
|
|
2230
2222
|
workspaceId: args["workspace-id"],
|
|
2231
2223
|
profile: args.profile
|
|
@@ -2275,10 +2267,7 @@ const deleteSecretCommand = defineAppCommand({
|
|
|
2275
2267
|
}).strict(),
|
|
2276
2268
|
run: async (args) => {
|
|
2277
2269
|
await assertWritable({ profile: args.profile });
|
|
2278
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
2279
|
-
useProfile: true,
|
|
2280
|
-
profile: args.profile
|
|
2281
|
-
}));
|
|
2270
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
2282
2271
|
const workspaceId = await loadWorkspaceId({
|
|
2283
2272
|
workspaceId: args["workspace-id"],
|
|
2284
2273
|
profile: args.profile
|
|
@@ -2327,10 +2316,7 @@ function secretInfo(secret) {
|
|
|
2327
2316
|
* @returns List of secrets
|
|
2328
2317
|
*/
|
|
2329
2318
|
async function secretList(options) {
|
|
2330
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
2331
|
-
useProfile: true,
|
|
2332
|
-
profile: options.profile
|
|
2333
|
-
}));
|
|
2319
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
|
|
2334
2320
|
const workspaceId = await loadWorkspaceId({
|
|
2335
2321
|
workspaceId: options.workspaceId,
|
|
2336
2322
|
profile: options.profile
|
|
@@ -2384,10 +2370,7 @@ const updateSecretCommand = defineAppCommand({
|
|
|
2384
2370
|
}).strict(),
|
|
2385
2371
|
run: async (args) => {
|
|
2386
2372
|
await assertWritable({ profile: args.profile });
|
|
2387
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
2388
|
-
useProfile: true,
|
|
2389
|
-
profile: args.profile
|
|
2390
|
-
}));
|
|
2373
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
2391
2374
|
const workspaceId = await loadWorkspaceId({
|
|
2392
2375
|
workspaceId: args["workspace-id"],
|
|
2393
2376
|
profile: args.profile
|
|
@@ -2440,10 +2423,7 @@ const createCommand$1 = defineAppCommand({
|
|
|
2440
2423
|
}).strict(),
|
|
2441
2424
|
run: async (args) => {
|
|
2442
2425
|
await assertWritable({ profile: args.profile });
|
|
2443
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
2444
|
-
useProfile: true,
|
|
2445
|
-
profile: args.profile
|
|
2446
|
-
}));
|
|
2426
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
2447
2427
|
const workspaceId = await loadWorkspaceId({
|
|
2448
2428
|
workspaceId: args["workspace-id"],
|
|
2449
2429
|
profile: args.profile
|
|
@@ -2473,10 +2453,7 @@ const deleteCommand$1 = defineAppCommand({
|
|
|
2473
2453
|
}).strict(),
|
|
2474
2454
|
run: async (args) => {
|
|
2475
2455
|
await assertWritable({ profile: args.profile });
|
|
2476
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
2477
|
-
useProfile: true,
|
|
2478
|
-
profile: args.profile
|
|
2479
|
-
}));
|
|
2456
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
2480
2457
|
const workspaceId = await loadWorkspaceId({
|
|
2481
2458
|
workspaceId: args["workspace-id"],
|
|
2482
2459
|
profile: args.profile
|
|
@@ -2521,10 +2498,7 @@ function vaultInfo(vault) {
|
|
|
2521
2498
|
* @returns List of vaults
|
|
2522
2499
|
*/
|
|
2523
2500
|
async function vaultList(options) {
|
|
2524
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
2525
|
-
useProfile: true,
|
|
2526
|
-
profile: options?.profile
|
|
2527
|
-
}));
|
|
2501
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: options?.profile }));
|
|
2528
2502
|
const workspaceId = await loadWorkspaceId({
|
|
2529
2503
|
workspaceId: options?.workspaceId,
|
|
2530
2504
|
profile: options?.profile
|
|
@@ -2591,33 +2565,159 @@ const secretCommand = defineCommand({
|
|
|
2591
2565
|
});
|
|
2592
2566
|
|
|
2593
2567
|
//#endregion
|
|
2594
|
-
//#region src/cli/commands/setup/github/
|
|
2595
|
-
|
|
2568
|
+
//#region src/cli/commands/setup/github/git.ts
|
|
2569
|
+
const defaultGitRunner = (args, cwd) => {
|
|
2570
|
+
const result = spawnSync("git", args, {
|
|
2571
|
+
cwd,
|
|
2572
|
+
encoding: "utf-8"
|
|
2573
|
+
});
|
|
2574
|
+
if (result.status !== 0 || typeof result.stdout !== "string") return null;
|
|
2575
|
+
return result.stdout.trim();
|
|
2576
|
+
};
|
|
2577
|
+
/**
|
|
2578
|
+
* Detect the remote default branch by reading `refs/remotes/origin/HEAD`.
|
|
2579
|
+
*
|
|
2580
|
+
* Throws an AI-first error (with a remediation hint) when the symbolic ref is
|
|
2581
|
+
* not set, so the caller can surface a clear next step instead of a silent
|
|
2582
|
+
* fallback.
|
|
2583
|
+
* @param cwd - Repository directory to inspect
|
|
2584
|
+
* @param run - Git runner, injectable for testing
|
|
2585
|
+
* @returns The default branch name (e.g. `main`)
|
|
2586
|
+
*/
|
|
2587
|
+
function detectDefaultBranch(cwd, run = defaultGitRunner) {
|
|
2588
|
+
const ref = run([
|
|
2589
|
+
"symbolic-ref",
|
|
2590
|
+
"--short",
|
|
2591
|
+
"refs/remotes/origin/HEAD"
|
|
2592
|
+
], cwd);
|
|
2593
|
+
const branch = ref?.startsWith("origin/") ? ref.slice(7) : ref;
|
|
2594
|
+
if (!branch) throw new Error("Could not detect the default branch from git. Pass --branch <name>, or run 'git remote set-head origin --auto' to record it.");
|
|
2595
|
+
return branch;
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
//#endregion
|
|
2599
|
+
//#region src/cli/commands/setup/github/lock.ts
|
|
2600
|
+
/** Current lock schema version. Bumped only on breaking lock-format changes. */
|
|
2601
|
+
const LOCK_VERSION = 1;
|
|
2602
|
+
/** Lock file path, relative to the repository root. */
|
|
2603
|
+
const LOCK_FILENAME = ".github/tailor-sdk.lock";
|
|
2604
|
+
/**
|
|
2605
|
+
* Compute the lock content hash for a rendered workflow file.
|
|
2606
|
+
* @param content - File content to hash
|
|
2607
|
+
* @returns `sha256:<hex>` digest string
|
|
2608
|
+
*/
|
|
2609
|
+
function hashContent(content) {
|
|
2610
|
+
return `sha256:${createHash("sha256").update(content, "utf-8").digest("hex")}`;
|
|
2611
|
+
}
|
|
2612
|
+
/**
|
|
2613
|
+
* Resolve the absolute lock file path for an output directory.
|
|
2614
|
+
* @param outputDir - Repository root where `.github` lives
|
|
2615
|
+
* @returns Absolute path to the lock file
|
|
2616
|
+
*/
|
|
2617
|
+
function lockPath(outputDir) {
|
|
2618
|
+
return path.join(outputDir, LOCK_FILENAME);
|
|
2619
|
+
}
|
|
2620
|
+
/**
|
|
2621
|
+
* Read and validate the lock file from disk.
|
|
2622
|
+
*
|
|
2623
|
+
* Returns null when no lock exists. Throws when the lock was written by a
|
|
2624
|
+
* newer SDK (forward-compatibility guard).
|
|
2625
|
+
* @param outputDir - Repository root where `.github` lives
|
|
2626
|
+
* @returns Parsed lock file, or null when absent
|
|
2627
|
+
*/
|
|
2628
|
+
function readLock(outputDir) {
|
|
2629
|
+
const file = lockPath(outputDir);
|
|
2630
|
+
if (!fs$1.existsSync(file)) return null;
|
|
2631
|
+
let parsed;
|
|
2632
|
+
try {
|
|
2633
|
+
parsed = JSON.parse(fs$1.readFileSync(file, "utf-8"));
|
|
2634
|
+
} catch (cause) {
|
|
2635
|
+
throw new Error(`${LOCK_FILENAME} is not valid JSON. The lock file is machine-owned; restore it from git (git checkout -- .github/tailor-sdk.lock) and re-run setup.`, { cause });
|
|
2636
|
+
}
|
|
2637
|
+
if (typeof parsed.version !== "number") throw new Error(`${LOCK_FILENAME} has no valid 'version' field. The lock file is machine-owned; restore it from git (git checkout -- .github/tailor-sdk.lock) and re-run setup.`);
|
|
2638
|
+
if (parsed.version > 1) throw new Error(`${LOCK_FILENAME} was written by a newer SDK (lock version ${String(parsed.version)}). Update @tailor-platform/sdk to continue (e.g. pnpm update @tailor-platform/sdk).`);
|
|
2639
|
+
if (!Array.isArray(parsed.targets)) throw new Error(`${LOCK_FILENAME} has no valid 'targets' array. The lock file is machine-owned; restore it from git (git checkout -- .github/tailor-sdk.lock) and re-run setup.`);
|
|
2640
|
+
return parsed;
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Write the lock file to disk (2-space JSON, trailing newline).
|
|
2644
|
+
* @param outputDir - Repository root where `.github` lives
|
|
2645
|
+
* @param lock - Lock file contents to serialize
|
|
2646
|
+
*/
|
|
2647
|
+
function writeLock(outputDir, lock) {
|
|
2648
|
+
const file = lockPath(outputDir);
|
|
2649
|
+
fs$1.mkdirSync(path.dirname(file), { recursive: true });
|
|
2650
|
+
fs$1.writeFileSync(file, `${JSON.stringify(lock, null, 2)}\n`, "utf-8");
|
|
2651
|
+
}
|
|
2652
|
+
/**
|
|
2653
|
+
* Find a lock target by identity. Targets are identified by (kind,
|
|
2654
|
+
* workspaceName); the full trigger/path cross-check is deferred to P2.
|
|
2655
|
+
* @param lock - Lock file to search, or null
|
|
2656
|
+
* @param kind - Target kind
|
|
2657
|
+
* @param workspaceName - Workspace name
|
|
2658
|
+
* @returns Matching target, or undefined
|
|
2659
|
+
*/
|
|
2660
|
+
function findTarget(lock, kind, workspaceName) {
|
|
2661
|
+
return lock?.targets.find((t) => t.kind === kind && t.workspaceName === workspaceName);
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
//#endregion
|
|
2665
|
+
//#region src/cli/commands/setup/github/branch.workflow.yml
|
|
2666
|
+
var branch_workflow_default = "# __HEADER__\nname: Tailor (__WORKSPACE_NAME__)\n\non:\n # __PULL_REQUEST_START__\n pull_request:\n branches: [\"__BRANCH__\"]\n # __PATHS__\n # __PULL_REQUEST_END__\n push:\n branches: [\"__BRANCH__\"]\n # __PATHS__\n workflow_dispatch:\n # __DISPATCH_INPUTS_START__\n inputs:\n dry-run:\n description: Preview changes without deploying\n type: boolean\n default: false\n # __DISPATCH_INPUTS_END__\n\npermissions:\n contents: read\n\njobs:\n # __PLAN_JOB_START__\n tailor-plan:\n if: >-\n github.event_name == 'pull_request' ||\n (github.event_name == 'workflow_dispatch' && inputs['dry-run'])\n runs-on: ubuntu-latest\n timeout-minutes: 30\n environment: __ENVIRONMENT__\n permissions:\n contents: read\n pull-requests: write\n concurrency:\n group: tailor-plan-__WORKSPACE_NAME__-${{ github.event.pull_request.number || github.run_id }}\n cancel-in-progress: true\n steps:\n - id: tailor-checkout\n uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\n # __SETUP_STEPS__\n - id: tailor-generate\n # __WORKING_DIRECTORY__\n run: __PM_EXEC__ tailor-sdk generate\n - id: tailor-generate-check\n run: |\n git add -A\n if ! git diff --cached --quiet; then\n git --no-pager diff --cached --stat\n echo \"::error::Generated files are out of date. Run 'tailor-sdk generate' locally and commit the result.\"\n exit 1\n fi\n - id: tailor-plan\n # Fork PRs cannot read secrets; the checks above still run for them.\n if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork\n uses: tailor-platform/actions/plan@4d0f160b6b5cc2f02594776665471497c297181e # v1.2.0\n with:\n workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}\n label: __WORKSPACE_NAME__\n # __WORKING_DIRECTORY__\n platform-client-id: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }}\n platform-client-secret: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }}\n github-token: ${{ secrets.GITHUB_TOKEN }}\n # __PLAN_JOB_END__\n tailor-deploy:\n # __DEPLOY_IF__\n runs-on: ubuntu-latest\n timeout-minutes: 30\n permissions:\n contents: read\n environment: __ENVIRONMENT__\n concurrency:\n group: tailor-deploy-__WORKSPACE_NAME__\n cancel-in-progress: false\n steps:\n - id: tailor-checkout\n uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\n # __SETUP_STEPS__\n - id: tailor-apply\n uses: tailor-platform/actions/deploy@4d0f160b6b5cc2f02594776665471497c297181e # v1.2.0\n with:\n workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}\n # __WORKING_DIRECTORY__\n platform-client-id: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }}\n platform-client-secret: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }}\n";
|
|
2596
2667
|
|
|
2597
2668
|
//#endregion
|
|
2598
2669
|
//#region src/cli/commands/setup/github/setup-bun.yml
|
|
2599
|
-
var setup_bun_default = "- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0\n- run: bun install --frozen-lockfile\n";
|
|
2670
|
+
var setup_bun_default = "- id: tailor-setup-bun\n uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0\n- id: tailor-install\n run: bun install --frozen-lockfile\n";
|
|
2600
2671
|
|
|
2601
2672
|
//#endregion
|
|
2602
2673
|
//#region src/cli/commands/setup/github/setup-npm.yml
|
|
2603
|
-
var setup_npm_default = "- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\n with:\n node-version-file: package.json\n cache: npm\n- run: npm ci\n";
|
|
2674
|
+
var setup_npm_default = "- id: tailor-setup-node\n uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\n with:\n node-version-file: package.json\n cache: npm\n- id: tailor-install\n run: npm ci\n";
|
|
2604
2675
|
|
|
2605
2676
|
//#endregion
|
|
2606
2677
|
//#region src/cli/commands/setup/github/setup-pnpm.yml
|
|
2607
|
-
var setup_pnpm_default = "- uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8\n- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\n with:\n node-version-file: package.json\n cache: pnpm\n- run: pnpm install --frozen-lockfile\n";
|
|
2678
|
+
var setup_pnpm_default = "- id: tailor-setup-pnpm\n uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8\n- id: tailor-setup-node\n uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\n with:\n node-version-file: package.json\n cache: pnpm\n- id: tailor-install\n run: pnpm install --frozen-lockfile\n";
|
|
2608
2679
|
|
|
2609
2680
|
//#endregion
|
|
2610
2681
|
//#region src/cli/commands/setup/github/setup-yarn.yml
|
|
2611
|
-
var setup_yarn_default = "- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\n with:\n node-version-file: package.json\n cache: yarn\n- run: yarn install --frozen-lockfile\n";
|
|
2682
|
+
var setup_yarn_default = "- id: tailor-setup-node\n uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\n with:\n node-version-file: package.json\n cache: yarn\n- id: tailor-install\n run: yarn install --frozen-lockfile\n";
|
|
2612
2683
|
|
|
2613
2684
|
//#endregion
|
|
2614
|
-
//#region src/cli/commands/setup/github/
|
|
2685
|
+
//#region src/cli/commands/setup/github/tag.workflow.yml
|
|
2686
|
+
var tag_workflow_default = "# __HEADER__\nname: Tailor (__WORKSPACE_NAME__)\n\non:\n push:\n tags: [\"__TAG_PATTERN__\"]\n workflow_dispatch:\n inputs:\n dry-run:\n description: Preview changes without deploying\n type: boolean\n default: false\n\npermissions:\n contents: read\n\njobs:\n # __TAG_GUARD_JOB_START__\n tailor-tag-guard:\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: read\n outputs:\n on-branch: ${{ steps.tailor-tag-guard.outputs.on-branch }}\n steps:\n - id: tailor-checkout\n uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\n with:\n fetch-depth: 0\n - id: tailor-tag-guard\n env:\n TARGET_BRANCH: \"__BRANCH__\"\n run: |\n git fetch origin \"$TARGET_BRANCH\"\n if git merge-base --is-ancestor \"$GITHUB_SHA\" \"origin/$TARGET_BRANCH\"; then\n echo \"on-branch=true\" >> \"$GITHUB_OUTPUT\"\n else\n # A tag outside the target branch is not an error — just skip.\n echo \"on-branch=false\" >> \"$GITHUB_OUTPUT\"\n echo \"::notice::Tag $GITHUB_REF_NAME is not reachable from $TARGET_BRANCH; skipping deploy.\"\n fi\n # __TAG_GUARD_JOB_END__\n tailor-plan:\n # __PLAN_NEEDS__\n # __PLAN_IF__\n runs-on: ubuntu-latest\n timeout-minutes: 30\n environment: __ENVIRONMENT__\n permissions:\n contents: read\n steps:\n - id: tailor-checkout\n uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\n # __SETUP_STEPS__\n - id: tailor-generate\n # __WORKING_DIRECTORY__\n run: __PM_EXEC__ tailor-sdk generate\n - id: tailor-generate-check\n run: |\n git add -A\n if ! git diff --cached --quiet; then\n git --no-pager diff --cached --stat\n echo \"::error::Generated files are out of date. Run 'tailor-sdk generate' locally and commit the result.\"\n exit 1\n fi\n - id: tailor-plan\n uses: tailor-platform/actions/plan@4d0f160b6b5cc2f02594776665471497c297181e # v1.2.0\n with:\n workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}\n label: __WORKSPACE_NAME__\n # __WORKING_DIRECTORY__\n platform-client-id: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }}\n platform-client-secret: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }}\n\n tailor-deploy:\n needs: tailor-plan\n if: ${{ !(github.event_name == 'workflow_dispatch' && inputs['dry-run']) }}\n environment: __ENVIRONMENT__\n runs-on: ubuntu-latest\n timeout-minutes: 30\n permissions:\n contents: read\n concurrency:\n group: tailor-deploy-__WORKSPACE_NAME__\n cancel-in-progress: false\n steps:\n - id: tailor-checkout\n uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\n # __SETUP_STEPS__\n - id: tailor-apply\n uses: tailor-platform/actions/deploy@4d0f160b6b5cc2f02594776665471497c297181e # v1.2.0\n with:\n workspace-id: ${{ vars.TAILOR_PLATFORM_WORKSPACE_ID }}\n # __WORKING_DIRECTORY__\n platform-client-id: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID }}\n platform-client-secret: ${{ secrets.TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET }}\n";
|
|
2687
|
+
|
|
2688
|
+
//#endregion
|
|
2689
|
+
//#region src/cli/commands/setup/github/templates.ts
|
|
2690
|
+
const setupStepIds = {
|
|
2691
|
+
pnpm: [
|
|
2692
|
+
"tailor-setup-pnpm",
|
|
2693
|
+
"tailor-setup-node",
|
|
2694
|
+
"tailor-install"
|
|
2695
|
+
],
|
|
2696
|
+
yarn: ["tailor-setup-node", "tailor-install"],
|
|
2697
|
+
npm: ["tailor-setup-node", "tailor-install"],
|
|
2698
|
+
bun: ["tailor-setup-bun", "tailor-install"]
|
|
2699
|
+
};
|
|
2615
2700
|
const setupSteps = {
|
|
2616
2701
|
pnpm: setup_pnpm_default,
|
|
2617
2702
|
yarn: setup_yarn_default,
|
|
2618
2703
|
npm: setup_npm_default,
|
|
2619
2704
|
bun: setup_bun_default
|
|
2620
2705
|
};
|
|
2706
|
+
const execPrefix = {
|
|
2707
|
+
npm: "npx",
|
|
2708
|
+
pnpm: "pnpm exec",
|
|
2709
|
+
yarn: "yarn",
|
|
2710
|
+
bun: "bunx"
|
|
2711
|
+
};
|
|
2712
|
+
const HEADER = `# Generated by \`tailor-sdk setup github\` — managed by the Tailor SDK.
|
|
2713
|
+
#
|
|
2714
|
+
# - Jobs and steps whose id starts with \`tailor-\` are managed by the SDK.
|
|
2715
|
+
# Do not edit or rename them.
|
|
2716
|
+
# - State is tracked in .github/tailor-sdk.lock (machine-owned: commit it, never edit it).
|
|
2717
|
+
# - Re-running \`tailor-sdk setup github\` regenerates this file. If you have
|
|
2718
|
+
# edited it by hand, regeneration stops and asks for --force (which discards
|
|
2719
|
+
# your edits), so prefer keeping customizations in your own jobs/steps and
|
|
2720
|
+
# re-running setup after SDK updates.`;
|
|
2621
2721
|
function indentSnippet(snippet, spaces) {
|
|
2622
2722
|
const indent = " ".repeat(spaces);
|
|
2623
2723
|
return snippet.trimEnd().split("\n").map((line) => line ? indent + line : line).join("\n");
|
|
@@ -2634,139 +2734,352 @@ function detectPackageManager(dir) {
|
|
|
2634
2734
|
if (fs$1.existsSync(path.join(dir, "package-lock.json"))) return "npm";
|
|
2635
2735
|
return "npm";
|
|
2636
2736
|
}
|
|
2737
|
+
function block(content, name, keep) {
|
|
2738
|
+
if (keep) return content.replace(new RegExp(`^ *# __${name}_(?:START|END)__\\n`, "gm"), "");
|
|
2739
|
+
return content.replace(new RegExp(`^ *# __${name}_START__\\n[\\s\\S]*?^ *# __${name}_END__\\n`, "m"), "");
|
|
2740
|
+
}
|
|
2741
|
+
function line(content, name, replacement) {
|
|
2742
|
+
const re = new RegExp(`^([ \\t]*)# __${name}__\\n`, "gm");
|
|
2743
|
+
return content.replace(re, (_match, indent) => {
|
|
2744
|
+
if (replacement === void 0) return "";
|
|
2745
|
+
return replacement.split("\n").map((l) => l ? indent + l : l).join("\n") + "\n";
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
function applyCommon(content, params) {
|
|
2749
|
+
const { workingDirectory, environment, packageManager } = params;
|
|
2750
|
+
let out = line(content, "HEADER", HEADER);
|
|
2751
|
+
out = line(out, "WORKING_DIRECTORY", workingDirectory ? `working-directory: ${workingDirectory}` : void 0);
|
|
2752
|
+
out = out.replace(/^ *# __SETUP_STEPS__$/gm, () => indentSnippet(setupSteps[packageManager], 6));
|
|
2753
|
+
return out.replaceAll("__WORKSPACE_NAME__", () => params.workspaceName).replaceAll("__ENVIRONMENT__", () => environment).replaceAll("__PM_EXEC__", () => execPrefix[packageManager]);
|
|
2754
|
+
}
|
|
2755
|
+
function setupIds(job, packageManager) {
|
|
2756
|
+
return setupStepIds[packageManager].map((id) => `${job}/${id}`);
|
|
2757
|
+
}
|
|
2637
2758
|
/**
|
|
2638
|
-
* Render the deploy workflow
|
|
2759
|
+
* Render the branch-target deploy workflow.
|
|
2639
2760
|
*
|
|
2640
|
-
*
|
|
2641
|
-
*
|
|
2642
|
-
*
|
|
2643
|
-
*
|
|
2761
|
+
* When `plan` is false the plan job, the pull_request trigger, and the
|
|
2762
|
+
* workflow_dispatch dry-run input are all removed (a plan-less workflow has no
|
|
2763
|
+
* meaningful dry-run), and the deploy job runs unconditionally.
|
|
2764
|
+
* @param params - Workspace and rendering configuration
|
|
2765
|
+
* @returns Rendered YAML and the list of managed job/step ids
|
|
2766
|
+
*/
|
|
2767
|
+
function renderBranchWorkflow(params) {
|
|
2768
|
+
const { branch, plan, packageManager } = params;
|
|
2769
|
+
let out = branch_workflow_default;
|
|
2770
|
+
out = block(out, "PLAN_JOB", plan);
|
|
2771
|
+
out = block(out, "PULL_REQUEST", plan);
|
|
2772
|
+
out = block(out, "DISPATCH_INPUTS", plan);
|
|
2773
|
+
out = line(out, "DEPLOY_IF", plan ? `if: >-\n github.event_name == 'push' ||\n (github.event_name == 'workflow_dispatch' && !inputs['dry-run'])` : void 0);
|
|
2774
|
+
out = line(out, "PATHS", params.workingDirectory ? `paths: ["${params.workingDirectory}/**"]` : void 0);
|
|
2775
|
+
out = applyCommon(out, params).replaceAll("__BRANCH__", () => branch);
|
|
2776
|
+
const generatedIds = [];
|
|
2777
|
+
if (plan) generatedIds.push("tailor-plan", "tailor-plan/tailor-checkout", ...setupIds("tailor-plan", packageManager), "tailor-plan/tailor-generate", "tailor-plan/tailor-generate-check", "tailor-plan/tailor-plan");
|
|
2778
|
+
generatedIds.push("tailor-deploy", "tailor-deploy/tailor-checkout", ...setupIds("tailor-deploy", packageManager), "tailor-deploy/tailor-apply");
|
|
2779
|
+
return {
|
|
2780
|
+
content: out,
|
|
2781
|
+
generatedIds
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
/**
|
|
2785
|
+
* Render the tag-target deploy workflow.
|
|
2644
2786
|
*
|
|
2645
|
-
*
|
|
2646
|
-
*
|
|
2647
|
-
*
|
|
2648
|
-
* @
|
|
2649
|
-
* @returns Workflow YAML content
|
|
2787
|
+
* When `branch` is set, a tag-reachability guard job is generated and the plan
|
|
2788
|
+
* job gates on it; otherwise no guard is emitted and tags deploy unconditionally.
|
|
2789
|
+
* @param params - Workspace and rendering configuration
|
|
2790
|
+
* @returns Rendered YAML and the list of managed job/step ids
|
|
2650
2791
|
*/
|
|
2651
|
-
function
|
|
2652
|
-
const {
|
|
2653
|
-
const
|
|
2654
|
-
|
|
2655
|
-
|
|
2792
|
+
function renderTagWorkflow(params) {
|
|
2793
|
+
const { tagPattern, branch, packageManager } = params;
|
|
2794
|
+
const hasGuard = branch !== void 0;
|
|
2795
|
+
let out = tag_workflow_default;
|
|
2796
|
+
out = block(out, "TAG_GUARD_JOB", hasGuard);
|
|
2797
|
+
out = line(out, "PLAN_NEEDS", hasGuard ? "needs: tailor-tag-guard" : void 0);
|
|
2798
|
+
out = line(out, "PLAN_IF", hasGuard ? `if: >-\n github.event_name == 'workflow_dispatch' ||\n needs.tailor-tag-guard.outputs.on-branch == 'true'` : void 0);
|
|
2799
|
+
out = applyCommon(out, params).replaceAll("__TAG_PATTERN__", () => tagPattern);
|
|
2800
|
+
if (hasGuard) out = out.replaceAll("__BRANCH__", () => branch);
|
|
2801
|
+
const generatedIds = [];
|
|
2802
|
+
if (hasGuard) generatedIds.push("tailor-tag-guard", "tailor-tag-guard/tailor-checkout", "tailor-tag-guard/tailor-tag-guard");
|
|
2803
|
+
generatedIds.push("tailor-plan", "tailor-plan/tailor-checkout", ...setupIds("tailor-plan", packageManager), "tailor-plan/tailor-generate", "tailor-plan/tailor-generate-check", "tailor-plan/tailor-plan", "tailor-deploy", "tailor-deploy/tailor-checkout", ...setupIds("tailor-deploy", packageManager), "tailor-deploy/tailor-apply");
|
|
2804
|
+
return {
|
|
2805
|
+
content: out,
|
|
2806
|
+
generatedIds
|
|
2807
|
+
};
|
|
2656
2808
|
}
|
|
2657
2809
|
|
|
2658
2810
|
//#endregion
|
|
2659
2811
|
//#region src/cli/commands/setup/github/github.ts
|
|
2812
|
+
async function defaultLoadConfigName(configPath) {
|
|
2813
|
+
const { config } = await loadConfig(configPath);
|
|
2814
|
+
return config.name;
|
|
2815
|
+
}
|
|
2816
|
+
const WORKSPACE_NAME_RE = /^[a-z0-9-]+$/;
|
|
2817
|
+
function validateWorkspaceName(name) {
|
|
2818
|
+
if (name.length < 3 || name.length > 63 || !WORKSPACE_NAME_RE.test(name) || name.startsWith("-") || name.endsWith("-")) 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 --workspace-name.`);
|
|
2819
|
+
}
|
|
2820
|
+
const BRANCH_RE = /^[A-Za-z0-9._/-]+$/;
|
|
2821
|
+
const TAG_PATTERN_RE = /^[A-Za-z0-9._/*?![\]-]+$/;
|
|
2822
|
+
function validateBranch(branch) {
|
|
2823
|
+
if (!BRANCH_RE.test(branch)) throw new Error(`Invalid branch name "${branch}". Only letters, numbers, ".", "_", "/", and "-" are supported here.`);
|
|
2824
|
+
}
|
|
2825
|
+
function validateTagPattern(pattern) {
|
|
2826
|
+
if (!TAG_PATTERN_RE.test(pattern)) throw new Error(`Invalid tag pattern "${pattern}". Only letters, numbers, ".", "_", "/", "-", and the glob characters "*?![]" are supported.`);
|
|
2827
|
+
}
|
|
2828
|
+
const ENVIRONMENT_RE = /^[A-Za-z0-9._/-]+$/;
|
|
2829
|
+
function validateEnvironment(environment) {
|
|
2830
|
+
if (!ENVIRONMENT_RE.test(environment)) throw new Error(`Invalid environment name "${environment}". Only letters, numbers, ".", "_", "/", and "-" are supported.`);
|
|
2831
|
+
}
|
|
2832
|
+
const DIR_RE = /^[A-Za-z0-9._/-]+$/;
|
|
2833
|
+
function validateDir(dir) {
|
|
2834
|
+
if (!DIR_RE.test(dir)) throw new Error(`Invalid --dir "${dir}". Only letters, numbers, ".", "_", "/", and "-" are supported.`);
|
|
2835
|
+
}
|
|
2836
|
+
function escapesRoot(rel) {
|
|
2837
|
+
return rel === ".." || rel.startsWith(`..${path.sep}`) || rel.startsWith("../") || path.isAbsolute(rel);
|
|
2838
|
+
}
|
|
2660
2839
|
/**
|
|
2661
|
-
*
|
|
2662
|
-
*
|
|
2663
|
-
*
|
|
2840
|
+
* Resolve the config file path for the given app directory.
|
|
2841
|
+
*
|
|
2842
|
+
* `--dir` must stay inside the repository: the value is embedded in workflow
|
|
2843
|
+
* `paths:` filters and the config under it gets mutated (id injection), so
|
|
2844
|
+
* absolute paths and `..` traversal are rejected.
|
|
2845
|
+
* @param outputDir - Repository root (cwd)
|
|
2846
|
+
* @param dir - App directory relative to the repo root
|
|
2847
|
+
* @returns Absolute path to tailor.config.ts
|
|
2664
2848
|
*/
|
|
2665
|
-
function
|
|
2666
|
-
const
|
|
2849
|
+
function resolveConfigPath(outputDir, dir) {
|
|
2850
|
+
const appDir = path.resolve(outputDir, dir);
|
|
2851
|
+
const rel = path.relative(outputDir, appDir);
|
|
2852
|
+
if (path.isAbsolute(dir) || escapesRoot(rel)) throw new Error(`--dir must be a relative path inside the repository (got "${dir}").`);
|
|
2853
|
+
if (fs$1.existsSync(appDir)) {
|
|
2854
|
+
const realAppDir = path.normalize(fs$1.realpathSync(appDir));
|
|
2855
|
+
const realOutputDir = path.normalize(fs$1.realpathSync(outputDir));
|
|
2856
|
+
if (escapesRoot(path.relative(realOutputDir, realAppDir))) throw new Error(`--dir must resolve to a directory inside the repository (got "${dir}", which links outside it).`);
|
|
2857
|
+
}
|
|
2858
|
+
const configPath = path.join(appDir, "tailor.config.ts");
|
|
2859
|
+
if (!fs$1.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.`);
|
|
2860
|
+
return configPath;
|
|
2861
|
+
}
|
|
2862
|
+
/**
|
|
2863
|
+
* Resolve all derived values and render the workflow content.
|
|
2864
|
+
* @param options - Setup options
|
|
2865
|
+
* @returns Resolved target metadata and rendered content
|
|
2866
|
+
*/
|
|
2867
|
+
async function resolve$1(options) {
|
|
2868
|
+
if (options.tag && !options.plan) throw new Error("--no-plan cannot be combined with --tag (tag targets always run plan before deploy). Drop --no-plan or use a branch target.");
|
|
2869
|
+
const dir = options.dir.replaceAll("\\", "/").replace(/\/{2,}/g, "/").replace(/^\.\//, "").replace(/\/$/, "") || ".";
|
|
2870
|
+
validateDir(dir);
|
|
2871
|
+
const workingDirectory = dir !== "." ? dir : void 0;
|
|
2872
|
+
const configPath = resolveConfigPath(options.outputDir, dir);
|
|
2873
|
+
const loadName = options.loadConfigName ?? defaultLoadConfigName;
|
|
2874
|
+
const workspaceName = options.workspaceName ?? await loadName(configPath);
|
|
2875
|
+
if (!workspaceName) throw new Error("Could not determine the workspace name. Pass --workspace-name, or set 'name' in tailor.config.ts.");
|
|
2876
|
+
validateWorkspaceName(workspaceName);
|
|
2877
|
+
const kind = options.tag ? "tag" : "branch";
|
|
2667
2878
|
const packageManager = detectPackageManager(options.outputDir);
|
|
2668
|
-
const
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2879
|
+
const environment = options.environment ?? workspaceName;
|
|
2880
|
+
validateEnvironment(environment);
|
|
2881
|
+
if (kind === "tag") validateTagPattern(options.tagPattern);
|
|
2882
|
+
let branch = null;
|
|
2883
|
+
let render;
|
|
2884
|
+
if (kind === "branch") {
|
|
2885
|
+
branch = options.branch ?? detectDefaultBranch(options.outputDir, options.gitRunner);
|
|
2886
|
+
validateBranch(branch);
|
|
2887
|
+
render = renderBranchWorkflow({
|
|
2888
|
+
workspaceName,
|
|
2889
|
+
branch,
|
|
2676
2890
|
workingDirectory,
|
|
2891
|
+
environment,
|
|
2677
2892
|
packageManager,
|
|
2678
|
-
|
|
2679
|
-
})
|
|
2680
|
-
}
|
|
2893
|
+
plan: options.plan
|
|
2894
|
+
});
|
|
2895
|
+
} else {
|
|
2896
|
+
branch = options.branch ?? null;
|
|
2897
|
+
if (branch !== null) validateBranch(branch);
|
|
2898
|
+
render = renderTagWorkflow({
|
|
2899
|
+
workspaceName,
|
|
2900
|
+
tagPattern: options.tagPattern,
|
|
2901
|
+
branch: options.branch,
|
|
2902
|
+
workingDirectory,
|
|
2903
|
+
environment,
|
|
2904
|
+
packageManager
|
|
2905
|
+
});
|
|
2906
|
+
}
|
|
2907
|
+
const file = `.github/workflows/tailor-${workspaceName}.yml`;
|
|
2908
|
+
const inputs = {
|
|
2909
|
+
branch,
|
|
2910
|
+
tagPattern: kind === "tag" ? options.tagPattern : null,
|
|
2911
|
+
environment,
|
|
2912
|
+
dir,
|
|
2913
|
+
packageManager,
|
|
2914
|
+
plan: kind === "branch" ? options.plan : true
|
|
2915
|
+
};
|
|
2916
|
+
return {
|
|
2917
|
+
kind,
|
|
2918
|
+
workspaceName,
|
|
2919
|
+
branch,
|
|
2920
|
+
environment,
|
|
2921
|
+
packageManager,
|
|
2922
|
+
render,
|
|
2923
|
+
inputs,
|
|
2924
|
+
file,
|
|
2925
|
+
configPath
|
|
2926
|
+
};
|
|
2681
2927
|
}
|
|
2682
2928
|
/**
|
|
2683
|
-
*
|
|
2684
|
-
* @param
|
|
2685
|
-
* @
|
|
2929
|
+
* Decide how to reconcile a target with the on-disk file and lock state.
|
|
2930
|
+
* @param obj - Decision inputs
|
|
2931
|
+
* @param obj.existing - The matching lock target, if any
|
|
2932
|
+
* @param obj.fileExists - Whether the workflow file is present on disk
|
|
2933
|
+
* @param obj.currentContent - On-disk content when present
|
|
2934
|
+
* @param obj.force - Whether --force was passed
|
|
2935
|
+
* @returns The reconciliation action
|
|
2686
2936
|
*/
|
|
2687
|
-
function
|
|
2688
|
-
const
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
if (
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
fs$1.writeFileSync(file.path, file.content);
|
|
2697
|
-
written.push(file.path);
|
|
2937
|
+
function decideAction(obj) {
|
|
2938
|
+
const { existing, fileExists, currentContent, force } = obj;
|
|
2939
|
+
if (!existing) {
|
|
2940
|
+
if (!fileExists) return { action: "create" };
|
|
2941
|
+
if (force) return { action: "regenerate" };
|
|
2942
|
+
return {
|
|
2943
|
+
action: "conflict",
|
|
2944
|
+
reason: "An unmanaged workflow file already exists at this path. Delete it, or pass --force to bring it under SDK management (this overwrites it)."
|
|
2945
|
+
};
|
|
2698
2946
|
}
|
|
2947
|
+
if (!fileExists) return { action: "restore" };
|
|
2948
|
+
if (currentContent !== null && hashContent(currentContent) === existing.contentHash) return { action: "regenerate" };
|
|
2949
|
+
if (force) return { action: "regenerate" };
|
|
2699
2950
|
return {
|
|
2700
|
-
|
|
2701
|
-
|
|
2951
|
+
action: "conflict",
|
|
2952
|
+
reason: "This workflow file has been edited by hand since it was generated. Re-run with --force to discard those edits and regenerate, or revert your changes."
|
|
2702
2953
|
};
|
|
2703
2954
|
}
|
|
2704
2955
|
/**
|
|
2705
|
-
*
|
|
2706
|
-
* @param
|
|
2956
|
+
* Guard against two targets of different kinds colliding on the same file path.
|
|
2957
|
+
* @param obj - Conflict inputs
|
|
2958
|
+
* @param obj.lock - Existing lock file, or null
|
|
2959
|
+
* @param obj.kind - Target kind being generated
|
|
2960
|
+
* @param obj.workspaceName - Workspace name being generated
|
|
2961
|
+
* @param obj.file - Target file path
|
|
2707
2962
|
*/
|
|
2708
|
-
function
|
|
2709
|
-
|
|
2710
|
-
const
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2963
|
+
function assertNoKindCollision(obj) {
|
|
2964
|
+
const { lock, kind, workspaceName, file } = obj;
|
|
2965
|
+
const collision = lock?.targets.find((t) => t.file === file && !(t.kind === kind && t.workspaceName === workspaceName));
|
|
2966
|
+
if (collision) throw new Error(`A ${collision.kind} target already owns ${file}, which conflicts with this ${kind} target. Pass a different name with --workspace-name to generate a separate workflow.`);
|
|
2967
|
+
}
|
|
2968
|
+
/**
|
|
2969
|
+
* Print next-step guidance after generating workflow files.
|
|
2970
|
+
* @param obj - Output context
|
|
2971
|
+
* @param obj.environment - Resolved GitHub Environment name for this target
|
|
2972
|
+
* @param obj.idInjected - Whether an app id was injected into the config
|
|
2973
|
+
*/
|
|
2974
|
+
function printNextSteps(obj) {
|
|
2975
|
+
const { environment, idInjected } = obj;
|
|
2719
2976
|
logger.newline();
|
|
2720
|
-
logger.info("Next steps
|
|
2721
|
-
logger.
|
|
2722
|
-
logger.log(`
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2977
|
+
logger.info("Next steps:");
|
|
2978
|
+
logger.newline();
|
|
2979
|
+
logger.log(`1. Set the machine-user credentials as secrets on the "${environment}" environment:`);
|
|
2980
|
+
logger.log(` gh secret set TAILOR_PLATFORM_MACHINE_USER_CLIENT_ID --env ${environment}`);
|
|
2981
|
+
logger.log(` gh secret set TAILOR_PLATFORM_MACHINE_USER_CLIENT_SECRET --env ${environment}`);
|
|
2982
|
+
logger.newline();
|
|
2983
|
+
logger.log(`2. Provision the workspace and set its id as the TAILOR_PLATFORM_WORKSPACE_ID variable on the "${environment}" environment:`);
|
|
2984
|
+
logger.log(" tailor-sdk workspace create # if it does not exist yet; copy the id");
|
|
2985
|
+
logger.log(` gh variable set TAILOR_PLATFORM_WORKSPACE_ID --env ${environment}`);
|
|
2986
|
+
logger.newline();
|
|
2987
|
+
logger.log("3. Commit the generated files:");
|
|
2988
|
+
logger.log(" - .github/workflows/tailor-*.yml");
|
|
2989
|
+
logger.log(" - .github/tailor-sdk.lock");
|
|
2990
|
+
if (idInjected) logger.log(" - tailor.config.ts (app id was added)");
|
|
2991
|
+
}
|
|
2992
|
+
/**
|
|
2993
|
+
* Generate the GitHub Actions workflow for a deploy target and reconcile it
|
|
2994
|
+
* with the lock file.
|
|
2995
|
+
* @param options - Setup options
|
|
2996
|
+
*/
|
|
2997
|
+
async function setupGitHub(options) {
|
|
2998
|
+
logBetaWarning("setup github");
|
|
2999
|
+
const resolved = await resolve$1(options);
|
|
3000
|
+
const lock = readLock(options.outputDir);
|
|
3001
|
+
const absFile = path.join(options.outputDir, resolved.file);
|
|
3002
|
+
assertNoKindCollision({
|
|
3003
|
+
lock,
|
|
3004
|
+
kind: resolved.kind,
|
|
3005
|
+
workspaceName: resolved.workspaceName,
|
|
3006
|
+
file: resolved.file
|
|
3007
|
+
});
|
|
3008
|
+
const existing = findTarget(lock, resolved.kind, resolved.workspaceName);
|
|
3009
|
+
const fileExists = fs$1.existsSync(absFile);
|
|
3010
|
+
const decision = decideAction({
|
|
3011
|
+
existing,
|
|
3012
|
+
fileExists,
|
|
3013
|
+
currentContent: fileExists ? fs$1.readFileSync(absFile, "utf-8") : null,
|
|
3014
|
+
force: options.force
|
|
3015
|
+
});
|
|
3016
|
+
if (decision.action === "conflict") throw new Error(`${resolved.file}: ${decision.reason}`);
|
|
3017
|
+
const idResult = await ensureConfigId(resolved.configPath);
|
|
3018
|
+
if (idResult === null) logger.warn("Could not find a defineConfig() call to confirm an app id. The CI deploy will fail unless your config resolves to one with an 'id'.");
|
|
3019
|
+
const idInjected = idResult?.injected ?? false;
|
|
3020
|
+
fs$1.mkdirSync(path.dirname(absFile), { recursive: true });
|
|
3021
|
+
fs$1.writeFileSync(absFile, resolved.render.content, "utf-8");
|
|
3022
|
+
const newTarget = {
|
|
3023
|
+
kind: resolved.kind,
|
|
3024
|
+
workspaceName: resolved.workspaceName,
|
|
3025
|
+
file: resolved.file,
|
|
3026
|
+
templateVersion: 1,
|
|
3027
|
+
inputs: resolved.inputs,
|
|
3028
|
+
generatedIds: resolved.render.generatedIds,
|
|
3029
|
+
ejectedIds: existing?.ejectedIds ?? [],
|
|
3030
|
+
contentHash: hashContent(resolved.render.content)
|
|
3031
|
+
};
|
|
3032
|
+
const targets = [...lock?.targets ?? []];
|
|
3033
|
+
const index = targets.findIndex((t) => t.kind === newTarget.kind && t.workspaceName === newTarget.workspaceName);
|
|
3034
|
+
if (index === -1) targets.push(newTarget);
|
|
3035
|
+
else targets[index] = newTarget;
|
|
3036
|
+
writeLock(options.outputDir, {
|
|
3037
|
+
version: 1,
|
|
3038
|
+
targets
|
|
3039
|
+
});
|
|
3040
|
+
if (decision.action === "restore") logger.success(`Regenerated ${styles.path(resolved.file)} (was missing on disk)`);
|
|
3041
|
+
else if (decision.action === "regenerate") logger.success(`Regenerated ${styles.path(resolved.file)}`);
|
|
3042
|
+
else logger.success(`Generated ${styles.path(resolved.file)}`);
|
|
3043
|
+
printNextSteps({
|
|
3044
|
+
environment: resolved.environment,
|
|
3045
|
+
idInjected
|
|
3046
|
+
});
|
|
2728
3047
|
}
|
|
2729
3048
|
|
|
2730
3049
|
//#endregion
|
|
2731
3050
|
//#region src/cli/commands/setup/github/index.ts
|
|
2732
3051
|
const githubCommand = defineAppCommand({
|
|
2733
3052
|
name: "github",
|
|
2734
|
-
description: "Generate GitHub Actions workflow
|
|
3053
|
+
description: "Generate a GitHub Actions deploy workflow. (beta)",
|
|
2735
3054
|
args: z.object({
|
|
2736
|
-
"workspace-name": arg(z.string(), {
|
|
3055
|
+
"workspace-name": arg(z.string().min(1).optional(), {
|
|
2737
3056
|
alias: "n",
|
|
2738
|
-
description: "Workspace name"
|
|
2739
|
-
}),
|
|
2740
|
-
"workspace-region": arg(z.string(), {
|
|
2741
|
-
alias: "r",
|
|
2742
|
-
description: "Workspace region"
|
|
3057
|
+
description: "Workspace name (defaults to the config 'name')"
|
|
2743
3058
|
}),
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
}),
|
|
2748
|
-
"
|
|
2749
|
-
|
|
2750
|
-
description: "Folder ID"
|
|
2751
|
-
}),
|
|
2752
|
-
dir: arg(z.string().default("."), {
|
|
3059
|
+
branch: arg(z.string().min(1).optional(), { description: "Branch target: deploy trigger branch (defaults to the detected default branch). Tag target: tag-reachability guard branch (no guard when omitted)" }),
|
|
3060
|
+
tag: arg(z.boolean().default(false), { description: "Generate a tag target (deploy on tag push)" }),
|
|
3061
|
+
"tag-pattern": arg(z.string().min(1).optional(), { description: "Tag glob to match (requires --tag; defaults to v*)" }),
|
|
3062
|
+
environment: arg(z.string().min(1).optional(), { description: "GitHub Environment for the plan/deploy jobs (defaults to the workspace name)" }),
|
|
3063
|
+
"no-plan": arg(z.boolean().default(false), { description: "Disable the plan job for a branch target (cannot be combined with --tag)" }),
|
|
3064
|
+
dir: arg(z.string().min(1).default("."), {
|
|
2753
3065
|
alias: "d",
|
|
2754
3066
|
description: "App directory (for monorepo setups)"
|
|
2755
3067
|
}),
|
|
2756
|
-
|
|
2757
|
-
alias: "p",
|
|
2758
|
-
description: "Include plan job for PR previews"
|
|
2759
|
-
})
|
|
3068
|
+
force: arg(z.boolean().default(false), { description: "Discard hand edits / take over unmanaged files and regenerate" })
|
|
2760
3069
|
}).strict(),
|
|
2761
|
-
run: (args) => {
|
|
2762
|
-
|
|
3070
|
+
run: async (args) => {
|
|
3071
|
+
if (args["tag-pattern"] !== void 0 && !args.tag) throw new Error("--tag-pattern requires --tag.");
|
|
3072
|
+
if (args["no-plan"] && args.tag) throw new Error("--no-plan cannot be combined with --tag.");
|
|
3073
|
+
await setupGitHub({
|
|
2763
3074
|
workspaceName: args["workspace-name"],
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
3075
|
+
branch: args.branch,
|
|
3076
|
+
tag: args.tag,
|
|
3077
|
+
tagPattern: args["tag-pattern"] ?? "v*",
|
|
3078
|
+
environment: args.environment,
|
|
3079
|
+
plan: !args["no-plan"],
|
|
2767
3080
|
dir: args.dir,
|
|
2768
|
-
|
|
2769
|
-
|
|
3081
|
+
force: args.force,
|
|
3082
|
+
outputDir: process.cwd()
|
|
2770
3083
|
});
|
|
2771
3084
|
}
|
|
2772
3085
|
});
|
|
@@ -3044,10 +3357,7 @@ const deployCommand = defineAppCommand({
|
|
|
3044
3357
|
run: async (args) => {
|
|
3045
3358
|
await assertWritable({ profile: args.profile });
|
|
3046
3359
|
logger.info(`Deploying static website "${args.name}" from directory: ${args.dir}`);
|
|
3047
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
3048
|
-
useProfile: true,
|
|
3049
|
-
profile: args.profile
|
|
3050
|
-
}));
|
|
3360
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
3051
3361
|
const name = args.name;
|
|
3052
3362
|
const dir = path.resolve(process.cwd(), args.dir);
|
|
3053
3363
|
const workspaceId = await loadWorkspaceId({
|
|
@@ -3093,10 +3403,7 @@ const domainGetCommand = defineAppCommand({
|
|
|
3093
3403
|
})
|
|
3094
3404
|
}).strict(),
|
|
3095
3405
|
run: async (args) => {
|
|
3096
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
3097
|
-
useProfile: true,
|
|
3098
|
-
profile: args.profile
|
|
3099
|
-
}));
|
|
3406
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
3100
3407
|
const workspaceId = await loadWorkspaceId({
|
|
3101
3408
|
workspaceId: args["workspace-id"],
|
|
3102
3409
|
profile: args.profile
|
|
@@ -3136,10 +3443,7 @@ const domainListCommand = defineAppCommand({
|
|
|
3136
3443
|
})
|
|
3137
3444
|
}).strict(),
|
|
3138
3445
|
run: async (args) => {
|
|
3139
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
3140
|
-
useProfile: true,
|
|
3141
|
-
profile: args.profile
|
|
3142
|
-
}));
|
|
3446
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
3143
3447
|
const workspaceId = await loadWorkspaceId({
|
|
3144
3448
|
workspaceId: args["workspace-id"],
|
|
3145
3449
|
profile: args.profile
|
|
@@ -3191,10 +3495,7 @@ const getCommand = defineAppCommand({
|
|
|
3191
3495
|
})
|
|
3192
3496
|
}).strict(),
|
|
3193
3497
|
run: async (args) => {
|
|
3194
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
3195
|
-
useProfile: true,
|
|
3196
|
-
profile: args.profile
|
|
3197
|
-
}));
|
|
3498
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: args.profile }));
|
|
3198
3499
|
const workspaceId = await loadWorkspaceId({
|
|
3199
3500
|
workspaceId: args["workspace-id"],
|
|
3200
3501
|
profile: args.profile
|
|
@@ -3229,10 +3530,7 @@ const getCommand = defineAppCommand({
|
|
|
3229
3530
|
* @returns List of static websites
|
|
3230
3531
|
*/
|
|
3231
3532
|
async function listStaticWebsites(options) {
|
|
3232
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
3233
|
-
useProfile: true,
|
|
3234
|
-
profile: options?.profile
|
|
3235
|
-
}));
|
|
3533
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: options?.profile }));
|
|
3236
3534
|
const workspaceId = await loadWorkspaceId({
|
|
3237
3535
|
workspaceId: options?.workspaceId,
|
|
3238
3536
|
profile: options?.profile
|
|
@@ -3250,7 +3548,7 @@ async function listStaticWebsites(options) {
|
|
|
3250
3548
|
workspaceId,
|
|
3251
3549
|
name: site.name,
|
|
3252
3550
|
description: site.description,
|
|
3253
|
-
url: site.url
|
|
3551
|
+
url: site.url,
|
|
3254
3552
|
allowedIpAddresses: site.allowedIpAddresses
|
|
3255
3553
|
}));
|
|
3256
3554
|
}
|
|
@@ -3365,7 +3663,7 @@ const CLEAN_ROOM_NOTES = [
|
|
|
3365
3663
|
"It does not copy Liam source code, generated JavaScript/CSS, parser internals, or layout internals."
|
|
3366
3664
|
];
|
|
3367
3665
|
function buildRevision(schema) {
|
|
3368
|
-
return hashContent(JSON.stringify(schema)).slice(0, 16);
|
|
3666
|
+
return hashContent$1(JSON.stringify(schema)).slice(0, 16);
|
|
3369
3667
|
}
|
|
3370
3668
|
function toTypeSource(source) {
|
|
3371
3669
|
if (!source) return void 0;
|
|
@@ -3551,10 +3849,7 @@ function initErdCommand() {
|
|
|
3551
3849
|
async function initErdDeployContext(args) {
|
|
3552
3850
|
initErdCommand();
|
|
3553
3851
|
return {
|
|
3554
|
-
client: await initOperatorClient(await loadAccessToken({
|
|
3555
|
-
useProfile: true,
|
|
3556
|
-
profile: args.profile
|
|
3557
|
-
})),
|
|
3852
|
+
client: await initOperatorClient(await loadAccessToken({ profile: args.profile })),
|
|
3558
3853
|
workspaceId: await loadWorkspaceId({
|
|
3559
3854
|
workspaceId: args.workspaceId,
|
|
3560
3855
|
profile: args.profile
|
|
@@ -4153,18 +4448,13 @@ const erdCommand = defineCommand({
|
|
|
4153
4448
|
*/
|
|
4154
4449
|
async function script(options) {
|
|
4155
4450
|
logBetaWarning("tailordb migration");
|
|
4156
|
-
|
|
4157
|
-
if (isValidMigrationNumber(options.number)) migrationNumber = parseInt(options.number, 10);
|
|
4158
|
-
else if (/^[1-9]\d*$/.test(options.number)) {
|
|
4159
|
-
migrationNumber = parseInt(options.number, 10);
|
|
4160
|
-
if (migrationNumber > 9999) throw new Error(`Migration number ${options.number} is out of range. Expected 1-9999.`);
|
|
4161
|
-
} else throw new Error(`Invalid migration number format: ${options.number}. Expected 4-digit format (e.g., 0001) or integer 1-9999 (e.g., 1).`);
|
|
4451
|
+
const migrationNumber = parseMigrationNumberArg(options.number);
|
|
4162
4452
|
if (migrationNumber === 0) throw new Error(`Migration ${options.number} is the initial schema snapshot and cannot have a migration script.`);
|
|
4163
4453
|
const { config } = await loadConfig(options.configPath);
|
|
4164
4454
|
const namespacesWithMigrations = getNamespacesWithMigrations(config, path.dirname(config.path));
|
|
4165
4455
|
if (namespacesWithMigrations.length === 0) throw new Error("No TailorDB services with migrations configuration found");
|
|
4166
4456
|
const targetNamespace = resolveTargetNamespace(namespacesWithMigrations, options.namespace);
|
|
4167
|
-
const { migrationsDir } = namespacesWithMigrations.find((ns) => ns.namespace === targetNamespace);
|
|
4457
|
+
const { migrationsDir } = assertDefined(namespacesWithMigrations.find((ns) => ns.namespace === targetNamespace), "namespace with migrations not found");
|
|
4168
4458
|
const diffPath = getMigrationFilePath(migrationsDir, migrationNumber, "diff");
|
|
4169
4459
|
if (!fs$1.existsSync(diffPath)) throw new Error(`Migration ${options.number} not found in ${migrationsDir}. Expected ${diffPath}.`);
|
|
4170
4460
|
const migratePath = getMigrationFilePath(migrationsDir, migrationNumber, "migrate");
|
|
@@ -4196,7 +4486,10 @@ function resolveTargetNamespace(namespacesWithMigrations, requested) {
|
|
|
4196
4486
|
if (!namespacesWithMigrations.some((ns) => ns.namespace === requested)) throw new Error(`Namespace "${requested}" not found or does not have migrations configured`);
|
|
4197
4487
|
return requested;
|
|
4198
4488
|
}
|
|
4199
|
-
if (namespacesWithMigrations.length === 1)
|
|
4489
|
+
if (namespacesWithMigrations.length === 1) {
|
|
4490
|
+
const [ns] = namespacesWithMigrations;
|
|
4491
|
+
return assertDefined(ns, "namespace with migrations missing").namespace;
|
|
4492
|
+
}
|
|
4200
4493
|
throw new Error(`Multiple TailorDB services found. Please specify namespace with --namespace flag: ${namespacesWithMigrations.map((ns) => ns.namespace).join(", ")}`);
|
|
4201
4494
|
}
|
|
4202
4495
|
const scriptCommand = defineAppCommand({
|
|
@@ -4244,12 +4537,11 @@ async function set(options) {
|
|
|
4244
4537
|
if (options.namespace) {
|
|
4245
4538
|
if (!namespacesWithMigrations.some((ns) => ns.namespace === options.namespace)) throw new Error(`Namespace "${options.namespace}" not found or does not have migrations configured`);
|
|
4246
4539
|
targetNamespace = options.namespace;
|
|
4247
|
-
} else if (namespacesWithMigrations.length === 1)
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
}));
|
|
4540
|
+
} else if (namespacesWithMigrations.length === 1) {
|
|
4541
|
+
const [ns] = namespacesWithMigrations;
|
|
4542
|
+
targetNamespace = assertDefined(ns, "namespace with migrations missing").namespace;
|
|
4543
|
+
} else throw new Error(`Multiple TailorDB services found. Please specify namespace with --namespace flag: ${namespacesWithMigrations.map((ns) => ns.namespace).join(", ")}`);
|
|
4544
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
|
|
4253
4545
|
const trn = resourceTrn(await loadWorkspaceId({
|
|
4254
4546
|
workspaceId: options.workspaceId,
|
|
4255
4547
|
profile: options.profile
|
|
@@ -4257,7 +4549,7 @@ async function set(options) {
|
|
|
4257
4549
|
let currentMigration;
|
|
4258
4550
|
try {
|
|
4259
4551
|
const { metadata } = await client.getMetadata({ trn });
|
|
4260
|
-
const label = metadata?.labels
|
|
4552
|
+
const label = metadata?.labels["sdk-migration"];
|
|
4261
4553
|
currentMigration = label ? parseMigrationLabelNumber(label) ?? 0 : 0;
|
|
4262
4554
|
} catch {
|
|
4263
4555
|
currentMigration = 0;
|
|
@@ -4332,10 +4624,7 @@ async function collectMigrationStatuses(options) {
|
|
|
4332
4624
|
if (namespacesWithMigrations.length === 0) throw new Error("No TailorDB services with migrations configuration found");
|
|
4333
4625
|
const targetNamespaces = options.namespace ? namespacesWithMigrations.filter((ns) => ns.namespace === options.namespace) : namespacesWithMigrations;
|
|
4334
4626
|
if (targetNamespaces.length === 0) throw new Error(`Namespace "${options.namespace}" not found or does not have migrations configured`);
|
|
4335
|
-
const client = await initOperatorClient(await loadAccessToken({
|
|
4336
|
-
useProfile: false,
|
|
4337
|
-
profile: options.profile
|
|
4338
|
-
}));
|
|
4627
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
|
|
4339
4628
|
const workspaceId = await loadWorkspaceId({
|
|
4340
4629
|
workspaceId: options.workspaceId,
|
|
4341
4630
|
profile: options.profile
|
|
@@ -4346,7 +4635,7 @@ async function collectMigrationStatuses(options) {
|
|
|
4346
4635
|
let currentMigration;
|
|
4347
4636
|
try {
|
|
4348
4637
|
const { metadata } = await client.getMetadata({ trn });
|
|
4349
|
-
const label = metadata?.labels
|
|
4638
|
+
const label = metadata?.labels["sdk-migration"];
|
|
4350
4639
|
currentMigration = label ? parseMigrationLabelNumber(label) ?? 0 : 0;
|
|
4351
4640
|
} catch {
|
|
4352
4641
|
currentMigration = 0;
|
|
@@ -4419,6 +4708,295 @@ const statusCommand = defineAppCommand({
|
|
|
4419
4708
|
}
|
|
4420
4709
|
});
|
|
4421
4710
|
|
|
4711
|
+
//#endregion
|
|
4712
|
+
//#region src/cli/commands/tailordb/migrate/sync.ts
|
|
4713
|
+
async function fetchRemoteGqlPermissions(client, workspaceId, namespace) {
|
|
4714
|
+
return fetchAll(async (pageToken, maxPageSize) => {
|
|
4715
|
+
try {
|
|
4716
|
+
const { permissions, nextPageToken } = await client.listTailorDBGQLPermissions({
|
|
4717
|
+
workspaceId,
|
|
4718
|
+
namespaceName: namespace,
|
|
4719
|
+
pageToken,
|
|
4720
|
+
pageSize: maxPageSize
|
|
4721
|
+
});
|
|
4722
|
+
return [permissions, nextPageToken];
|
|
4723
|
+
} catch (error) {
|
|
4724
|
+
if (error instanceof ConnectError && error.code === Code.NotFound) return [[], ""];
|
|
4725
|
+
throw error;
|
|
4726
|
+
}
|
|
4727
|
+
});
|
|
4728
|
+
}
|
|
4729
|
+
async function fetchRemoteTypes(client, workspaceId, namespace) {
|
|
4730
|
+
return fetchAll(async (pageToken, maxPageSize) => {
|
|
4731
|
+
try {
|
|
4732
|
+
const { tailordbTypes, nextPageToken } = await client.listTailorDBTypes({
|
|
4733
|
+
workspaceId,
|
|
4734
|
+
namespaceName: namespace,
|
|
4735
|
+
pageToken,
|
|
4736
|
+
pageSize: maxPageSize
|
|
4737
|
+
});
|
|
4738
|
+
return [tailordbTypes, nextPageToken];
|
|
4739
|
+
} catch (error) {
|
|
4740
|
+
if (error instanceof ConnectError && error.code === Code.NotFound) throw new Error(`Cannot sync: TailorDB namespace "${namespace}" has not been deployed yet.`, { cause: error });
|
|
4741
|
+
throw error;
|
|
4742
|
+
}
|
|
4743
|
+
});
|
|
4744
|
+
}
|
|
4745
|
+
/**
|
|
4746
|
+
* Verify that replaying the full migration history reproduces the current
|
|
4747
|
+
* local type definitions, before anything is sent to the remote.
|
|
4748
|
+
*
|
|
4749
|
+
* Sync force-applies a snapshot reconstructed from the migration history, so
|
|
4750
|
+
* the history itself must be trustworthy. When the reconstruction at the
|
|
4751
|
+
* latest migration does not match the schema defined in the local type files,
|
|
4752
|
+
* either the migration files were edited incorrectly or a schema change has
|
|
4753
|
+
* not been recorded as a migration yet — and overwriting the remote with an
|
|
4754
|
+
* unverified snapshot could destroy data. Fails before any RPC is issued.
|
|
4755
|
+
*
|
|
4756
|
+
* Returns the manifest generation options deploy would use for this
|
|
4757
|
+
* namespace (executor-driven publishRecordEvents and namespace
|
|
4758
|
+
* gqlOperations), so the synced manifests match what deploy produces.
|
|
4759
|
+
* @param loaded - Result of `loadConfig` (config and plugins)
|
|
4760
|
+
* @param target - Namespace whose migration history is being synced
|
|
4761
|
+
* @returns Options for `generateAllTypeManifestsFromSnapshot`
|
|
4762
|
+
*/
|
|
4763
|
+
async function assertMigrationsReproduceLocalTypes(loaded, target) {
|
|
4764
|
+
const { config, plugins } = loaded;
|
|
4765
|
+
const pluginManager = plugins.length > 0 ? new PluginManager(plugins) : void 0;
|
|
4766
|
+
const { defineApplication, generatePluginFilesIfNeeded } = await import("../application-DSXntqnV.mjs");
|
|
4767
|
+
const application = defineApplication({
|
|
4768
|
+
config,
|
|
4769
|
+
pluginManager
|
|
4770
|
+
});
|
|
4771
|
+
const tailordbService = application.tailorDBServices.find((s) => s.namespace === target.namespace);
|
|
4772
|
+
if (!tailordbService) throw new Error(`No TailorDB service found for namespace "${target.namespace}"`);
|
|
4773
|
+
for (const service of application.tailorDBServices) {
|
|
4774
|
+
await service.loadTypes();
|
|
4775
|
+
await service.processNamespacePlugins();
|
|
4776
|
+
}
|
|
4777
|
+
const pluginExecutorFiles = generatePluginFilesIfNeeded(pluginManager, application.tailorDBServices, config.path);
|
|
4778
|
+
const executorService = application.executorService ?? (pluginExecutorFiles.length > 0 ? (await import("../service-BHQIerYh.mjs")).createExecutorService({ config: { files: [] } }) : void 0);
|
|
4779
|
+
await executorService?.loadExecutors();
|
|
4780
|
+
if (pluginExecutorFiles.length > 0) await executorService?.loadPluginExecutorFiles([...pluginExecutorFiles]);
|
|
4781
|
+
const executorUsedTypes = /* @__PURE__ */ new Set();
|
|
4782
|
+
for (const executor of Object.values(executorService?.executors ?? {})) if (executor.trigger.kind === "tailordb") executorUsedTypes.add(executor.trigger.typeName);
|
|
4783
|
+
const manifestOptions = {
|
|
4784
|
+
executorUsedTypes,
|
|
4785
|
+
namespaceGqlOperations: tailordbService.config.gqlOperations
|
|
4786
|
+
};
|
|
4787
|
+
const latestSnapshot = reconstructSnapshotFromMigrations(target.migrationsDir);
|
|
4788
|
+
if (!latestSnapshot) return manifestOptions;
|
|
4789
|
+
const diff = compareLocalTypesWithSnapshot(latestSnapshot, createSnapshotFromLocalTypes(tailordbService.types, target.namespace).types, target.namespace);
|
|
4790
|
+
if (!hasChanges(diff)) return manifestOptions;
|
|
4791
|
+
logger.error(`Migration history does not reproduce the current local schema for namespace ${styles.bold(target.namespace)}:`);
|
|
4792
|
+
logger.log(formatMigrationDiff(diff));
|
|
4793
|
+
logger.newline();
|
|
4794
|
+
logger.info("This usually means one of the following:");
|
|
4795
|
+
logger.info(" - Migration files were edited and replaying them no longer matches the type definitions — fix the migration files.", { mode: "plain" });
|
|
4796
|
+
logger.info(" - Type definitions changed without a new migration — run 'tailor-sdk tailordb migration generate' first.", { mode: "plain" });
|
|
4797
|
+
logger.newline();
|
|
4798
|
+
throw new Error("Refusing to sync: the migration history must reproduce the current local schema before it can be applied to the remote.");
|
|
4799
|
+
}
|
|
4800
|
+
/**
|
|
4801
|
+
* Fetch the namespace's metadata labels and current migration number.
|
|
4802
|
+
*
|
|
4803
|
+
* Only GetMetadata NotFound is treated as "metadata does not exist yet".
|
|
4804
|
+
* Any other failure aborts the sync (which has not mutated anything at this
|
|
4805
|
+
* point): the fetched labels are written back verbatim at the end, so
|
|
4806
|
+
* proceeding with empty labels after a transient error would wipe the
|
|
4807
|
+
* namespace's existing metadata.
|
|
4808
|
+
* @param client - Operator client
|
|
4809
|
+
* @param trn - Namespace TRN
|
|
4810
|
+
* @returns Existing labels and the parsed current migration number
|
|
4811
|
+
*/
|
|
4812
|
+
async function fetchRemoteMigrationState(client, trn) {
|
|
4813
|
+
try {
|
|
4814
|
+
const { metadata } = await client.getMetadata({ trn });
|
|
4815
|
+
const labels = metadata?.labels ?? {};
|
|
4816
|
+
const label = labels[MIGRATION_LABEL_KEY];
|
|
4817
|
+
return {
|
|
4818
|
+
labels,
|
|
4819
|
+
current: label ? parseMigrationLabelNumber(label) : null
|
|
4820
|
+
};
|
|
4821
|
+
} catch (error) {
|
|
4822
|
+
if (error instanceof ConnectError && error.code === Code.NotFound) return {
|
|
4823
|
+
labels: {},
|
|
4824
|
+
current: null
|
|
4825
|
+
};
|
|
4826
|
+
throw error;
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
function selectTargetNamespace(namespacesWithMigrations, requested) {
|
|
4830
|
+
if (namespacesWithMigrations.length === 0) throw new Error("No TailorDB services with migrations configuration found");
|
|
4831
|
+
if (requested) {
|
|
4832
|
+
const found = namespacesWithMigrations.find((ns) => ns.namespace === requested);
|
|
4833
|
+
if (!found) throw new Error(`Namespace "${requested}" not found or does not have migrations configured`);
|
|
4834
|
+
return found;
|
|
4835
|
+
}
|
|
4836
|
+
if (namespacesWithMigrations.length > 1) throw new Error(`Multiple TailorDB services found. Please specify namespace with --namespace flag: ${namespacesWithMigrations.map((ns) => ns.namespace).join(", ")}`);
|
|
4837
|
+
return assertDefined(namespacesWithMigrations[0], "namespace with migrations missing");
|
|
4838
|
+
}
|
|
4839
|
+
/**
|
|
4840
|
+
* Sync remote TailorDB schema to a specific migration snapshot.
|
|
4841
|
+
*
|
|
4842
|
+
* Reconstructs the schema state at `<number>` from `0000/schema.json` + diffs,
|
|
4843
|
+
* then issues create/update/delete RPCs so the remote matches that snapshot.
|
|
4844
|
+
* Updates the migration label to `<number>` on success. Before any remote
|
|
4845
|
+
* mutation, verifies that the migration history reproduces the current local
|
|
4846
|
+
* type definitions (see {@link assertMigrationsReproduceLocalTypes}).
|
|
4847
|
+
*
|
|
4848
|
+
* Intended for recovering from drift introduced by `deploy --no-schema-check`
|
|
4849
|
+
* runs against an older revision: instead of having to `git checkout` that
|
|
4850
|
+
* revision and re-deploy, the operator can sync the remote back to a known
|
|
4851
|
+
* snapshot version directly.
|
|
4852
|
+
* @param options - Command options
|
|
4853
|
+
*/
|
|
4854
|
+
async function sync(options) {
|
|
4855
|
+
logBetaWarning("tailordb migration");
|
|
4856
|
+
const targetVersion = parseMigrationNumberArg(options.number);
|
|
4857
|
+
const loaded = await loadConfig(options.configPath);
|
|
4858
|
+
const { config } = loaded;
|
|
4859
|
+
const target = selectTargetNamespace(getNamespacesWithMigrations(config, path.dirname(config.path)), options.namespace);
|
|
4860
|
+
assertValidMigrationFiles(target.migrationsDir, target.namespace);
|
|
4861
|
+
const latest = getLatestMigrationNumber(target.migrationsDir);
|
|
4862
|
+
if (targetVersion > latest) throw new Error(`Migration ${formatMigrationNumber(targetVersion)} does not exist in working tree (latest is ${formatMigrationNumber(latest)}).`);
|
|
4863
|
+
const snapshot = reconstructSnapshotFromMigrations(target.migrationsDir, targetVersion);
|
|
4864
|
+
if (!snapshot) throw new Error(`No initial schema snapshot found in ${target.migrationsDir}. Expected 0000/schema.json.`);
|
|
4865
|
+
const manifestOptions = await assertMigrationsReproduceLocalTypes(loaded, target);
|
|
4866
|
+
const client = await initOperatorClient(await loadAccessToken({ profile: options.profile }));
|
|
4867
|
+
const workspaceId = await loadWorkspaceId({
|
|
4868
|
+
workspaceId: options.workspaceId,
|
|
4869
|
+
profile: options.profile
|
|
4870
|
+
});
|
|
4871
|
+
const trn = resourceTrn(workspaceId, "tailordb", target.namespace);
|
|
4872
|
+
const remoteState = await fetchRemoteMigrationState(client, trn);
|
|
4873
|
+
const remoteTypes = await fetchRemoteTypes(client, workspaceId, target.namespace);
|
|
4874
|
+
const { creates, updates, deletes } = compareSnapshotWithRemote(snapshot, new Set(remoteTypes.map((t) => t.name)));
|
|
4875
|
+
const remoteGqlPermissions = await fetchRemoteGqlPermissions(client, workspaceId, target.namespace);
|
|
4876
|
+
const remoteGqlPermissionTypes = new Set(remoteGqlPermissions.map((p) => p.typeName));
|
|
4877
|
+
const desiredGqlPermissions = Object.entries(snapshot.types).flatMap(([typeName, snapshotType]) => snapshotType.permissions?.gql ? [{
|
|
4878
|
+
typeName,
|
|
4879
|
+
permission: protoGqlPermission(snapshotType.permissions.gql)
|
|
4880
|
+
}] : []);
|
|
4881
|
+
const desiredGqlPermissionTypes = new Set(desiredGqlPermissions.map((p) => p.typeName));
|
|
4882
|
+
const gqlPermissionDeletes = remoteGqlPermissions.filter((p) => !desiredGqlPermissionTypes.has(p.typeName));
|
|
4883
|
+
const current = remoteState.current;
|
|
4884
|
+
logger.newline();
|
|
4885
|
+
logger.info(`Namespace: ${styles.bold(target.namespace)}`);
|
|
4886
|
+
logger.log(` Current migration: ${current === null ? "<unset>" : styles.bold(formatMigrationNumber(current))}`);
|
|
4887
|
+
logger.log(` Target migration: ${styles.bold(formatMigrationNumber(targetVersion))}`);
|
|
4888
|
+
logger.log(` Types to create: ${styles.bold(String(creates.length))}`);
|
|
4889
|
+
logger.log(` Types to update: ${styles.bold(String(updates.length))}`);
|
|
4890
|
+
logger.log(` Types to delete: ${styles.bold(String(deletes.length))}`);
|
|
4891
|
+
logger.log(` GQL permissions to set: ${styles.bold(String(desiredGqlPermissions.length))}`);
|
|
4892
|
+
logger.log(` GQL permissions to delete: ${styles.bold(String(gqlPermissionDeletes.length))}`);
|
|
4893
|
+
logger.newline();
|
|
4894
|
+
if (creates.length + updates.length + deletes.length + desiredGqlPermissions.length + gqlPermissionDeletes.length === 0) logger.info("No types to apply; only the migration label will be updated.");
|
|
4895
|
+
else {
|
|
4896
|
+
logger.warn("This operation will overwrite remote TailorDB types to match the selected snapshot.");
|
|
4897
|
+
if (deletes.length > 0) logger.warn("Existing data in deleted types will be lost.");
|
|
4898
|
+
logger.newline();
|
|
4899
|
+
}
|
|
4900
|
+
logger.warn("Sync never runs migrate.ts scripts; it only applies the schema snapshot and moves the migration label.");
|
|
4901
|
+
logger.newline();
|
|
4902
|
+
if (current !== null && targetVersion < current) {
|
|
4903
|
+
logger.warn(`Migrations ${formatMigrationNumber(targetVersion + 1)}–${formatMigrationNumber(current)} will become pending again and re-execute on the next deploy, including their migrate.ts scripts. Make sure those scripts are idempotent (safe to re-run).`);
|
|
4904
|
+
logger.newline();
|
|
4905
|
+
} else if (current !== null && targetVersion > current) {
|
|
4906
|
+
logger.warn(`Moving the migration label forwards (${formatMigrationNumber(current)} → ${formatMigrationNumber(targetVersion)}): migrate.ts scripts for migrations ${formatMigrationNumber(current + 1)}–${formatMigrationNumber(targetVersion)} will not run on the next deploy.`);
|
|
4907
|
+
logger.newline();
|
|
4908
|
+
}
|
|
4909
|
+
if (!options.yes) {
|
|
4910
|
+
if (!await prompt.confirm({
|
|
4911
|
+
message: `Continue and set migration label to ${formatMigrationNumber(targetVersion)}?`,
|
|
4912
|
+
default: false
|
|
4913
|
+
})) {
|
|
4914
|
+
logger.info("Operation cancelled.");
|
|
4915
|
+
return;
|
|
4916
|
+
}
|
|
4917
|
+
logger.newline();
|
|
4918
|
+
}
|
|
4919
|
+
const manifests = generateAllTypeManifestsFromSnapshot(snapshot, manifestOptions);
|
|
4920
|
+
const manifestFor = (typeName) => {
|
|
4921
|
+
const manifest = manifests.get(typeName);
|
|
4922
|
+
if (!manifest) throw new Error(`Internal error: no manifest generated for type "${typeName}". No changes were applied.`);
|
|
4923
|
+
return manifest;
|
|
4924
|
+
};
|
|
4925
|
+
const createManifests = creates.map((typeName) => manifestFor(typeName));
|
|
4926
|
+
const updateManifests = updates.map((typeName) => manifestFor(typeName));
|
|
4927
|
+
try {
|
|
4928
|
+
await Promise.all([...createManifests.map((tailordbType) => client.createTailorDBType({
|
|
4929
|
+
workspaceId,
|
|
4930
|
+
namespaceName: target.namespace,
|
|
4931
|
+
tailordbType
|
|
4932
|
+
})), ...updateManifests.map((tailordbType) => client.updateTailorDBType({
|
|
4933
|
+
workspaceId,
|
|
4934
|
+
namespaceName: target.namespace,
|
|
4935
|
+
tailordbType
|
|
4936
|
+
}))]);
|
|
4937
|
+
} catch (error) {
|
|
4938
|
+
handleOptionalToRequiredError(error, ["The target snapshot marks a field as required, but existing remote records have no value for it.", "Populate those records first (e.g. with a migration script applied via 'tailor-sdk deploy'), then re-run the sync."]);
|
|
4939
|
+
}
|
|
4940
|
+
await Promise.all(desiredGqlPermissions.map(({ typeName, permission }) => {
|
|
4941
|
+
const request = {
|
|
4942
|
+
workspaceId,
|
|
4943
|
+
namespaceName: target.namespace,
|
|
4944
|
+
typeName,
|
|
4945
|
+
permission
|
|
4946
|
+
};
|
|
4947
|
+
return remoteGqlPermissionTypes.has(typeName) ? client.updateTailorDBGQLPermission(request) : client.createTailorDBGQLPermission(request);
|
|
4948
|
+
}));
|
|
4949
|
+
await Promise.all(gqlPermissionDeletes.map((p) => client.deleteTailorDBGQLPermission({
|
|
4950
|
+
workspaceId,
|
|
4951
|
+
namespaceName: target.namespace,
|
|
4952
|
+
typeName: p.typeName
|
|
4953
|
+
})));
|
|
4954
|
+
await Promise.all(deletes.map((typeName) => client.deleteTailorDBType({
|
|
4955
|
+
workspaceId,
|
|
4956
|
+
namespaceName: target.namespace,
|
|
4957
|
+
tailordbTypeName: typeName
|
|
4958
|
+
})));
|
|
4959
|
+
await client.setMetadata({
|
|
4960
|
+
trn,
|
|
4961
|
+
labels: {
|
|
4962
|
+
...remoteState.labels,
|
|
4963
|
+
[MIGRATION_LABEL_KEY]: `${"m"}${formatMigrationNumber(targetVersion)}`
|
|
4964
|
+
}
|
|
4965
|
+
});
|
|
4966
|
+
logger.success(`Synced namespace ${styles.bold(target.namespace)} to migration ${styles.bold(formatMigrationNumber(targetVersion))}.`);
|
|
4967
|
+
if (targetVersion < latest) {
|
|
4968
|
+
logger.newline();
|
|
4969
|
+
logger.info(`Run 'tailor-sdk deploy' to apply migrations ${formatMigrationNumber(targetVersion + 1)}–${formatMigrationNumber(latest)} from the working tree.`);
|
|
4970
|
+
}
|
|
4971
|
+
}
|
|
4972
|
+
const syncCommand = defineAppCommand({
|
|
4973
|
+
name: "sync",
|
|
4974
|
+
description: "Sync remote TailorDB schema to a specific migration snapshot (recovery from --no-schema-check drift).",
|
|
4975
|
+
args: z.object({
|
|
4976
|
+
...deploymentArgs,
|
|
4977
|
+
...confirmationArgs,
|
|
4978
|
+
number: arg(z.string(), {
|
|
4979
|
+
positional: true,
|
|
4980
|
+
description: "Migration number to sync to (e.g., 0001 or 1; 0 targets the baseline snapshot)"
|
|
4981
|
+
}),
|
|
4982
|
+
namespace: arg(z.string().optional(), {
|
|
4983
|
+
alias: "n",
|
|
4984
|
+
description: "Target TailorDB namespace (required if multiple namespaces exist)"
|
|
4985
|
+
})
|
|
4986
|
+
}).strict(),
|
|
4987
|
+
run: async (args) => {
|
|
4988
|
+
await assertWritable({ profile: args.profile });
|
|
4989
|
+
await sync({
|
|
4990
|
+
configPath: args.config,
|
|
4991
|
+
number: args.number,
|
|
4992
|
+
namespace: args.namespace,
|
|
4993
|
+
yes: args.yes,
|
|
4994
|
+
workspaceId: args["workspace-id"],
|
|
4995
|
+
profile: args.profile
|
|
4996
|
+
});
|
|
4997
|
+
}
|
|
4998
|
+
});
|
|
4999
|
+
|
|
4422
5000
|
//#endregion
|
|
4423
5001
|
//#region src/cli/commands/tailordb/migrate/index.ts
|
|
4424
5002
|
/**
|
|
@@ -4429,6 +5007,7 @@ const statusCommand = defineAppCommand({
|
|
|
4429
5007
|
* - script: Add a migrate.ts template to an existing migration
|
|
4430
5008
|
* - set: Set migration checkpoint to a specific number
|
|
4431
5009
|
* - status: Show migration status for TailorDB namespaces
|
|
5010
|
+
* - sync: Sync remote TailorDB schema to a specific migration snapshot
|
|
4432
5011
|
*/
|
|
4433
5012
|
const migrationCommand = defineCommand({
|
|
4434
5013
|
name: "migration",
|
|
@@ -4437,7 +5016,8 @@ const migrationCommand = defineCommand({
|
|
|
4437
5016
|
generate: generateCommand$1,
|
|
4438
5017
|
script: scriptCommand,
|
|
4439
5018
|
set: setCommand,
|
|
4440
|
-
status: statusCommand
|
|
5019
|
+
status: statusCommand,
|
|
5020
|
+
sync: syncCommand
|
|
4441
5021
|
}
|
|
4442
5022
|
});
|
|
4443
5023
|
|
|
@@ -4472,7 +5052,7 @@ const upgradeCommand = defineAppCommand({
|
|
|
4472
5052
|
run: async (args) => {
|
|
4473
5053
|
const { initTelemetry } = await import("../telemetry-w92bvGdC.mjs");
|
|
4474
5054
|
await initTelemetry();
|
|
4475
|
-
const { upgrade } = await import("../service-
|
|
5055
|
+
const { upgrade } = await import("../service-DMohAx8a2.mjs");
|
|
4476
5056
|
await upgrade({
|
|
4477
5057
|
from: args.from,
|
|
4478
5058
|
dryRun: args["dry-run"],
|
|
@@ -4898,7 +5478,7 @@ runMain(mainCommand, {
|
|
|
4898
5478
|
if (isVerbose() && error.stack) logger.debug(`\nStack trace:\n${error.stack}`);
|
|
4899
5479
|
} else logger.error(`Unknown error: ${error}`);
|
|
4900
5480
|
if (!isCLIError(error) && (!(error instanceof Error) || error instanceof TypeError || error instanceof RangeError)) {
|
|
4901
|
-
const { reportCrash } = await import("../crashreport-
|
|
5481
|
+
const { reportCrash } = await import("../crashreport-D1wKBJ8N.mjs");
|
|
4902
5482
|
await reportCrash(error, "handledError");
|
|
4903
5483
|
}
|
|
4904
5484
|
}
|