@workflow-code/cli 0.1.2 → 0.1.4-20260814002

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.
@@ -1,12 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  __commonJS,
4
- __toESM
5
- } from "./chunk-FYS2JH42.js";
4
+ __toESM,
5
+ getCurrentLocale,
6
+ i18n,
7
+ prepareCliInvocation,
8
+ translateWorkflowCodeError
9
+ } from "./chunk-3ZXXRODP.js";
6
10
 
7
- // ../../node_modules/.pnpm/ignore@7.0.6/node_modules/ignore/index.js
11
+ // node_modules/.pnpm/ignore@7.0.6/node_modules/ignore/index.js
8
12
  var require_ignore = __commonJS({
9
- "../../node_modules/.pnpm/ignore@7.0.6/node_modules/ignore/index.js"(exports, module) {
13
+ "node_modules/.pnpm/ignore@7.0.6/node_modules/ignore/index.js"(exports, module) {
10
14
  "use strict";
11
15
  function makeArray(subject) {
12
16
  return Array.isArray(subject) ? subject : [subject];
@@ -465,13 +469,13 @@ var require_ignore = __commonJS({
465
469
 
466
470
  // src/workspace.ts
467
471
  import { spawn } from "child_process";
468
- import { randomUUID } from "crypto";
472
+ import { createHash, randomUUID } from "crypto";
469
473
  import { existsSync as existsSync2 } from "fs";
470
- import { mkdir as mkdir3, readFile as readFile3, rm as rm2, writeFile as writeFile2 } from "fs/promises";
474
+ import { copyFile as copyFile2, mkdir as mkdir3, readFile as readFile3, readdir as readdir2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
471
475
  import path4 from "path";
472
476
  import { fileURLToPath, pathToFileURL } from "url";
473
477
 
474
- // ../../shared/auth-config/index.ts
478
+ // .shared/auth-config/index.ts
475
479
  var WORKFLOW_AUTH_FILE_NAME = "workflow-auth.json";
476
480
  var DEFAULT_MAC_PLATFORM = "darwin";
477
481
  var DEFAULT_WINDOWS_PLATFORM = "win32";
@@ -487,7 +491,7 @@ function normalizeWorkflowAuthConfig(value) {
487
491
  return createEmptyWorkflowAuthConfig();
488
492
  }
489
493
  return {
490
- // issue #90 Accept legacy field names (remoteUrl, token) from pre-migration
494
+ // Accept legacy field names (remoteUrl, token) from pre-migration
491
495
  // config files so existing users don't lose their credentials on upgrade.
492
496
  serverUrl: readString(value.serverUrl) ?? readString(value.remoteUrl) ?? "",
493
497
  apiKey: readString(value.apiKey) ?? readString(value.token) ?? "",
@@ -576,24 +580,29 @@ function readGlobalProcess() {
576
580
  return processValue;
577
581
  }
578
582
 
579
- // ../../shared/workflow-ids/index.ts
583
+ // .shared/workflow-ids/index.ts
580
584
  import path from "path";
581
585
  var WORKFLOW_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
582
586
  function isWorkflowUuid(value) {
583
587
  return WORKFLOW_UUID_PATTERN.test(value.trim());
584
588
  }
585
589
 
586
- // ../../shared/project-upload/node.ts
590
+ // .shared/project-upload/node.ts
587
591
  import { copyFile, lstat, mkdir, readFile, readdir } from "fs/promises";
588
592
  import path2 from "path";
589
593
 
590
- // ../../shared/project-upload/index.ts
594
+ // .shared/project-upload/index.ts
591
595
  var import_ignore = __toESM(require_ignore(), 1);
592
596
 
593
- // ../../shared/kanban-project/index.ts
597
+ // .shared/kanban-project/index.ts
594
598
  var KANBAN_MANIFEST_FILE = "kanban.json";
595
599
  var KANBAN_ENTRY_FILE = "index.html";
596
600
  var KANBAN_ARTIFACT_DIR = ".";
601
+ var KANBAN_DATA_SOURCE_ID_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
602
+ var KANBAN_WORKFLOW_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
603
+ var MAX_KANBAN_DATA_SOURCES = 32;
604
+ var MAX_KANBAN_DATA_SOURCE_PREFIXES = 32;
605
+ var MAX_KANBAN_DATA_SOURCE_KEY_LENGTH = 1024;
597
606
  function readPackageProjectType(value) {
598
607
  if (!isRecord2(value)) return void 0;
599
608
  const workflowCode = value["workflowCode"];
@@ -624,7 +633,8 @@ function readKanbanArtifactConfig(value) {
624
633
  "workflowCode.kanban.entry",
625
634
  KANBAN_ENTRY_FILE,
626
635
  false
627
- )
636
+ ),
637
+ dataSources: parseKanbanDataSources(rawConfig?.["dataSources"])
628
638
  };
629
639
  }
630
640
  function createKanbanArtifactEntryPath(config) {
@@ -651,11 +661,72 @@ function readKanbanRelativePath(value, field, fallback, allowDot) {
651
661
  }
652
662
  return normalized;
653
663
  }
664
+ function parseKanbanDataSources(value) {
665
+ if (value === void 0) return [];
666
+ if (!Array.isArray(value)) {
667
+ throw new Error("package.json workflowCode.kanban.dataSources must be an array.");
668
+ }
669
+ if (value.length > MAX_KANBAN_DATA_SOURCES) {
670
+ throw new Error(`package.json workflowCode.kanban.dataSources supports at most ${MAX_KANBAN_DATA_SOURCES} items.`);
671
+ }
672
+ const ids = /* @__PURE__ */ new Set();
673
+ return value.map((item, index) => {
674
+ if (!isRecord2(item)) {
675
+ throw new Error(`package.json workflowCode.kanban.dataSources[${index}] must be an object.`);
676
+ }
677
+ const supportedFields = /* @__PURE__ */ new Set(["id", "kind", "projectId", "keyPrefixes"]);
678
+ if (Object.keys(item).some((field) => !supportedFields.has(field))) {
679
+ throw new Error(`package.json workflowCode.kanban.dataSources[${index}] contains unsupported fields.`);
680
+ }
681
+ const id = readNonEmptyString(item["id"], `workflowCode.kanban.dataSources[${index}].id`);
682
+ if (!KANBAN_DATA_SOURCE_ID_PATTERN.test(id)) {
683
+ throw new Error(`package.json workflowCode.kanban.dataSources[${index}].id is invalid.`);
684
+ }
685
+ if (ids.has(id)) {
686
+ throw new Error(`package.json workflowCode.kanban.dataSources contains duplicate id "${id}".`);
687
+ }
688
+ ids.add(id);
689
+ if (item["kind"] !== "workflow-kv") {
690
+ throw new Error(`package.json workflowCode.kanban.dataSources[${index}].kind is unsupported.`);
691
+ }
692
+ const projectId = readNonEmptyString(
693
+ item["projectId"],
694
+ `workflowCode.kanban.dataSources[${index}].projectId`
695
+ );
696
+ if (!KANBAN_WORKFLOW_UUID_PATTERN.test(projectId)) {
697
+ throw new Error(`package.json workflowCode.kanban.dataSources[${index}].projectId must be a UUID.`);
698
+ }
699
+ const rawPrefixes = item["keyPrefixes"];
700
+ if (!Array.isArray(rawPrefixes) || rawPrefixes.length === 0 || rawPrefixes.length > MAX_KANBAN_DATA_SOURCE_PREFIXES) {
701
+ throw new Error(
702
+ `package.json workflowCode.kanban.dataSources[${index}].keyPrefixes must contain 1 to ${MAX_KANBAN_DATA_SOURCE_PREFIXES} items.`
703
+ );
704
+ }
705
+ const keyPrefixes = rawPrefixes.map((prefix, prefixIndex) => {
706
+ const normalized = readNonEmptyString(
707
+ prefix,
708
+ `workflowCode.kanban.dataSources[${index}].keyPrefixes[${prefixIndex}]`
709
+ );
710
+ if (normalized.length > MAX_KANBAN_DATA_SOURCE_KEY_LENGTH || normalized.includes("\0")) {
711
+ throw new Error(`package.json workflowCode.kanban.dataSources[${index}].keyPrefixes[${prefixIndex}] is invalid.`);
712
+ }
713
+ return normalized;
714
+ });
715
+ if (new Set(keyPrefixes).size !== keyPrefixes.length) {
716
+ throw new Error(`package.json workflowCode.kanban.dataSources[${index}].keyPrefixes contains duplicates.`);
717
+ }
718
+ return { id, kind: "workflow-kv", projectId, keyPrefixes };
719
+ });
720
+ }
721
+ function readNonEmptyString(value, field) {
722
+ if (typeof value !== "string" || value.trim() === "") throw new Error(`kanban.json ${field} must be a non-empty string.`);
723
+ return value.trim();
724
+ }
654
725
  function isRecord2(value) {
655
726
  return typeof value === "object" && value !== null && !Array.isArray(value);
656
727
  }
657
728
 
658
- // ../../shared/project-upload/index.ts
729
+ // .shared/project-upload/index.ts
659
730
  var WORKFLOW_IGNORE_FILE = ".workflowignore";
660
731
  function hasWorkflowIgnoreRules(content) {
661
732
  if (content === void 0) return false;
@@ -730,7 +801,7 @@ function findEffectiveWorkflowIgnoreRule(normalizedPath, ruleLines) {
730
801
  return ignored ? effectiveRule : void 0;
731
802
  }
732
803
 
733
- // ../../shared/project-upload/node.ts
804
+ // .shared/project-upload/node.ts
734
805
  async function createWorkflowUploadPlan(rootDir) {
735
806
  const projectRoot = path2.resolve(rootDir);
736
807
  const workflowIgnorePath = path2.join(projectRoot, WORKFLOW_IGNORE_FILE);
@@ -838,6 +909,15 @@ async function readWorkflowUploadUtf8File(filePath, label) {
838
909
  }
839
910
  }
840
911
 
912
+ // src/workspace.ts
913
+ import {
914
+ createWorkflowProjectDependencyLocks,
915
+ inspectLocalWorkflowStructure,
916
+ isProjectGroupHostDataPath,
917
+ parseWorkflowProjectDependencyLocks,
918
+ readKanbanArtifactConfig as readKanbanArtifactConfig2
919
+ } from "workflow-code";
920
+
841
921
  // src/auth-config.ts
842
922
  import { readFileSync } from "fs";
843
923
  import { mkdir as mkdir2, readFile as readFile2, rm, writeFile } from "fs/promises";
@@ -876,7 +956,7 @@ import path3 from "path";
876
956
  import { parse } from "dotenv";
877
957
  function loadCliEnv() {
878
958
  const cliRoot = process.cwd();
879
- const defaultRepoRoot = findRepoRoot(cliRoot) ?? cliRoot;
959
+ const defaultRepoRoot = findWorkspaceRepositoryRoot(cliRoot) ?? cliRoot;
880
960
  const initialRepoRoot = process.env.WORKFLOW_REPO_ROOT ? path3.resolve(process.env.WORKFLOW_REPO_ROOT) : defaultRepoRoot;
881
961
  loadEnvFiles([
882
962
  path3.join(cliRoot, ".env"),
@@ -887,10 +967,10 @@ function loadCliEnv() {
887
967
  loadEnvFiles([path3.join(resolvedRepoRoot, ".env")]);
888
968
  }
889
969
  }
890
- function findRepoRoot(startDir) {
970
+ function findWorkspaceRepositoryRoot(startDir) {
891
971
  let current = path3.resolve(startDir);
892
972
  while (true) {
893
- if (existsSync(path3.join(current, "pnpm-workspace.yaml")) && existsSync(path3.join(current, "packages", "cli", "package.json"))) {
973
+ if (existsSync(path3.join(current, "pnpm-workspace.yaml")) && existsSync(path3.join(current, "package.json")) && existsSync(path3.join(current, "workspace", "package.json")) && existsSync(path3.join(current, "workspace", "workflow"))) {
894
974
  return current;
895
975
  }
896
976
  const parent = path3.dirname(current);
@@ -920,7 +1000,10 @@ var DEFAULT_SERVER_URL = "http://localhost:7125";
920
1000
  var CLI_MODULE_DIR = path4.dirname(fileURLToPath(import.meta.url));
921
1001
  var DEFAULT_DEVICE_FLOW_CLIENT_NAME = "Workflow Workspace CLI";
922
1002
  loadCliEnv();
923
- async function main(argv = process.argv.slice(2)) {
1003
+ async function main(argv = process.argv.slice(2), options = {}) {
1004
+ if (options.localeInitialized !== true) {
1005
+ argv = prepareCliInvocation(argv).argv;
1006
+ }
924
1007
  const parsed = parseArgs(argv);
925
1008
  switch (parsed.command) {
926
1009
  case "login":
@@ -966,13 +1049,13 @@ async function main(argv = process.argv.slice(2)) {
966
1049
  printUsage();
967
1050
  return;
968
1051
  default:
969
- throw new Error(`Unknown command: ${parsed.command}`);
1052
+ throw new Error(i18n.t("unknownWorkspaceCommand", { ns: "cli", command: parsed.command }));
970
1053
  }
971
1054
  }
972
1055
  async function commandLogin(parsed) {
973
1056
  const server = parsed.options.server.trim();
974
1057
  if (server === "") {
975
- throw new Error("login requires --server.");
1058
+ throw new Error(i18n.t("loginRequiresServer", { ns: "cli" }));
976
1059
  }
977
1060
  const started = await fetchApi({
978
1061
  server,
@@ -980,7 +1063,7 @@ async function commandLogin(parsed) {
980
1063
  path: "/api/auth/device/start",
981
1064
  method: "POST",
982
1065
  body: {
983
- // issue #93 phase 1 moves workspace login to device flow and stores a
1066
+ // phase 1 moves workspace login to device flow and stores a
984
1067
  // dedicated CLI auth file instead of sharing the App config.
985
1068
  clientName: DEFAULT_DEVICE_FLOW_CLIENT_NAME,
986
1069
  source: "cli"
@@ -994,9 +1077,11 @@ async function commandLogin(parsed) {
994
1077
  }
995
1078
  const verificationUrl = started.data.verificationUriComplete || started.data.verificationUri;
996
1079
  void openExternalBrowser(verificationUrl);
997
- console.error(`Open the verification page to approve this login:
998
- ${verificationUrl}
999
- User code: ${started.data.userCode}`);
1080
+ console.error(i18n.t("deviceLoginPrompt", {
1081
+ ns: "cli",
1082
+ url: verificationUrl,
1083
+ code: started.data.userCode
1084
+ }));
1000
1085
  const completed = await waitForDeviceToken({
1001
1086
  server,
1002
1087
  deviceCode: started.data.deviceCode,
@@ -1048,12 +1133,26 @@ async function commandStatus() {
1048
1133
  }
1049
1134
  async function commandPack(parsed) {
1050
1135
  const workflowName = requirePositional(parsed, 0, "workflow");
1136
+ if (parsed.options.projectGroup === true || parsed.options.publishGroup === true || parsed.options.dependencies.length > 0 || parsed.options.dataGrants.length > 0) {
1137
+ const packed = await packProjectGroup({
1138
+ workflowArg: workflowName,
1139
+ sourcePath: parsed.options.path,
1140
+ outputPath: parsed.options.output,
1141
+ dependencyOptions: parsed.options.dependencies
1142
+ });
1143
+ console.log(JSON.stringify({ archivePath: packed.archivePath, manifest: packed.manifest }, null, 2));
1144
+ return;
1145
+ }
1051
1146
  const archivePath = await packWorkflow(workflowName, parsed.options.output, parsed.options.path);
1052
1147
  console.log(JSON.stringify({ archivePath }, null, 2));
1053
1148
  }
1054
1149
  async function commandUpload(parsed) {
1055
1150
  requireLogin(parsed);
1056
1151
  const workflowArg = requirePositional(parsed, 0, "workflow");
1152
+ if (shouldUseProjectGroupUpload(parsed.options)) {
1153
+ await commandProjectGroupUpload(parsed, workflowArg);
1154
+ return;
1155
+ }
1057
1156
  const packageContext = await readWorkflowPackageContext(workflowArg, parsed.options.path);
1058
1157
  let workflowId = packageContext.workflowId;
1059
1158
  if (parsed.options.create === true) {
@@ -1065,7 +1164,7 @@ async function commandUpload(parsed) {
1065
1164
  workflowId = created.workflowId;
1066
1165
  }
1067
1166
  if (!workflowId) {
1068
- throw new Error("Workflow package.json must include a UUID id. Use `workflow-code workspace upload <workflow> --create` after login to create and bind a server project.");
1167
+ throw new Error(i18n.t("workflowIdRequired", { ns: "cli" }));
1069
1168
  }
1070
1169
  const archivePath = parsed.options.file ? path4.resolve(parsed.options.file) : await packWorkflow(workflowArg, parsed.options.output, parsed.options.path);
1071
1170
  const uploadResult = await uploadPackage({
@@ -1109,6 +1208,101 @@ async function commandUpload(parsed) {
1109
1208
  assertApiOk(publishResult);
1110
1209
  }
1111
1210
  }
1211
+ function shouldUseProjectGroupUpload(options) {
1212
+ return options.projectGroup === true || options.publishGroup === true || options.dependencies.length > 0 || options.dataGrants.length > 0;
1213
+ }
1214
+ async function commandProjectGroupUpload(parsed, workflowArg, forcePublish = false) {
1215
+ if (parsed.options.file !== void 0) {
1216
+ throw new Error(i18n.t("projectGroupUploadFileUnsupported", { ns: "cli" }));
1217
+ }
1218
+ let rootContext = await readWorkflowPackageContext(workflowArg, parsed.options.path);
1219
+ if (parsed.options.create === true || rootContext.workflowId !== void 0) {
1220
+ await ensureWorkflowProjectCreated({
1221
+ packageContext: rootContext,
1222
+ server: parsed.options.server,
1223
+ token: parsed.options.token
1224
+ });
1225
+ rootContext = await readWorkflowPackageContext(workflowArg, parsed.options.path);
1226
+ }
1227
+ if (!rootContext.workflowId) {
1228
+ throw new Error(i18n.t("projectGroupUploadRootIdRequired", { ns: "cli" }));
1229
+ }
1230
+ const contexts = await readProjectGroupContexts({
1231
+ workflowArg,
1232
+ sourcePath: parsed.options.path,
1233
+ dependencyOptions: parsed.options.dependencies
1234
+ });
1235
+ for (const dependency of contexts.dependencies) {
1236
+ await ensureWorkflowProjectCreated({
1237
+ packageContext: dependency,
1238
+ server: parsed.options.server,
1239
+ token: parsed.options.token
1240
+ });
1241
+ }
1242
+ const packed = await packProjectGroup({
1243
+ workflowArg,
1244
+ sourcePath: parsed.options.path,
1245
+ outputPath: parsed.options.output,
1246
+ dependencyOptions: parsed.options.dependencies,
1247
+ contexts
1248
+ });
1249
+ const planned = await fetchApi({
1250
+ server: parsed.options.server,
1251
+ token: parsed.options.token,
1252
+ path: "/api/deployments",
1253
+ method: "POST",
1254
+ body: {
1255
+ manifest: packed.manifest,
1256
+ releaseLog: parsed.options.releaseLog
1257
+ }
1258
+ });
1259
+ if (planned.errCode !== 0 || planned.data === void 0) {
1260
+ printApiResponse(planned);
1261
+ assertApiOk(planned);
1262
+ return;
1263
+ }
1264
+ const prepared = await uploadProjectGroupPackage({
1265
+ deploymentId: planned.data.id,
1266
+ archivePath: packed.archivePath,
1267
+ server: parsed.options.server,
1268
+ token: parsed.options.token
1269
+ });
1270
+ if (prepared.errCode !== 0 || prepared.data === void 0) {
1271
+ printApiResponse(prepared);
1272
+ assertApiOk(prepared);
1273
+ return;
1274
+ }
1275
+ const grantsByProject = resolveExplicitDataGrants(
1276
+ packed.manifest.dependencies,
1277
+ parsed.options.dataGrants
1278
+ );
1279
+ for (const [sourceProjectId, prefixes] of grantsByProject) {
1280
+ const grant = await fetchApi({
1281
+ server: parsed.options.server,
1282
+ token: parsed.options.token,
1283
+ path: `/api/workflows/${encodeURIComponent(sourceProjectId)}/data-source-grants/${encodeURIComponent(packed.manifest.rootProjectId)}`,
1284
+ method: "PUT",
1285
+ body: { keyPrefixes: prefixes }
1286
+ });
1287
+ if (grant.errCode !== 0) {
1288
+ printApiResponse(grant);
1289
+ assertApiOk(grant);
1290
+ return;
1291
+ }
1292
+ }
1293
+ if (!forcePublish && parsed.options.releaseLog === void 0 && parsed.options.publishGroup !== true) {
1294
+ printApiResponse(prepared);
1295
+ return;
1296
+ }
1297
+ const published = await fetchApi({
1298
+ server: parsed.options.server,
1299
+ token: parsed.options.token,
1300
+ path: `/api/deployments/${encodeURIComponent(planned.data.id)}/publish`,
1301
+ method: "POST"
1302
+ });
1303
+ printApiResponse(published);
1304
+ assertApiOk(published);
1305
+ }
1112
1306
  async function commandPreparation(parsed) {
1113
1307
  requireLogin(parsed);
1114
1308
  const workflowId = requirePositional(parsed, 0, "workflow");
@@ -1133,6 +1327,7 @@ async function commandRun(parsed) {
1133
1327
  method: "POST",
1134
1328
  body: {
1135
1329
  target: parsed.options.target ?? "latest",
1330
+ entrypointId: parsed.options.entrypoint,
1136
1331
  args: workflowArgs
1137
1332
  }
1138
1333
  });
@@ -1151,6 +1346,7 @@ async function commandDebugNode(parsed) {
1151
1346
  method: "POST",
1152
1347
  body: {
1153
1348
  target: parsed.options.target ?? "latest",
1349
+ entrypointId: parsed.options.entrypoint,
1154
1350
  args: workflowArgs
1155
1351
  }
1156
1352
  });
@@ -1172,7 +1368,36 @@ async function commandDownload(parsed) {
1172
1368
  requireLogin(parsed);
1173
1369
  const workflowArg = requirePositional(parsed, 0, "workflow");
1174
1370
  const workflowId = await resolveRemoteWorkflowId(parsed);
1175
- const target = parsed.options.target ?? "draft";
1371
+ const target = parsed.options.target ?? "latest";
1372
+ if (target !== "draft") {
1373
+ const result2 = await fetchApi({
1374
+ server: parsed.options.server,
1375
+ token: parsed.options.token,
1376
+ path: `/api/workflows/${encodeURIComponent(workflowId)}/project-group-download?target=${encodeURIComponent(target)}`
1377
+ });
1378
+ assertApiOk(result2);
1379
+ if (result2.errCode !== 0 || result2.data === void 0) {
1380
+ printApiResponse(result2);
1381
+ return;
1382
+ }
1383
+ const repoRoot2 = resolveRepoRoot();
1384
+ const rootTargetDir = parsed.options.path ? path4.resolve(parsed.options.path) : path4.join(repoRoot2, "workspace", "workflow", workflowArg);
1385
+ const materialized = await materializeProjectGroupDownload(result2.data, rootTargetDir);
1386
+ const locallyAvailableProjectIds = new Set(materialized.map((project) => project.projectId));
1387
+ const skippedDependencies = result2.data.skippedDependencies.filter((item) => item.dependency.delivery !== "included" || !locallyAvailableProjectIds.has(item.dependency.projectId));
1388
+ const complete = !skippedDependencies.some((item) => item.dependency.delivery === "included");
1389
+ printApiResponse({
1390
+ ...result2,
1391
+ data: {
1392
+ ...result2.data,
1393
+ complete,
1394
+ copyAllowed: complete,
1395
+ skippedDependencies,
1396
+ projects: materialized
1397
+ }
1398
+ });
1399
+ return;
1400
+ }
1176
1401
  const result = await fetchApi({
1177
1402
  server: parsed.options.server,
1178
1403
  token: parsed.options.token,
@@ -1196,6 +1421,22 @@ async function commandDownload(parsed) {
1196
1421
  }
1197
1422
  async function commandPublish(parsed) {
1198
1423
  requireLogin(parsed);
1424
+ if (parsed.options.deployment !== void 0) {
1425
+ const publishResult2 = await fetchApi({
1426
+ server: parsed.options.server,
1427
+ token: parsed.options.token,
1428
+ path: `/api/deployments/${encodeURIComponent(parsed.options.deployment)}/publish`,
1429
+ method: "POST"
1430
+ });
1431
+ printApiResponse(publishResult2);
1432
+ assertApiOk(publishResult2);
1433
+ return;
1434
+ }
1435
+ if (shouldUseProjectGroupUpload(parsed.options)) {
1436
+ const workflowArg = requirePositional(parsed, 0, "workflow");
1437
+ await commandProjectGroupUpload(parsed, workflowArg, true);
1438
+ return;
1439
+ }
1199
1440
  const workflowId = await resolveRemoteWorkflowId(parsed);
1200
1441
  const publishResult = await fetchApi({
1201
1442
  server: parsed.options.server,
@@ -1210,6 +1451,279 @@ async function commandPublish(parsed) {
1210
1451
  printApiResponse(publishResult);
1211
1452
  assertApiOk(publishResult);
1212
1453
  }
1454
+ async function materializeProjectGroupDownload(download, rootTargetDir) {
1455
+ if (!isWorkflowUuid(download.rootProjectId)) {
1456
+ throw new Error(i18n.t("projectGroupDownloadInvalidRootId", { ns: "cli" }));
1457
+ }
1458
+ const dependencies = parseWorkflowProjectDependencyLocks(download.dependencies, { published: true });
1459
+ const includedDependencies = dependencies.filter((dependency) => dependency.delivery === "included");
1460
+ const includedProjectIds = new Set(includedDependencies.map((dependency) => dependency.projectId));
1461
+ const rootProjects = download.projects.filter((project) => project.role === "root");
1462
+ if (rootProjects.length !== 1 || rootProjects[0]?.projectId !== download.rootProjectId) {
1463
+ throw new Error(i18n.t("projectGroupDownloadRootCountInvalid", { ns: "cli" }));
1464
+ }
1465
+ const rootProject = rootProjects[0];
1466
+ if (rootProject.version !== download.rootVersion) {
1467
+ throw new Error(i18n.t("projectGroupDownloadRootVersionMismatch", { ns: "cli" }));
1468
+ }
1469
+ const rootPackageFile = rootProject.files.find((file) => normalizeWorkflowFilePath(file.path) === "package.json");
1470
+ if (rootPackageFile === void 0 || rootPackageFile.encoding === "base64") {
1471
+ throw new Error(i18n.t("projectGroupDownloadRootPackageUtf8", { ns: "cli" }));
1472
+ }
1473
+ let rootPackageJson;
1474
+ try {
1475
+ rootPackageJson = JSON.parse(rootPackageFile.content);
1476
+ } catch {
1477
+ throw new Error(i18n.t("projectGroupDownloadRootPackageInvalidJson", { ns: "cli" }));
1478
+ }
1479
+ const declaredSources = readKanbanArtifactConfig2(rootPackageJson).dataSources;
1480
+ const selfReference = declaredSources.find((source) => source.projectId === download.rootProjectId);
1481
+ if (selfReference !== void 0) {
1482
+ throw new Error(i18n.t("projectGroupDownloadSelfReference", {
1483
+ ns: "cli",
1484
+ sourceId: JSON.stringify(selfReference.id)
1485
+ }));
1486
+ }
1487
+ if (declaredSources.length !== dependencies.length || declaredSources.some((source) => {
1488
+ const dependency = dependencies.find((candidate) => candidate.sourceId === source.id);
1489
+ return dependency === void 0 || dependency.kind !== source.kind || dependency.projectId !== source.projectId || JSON.stringify(dependency.keyPrefixes) !== JSON.stringify(source.keyPrefixes);
1490
+ })) {
1491
+ throw new Error(i18n.t("projectGroupDownloadLockMismatch", { ns: "cli" }));
1492
+ }
1493
+ const responseProjectIds = /* @__PURE__ */ new Set();
1494
+ for (const project of download.projects) {
1495
+ if (!isWorkflowUuid(project.projectId) || responseProjectIds.has(project.projectId)) {
1496
+ throw new Error(i18n.t("projectGroupDownloadInvalidOrDuplicateProjectId", {
1497
+ ns: "cli",
1498
+ projectId: JSON.stringify(project.projectId)
1499
+ }));
1500
+ }
1501
+ responseProjectIds.add(project.projectId);
1502
+ const filePaths = /* @__PURE__ */ new Set();
1503
+ for (const file of project.files) {
1504
+ const safePath = normalizeWorkflowFilePath(file.path);
1505
+ if (isProjectGroupHostDataPath(safePath)) {
1506
+ throw new Error(i18n.t("projectGroupDownloadForbiddenHostData", {
1507
+ ns: "cli",
1508
+ path: JSON.stringify(file.path)
1509
+ }));
1510
+ }
1511
+ const fileKey = createPortableLocalPathKey(safePath);
1512
+ if (filePaths.has(fileKey)) {
1513
+ throw new Error(i18n.t("projectGroupDownloadDuplicateFile", {
1514
+ ns: "cli",
1515
+ path: JSON.stringify(safePath)
1516
+ }));
1517
+ }
1518
+ filePaths.add(fileKey);
1519
+ }
1520
+ const packageFile = project.files.find((file) => normalizeWorkflowFilePath(file.path) === "package.json");
1521
+ if (packageFile === void 0 || packageFile.encoding === "base64") {
1522
+ throw new Error(i18n.t("projectGroupDownloadPackageUtf8", { ns: "cli", role: project.role }));
1523
+ }
1524
+ let packageJson;
1525
+ try {
1526
+ packageJson = JSON.parse(packageFile.content);
1527
+ } catch {
1528
+ throw new Error(i18n.t("projectGroupDownloadPackageInvalidJson", { ns: "cli", role: project.role }));
1529
+ }
1530
+ if (packageJson["id"] !== project.projectId) {
1531
+ throw new Error(i18n.t("projectGroupDownloadedPackageIdMismatch", {
1532
+ ns: "cli",
1533
+ projectId: JSON.stringify(project.projectId)
1534
+ }));
1535
+ }
1536
+ if (hashWorkflowSourceFiles(project.files) !== project.sourceHash) {
1537
+ throw new Error(i18n.t("projectGroupDownloadSourceHashMismatch", {
1538
+ ns: "cli",
1539
+ role: project.role,
1540
+ projectId: JSON.stringify(project.projectId)
1541
+ }));
1542
+ }
1543
+ if (project.role === "root" && project.sourceId !== void 0) {
1544
+ throw new Error(i18n.t("projectGroupDownloadRootSourceIdForbidden", { ns: "cli" }));
1545
+ }
1546
+ if (project.role === "dependency") {
1547
+ const matchingDependencies = includedDependencies.filter((dependency) => dependency.projectId === project.projectId && dependency.version === project.version && dependency.sourceHash === project.sourceHash);
1548
+ if (matchingDependencies.length === 0 || project.sourceId !== void 0 && !matchingDependencies.some((dependency) => dependency.sourceId === project.sourceId)) {
1549
+ throw new Error(i18n.t("projectGroupDownloadUndeclaredDependency", {
1550
+ ns: "cli",
1551
+ projectId: JSON.stringify(project.projectId)
1552
+ }));
1553
+ }
1554
+ }
1555
+ }
1556
+ const parentDir = path4.dirname(rootTargetDir);
1557
+ const existingById = await findSiblingProjectsById(parentDir);
1558
+ const materialized = [];
1559
+ const pending = [];
1560
+ const targetKeys = /* @__PURE__ */ new Set();
1561
+ const orderedProjects = [...download.projects].sort((left, right) => left.role === right.role ? left.projectId.localeCompare(right.projectId) : left.role === "root" ? -1 : 1);
1562
+ for (const project of orderedProjects) {
1563
+ const existingPath = existingById.get(project.projectId);
1564
+ if (existingPath !== void 0) {
1565
+ await assertDownloadedProjectRole(existingPath, project.projectId, project.role);
1566
+ materialized.push({
1567
+ ...project.sourceId === void 0 ? {} : { sourceId: project.sourceId },
1568
+ projectId: project.projectId,
1569
+ projectName: project.projectName,
1570
+ role: project.role,
1571
+ path: existingPath,
1572
+ alreadyAvailable: true
1573
+ });
1574
+ continue;
1575
+ }
1576
+ const targetDir = project.role === "root" ? rootTargetDir : path4.join(parentDir, createProjectDirectoryName(project.projectName, project.projectId));
1577
+ if (existsSync2(targetDir)) {
1578
+ throw new Error(i18n.t("projectGroupDownloadDirectoryProjectMismatch", {
1579
+ ns: "cli",
1580
+ projectName: JSON.stringify(project.projectName),
1581
+ path: JSON.stringify(targetDir)
1582
+ }));
1583
+ }
1584
+ const targetKey = createPortableLocalPathKey(path4.resolve(targetDir));
1585
+ if (targetKeys.has(targetKey)) {
1586
+ throw new Error(i18n.t("projectGroupDownloadDirectoryCollision", {
1587
+ ns: "cli",
1588
+ projectName: JSON.stringify(project.projectName),
1589
+ path: JSON.stringify(targetDir)
1590
+ }));
1591
+ }
1592
+ targetKeys.add(targetKey);
1593
+ pending.push({ project, targetDir });
1594
+ }
1595
+ for (const projectId of includedProjectIds) {
1596
+ if (responseProjectIds.has(projectId)) continue;
1597
+ const existingPath = existingById.get(projectId);
1598
+ if (existingPath === void 0) continue;
1599
+ const packageJson = await assertDownloadedProjectRole(existingPath, projectId, "dependency");
1600
+ materialized.push({
1601
+ projectId,
1602
+ projectName: readRequiredPackageString(packageJson, "name"),
1603
+ role: "dependency",
1604
+ path: existingPath,
1605
+ alreadyAvailable: true
1606
+ });
1607
+ }
1608
+ await mkdir3(parentDir, { recursive: true });
1609
+ const stagingRoot = path4.join(parentDir, `.workflow-download-group-${randomUUID()}`);
1610
+ const staged = [];
1611
+ const moved = [];
1612
+ try {
1613
+ await mkdir3(stagingRoot, { recursive: false });
1614
+ for (const item of pending) {
1615
+ const { project, targetDir } = item;
1616
+ const stagingDir = path4.join(stagingRoot, project.projectId);
1617
+ await mkdir3(stagingDir, { recursive: true });
1618
+ for (const file of project.files) {
1619
+ const safePath = normalizeWorkflowFilePath(file.path);
1620
+ const targetPath = path4.join(stagingDir, ...safePath.split("/"));
1621
+ await mkdir3(path4.dirname(targetPath), { recursive: true });
1622
+ await writeFile2(targetPath, decodeWorkflowFileContent(file));
1623
+ }
1624
+ const packageJson = await readPackageJsonFile(path4.join(stagingDir, "package.json"));
1625
+ if (packageJson["id"] !== project.projectId) {
1626
+ throw new Error(i18n.t("projectGroupDownloadedPackageIdMismatch", {
1627
+ ns: "cli",
1628
+ projectId: JSON.stringify(project.projectId)
1629
+ }));
1630
+ }
1631
+ await assertDownloadedProjectRole(stagingDir, project.projectId, project.role, packageJson);
1632
+ staged.push({ project, targetDir, stagingDir });
1633
+ }
1634
+ for (const item of staged) {
1635
+ await rename(item.stagingDir, item.targetDir);
1636
+ moved.push(item.targetDir);
1637
+ existingById.set(item.project.projectId, item.targetDir);
1638
+ materialized.push({
1639
+ ...item.project.sourceId === void 0 ? {} : { sourceId: item.project.sourceId },
1640
+ projectId: item.project.projectId,
1641
+ projectName: item.project.projectName,
1642
+ role: item.project.role,
1643
+ path: item.targetDir,
1644
+ alreadyAvailable: false
1645
+ });
1646
+ }
1647
+ } catch (error) {
1648
+ for (const targetDir of moved) {
1649
+ await rm2(targetDir, { recursive: true, force: true }).catch(() => void 0);
1650
+ }
1651
+ throw error;
1652
+ } finally {
1653
+ await rm2(stagingRoot, { recursive: true, force: true }).catch(() => void 0);
1654
+ }
1655
+ return materialized.sort((left, right) => left.role === right.role ? left.projectId.localeCompare(right.projectId) : left.role === "root" ? -1 : 1);
1656
+ }
1657
+ async function assertDownloadedProjectRole(projectDir, projectId, role, packageJson) {
1658
+ const metadata = packageJson ?? await readPackageJsonFile(path4.join(projectDir, "package.json"));
1659
+ if (metadata["id"] !== projectId) {
1660
+ throw new Error(i18n.t("projectGroupDownloadedPackageIdMismatch", {
1661
+ ns: "cli",
1662
+ projectId: JSON.stringify(projectId)
1663
+ }));
1664
+ }
1665
+ const projectType = inspectLocalWorkflowStructure({ workflowDir: projectDir }).projectType;
1666
+ const expectedProjectType = role === "root" ? "kanban" : "workflow";
1667
+ if (projectType !== expectedProjectType) {
1668
+ throw new Error(i18n.t("projectGroupDownloadedProjectTypeMismatch", {
1669
+ ns: "cli",
1670
+ projectId: JSON.stringify(projectId),
1671
+ projectType: expectedProjectType
1672
+ }));
1673
+ }
1674
+ return metadata;
1675
+ }
1676
+ function createPortableLocalPathKey(filePath) {
1677
+ return filePath.normalize("NFC").toLowerCase();
1678
+ }
1679
+ function resolveExplicitDataGrants(dependencies, sourceIds) {
1680
+ const selected = /* @__PURE__ */ new Set();
1681
+ for (const sourceId of sourceIds) {
1682
+ if (selected.has(sourceId)) {
1683
+ throw new Error(i18n.t("duplicateGrantDataAccessSource", {
1684
+ ns: "cli",
1685
+ sourceId: JSON.stringify(sourceId)
1686
+ }));
1687
+ }
1688
+ selected.add(sourceId);
1689
+ }
1690
+ const grants = /* @__PURE__ */ new Map();
1691
+ for (const sourceId of selected) {
1692
+ const dependency = dependencies.find((candidate) => candidate.sourceId === sourceId);
1693
+ if (dependency === void 0) {
1694
+ throw new Error(i18n.t("unknownGrantDataAccessSource", {
1695
+ ns: "cli",
1696
+ sourceId: JSON.stringify(sourceId)
1697
+ }));
1698
+ }
1699
+ grants.set(dependency.projectId, [
1700
+ ...grants.get(dependency.projectId) ?? [],
1701
+ ...dependency.keyPrefixes
1702
+ ]);
1703
+ }
1704
+ for (const [projectId, prefixes] of grants) {
1705
+ grants.set(projectId, [...new Set(prefixes)]);
1706
+ }
1707
+ return grants;
1708
+ }
1709
+ async function findSiblingProjectsById(parentDir) {
1710
+ const projects = /* @__PURE__ */ new Map();
1711
+ const entries = await readdir2(parentDir, { withFileTypes: true }).catch(() => []);
1712
+ for (const entry of entries) {
1713
+ if (!entry.isDirectory()) continue;
1714
+ const projectDir = path4.join(parentDir, entry.name);
1715
+ const packageJson = await readPackageJsonFile(path4.join(projectDir, "package.json")).catch(() => void 0);
1716
+ const projectId = packageJson?.["id"];
1717
+ if (typeof projectId === "string" && isWorkflowUuid(projectId) && !projects.has(projectId)) {
1718
+ projects.set(projectId, projectDir);
1719
+ }
1720
+ }
1721
+ return projects;
1722
+ }
1723
+ function createProjectDirectoryName(projectName, projectId) {
1724
+ const normalized = projectName.normalize("NFC").replace(/^@/, "").replace(/[\\/:*?"<>|\u0000-\u001f]/g, "-").replace(/^\.+|\.+$/g, "").trim();
1725
+ return normalized === "" ? projectId : normalized;
1726
+ }
1213
1727
  async function commandHealth(parsed) {
1214
1728
  const result = await fetchApi({
1215
1729
  server: parsed.options.server,
@@ -1220,7 +1734,7 @@ async function commandHealth(parsed) {
1220
1734
  printApiResponse(result);
1221
1735
  assertApiOk(result);
1222
1736
  }
1223
- async function packWorkflow(workflowName, outputPath, sourcePath) {
1737
+ async function packWorkflow(workflowName, outputPath, sourcePath, options = {}) {
1224
1738
  const repoRoot = resolveRepoRoot();
1225
1739
  const workflowDir = sourcePath === void 0 ? path4.join(repoRoot, "workspace", "workflow", workflowName) : path4.resolve(sourcePath);
1226
1740
  await readPackageJsonFile(path4.join(workflowDir, "package.json"));
@@ -1235,17 +1749,166 @@ async function packWorkflow(workflowName, outputPath, sourcePath) {
1235
1749
  workspaceRoot,
1236
1750
  workflowDir,
1237
1751
  workflowName,
1238
- stagingRoot: path4.join(stagingDir, "workspace", "workflow")
1752
+ stagingRoot: path4.join(stagingDir, "workspace", "workflow"),
1753
+ projectGroupSafe: options.projectGroupSafe === true
1239
1754
  });
1240
1755
  const result = await runCommand("tar", ["-czf", archivePath, "-C", stagingDir, "."], {
1241
1756
  cwd: repoRoot
1242
1757
  });
1243
1758
  await rm2(path4.dirname(stagingDir), { recursive: true, force: true });
1244
1759
  if (result.exitCode !== 0) {
1245
- throw new Error(`Failed to create archive: ${result.stderr || result.stdout}`);
1760
+ throw new Error(i18n.t("archiveCreateFailed", {
1761
+ ns: "cli",
1762
+ details: result.stderr || result.stdout
1763
+ }));
1246
1764
  }
1247
1765
  return archivePath;
1248
1766
  }
1767
+ async function readProjectGroupContexts(options) {
1768
+ const root = await readWorkflowPackageContext(options.workflowArg, options.sourcePath);
1769
+ if (root.projectType !== "kanban") {
1770
+ throw new Error(i18n.t("projectGroupRootMustBeKanban", { ns: "cli" }));
1771
+ }
1772
+ if (!root.workflowId) {
1773
+ throw new Error(i18n.t("projectGroupRootIdRequired", { ns: "cli" }));
1774
+ }
1775
+ const dataSources = readKanbanArtifactConfig2(root.packageJson).dataSources;
1776
+ const selfReference = dataSources.find((source) => source.projectId === root.workflowId);
1777
+ if (selfReference !== void 0) {
1778
+ throw new Error(i18n.t("projectGroupSelfReference", {
1779
+ ns: "cli",
1780
+ sourceId: JSON.stringify(selfReference.id)
1781
+ }));
1782
+ }
1783
+ const dependencyPaths = /* @__PURE__ */ new Map();
1784
+ for (const option of options.dependencyOptions) {
1785
+ const separator = option.indexOf("=");
1786
+ const sourceId = separator < 1 ? "" : option.slice(0, separator).trim();
1787
+ const dependencyPath = separator < 0 ? "" : option.slice(separator + 1).trim();
1788
+ if (sourceId === "" || dependencyPath === "") {
1789
+ throw new Error(i18n.t("invalidDependencyOption", {
1790
+ ns: "cli",
1791
+ option: JSON.stringify(option)
1792
+ }));
1793
+ }
1794
+ if (dependencyPaths.has(sourceId)) {
1795
+ throw new Error(i18n.t("duplicateDependencySource", {
1796
+ ns: "cli",
1797
+ sourceId: JSON.stringify(sourceId)
1798
+ }));
1799
+ }
1800
+ dependencyPaths.set(sourceId, path4.resolve(dependencyPath));
1801
+ }
1802
+ const locks = createWorkflowProjectDependencyLocks(
1803
+ dataSources,
1804
+ [...dependencyPaths.keys()].map((sourceId) => ({ sourceId, delivery: "included" }))
1805
+ );
1806
+ const dependenciesByProject = /* @__PURE__ */ new Map();
1807
+ for (const [sourceId, dependencyPath] of dependencyPaths) {
1808
+ const source = dataSources.find((candidate) => candidate.id === sourceId);
1809
+ const context = await readWorkflowPackageContext(sourceId, dependencyPath);
1810
+ if (context.projectType !== "workflow") {
1811
+ throw new Error(i18n.t("dependencyMustBeWorkflow", {
1812
+ ns: "cli",
1813
+ sourceId: JSON.stringify(sourceId)
1814
+ }));
1815
+ }
1816
+ if (context.workflowId !== source.projectId) {
1817
+ throw new Error(i18n.t("dependencyPackageIdMismatch", {
1818
+ ns: "cli",
1819
+ sourceId: JSON.stringify(sourceId),
1820
+ projectId: JSON.stringify(source.projectId)
1821
+ }));
1822
+ }
1823
+ const existing = dependenciesByProject.get(source.projectId);
1824
+ if (existing !== void 0 && existing.workflowDir !== context.workflowDir) {
1825
+ throw new Error(i18n.t("dependencyProjectPathConflict", {
1826
+ ns: "cli",
1827
+ projectId: JSON.stringify(source.projectId)
1828
+ }));
1829
+ }
1830
+ dependenciesByProject.set(source.projectId, context);
1831
+ }
1832
+ return { root, dependencies: [...dependenciesByProject.values()], locks };
1833
+ }
1834
+ async function packProjectGroup(options) {
1835
+ const contexts = options.contexts ?? await readProjectGroupContexts(options);
1836
+ const repoRoot = resolveRepoRoot();
1837
+ const packsDir = process.env.WORKFLOW_WORKSPACE_PACKS_DIR ? path4.resolve(process.env.WORKFLOW_WORKSPACE_PACKS_DIR) : path4.join(repoRoot, "workspace", ".packs");
1838
+ const groupRoot = path4.join(packsDir, "tmp", `project-group-${randomUUID()}`);
1839
+ const projectsDir = path4.join(groupRoot, "projects");
1840
+ const archivePath = options.outputPath ? path4.resolve(options.outputPath) : path4.join(packsDir, `project-group-${createTimestamp()}.tgz`);
1841
+ await mkdir3(projectsDir, { recursive: true });
1842
+ await mkdir3(path4.dirname(archivePath), { recursive: true });
1843
+ const rootArchivePath = `projects/${contexts.root.workflowId}.tgz`;
1844
+ const projects = [{
1845
+ projectId: contexts.root.workflowId,
1846
+ role: "root",
1847
+ archivePath: rootArchivePath
1848
+ }];
1849
+ try {
1850
+ await packWorkflow(
1851
+ contexts.root.workflowArg,
1852
+ path4.join(groupRoot, ...rootArchivePath.split("/")),
1853
+ contexts.root.workflowDir,
1854
+ { projectGroupSafe: true }
1855
+ );
1856
+ for (const dependency of contexts.dependencies) {
1857
+ const dependencyArchivePath = `projects/${dependency.workflowId}.tgz`;
1858
+ projects.push({
1859
+ projectId: dependency.workflowId,
1860
+ role: "dependency",
1861
+ archivePath: dependencyArchivePath
1862
+ });
1863
+ await packWorkflow(
1864
+ dependency.workflowArg,
1865
+ path4.join(groupRoot, ...dependencyArchivePath.split("/")),
1866
+ dependency.workflowDir,
1867
+ { projectGroupSafe: true }
1868
+ );
1869
+ }
1870
+ const manifest = {
1871
+ version: 1,
1872
+ rootProjectId: contexts.root.workflowId,
1873
+ projects,
1874
+ dependencies: contexts.locks
1875
+ };
1876
+ await writeFile2(path4.join(groupRoot, "project-group.json"), `${JSON.stringify(manifest, null, 2)}
1877
+ `);
1878
+ const result = await runCommand("tar", ["-czf", archivePath, "-C", groupRoot, "."], { cwd: repoRoot });
1879
+ if (result.exitCode !== 0) {
1880
+ throw new Error(i18n.t("projectGroupArchiveCreateFailed", {
1881
+ ns: "cli",
1882
+ details: result.stderr || result.stdout
1883
+ }));
1884
+ }
1885
+ return {
1886
+ archivePath,
1887
+ manifest,
1888
+ root: contexts.root,
1889
+ dependencies: contexts.dependencies
1890
+ };
1891
+ } finally {
1892
+ await rm2(groupRoot, { recursive: true, force: true });
1893
+ }
1894
+ }
1895
+ async function uploadProjectGroupPackage(options) {
1896
+ const archiveBuffer = await readFile3(options.archivePath);
1897
+ const archiveName = path4.basename(options.archivePath);
1898
+ const response = await fetch(
1899
+ `${trimTrailingSlash(options.server)}/api/deployments/${encodeURIComponent(options.deploymentId)}/package?fileName=${encodeURIComponent(archiveName)}`,
1900
+ {
1901
+ method: "POST",
1902
+ headers: {
1903
+ authorization: `Bearer ${options.token}`,
1904
+ "content-type": "application/octet-stream",
1905
+ "x-workflow-package-name": archiveName
1906
+ },
1907
+ body: new Blob([archiveBuffer])
1908
+ }
1909
+ );
1910
+ return response.json();
1911
+ }
1249
1912
  async function resolveRemoteWorkflowId(parsed) {
1250
1913
  const workflowArg = requirePositional(parsed, 0, "workflow");
1251
1914
  if (isWorkflowUuid(workflowArg)) {
@@ -1255,7 +1918,10 @@ async function resolveRemoteWorkflowId(parsed) {
1255
1918
  if (packageContext.workflowId) {
1256
1919
  return packageContext.workflowId;
1257
1920
  }
1258
- throw new Error(`Workflow "${workflowArg}" is not bound to a server UUID. Create or bind it first with \`workflow-code workspace upload ${workflowArg} --create\`.`);
1921
+ throw new Error(i18n.t("workflowNotBound", {
1922
+ ns: "cli",
1923
+ workflow: workflowArg
1924
+ }));
1259
1925
  }
1260
1926
  async function readWorkflowPackageContext(workflowArg, sourcePath) {
1261
1927
  const repoRoot = resolveRepoRoot();
@@ -1264,12 +1930,14 @@ async function readWorkflowPackageContext(workflowArg, sourcePath) {
1264
1930
  const packageJson = await readPackageJsonFile(packageJsonPath);
1265
1931
  const workflowId = typeof packageJson.id === "string" && isWorkflowUuid(packageJson.id) ? packageJson.id.trim() : void 0;
1266
1932
  const workflowName = readRequiredPackageString(packageJson, "name");
1933
+ const projectType = inspectLocalWorkflowStructure({ workflowDir }).projectType;
1267
1934
  return {
1268
1935
  workflowArg,
1269
1936
  workflowDir,
1270
1937
  packageJsonPath,
1271
1938
  workflowId,
1272
1939
  workflowName,
1940
+ projectType,
1273
1941
  packageJson
1274
1942
  };
1275
1943
  }
@@ -1281,13 +1949,14 @@ async function ensureWorkflowProjectCreated(options) {
1281
1949
  method: "POST",
1282
1950
  body: {
1283
1951
  name: options.packageContext.workflowName,
1952
+ projectType: options.packageContext.projectType,
1284
1953
  ...options.packageContext.workflowId ? { workflowId: options.packageContext.workflowId } : {}
1285
1954
  }
1286
1955
  });
1287
1956
  if (created.errCode !== 0 || created.data === void 0) {
1288
1957
  printApiResponse(created);
1289
1958
  assertApiOk(created);
1290
- throw new Error("Workflow project creation failed.");
1959
+ throw new Error(i18n.t("projectCreationFailed", { ns: "cli" }));
1291
1960
  }
1292
1961
  if (!options.packageContext.workflowId || options.packageContext.workflowId !== created.data.workflowId) {
1293
1962
  await writeWorkflowPackageId(options.packageContext.packageJsonPath, created.data.workflowId);
@@ -1298,20 +1967,20 @@ async function readPackageJsonFile(packageJsonPath) {
1298
1967
  try {
1299
1968
  const raw = JSON.parse(await readFile3(packageJsonPath, "utf8"));
1300
1969
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1301
- throw new Error(`Workflow package "${packageJsonPath}" must contain a JSON object.`);
1970
+ throw new Error(i18n.t("packageMustBeObject", { ns: "cli", path: packageJsonPath }));
1302
1971
  }
1303
1972
  return raw;
1304
1973
  } catch (error) {
1305
1974
  if (error instanceof Error) {
1306
1975
  throw error;
1307
1976
  }
1308
- throw new Error(`Unable to read workflow package "${packageJsonPath}".`);
1977
+ throw new Error(i18n.t("packageReadFailed", { ns: "cli", path: packageJsonPath }));
1309
1978
  }
1310
1979
  }
1311
1980
  function readRequiredPackageString(packageJson, key) {
1312
1981
  const value = packageJson[key];
1313
1982
  if (typeof value !== "string" || value.trim() === "") {
1314
- throw new Error(`Workflow package.json must include string "${key}".`);
1983
+ throw new Error(i18n.t("packageStringRequired", { ns: "cli", key }));
1315
1984
  }
1316
1985
  return value.trim();
1317
1986
  }
@@ -1336,7 +2005,8 @@ async function uploadPackage(options) {
1336
2005
  headers: {
1337
2006
  authorization: `Bearer ${options.token}`,
1338
2007
  "content-type": "application/octet-stream",
1339
- "x-workflow-package-name": archiveName
2008
+ "x-workflow-package-name": archiveName,
2009
+ "x-workflow-locale": getCurrentLocale()
1340
2010
  },
1341
2011
  body: new Blob([archiveBuffer])
1342
2012
  }
@@ -1356,7 +2026,11 @@ async function waitForPreparation(options) {
1356
2026
  }
1357
2027
  if (response.data.stage !== previousStage) {
1358
2028
  previousStage = response.data.stage;
1359
- process.stderr.write(`Preparation ${response.data.status}: ${response.data.stage}
2029
+ process.stderr.write(`${i18n.t("preparationProgress", {
2030
+ ns: "cli",
2031
+ status: response.data.status,
2032
+ stage: response.data.stage
2033
+ })}
1360
2034
  `);
1361
2035
  }
1362
2036
  if (response.data.status === "success") {
@@ -1391,6 +2065,7 @@ async function fetchApi(options) {
1391
2065
  if (options.body !== void 0) {
1392
2066
  headers["content-type"] = "application/json";
1393
2067
  }
2068
+ headers["x-workflow-locale"] = getCurrentLocale();
1394
2069
  const response = await fetch(`${trimTrailingSlash(options.server)}${options.path}`, {
1395
2070
  method: options.method ?? "GET",
1396
2071
  headers,
@@ -1398,8 +2073,29 @@ async function fetchApi(options) {
1398
2073
  });
1399
2074
  return response.json();
1400
2075
  }
1401
- async function copyWorkflowSource(sourceDir, targetDir) {
1402
- return copyWorkflowUploadFiles(sourceDir, targetDir);
2076
+ async function copyWorkflowSource(sourceDir, targetDir, options = {}) {
2077
+ if (options.projectGroupSafe !== true) return copyWorkflowUploadFiles(sourceDir, targetDir);
2078
+ const plan = await createWorkflowUploadPlan(sourceDir);
2079
+ const files = plan.files.filter((file) => !isProjectGroupHostDataPath(file.path));
2080
+ const forbiddenRequiredPath = plan.requiredPaths.find((requiredPath) => isProjectGroupHostDataPath(requiredPath));
2081
+ if (forbiddenRequiredPath !== void 0) {
2082
+ throw new Error(i18n.t("projectGroupRequiredPathForbidden", {
2083
+ ns: "cli",
2084
+ path: JSON.stringify(forbiddenRequiredPath)
2085
+ }));
2086
+ }
2087
+ await mkdir3(targetDir, { recursive: true });
2088
+ for (const file of files) {
2089
+ const targetPath = path4.join(targetDir, ...file.path.split("/"));
2090
+ await mkdir3(path4.dirname(targetPath), { recursive: true });
2091
+ await copyFile2(file.absolutePath, targetPath);
2092
+ }
2093
+ return {
2094
+ ...plan,
2095
+ files,
2096
+ ignoredPaths: [...plan.ignoredPaths, ...plan.files.filter((file) => isProjectGroupHostDataPath(file.path)).map((file) => file.path)].sort((left, right) => left.localeCompare(right)),
2097
+ totalBytes: files.reduce((total, file) => total + file.size, 0)
2098
+ };
1403
2099
  }
1404
2100
  async function stageWorkflowWithLocalDependencies(options) {
1405
2101
  const queue = [options.workflowName];
@@ -1412,20 +2108,25 @@ async function stageWorkflowWithLocalDependencies(options) {
1412
2108
  visited.add(currentWorkflowName);
1413
2109
  const sourceDir = currentWorkflowName === options.workflowName ? options.workflowDir : path4.join(options.workspaceRoot, currentWorkflowName);
1414
2110
  if (!existsSync2(path4.join(sourceDir, "package.json"))) {
1415
- throw new Error(
1416
- `Workflow "${options.workflowName}" references local workflow "${currentWorkflowName}", but ${sourceDir} is missing package.json.`
1417
- );
2111
+ throw new Error(i18n.t("localDependencyMissingPackage", {
2112
+ ns: "cli",
2113
+ workflow: options.workflowName,
2114
+ dependency: currentWorkflowName,
2115
+ path: sourceDir
2116
+ }));
1418
2117
  }
1419
2118
  const uploadPlan = await copyWorkflowSource(
1420
2119
  sourceDir,
1421
- path4.join(options.stagingRoot, currentWorkflowName)
2120
+ path4.join(options.stagingRoot, currentWorkflowName),
2121
+ { projectGroupSafe: options.projectGroupSafe }
1422
2122
  );
1423
2123
  if (currentWorkflowName === options.workflowName && !uploadPlan.workflowIgnoreConfigured) {
1424
- process.stderr.write([
1425
- "Warning: .workflowignore is missing or has no active rules; all regular project files will be uploaded.",
1426
- `.gitignore is not used. Files: ${uploadPlan.files.length}; bytes: ${uploadPlan.totalBytes}.`,
1427
- ""
1428
- ].join("\n"));
2124
+ process.stderr.write(`${i18n.t("workflowIgnoreWarning", {
2125
+ ns: "cli",
2126
+ files: uploadPlan.files.length,
2127
+ bytes: uploadPlan.totalBytes
2128
+ })}
2129
+ `);
1429
2130
  }
1430
2131
  for (const dependency of await findLocalWorkflowDependencies(sourceDir)) {
1431
2132
  if (!visited.has(dependency)) {
@@ -1460,7 +2161,7 @@ async function findLocalWorkflowDependencies(sourceDir) {
1460
2161
  function normalizeWorkflowFilePath(filePath) {
1461
2162
  const normalized = filePath.replaceAll("\\", "/").replace(/^\.\/+/, "");
1462
2163
  if (normalized === "" || normalized.startsWith("/") || /^[a-zA-Z]:/.test(normalized) || normalized.split("/").some((part) => part === "" || part === "." || part === "..")) {
1463
- throw new Error(`Invalid workflow file path "${filePath}".`);
2164
+ throw new Error(i18n.t("invalidWorkflowFilePath", { ns: "cli", path: filePath }));
1464
2165
  }
1465
2166
  return normalized;
1466
2167
  }
@@ -1469,28 +2170,48 @@ function decodeWorkflowFileContent(file) {
1469
2170
  return file.content;
1470
2171
  }
1471
2172
  if (file.encoding !== "base64") {
1472
- throw new Error(`Unsupported encoding for workflow file "${file.path}".`);
2173
+ throw new Error(i18n.t("unsupportedWorkflowEncoding", { ns: "cli", path: file.path }));
1473
2174
  }
1474
2175
  const normalized = file.content.replace(/=+$/, "");
1475
2176
  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(file.content)) {
1476
- throw new Error(`Invalid base64 content for workflow file "${file.path}".`);
2177
+ throw new Error(i18n.t("invalidBase64Content", { ns: "cli", path: file.path }));
1477
2178
  }
1478
2179
  const decoded = Buffer.from(file.content, "base64");
1479
2180
  if (decoded.toString("base64").replace(/=+$/, "") !== normalized) {
1480
- throw new Error(`Invalid base64 content for workflow file "${file.path}".`);
2181
+ throw new Error(i18n.t("invalidBase64Content", { ns: "cli", path: file.path }));
1481
2182
  }
1482
2183
  return decoded;
1483
2184
  }
2185
+ function hashWorkflowSourceFiles(files) {
2186
+ const hash = createHash("sha256");
2187
+ const normalizedFiles = files.map((file) => ({
2188
+ file,
2189
+ path: normalizeWorkflowFilePath(file.path)
2190
+ })).sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
2191
+ for (const item of normalizedFiles) {
2192
+ hash.update(item.path);
2193
+ hash.update("\0");
2194
+ hash.update(decodeWorkflowFileContent(item.file));
2195
+ hash.update("\0");
2196
+ }
2197
+ return hash.digest("hex");
2198
+ }
1484
2199
  function parseArgs(argv) {
1485
2200
  const [command = "", ...rest] = argv;
1486
2201
  const positional = [];
1487
2202
  const authConfig = readCliAuthConfigSync();
1488
2203
  const options = {
1489
2204
  server: process.env.WORKFLOW_SERVER_URL ?? authConfig.serverUrl ?? DEFAULT_SERVER_URL,
1490
- token: process.env.WORKFLOW_SERVER_ADMIN_KEY ?? authConfig.apiKey ?? ""
2205
+ token: process.env.WORKFLOW_SERVER_ADMIN_KEY ?? authConfig.apiKey ?? "",
2206
+ dependencies: [],
2207
+ dataGrants: []
1491
2208
  };
1492
2209
  for (let index = 0; index < rest.length; index += 1) {
1493
2210
  const arg = rest[index];
2211
+ if (arg === "--") {
2212
+ positional.push(...rest.slice(index + 1));
2213
+ break;
2214
+ }
1494
2215
  switch (arg) {
1495
2216
  case "--server":
1496
2217
  options.server = requireValue(rest, index += 1, "--server");
@@ -1501,8 +2222,11 @@ function parseArgs(argv) {
1501
2222
  case "--target":
1502
2223
  options.target = requireValue(rest, index += 1, "--target");
1503
2224
  break;
2225
+ case "--entrypoint":
2226
+ options.entrypoint = requireValue(rest, index += 1, "--entrypoint");
2227
+ break;
1504
2228
  case "--version":
1505
- throw new Error("--version is no longer supported. Publish versions are managed by the system.");
2229
+ throw new Error(i18n.t("versionOptionUnsupported", { ns: "cli" }));
1506
2230
  case "--release-log":
1507
2231
  options.releaseLog = requireValue(rest, index += 1, "--release-log");
1508
2232
  break;
@@ -1521,10 +2245,25 @@ function parseArgs(argv) {
1521
2245
  case "--no-wait":
1522
2246
  options.noWait = true;
1523
2247
  break;
2248
+ case "--dependency":
2249
+ options.dependencies.push(requireValue(rest, index += 1, "--dependency"));
2250
+ break;
2251
+ case "--grant-data-access":
2252
+ options.dataGrants.push(requireValue(rest, index += 1, "--grant-data-access"));
2253
+ break;
2254
+ case "--project-group":
2255
+ options.projectGroup = true;
2256
+ break;
2257
+ case "--publish-group":
2258
+ options.publishGroup = true;
2259
+ break;
2260
+ case "--deployment":
2261
+ options.deployment = requireValue(rest, index += 1, "--deployment");
2262
+ break;
1524
2263
  case "--source-mode": {
1525
2264
  const value = requireValue(rest, index += 1, "--source-mode");
1526
2265
  if (value !== "bundled" && value !== "source") {
1527
- throw new Error('--source-mode must be "bundled" or "source".');
2266
+ throw new Error(i18n.t("invalidSourceMode", { ns: "cli" }));
1528
2267
  }
1529
2268
  options.sourceMode = value;
1530
2269
  break;
@@ -1535,7 +2274,31 @@ function parseArgs(argv) {
1535
2274
  }
1536
2275
  }
1537
2276
  if (command === "upload" && options.noWait === true && options.releaseLog !== void 0) {
1538
- throw new Error("--no-wait cannot be combined with --release-log because publishing requires a prepared draft.");
2277
+ throw new Error(i18n.t("noWaitReleaseLogConflict", { ns: "cli" }));
2278
+ }
2279
+ if (options.noWait === true && command !== "upload") {
2280
+ throw new Error(i18n.t("noWaitCommandUnsupported", { ns: "cli" }));
2281
+ }
2282
+ if (command === "upload" && options.noWait === true && shouldUseProjectGroupUpload(options)) {
2283
+ throw new Error(i18n.t("noWaitProjectGroupUnsupported", { ns: "cli" }));
2284
+ }
2285
+ if (options.dependencies.length > 0 && command !== "pack" && command !== "upload" && command !== "publish") {
2286
+ throw new Error(i18n.t("dependencyOptionCommandUnsupported", { ns: "cli" }));
2287
+ }
2288
+ if (options.dataGrants.length > 0 && command !== "upload" && command !== "publish") {
2289
+ throw new Error(i18n.t("grantDataAccessCommandUnsupported", { ns: "cli" }));
2290
+ }
2291
+ if (options.deployment !== void 0 && command !== "publish") {
2292
+ throw new Error(i18n.t("deploymentCommandUnsupported", { ns: "cli" }));
2293
+ }
2294
+ if (options.publishGroup === true && command !== "upload") {
2295
+ throw new Error(i18n.t("publishGroupCommandUnsupported", { ns: "cli" }));
2296
+ }
2297
+ if (options.projectGroup === true && command !== "pack" && command !== "upload" && command !== "publish") {
2298
+ throw new Error(i18n.t("projectGroupCommandUnsupported", { ns: "cli" }));
2299
+ }
2300
+ if (options.deployment !== void 0 && (options.projectGroup === true || options.dependencies.length > 0 || options.dataGrants.length > 0 || options.path !== void 0 || options.file !== void 0 || options.output !== void 0 || options.create === true || options.releaseLog !== void 0 || options.sourceMode !== void 0)) {
2301
+ throw new Error(i18n.t("deploymentOptionsConflict", { ns: "cli" }));
1539
2302
  }
1540
2303
  return {
1541
2304
  command,
@@ -1545,20 +2308,20 @@ function parseArgs(argv) {
1545
2308
  }
1546
2309
  function requireLogin(parsed) {
1547
2310
  if (parsed.options.server.trim() === "" || parsed.options.token.trim() === "") {
1548
- throw new Error("Not logged in. Run: workspace login --server <url> or pass --token / WORKFLOW_SERVER_ADMIN_KEY.");
2311
+ throw new Error(i18n.t("notLoggedIn", { ns: "cli" }));
1549
2312
  }
1550
2313
  }
1551
2314
  function requirePositional(parsed, index, name) {
1552
2315
  const value = parsed.positional[index];
1553
2316
  if (value === void 0 || value.trim() === "") {
1554
- throw new Error(`Missing ${name}.`);
2317
+ throw new Error(i18n.t("missingPositional", { ns: "cli", name }));
1555
2318
  }
1556
2319
  return value;
1557
2320
  }
1558
2321
  function requireValue(argv, index, name) {
1559
2322
  const value = argv[index];
1560
2323
  if (value === void 0 || value.trim() === "") {
1561
- throw new Error(`Missing value for ${name}.`);
2324
+ throw new Error(i18n.t("missingOptionValue", { ns: "cli", name }));
1562
2325
  }
1563
2326
  return value;
1564
2327
  }
@@ -1603,67 +2366,28 @@ function assertApiOk(response) {
1603
2366
  function formatForbiddenMessage(response) {
1604
2367
  const message = response.errMessage.toLowerCase();
1605
2368
  if (message.includes("blocked from running")) {
1606
- return "403: \u5F53\u524D\u8D26\u53F7\u5DF2\u88AB\u8BE5\u9879\u76EE\u7981\u6B62\u8FD0\u884C\u3002";
2369
+ return i18n.t("forbiddenBlocked", { ns: "cli" });
1607
2370
  }
1608
2371
  if (message.includes("permission denied")) {
1609
- return "403: \u5F53\u524D\u8D26\u53F7\u5BF9\u8BE5\u9879\u76EE\u6CA1\u6709\u8DB3\u591F\u6743\u9650\u3002";
2372
+ return i18n.t("forbiddenNoPermission", { ns: "cli" });
1610
2373
  }
1611
- return `403: ${response.errMessage}`;
2374
+ return i18n.t("forbiddenFallback", { ns: "cli", message: response.errMessage });
1612
2375
  }
1613
2376
  function printUsage() {
1614
- console.log([
1615
- "Usage: workflow-code workspace <command> [args]",
1616
- "",
1617
- "Local repo wrapper:",
1618
- " pnpm -C workspace workspace <command> [args]",
1619
- "",
1620
- "Commands:",
1621
- " login --server <url>",
1622
- " logout",
1623
- " status",
1624
- " health",
1625
- " pack <workflow> [--path <workflow-dir>] [--output <file.tgz>]",
1626
- " upload <workflow> [--path <workflow-dir>] [--file <archive>] [--release-log <text>] [--source-mode bundled|source] [--no-wait] [--create]",
1627
- " preparation <workflow-id> <job-id>",
1628
- " download <workflow> [--target draft|latest|version] [--path <dir>]",
1629
- " publish <workflow> [--release-log <text>] [--source-mode bundled|source]",
1630
- " run <workflow> [...args] [--target draft|latest|version]",
1631
- " debug-node <workflow> <node> [...args] [--target draft|latest|version]",
1632
- " versions <workflow>",
1633
- "",
1634
- "Options:",
1635
- " --server <url> Defaults to WORKFLOW_SERVER_URL, saved CLI auth, or http://localhost:7125",
1636
- " --token <token> Optional manual override. Defaults to WORKFLOW_SERVER_ADMIN_KEY or saved CLI auth API key"
1637
- ].join("\n"));
2377
+ console.log(i18n.t("workspaceHelp", { ns: "cli" }));
1638
2378
  }
1639
2379
  function resolveRepoRoot(startDir = process.cwd()) {
1640
2380
  if (process.env.WORKFLOW_REPO_ROOT !== void 0) {
1641
2381
  return path4.resolve(process.env.WORKFLOW_REPO_ROOT);
1642
2382
  }
1643
2383
  for (const candidate of [startDir, CLI_MODULE_DIR]) {
1644
- const repoRoot = findRepoRoot2(candidate);
2384
+ const repoRoot = findWorkspaceRepositoryRoot(candidate);
1645
2385
  if (repoRoot !== void 0) {
1646
2386
  return repoRoot;
1647
2387
  }
1648
2388
  }
1649
2389
  return process.cwd();
1650
2390
  }
1651
- function findRepoRoot2(startDir) {
1652
- let current = path4.resolve(startDir);
1653
- while (true) {
1654
- if (isRepoRoot(current)) {
1655
- return current;
1656
- }
1657
- const parent = path4.dirname(current);
1658
- if (parent === current) {
1659
- return void 0;
1660
- }
1661
- current = parent;
1662
- }
1663
- }
1664
- function isRepoRoot(candidate) {
1665
- return existsSync2(path4.join(candidate, "pnpm-workspace.yaml")) && existsSync2(path4.join(candidate, "package.json")) && existsSync2(path4.join(candidate, "workspace", "package.json")) && existsSync2(path4.join(candidate, "packages", "cli", "package.json"));
1666
- }
1667
2391
  function trimTrailingSlash(value) {
1668
2392
  return value.replace(/\/+$/, "");
1669
2393
  }
@@ -1731,7 +2455,7 @@ function readRetryAfterSeconds(data) {
1731
2455
  }
1732
2456
  if (process.argv[1] !== void 0 && import.meta.url === pathToFileURL(path4.resolve(process.argv[1])).href) {
1733
2457
  main().catch((error) => {
1734
- console.error(error instanceof Error ? error.message : String(error));
2458
+ console.error(translateWorkflowCodeError(error));
1735
2459
  process.exitCode = 1;
1736
2460
  });
1737
2461
  }
@@ -1742,13 +2466,18 @@ export {
1742
2466
  formatForbiddenMessage,
1743
2467
  getPreparation,
1744
2468
  main,
2469
+ materializeProjectGroupDownload,
1745
2470
  normalizeWorkflowFilePath,
2471
+ packProjectGroup,
1746
2472
  packWorkflow,
1747
2473
  parseArgs,
1748
2474
  readPackageJsonFile,
2475
+ readProjectGroupContexts,
1749
2476
  readWorkflowPackageContext,
2477
+ resolveExplicitDataGrants,
1750
2478
  resolveRemoteWorkflowId,
1751
2479
  resolveRepoRoot,
2480
+ shouldUseProjectGroupUpload,
1752
2481
  stageWorkflowWithLocalDependencies,
1753
2482
  waitForDeviceToken,
1754
2483
  waitForPreparation,