@camunda8/cli 2.7.0-alpha.8 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/EXAMPLES.md +14 -1
  2. package/README.md +11 -3
  3. package/dist/client.d.ts +2 -2
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js.map +1 -1
  6. package/dist/command-dispatch.d.ts.map +1 -1
  7. package/dist/command-dispatch.js +22 -1
  8. package/dist/command-dispatch.js.map +1 -1
  9. package/dist/command-framework.d.ts +36 -6
  10. package/dist/command-framework.d.ts.map +1 -1
  11. package/dist/command-framework.js +53 -19
  12. package/dist/command-framework.js.map +1 -1
  13. package/dist/command-registry.d.ts +89 -30
  14. package/dist/command-registry.d.ts.map +1 -1
  15. package/dist/command-registry.js +112 -35
  16. package/dist/command-registry.js.map +1 -1
  17. package/dist/command-validation.d.ts +14 -6
  18. package/dist/command-validation.d.ts.map +1 -1
  19. package/dist/command-validation.js +69 -8
  20. package/dist/command-validation.js.map +1 -1
  21. package/dist/commands/completion.d.ts +19 -0
  22. package/dist/commands/completion.d.ts.map +1 -1
  23. package/dist/commands/completion.js +36 -14
  24. package/dist/commands/completion.js.map +1 -1
  25. package/dist/commands/deployments.d.ts +30 -3
  26. package/dist/commands/deployments.d.ts.map +1 -1
  27. package/dist/commands/deployments.js +320 -218
  28. package/dist/commands/deployments.js.map +1 -1
  29. package/dist/commands/forms.d.ts.map +1 -1
  30. package/dist/commands/forms.js +2 -3
  31. package/dist/commands/forms.js.map +1 -1
  32. package/dist/commands/identity-groups.d.ts.map +1 -1
  33. package/dist/commands/identity-groups.js +3 -5
  34. package/dist/commands/identity-groups.js.map +1 -1
  35. package/dist/commands/identity-mapping-rules.d.ts.map +1 -1
  36. package/dist/commands/identity-mapping-rules.js +5 -9
  37. package/dist/commands/identity-mapping-rules.js.map +1 -1
  38. package/dist/commands/identity-roles.d.ts.map +1 -1
  39. package/dist/commands/identity-roles.js +3 -5
  40. package/dist/commands/identity-roles.js.map +1 -1
  41. package/dist/commands/identity-tenants.d.ts.map +1 -1
  42. package/dist/commands/identity-tenants.js +3 -5
  43. package/dist/commands/identity-tenants.js.map +1 -1
  44. package/dist/commands/identity-users.d.ts.map +1 -1
  45. package/dist/commands/identity-users.js +3 -5
  46. package/dist/commands/identity-users.js.map +1 -1
  47. package/dist/commands/identity.d.ts +29 -2
  48. package/dist/commands/identity.d.ts.map +1 -1
  49. package/dist/commands/identity.js +247 -208
  50. package/dist/commands/identity.js.map +1 -1
  51. package/dist/commands/incidents.d.ts.map +1 -1
  52. package/dist/commands/incidents.js +2 -3
  53. package/dist/commands/incidents.js.map +1 -1
  54. package/dist/commands/jobs.d.ts.map +1 -1
  55. package/dist/commands/jobs.js +5 -9
  56. package/dist/commands/jobs.js.map +1 -1
  57. package/dist/commands/mcp-proxy.d.ts +4 -4
  58. package/dist/commands/mcp-proxy.d.ts.map +1 -1
  59. package/dist/commands/mcp-proxy.js +49 -39
  60. package/dist/commands/mcp-proxy.js.map +1 -1
  61. package/dist/commands/messages.d.ts.map +1 -1
  62. package/dist/commands/messages.js +2 -4
  63. package/dist/commands/messages.js.map +1 -1
  64. package/dist/commands/open.d.ts +4 -2
  65. package/dist/commands/open.d.ts.map +1 -1
  66. package/dist/commands/open.js +17 -18
  67. package/dist/commands/open.js.map +1 -1
  68. package/dist/commands/plugins.d.ts.map +1 -1
  69. package/dist/commands/plugins.js +16 -32
  70. package/dist/commands/plugins.js.map +1 -1
  71. package/dist/commands/process-instances.d.ts.map +1 -1
  72. package/dist/commands/process-instances.js +5 -9
  73. package/dist/commands/process-instances.js.map +1 -1
  74. package/dist/commands/profiles.d.ts.map +1 -1
  75. package/dist/commands/profiles.js +8 -16
  76. package/dist/commands/profiles.js.map +1 -1
  77. package/dist/commands/resource-extensions.d.ts +8 -0
  78. package/dist/commands/resource-extensions.d.ts.map +1 -0
  79. package/dist/commands/resource-extensions.js +20 -0
  80. package/dist/commands/resource-extensions.js.map +1 -0
  81. package/dist/commands/run.d.ts +2 -1
  82. package/dist/commands/run.d.ts.map +1 -1
  83. package/dist/commands/run.js +10 -4
  84. package/dist/commands/run.js.map +1 -1
  85. package/dist/commands/search.d.ts.map +1 -1
  86. package/dist/commands/search.js +4 -8
  87. package/dist/commands/search.js.map +1 -1
  88. package/dist/commands/session.d.ts.map +1 -1
  89. package/dist/commands/session.js +4 -7
  90. package/dist/commands/session.js.map +1 -1
  91. package/dist/commands/user-tasks.d.ts.map +1 -1
  92. package/dist/commands/user-tasks.js +2 -3
  93. package/dist/commands/user-tasks.js.map +1 -1
  94. package/dist/commands/variables.d.ts +10 -0
  95. package/dist/commands/variables.d.ts.map +1 -0
  96. package/dist/commands/variables.js +51 -0
  97. package/dist/commands/variables.js.map +1 -0
  98. package/dist/commands/watch.d.ts +7 -5
  99. package/dist/commands/watch.d.ts.map +1 -1
  100. package/dist/commands/watch.js +90 -39
  101. package/dist/commands/watch.js.map +1 -1
  102. package/dist/errors.d.ts +62 -8
  103. package/dist/errors.d.ts.map +1 -1
  104. package/dist/errors.js +99 -10
  105. package/dist/errors.js.map +1 -1
  106. package/dist/index.d.ts.map +1 -1
  107. package/dist/index.js +22 -39
  108. package/dist/index.js.map +1 -1
  109. package/package.json +8 -5
@@ -5,12 +5,13 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
5
5
  import { basename, dirname, extname, join, relative, resolve } from "node:path";
6
6
  import { TenantId } from "@camunda8/orchestration-cluster-api";
7
7
  import { createClient } from "../client.js";
8
- import { defineCommand } from "../command-framework.js";
9
- import { resolveClusterConfig, resolveTenantId } from "../config.js";
8
+ import { defineCommand, dryRun } from "../command-framework.js";
9
+ import { resolveTenantId } from "../config.js";
10
+ import { normalizeToError, SilentError } from "../errors.js";
10
11
  import { isIgnored, loadIgnoreRules } from "../ignore.js";
11
12
  import { getLogger, isRecord } from "../logger.js";
12
13
  import { c8ctl } from "../runtime.js";
13
- const RESOURCE_EXTENSIONS = [".bpmn", ".dmn", ".form"];
14
+ import { DEPLOYABLE_EXTENSIONS } from "./resource-extensions.js";
14
15
  const PROCESS_APPLICATION_FILE = ".process-application";
15
16
  /**
16
17
  * Helper to output messages that respect JSON mode for Unix pipe compatibility
@@ -89,7 +90,7 @@ function findGroupRoot(filePath, basePath) {
89
90
  /**
90
91
  * Recursively collect resource files from a directory
91
92
  */
92
- function collectResourceFiles(dirPath, collected = [], basePath, ig, ignoreBaseDir) {
93
+ function collectResourceFiles(dirPath, collected = [], basePath, ig, ignoreBaseDir, force) {
93
94
  if (!existsSync(dirPath)) {
94
95
  return collected;
95
96
  }
@@ -102,18 +103,19 @@ function collectResourceFiles(dirPath, collected = [], basePath, ig, ignoreBaseD
102
103
  if (ig && ignoreBaseDir && isIgnored(ig, dirPath, ignoreBaseDir)) {
103
104
  return collected;
104
105
  }
105
- const ext = extname(dirPath);
106
- if (RESOURCE_EXTENSIONS.includes(ext)) {
107
- const groupInfo = findGroupRoot(dirPath, basePath);
108
- collected.push({
109
- path: dirPath,
110
- name: basename(dirPath),
111
- content: readFileSync(dirPath),
112
- isBuildingBlock: groupInfo.type === "bb",
113
- isProcessApplication: groupInfo.type === "pa",
114
- groupPath: groupInfo.root || undefined,
115
- });
106
+ // Unless --force, reject files with unsupported extensions
107
+ if (!force && !DEPLOYABLE_EXTENSIONS.includes(extname(dirPath))) {
108
+ return collected;
116
109
  }
110
+ const groupInfo = findGroupRoot(dirPath, basePath);
111
+ collected.push({
112
+ path: dirPath,
113
+ name: basename(dirPath),
114
+ content: readFileSync(dirPath),
115
+ isBuildingBlock: groupInfo.type === "bb",
116
+ isProcessApplication: groupInfo.type === "pa",
117
+ groupPath: groupInfo.root || undefined,
118
+ });
117
119
  return collected;
118
120
  }
119
121
  if (stat.isDirectory()) {
@@ -144,35 +146,40 @@ function collectResourceFiles(dirPath, collected = [], basePath, ig, ignoreBaseD
144
146
  }
145
147
  }
146
148
  else if (entryStat.isFile()) {
149
+ // Skip hidden files (e.g. .c8ignore, .process-application)
150
+ if (entry.startsWith(".")) {
151
+ return;
152
+ }
147
153
  // Skip ignored files
148
154
  if (ig && ignoreBaseDir && isIgnored(ig, fullPath, ignoreBaseDir)) {
149
155
  return;
150
156
  }
157
+ // Unless --force, only collect files with known deployable extensions
158
+ if (!force && !DEPLOYABLE_EXTENSIONS.includes(extname(fullPath))) {
159
+ return;
160
+ }
151
161
  files.push(fullPath);
152
162
  }
153
163
  });
154
164
  // Process files in current directory first
155
165
  files.forEach((file) => {
156
- const ext = extname(file);
157
- if (RESOURCE_EXTENSIONS.includes(ext)) {
158
- const groupInfo = findGroupRoot(file, basePath);
159
- collected.push({
160
- path: file,
161
- name: basename(file),
162
- content: readFileSync(file),
163
- isBuildingBlock: groupInfo.type === "bb",
164
- isProcessApplication: groupInfo.type === "pa",
165
- groupPath: groupInfo.root || undefined,
166
- });
167
- }
166
+ const groupInfo = findGroupRoot(file, basePath);
167
+ collected.push({
168
+ path: file,
169
+ name: basename(file),
170
+ content: readFileSync(file),
171
+ isBuildingBlock: groupInfo.type === "bb",
172
+ isProcessApplication: groupInfo.type === "pa",
173
+ groupPath: groupInfo.root || undefined,
174
+ });
168
175
  });
169
176
  // Process building block folders first (prioritized)
170
177
  bbFolders.forEach((bbFolder) => {
171
- collectResourceFiles(bbFolder, collected, basePath, ig, ignoreBaseDir);
178
+ collectResourceFiles(bbFolder, collected, basePath, ig, ignoreBaseDir, force);
172
179
  });
173
180
  // Then process regular folders
174
181
  regularFolders.forEach((regularFolder) => {
175
- collectResourceFiles(regularFolder, collected, basePath, ig, ignoreBaseDir);
182
+ collectResourceFiles(regularFolder, collected, basePath, ig, ignoreBaseDir, force);
176
183
  });
177
184
  }
178
185
  return collected;
@@ -193,195 +200,249 @@ function findDuplicateDefinitionIds(resources) {
193
200
  return new Map([...idMap].filter(([, paths]) => paths.length > 1));
194
201
  }
195
202
  /**
196
- * Deploy resources
203
+ * Collect deployable resources from the given paths, applying `.c8ignore`
204
+ * rules. Throws on the two pre-API guard failures so callers (deploy
205
+ * handler, watch, dry-run preview) all surface the same errors with the
206
+ * same wording.
207
+ *
208
+ * Shared between `deployCommand` (used for both the dry-run preview and
209
+ * the execute path via `deployResources`) and `deployResources` itself.
197
210
  */
198
- export async function deploy(paths, options) {
211
+ function collectResourcesForPaths(paths, force) {
212
+ if (paths.length === 0) {
213
+ throw new Error("No paths provided. Use: c8 deploy <path> or c8 deploy (for current directory)");
214
+ }
215
+ // Load .c8ignore rules from the working directory
216
+ const ignoreBaseDir = resolve(process.cwd());
217
+ const ig = loadIgnoreRules(ignoreBaseDir);
218
+ const resources = [];
219
+ paths.forEach((path) => {
220
+ collectResourceFiles(path, resources, undefined, ig, ignoreBaseDir, force);
221
+ });
222
+ if (resources.length === 0) {
223
+ throw new Error("No deployable files found in the specified paths");
224
+ }
225
+ return resources;
226
+ }
227
+ /**
228
+ * Internal helper: deploy the given paths to the cluster and render the
229
+ * result table. Used by `deployCommand` (the standard CLI entry point)
230
+ * and by `watchCommand` (for change-triggered re-deploys).
231
+ *
232
+ * Does NOT consult `c8ctl.dryRun` — dry-run handling lives in the
233
+ * `deployCommand` handler so the framework's `dryRun()` helper owns
234
+ * preview emission. Watch never triggers a dry-run, so this split also
235
+ * removes a footgun where a stale dry-run flag could suppress a watch
236
+ * deploy.
237
+ */
238
+ export async function deployResources(paths, options) {
199
239
  const logger = getLogger();
200
240
  const tenantId = resolveTenantId(options.profile);
201
- const resources = [];
202
- try {
203
- // Store the base paths for relative path calculation
204
- const basePaths = paths.length === 0 ? [process.cwd()] : paths;
205
- if (paths.length === 0) {
206
- logger.error("No paths provided. Use: c8 deploy <path> or c8 deploy (for current directory)");
207
- process.exit(1);
208
- }
209
- // Load .c8ignore rules from the working directory
210
- const ignoreBaseDir = resolve(process.cwd());
211
- const ig = loadIgnoreRules(ignoreBaseDir);
212
- // Collect all resource files (respecting .c8ignore)
213
- paths.forEach((path) => {
214
- collectResourceFiles(path, resources, undefined, ig, ignoreBaseDir);
215
- });
216
- if (resources.length === 0) {
217
- logger.error("No BPMN/DMN/Form files found in the specified paths");
218
- process.exit(1);
219
- }
220
- // Dry-run: emit the would-be API request without executing
221
- if (c8ctl.dryRun) {
222
- const config = resolveClusterConfig(options.profile);
223
- logger.json({
224
- dryRun: true,
225
- command: "deploy",
226
- method: "POST",
227
- url: `${config.baseUrl}/deployments`,
228
- body: {
229
- tenantId,
230
- resources: resources.map((r) => ({ name: r.name })),
231
- },
232
- });
233
- return;
234
- }
235
- const client = createClient(options.profile);
236
- // Calculate relative paths for display
237
- const basePath = basePaths.length === 1 ? basePaths[0] : process.cwd();
238
- resources.forEach((r) => {
239
- r.relativePath = relative(basePath, r.path) || r.name;
240
- });
241
- // Sort: group resources by their group, with building blocks first, then process applications, then standalone
242
- resources.sort((a, b) => {
243
- // Building blocks have highest priority
244
- if (a.isBuildingBlock && !b.isBuildingBlock)
245
- return -1;
246
- if (!a.isBuildingBlock && b.isBuildingBlock)
247
- return 1;
248
- // Within building blocks, group by groupPath
249
- if (a.isBuildingBlock && b.isBuildingBlock) {
250
- if (a.groupPath && b.groupPath) {
251
- const groupCompare = a.groupPath.localeCompare(b.groupPath);
252
- if (groupCompare !== 0)
253
- return groupCompare;
254
- }
255
- return a.path.localeCompare(b.path);
256
- }
257
- // Process applications come next
258
- if (a.isProcessApplication && !b.isProcessApplication)
259
- return -1;
260
- if (!a.isProcessApplication && b.isProcessApplication)
261
- return 1;
262
- // Within process applications, group by groupPath
263
- if (a.isProcessApplication && b.isProcessApplication) {
264
- if (a.groupPath && b.groupPath) {
265
- const groupCompare = a.groupPath.localeCompare(b.groupPath);
266
- if (groupCompare !== 0)
267
- return groupCompare;
268
- }
269
- return a.path.localeCompare(b.path);
241
+ // ─── Pre-API-call validation and preparation ────────────────────────
242
+ // These steps run OUTSIDE any try/catch so validation errors bubble
243
+ // straight to the framework's `handleCommandError`. Only the actual
244
+ // HTTP deploy call (further down) is wrapped in a catch that routes
245
+ // through `handleDeploymentError` for rich Problem-Detail rendering.
246
+ const resources = collectResourcesForPaths(paths, options.force);
247
+ // Store the base paths for relative path calculation. Safe to assign
248
+ // directly now: the empty-paths guard inside `collectResourcesForPaths`
249
+ // has already thrown.
250
+ const basePaths = paths;
251
+ const client = createClient(options.profile);
252
+ // Calculate relative paths for display
253
+ const basePath = basePaths.length === 1 ? basePaths[0] : process.cwd();
254
+ resources.forEach((r) => {
255
+ r.relativePath = relative(basePath, r.path) || r.name;
256
+ });
257
+ // Sort: group resources by their group, with building blocks first, then process applications, then standalone
258
+ resources.sort((a, b) => {
259
+ // Building blocks have highest priority
260
+ if (a.isBuildingBlock && !b.isBuildingBlock)
261
+ return -1;
262
+ if (!a.isBuildingBlock && b.isBuildingBlock)
263
+ return 1;
264
+ // Within building blocks, group by groupPath
265
+ if (a.isBuildingBlock && b.isBuildingBlock) {
266
+ if (a.groupPath && b.groupPath) {
267
+ const groupCompare = a.groupPath.localeCompare(b.groupPath);
268
+ if (groupCompare !== 0)
269
+ return groupCompare;
270
270
  }
271
- // Finally, standalone resources sorted by path
272
271
  return a.path.localeCompare(b.path);
273
- });
274
- // Validate for duplicate process/decision IDs
275
- const duplicates = findDuplicateDefinitionIds(resources);
276
- if (duplicates.size > 0) {
277
- logger.error("Cannot deploy: Multiple files with the same process/decision ID in one deployment");
278
- duplicates.forEach((paths, id) => {
279
- logMessage(` Process/Decision ID "${id}" found in: ${paths.join(", ")}`);
280
- });
281
- logMessage("\nCamunda does not allow deploying multiple resources with the same definition ID in a single deployment.");
282
- logMessage("Please deploy these files separately or ensure each process/decision has a unique ID.");
283
- process.exit(1);
284
272
  }
285
- logger.info(`Deploying ${resources.length} resource(s)...`);
286
- // Create a mapping from definition ID to resource file for later reference
287
- const definitionIdToResource = new Map();
288
- const formNameToResource = new Map();
289
- resources.forEach((r) => {
290
- const ext = extname(r.path);
291
- if (ext === ".bpmn" || ext === ".dmn") {
292
- const defId = extractDefinitionId(r.content, ext);
293
- if (defId) {
294
- definitionIdToResource.set(defId, r);
295
- }
273
+ // Process applications come next
274
+ if (a.isProcessApplication && !b.isProcessApplication)
275
+ return -1;
276
+ if (!a.isProcessApplication && b.isProcessApplication)
277
+ return 1;
278
+ // Within process applications, group by groupPath
279
+ if (a.isProcessApplication && b.isProcessApplication) {
280
+ if (a.groupPath && b.groupPath) {
281
+ const groupCompare = a.groupPath.localeCompare(b.groupPath);
282
+ if (groupCompare !== 0)
283
+ return groupCompare;
296
284
  }
297
- else if (ext === ".form") {
298
- // Forms are matched by filename (without extension)
299
- const formId = basename(r.name, ".form");
300
- formNameToResource.set(formId, r);
301
- }
302
- });
303
- // Create deployment request - convert buffers to File objects with proper MIME types
304
- const result = await client.createDeployment({
305
- tenantId: TenantId.assumeExists(tenantId),
306
- resources: resources.map((r) => {
307
- // Determine MIME type based on extension
308
- const ext = r.name.split(".").pop()?.toLowerCase();
309
- const mimeType = ext === "bpmn"
310
- ? "application/xml"
311
- : ext === "dmn"
312
- ? "application/xml"
313
- : ext === "form"
314
- ? "application/json"
315
- : "application/octet-stream";
316
- // Convert Buffer to Uint8Array for File constructor
317
- return new File([new Uint8Array(r.content)], r.name, {
318
- type: mimeType,
319
- });
320
- }),
321
- });
322
- logger.success("Deployment successful", result.deploymentKey.toString());
323
- // Normalize all deployed resources into a common structure
324
- const allResources = [
325
- ...result.processes.map((proc) => ({
326
- type: "Process",
327
- id: proc.processDefinitionId,
328
- version: proc.processDefinitionVersion,
329
- key: proc.processDefinitionKey.toString(),
330
- resource: definitionIdToResource.get(proc.processDefinitionId),
331
- })),
332
- ...result.decisions.map((dec) => ({
333
- type: "Decision",
334
- id: dec.decisionDefinitionId || "-",
335
- version: dec.version ?? "-",
336
- key: dec.decisionDefinitionKey?.toString() || "-",
337
- resource: definitionIdToResource.get(dec.decisionDefinitionId || ""),
338
- })),
339
- ...result.forms.map((form) => ({
340
- type: "Form",
341
- id: form.formId || "-",
342
- version: form.version ?? "-",
343
- key: form.formKey?.toString() || "-",
344
- resource: formNameToResource.get(form.formId || ""),
345
- })),
346
- ];
347
- const tableData = allResources.map(({ type, id, version, key, resource }) => {
348
- const fileDisplay = resource
349
- ? `${resource.isBuildingBlock ? "🧱 " : ""}${resource.isProcessApplication ? "📦 " : ""}${resource.relativePath || resource.name}`
350
- : "-";
351
- // Extract directory path for grouping (e.g., "bla/_bb-building-block" or "pa")
352
- const sortKey = resource?.relativePath
353
- ? resource.relativePath.substring(0, resource.relativePath.lastIndexOf("/") + 1) || resource.relativePath
354
- : "zzz"; // Resources without paths go last
355
- return {
356
- File: fileDisplay,
357
- Type: type,
358
- ID: id,
359
- Version: version,
360
- Key: key,
361
- sortKey,
362
- };
285
+ return a.path.localeCompare(b.path);
286
+ }
287
+ // Finally, standalone resources sorted by path
288
+ return a.path.localeCompare(b.path);
289
+ });
290
+ // Validate for duplicate process/decision IDs
291
+ const duplicates = findDuplicateDefinitionIds(resources);
292
+ if (duplicates.size > 0) {
293
+ // Single source of truth for both the user-visible logger.error and
294
+ // the SilentError message — keeps stderr and `--verbose` rethrow
295
+ // stack message aligned even if the wording is later edited.
296
+ const duplicateIdsMessage = "Cannot deploy: Multiple files with the same process/decision ID in one deployment";
297
+ // Pre-render the rich detail (per-id file list + guidance) so the
298
+ // user sees actionable context, then throw a SilentError so the
299
+ // framework records the failure without re-rendering a duplicate
300
+ // summary line.
301
+ logger.error(duplicateIdsMessage);
302
+ duplicates.forEach((dupPaths, id) => {
303
+ logMessage(` Process/Decision ID "${id}" found in: ${dupPaths.join(", ")}`);
363
304
  });
364
- // Sort by directory path (grouping), then by file name
365
- tableData.sort((a, b) => {
366
- if (a.sortKey !== b.sortKey) {
367
- return a.sortKey.localeCompare(b.sortKey);
305
+ logMessage("\nCamunda does not allow deploying multiple resources with the same definition ID in a single deployment.");
306
+ logMessage("Please deploy these files separately or ensure each process/decision has a unique ID.");
307
+ throw new SilentError(duplicateIdsMessage);
308
+ }
309
+ logger.info(`Deploying ${resources.length} resource(s)...`);
310
+ // Create a mapping from definition ID to resource file for later reference
311
+ const definitionIdToResource = new Map();
312
+ const formNameToResource = new Map();
313
+ resources.forEach((r) => {
314
+ const ext = extname(r.path);
315
+ if (ext === ".bpmn" || ext === ".dmn") {
316
+ const defId = extractDefinitionId(r.content, ext);
317
+ if (defId) {
318
+ definitionIdToResource.set(defId, r);
368
319
  }
369
- return a.File.localeCompare(b.File);
370
- });
371
- // Remove sortKey before displaying
372
- const displayData = tableData.map(({ File, Type, ID, Version, Key }) => ({
373
- File,
374
- Type,
375
- ID,
376
- Version,
377
- Key,
378
- }));
379
- if (displayData.length > 0) {
380
- logger.table(displayData);
320
+ }
321
+ else if (ext === ".form") {
322
+ // Forms are matched by filename (without extension)
323
+ const formId = basename(r.name, ".form");
324
+ formNameToResource.set(formId, r);
325
+ }
326
+ });
327
+ // ─── API call ────────────────────────────────────────────────────────
328
+ // Only this section is wrapped in a catch that routes through
329
+ // `handleDeploymentError`. Pre-API errors above bubble to the
330
+ // framework directly.
331
+ // Create deployment request - convert buffers to File objects with proper MIME types
332
+ const pendingDeploy = client.createDeployment({
333
+ tenantId: TenantId.assumeExists(tenantId),
334
+ resources: resources.map((r) => {
335
+ // Determine MIME type based on extension
336
+ const ext = r.name.split(".").pop()?.toLowerCase();
337
+ const mimeType = ext === "bpmn"
338
+ ? "application/xml"
339
+ : ext === "dmn"
340
+ ? "application/xml"
341
+ : ext === "form"
342
+ ? "application/json"
343
+ : "application/octet-stream";
344
+ // Convert Buffer to Uint8Array for File constructor
345
+ return new File([new Uint8Array(r.content)], r.name, {
346
+ type: mimeType,
347
+ });
348
+ }),
349
+ });
350
+ // Wire optional cancellation: when the caller's AbortSignal fires,
351
+ // cancel the underlying CancelablePromise so the in-flight HTTP
352
+ // request is aborted promptly. Used by `watch` so SIGINT mid-deploy
353
+ // shuts down within ~one event-loop tick rather than blocking on
354
+ // the network round-trip.
355
+ const onAbort = () => {
356
+ pendingDeploy.cancel();
357
+ };
358
+ if (options.signal) {
359
+ if (options.signal.aborted) {
360
+ onAbort();
361
+ }
362
+ else {
363
+ options.signal.addEventListener("abort", onAbort, { once: true });
381
364
  }
382
365
  }
366
+ let result;
367
+ try {
368
+ result = await pendingDeploy;
369
+ }
383
370
  catch (error) {
371
+ options.signal?.removeEventListener("abort", onAbort);
372
+ // Caller-initiated cancellation (e.g. SIGINT in `watch`): the
373
+ // CancelablePromise rejects with a "Cancelled" error after we
374
+ // invoked `pendingDeploy.cancel()` from the abort listener. The
375
+ // caller already knows the request was aborted — do not surface
376
+ // it as a user-visible deploy failure or exit non-zero.
377
+ if (options.signal?.aborted) {
378
+ return;
379
+ }
384
380
  handleDeploymentError(error, resources, logger, options.continueOnError, options.continueOnUserError);
381
+ // `handleDeploymentError` either throws (terminal) or returns
382
+ // (continue-on-error). On the continue path, skip the success
383
+ // render below — there's no result to render.
384
+ return;
385
+ }
386
+ options.signal?.removeEventListener("abort", onAbort);
387
+ logger.success("Deployment successful", result.deploymentKey.toString());
388
+ // Normalize all deployed resources into a common structure
389
+ const allResources = [
390
+ ...result.processes.map((proc) => ({
391
+ type: "Process",
392
+ id: proc.processDefinitionId,
393
+ version: proc.processDefinitionVersion,
394
+ key: proc.processDefinitionKey.toString(),
395
+ resource: definitionIdToResource.get(proc.processDefinitionId),
396
+ })),
397
+ ...result.decisions.map((dec) => ({
398
+ type: "Decision",
399
+ id: dec.decisionDefinitionId || "-",
400
+ version: dec.version ?? "-",
401
+ key: dec.decisionDefinitionKey?.toString() || "-",
402
+ resource: definitionIdToResource.get(dec.decisionDefinitionId || ""),
403
+ })),
404
+ ...result.forms.map((form) => ({
405
+ type: "Form",
406
+ id: form.formId || "-",
407
+ version: form.version ?? "-",
408
+ key: form.formKey?.toString() || "-",
409
+ resource: formNameToResource.get(form.formId || ""),
410
+ })),
411
+ ];
412
+ const tableData = allResources.map(({ type, id, version, key, resource }) => {
413
+ const fileDisplay = resource
414
+ ? `${resource.isBuildingBlock ? "🧱 " : ""}${resource.isProcessApplication ? "📦 " : ""}${resource.relativePath || resource.name}`
415
+ : "-";
416
+ // Extract directory path for grouping (e.g., "bla/_bb-building-block" or "pa")
417
+ const sortKey = resource?.relativePath
418
+ ? resource.relativePath.substring(0, resource.relativePath.lastIndexOf("/") + 1) || resource.relativePath
419
+ : "zzz"; // Resources without paths go last
420
+ return {
421
+ File: fileDisplay,
422
+ Type: type,
423
+ ID: id,
424
+ Version: version,
425
+ Key: key,
426
+ sortKey,
427
+ };
428
+ });
429
+ // Sort by directory path (grouping), then by file name
430
+ tableData.sort((a, b) => {
431
+ if (a.sortKey !== b.sortKey) {
432
+ return a.sortKey.localeCompare(b.sortKey);
433
+ }
434
+ return a.File.localeCompare(b.File);
435
+ });
436
+ // Remove sortKey before displaying
437
+ const displayData = tableData.map(({ File, Type, ID, Version, Key }) => ({
438
+ File,
439
+ Type,
440
+ ID,
441
+ Version,
442
+ Key,
443
+ }));
444
+ if (displayData.length > 0) {
445
+ logger.table(displayData);
385
446
  }
386
447
  }
387
448
  /**
@@ -397,9 +458,14 @@ function handleDeploymentError(error, resources, logger, continueOnError, contin
397
458
  if (shouldContinue) {
398
459
  throw error;
399
460
  }
400
- const normalizedError = error instanceof Error ? error : new Error(String(error));
401
- console.error(normalizedError);
402
- process.exit(1);
461
+ // Verbose mode: surface the original error to the framework so
462
+ // `handleCommandError` rethrows it and Node prints the stack trace.
463
+ // Non-Error throws (e.g. RFC 9457 problem-detail plain objects from
464
+ // the SDK) are normalized via the centralized helper so the message
465
+ // is built from `title` / `detail` / `status` instead of collapsing
466
+ // to `Error: [object Object]`. The original value is preserved as
467
+ // `cause` so it remains inspectable.
468
+ throw normalizeToError(error, "Deployment request failed");
403
469
  }
404
470
  // Try to interpret common transport/network issues first for actionable guidance
405
471
  const deriveNetworkErrorTitle = (err) => {
@@ -456,7 +522,10 @@ function handleDeploymentError(error, resources, logger, continueOnError, contin
456
522
  if (shouldContinue) {
457
523
  return;
458
524
  }
459
- process.exit(1);
525
+ // Pre-rendered the rich error context above; throw a SilentError so
526
+ // `handleCommandError` exits non-zero without re-rendering a
527
+ // duplicate "Failed to deploy: <message>" summary line.
528
+ throw new SilentError(title);
460
529
  }
461
530
  /**
462
531
  * Format the error detail for better readability
@@ -471,11 +540,8 @@ function formatDeploymentErrorDetail(detail) {
471
540
  const result = [];
472
541
  let inFileError = false;
473
542
  for (const line of lines) {
474
- if (line.startsWith("'") &&
475
- (line.includes(".bpmn") ||
476
- line.includes(".dmn") ||
477
- line.includes(".form"))) {
478
- // This is a file-specific error
543
+ if (line.startsWith("'") && line.includes("':")) {
544
+ // This is a file-specific error (detected by 'filename': pattern)
479
545
  inFileError = true;
480
546
  result.push(` 📄 ${line}`);
481
547
  }
@@ -548,15 +614,51 @@ function printDeploymentHints(title, detail, status, resources) {
548
614
  logMessage(h);
549
615
  });
550
616
  }
551
- // ─── defineCommand wrapper ───────────────────────────────────────────────────
552
- /** Side-effectful: collects files, validates, deploys, and renders its own table output. */
553
- export const deployCommand = defineCommand("deploy", "", async (ctx) => {
617
+ // ─── defineCommand ───────────────────────────────────────────────────────────
618
+ /**
619
+ * Side-effectful: collects files, validates, deploys, and renders its own
620
+ * table output.
621
+ *
622
+ * The body lives directly in the handler (per #288): argument-shape
623
+ * resolution, dry-run preview via the framework's `dryRun()` helper,
624
+ * and the call into the shared `deployResources` helper that watch also
625
+ * uses for change-triggered re-deploys.
626
+ */
627
+ export const deployCommand = defineCommand("deploy", "", async (ctx, flags) => {
628
+ // Argument shape: `c8 deploy [path...]`. With no positional, default
629
+ // to cwd. Pinned by tests/unit/deploy-behaviour.test.ts.
554
630
  const paths = ctx.resource
555
631
  ? [ctx.resource, ...ctx.positionals]
556
632
  : ctx.positionals.length > 0
557
633
  ? ctx.positionals
558
634
  : ["."];
559
- await deploy(paths, { profile: ctx.profile });
635
+ // Dry-run preview. Collect resources first so the preview body
636
+ // reflects what would actually be sent — and so the empty-paths /
637
+ // no-files guards still surface as thrown errors before we emit.
638
+ // Uses `ctx.dryRun` and `ctx.tenantId` from the framework rather
639
+ // than reaching into the global runtime/config layer.
640
+ if (ctx.dryRun) {
641
+ const previewResources = collectResourcesForPaths(paths, flags.force);
642
+ const dr = dryRun({
643
+ command: "deploy",
644
+ method: "POST",
645
+ endpoint: "/deployments",
646
+ profile: ctx.profile,
647
+ body: {
648
+ tenantId: ctx.tenantId,
649
+ resources: previewResources.map((r) => ({ name: r.name })),
650
+ },
651
+ });
652
+ if (dr)
653
+ return dr;
654
+ }
655
+ // Execute path: only reached when not in dry-run mode (the branch
656
+ // above returns early). `deployResources` runs its own
657
+ // `collectResourcesForPaths` internally and renders the success
658
+ // table. Keeping the helper self-contained avoids threading
659
+ // pre-collected state between the handler and the shared helper
660
+ // used by `watch`.
661
+ await deployResources(paths, { profile: ctx.profile, force: flags.force });
560
662
  return { kind: "none" };
561
663
  });
562
664
  //# sourceMappingURL=deployments.js.map