clay-server 4.0.0-beta.8 → 4.0.0
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/lib/capsule-display-floor.js +51 -0
- package/lib/capsule-frame-server.js +255 -0
- package/lib/capsule-mate-turn.js +105 -0
- package/lib/capsule-pig-logic.js +300 -0
- package/lib/capsule-server-runtimes.js +39 -0
- package/lib/capsules/pig/display.js +198 -0
- package/lib/capsules/pig/manifest.json +9 -0
- package/lib/capsules/pig/ui.json +84 -0
- package/lib/config.js +25 -1
- package/lib/daemon-projects.js +13 -8
- package/lib/daemon.js +127 -14
- package/lib/git-cli.js +95 -1
- package/lib/knowledge-import.js +436 -0
- package/lib/knowledge-record-store.js +188 -0
- package/lib/knowledge-search.js +240 -0
- package/lib/mate-knowledge-mcp-server.js +151 -0
- package/lib/mate-knowledge-migration.js +283 -0
- package/lib/mate-knowledge-service.js +488 -0
- package/lib/mate-knowledge-sync.js +482 -0
- package/lib/migrate-single-user.js +2 -0
- package/lib/notes-lifecycle.js +149 -0
- package/lib/notes.js +60 -3
- package/lib/os-users.js +3 -1
- package/lib/project-capsule-catalog.js +9 -1
- package/lib/project-capsule-turn.js +96 -0
- package/lib/project-connection.js +15 -1
- package/lib/project-file-history.js +223 -0
- package/lib/project-file-watch.js +77 -49
- package/lib/project-filesystem.js +31 -220
- package/lib/project-http.js +15 -1
- package/lib/project-knowledge.js +22 -0
- package/lib/project-log-context.js +172 -0
- package/lib/project-log-feedback-delivery.js +118 -0
- package/lib/project-logs-comments.js +137 -0
- package/lib/project-logs-context-state.js +42 -0
- package/lib/project-logs-mcp-server.js +328 -0
- package/lib/project-logs-query.js +185 -0
- package/lib/project-logs-root.js +75 -0
- package/lib/project-logs-schema.js +259 -0
- package/lib/project-logs-service.js +463 -0
- package/lib/project-logs-snapshot.js +205 -0
- package/lib/project-logs-store.js +495 -0
- package/lib/project-logs-versioning.js +132 -0
- package/lib/project-logs.js +400 -0
- package/lib/project-mate-interaction.js +14 -0
- package/lib/project-mate-knowledge.js +110 -0
- package/lib/project-memory.js +22 -0
- package/lib/project-message-delivery.js +70 -0
- package/lib/project-pair-lifecycle.js +460 -0
- package/lib/project-request-access.js +66 -0
- package/lib/project-session-handoff.js +1 -1
- package/lib/project-session-notes.js +64 -13
- package/lib/project-session-pair.js +159 -146
- package/lib/project-session-spawn.js +1 -1
- package/lib/project-sessions.js +58 -13
- package/lib/project-user-message.js +53 -4
- package/lib/project-worker-permission.js +430 -0
- package/lib/project-worker-proposal.js +208 -62
- package/lib/project.js +185 -6
- package/lib/public/app.js +63 -35
- package/lib/public/css/admin.css +2 -2
- package/lib/public/css/capsule-ui.css +38 -0
- package/lib/public/css/command-palette.css +19 -9
- package/lib/public/css/git-placard.css +154 -0
- package/lib/public/css/home-hub.css +4 -0
- package/lib/public/css/home-session-actions.css +66 -0
- package/lib/public/css/home-sidebar.css +66 -0
- package/lib/public/css/input.css +84 -3
- package/lib/public/css/messages.css +40 -1
- package/lib/public/css/mobile-nav.css +80 -0
- package/lib/public/css/notifications-center.css +67 -0
- package/lib/public/css/pane.css +97 -10
- package/lib/public/css/project-logs.css +198 -0
- package/lib/public/css/scheduler.css +1 -1
- package/lib/public/css/session-actions.css +16 -101
- package/lib/public/css/sidebar.css +103 -0
- package/lib/public/css/sticky-notes.css +237 -229
- package/lib/public/css/worker-proposal.css +36 -3
- package/lib/public/index.html +43 -11
- package/lib/public/modules/admin.js +8 -8
- package/lib/public/modules/app-connection.js +124 -6
- package/lib/public/modules/app-home-hub.js +12 -8
- package/lib/public/modules/app-message-cards.js +2 -1
- package/lib/public/modules/app-messages.js +92 -11
- package/lib/public/modules/app-misc.js +2 -14
- package/lib/public/modules/app-notifications.js +11 -7
- package/lib/public/modules/app-panels.js +3 -2
- package/lib/public/modules/app-projects.js +56 -18
- package/lib/public/modules/app-rendering.js +1 -0
- package/lib/public/modules/capsule-preference.js +44 -0
- package/lib/public/modules/chat-bubble-renderer.js +1 -1
- package/lib/public/modules/clay-log-links.js +70 -0
- package/lib/public/modules/command-palette.js +52 -15
- package/lib/public/modules/context-sources.js +0 -3
- package/lib/public/modules/git-agent-sessions.js +85 -0
- package/lib/public/modules/git-panel.js +111 -84
- package/lib/public/modules/git-placard.js +161 -0
- package/lib/public/modules/home-chat-empty-state.js +39 -0
- package/lib/public/modules/home-chat-identity.js +9 -0
- package/lib/public/modules/home-conversations-sheet.js +102 -48
- package/lib/public/modules/home-mate-chat.js +15 -35
- package/lib/public/modules/home-session-actions.js +6 -0
- package/lib/public/modules/home-sidebar-chat-list.js +101 -41
- package/lib/public/modules/home-sidebar.js +7 -0
- package/lib/public/modules/home-surface-boot.js +7 -3
- package/lib/public/modules/home-tool-frame.js +182 -0
- package/lib/public/modules/home-tools.js +138 -1
- package/lib/public/modules/input.js +38 -7
- package/lib/public/modules/markdown.js +2 -0
- package/lib/public/modules/mate-sidebar.js +0 -8
- package/lib/public/modules/message-delivery.js +97 -0
- package/lib/public/modules/paste-modal.js +84 -0
- package/lib/public/modules/project-activation.js +45 -0
- package/lib/public/modules/project-logs-render.js +405 -0
- package/lib/public/modules/project-logs.js +461 -0
- package/lib/public/modules/project-removal-target.js +26 -0
- package/lib/public/modules/scheduler.js +17 -3
- package/lib/public/modules/session-actions.js +29 -105
- package/lib/public/modules/session-hierarchy.js +54 -0
- package/lib/public/modules/sidebar-mobile.js +40 -15
- package/lib/public/modules/sidebar-session-hierarchy.js +214 -0
- package/lib/public/modules/sidebar-sessions.js +77 -11
- package/lib/public/modules/sidebar.js +17 -5
- package/lib/public/modules/split-group-helpers.js +8 -0
- package/lib/public/modules/split-pair-ui.js +15 -4
- package/lib/public/modules/split-view.js +5 -2
- package/lib/public/modules/split-worker-runtime.js +24 -0
- package/lib/public/modules/sticky-note-markdown.js +5 -0
- package/lib/public/modules/sticky-notes-browser.js +326 -0
- package/lib/public/modules/sticky-notes-card.js +360 -0
- package/lib/public/modules/sticky-notes-editor.js +291 -0
- package/lib/public/modules/sticky-notes-shared.js +90 -0
- package/lib/public/modules/sticky-notes.js +170 -946
- package/lib/public/modules/thinking-lifecycle.js +175 -0
- package/lib/public/modules/thinking-summary.js +28 -0
- package/lib/public/modules/thinking-view.js +75 -0
- package/lib/public/modules/tool-palette-order.js +143 -0
- package/lib/public/modules/tool-palette-overlays.js +159 -0
- package/lib/public/modules/tool-palette.js +21 -188
- package/lib/public/modules/tools.js +31 -123
- package/lib/public/modules/update-snooze.js +169 -0
- package/lib/public/modules/user-settings.js +4 -0
- package/lib/public/modules/worker-pane-lock.js +176 -0
- package/lib/public/modules/worker-proposal-state.js +16 -0
- package/lib/public/modules/worker-proposal.js +49 -20
- package/lib/public/style.css +3 -1
- package/lib/sdk-bridge.js +135 -46
- package/lib/sdk-message-processor.js +36 -19
- package/lib/server-experimental-settings.js +49 -0
- package/lib/server-global-ws.js +11 -0
- package/lib/server-home-chat-events.js +31 -1
- package/lib/server-home-chat.js +27 -4
- package/lib/server-home-clay-entry.js +1 -1
- package/lib/server-home-clay-session-links.js +34 -5
- package/lib/server-settings.js +5 -0
- package/lib/server-tools.js +127 -9
- package/lib/server.js +137 -22
- package/lib/session-driver-eligibility.js +38 -0
- package/lib/session-driver-orchestration.js +31 -0
- package/lib/session-hygiene.js +3 -0
- package/lib/session-notes-mcp-server.js +25 -5
- package/lib/session-pair-factory.js +247 -0
- package/lib/session-pair-mcp-server.js +46 -8
- package/lib/session-pair-prompts.js +79 -0
- package/lib/session-pair-turn-control.js +153 -0
- package/lib/session-provenance.js +119 -0
- package/lib/session-spawn-mcp-server.js +1 -1
- package/lib/session-split-groups.js +4 -1
- package/lib/session-title-policy.js +60 -0
- package/lib/session-visibility.js +41 -0
- package/lib/sessions.js +57 -10
- package/lib/tools-registry.js +89 -10
- package/lib/update-snooze.js +437 -0
- package/lib/users-experimental-preferences.js +30 -0
- package/lib/users-permissions.js +6 -6
- package/lib/users.js +22 -16
- package/lib/workspace-query-access.js +102 -0
- package/lib/workspace-query-service.js +24 -17
- package/lib/worktree.js +25 -38
- package/lib/ws-schema.js +38 -7
- package/lib/yoke/adapters/claude-worker.js +16 -6
- package/lib/yoke/adapters/claude.js +12 -0
- package/lib/yoke/adapters/codex.js +24 -14
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// Server-side Logic for the Pig validation Capsule.
|
|
2
|
+
//
|
|
3
|
+
// Logic is the single source of truth. Every state change in the game happens
|
|
4
|
+
// through one of these functions and nowhere else: the die is rolled here, the
|
|
5
|
+
// turn rules are enforced here, and the game record lives in the Capsule's own
|
|
6
|
+
// datastore. A human button click and a Mate clay_tool_act call arrive at the
|
|
7
|
+
// same act pipeline with the same rules, so neither party can break a rule the
|
|
8
|
+
// other is bound by, and neither sees state the other cannot.
|
|
9
|
+
//
|
|
10
|
+
// The two seats are "user" and "mate". The server resolves its own actor before
|
|
11
|
+
// calling in, and the human actor maps onto the user seat; seat is never read
|
|
12
|
+
// from caller-supplied text.
|
|
13
|
+
//
|
|
14
|
+
// Rules: seats alternate turns. On your turn you may roll as often as you like;
|
|
15
|
+
// each roll adds to the turn total, but rolling a 1 loses the whole turn total
|
|
16
|
+
// and passes play. Holding banks the turn total into your score, and the first
|
|
17
|
+
// seat to bank at least 100 points wins. A finished game is frozen until
|
|
18
|
+
// someone resets it; only the user may reset a game in progress, because the
|
|
19
|
+
// Capsule belongs to the human, not to the Mate.
|
|
20
|
+
|
|
21
|
+
var crypto = require("crypto");
|
|
22
|
+
|
|
23
|
+
var GAME_DOC_ID = "game";
|
|
24
|
+
var TARGET_SCORE = 100;
|
|
25
|
+
var BUST_FACE = 1;
|
|
26
|
+
var USER = "user";
|
|
27
|
+
var MATE = "mate";
|
|
28
|
+
var SEATS = [USER, MATE];
|
|
29
|
+
var SEAT_LABELS = { user: "You", mate: "Your Mate" };
|
|
30
|
+
var MAX_RECENT_ROLLS = 12;
|
|
31
|
+
|
|
32
|
+
// Every read-modify-write for one stored game runs to completion before the
|
|
33
|
+
// next one starts. Two acts that arrive together would otherwise both validate
|
|
34
|
+
// against the same turn, and the second write would silently discard the first.
|
|
35
|
+
var gameLocks = Object.create(null);
|
|
36
|
+
|
|
37
|
+
function runExclusive(key, operation) {
|
|
38
|
+
var previous = gameLocks[key] || Promise.resolve();
|
|
39
|
+
var settled = previous.then(operation, operation);
|
|
40
|
+
gameLocks[key] = settled.then(function () {}, function () {});
|
|
41
|
+
return settled;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function secureRollDie() {
|
|
45
|
+
return crypto.randomInt(1, 7);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function otherSeat(seat) {
|
|
49
|
+
return seat === USER ? MATE : USER;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function newGame() {
|
|
53
|
+
return {
|
|
54
|
+
status: "playing",
|
|
55
|
+
turn: USER,
|
|
56
|
+
scores: { user: 0, mate: 0 },
|
|
57
|
+
turnTotal: 0,
|
|
58
|
+
lastRoll: null,
|
|
59
|
+
lastActor: null,
|
|
60
|
+
winner: null,
|
|
61
|
+
target: TARGET_SCORE,
|
|
62
|
+
sequence: 0,
|
|
63
|
+
eventSeq: 0,
|
|
64
|
+
recentRolls: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function safeScore(value) {
|
|
69
|
+
return Number.isInteger(value) && value >= 0 && value <= 10000 ? value : 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Counters are ordering, not points: they only ever grow, so they carry no
|
|
73
|
+
// upper bound. eventSeq in particular must survive a reset, because a Display
|
|
74
|
+
// that saw event N must treat anything at or below N as already rendered.
|
|
75
|
+
function safeCounter(value) {
|
|
76
|
+
return Number.isInteger(value) && value >= 0 ? value : 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// A stored game is data, not a promise about shape. Anything unreadable is
|
|
80
|
+
// replaced by a fresh game rather than trusted.
|
|
81
|
+
function normalizeState(stored) {
|
|
82
|
+
if (!stored || typeof stored !== "object" || Array.isArray(stored)) return newGame();
|
|
83
|
+
if (SEATS.indexOf(stored.turn) === -1) return newGame();
|
|
84
|
+
if (stored.status !== "playing" && stored.status !== "complete") return newGame();
|
|
85
|
+
var scores = stored.scores && typeof stored.scores === "object" ? stored.scores : {};
|
|
86
|
+
var rolls = Array.isArray(stored.recentRolls) ? stored.recentRolls.slice(-MAX_RECENT_ROLLS) : [];
|
|
87
|
+
return {
|
|
88
|
+
status: stored.status,
|
|
89
|
+
turn: stored.turn,
|
|
90
|
+
scores: { user: safeScore(scores.user), mate: safeScore(scores.mate) },
|
|
91
|
+
turnTotal: safeScore(stored.turnTotal),
|
|
92
|
+
lastRoll: Number.isInteger(stored.lastRoll) ? stored.lastRoll : null,
|
|
93
|
+
lastActor: SEATS.indexOf(stored.lastActor) !== -1 ? stored.lastActor : null,
|
|
94
|
+
winner: SEATS.indexOf(stored.winner) !== -1 ? stored.winner : null,
|
|
95
|
+
target: TARGET_SCORE,
|
|
96
|
+
sequence: safeCounter(stored.sequence),
|
|
97
|
+
eventSeq: safeCounter(stored.eventSeq),
|
|
98
|
+
recentRolls: rolls.filter(function (entry) {
|
|
99
|
+
return entry && typeof entry.text === "string" && Number.isInteger(entry.face);
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// recentRolls holds rolls and only rolls, including a bust. Banking and winning
|
|
105
|
+
// are turn outcomes rather than rolls, so they never appear here.
|
|
106
|
+
function appendRoll(state, seat, face, text) {
|
|
107
|
+
var next = state.sequence + 1;
|
|
108
|
+
state.sequence = next;
|
|
109
|
+
state.recentRolls = state.recentRolls
|
|
110
|
+
.concat([{ id: "roll-" + next, seat: seat, face: face, text: text }])
|
|
111
|
+
.slice(-MAX_RECENT_ROLLS);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function requirePlayableTurn(state, seat) {
|
|
115
|
+
if (state.status !== "playing") {
|
|
116
|
+
throw new Error("This game is already over. Reset it to play again.");
|
|
117
|
+
}
|
|
118
|
+
if (state.turn !== seat) {
|
|
119
|
+
throw new Error("This move is out of turn: " + SEAT_LABELS[state.turn] + " to play.");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function roll(state, seat, rollDie) {
|
|
124
|
+
requirePlayableTurn(state, seat);
|
|
125
|
+
var face = rollDie();
|
|
126
|
+
if (!Number.isInteger(face) || face < 1 || face > 6) throw new Error("The die produced an invalid face.");
|
|
127
|
+
state.lastRoll = face;
|
|
128
|
+
state.lastActor = seat;
|
|
129
|
+
if (face === BUST_FACE) {
|
|
130
|
+
appendRoll(state, seat, face, SEAT_LABELS[seat] + " rolled a 1 and lost " + state.turnTotal + " point(s).");
|
|
131
|
+
state.turnTotal = 0;
|
|
132
|
+
state.turn = otherSeat(seat);
|
|
133
|
+
return state;
|
|
134
|
+
}
|
|
135
|
+
state.turnTotal += face;
|
|
136
|
+
appendRoll(state, seat, face, SEAT_LABELS[seat] + " rolled a " + face + " for a turn total of " + state.turnTotal + ".");
|
|
137
|
+
return state;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function hold(state, seat) {
|
|
141
|
+
requirePlayableTurn(state, seat);
|
|
142
|
+
var banked = state.turnTotal;
|
|
143
|
+
state.scores[seat] += banked;
|
|
144
|
+
state.lastActor = seat;
|
|
145
|
+
state.turnTotal = 0;
|
|
146
|
+
if (state.scores[seat] >= state.target) {
|
|
147
|
+
state.status = "complete";
|
|
148
|
+
state.winner = seat;
|
|
149
|
+
return state;
|
|
150
|
+
}
|
|
151
|
+
state.turn = otherSeat(seat);
|
|
152
|
+
return state;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function reset(state, seat) {
|
|
156
|
+
if (state.status === "playing" && seat !== USER) {
|
|
157
|
+
throw new Error("Only the user may reset a game that is still in progress.");
|
|
158
|
+
}
|
|
159
|
+
return newGame();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function statusTextFor(state, complete, userTurn) {
|
|
163
|
+
if (complete) {
|
|
164
|
+
return state.winner === USER ? "You win this game!" : "Your Mate wins this game.";
|
|
165
|
+
}
|
|
166
|
+
if (userTurn) {
|
|
167
|
+
return state.turnTotal > 0
|
|
168
|
+
? "Your turn: roll again or hold to bank " + state.turnTotal + " point(s)."
|
|
169
|
+
: "Your turn: roll the die.";
|
|
170
|
+
}
|
|
171
|
+
return "Your Mate's turn. Moves appear here as they happen.";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The projection a Display renders and a Mate reads. It adds no meaning that is
|
|
175
|
+
// absent from state: every derived field is a restatement of the fields above.
|
|
176
|
+
// The score series exist because a progress chart binds to a collection.
|
|
177
|
+
function project(state) {
|
|
178
|
+
var complete = state.status === "complete";
|
|
179
|
+
var userTurn = !complete && state.turn === USER;
|
|
180
|
+
return {
|
|
181
|
+
status: state.status,
|
|
182
|
+
turn: state.turn,
|
|
183
|
+
scores: { user: state.scores.user, mate: state.scores.mate },
|
|
184
|
+
userScoreSeries: [{ seat: SEAT_LABELS.user, value: state.scores.user }],
|
|
185
|
+
mateScoreSeries: [{ seat: SEAT_LABELS.mate, value: state.scores.mate }],
|
|
186
|
+
turnTotal: state.turnTotal,
|
|
187
|
+
turnTotalSeries: [{ label: "Turn total", value: state.turnTotal }],
|
|
188
|
+
turnTotalText: "Turn total " + state.turnTotal + " of " + state.target + ", " + SEAT_LABELS[state.turn] + " to play.",
|
|
189
|
+
statusText: statusTextFor(state, complete, userTurn),
|
|
190
|
+
lastRollText: state.lastRoll === null
|
|
191
|
+
? "No rolls yet."
|
|
192
|
+
: "Last roll: " + state.lastRoll + " by " + SEAT_LABELS[state.lastActor || state.turn] + ".",
|
|
193
|
+
lastRoll: state.lastRoll,
|
|
194
|
+
lastActor: state.lastActor,
|
|
195
|
+
winner: state.winner,
|
|
196
|
+
target: state.target,
|
|
197
|
+
recentRolls: state.recentRolls,
|
|
198
|
+
complete: complete,
|
|
199
|
+
userTurn: userTurn,
|
|
200
|
+
eventSeq: state.eventSeq,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// When the Mate should be engaged is a game rule, so Logic declares it on the
|
|
205
|
+
// event rather than the host inferring it from Pig's fields. The host bridge
|
|
206
|
+
// delivers any declared engagement generically for every Capsule: "turn"
|
|
207
|
+
// means play your seat now, "start" means the human opened a fresh game and
|
|
208
|
+
// you should acknowledge it without acting.
|
|
209
|
+
function engagementFor(seat, actionId, next) {
|
|
210
|
+
if (seat !== USER) return null;
|
|
211
|
+
if (next.status === "playing" && next.turn === MATE) return { kind: "turn" };
|
|
212
|
+
if (actionId === "reset" && next.status === "playing") return { kind: "start" };
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Causality for the live Display: {actor, action, previous, next} plus a
|
|
217
|
+
// monotonic seq. Sending only the new state would make a Mate's move teleport
|
|
218
|
+
// onto the human's screen; the event lets the Display replay and attribute it.
|
|
219
|
+
function buildEvent(seat, actionId, previous, next) {
|
|
220
|
+
var event = {
|
|
221
|
+
seq: next.eventSeq,
|
|
222
|
+
actor: seat,
|
|
223
|
+
action: actionId,
|
|
224
|
+
previous: previous,
|
|
225
|
+
next: next,
|
|
226
|
+
};
|
|
227
|
+
var engage = engagementFor(seat, actionId, next);
|
|
228
|
+
if (engage) event.engage = engage;
|
|
229
|
+
return event;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function seatFor(context) {
|
|
233
|
+
var actor = context && context.actor;
|
|
234
|
+
if (actor === "human" || actor === USER) return USER;
|
|
235
|
+
if (actor === MATE) return MATE;
|
|
236
|
+
throw new Error("The Capsule caller seat could not be determined.");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// One act pipeline. The human surface and the Mate MCP surface both land here.
|
|
240
|
+
function createRuntime(options) {
|
|
241
|
+
options = options || {};
|
|
242
|
+
var storage = options.storage;
|
|
243
|
+
var rollDie = typeof options.rollDie === "function" ? options.rollDie : secureRollDie;
|
|
244
|
+
var lockKey = options.lockKey || "pig";
|
|
245
|
+
if (!storage) throw new Error("The Pig Capsule requires its datastore.");
|
|
246
|
+
|
|
247
|
+
async function writeState(state) {
|
|
248
|
+
await storage.put({ _id: GAME_DOC_ID, state: state });
|
|
249
|
+
return state;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function readState() {
|
|
253
|
+
var doc = await storage.get(GAME_DOC_ID);
|
|
254
|
+
if (!doc) return writeState(newGame());
|
|
255
|
+
return normalizeState(doc.state);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function snapshot(context) {
|
|
259
|
+
seatFor(context);
|
|
260
|
+
return runExclusive(lockKey, async function () {
|
|
261
|
+
return project(await readState());
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// An act returns {state, event}: the new projection for the caller and the
|
|
266
|
+
// causal event for every watching Display. Both are built inside the lock,
|
|
267
|
+
// so seq order on the wire is the order the rules actually ran in.
|
|
268
|
+
async function act(context, actionId, args) {
|
|
269
|
+
var seat = seatFor(context);
|
|
270
|
+
return runExclusive(lockKey, async function () {
|
|
271
|
+
var state = await readState();
|
|
272
|
+
var previous = project(state);
|
|
273
|
+
var next;
|
|
274
|
+
if (actionId === "roll") next = roll(state, seat, rollDie);
|
|
275
|
+
else if (actionId === "hold") next = hold(state, seat);
|
|
276
|
+
else if (actionId === "reset") next = reset(state, seat);
|
|
277
|
+
else throw new Error("Unknown Pig action '" + String(actionId) + "'.");
|
|
278
|
+
next.eventSeq = previous.eventSeq + 1;
|
|
279
|
+
await writeState(next);
|
|
280
|
+
var projected = project(next);
|
|
281
|
+
return { state: projected, event: buildEvent(seat, actionId, previous, projected) };
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return { snapshot: snapshot, act: act };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = {
|
|
289
|
+
GAME_DOC_ID: GAME_DOC_ID,
|
|
290
|
+
TARGET_SCORE: TARGET_SCORE,
|
|
291
|
+
SEATS: SEATS,
|
|
292
|
+
newGame: newGame,
|
|
293
|
+
normalizeState: normalizeState,
|
|
294
|
+
project: project,
|
|
295
|
+
buildEvent: buildEvent,
|
|
296
|
+
roll: roll,
|
|
297
|
+
hold: hold,
|
|
298
|
+
reset: reset,
|
|
299
|
+
createRuntime: createRuntime,
|
|
300
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Trusted server-runtime Logic for shipped Capsules.
|
|
2
|
+
//
|
|
3
|
+
// A server Capsule's Logic runs in this process rather than in a browser
|
|
4
|
+
// worker, so it stays available with no home screen open and owns randomness
|
|
5
|
+
// and hidden state the client must never hold. Only Capsules shipped in
|
|
6
|
+
// lib/capsules/ can claim the server runtime (see tools-registry
|
|
7
|
+
// isTrustedServerRuntime), and only IDs listed here resolve to real Logic.
|
|
8
|
+
//
|
|
9
|
+
// A runtime is created per call. State lives in the Capsule's datastore, never
|
|
10
|
+
// in this module, so nothing here is shared between users.
|
|
11
|
+
|
|
12
|
+
var path = require("path");
|
|
13
|
+
var toolStorage = require("./tool-storage");
|
|
14
|
+
var toolsRegistry = require("./tools-registry");
|
|
15
|
+
var pigLogic = require("./capsule-pig-logic");
|
|
16
|
+
|
|
17
|
+
var RUNTIME_FACTORIES = {
|
|
18
|
+
pig: pigLogic.createRuntime,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function hasRuntime(toolId) {
|
|
22
|
+
return Object.prototype.hasOwnProperty.call(RUNTIME_FACTORIES, toolId);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createRuntime(toolId, ctx, options) {
|
|
26
|
+
if (!hasRuntime(toolId)) return null;
|
|
27
|
+
var settings = Object.assign({}, options || {});
|
|
28
|
+
if (!settings.storage) settings.storage = toolStorage.createToolStorage(ctx, toolId);
|
|
29
|
+
// One stored game, one serialization key, no matter how many runtime
|
|
30
|
+
// instances are created for it.
|
|
31
|
+
if (!settings.lockKey) settings.lockKey = path.join(toolsRegistry.resolveToolsRoot(ctx), toolId);
|
|
32
|
+
return RUNTIME_FACTORIES[toolId](settings);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
hasRuntime: hasRuntime,
|
|
37
|
+
createRuntime: createRuntime,
|
|
38
|
+
ids: Object.keys(RUNTIME_FACTORIES),
|
|
39
|
+
};
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Pig rich Display element. Runs inside the sandboxed Capsule frame.
|
|
2
|
+
//
|
|
3
|
+
// This element is pure rendering over the same projection the floor renders
|
|
4
|
+
// and a Mate reads: it invents no meaning of its own, and its only outward
|
|
5
|
+
// capability is ClayCapsule.act, which lands in the same Logic pipeline as a
|
|
6
|
+
// floor button. Swapping this file in or out changes nothing a Mate sees.
|
|
7
|
+
|
|
8
|
+
(function () {
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
var api = window.ClayCapsule;
|
|
12
|
+
var state = null;
|
|
13
|
+
var caption = "";
|
|
14
|
+
var animating = null;
|
|
15
|
+
|
|
16
|
+
document.body.style.margin = "0";
|
|
17
|
+
document.body.style.background = "#141414";
|
|
18
|
+
document.body.style.fontFamily = "system-ui, sans-serif";
|
|
19
|
+
|
|
20
|
+
var root = document.createElement("div");
|
|
21
|
+
root.style.padding = "12px";
|
|
22
|
+
document.body.appendChild(root);
|
|
23
|
+
|
|
24
|
+
var captionEl = document.createElement("div");
|
|
25
|
+
captionEl.style.color = "#b8b8b8";
|
|
26
|
+
captionEl.style.fontSize = "12px";
|
|
27
|
+
captionEl.style.minHeight = "16px";
|
|
28
|
+
captionEl.style.marginBottom = "8px";
|
|
29
|
+
root.appendChild(captionEl);
|
|
30
|
+
|
|
31
|
+
var canvas = document.createElement("canvas");
|
|
32
|
+
canvas.width = 560;
|
|
33
|
+
canvas.height = 190;
|
|
34
|
+
canvas.style.width = "100%";
|
|
35
|
+
canvas.style.display = "block";
|
|
36
|
+
root.appendChild(canvas);
|
|
37
|
+
var ctx = canvas.getContext("2d");
|
|
38
|
+
|
|
39
|
+
var controls = document.createElement("div");
|
|
40
|
+
controls.style.display = "flex";
|
|
41
|
+
controls.style.gap = "8px";
|
|
42
|
+
controls.style.marginTop = "10px";
|
|
43
|
+
root.appendChild(controls);
|
|
44
|
+
|
|
45
|
+
var rulesEl = document.createElement("p");
|
|
46
|
+
rulesEl.textContent = "Pig is a push-your-luck dice game: roll to build this turn's total, but a 1 loses it. Hold to bank your points; the first player to 100 wins.";
|
|
47
|
+
rulesEl.style.margin = "12px 0 0";
|
|
48
|
+
rulesEl.style.color = "#b8b8b8";
|
|
49
|
+
rulesEl.style.fontSize = "12px";
|
|
50
|
+
rulesEl.style.lineHeight = "1.45";
|
|
51
|
+
root.appendChild(rulesEl);
|
|
52
|
+
|
|
53
|
+
function makeButton(label, action, accent) {
|
|
54
|
+
var button = document.createElement("button");
|
|
55
|
+
button.type = "button";
|
|
56
|
+
button.textContent = label;
|
|
57
|
+
button.style.flex = "1";
|
|
58
|
+
button.style.padding = "8px 0";
|
|
59
|
+
button.style.border = "0";
|
|
60
|
+
button.style.borderRadius = "8px";
|
|
61
|
+
button.style.fontSize = "13px";
|
|
62
|
+
button.style.cursor = "pointer";
|
|
63
|
+
button.style.background = accent ? "#c96f4a" : "#2c2c2c";
|
|
64
|
+
button.style.color = accent ? "#141414" : "#d8d8d8";
|
|
65
|
+
button.addEventListener("click", function () { api.act(action, {}); });
|
|
66
|
+
controls.appendChild(button);
|
|
67
|
+
return button;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var rollButton = makeButton("Roll", "roll", true);
|
|
71
|
+
var holdButton = makeButton("Hold", "hold", false);
|
|
72
|
+
var resetButton = makeButton("Reset", "reset", false);
|
|
73
|
+
|
|
74
|
+
function drawBar(y, label, value, target, color) {
|
|
75
|
+
ctx.fillStyle = "#b8b8b8";
|
|
76
|
+
ctx.font = "12px system-ui, sans-serif";
|
|
77
|
+
ctx.fillText(label + " " + value + " / " + target, 12, y - 6);
|
|
78
|
+
ctx.fillStyle = "#2c2c2c";
|
|
79
|
+
ctx.fillRect(12, y, 380, 14);
|
|
80
|
+
ctx.fillStyle = color;
|
|
81
|
+
var width = Math.max(0, Math.min(1, value / target)) * 380;
|
|
82
|
+
ctx.fillRect(12, y, width, 14);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function drawDie(face, highlight) {
|
|
86
|
+
var x = 430;
|
|
87
|
+
var y = 40;
|
|
88
|
+
var size = 96;
|
|
89
|
+
ctx.fillStyle = highlight ? "#f0e5d8" : "#d8d8d8";
|
|
90
|
+
ctx.beginPath();
|
|
91
|
+
ctx.roundRect(x, y, size, size, 14);
|
|
92
|
+
ctx.fill();
|
|
93
|
+
if (!face) return;
|
|
94
|
+
ctx.fillStyle = face === 1 ? "#c9564a" : "#141414";
|
|
95
|
+
var spots = {
|
|
96
|
+
1: [[0.5, 0.5]],
|
|
97
|
+
2: [[0.28, 0.28], [0.72, 0.72]],
|
|
98
|
+
3: [[0.25, 0.25], [0.5, 0.5], [0.75, 0.75]],
|
|
99
|
+
4: [[0.28, 0.28], [0.72, 0.28], [0.28, 0.72], [0.72, 0.72]],
|
|
100
|
+
5: [[0.25, 0.25], [0.75, 0.25], [0.5, 0.5], [0.25, 0.75], [0.75, 0.75]],
|
|
101
|
+
6: [[0.28, 0.22], [0.72, 0.22], [0.28, 0.5], [0.72, 0.5], [0.28, 0.78], [0.72, 0.78]],
|
|
102
|
+
};
|
|
103
|
+
var pips = spots[face] || [];
|
|
104
|
+
for (var i = 0; i < pips.length; i++) {
|
|
105
|
+
ctx.beginPath();
|
|
106
|
+
ctx.arc(x + pips[i][0] * size, y + pips[i][1] * size, 7, 0, Math.PI * 2);
|
|
107
|
+
ctx.fill();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function draw(faceOverride, highlight) {
|
|
112
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
113
|
+
if (!state) {
|
|
114
|
+
ctx.fillStyle = "#b8b8b8";
|
|
115
|
+
ctx.font = "13px system-ui, sans-serif";
|
|
116
|
+
ctx.fillText("Waiting for the game state...", 12, 30);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
var target = state.target || 100;
|
|
120
|
+
var scores = state.scores || {};
|
|
121
|
+
drawBar(36, "You", scores.user || 0, target, "#5a9e6f");
|
|
122
|
+
drawBar(78, "Your Mate", scores.mate || 0, target, "#6f7fc9");
|
|
123
|
+
ctx.fillStyle = "#d8d8d8";
|
|
124
|
+
ctx.font = "13px system-ui, sans-serif";
|
|
125
|
+
ctx.fillText(state.turnTotalText || "", 12, 122);
|
|
126
|
+
if (state.complete && state.winner) {
|
|
127
|
+
ctx.fillStyle = "#f0c96f";
|
|
128
|
+
ctx.font = "600 15px system-ui, sans-serif";
|
|
129
|
+
ctx.fillText((state.winner === "user" ? "You win!" : "Your Mate wins!"), 12, 152);
|
|
130
|
+
}
|
|
131
|
+
drawDie(faceOverride !== undefined ? faceOverride : state.lastRoll, !!highlight);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function syncButtons() {
|
|
135
|
+
var userTurn = !!(state && state.userTurn);
|
|
136
|
+
rollButton.disabled = !userTurn;
|
|
137
|
+
holdButton.disabled = !userTurn;
|
|
138
|
+
rollButton.style.opacity = userTurn ? "1" : "0.4";
|
|
139
|
+
holdButton.style.opacity = userTurn ? "1" : "0.4";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Animates a decided roll: a brief pip shuffle that settles on the face the
|
|
143
|
+
// server already rolled. Pure presentation of a fact already in state.
|
|
144
|
+
function animateRoll(finalFace, next) {
|
|
145
|
+
if (animating) clearInterval(animating.timer);
|
|
146
|
+
var frames = 8;
|
|
147
|
+
var animation = { timer: null };
|
|
148
|
+
animating = animation;
|
|
149
|
+
animation.timer = setInterval(function () {
|
|
150
|
+
frames--;
|
|
151
|
+
if (frames <= 0) {
|
|
152
|
+
clearInterval(animation.timer);
|
|
153
|
+
if (animating === animation) animating = null;
|
|
154
|
+
state = next;
|
|
155
|
+
draw(finalFace, finalFace === 1);
|
|
156
|
+
syncButtons();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
draw((frames % 6) + 1, true);
|
|
160
|
+
}, 55);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
api.onState = function (nextState) {
|
|
164
|
+
state = nextState || {};
|
|
165
|
+
if (!animating) {
|
|
166
|
+
draw();
|
|
167
|
+
syncButtons();
|
|
168
|
+
}
|
|
169
|
+
captionEl.textContent = caption;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
api.onEvent = function (event) {
|
|
173
|
+
if (!event || !event.next) return;
|
|
174
|
+
var who = event.actor === "mate" ? "Your Mate" : "You";
|
|
175
|
+
var next = event.next;
|
|
176
|
+
if (event.action === "roll") {
|
|
177
|
+
caption = next.lastRoll === 1
|
|
178
|
+
? who + " rolled a 1 and lost the turn total."
|
|
179
|
+
: who + " rolled a " + next.lastRoll + ".";
|
|
180
|
+
captionEl.textContent = caption;
|
|
181
|
+
animateRoll(next.lastRoll, next);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (event.action === "hold") {
|
|
185
|
+
var banked = (event.previous && event.previous.turnTotal) || 0;
|
|
186
|
+
caption = who + " held and banked " + banked + " point(s).";
|
|
187
|
+
} else if (event.action === "reset") {
|
|
188
|
+
caption = who + " started a new game.";
|
|
189
|
+
}
|
|
190
|
+
state = next;
|
|
191
|
+
captionEl.textContent = caption;
|
|
192
|
+
draw();
|
|
193
|
+
syncButtons();
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
draw();
|
|
197
|
+
syncButtons();
|
|
198
|
+
})();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "pig",
|
|
3
|
+
"name": "Pig",
|
|
4
|
+
"runtime": "server",
|
|
5
|
+
"lucideIcon": "sparkles",
|
|
6
|
+
"description": "Two-player push-your-luck dice game shared by the user and one Mate.",
|
|
7
|
+
"useWhen": "Use when the user wants to play Pig, take a turn, or hear how the current game stands.",
|
|
8
|
+
"skills": "# Pig\n\nA two-seat push-your-luck dice game. The seats are `user` and `mate`; the person holds the user seat and you hold the mate seat. All randomness, turn order, and scoring are enforced server-side by the Capsule's Logic; you cannot roll a die yourself, and neither can the user.\n\n## Rules\n\n- Seats alternate turns. On your turn you may roll as often as you like.\n- Each roll adds its face value to the turn total. Rolling a 1 loses the entire turn total and passes play to the other seat.\n- Holding banks the turn total into your score and passes play.\n- The first seat to bank at least 100 points wins. A finished game is frozen until it is reset.\n- Only the user may reset a game that is still in progress. You may reset once the game is complete.\n\n## How to play\n\n1. `clay_tool_snapshot` with toolId `pig` to read the current state: `turn`, `scores.user`, `scores.mate`, `turnTotal`, `status`, `winner`, and `recentRolls`, which lists recent rolls only and never holds or wins.\n2. Act only when `turn` is `mate`. `clay_tool_act` with actionId `roll`, `hold`, or `reset`. Arguments are not used.\n3. An out-of-turn or post-game call is refused by Logic with an explanatory error. Read the snapshot again rather than retrying blindly.\n4. Every act returns the complete new state, so a snapshot immediately after an act is redundant.\n\n## Playing well\n\nThe interesting part of this Capsule is the judgement call, not the mechanics: decide whether to roll again or hold, and say why in your own words before you act. Typical play holds at a turn total near 20, but the score gap matters more than the rule of thumb, so explain the trade-off you are taking. Never claim a roll result before the act returns it.\n\n## Turn flow\n\nWhen the user's move hands the turn to your seat, Clay wakes you in a dedicated game session with a \"Capsule turn\" message. Take your whole turn there: roll as often as you judge right, then hold (or bust), and stop once the turn passes away from your seat. The user's Display updates live as you act, so never narrate moves you have not made. If nothing wakes you, you are not on turn."
|
|
9
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "stack",
|
|
3
|
+
"props": { "gap": "md" },
|
|
4
|
+
"children": [
|
|
5
|
+
{
|
|
6
|
+
"type": "callout",
|
|
7
|
+
"when": { "equals": { "path": "$state.complete", "value": true } },
|
|
8
|
+
"props": { "title": "Game over", "text": { "$bind": "$state.statusText", "fallback": "Game over." }, "icon": "circle-check", "variant": "soft" },
|
|
9
|
+
"else": {
|
|
10
|
+
"type": "callout",
|
|
11
|
+
"props": { "title": "Pig", "text": { "$bind": "$state.statusText", "fallback": "Your turn: roll the die." }, "icon": "sparkles", "variant": "soft" }
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"type": "row",
|
|
16
|
+
"props": { "gap": "md", "wrap": true },
|
|
17
|
+
"children": [
|
|
18
|
+
{
|
|
19
|
+
"type": "chart",
|
|
20
|
+
"bind": "userScoreSeries",
|
|
21
|
+
"props": { "label": "Your score", "kind": "progress", "categoryKey": "seat", "valueKey": "value", "max": 100 }
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "chart",
|
|
25
|
+
"bind": "mateScoreSeries",
|
|
26
|
+
"props": { "label": "Your Mate's score", "kind": "progress", "categoryKey": "seat", "valueKey": "value", "max": 100 }
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"type": "row",
|
|
32
|
+
"props": { "gap": "md", "align": "center", "wrap": true },
|
|
33
|
+
"children": [
|
|
34
|
+
{
|
|
35
|
+
"type": "chart",
|
|
36
|
+
"bind": "turnTotalSeries",
|
|
37
|
+
"props": { "label": "Turn total", "kind": "metric", "valueKey": "value" }
|
|
38
|
+
},
|
|
39
|
+
{ "type": "text", "props": { "text": { "$bind": "$state.lastRollText", "fallback": "No rolls yet." }, "role": "muted" } }
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"type": "row",
|
|
44
|
+
"props": { "gap": "sm", "wrap": true },
|
|
45
|
+
"children": [
|
|
46
|
+
{
|
|
47
|
+
"type": "button",
|
|
48
|
+
"action": "roll",
|
|
49
|
+
"when": { "equals": { "path": "$state.userTurn", "value": true } },
|
|
50
|
+
"props": { "label": "Roll", "variant": "primary" },
|
|
51
|
+
"else": { "type": "button", "action": "roll", "props": { "label": "Roll", "variant": "primary", "disabled": true } }
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"type": "button",
|
|
55
|
+
"action": "hold",
|
|
56
|
+
"when": { "equals": { "path": "$state.userTurn", "value": true } },
|
|
57
|
+
"props": { "label": "Hold", "variant": "secondary" },
|
|
58
|
+
"else": { "type": "button", "action": "hold", "props": { "label": "Hold", "variant": "secondary", "disabled": true } }
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"type": "button",
|
|
62
|
+
"action": "reset",
|
|
63
|
+
"when": { "equals": { "path": "$state.complete", "value": true } },
|
|
64
|
+
"props": { "label": "New game", "variant": "primary" },
|
|
65
|
+
"else": { "type": "button", "action": "reset", "props": { "label": "Start over", "variant": "ghost" } }
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"type": "section",
|
|
71
|
+
"props": { "label": "Recent rolls", "variant": "inset", "gap": "xs" },
|
|
72
|
+
"children": [
|
|
73
|
+
{
|
|
74
|
+
"type": "list",
|
|
75
|
+
"bind": "recentRolls",
|
|
76
|
+
"props": { "variant": "divided", "gap": "xs" },
|
|
77
|
+
"children": [
|
|
78
|
+
{ "type": "text", "props": { "text": "$item.text", "role": "body" } }
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
package/lib/config.js
CHANGED
|
@@ -148,12 +148,36 @@ function saveConfig(config) {
|
|
|
148
148
|
chmodSafe(configPath(), 0o600);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
// Does a process with this pid exist? Existence only — never ownership.
|
|
152
|
+
//
|
|
153
|
+
// `process.kill(pid, 0)` sends no signal; it just asks the kernel. Three
|
|
154
|
+
// answers matter, and conflating them has caused real damage:
|
|
155
|
+
//
|
|
156
|
+
// success -> the process exists and we may signal it
|
|
157
|
+
// EPERM -> the process EXISTS but belongs to another user. This is alive.
|
|
158
|
+
// Reading it as dead is how a daemon started by another OS user
|
|
159
|
+
// (or under OS-user isolation) got its config cleared and its
|
|
160
|
+
// socket unlinked by a second daemon, which then started
|
|
161
|
+
// alongside it.
|
|
162
|
+
// ESRCH -> no such process. This is the only "dead".
|
|
163
|
+
//
|
|
164
|
+
// Any other error is a question we could not answer, so we answer "alive":
|
|
165
|
+
// every caller uses this to gate a destructive or duplicating action
|
|
166
|
+
// (clearStaleConfig unlinks the live socket; the migration lock would be
|
|
167
|
+
// stolen), and doing nothing is always the recoverable choice.
|
|
168
|
+
//
|
|
169
|
+
// The pid is validated first, so a malformed value never reaches the kernel
|
|
170
|
+
// call. That matters beyond hygiene: process.kill accepts 0 and negative pids
|
|
171
|
+
// as process-GROUP selectors and returns success for them, so an unvalidated
|
|
172
|
+
// 0 or -1 in a config file used to report a live daemon that does not exist.
|
|
151
173
|
function isPidAlive(pid) {
|
|
174
|
+
if (typeof pid !== "number" || !isFinite(pid) || Math.floor(pid) !== pid || pid <= 0) return false;
|
|
152
175
|
try {
|
|
153
176
|
process.kill(pid, 0);
|
|
154
177
|
return true;
|
|
155
178
|
} catch (e) {
|
|
156
|
-
return false;
|
|
179
|
+
if (e && e.code === "ESRCH") return false;
|
|
180
|
+
return true;
|
|
157
181
|
}
|
|
158
182
|
}
|
|
159
183
|
|