Skip to content

Commit dde712d

Browse files
committed
feat: add watch command to monitor files for changes and auto-deploy
1 parent fce3a30 commit dde712d

File tree

6 files changed

+172
-0
lines changed

6 files changed

+172
-0
lines changed

src/commands/help.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Commands:
5050
correlate msg <name> Correlate message
5151
deploy [path...] Deploy BPMN/DMN/forms
5252
run <path> Deploy and start process
53+
watch [path...] Watch files for changes and auto-deploy
5354
add profile <name> Add a profile
5455
remove profile <name> Remove a profile (alias: rm)
5556
load plugin <name> Load a c8ctl plugin from npm registry
@@ -77,6 +78,7 @@ Examples:
7778
c8ctl create pi --bpmnProcessId=myProcess
7879
c8ctl deploy ./my-process.bpmn Deploy a BPMN file
7980
c8ctl run ./my-process.bpmn Deploy and start process
81+
c8ctl watch ./src Watch directory for changes
8082
c8ctl use profile prod Set active profile
8183
c8ctl output json Switch to JSON output
8284
c8ctl load plugin my-plugin Load plugin from npm registry

src/commands/watch.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Watch command - monitor files for changes and auto-deploy
3+
*/
4+
5+
import { watch } from 'node:fs';
6+
import { resolve, extname, basename } from 'node:path';
7+
import { existsSync, statSync } from 'node:fs';
8+
import { getLogger } from '../logger.ts';
9+
import { deploy } from './deployments.ts';
10+
11+
const WATCHED_EXTENSIONS = ['.bpmn', '.dmn', '.form'];
12+
13+
/**
14+
* Watch for file changes and auto-deploy
15+
*/
16+
export async function watchFiles(paths: string[], options: {
17+
profile?: string;
18+
}): Promise<void> {
19+
const logger = getLogger();
20+
21+
if (!paths || paths.length === 0) {
22+
paths = ['.'];
23+
}
24+
25+
// Resolve all paths
26+
const resolvedPaths = paths.map(p => resolve(p));
27+
28+
// Validate paths exist
29+
for (const path of resolvedPaths) {
30+
if (!existsSync(path)) {
31+
logger.error(`Path does not exist: ${path}`);
32+
process.exit(1);
33+
}
34+
}
35+
36+
logger.info(`👁️ Watching for changes in: ${resolvedPaths.join(', ')}`);
37+
logger.info(`📋 Monitoring extensions: ${WATCHED_EXTENSIONS.join(', ')}`);
38+
logger.info('Press Ctrl+C to stop watching\n');
39+
40+
// Keep track of recently deployed files to avoid duplicate deploys
41+
const recentlyDeployed = new Map<string, number>();
42+
const DEPLOY_COOLDOWN = 1000; // 1 second cooldown
43+
44+
// Watch each path
45+
for (const path of resolvedPaths) {
46+
const stats = statSync(path);
47+
const isDirectory = stats.isDirectory();
48+
49+
const watcher = watch(path, { recursive: isDirectory }, async (eventType, filename) => {
50+
if (!filename) return;
51+
52+
const ext = extname(filename);
53+
if (!WATCHED_EXTENSIONS.includes(ext)) {
54+
return;
55+
}
56+
57+
const fullPath = isDirectory ? resolve(path, filename) : path;
58+
59+
// Check cooldown to prevent duplicate deploys
60+
const lastDeploy = recentlyDeployed.get(fullPath);
61+
const now = Date.now();
62+
if (lastDeploy && (now - lastDeploy) < DEPLOY_COOLDOWN) {
63+
return;
64+
}
65+
66+
// Check if file still exists (might have been deleted)
67+
if (!existsSync(fullPath)) {
68+
logger.info(`⚠️ File deleted, skipping: ${basename(filename)}`);
69+
return;
70+
}
71+
72+
logger.info(`\n🔄 Change detected: ${basename(filename)}`);
73+
recentlyDeployed.set(fullPath, now);
74+
75+
try {
76+
await deploy([fullPath], { profile: options.profile });
77+
} catch (error) {
78+
logger.error(`Failed to deploy ${basename(filename)}`, error as Error);
79+
}
80+
});
81+
82+
// Handle watcher errors
83+
watcher.on('error', (error) => {
84+
logger.error('Watcher error', error);
85+
});
86+
}
87+
88+
// Keep process alive
89+
process.on('SIGINT', () => {
90+
logger.info('\n\n🍹 - bottoms up.');
91+
process.exit(0);
92+
});
93+
}

src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { publishMessage, correlateMessage } from './commands/messages.ts';
2323
import { getTopology } from './commands/topology.ts';
2424
import { deploy } from './commands/deployments.ts';
2525
import { run } from './commands/run.ts';
26+
import { watchFiles } from './commands/watch.ts';
2627
import { loadPlugin, unloadPlugin, listPlugins } from './commands/plugins.ts';
2728
import {
2829
loadInstalledPlugins,
@@ -425,6 +426,15 @@ async function main() {
425426
return;
426427
}
427428

429+
// Handle watch command
430+
if (verb === 'watch' || verb === 'w') {
431+
const paths = resource ? [resource, ...args] : (args.length > 0 ? args : ['.']);
432+
await watchFiles(paths, {
433+
profile: values.profile as string | undefined,
434+
});
435+
return;
436+
}
437+
428438
// Try to execute plugin command (before verb-only check)
429439
if (await executePluginCommand(verb, resource ? [resource, ...args] : args)) {
430440
return;

tests/fixtures/list-pis/.process-application

Whitespace-only changes.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_015jdpz" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.43.1" modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.8.0">
3+
<bpmn:process id="Process_0t60ay7" isExecutable="true">
4+
<bpmn:startEvent id="StartEvent_1">
5+
<bpmn:outgoing>Flow_1khnnfz</bpmn:outgoing>
6+
</bpmn:startEvent>
7+
<bpmn:sequenceFlow id="Flow_1khnnfz" sourceRef="StartEvent_1" targetRef="Activity_13vzxt6" />
8+
<bpmn:endEvent id="Event_135jrp7">
9+
<bpmn:incoming>Flow_0iusive</bpmn:incoming>
10+
</bpmn:endEvent>
11+
<bpmn:sequenceFlow id="Flow_0iusive" sourceRef="Activity_13vzxt6" targetRef="Event_135jrp7" />
12+
<bpmn:userTask id="Activity_13vzxt6">
13+
<bpmn:extensionElements>
14+
<zeebe:userTask />
15+
<zeebe:formDefinition formId="some-form" />
16+
</bpmn:extensionElements>
17+
<bpmn:incoming>Flow_1khnnfz</bpmn:incoming>
18+
<bpmn:outgoing>Flow_0iusive</bpmn:outgoing>
19+
</bpmn:userTask>
20+
</bpmn:process>
21+
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
22+
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0t60ay7">
23+
<bpmndi:BPMNShape id="Event_135jrp7_di" bpmnElement="Event_135jrp7">
24+
<dc:Bounds x="422" y="102" width="36" height="36" />
25+
</bpmndi:BPMNShape>
26+
<bpmndi:BPMNShape id="Activity_0pfgzk4_di" bpmnElement="Activity_13vzxt6">
27+
<dc:Bounds x="270" y="80" width="100" height="80" />
28+
</bpmndi:BPMNShape>
29+
<bpmndi:BPMNShape id="StartEvent_1_di" bpmnElement="StartEvent_1">
30+
<dc:Bounds x="182" y="132" width="36" height="36" />
31+
</bpmndi:BPMNShape>
32+
<bpmndi:BPMNEdge id="Flow_1khnnfz_di" bpmnElement="Flow_1khnnfz">
33+
<di:waypoint x="218" y="150" />
34+
<di:waypoint x="244" y="150" />
35+
<di:waypoint x="244" y="120" />
36+
<di:waypoint x="270" y="120" />
37+
</bpmndi:BPMNEdge>
38+
<bpmndi:BPMNEdge id="Flow_0iusive_di" bpmnElement="Flow_0iusive">
39+
<di:waypoint x="370" y="120" />
40+
<di:waypoint x="422" y="120" />
41+
</bpmndi:BPMNEdge>
42+
</bpmndi:BPMNPlane>
43+
</bpmndi:BPMNDiagram>
44+
</bpmn:definitions>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"components": [
3+
{
4+
"label": "Text field 1",
5+
"type": "textfield",
6+
"layout": {
7+
"row": "Row_0ooayjp",
8+
"columns": null
9+
},
10+
"id": "Field_1vt56r5",
11+
"key": "textfield_c9fwqc"
12+
}
13+
],
14+
"type": "default",
15+
"id": "some-form",
16+
"executionPlatform": "Camunda Cloud",
17+
"executionPlatformVersion": "8.8.0",
18+
"exporter": {
19+
"name": "Camunda Modeler",
20+
"version": "5.43.1"
21+
},
22+
"schemaVersion": 19
23+
}

0 commit comments

Comments
 (0)