@camunda8/cli 2.7.0-alpha.9 → 2.8.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EXAMPLES.md +14 -1
- package/README.md +11 -3
- package/dist/client.d.ts +2 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/command-dispatch.d.ts.map +1 -1
- package/dist/command-dispatch.js +22 -1
- package/dist/command-dispatch.js.map +1 -1
- package/dist/command-framework.d.ts +36 -6
- package/dist/command-framework.d.ts.map +1 -1
- package/dist/command-framework.js +53 -19
- package/dist/command-framework.js.map +1 -1
- package/dist/command-registry.d.ts +85 -61
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +108 -52
- package/dist/command-registry.js.map +1 -1
- package/dist/command-validation.d.ts +14 -6
- package/dist/command-validation.d.ts.map +1 -1
- package/dist/command-validation.js +69 -8
- package/dist/command-validation.js.map +1 -1
- package/dist/commands/completion.d.ts +19 -0
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +36 -14
- package/dist/commands/completion.js.map +1 -1
- package/dist/commands/deployments.d.ts +30 -3
- package/dist/commands/deployments.d.ts.map +1 -1
- package/dist/commands/deployments.js +320 -218
- package/dist/commands/deployments.js.map +1 -1
- package/dist/commands/forms.d.ts.map +1 -1
- package/dist/commands/forms.js +2 -3
- package/dist/commands/forms.js.map +1 -1
- package/dist/commands/help.d.ts.map +1 -1
- package/dist/commands/help.js +39 -39
- package/dist/commands/help.js.map +1 -1
- package/dist/commands/identity-groups.d.ts.map +1 -1
- package/dist/commands/identity-groups.js +3 -5
- package/dist/commands/identity-groups.js.map +1 -1
- package/dist/commands/identity-mapping-rules.d.ts.map +1 -1
- package/dist/commands/identity-mapping-rules.js +5 -9
- package/dist/commands/identity-mapping-rules.js.map +1 -1
- package/dist/commands/identity-roles.d.ts.map +1 -1
- package/dist/commands/identity-roles.js +3 -5
- package/dist/commands/identity-roles.js.map +1 -1
- package/dist/commands/identity-tenants.d.ts.map +1 -1
- package/dist/commands/identity-tenants.js +3 -5
- package/dist/commands/identity-tenants.js.map +1 -1
- package/dist/commands/identity-users.d.ts.map +1 -1
- package/dist/commands/identity-users.js +3 -5
- package/dist/commands/identity-users.js.map +1 -1
- package/dist/commands/identity.d.ts +29 -2
- package/dist/commands/identity.d.ts.map +1 -1
- package/dist/commands/identity.js +247 -208
- package/dist/commands/identity.js.map +1 -1
- package/dist/commands/incidents.d.ts.map +1 -1
- package/dist/commands/incidents.js +2 -3
- package/dist/commands/incidents.js.map +1 -1
- package/dist/commands/jobs.d.ts.map +1 -1
- package/dist/commands/jobs.js +5 -9
- package/dist/commands/jobs.js.map +1 -1
- package/dist/commands/mcp-proxy.d.ts +4 -4
- package/dist/commands/mcp-proxy.d.ts.map +1 -1
- package/dist/commands/mcp-proxy.js +49 -39
- package/dist/commands/mcp-proxy.js.map +1 -1
- package/dist/commands/messages.d.ts.map +1 -1
- package/dist/commands/messages.js +2 -4
- package/dist/commands/messages.js.map +1 -1
- package/dist/commands/open.d.ts +4 -2
- package/dist/commands/open.d.ts.map +1 -1
- package/dist/commands/open.js +17 -18
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/plugins.d.ts.map +1 -1
- package/dist/commands/plugins.js +16 -32
- package/dist/commands/plugins.js.map +1 -1
- package/dist/commands/process-instances.d.ts.map +1 -1
- package/dist/commands/process-instances.js +5 -9
- package/dist/commands/process-instances.js.map +1 -1
- package/dist/commands/profiles.d.ts.map +1 -1
- package/dist/commands/profiles.js +8 -16
- package/dist/commands/profiles.js.map +1 -1
- package/dist/commands/resource-extensions.d.ts +8 -0
- package/dist/commands/resource-extensions.d.ts.map +1 -0
- package/dist/commands/resource-extensions.js +20 -0
- package/dist/commands/resource-extensions.js.map +1 -0
- package/dist/commands/run.d.ts +2 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +10 -4
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/search.d.ts.map +1 -1
- package/dist/commands/search.js +4 -8
- package/dist/commands/search.js.map +1 -1
- package/dist/commands/session.d.ts.map +1 -1
- package/dist/commands/session.js +4 -7
- package/dist/commands/session.js.map +1 -1
- package/dist/commands/user-tasks.d.ts.map +1 -1
- package/dist/commands/user-tasks.js +2 -3
- package/dist/commands/user-tasks.js.map +1 -1
- package/dist/commands/variables.d.ts +10 -0
- package/dist/commands/variables.d.ts.map +1 -0
- package/dist/commands/variables.js +51 -0
- package/dist/commands/variables.js.map +1 -0
- package/dist/commands/watch.d.ts +7 -5
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +90 -39
- package/dist/commands/watch.js.map +1 -1
- package/dist/errors.d.ts +62 -8
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +99 -10
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -39
- package/dist/index.js.map +1 -1
- package/package.json +7 -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 {
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
if (
|
|
107
|
-
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
try
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
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
|
-
(
|
|
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
|
|
552
|
-
/**
|
|
553
|
-
|
|
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
|
-
|
|
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
|