chalkbridge 1.0.0 → 1.0.3
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/CHANGELOG.md +100 -0
- package/README.md +16 -0
- package/dist/{chunk-HFKCLTVU.js → chunk-VTFAXWQJ.js} +18 -4
- package/dist/cli.js +356 -46
- package/dist/index.js +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# chalkbridge
|
|
2
|
+
|
|
3
|
+
## 1.0.3
|
|
4
|
+
|
|
5
|
+
**An upgrade now actually arrives, a bridge can be stopped, and it ends when you are
|
|
6
|
+
done with it.**
|
|
7
|
+
|
|
8
|
+
(1.0.2 was built and verified but never published — everything below shipped as 1.0.3.)
|
|
9
|
+
|
|
10
|
+
The bridge is spawned detached and outlives the session that started it — deliberately,
|
|
11
|
+
because writes have to succeed with no browser connected. But `ensureBridge` reused
|
|
12
|
+
whatever was already listening without ever checking its version, so `npx -y chalkbridge
|
|
13
|
+
mcp` would fetch a new package, find the **old** bridge still answering, and reuse it.
|
|
14
|
+
Every fix would ship and none would arrive, silently. If 1.0.1 seemed not to change
|
|
15
|
+
anything for you, that is why.
|
|
16
|
+
|
|
17
|
+
- **A bridge from another build is replaced.** It is asked to stop first, so its pending
|
|
18
|
+
`.excalidraw` write still reaches the disk.
|
|
19
|
+
- **`chalkbridge stop`** ends the bridge for the current repo, flushing anything unsaved.
|
|
20
|
+
Until now the only options were a reboot or hunting the pid.
|
|
21
|
+
- **`POST /api/shutdown`** is what both use — a request rather than a signal, because on
|
|
22
|
+
Windows a signal from another process terminates outright and the flush never happens.
|
|
23
|
+
- **A pid fallback for bridges built before this release.** 1.0.0 and 1.0.1 have no
|
|
24
|
+
shutdown endpoint, so the polite path is unavailable for exactly the bridges an upgrade
|
|
25
|
+
has to replace. Without it, upgrading from either would leave the new build unable to
|
|
26
|
+
start at all. Found by running the published 1.0.1 against a local build.
|
|
27
|
+
|
|
28
|
+
**And it ends when you are done with it.** Until now a bridge ran until the machine
|
|
29
|
+
restarted — one per repo, forever — which is not what anybody expects after closing a chat
|
|
30
|
+
and a tab. It now exits when nothing is watching *and* nothing is asking, for 90 seconds:
|
|
31
|
+
|
|
32
|
+
| | |
|
|
33
|
+
|---|---|
|
|
34
|
+
| chat closed, tab closed | exits |
|
|
35
|
+
| tab refreshed | reconnects in about a second, so it stays |
|
|
36
|
+
| chat closed, tab still open | stays — somebody is looking at the canvas |
|
|
37
|
+
| Claude drawing with no tab open | every tool call counts, so it stays |
|
|
38
|
+
| two chats on one repo | activity keeps it alive |
|
|
39
|
+
|
|
40
|
+
One rule, no session counting. `CHALK_IDLE_TIMEOUT` tunes it and `0` restores the old
|
|
41
|
+
behaviour.
|
|
42
|
+
|
|
43
|
+
**A tool call now survives its own bridge.** That is what makes the above invisible: if a
|
|
44
|
+
bridge has given up, the next call re-runs the bootstrap — spawning one and reopening the
|
|
45
|
+
canvas — instead of failing. Previously a bridge lost mid-session meant every call failed
|
|
46
|
+
until Claude Code was restarted.
|
|
47
|
+
|
|
48
|
+
After this release, updating really is just restarting Claude Code.
|
|
49
|
+
|
|
50
|
+
## 1.0.1
|
|
51
|
+
|
|
52
|
+
**Arrows are drawn where they belong, the first time.**
|
|
53
|
+
|
|
54
|
+
Six bugs of the same family, all found by drawing one real diagram — 29 boxes, 34 arrows —
|
|
55
|
+
which cost roughly forty tool calls of workaround. Every one of them answered `applied`
|
|
56
|
+
and then did less than that, which is the most expensive shape a bug can have: the caller
|
|
57
|
+
spends its next calls re-reading to find out why.
|
|
58
|
+
|
|
59
|
+
- **`chalk_connect` now places the arrow.** The expander returns a stub at the origin with
|
|
60
|
+
`points: [[0.5, 0], [99.5, 0]]` — the bindings were correct and the geometry was a
|
|
61
|
+
placeholder — so every new arrow landed in the same corner and looked like a scribble.
|
|
62
|
+
- **`chalk_update_elements` redraws the arrows of a box it moved**, and brings the box's
|
|
63
|
+
caption with it. Only `chalk_arrange` used to do either, so the only way to repair a
|
|
64
|
+
diagram was to displace every element and align it back, one row at a time.
|
|
65
|
+
- **`chalk_frame` works more than once.** Upstream rebuilds a frame from its `children`,
|
|
66
|
+
which a frame that already exists does not have, so it threw — and *every second frame
|
|
67
|
+
failed*, whatever was in it.
|
|
68
|
+
- **`patch.role` restyles** instead of writing a junk `role` field onto the element.
|
|
69
|
+
Explicit properties still win over what the role implies.
|
|
70
|
+
- **`patch.label` renames the caption** instead of writing a junk `label` field, and says
|
|
71
|
+
so plainly when there is no caption to rename rather than silently doing nothing.
|
|
72
|
+
- **Deleting a container tombstones its caption**, instead of leaving a word floating on
|
|
73
|
+
the canvas that nothing owns.
|
|
74
|
+
|
|
75
|
+
`applied` and `deleted` now list only the ids you asked about. A caption that followed its
|
|
76
|
+
box is a consequence, not a result you should have to reason about.
|
|
77
|
+
|
|
78
|
+
No schema changes — every tool takes and returns exactly what it did in 1.0.0.
|
|
79
|
+
|
|
80
|
+
## 1.0.0
|
|
81
|
+
|
|
82
|
+
First release.
|
|
83
|
+
|
|
84
|
+
A self-hosted Excalidraw canvas that Claude Code can read and draw on. The canvas opens in
|
|
85
|
+
your browser, you select on it and ask in the terminal, and the diagram is a plain
|
|
86
|
+
`.excalidraw` file in your repository.
|
|
87
|
+
|
|
88
|
+
- **Read** — `chalk_describe_scene` renders the canvas as a compact text form, 3.7× smaller
|
|
89
|
+
than the same structure as JSON. `chalk_get_selection` answers "what are these?"
|
|
90
|
+
including the edges that cross the selection boundary.
|
|
91
|
+
- **Draw** — boxes by `role` rather than by colour, arrows bound to their boxes, layered
|
|
92
|
+
graph layout, frames, groups, annotations. Writes succeed with no browser connected.
|
|
93
|
+
- **Illustrate** — a machine-wide icon catalog with FTS5 search; a hand-drawn set is
|
|
94
|
+
bundled and `chalk_install_pack("devicon")` adds technology logos in one call. Vendor
|
|
95
|
+
sets return instructions and are never downloaded for you.
|
|
96
|
+
- **Export** — PNG and SVG rendered by your own browser, and `.excalidraw` written where
|
|
97
|
+
you asked.
|
|
98
|
+
- **Undo** — labelled snapshots.
|
|
99
|
+
|
|
100
|
+
Node 22 or newer. No native modules, no container, nothing leaves the machine.
|
package/README.md
CHANGED
|
@@ -53,10 +53,26 @@ reach loopback. Do not work around it by binding a wider address.
|
|
|
53
53
|
```bash
|
|
54
54
|
chalkbridge health # what is running for this repository
|
|
55
55
|
chalkbridge serve # start the bridge by hand
|
|
56
|
+
chalkbridge stop # end it, flushing anything unsaved
|
|
56
57
|
chalkbridge icons list # what is in the machine-wide icon catalog
|
|
57
58
|
chalkbridge icons install devicon
|
|
58
59
|
```
|
|
59
60
|
|
|
61
|
+
**The bridge is spawned detached and outlives your session** — deliberately, because
|
|
62
|
+
writes have to succeed with no browser connected. It ends when nothing is watching *and*
|
|
63
|
+
nothing is asking:
|
|
64
|
+
|
|
65
|
+
| | |
|
|
66
|
+
|---|---|
|
|
67
|
+
| chat closed, tab closed | exits after 90 seconds |
|
|
68
|
+
| tab refreshed | reconnects in about a second, so it stays |
|
|
69
|
+
| chat closed, tab still open | stays — you are still looking at the canvas |
|
|
70
|
+
| Claude drawing with no tab | every tool call counts, so it stays |
|
|
71
|
+
|
|
72
|
+
If it has already given up, the next tool call brings it back and reopens the canvas, so
|
|
73
|
+
you never have to think about it. `chalkbridge stop` ends one now; `CHALK_IDLE_TIMEOUT`
|
|
74
|
+
tunes the ninety seconds, and `0` keeps it up forever.
|
|
75
|
+
|
|
60
76
|
`CHALK_NO_AUTOSTART=1` and `CHALK_NO_OPEN=1` turn off the two automatic behaviours;
|
|
61
77
|
`CHALK_PORT` pins the port; `CHALK_DATA_DIR` moves the icon catalog.
|
|
62
78
|
|
|
@@ -1248,6 +1248,14 @@ var bridgeUnreachable = (port) => port === null ? err("BRIDGE_UNREACHABLE", "no
|
|
|
1248
1248
|
hint: "the bridge may have exited \u2014 `chalkbridge health` reports what is running",
|
|
1249
1249
|
detail: { port }
|
|
1250
1250
|
});
|
|
1251
|
+
var bridgeVersionMismatch = (running, ours, port) => err(
|
|
1252
|
+
"BRIDGE_VERSION_MISMATCH",
|
|
1253
|
+
`a Chalkbridge ${running} bridge is running on 127.0.0.1:${port} and this is ${ours}`,
|
|
1254
|
+
{
|
|
1255
|
+
hint: "it did not stop when asked \u2014 run `chalkbridge stop` in this repo, or end that process, and try again",
|
|
1256
|
+
detail: { running, ours, port }
|
|
1257
|
+
}
|
|
1258
|
+
);
|
|
1251
1259
|
var portUnavailable = (from, tried) => err("PORT_UNAVAILABLE", `no free port between ${from} and ${from + tried - 1}`, {
|
|
1252
1260
|
hint: "something is occupying the whole range \u2014 set CHALK_PORT to pick a different one",
|
|
1253
1261
|
detail: { from, tried }
|
|
@@ -1266,6 +1274,11 @@ var unknownElement = (id) => err("UNKNOWN_ELEMENT", `no element ${id} in this sc
|
|
|
1266
1274
|
tool: "chalk_describe_scene",
|
|
1267
1275
|
detail: { id }
|
|
1268
1276
|
});
|
|
1277
|
+
var noBoundLabel = (id) => err("NO_BOUND_LABEL", `element ${id} has no caption to rename`, {
|
|
1278
|
+
hint: "draw it with a label in the first place, or put the text beside it with chalk_annotate",
|
|
1279
|
+
tool: "chalk_annotate",
|
|
1280
|
+
detail: { id }
|
|
1281
|
+
});
|
|
1269
1282
|
var emptySelection = (tool) => err("EMPTY_SELECTION", "no elements named \u2014 there is nothing to act on", {
|
|
1270
1283
|
hint: "pass the ids you mean, or call chalk_get_selection to find out what the user is pointing at",
|
|
1271
1284
|
tool: "chalk_get_selection",
|
|
@@ -1453,7 +1466,8 @@ function createNodeSkeletonExpander() {
|
|
|
1453
1466
|
if (!loaded.ok) return loaded;
|
|
1454
1467
|
let raw;
|
|
1455
1468
|
try {
|
|
1456
|
-
|
|
1469
|
+
const convertible = (context ?? []).filter((element) => element.type !== "frame");
|
|
1470
|
+
raw = loaded.value([...convertible, ...skeletons], { regenerateIds: false });
|
|
1457
1471
|
} catch (thrown) {
|
|
1458
1472
|
return fail(expansionFailed(describeThrown(thrown)));
|
|
1459
1473
|
}
|
|
@@ -1489,6 +1503,6 @@ var stable = (element) => {
|
|
|
1489
1503
|
return rest;
|
|
1490
1504
|
};
|
|
1491
1505
|
|
|
1492
|
-
export { AnnotateInput, AnnotateOutput, ApiError, ArrangeOutput, ArrangeToolInput, BINDING_FIELDS, ClientMessage, ConnectOutput, ConnectToolInput, CreateElementsInput, CreateElementsOutput, DebugElementRequest, DeleteElementsInput, DeleteElementsOutput, DescribeSceneInput, DescribeSceneOutput, DetailLevel, EXCALIDRAW_FILE_TYPE, EXCALIDRAW_FILE_VERSION, ElementId, EmbedInput, EmbedOutput, ExcalidrawFile, ExportImageInput, ExportImageOutput, ExportSceneInput, ExportSceneOutput, FLAG_GLYPH, FileId, FocusInput, FocusOutput, FrameId, FrameInput, FrameOutput, GUIDE_TOPICS, GetElementsInput, GetElementsOutput, GetSelectionInput, GetSelectionOutput, GroupId, GroupInput, GroupOutput, GuideTopic, HOUSE, HealthResponse, IconId, IconItem, ImportSceneInput, ImportSceneOutput, Index, InstallPackInput, InstallPackOutput, KIND_ABBREVIATION, ListPacksInput, ListPacksOutput, ListScenesInput, ListScenesOutput, PROTOCOL_VERSION, PackKind, PackName, PackRecipe, PlaceIconInput, PlaceIconOutput, ROUNDNESS, RawElementsInput, RawElementsOutput, ReadMeInput, ReadMeOutput, RemovePackInput, RemovePackOutput, RestoreSnapshotInput, RestoreSnapshotOutput, SERVER_MANAGED_FIELDS, STALE_SELECTION_MS, Scene, SceneElement, SceneId, SceneSettingsInput, SceneSettingsOutput, ScreenshotInput, ScreenshotOutput, ScreenshotResponse, SearchIconsInput, SearchIconsOutput, SnapshotId, SnapshotInput, SnapshotOutput, UngroupInput, UngroupOutput, UpdateElementsInput, UpdateElementsOutput, assertNever, bridgeSpawnFailed, bridgeUnreachable, catalogUnavailable, createNodeSkeletonExpander, deepEqual, describeThrown, diskConflict, emptySelection, expansionFailed, exportFailed, exportTimedOut, fail, frameIdOf, iconIdOf, idCollision, isFail, isOk, noBrowser, noScenes, notAPack, notARepo, ok, packInstallFailed, partition, pathOutsideRepo, point, portUnavailable, refusedFieldMessage, sceneFileMissing, staleVersion, storeUnavailable, unknownElement, unknownIcon, unknownScene, unknownSnapshot, unreadableSceneFile };
|
|
1493
|
-
//# sourceMappingURL=chunk-
|
|
1494
|
-
//# sourceMappingURL=chunk-
|
|
1506
|
+
export { AnnotateInput, AnnotateOutput, ApiError, ArrangeOutput, ArrangeToolInput, BINDING_FIELDS, ClientMessage, ConnectOutput, ConnectToolInput, CreateElementsInput, CreateElementsOutput, DebugElementRequest, DeleteElementsInput, DeleteElementsOutput, DescribeSceneInput, DescribeSceneOutput, DetailLevel, EXCALIDRAW_FILE_TYPE, EXCALIDRAW_FILE_VERSION, ElementId, EmbedInput, EmbedOutput, ExcalidrawFile, ExportImageInput, ExportImageOutput, ExportSceneInput, ExportSceneOutput, FLAG_GLYPH, FileId, FocusInput, FocusOutput, FrameId, FrameInput, FrameOutput, GUIDE_TOPICS, GetElementsInput, GetElementsOutput, GetSelectionInput, GetSelectionOutput, GroupId, GroupInput, GroupOutput, GuideTopic, HOUSE, HealthResponse, IconId, IconItem, ImportSceneInput, ImportSceneOutput, Index, InstallPackInput, InstallPackOutput, KIND_ABBREVIATION, ListPacksInput, ListPacksOutput, ListScenesInput, ListScenesOutput, PROTOCOL_VERSION, PackKind, PackName, PackRecipe, PlaceIconInput, PlaceIconOutput, ROUNDNESS, RawElementsInput, RawElementsOutput, ReadMeInput, ReadMeOutput, RemovePackInput, RemovePackOutput, RestoreSnapshotInput, RestoreSnapshotOutput, SERVER_MANAGED_FIELDS, STALE_SELECTION_MS, Scene, SceneElement, SceneId, SceneSettingsInput, SceneSettingsOutput, ScreenshotInput, ScreenshotOutput, ScreenshotResponse, SearchIconsInput, SearchIconsOutput, SnapshotId, SnapshotInput, SnapshotOutput, UngroupInput, UngroupOutput, UpdateElementsInput, UpdateElementsOutput, assertNever, bridgeSpawnFailed, bridgeUnreachable, bridgeVersionMismatch, catalogUnavailable, createNodeSkeletonExpander, deepEqual, describeThrown, diskConflict, emptySelection, expansionFailed, exportFailed, exportTimedOut, fail, frameIdOf, iconIdOf, idCollision, isFail, isOk, noBoundLabel, noBrowser, noScenes, notAPack, notARepo, ok, packInstallFailed, partition, pathOutsideRepo, point, portUnavailable, refusedFieldMessage, sceneFileMissing, staleVersion, storeUnavailable, unknownElement, unknownIcon, unknownScene, unknownSnapshot, unreadableSceneFile };
|
|
1507
|
+
//# sourceMappingURL=chunk-VTFAXWQJ.js.map
|
|
1508
|
+
//# sourceMappingURL=chunk-VTFAXWQJ.js.map
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { SceneElement, PackName, PackKind, IconId, SceneId, Scene, SnapshotId, SERVER_MANAGED_FIELDS, BINDING_FIELDS, GUIDE_TOPICS, DetailLevel, createNodeSkeletonExpander, ok, fail, bridgeSpawnFailed, bridgeUnreachable, GuideTopic, storeUnavailable, describeThrown, unreadableSceneFile, pathOutsideRepo, packInstallFailed, sceneFileMissing, notAPack, PackRecipe, portUnavailable, DescribeSceneOutput, DescribeSceneInput, GetSelectionOutput, GetSelectionInput, GetElementsOutput, GetElementsInput, ListScenesOutput, ListScenesInput, SceneSettingsOutput, SceneSettingsInput, SearchIconsOutput, SearchIconsInput, ScreenshotOutput, ScreenshotInput, ReadMeOutput, ReadMeInput, PlaceIconOutput, PlaceIconInput, SnapshotOutput, SnapshotInput, RestoreSnapshotOutput, RestoreSnapshotInput, ExportSceneOutput, ExportSceneInput, ImportSceneOutput, ImportSceneInput, ExportImageOutput, ExportImageInput, InstallPackOutput, InstallPackInput, notARepo, ScreenshotResponse, RemovePackOutput, ListPacksOutput, RawElementsOutput, FocusOutput, EmbedOutput, AnnotateOutput, FrameOutput, UngroupOutput, GroupOutput, ArrangeOutput, DeleteElementsOutput, UpdateElementsOutput, ConnectOutput, CreateElementsOutput, HealthResponse, iconIdOf, unknownScene, unknownSnapshot, exportFailed, unknownIcon, HOUSE, FileId, ElementId, diskConflict, refusedFieldMessage, idCollision, expansionFailed, emptySelection, unknownElement, frameIdOf, GroupId, staleVersion, deepEqual, exportTimedOut, noBrowser, DebugElementRequest, CreateElementsInput, ConnectToolInput, UpdateElementsInput, DeleteElementsInput, ArrangeToolInput, GroupInput, UngroupInput, FrameInput, AnnotateInput, EmbedInput, FocusInput, RawElementsInput, ListPacksInput, RemovePackInput, Index, ROUNDNESS, STALE_SELECTION_MS, noScenes, ClientMessage, assertNever, PROTOCOL_VERSION, ApiError, EXCALIDRAW_FILE_VERSION, EXCALIDRAW_FILE_TYPE,
|
|
2
|
+
import { SceneElement, PackName, PackKind, IconId, SceneId, Scene, SnapshotId, SERVER_MANAGED_FIELDS, BINDING_FIELDS, GUIDE_TOPICS, DetailLevel, createNodeSkeletonExpander, ok, fail, bridgeSpawnFailed, bridgeVersionMismatch, bridgeUnreachable, GuideTopic, storeUnavailable, describeThrown, unreadableSceneFile, pathOutsideRepo, packInstallFailed, sceneFileMissing, notAPack, PackRecipe, portUnavailable, DescribeSceneOutput, DescribeSceneInput, GetSelectionOutput, GetSelectionInput, GetElementsOutput, GetElementsInput, ListScenesOutput, ListScenesInput, SceneSettingsOutput, SceneSettingsInput, SearchIconsOutput, SearchIconsInput, ScreenshotOutput, ScreenshotInput, ReadMeOutput, ReadMeInput, PlaceIconOutput, PlaceIconInput, SnapshotOutput, SnapshotInput, RestoreSnapshotOutput, RestoreSnapshotInput, ExportSceneOutput, ExportSceneInput, ImportSceneOutput, ImportSceneInput, ExportImageOutput, ExportImageInput, InstallPackOutput, InstallPackInput, notARepo, ScreenshotResponse, RemovePackOutput, ListPacksOutput, RawElementsOutput, FocusOutput, EmbedOutput, AnnotateOutput, FrameOutput, UngroupOutput, GroupOutput, ArrangeOutput, DeleteElementsOutput, UpdateElementsOutput, ConnectOutput, CreateElementsOutput, HealthResponse, iconIdOf, unknownScene, unknownSnapshot, exportFailed, unknownIcon, HOUSE, FileId, ElementId, diskConflict, refusedFieldMessage, idCollision, expansionFailed, emptySelection, unknownElement, frameIdOf, GroupId, staleVersion, noBoundLabel, deepEqual, exportTimedOut, noBrowser, DebugElementRequest, CreateElementsInput, ConnectToolInput, UpdateElementsInput, DeleteElementsInput, ArrangeToolInput, GroupInput, UngroupInput, FrameInput, AnnotateInput, EmbedInput, FocusInput, RawElementsInput, ListPacksInput, RemovePackInput, Index, ROUNDNESS, STALE_SELECTION_MS, noScenes, ClientMessage, assertNever, PROTOCOL_VERSION, ApiError, EXCALIDRAW_FILE_VERSION, EXCALIDRAW_FILE_TYPE, point, FLAG_GLYPH, KIND_ABBREVIATION, catalogUnavailable, FrameId, IconItem, ExcalidrawFile } from './chunk-VTFAXWQJ.js';
|
|
3
3
|
import './chunk-7D4SUZUM.js';
|
|
4
4
|
import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
@@ -1046,9 +1046,11 @@ var KNOWN_CODES = /* @__PURE__ */ new Set([
|
|
|
1046
1046
|
"BRIDGE_SPAWN_FAILED",
|
|
1047
1047
|
"BRIDGE_UNREACHABLE",
|
|
1048
1048
|
"PORT_UNAVAILABLE",
|
|
1049
|
+
"BRIDGE_VERSION_MISMATCH",
|
|
1049
1050
|
"STALE_VERSION",
|
|
1050
1051
|
"ID_COLLISION",
|
|
1051
1052
|
"UNKNOWN_ELEMENT",
|
|
1053
|
+
"NO_BOUND_LABEL",
|
|
1052
1054
|
"EMPTY_SELECTION",
|
|
1053
1055
|
"DISK_CONFLICT",
|
|
1054
1056
|
"UNREADABLE_SCENE_FILE",
|
|
@@ -1098,23 +1100,48 @@ function createBridgeClient() {
|
|
|
1098
1100
|
return null;
|
|
1099
1101
|
}
|
|
1100
1102
|
},
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1103
|
+
async shutdown(port, timeoutMs = DEFAULT_HEALTH_TIMEOUT_MS) {
|
|
1104
|
+
try {
|
|
1105
|
+
await fetch(`http://127.0.0.1:${port}/api/shutdown`, {
|
|
1106
|
+
method: "POST",
|
|
1107
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1108
|
+
});
|
|
1109
|
+
} catch {
|
|
1110
|
+
}
|
|
1111
|
+
const deadline = Date.now() + timeoutMs * 4;
|
|
1112
|
+
while (Date.now() < deadline) {
|
|
1113
|
+
if (await this.health(port, timeoutMs) === null) return true;
|
|
1114
|
+
await new Promise((resolve6) => setTimeout(resolve6, 100));
|
|
1115
|
+
}
|
|
1116
|
+
return false;
|
|
1117
|
+
},
|
|
1118
|
+
connect(port, reconnect) {
|
|
1119
|
+
let current = port;
|
|
1120
|
+
async function attempt(path, init, timeoutMs) {
|
|
1105
1121
|
try {
|
|
1106
|
-
|
|
1122
|
+
return await fetch(`http://127.0.0.1:${current}${path}`, {
|
|
1107
1123
|
...init,
|
|
1108
1124
|
signal: AbortSignal.timeout(timeoutMs)
|
|
1109
1125
|
});
|
|
1110
1126
|
} catch {
|
|
1111
|
-
return
|
|
1127
|
+
return null;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
async function call(path, schema, init, timeoutMs = DEFAULT_API_TIMEOUT_MS) {
|
|
1131
|
+
let response = await attempt(path, init, timeoutMs);
|
|
1132
|
+
if (response === null && reconnect !== void 0) {
|
|
1133
|
+
const revived = await reconnect();
|
|
1134
|
+
if (revived !== null) {
|
|
1135
|
+
current = revived;
|
|
1136
|
+
response = await attempt(path, init, timeoutMs);
|
|
1137
|
+
}
|
|
1112
1138
|
}
|
|
1139
|
+
if (response === null) return fail(bridgeUnreachable(current));
|
|
1113
1140
|
const body = await response.json().catch(() => null);
|
|
1114
|
-
if (!response.ok) return fail(toChalkError(body,
|
|
1141
|
+
if (!response.ok) return fail(toChalkError(body, current));
|
|
1115
1142
|
const parsed = schema.safeParse(body);
|
|
1116
1143
|
if (!parsed.success) {
|
|
1117
|
-
return fail(bridgeUnreachable(
|
|
1144
|
+
return fail(bridgeUnreachable(current));
|
|
1118
1145
|
}
|
|
1119
1146
|
return ok(parsed.data);
|
|
1120
1147
|
}
|
|
@@ -1725,6 +1752,24 @@ function createBridgeLauncher(options, deps) {
|
|
|
1725
1752
|
await sleep(pollIntervalMs);
|
|
1726
1753
|
}
|
|
1727
1754
|
return fail(bridgeSpawnFailed(`it did not start listening within ${readyTimeoutMs}ms`));
|
|
1755
|
+
},
|
|
1756
|
+
async stop(pid) {
|
|
1757
|
+
try {
|
|
1758
|
+
process.kill(pid, "SIGTERM");
|
|
1759
|
+
} catch {
|
|
1760
|
+
return false;
|
|
1761
|
+
}
|
|
1762
|
+
const deadline = Date.now() + 5e3;
|
|
1763
|
+
while (Date.now() < deadline) {
|
|
1764
|
+
try {
|
|
1765
|
+
process.kill(pid, 0);
|
|
1766
|
+
} catch {
|
|
1767
|
+
return true;
|
|
1768
|
+
}
|
|
1769
|
+
await sleep(pollIntervalMs);
|
|
1770
|
+
}
|
|
1771
|
+
deps.logger.warn("a bridge did not exit after being signalled", { pid });
|
|
1772
|
+
return false;
|
|
1728
1773
|
}
|
|
1729
1774
|
};
|
|
1730
1775
|
}
|
|
@@ -1733,6 +1778,48 @@ function createBridgeLauncher(options, deps) {
|
|
|
1733
1778
|
function createClock() {
|
|
1734
1779
|
return { now: () => Date.now() };
|
|
1735
1780
|
}
|
|
1781
|
+
|
|
1782
|
+
// src/adapters/system/idle-watch.ts
|
|
1783
|
+
var intervalFor = (timeoutMs) => Math.max(1e3, Math.min(1e4, Math.floor(timeoutMs / 3)));
|
|
1784
|
+
function createIdleWatch(options, deps) {
|
|
1785
|
+
let lastActivity = deps.clock.now();
|
|
1786
|
+
let timer = null;
|
|
1787
|
+
let fired = false;
|
|
1788
|
+
return {
|
|
1789
|
+
touch() {
|
|
1790
|
+
lastActivity = deps.clock.now();
|
|
1791
|
+
},
|
|
1792
|
+
start({ isWatched, onIdle }) {
|
|
1793
|
+
const timeoutMs = options.timeoutMs;
|
|
1794
|
+
if (timeoutMs === null) {
|
|
1795
|
+
deps.logger.debug("idle shutdown is disabled", { reason: "CHALK_IDLE_TIMEOUT=0" });
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
if (timer !== null) return;
|
|
1799
|
+
const every = options.checkIntervalMs ?? intervalFor(timeoutMs);
|
|
1800
|
+
timer = setInterval(() => {
|
|
1801
|
+
if (fired) return;
|
|
1802
|
+
if (isWatched()) {
|
|
1803
|
+
lastActivity = deps.clock.now();
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
if (deps.clock.now() - lastActivity < timeoutMs) return;
|
|
1807
|
+
fired = true;
|
|
1808
|
+
deps.logger.info("idle \u2014 nothing watching and nothing asking", {
|
|
1809
|
+
idleMs: deps.clock.now() - lastActivity
|
|
1810
|
+
});
|
|
1811
|
+
onIdle();
|
|
1812
|
+
}, every);
|
|
1813
|
+
timer.unref?.();
|
|
1814
|
+
deps.logger.debug("watching for idle", { timeoutMs, every });
|
|
1815
|
+
},
|
|
1816
|
+
stop() {
|
|
1817
|
+
if (timer === null) return;
|
|
1818
|
+
clearInterval(timer);
|
|
1819
|
+
timer = null;
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1736
1823
|
var ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
1737
1824
|
var shortId = (length) => {
|
|
1738
1825
|
let out = "";
|
|
@@ -2426,7 +2513,20 @@ function ensureBridge(deps) {
|
|
|
2426
2513
|
const repoRoot = await deps.projectDir.findRepoRoot(options.cwd);
|
|
2427
2514
|
if (!repoRoot.ok) return repoRoot;
|
|
2428
2515
|
const existingPort = await deps.projectDir.readPort(repoRoot.value);
|
|
2429
|
-
|
|
2516
|
+
let existing = existingPort === null ? null : await deps.client.health(existingPort);
|
|
2517
|
+
if (existing !== null && existingPort !== null && existing.version !== deps.version) {
|
|
2518
|
+
deps.logger.info("replacing a bridge from another build", {
|
|
2519
|
+
running: existing.version,
|
|
2520
|
+
ours: deps.version,
|
|
2521
|
+
port: existingPort
|
|
2522
|
+
});
|
|
2523
|
+
const stopped = await deps.client.shutdown(existingPort) || await deps.launcher.stop(existing.pid);
|
|
2524
|
+
if (!stopped) {
|
|
2525
|
+
return fail(bridgeVersionMismatch(existing.version, deps.version, existingPort));
|
|
2526
|
+
}
|
|
2527
|
+
await deps.projectDir.clearPort(repoRoot.value);
|
|
2528
|
+
existing = null;
|
|
2529
|
+
}
|
|
2430
2530
|
let port = existing === null ? null : existingPort;
|
|
2431
2531
|
let health = existing;
|
|
2432
2532
|
let spawned = false;
|
|
@@ -3237,8 +3337,42 @@ function edgePoint(from, to, gap) {
|
|
|
3237
3337
|
const t = scale + push;
|
|
3238
3338
|
return [cx + dx * t, cy + dy * t];
|
|
3239
3339
|
}
|
|
3240
|
-
function
|
|
3340
|
+
function redrawOne(current, labels, arrow) {
|
|
3341
|
+
if (arrow.type !== "arrow") return [];
|
|
3342
|
+
const fromId = startOf(arrow);
|
|
3343
|
+
const toId = endOf(arrow);
|
|
3344
|
+
if (fromId === null || toId === null) return [];
|
|
3345
|
+
const from = current.get(fromId);
|
|
3346
|
+
const to = current.get(toId);
|
|
3347
|
+
if (from === void 0 || to === void 0) return [];
|
|
3348
|
+
const startGap = arrow.startBinding?.gap ?? 4;
|
|
3349
|
+
const endGap = arrow.endBinding?.gap ?? 4;
|
|
3350
|
+
const [sx, sy] = edgePoint(boxOf(from), boxOf(to), startGap);
|
|
3351
|
+
const [ex, ey] = edgePoint(boxOf(to), boxOf(from), endGap);
|
|
3352
|
+
const points = [point(0, 0), point(ex - sx, ey - sy)];
|
|
3353
|
+
const redrawn = {
|
|
3354
|
+
...arrow,
|
|
3355
|
+
x: sx,
|
|
3356
|
+
y: sy,
|
|
3357
|
+
width: Math.abs(ex - sx),
|
|
3358
|
+
height: Math.abs(ey - sy),
|
|
3359
|
+
points,
|
|
3360
|
+
lastCommittedPoint: null
|
|
3361
|
+
};
|
|
3362
|
+
const label = labels.get(arrow.id);
|
|
3363
|
+
if (label === void 0) return [redrawn];
|
|
3364
|
+
return [
|
|
3365
|
+
redrawn,
|
|
3366
|
+
{
|
|
3367
|
+
...label,
|
|
3368
|
+
x: (sx + ex) / 2 - label.width / 2,
|
|
3369
|
+
y: (sy + ey) / 2 - label.height / 2
|
|
3370
|
+
}
|
|
3371
|
+
];
|
|
3372
|
+
}
|
|
3373
|
+
function redrawArrowsTouching(scene, moved) {
|
|
3241
3374
|
const current = new Map(scene.map((element) => [element.id, moved.get(element.id) ?? element]));
|
|
3375
|
+
const labels = labelsByContainer([...current.values()]);
|
|
3242
3376
|
const redrawn = [];
|
|
3243
3377
|
for (const element of current.values()) {
|
|
3244
3378
|
if (element.type !== "arrow") continue;
|
|
@@ -3246,26 +3380,16 @@ function rebindArrows(scene, moved) {
|
|
|
3246
3380
|
const toId = endOf(element);
|
|
3247
3381
|
if (fromId === null || toId === null) continue;
|
|
3248
3382
|
if (!moved.has(fromId) && !moved.has(toId)) continue;
|
|
3249
|
-
|
|
3250
|
-
const to = current.get(toId);
|
|
3251
|
-
if (from === void 0 || to === void 0) continue;
|
|
3252
|
-
const startGap = element.startBinding?.gap ?? 4;
|
|
3253
|
-
const endGap = element.endBinding?.gap ?? 4;
|
|
3254
|
-
const [sx, sy] = edgePoint(boxOf(from), boxOf(to), startGap);
|
|
3255
|
-
const [ex, ey] = edgePoint(boxOf(to), boxOf(from), endGap);
|
|
3256
|
-
const points = [point(0, 0), point(ex - sx, ey - sy)];
|
|
3257
|
-
redrawn.push({
|
|
3258
|
-
...element,
|
|
3259
|
-
x: sx,
|
|
3260
|
-
y: sy,
|
|
3261
|
-
width: Math.abs(ex - sx),
|
|
3262
|
-
height: Math.abs(ey - sy),
|
|
3263
|
-
points,
|
|
3264
|
-
lastCommittedPoint: null
|
|
3265
|
-
});
|
|
3383
|
+
redrawn.push(...redrawOne(current, labels, element));
|
|
3266
3384
|
}
|
|
3267
3385
|
return redrawn;
|
|
3268
3386
|
}
|
|
3387
|
+
function redrawArrows(scene, ids) {
|
|
3388
|
+
const current = new Map(scene.map((element) => [element.id, element]));
|
|
3389
|
+
const labels = labelsByContainer(scene);
|
|
3390
|
+
const wanted = new Set(ids);
|
|
3391
|
+
return [...current.values()].filter((element) => wanted.has(element.id)).flatMap((element) => redrawOne(current, labels, element));
|
|
3392
|
+
}
|
|
3269
3393
|
function translate(scene, deltas) {
|
|
3270
3394
|
const labels = labelsByContainer(scene);
|
|
3271
3395
|
const moved = /* @__PURE__ */ new Map();
|
|
@@ -3280,7 +3404,7 @@ function translate(scene, deltas) {
|
|
|
3280
3404
|
moved.set(label.id, { ...label, x: label.x + dx, y: label.y + dy });
|
|
3281
3405
|
}
|
|
3282
3406
|
}
|
|
3283
|
-
return [...moved.values(), ...
|
|
3407
|
+
return [...moved.values(), ...redrawArrowsTouching(scene, moved)];
|
|
3284
3408
|
}
|
|
3285
3409
|
var alignments = {
|
|
3286
3410
|
"align-left": (box, bounds) => [bounds.x - box.x, 0],
|
|
@@ -3576,7 +3700,15 @@ function connectElements(deps) {
|
|
|
3576
3700
|
};
|
|
3577
3701
|
const created = await deps.write.create(scene.value, [skeleton], true);
|
|
3578
3702
|
if (!created.ok) return created;
|
|
3579
|
-
const
|
|
3703
|
+
const placed = redrawArrows(
|
|
3704
|
+
[...scene.value.elements, ...created.value.created, ...created.value.changed],
|
|
3705
|
+
[skeleton.id]
|
|
3706
|
+
);
|
|
3707
|
+
const geometry = new Map(placed.map((element) => [element.id, element]));
|
|
3708
|
+
const committed = deps.write.append(scene.value, {
|
|
3709
|
+
created: created.value.created.map((element) => geometry.get(element.id) ?? element),
|
|
3710
|
+
changed: created.value.changed
|
|
3711
|
+
});
|
|
3580
3712
|
if (!committed.ok) return committed;
|
|
3581
3713
|
return ok({ ...committed.value, arrowId: skeleton.id });
|
|
3582
3714
|
};
|
|
@@ -4195,6 +4327,13 @@ function rawElements(deps) {
|
|
|
4195
4327
|
|
|
4196
4328
|
// src/app/usecases/write/update-elements.ts
|
|
4197
4329
|
var missing = (id) => toRejection(id, unknownElement(id));
|
|
4330
|
+
var onlyRequested = (landed, requested) => {
|
|
4331
|
+
const asked = new Set(requested);
|
|
4332
|
+
return landed.filter((id) => asked.has(id));
|
|
4333
|
+
};
|
|
4334
|
+
var captionOf = (scene, container) => scene.find(
|
|
4335
|
+
(element) => element.type === "text" && element.containerId === container && !element.isDeleted
|
|
4336
|
+
);
|
|
4198
4337
|
function updateElements(deps) {
|
|
4199
4338
|
return (input) => {
|
|
4200
4339
|
const scene = deps.write.resolve(input.sceneId);
|
|
@@ -4203,25 +4342,73 @@ function updateElements(deps) {
|
|
|
4203
4342
|
const changed = [];
|
|
4204
4343
|
const rejected = [];
|
|
4205
4344
|
const expectations = [];
|
|
4345
|
+
const moved = /* @__PURE__ */ new Map();
|
|
4346
|
+
const knockOn = [];
|
|
4206
4347
|
for (const update of input.updates) {
|
|
4207
4348
|
const existing = byId.get(update.id);
|
|
4208
4349
|
if (existing === void 0 || existing.isDeleted) {
|
|
4209
4350
|
rejected.push(missing(update.id));
|
|
4210
4351
|
continue;
|
|
4211
4352
|
}
|
|
4353
|
+
const { role, label, ...rest } = update.patch;
|
|
4212
4354
|
const patch = Object.fromEntries(
|
|
4213
|
-
Object.entries(
|
|
4355
|
+
Object.entries(rest).filter(([, value]) => value !== void 0)
|
|
4214
4356
|
);
|
|
4215
|
-
|
|
4357
|
+
if (label !== void 0) {
|
|
4358
|
+
const caption = captionOf(scene.value.elements, update.id);
|
|
4359
|
+
if (caption === void 0) {
|
|
4360
|
+
rejected.push(toRejection(update.id, noBoundLabel(update.id)));
|
|
4361
|
+
continue;
|
|
4362
|
+
}
|
|
4363
|
+
knockOn.push({ ...caption, text: label, originalText: label });
|
|
4364
|
+
}
|
|
4365
|
+
const next = {
|
|
4366
|
+
...existing,
|
|
4367
|
+
...role === void 0 ? {} : styleForRole(role),
|
|
4368
|
+
...patch
|
|
4369
|
+
};
|
|
4370
|
+
changed.push(next);
|
|
4216
4371
|
expectations.push({ id: update.id, expectedVersion: update.expectedVersion });
|
|
4372
|
+
const dx = next.x - existing.x;
|
|
4373
|
+
const dy = next.y - existing.y;
|
|
4374
|
+
if (dx !== 0 || dy !== 0) moved.set(update.id, next);
|
|
4375
|
+
else if (next.width !== existing.width || next.height !== existing.height) {
|
|
4376
|
+
moved.set(update.id, next);
|
|
4377
|
+
}
|
|
4217
4378
|
}
|
|
4218
|
-
|
|
4379
|
+
if (moved.size > 0) {
|
|
4380
|
+
for (const [id, next] of moved) {
|
|
4381
|
+
const before = byId.get(id);
|
|
4382
|
+
const caption = captionOf(scene.value.elements, id);
|
|
4383
|
+
if (before === void 0 || caption === void 0) continue;
|
|
4384
|
+
knockOn.push({
|
|
4385
|
+
...caption,
|
|
4386
|
+
x: caption.x + (next.x - before.x),
|
|
4387
|
+
y: caption.y + (next.y - before.y)
|
|
4388
|
+
});
|
|
4389
|
+
}
|
|
4390
|
+
knockOn.push(...redrawArrowsTouching(scene.value.elements, moved));
|
|
4391
|
+
}
|
|
4392
|
+
for (const element of knockOn) {
|
|
4393
|
+
const before = byId.get(element.id);
|
|
4394
|
+
if (before !== void 0) {
|
|
4395
|
+
expectations.push({ id: element.id, expectedVersion: before.version });
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
const applied = deps.write.apply(scene.value, [...changed, ...knockOn], { expectations });
|
|
4219
4399
|
if (!applied.ok) return applied;
|
|
4220
4400
|
return ok({
|
|
4221
4401
|
sceneId: applied.value.sceneId,
|
|
4222
4402
|
sceneVersion: applied.value.sceneVersion,
|
|
4223
4403
|
broadcastTo: applied.value.broadcastTo,
|
|
4224
|
-
|
|
4404
|
+
// What the caller asked for, not everything that moved. A caption that followed its
|
|
4405
|
+
// box and an arrow that was redrawn are consequences; listing them would turn this
|
|
4406
|
+
// into "every element that changed" and leave the caller working out which of them
|
|
4407
|
+
// it had actually requested.
|
|
4408
|
+
applied: onlyRequested(
|
|
4409
|
+
applied.value.applied,
|
|
4410
|
+
input.updates.map((update) => update.id)
|
|
4411
|
+
),
|
|
4225
4412
|
rejected: [...rejected, ...applied.value.rejected]
|
|
4226
4413
|
});
|
|
4227
4414
|
};
|
|
@@ -4255,6 +4442,11 @@ function deleteElements(deps) {
|
|
|
4255
4442
|
id: deletion.id,
|
|
4256
4443
|
expectedVersion: deletion.expectedVersion ?? existing.version
|
|
4257
4444
|
});
|
|
4445
|
+
const caption = captionOf(scene.value.elements, deletion.id);
|
|
4446
|
+
if (caption !== void 0) {
|
|
4447
|
+
changed.push({ ...caption, isDeleted: true });
|
|
4448
|
+
expectations.push({ id: caption.id, expectedVersion: caption.version });
|
|
4449
|
+
}
|
|
4258
4450
|
}
|
|
4259
4451
|
const applied = deps.write.apply(scene.value, changed, { expectations });
|
|
4260
4452
|
if (!applied.ok) return applied;
|
|
@@ -4262,7 +4454,10 @@ function deleteElements(deps) {
|
|
|
4262
4454
|
sceneId: applied.value.sceneId,
|
|
4263
4455
|
sceneVersion: applied.value.sceneVersion,
|
|
4264
4456
|
broadcastTo: applied.value.broadcastTo,
|
|
4265
|
-
deleted:
|
|
4457
|
+
deleted: onlyRequested(
|
|
4458
|
+
applied.value.applied,
|
|
4459
|
+
input.deletions.map((deletion) => deletion.id)
|
|
4460
|
+
),
|
|
4266
4461
|
rejected: [...rejected, ...applied.value.rejected]
|
|
4267
4462
|
});
|
|
4268
4463
|
};
|
|
@@ -4275,6 +4470,7 @@ function printLine(line) {
|
|
|
4275
4470
|
}
|
|
4276
4471
|
var DEFAULT_PORT = 7180;
|
|
4277
4472
|
var PORT_SCAN_RANGE = 32;
|
|
4473
|
+
var DEFAULT_IDLE_SECONDS = 90;
|
|
4278
4474
|
var truthy = (value) => value !== void 0 && value !== "" && value !== "0" && value.toLowerCase() !== "false";
|
|
4279
4475
|
var EnvSchema = z.object({
|
|
4280
4476
|
CHALK_PORT: z.coerce.number().int().min(1).max(65535).optional(),
|
|
@@ -4287,12 +4483,18 @@ var EnvSchema = z.object({
|
|
|
4287
4483
|
* icon pack into the developer real data directory would be a test that changed the
|
|
4288
4484
|
* machine it ran on.
|
|
4289
4485
|
*/
|
|
4290
|
-
CHALK_DATA_DIR: z.string().min(1).optional()
|
|
4486
|
+
CHALK_DATA_DIR: z.string().min(1).optional(),
|
|
4487
|
+
/**
|
|
4488
|
+
* Seconds a bridge may sit with no browser client and no tool call before it exits.
|
|
4489
|
+
* `0` keeps it up forever, which is what every version before v1.0.2 did.
|
|
4490
|
+
*/
|
|
4491
|
+
CHALK_IDLE_TIMEOUT: z.coerce.number().int().min(0).max(86400).optional()
|
|
4291
4492
|
});
|
|
4292
4493
|
function readEnv(source) {
|
|
4293
4494
|
const parsed = EnvSchema.safeParse(source);
|
|
4294
4495
|
const env = parsed.success ? parsed.data : {};
|
|
4295
4496
|
const explicit2 = env.CHALK_PORT ?? null;
|
|
4497
|
+
const idleSeconds = env.CHALK_IDLE_TIMEOUT ?? DEFAULT_IDLE_SECONDS;
|
|
4296
4498
|
return {
|
|
4297
4499
|
listenFrom: explicit2 ?? DEFAULT_PORT,
|
|
4298
4500
|
listenTries: explicit2 === null ? PORT_SCAN_RANGE : 1,
|
|
@@ -4300,12 +4502,13 @@ function readEnv(source) {
|
|
|
4300
4502
|
autoStart: !truthy(env.CHALK_NO_AUTOSTART),
|
|
4301
4503
|
autoOpen: !truthy(env.CHALK_NO_OPEN),
|
|
4302
4504
|
dataDir: env.CHALK_DATA_DIR ?? null,
|
|
4505
|
+
idleTimeoutMs: idleSeconds === 0 ? null : idleSeconds * 1e3,
|
|
4303
4506
|
logLevel: env.CHALK_LOG_LEVEL ?? "info"
|
|
4304
4507
|
};
|
|
4305
4508
|
}
|
|
4306
4509
|
|
|
4307
4510
|
// src/composition/container.ts
|
|
4308
|
-
var PACKAGE_VERSION = "1.0.
|
|
4511
|
+
var PACKAGE_VERSION = "1.0.3";
|
|
4309
4512
|
function readRuntime() {
|
|
4310
4513
|
return {
|
|
4311
4514
|
env: process.env,
|
|
@@ -4369,6 +4572,7 @@ function buildCommandContext(runtime = readRuntime()) {
|
|
|
4369
4572
|
if (!opened.ok) return opened;
|
|
4370
4573
|
const store = opened.value;
|
|
4371
4574
|
const broadcaster = createBroadcaster(idGen, logger);
|
|
4575
|
+
const idleWatch = createIdleWatch({ timeoutMs: config.idleTimeoutMs }, { clock, logger });
|
|
4372
4576
|
const presenceTracker = createPresenceTracker();
|
|
4373
4577
|
const expander = createNodeSkeletonExpander();
|
|
4374
4578
|
const repoWriter = createRepoWriter({ repoRoot }, { store, logger });
|
|
@@ -4376,6 +4580,7 @@ function buildCommandContext(runtime = readRuntime()) {
|
|
|
4376
4580
|
const diskWatcher = createDiskWatcher({ logger });
|
|
4377
4581
|
const commit = commitScene({ store, repoWriter });
|
|
4378
4582
|
const repoFiles = createRepoFiles({ repoRoot });
|
|
4583
|
+
let onShutdown = null;
|
|
4379
4584
|
const exportBroker = createExportBroker({}, { broadcaster, idGen, logger });
|
|
4380
4585
|
const globalPaths = globalPathsOf(runtime, config);
|
|
4381
4586
|
let catalog = null;
|
|
@@ -4539,6 +4744,10 @@ function buildCommandContext(runtime = readRuntime()) {
|
|
|
4539
4744
|
// logic in `app/`, and a lifecycle command has no more business holding a store
|
|
4540
4745
|
// than a route does.
|
|
4541
4746
|
sceneFilePaths: () => store.list().map((scene) => scene.repoPath).filter((path) => path !== null).map((path) => resolve(repoRoot, path)),
|
|
4747
|
+
idleWatch,
|
|
4748
|
+
onShutdownRequest: (handler) => {
|
|
4749
|
+
onShutdown = handler;
|
|
4750
|
+
},
|
|
4542
4751
|
close: () => {
|
|
4543
4752
|
repoWriter.cancel();
|
|
4544
4753
|
exportBroker.abandon();
|
|
@@ -4579,6 +4788,7 @@ function buildCommandContext(runtime = readRuntime()) {
|
|
|
4579
4788
|
importScene: usecases.importScene,
|
|
4580
4789
|
broadcaster,
|
|
4581
4790
|
identity,
|
|
4791
|
+
requestShutdown: () => onShutdown?.(),
|
|
4582
4792
|
logger
|
|
4583
4793
|
},
|
|
4584
4794
|
ws: {
|
|
@@ -4602,24 +4812,42 @@ function buildCommandContext(runtime = readRuntime()) {
|
|
|
4602
4812
|
{ cliEntry: runtime.entryPath, execPath: runtime.execPath, env: runtime.env },
|
|
4603
4813
|
{ projectDir, client, logger }
|
|
4604
4814
|
);
|
|
4815
|
+
const bootstrap = ensureBridge({
|
|
4816
|
+
// Compared against whatever is already listening: an upgrade that reused an
|
|
4817
|
+
// older detached bridge would ship every fix and deliver none of them.
|
|
4818
|
+
version: runtime.version,
|
|
4819
|
+
projectDir,
|
|
4820
|
+
client,
|
|
4821
|
+
launcher,
|
|
4822
|
+
opener: createBrowserOpener(logger),
|
|
4823
|
+
logger
|
|
4824
|
+
});
|
|
4605
4825
|
return {
|
|
4606
4826
|
logger,
|
|
4607
4827
|
projectDir,
|
|
4608
4828
|
client,
|
|
4829
|
+
launcher,
|
|
4609
4830
|
// A7 — the tool handlers never see a port, a URL, or a `fetch`. They see the
|
|
4610
4831
|
// bridge's read surface and a shelf of markdown, and nothing else exists to
|
|
4611
4832
|
// tempt them into caching a scene (invariant 1).
|
|
4833
|
+
//
|
|
4834
|
+
// The `reconnect` argument is what makes a session survive its own bridge. One
|
|
4835
|
+
// now exits when nothing is watching and nothing is asking, so "the port this
|
|
4836
|
+
// session started with" stops being true the moment the user closes the tab and
|
|
4837
|
+
// walks away. Re-running the bootstrap puts a bridge *and a tab* back, which is
|
|
4838
|
+
// what the session's first call did, so a tool call after a break just works.
|
|
4612
4839
|
createToolDeps: (port) => ({
|
|
4613
|
-
bridge: client.connect(port)
|
|
4840
|
+
bridge: client.connect(port, async () => {
|
|
4841
|
+
const revived = await bootstrap({
|
|
4842
|
+
cwd: runtime.cwd,
|
|
4843
|
+
autoStart: config.autoStart,
|
|
4844
|
+
autoOpen: config.autoOpen
|
|
4845
|
+
});
|
|
4846
|
+
return revived.ok ? revived.value.port : null;
|
|
4847
|
+
}),
|
|
4614
4848
|
guides: createGuideLibrary({ dir: resolveGuidesDir() }, { logger })
|
|
4615
4849
|
}),
|
|
4616
|
-
ensureBridge:
|
|
4617
|
-
projectDir,
|
|
4618
|
-
client,
|
|
4619
|
-
launcher,
|
|
4620
|
-
opener: createBrowserOpener(logger),
|
|
4621
|
-
logger
|
|
4622
|
-
})
|
|
4850
|
+
ensureBridge: bootstrap
|
|
4623
4851
|
};
|
|
4624
4852
|
}
|
|
4625
4853
|
};
|
|
@@ -5606,6 +5834,8 @@ var STATUS = {
|
|
|
5606
5834
|
BRIDGE_SPAWN_FAILED: 500,
|
|
5607
5835
|
BRIDGE_UNREACHABLE: 503,
|
|
5608
5836
|
PORT_UNAVAILABLE: 500,
|
|
5837
|
+
// 409: something is there, it is simply not the build this process belongs to.
|
|
5838
|
+
BRIDGE_VERSION_MISMATCH: 409,
|
|
5609
5839
|
// 409 for both: a stale version and a file that changed underneath us are the same
|
|
5610
5840
|
// shape of problem — the caller's view of the world is out of date, and re-reading is
|
|
5611
5841
|
// the way forward.
|
|
@@ -5613,6 +5843,8 @@ var STATUS = {
|
|
|
5613
5843
|
ID_COLLISION: 409,
|
|
5614
5844
|
// 404, not 409: the id is not there to conflict with.
|
|
5615
5845
|
UNKNOWN_ELEMENT: 404,
|
|
5846
|
+
// 422: the id is fine and the request is well-formed, there is simply nothing to rename.
|
|
5847
|
+
NO_BOUND_LABEL: 422,
|
|
5616
5848
|
EMPTY_SELECTION: 400,
|
|
5617
5849
|
DISK_CONFLICT: 409,
|
|
5618
5850
|
UNREADABLE_SCENE_FILE: 422,
|
|
@@ -5862,6 +6094,20 @@ var sessionRoutes = [
|
|
|
5862
6094
|
})
|
|
5863
6095
|
];
|
|
5864
6096
|
|
|
6097
|
+
// src/interfaces/http/routes/shutdown.ts
|
|
6098
|
+
var shutdownRoute = {
|
|
6099
|
+
name: "shutdown",
|
|
6100
|
+
register(app, deps) {
|
|
6101
|
+
app.post("/api/shutdown", async (_request, reply) => {
|
|
6102
|
+
const identity = deps.identity();
|
|
6103
|
+
deps.logger.info("shutdown requested over the API", { pid: identity.pid });
|
|
6104
|
+
await reply.send({ stopping: true, pid: identity.pid, version: identity.version });
|
|
6105
|
+
setTimeout(() => deps.requestShutdown(), 0);
|
|
6106
|
+
return reply;
|
|
6107
|
+
});
|
|
6108
|
+
}
|
|
6109
|
+
};
|
|
6110
|
+
|
|
5865
6111
|
// src/interfaces/http/routes/write.ts
|
|
5866
6112
|
var writeRoutes = [
|
|
5867
6113
|
postRoute({
|
|
@@ -5947,6 +6193,7 @@ var ROUTES = [
|
|
|
5947
6193
|
elementsRoute,
|
|
5948
6194
|
settingsRoute,
|
|
5949
6195
|
debugElementRoute,
|
|
6196
|
+
shutdownRoute,
|
|
5950
6197
|
...writeRoutes,
|
|
5951
6198
|
...sessionRoutes
|
|
5952
6199
|
];
|
|
@@ -5960,6 +6207,9 @@ async function createHttpServer(deps) {
|
|
|
5960
6207
|
bodyLimit: 32 * 1024 * 1024
|
|
5961
6208
|
});
|
|
5962
6209
|
await app.register(websocket);
|
|
6210
|
+
app.addHook("onRequest", async () => {
|
|
6211
|
+
deps.touch();
|
|
6212
|
+
});
|
|
5963
6213
|
for (const route of ROUTES) {
|
|
5964
6214
|
route.register(app, deps.routes);
|
|
5965
6215
|
deps.logger.debug("registered route", { route: route.name });
|
|
@@ -5969,6 +6219,7 @@ async function createHttpServer(deps) {
|
|
|
5969
6219
|
const send = (message) => {
|
|
5970
6220
|
socket.send(JSON.stringify(message));
|
|
5971
6221
|
};
|
|
6222
|
+
deps.touch();
|
|
5972
6223
|
const handle = deps.broadcaster.register(send);
|
|
5973
6224
|
const session = { handle, send };
|
|
5974
6225
|
socket.on("message", (data) => {
|
|
@@ -6049,7 +6300,8 @@ ${built.error.hint ?? ""}
|
|
|
6049
6300
|
ws: services.ws,
|
|
6050
6301
|
broadcaster: services.broadcaster,
|
|
6051
6302
|
logger: services.logger,
|
|
6052
|
-
uiDistPath: services.uiDistPath
|
|
6303
|
+
uiDistPath: services.uiDistPath,
|
|
6304
|
+
touch: () => services.idleWatch.touch()
|
|
6053
6305
|
});
|
|
6054
6306
|
const listening = await listenOnFirstFreePort(app, {
|
|
6055
6307
|
host: "127.0.0.1",
|
|
@@ -6076,6 +6328,7 @@ ${built.error.hint ?? ""}
|
|
|
6076
6328
|
await new Promise((resolve6) => {
|
|
6077
6329
|
const shutdown = async (signal) => {
|
|
6078
6330
|
services.logger.info("shutting down", { signal });
|
|
6331
|
+
services.idleWatch.stop();
|
|
6079
6332
|
await services.diskWatcher.stop();
|
|
6080
6333
|
const flushed = await services.repoWriter.flushAll();
|
|
6081
6334
|
if (flushed.length > 0) {
|
|
@@ -6086,18 +6339,75 @@ ${built.error.hint ?? ""}
|
|
|
6086
6339
|
services.close();
|
|
6087
6340
|
resolve6();
|
|
6088
6341
|
};
|
|
6342
|
+
services.idleWatch.start({
|
|
6343
|
+
isWatched: () => services.broadcaster.clientCount() > 0,
|
|
6344
|
+
onIdle: () => void shutdown("idle")
|
|
6345
|
+
});
|
|
6089
6346
|
process.once("SIGINT", () => void shutdown("SIGINT"));
|
|
6090
6347
|
process.once("SIGTERM", () => void shutdown("SIGTERM"));
|
|
6348
|
+
services.onShutdownRequest(() => void shutdown("api"));
|
|
6091
6349
|
});
|
|
6092
6350
|
return 0;
|
|
6093
6351
|
}
|
|
6094
6352
|
};
|
|
6095
6353
|
|
|
6354
|
+
// src/interfaces/cli/commands/stop.ts
|
|
6355
|
+
var stopCommand = {
|
|
6356
|
+
name: "stop",
|
|
6357
|
+
summary: "stop the bridge running for this repo, flushing anything unsaved",
|
|
6358
|
+
detail: [
|
|
6359
|
+
"Ends the bridge for the repository containing the working directory.",
|
|
6360
|
+
"",
|
|
6361
|
+
"The bridge flushes every pending .excalidraw write, stops watching the repo, and",
|
|
6362
|
+
"clears its port file on the way out \u2014 so nothing drawn in the last two seconds is",
|
|
6363
|
+
"lost. Nothing is started; the next Claude Code session starts a fresh one."
|
|
6364
|
+
].join("\n"),
|
|
6365
|
+
async run(context) {
|
|
6366
|
+
const services = context.createClientServices({ prefix: "[stop]" });
|
|
6367
|
+
const repoRoot = await services.projectDir.findRepoRoot(context.runtime.cwd);
|
|
6368
|
+
if (!repoRoot.ok) {
|
|
6369
|
+
context.print(repoRoot.error.message);
|
|
6370
|
+
if (repoRoot.error.hint !== void 0) context.print(` ${repoRoot.error.hint}`);
|
|
6371
|
+
return 1;
|
|
6372
|
+
}
|
|
6373
|
+
const port = await services.projectDir.readPort(repoRoot.value);
|
|
6374
|
+
if (port === null) {
|
|
6375
|
+
context.print(`no bridge is running for ${repoRoot.value}`);
|
|
6376
|
+
return 0;
|
|
6377
|
+
}
|
|
6378
|
+
const health = await services.client.health(port);
|
|
6379
|
+
if (health === null) {
|
|
6380
|
+
await services.projectDir.clearPort(repoRoot.value);
|
|
6381
|
+
context.print(`nothing was listening on 127.0.0.1:${port} \u2014 cleared the stale port file`);
|
|
6382
|
+
return 0;
|
|
6383
|
+
}
|
|
6384
|
+
context.print(
|
|
6385
|
+
`stopping chalkbridge ${health.version} on 127.0.0.1:${port} (pid ${health.pid})`
|
|
6386
|
+
);
|
|
6387
|
+
if (await services.client.shutdown(port)) {
|
|
6388
|
+
context.print(" stopped \u2014 pending scene writes were flushed");
|
|
6389
|
+
return 0;
|
|
6390
|
+
}
|
|
6391
|
+
context.print(" it has no shutdown endpoint (built before v1.0.2) \u2014 ending it by pid");
|
|
6392
|
+
if (await services.launcher.stop(health.pid)) {
|
|
6393
|
+
await services.projectDir.clearPort(repoRoot.value);
|
|
6394
|
+
context.print(` stopped pid ${health.pid}`);
|
|
6395
|
+
return 0;
|
|
6396
|
+
}
|
|
6397
|
+
context.print(" it would not stop \u2014 end it yourself:");
|
|
6398
|
+
context.print(
|
|
6399
|
+
context.runtime.platform === "win32" ? ` taskkill /PID ${health.pid} /F` : ` kill ${health.pid}`
|
|
6400
|
+
);
|
|
6401
|
+
return 1;
|
|
6402
|
+
}
|
|
6403
|
+
};
|
|
6404
|
+
|
|
6096
6405
|
// src/interfaces/cli/registry.ts
|
|
6097
6406
|
var COMMANDS = [
|
|
6098
6407
|
serveCommand,
|
|
6099
6408
|
mcpCommand,
|
|
6100
6409
|
healthCommand,
|
|
6410
|
+
stopCommand,
|
|
6101
6411
|
iconsCommand
|
|
6102
6412
|
];
|
|
6103
6413
|
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createNodeSkeletonExpander, fail, isFail, isOk, ok, partition } from './chunk-
|
|
1
|
+
export { createNodeSkeletonExpander, fail, isFail, isOk, ok, partition } from './chunk-VTFAXWQJ.js';
|
|
2
2
|
import './chunk-7D4SUZUM.js';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chalkbridge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Self-hosted Excalidraw canvas for Claude Code to draw and reason on architecture diagrams.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"ui-dist",
|
|
23
23
|
"guides",
|
|
24
24
|
"packs",
|
|
25
|
+
"CHANGELOG.md",
|
|
25
26
|
"THIRD-PARTY-NOTICES.md"
|
|
26
27
|
],
|
|
27
28
|
"bin": {
|