@camunda8/cli 2.7.0-alpha.6 → 2.7.0-alpha.7

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/README.md CHANGED
@@ -65,6 +65,7 @@ c8ctl help fail # Shows fail command with all flags
65
65
  c8ctl help activate # Shows activate command with all flags
66
66
  c8ctl help publish # Shows publish command with all flags
67
67
  c8ctl help correlate # Shows correlate command with all flags
68
+ c8ctl help cluster # Shows local cluster management help
68
69
  c8ctl help profiles # Shows profile management help
69
70
  c8ctl help plugin # Shows plugin management help
70
71
 
@@ -568,6 +569,67 @@ c8ctl help list # → JSON for specific command
568
569
 
569
570
  ---
570
571
 
572
+ ### Local Cluster
573
+
574
+ c8ctl includes a built-in `cluster` command for managing a local Camunda 8 instance (powered by a default plugin). No Docker or docker-compose required — it downloads and runs Camunda directly.
575
+
576
+ ```bash
577
+ # Start the latest stable version
578
+ c8ctl cluster start
579
+
580
+ # Start a specific version
581
+ c8ctl cluster start 8.9
582
+ c8ctl cluster start 8.9.0-alpha5
583
+
584
+ # Stop the running cluster
585
+ c8ctl cluster stop
586
+
587
+ # Check cluster status
588
+ c8ctl cluster status
589
+
590
+ # Stream cluster logs
591
+ c8ctl cluster logs
592
+
593
+ # List locally cached versions
594
+ c8ctl cluster list
595
+
596
+ # List available remote versions
597
+ c8ctl cluster list-remote
598
+
599
+ # Pre-download a version without starting it
600
+ c8ctl cluster install 8.9
601
+
602
+ # Remove a cached version
603
+ c8ctl cluster delete 8.9
604
+ ```
605
+
606
+ #### Version Aliases
607
+
608
+ Instead of an exact version number, you can use:
609
+
610
+ - **`stable`** — the latest GA release (highest minor version that has shipped a `.0` release)
611
+ - **`alpha`** — the latest alpha-train release (highest minor version overall, which may only have alpha builds)
612
+ - **A major.minor pattern** like `8.9` — resolves to the latest patch/alpha within that minor
613
+
614
+ `c8ctl cluster start` with no version argument defaults to `stable`.
615
+
616
+ ```bash
617
+ c8ctl cluster start stable
618
+ c8ctl cluster start alpha
619
+ c8ctl cluster start 8.9 # latest 8.9.x
620
+ ```
621
+
622
+ #### Online vs Offline Behaviour
623
+
624
+ - **`cluster start`** prefers locally cached versions. If the requested version is already installed, it starts immediately without going online. A non-blocking background check runs to hint if a newer build is available, but never delays startup.
625
+ - **`cluster install`** always checks the remote download server for the latest build. If a newer ETag is detected for an already-installed version, it re-downloads.
626
+ - **`cluster list-remote`** fetches the full list of available versions from the download server.
627
+ - **Offline fallback**: if the network is unavailable, alias resolution falls back to a locally cached mapping, then to a hardcoded default.
628
+
629
+ Run `c8ctl help cluster` for full details. See [EXAMPLES.md](EXAMPLES.md#local-cluster) for a complete local development workflow.
630
+
631
+ ---
632
+
571
633
  ### Core Components
572
634
 
573
635
  - **Logger** (`src/logger.ts`): Handles output in text or JSON mode
@@ -604,6 +666,7 @@ c8ctl <verb> <resource> [arguments] [flags]
604
666
  - `sync` - Synchronize plugins
605
667
  - `use` - Set active profile or tenant
606
668
  - `output` - Show or set output format
669
+ - `cluster` - Manage local Camunda 8 cluster (start, stop, status, logs, install, delete, list, list-remote)
607
670
  - `completion` - Generate shell completion script
608
671
  - `feedback` - Open the feedback page to report issues or request features
609
672
 
@@ -892,12 +892,115 @@ async function startC8Run(config, debug = false) {
892
892
  }
893
893
  }
894
894
 
895
- async function stopC8Run(config) {
895
+ export function hasRunningClusterPidfiles(cacheDir) {
896
+ if (!existsSync(cacheDir)) {
897
+ return false;
898
+ }
899
+
900
+ const versionDirs = readdirSync(cacheDir, { withFileTypes: true })
901
+ .filter(
902
+ (entry) =>
903
+ entry.isDirectory() && entry.name.startsWith('c8run-'),
904
+ )
905
+ .map((entry) => join(cacheDir, entry.name));
906
+
907
+ // Scan each version dir and its immediate subdirectories (max depth 1)
908
+ // rather than a full recursive DFS — c8run installs can contain large
909
+ // extracted trees, logs, and data that would make a deep walk slow.
910
+ for (const versionDir of versionDirs) {
911
+ let entries;
912
+ try {
913
+ entries = readdirSync(versionDir, { withFileTypes: true });
914
+ } catch {
915
+ continue;
916
+ }
917
+
918
+ // Collect directories at this level to also check one level deeper.
919
+ const dirsToCheck = [versionDir];
920
+ for (const entry of entries) {
921
+ if (entry.isDirectory()) {
922
+ dirsToCheck.push(join(versionDir, entry.name));
923
+ }
924
+ }
925
+
926
+ for (const dir of dirsToCheck) {
927
+ let dirEntries;
928
+ try {
929
+ dirEntries = readdirSync(dir, { withFileTypes: true });
930
+ } catch {
931
+ continue;
932
+ }
933
+
934
+ for (const entry of dirEntries) {
935
+ if (!entry.isFile() || !entry.name.endsWith('.process')) {
936
+ continue;
937
+ }
938
+
939
+ const entryPath = join(dir, entry.name);
940
+
941
+ let pid;
942
+ try {
943
+ pid = Number.parseInt(readFileSync(entryPath, 'utf-8').trim(), 10);
944
+ } catch {
945
+ // Pidfile may have been removed between listing and reading.
946
+ continue;
947
+ }
948
+
949
+ if (!Number.isInteger(pid) || pid <= 0) {
950
+ continue;
951
+ }
952
+
953
+ try {
954
+ process.kill(pid, 0);
955
+ return true;
956
+ } catch (error) {
957
+ if (error && error.code === 'EPERM') {
958
+ return true;
959
+ }
960
+ }
961
+ }
962
+ }
963
+ }
964
+
965
+ return false;
966
+ }
967
+
968
+ export async function stopC8Run(config, debug = false) {
896
969
  const logger = getLogger();
897
970
  const markerFile = join(config.cacheDir, ACTIVE_MARKER_FILE);
898
971
  const versionFile = join(config.cacheDir, VERSION_MARKER_FILE);
899
972
 
900
973
  const markerExists = existsSync(markerFile);
974
+ const clusterAppearsRunning = hasRunningClusterPidfiles(config.cacheDir);
975
+
976
+ if (!markerExists && !clusterAppearsRunning) {
977
+ logger.warn(
978
+ 'No cluster is currently running.',
979
+ );
980
+ return;
981
+ }
982
+
983
+ if (markerExists && !clusterAppearsRunning) {
984
+ // Stale marker left behind (e.g. crash or forced kill). Clean up and
985
+ // return early — there is nothing to stop.
986
+ logger.warn(
987
+ 'Cluster marker file found, but no running cluster processes detected. Cleaning up stale marker.',
988
+ );
989
+ if (existsSync(markerFile)) {
990
+ rmSync(markerFile);
991
+ }
992
+ if (existsSync(versionFile)) {
993
+ rmSync(versionFile);
994
+ }
995
+ return;
996
+ }
997
+
998
+ if (!markerExists && clusterAppearsRunning) {
999
+ logger.warn(
1000
+ 'Cluster marker file is missing, but running cluster processes were detected. Proceeding with stop.',
1001
+ );
1002
+ }
1003
+
901
1004
  const installedVersions = existsSync(config.cacheDir)
902
1005
  ? readdirSync(config.cacheDir, { withFileTypes: true })
903
1006
  .filter(
@@ -908,13 +1011,6 @@ async function stopC8Run(config) {
908
1011
  .sort()
909
1012
  : [];
910
1013
 
911
- if (!markerExists && installedVersions.length === 0) {
912
- logger.warn(
913
- 'No running cluster found (use "c8ctl cluster start" to start one).',
914
- );
915
- return;
916
- }
917
-
918
1014
  const versionsToTry = [];
919
1015
 
920
1016
  if (existsSync(versionFile)) {
@@ -949,18 +1045,53 @@ async function stopC8Run(config) {
949
1045
 
950
1046
  attempted += 1;
951
1047
 
952
- const { code: exitCode, signal: exitSignal } = await new Promise(
1048
+ const { code: exitCode, signal: exitSignal, stdout: procStdout, stderr: procStderr } = await new Promise(
953
1049
  (resolve, reject) => {
954
1050
  const proc = spawn(binaryPath, ['stop'], {
955
- stdio: 'inherit',
1051
+ stdio: ['ignore', 'pipe', 'pipe'],
956
1052
  cwd: dirname(binaryPath),
957
1053
  });
958
- proc.on('exit', (code, signal) => resolve({ code, signal }));
1054
+
1055
+ const stdoutChunks = [];
1056
+ const stderrChunks = [];
1057
+ let stdoutLen = 0;
1058
+ let stderrLen = 0;
1059
+ const MAX_BUFFER = 64 * 1024; // 64 KB cap per stream
1060
+
1061
+ proc.stdout?.on('data', (chunk) => {
1062
+ const text = typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
1063
+ if (debug) {
1064
+ process.stdout.write(text);
1065
+ }
1066
+ if (stdoutLen < MAX_BUFFER) {
1067
+ stdoutChunks.push(text);
1068
+ stdoutLen += text.length;
1069
+ }
1070
+ });
1071
+ proc.stderr?.on('data', (chunk) => {
1072
+ const text = typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
1073
+ if (debug) {
1074
+ process.stderr.write(text);
1075
+ }
1076
+ if (stderrLen < MAX_BUFFER) {
1077
+ stderrChunks.push(text);
1078
+ stderrLen += text.length;
1079
+ }
1080
+ });
1081
+
1082
+ proc.on('close', (code, signal) =>
1083
+ resolve({
1084
+ code,
1085
+ signal,
1086
+ stdout: stdoutChunks.join(''),
1087
+ stderr: stderrChunks.join(''),
1088
+ }),
1089
+ );
959
1090
  proc.on('error', reject);
960
1091
  },
961
1092
  ).catch((error) => {
962
1093
  lastError = error instanceof Error ? error : new Error(String(error));
963
- return { code: -1, signal: null };
1094
+ return { code: -1, signal: null, stdout: '', stderr: '' };
964
1095
  });
965
1096
 
966
1097
  if (exitCode === 0 || (exitCode === null && !exitSignal)) {
@@ -971,7 +1102,10 @@ async function stopC8Run(config) {
971
1102
  `Stop command terminated by signal ${exitSignal}`,
972
1103
  );
973
1104
  } else if (exitCode !== -1) {
974
- lastError = new Error(`Stop command failed with code ${exitCode}`);
1105
+ const output = [procStdout, procStderr].filter(Boolean).join('\n');
1106
+ lastError = new Error(
1107
+ `Stop command failed with code ${exitCode}${output ? `:\n${output}` : ''}`,
1108
+ );
975
1109
  }
976
1110
  }
977
1111
 
@@ -1522,7 +1656,7 @@ export const commands = {
1522
1656
  }
1523
1657
  } else if (parsed.subcommand === 'stop') {
1524
1658
  try {
1525
- await stopC8Run(config);
1659
+ await stopC8Run(config, parsed.debug);
1526
1660
  } catch (error) {
1527
1661
  logger.error(`Failed to stop cluster: ${error}`);
1528
1662
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda8/cli",
3
- "version": "2.7.0-alpha.6",
3
+ "version": "2.7.0-alpha.7",
4
4
  "description": "Camunda 8 CLI - minimal-dependency CLI for Camunda 8 operations",
5
5
  "type": "module",
6
6
  "engines": {