@commonlyai/cli 0.1.77 → 0.1.78

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/cli",
3
- "version": "0.1.77",
3
+ "version": "0.1.78",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -173,8 +173,10 @@ export const liveInUsePids = (home, version, {
173
173
  *
174
174
  * Called by `prepareMcpSpawn`, keyed by THIS process: the MCP server runs as a
175
175
  * descendant of the process that materialised the command, so while that process
176
- * lives its version dir must not be pruned. Best-effort by construction — a home
177
- * that cannot be written to costs the seat nothing but its claim.
176
+ * lives its version dir must not be pruned. A claim that cannot be written is
177
+ * reported, never swallowed: `prepareMcpSpawn` refuses to exec from a dir it
178
+ * could not claim (Vera 74903), because an unclaimed dir is one the pruner is
179
+ * free to `rm -rf` while this seat is running out of it.
178
180
  */
179
181
  export const claimInUse = ({
180
182
  home,
@@ -184,15 +186,30 @@ export const claimInUse = ({
184
186
  writeFile = writeFileSync,
185
187
  } = {}) => {
186
188
  try {
187
- if (!isUsableWarmHome(home) || !parseVersion(version) || parsePid(pid) === null) return false;
189
+ if (!isUsableWarmHome(home) || !parseVersion(version) || parsePid(pid) === null) {
190
+ return { claimed: false, reason: 'not_claimable' };
191
+ }
188
192
  mkdir(inUseDirFor(home, version), { recursive: true });
189
193
  writeFile(inUseMarkerPath(home, version, pid), '');
190
- return true;
191
- } catch {
192
- return false;
194
+ return { claimed: true, reason: null };
195
+ } catch (error) {
196
+ // The errno is the difference between a home nobody can write to and a
197
+ // `.inuse` path that is a file, and only one of those is worth a log line
198
+ // that says which.
199
+ return { claimed: false, reason: (error && error.code) || 'claim_failed' };
193
200
  }
194
201
  };
195
202
 
203
+ /**
204
+ * Say it out loud. A seat whose claim failed is still a seat, but it must not be
205
+ * a silent one: warn() is injectable so a test can hold the sentence.
206
+ */
207
+ const defaultWarn = (line) => {
208
+ try {
209
+ process.stderr.write(`${line}\n`);
210
+ } catch { /* a seat that cannot warn still runs */ }
211
+ };
212
+
196
213
  const defaultRead = (path) => {
197
214
  try {
198
215
  return existsSync(path) ? readFileSync(path, 'utf8') : null;
@@ -435,17 +452,31 @@ export const prepareMcpSpawn = (command, {
435
452
  now = Date.now(),
436
453
  readFile = defaultRead,
437
454
  exists = defaultExists,
455
+ warn = defaultWarn,
438
456
  } = {}) => {
439
457
  const resolvedHome = resolveHomeDir(home);
440
458
  try {
441
459
  const plan = planMcpSpawn(command, { home: resolvedHome, readFile, exists });
442
460
  // Claim the version this spawn is about to execute from, BEFORE returning the
443
461
  // command, so a prune that runs while the seat is starting cannot take it.
444
- if (plan.source === 'home') claimInUse({ home: resolvedHome, version: plan.version });
462
+ let resolved = plan.command;
463
+ if (plan.source === 'home') {
464
+ const claim = claimInUse({ home: resolvedHome, version: plan.version });
465
+ // A claim that could not be written means the pruner may take this very
466
+ // directory out from under the running seat — the hazard the claim exists
467
+ // for. The declared command is what the guard approved and what no pruner
468
+ // owns, so the spawn falls back to it rather than executing unprotected,
469
+ // and says why. The warm still runs: with the seat off the dir, a prune or
470
+ // a repair is safe.
471
+ if (!claim.claimed) {
472
+ warn(`commonly: cannot claim ${versionDirFor(resolvedHome, plan.version)} for pid ${process.pid} (${claim.reason}) — running the declared command instead, because a prune could delete the directory this seat would run from`);
473
+ resolved = command;
474
+ }
475
+ }
445
476
  const due = !lockPresent(resolvedHome, { exists })
446
477
  && warmLooksWanted(resolvedHome, plan.version, { now, readFile });
447
478
  if (due) kickWarm({ home: resolvedHome, apiUrl, spawnImpl, childPath });
448
- return plan.command;
479
+ return resolved;
449
480
  } catch {
450
481
  // A rewrite that throws must not cost the seat its tools: the declaration
451
482
  // is what the guard approved, and it still runs.