@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 +63 -0
- package/dist/default-plugins/cluster/c8ctl-plugin.js +148 -14
- package/package.json +1 -1
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
|
-
|
|
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: '
|
|
1051
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
956
1052
|
cwd: dirname(binaryPath),
|
|
957
1053
|
});
|
|
958
|
-
|
|
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
|
-
|
|
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);
|