@floh-solutions/pharos-cli 0.32.0 → 0.34.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/README.md +154 -32
- package/dist/capabilities.d.ts +27 -0
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +144 -41
- package/dist/capabilities.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +62 -14
- package/dist/cli.js.map +1 -1
- package/dist/commands/delegate.d.ts +151 -6
- package/dist/commands/delegate.d.ts.map +1 -1
- package/dist/commands/delegate.js +575 -147
- package/dist/commands/delegate.js.map +1 -1
- package/dist/commands/doctor.d.ts +41 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +345 -44
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/pr.d.ts +56 -0
- package/dist/commands/pr.d.ts.map +1 -0
- package/dist/commands/pr.js +202 -0
- package/dist/commands/pr.js.map +1 -0
- package/dist/commands/setup.js +21 -3
- package/dist/commands/setup.js.map +1 -1
- package/dist/delegate/argus.d.ts +111 -2
- package/dist/delegate/argus.d.ts.map +1 -1
- package/dist/delegate/argus.js +99 -1
- package/dist/delegate/argus.js.map +1 -1
- package/dist/delegate/hosts.d.ts +29 -0
- package/dist/delegate/hosts.d.ts.map +1 -1
- package/dist/delegate/hosts.js +23 -0
- package/dist/delegate/hosts.js.map +1 -1
- package/dist/delegate/sessions.d.ts +174 -1
- package/dist/delegate/sessions.d.ts.map +1 -1
- package/dist/delegate/sessions.js +282 -28
- package/dist/delegate/sessions.js.map +1 -1
- package/dist/delegate/vsix.d.ts +26 -0
- package/dist/delegate/vsix.d.ts.map +1 -1
- package/dist/delegate/vsix.js +22 -5
- package/dist/delegate/vsix.js.map +1 -1
- package/package.json +3 -3
- package/skill/SKILL.md +32 -9
- package/vscode/pharos-bridge.json +1 -1
- package/vscode/pharos-bridge.vsix +0 -0
package/dist/commands/doctor.js
CHANGED
|
@@ -3,9 +3,11 @@ import { access, constants } from "node:fs/promises";
|
|
|
3
3
|
import { homedir, platform } from "node:os";
|
|
4
4
|
import { delimiter, join } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { MARKETPLACE_NAME, PLUGIN } from "../capabilities.js";
|
|
6
7
|
import { CONVERT_RELEASE } from "../convert.js";
|
|
7
|
-
import { argusSocketPath, ArgusUnreachableError, askArgus, delegateStatusRequest, PROBE_REQUEST, readTimeoutMs, servesDelegate, systemArgusDeps, } from "../delegate/argus.js";
|
|
8
|
+
import { argusSocketPath, ArgusUnreachableError, askArgus, candidatesSupport, delegateCandidatesRequest, delegateStatusRequest, PROBE_REQUEST, readTimeoutMs, servesDelegate, systemArgusDeps, } from "../delegate/argus.js";
|
|
8
9
|
import { findApp, HOSTS } from "../delegate/hosts.js";
|
|
10
|
+
import { carriedProfiles, carriedSentence, systemProbes } from "../delegate/sessions.js";
|
|
9
11
|
import { BRIDGE_EXTENSION_ID, bridgeAtLeast, bundledBridge, bundledVsixPath, CLAUDE_EXTENSION_ID, CODEX_EXTENSION_ID, extensionsDirectory, installedExtensions, SESSION_BRIDGE_VERSION, } from "../delegate/vsix.js";
|
|
10
12
|
import { emit, emitText } from "../output.js";
|
|
11
13
|
const run = promisify(execFile);
|
|
@@ -199,22 +201,24 @@ export async function runDoctor(io, env, options, pretty, version) {
|
|
|
199
201
|
+ "`pharos setup --convert` replaces it.";
|
|
200
202
|
}
|
|
201
203
|
const config = configCheck(env);
|
|
202
|
-
// **Run together, reported in order.** These
|
|
203
|
-
// other and every one of them is slow for a different reason —
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
const [
|
|
204
|
+
// **Run together, reported in order.** These four are independent of each
|
|
205
|
+
// other and every one of them is slow for a different reason — the skill rows
|
|
206
|
+
// shell out to another CLI, `--verify` makes two Azure DevOps calls, and `gh
|
|
207
|
+
// auth status` validates each token against github.com. Serially that is the
|
|
208
|
+
// sum; the report is assembled afterwards, so the order a reader sees is
|
|
209
|
+
// unchanged.
|
|
210
|
+
const [skills, scopes, github, delegation] = await Promise.all([
|
|
209
211
|
// The skill is what teaches a session this tool exists. A CLI installed
|
|
210
212
|
// without it is a CLI nobody reaches for, so its absence belongs in tier 0
|
|
211
|
-
// beside the things that make pharos work at all.
|
|
212
|
-
|
|
213
|
+
// beside the things that make pharos work at all. One row per agent that is
|
|
214
|
+
// actually here — the probes above have already answered that, so this asks
|
|
215
|
+
// nothing it can read.
|
|
216
|
+
skillChecks(env, checks.find((c) => c.id === "claude"), checks.find((c) => c.id === "codex")),
|
|
213
217
|
options.verify ? verifyScopes(env, config.ok) : Promise.resolve([]),
|
|
214
218
|
githubChecks(env, checks.find((c) => c.id === "gh"), options.verify),
|
|
215
219
|
delegationChecks(env),
|
|
216
220
|
]);
|
|
217
|
-
checks.push(
|
|
221
|
+
checks.push(...skills, config, ...scopes, ...github, ...delegation);
|
|
218
222
|
const result = {
|
|
219
223
|
ok: checks.filter((c) => c.tier === 0).every((c) => c.ok),
|
|
220
224
|
checks,
|
|
@@ -322,35 +326,90 @@ function installCommand(probe) {
|
|
|
322
326
|
return probe.install.apt ?? null;
|
|
323
327
|
}
|
|
324
328
|
/**
|
|
325
|
-
* Is the skill installed
|
|
329
|
+
* Is the skill installed for each agent on this machine, and is it CURRENT?
|
|
326
330
|
*
|
|
327
|
-
*
|
|
328
|
-
* new version to the marketplace does not update an installed plugin** — found
|
|
329
|
-
* on the author's own machine, running 1.17.0 at user scope and 1.12.0 at
|
|
330
|
-
* project scope while 1.19.0 had been published. Seven versions of corrections
|
|
331
|
-
* that shipped and were never read.
|
|
331
|
+
* ## One row per agent FOUND, and that is the whole shape (#1399)
|
|
332
332
|
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
333
|
+
* The skill used to be one row about Claude Code, because Claude Code was the
|
|
334
|
+
* only agent that could install it. **Codex reads the same Claude-format
|
|
335
|
+
* `.claude-plugin/marketplace.json`** — it generates its own
|
|
336
|
+
* `.codex-plugin/plugin.json` scoped to `./skills/pharos` and reports
|
|
337
|
+
* `pharos@floh-skills installed, enabled 1.28.0` — so a machine can now run two
|
|
338
|
+
* agents, and one of them can be several versions behind the other or missing
|
|
339
|
+
* the skill entirely. One row cannot hold two independent answers.
|
|
340
|
+
*
|
|
341
|
+
* So: `skill` keeps its id and now means *the skill in Claude Code*, and
|
|
342
|
+
* `skill-codex` is the Codex row. **Each is emitted only when its agent is
|
|
343
|
+
* actually on PATH**, which is the same rule the `<host>-bridge` rows follow —
|
|
344
|
+
* the `claude` and `codex` rows below in tier 2 are the fact for a missing
|
|
345
|
+
* agent, and a column of red about a tool you do not use is noise.
|
|
346
|
+
*
|
|
347
|
+
* That fixes something that was wrong before it: the old row reported `ok:
|
|
348
|
+
* false` whenever `claude` was absent, and it is TIER 0, so a perfectly healthy
|
|
349
|
+
* Codex-only machine was told pharos itself was not in working order.
|
|
350
|
+
*
|
|
351
|
+
* ## The half that hides
|
|
352
|
+
*
|
|
353
|
+
* **Publishing a new version to the marketplace does not update an installed
|
|
354
|
+
* plugin** — found on the author's own machine, running 1.17.0 at user scope
|
|
355
|
+
* and 1.12.0 at project scope while 1.19.0 had been published. Seven versions
|
|
356
|
+
* of corrections that shipped and were never read. Each agent has its own
|
|
357
|
+
* update command and they are not the same, so each row names its own.
|
|
358
|
+
*
|
|
359
|
+
* There is no comparison against "the version this CLI ships", deliberately:
|
|
360
|
+
* the skill's version is a field in the marketplace manifest over in
|
|
361
|
+
* `floh-skills`, bumped by hand at release time, and the npm package does not
|
|
362
|
+
* carry it. A constant copied in here would be the `CONVERT_RELEASE` hash
|
|
363
|
+
* written by hand — a number nobody checked, confidently wrong the first time
|
|
364
|
+
* somebody forgets it. What IS measurable is the two agents against each other,
|
|
365
|
+
* and {@link markSkillDrift} says so when they disagree.
|
|
336
366
|
*/
|
|
337
|
-
async function
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
367
|
+
async function skillChecks(env, claude, codex) {
|
|
368
|
+
const wanted = [];
|
|
369
|
+
if (claude?.ok === true)
|
|
370
|
+
wanted.push(claudeSkillCheck(env));
|
|
371
|
+
if (codex?.ok === true)
|
|
372
|
+
wanted.push(codexSkillCheck(env));
|
|
373
|
+
const rows = await Promise.all(wanted);
|
|
374
|
+
markSkillDrift(rows);
|
|
375
|
+
return rows;
|
|
376
|
+
}
|
|
377
|
+
/** What every skill row promises, whichever agent it is about. */
|
|
378
|
+
function skillBase(id, agent) {
|
|
379
|
+
return {
|
|
380
|
+
id,
|
|
381
|
+
name: `The pharos skill in ${agent}`,
|
|
341
382
|
tier: 0,
|
|
342
|
-
unlocks:
|
|
343
|
-
without:
|
|
383
|
+
unlocks: `${agent} knows to reach for pharos without being told.`,
|
|
384
|
+
without: `The CLI works, but no ${agent} session knows it is there.`,
|
|
344
385
|
install: "pharos setup --install skill",
|
|
345
386
|
};
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* The Claude Code row. Only reached when `claude` is on PATH.
|
|
390
|
+
*
|
|
391
|
+
* A failure here is therefore NOT "no plugin system" — that was the old
|
|
392
|
+
* meaning, and it is now the absent row. It is a `claude` that could not answer,
|
|
393
|
+
* which is a third state and is worded as one: reporting "not installed" about
|
|
394
|
+
* a question nobody managed to ask is the overclaim `gh-auth` exists to avoid.
|
|
395
|
+
*/
|
|
396
|
+
async function claudeSkillCheck(env) {
|
|
397
|
+
const base = skillBase("skill", "Claude Code");
|
|
346
398
|
try {
|
|
347
399
|
const { stdout } = await run("claude", ["plugin", "list"], { timeout: 20_000, env });
|
|
348
400
|
// `pharos@floh-skills` then a Version line somewhere after it. Scoped to a
|
|
349
401
|
// window rather than the whole output, so another plugin's version cannot
|
|
350
402
|
// be read as this one's.
|
|
351
|
-
const at = stdout.indexOf(
|
|
403
|
+
const at = stdout.indexOf(PLUGIN);
|
|
352
404
|
if (at === -1) {
|
|
353
|
-
return {
|
|
405
|
+
return {
|
|
406
|
+
...base,
|
|
407
|
+
ok: false,
|
|
408
|
+
version: null,
|
|
409
|
+
path: null,
|
|
410
|
+
note: `Claude Code is here and \`${PLUGIN}\` is not among its plugins. `
|
|
411
|
+
+ "`pharos setup --install skill` adds the marketplace and installs it.",
|
|
412
|
+
};
|
|
354
413
|
}
|
|
355
414
|
const version = /Version:\s*(\S+)/.exec(stdout.slice(at, at + 200))?.[1] ?? null;
|
|
356
415
|
return {
|
|
@@ -359,23 +418,171 @@ async function skillCheck(env) {
|
|
|
359
418
|
version,
|
|
360
419
|
path: null,
|
|
361
420
|
note: "Publishing a new skill does NOT update an installed one — "
|
|
362
|
-
+
|
|
421
|
+
+ `\`claude plugin update ${PLUGIN}\` does, and a restart loads it.`,
|
|
363
422
|
};
|
|
364
423
|
}
|
|
365
|
-
catch {
|
|
366
|
-
// No `claude` on PATH: codex, antigravity and a plain shell all use this
|
|
367
|
-
// CLI and have no plugin system. Not a failure, just not applicable.
|
|
424
|
+
catch (error) {
|
|
368
425
|
return {
|
|
369
426
|
...base,
|
|
370
427
|
ok: false,
|
|
371
428
|
version: null,
|
|
372
429
|
path: null,
|
|
373
430
|
install: null,
|
|
374
|
-
note:
|
|
375
|
-
+ "
|
|
431
|
+
note: `\`claude plugin list\` could not be run: ${messageOf(error)}. So this is NOT known to `
|
|
432
|
+
+ "be missing — the question could not be asked. A Claude Code without a plugin "
|
|
433
|
+
+ "subcommand is the likely cause.",
|
|
376
434
|
};
|
|
377
435
|
}
|
|
378
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* The Codex row. Only reached when `codex` is on PATH.
|
|
439
|
+
*
|
|
440
|
+
* `codex plugin list --json --available --marketplace floh-skills` is the whole
|
|
441
|
+
* probe, and every part of that command line is load-bearing:
|
|
442
|
+
*
|
|
443
|
+
* - **`--marketplace`** narrows it to ours. Without it `--available`
|
|
444
|
+
* enumerates every remote marketplace Codex knows — 3712 entries on the
|
|
445
|
+
* machine this was measured on. Narrowed it is nine, offline, in 0.07s.
|
|
446
|
+
* - **`--available`** is what separates *the marketplace is configured and
|
|
447
|
+
* pharos is not installed from it* from *there is no floh-skills
|
|
448
|
+
* marketplace here at all*. Both are fixed by the same command, but they
|
|
449
|
+
* are different facts and a reader deserves the true one.
|
|
450
|
+
* - **`--json`** because the table is a table. An unknown marketplace name is
|
|
451
|
+
* two empty arrays and exit 0, so absence never arrives as an error.
|
|
452
|
+
*
|
|
453
|
+
* The table is still read as a fallback: a Codex old enough to have `codex
|
|
454
|
+
* plugin` and not `--json` would otherwise be reported as "not installed",
|
|
455
|
+
* which is the one answer that is worse than "could not tell".
|
|
456
|
+
*/
|
|
457
|
+
async function codexSkillCheck(env) {
|
|
458
|
+
const base = skillBase("skill-codex", "Codex");
|
|
459
|
+
const state = await codexPluginState(env);
|
|
460
|
+
if (state.kind === "installed") {
|
|
461
|
+
return {
|
|
462
|
+
...base,
|
|
463
|
+
ok: true,
|
|
464
|
+
version: state.version,
|
|
465
|
+
path: null,
|
|
466
|
+
note: "Publishing a new skill does NOT update an installed one, and for Codex it takes two "
|
|
467
|
+
+ `commands rather than one: \`codex plugin marketplace upgrade ${MARKETPLACE_NAME}\` `
|
|
468
|
+
+ `refreshes the snapshot, then \`codex plugin add ${PLUGIN}\` installs from it. `
|
|
469
|
+
+ "Codex reads the same Claude-format marketplace manifest we publish for Claude Code.",
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
if (state.kind === "not-installed") {
|
|
473
|
+
return {
|
|
474
|
+
...base,
|
|
475
|
+
ok: false,
|
|
476
|
+
version: null,
|
|
477
|
+
path: null,
|
|
478
|
+
note: `Codex has the ${MARKETPLACE_NAME} marketplace configured and has not installed `
|
|
479
|
+
+ `\`${PLUGIN}\` from it`
|
|
480
|
+
+ `${state.offered === null ? "" : ` — the snapshot offers ${state.offered}`}. `
|
|
481
|
+
+ "`pharos setup --install skill` installs it.",
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
if (state.kind === "no-marketplace") {
|
|
485
|
+
return {
|
|
486
|
+
...base,
|
|
487
|
+
ok: false,
|
|
488
|
+
version: null,
|
|
489
|
+
path: null,
|
|
490
|
+
note: `Codex has no ${MARKETPLACE_NAME} marketplace configured, so there is nothing to `
|
|
491
|
+
+ "install the skill from yet. `pharos setup --install skill` adds it and installs.",
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
...base,
|
|
496
|
+
ok: false,
|
|
497
|
+
version: null,
|
|
498
|
+
path: null,
|
|
499
|
+
// No install command: we do not know that `pharos setup --install skill`
|
|
500
|
+
// can work here, and a button that fails is worse than no button. The
|
|
501
|
+
// likely fix is named in prose instead.
|
|
502
|
+
install: null,
|
|
503
|
+
note: `Codex is here but could not be asked: ${state.detail}. So this is NOT known to be `
|
|
504
|
+
+ "missing. A Codex predating `codex plugin` is the likely cause; `npm i -g @openai/codex` "
|
|
505
|
+
+ "updates it.",
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
async function codexPluginState(env) {
|
|
509
|
+
const args = ["plugin", "list", "--json", "--available", "--marketplace", MARKETPLACE_NAME];
|
|
510
|
+
let jsonFailure;
|
|
511
|
+
try {
|
|
512
|
+
const { stdout } = await run("codex", args, {
|
|
513
|
+
timeout: 20_000,
|
|
514
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
515
|
+
env,
|
|
516
|
+
});
|
|
517
|
+
const parsed = JSON.parse(stdout);
|
|
518
|
+
const ours = (list) => (list ?? []).find((entry) => entry.pluginId === PLUGIN);
|
|
519
|
+
const installed = ours(parsed.installed);
|
|
520
|
+
if (installed !== undefined) {
|
|
521
|
+
return { kind: "installed", version: asVersion(installed.version) };
|
|
522
|
+
}
|
|
523
|
+
const available = ours(parsed.available);
|
|
524
|
+
if (available !== undefined) {
|
|
525
|
+
return { kind: "not-installed", offered: asVersion(available.version) };
|
|
526
|
+
}
|
|
527
|
+
return { kind: "no-marketplace" };
|
|
528
|
+
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
jsonFailure = messageOf(error);
|
|
531
|
+
}
|
|
532
|
+
// The table. Same question, worse grammar: one row per plugin, the status
|
|
533
|
+
// column reading `installed, enabled` or `not installed`.
|
|
534
|
+
try {
|
|
535
|
+
const { stdout } = await run("codex", ["plugin", "list", "--marketplace", MARKETPLACE_NAME], {
|
|
536
|
+
timeout: 20_000,
|
|
537
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
538
|
+
env,
|
|
539
|
+
});
|
|
540
|
+
const line = stdout.split("\n").find((row) => row.trimStart().startsWith(PLUGIN));
|
|
541
|
+
if (line === undefined)
|
|
542
|
+
return { kind: "no-marketplace" };
|
|
543
|
+
// Read only what follows the id, so a marketplace or path containing the
|
|
544
|
+
// word cannot be mistaken for the status.
|
|
545
|
+
const rest = line.slice(line.indexOf(PLUGIN) + PLUGIN.length);
|
|
546
|
+
if (/\bnot installed\b/.test(rest))
|
|
547
|
+
return { kind: "not-installed", offered: null };
|
|
548
|
+
if (/\binstalled\b/.test(rest)) {
|
|
549
|
+
return { kind: "installed", version: /(\d+\.\d+\.\d+[\w.+-]*)/.exec(rest)?.[1] ?? null };
|
|
550
|
+
}
|
|
551
|
+
return { kind: "unasked", detail: `\`codex plugin list\` printed a row this cannot read` };
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
return { kind: "unasked", detail: `${jsonFailure}; and without --json: ${messageOf(error)}` };
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function asVersion(value) {
|
|
558
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Two agents, one skill, two different versions — say so on both rows.
|
|
562
|
+
*
|
|
563
|
+
* This is the comparison that is actually available here (see
|
|
564
|
+
* {@link skillChecks}), and it is the origin bug wearing the other hat: the
|
|
565
|
+
* operator's machine ran Claude Code on 1.27.0 while Codex, installed by hand
|
|
566
|
+
* minutes earlier, was on 1.28.0. Nothing anywhere reported it, and each row on
|
|
567
|
+
* its own looks entirely healthy.
|
|
568
|
+
*
|
|
569
|
+
* Exported because it is the assertable half: arranging two agents at two
|
|
570
|
+
* versions on a real machine is not something a test may do.
|
|
571
|
+
*/
|
|
572
|
+
export function markSkillDrift(rows) {
|
|
573
|
+
const installed = rows.filter((row) => row.ok && row.version !== null);
|
|
574
|
+
if (installed.length < 2)
|
|
575
|
+
return;
|
|
576
|
+
const versions = new Set(installed.map((row) => row.version));
|
|
577
|
+
if (versions.size < 2)
|
|
578
|
+
return;
|
|
579
|
+
const inventory = installed.map((row) => `${row.name} has ${row.version}`).join(", ");
|
|
580
|
+
for (const row of installed) {
|
|
581
|
+
row.note = `${row.note ?? ""} One skill, two versions on this machine: ${inventory}. `
|
|
582
|
+
+ "Update the older one — the two agents install from the same marketplace and there is "
|
|
583
|
+
+ "no reason for them to disagree.";
|
|
584
|
+
}
|
|
585
|
+
}
|
|
379
586
|
/** Configuration, read from the environment — no network unless asked. */
|
|
380
587
|
function configCheck(env) {
|
|
381
588
|
const missing = ["ADO_ORG", "ADO_PROJECT", "ADO_PAT"].filter((name) => (env[name] ?? "") === "");
|
|
@@ -995,13 +1202,34 @@ export async function repoAccessCheck(client, bindings, configPath) {
|
|
|
995
1202
|
* not `osascript`). Extensions from the flavour's `extensions.json`, then its
|
|
996
1203
|
* folder names. The bridge is compared to the one this CLI ships, read from
|
|
997
1204
|
* `vscode/pharos-bridge.json` beside the bundled `.vsix`.
|
|
1205
|
+
*
|
|
1206
|
+
* ## …and which login a RUNNING host would start a session as (#1431)
|
|
1207
|
+
*
|
|
1208
|
+
* A VS Code started from a shell that had `CLAUDE_CONFIG_DIR` set passes it
|
|
1209
|
+
* to every terminal it opens, so a Claude session started there runs as that
|
|
1210
|
+
* login — an Argus agent account, on the machine it was measured on. Nothing
|
|
1211
|
+
* is wrong and nothing is refused; the row's note says so, with the fix,
|
|
1212
|
+
* because the alternative is finding out from a session that answers as the
|
|
1213
|
+
* wrong account. Terminal is asked the same question and cannot answer it on
|
|
1214
|
+
* macOS 26 — a platform binary's environment is not shown to `ps -E` — so its
|
|
1215
|
+
* row says nothing rather than something unverified.
|
|
998
1216
|
*/
|
|
999
1217
|
async function delegationChecks(env) {
|
|
1000
|
-
const hosts = await Promise.all([
|
|
1001
|
-
id
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1218
|
+
const [hosts, carried] = await Promise.all([
|
|
1219
|
+
Promise.all(["terminal", "vscode", "vscode-insiders", "navarch"].map(async (id) => ({
|
|
1220
|
+
id,
|
|
1221
|
+
app: await findApp(id, env),
|
|
1222
|
+
}))),
|
|
1223
|
+
// `ps -E` is the BSD spelling, and every host here is a macOS app anyway.
|
|
1224
|
+
// A failed read is no note rather than a wrong one.
|
|
1225
|
+
platform() === "darwin" ? carriedProfiles(systemProbes(env, null)).catch(() => []) : Promise.resolve([]),
|
|
1226
|
+
]);
|
|
1227
|
+
const home = env["HOME"] ?? homedir();
|
|
1228
|
+
const checks = hosts.map(({ id, app }) => {
|
|
1229
|
+
const check = hostCheck(id, app);
|
|
1230
|
+
const note = hostCarriedNote(carried.find((entry) => entry.host === id) ?? null, home);
|
|
1231
|
+
return note === "" ? check : { ...check, note: `${check.note ?? ""}${note}` };
|
|
1232
|
+
});
|
|
1005
1233
|
const navarch = hosts.find(({ id }) => id === "navarch");
|
|
1006
1234
|
if (navarch?.app != null)
|
|
1007
1235
|
checks.push(await argusDelegateCheck(env));
|
|
@@ -1038,6 +1266,17 @@ async function delegationChecks(env) {
|
|
|
1038
1266
|
*
|
|
1039
1267
|
* `install` is null on purpose: the fix is a reload, and a reload ends every
|
|
1040
1268
|
* live worker session. That is a person's to time, never a button.
|
|
1269
|
+
*
|
|
1270
|
+
* **A daemon that serves the verb is then asked a second question**, because
|
|
1271
|
+
* there are two generations of "current" and they behave differently (#1412).
|
|
1272
|
+
* argus #1402 added a target worker, a GitHub account and the
|
|
1273
|
+
* `delegate_candidates` read in one change, so one probe answers for all
|
|
1274
|
+
* three — and the gap matters more than a version gap usually does: `delegate`
|
|
1275
|
+
* IGNORES a key it does not know, so on the older daemon `--session` would be
|
|
1276
|
+
* discarded and the prompt typed into whichever worker the find liked. The CLI
|
|
1277
|
+
* refuses rather than let that happen, and this row is where somebody finds
|
|
1278
|
+
* out before they hit it. It stays `ok: true`: the verb works, and a red row
|
|
1279
|
+
* for a feature the machine has never used is a red row nobody reads.
|
|
1041
1280
|
*/
|
|
1042
1281
|
async function argusDelegateCheck(env) {
|
|
1043
1282
|
const socketPath = argusSocketPath(env);
|
|
@@ -1070,7 +1309,8 @@ async function argusDelegateCheck(env) {
|
|
|
1070
1309
|
return {
|
|
1071
1310
|
...base,
|
|
1072
1311
|
ok: true,
|
|
1073
|
-
note: `argusd is running at ${socketPath} and serves the delegate verb
|
|
1312
|
+
note: `argusd is running at ${socketPath} and serves the delegate verb.`
|
|
1313
|
+
+ ` ${await targetSupportNote(systemArgusDeps(env))}`,
|
|
1074
1314
|
};
|
|
1075
1315
|
}
|
|
1076
1316
|
return {
|
|
@@ -1083,6 +1323,54 @@ async function argusDelegateCheck(env) {
|
|
|
1083
1323
|
+ " forwarding a verb it does not itself know.",
|
|
1084
1324
|
};
|
|
1085
1325
|
}
|
|
1326
|
+
/**
|
|
1327
|
+
* Does this daemon read a target worker and a GitHub account? — one sentence
|
|
1328
|
+
* for the `argus-delegate` row.
|
|
1329
|
+
*
|
|
1330
|
+
* The probe is `delegate_candidates` with **no `directory`**, which is the
|
|
1331
|
+
* cheapest form of the question: a daemon that serves the op refuses
|
|
1332
|
+
* `bad-directory` (it validates exactly as `delegate` does), and one that does
|
|
1333
|
+
* not answers `unknown-op`. Either way nothing is read, nothing is written,
|
|
1334
|
+
* and no folder has to exist for the question to be askable — which matters,
|
|
1335
|
+
* because `doctor` is asked on machines with no project linked at all.
|
|
1336
|
+
*
|
|
1337
|
+
* A socket that breaks between the two reads says so rather than guessing: the
|
|
1338
|
+
* row above it has already reported the daemon is up, so silence here would
|
|
1339
|
+
* read as an answer.
|
|
1340
|
+
*/
|
|
1341
|
+
export async function targetSupportNote(argus) {
|
|
1342
|
+
let reply;
|
|
1343
|
+
try {
|
|
1344
|
+
reply = await askArgus(argus, delegateCandidatesRequest("", "claude"), readTimeoutMs(0));
|
|
1345
|
+
}
|
|
1346
|
+
catch (error) {
|
|
1347
|
+
return "Whether it also serves `delegate_candidates` and `gh_account` could not be read: "
|
|
1348
|
+
+ `${error instanceof Error ? error.message : String(error)}.`;
|
|
1349
|
+
}
|
|
1350
|
+
return candidatesSupport(reply) === "no-op"
|
|
1351
|
+
? "It PREDATES `delegate_candidates` and `gh_account` (argus #1402), so `--list --host navarch`"
|
|
1352
|
+
+ " falls back to reading `fleet_status` itself, and `--session` and `--gh-account` are refused"
|
|
1353
|
+
+ " `argusd-outdated` there — this daemon would ignore both rather than refuse them, and type"
|
|
1354
|
+
+ " the prompt into whichever worker its own find picked. Reloading argusd lifts that, and"
|
|
1355
|
+
+ " ends every live worker session."
|
|
1356
|
+
: "It also serves `delegate_candidates` and `gh_account` (argus #1402), so `--list --host navarch`"
|
|
1357
|
+
+ " is the daemon's own answer to its own find, and `--session` and `--gh-account` are honoured.";
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* What a host row adds when the running host carries a non-default
|
|
1361
|
+
* `CLAUDE_CONFIG_DIR`, with its leading space — or nothing.
|
|
1362
|
+
*
|
|
1363
|
+
* The same sentence `pharos delegate` puts in its detail, from the same
|
|
1364
|
+
* function, plus the one thing only a machine-wide report can add: that the
|
|
1365
|
+
* sessions ALREADY running there are found and named, so the only surprise
|
|
1366
|
+
* left is the next one it starts.
|
|
1367
|
+
*/
|
|
1368
|
+
export function hostCarriedNote(carried, home) {
|
|
1369
|
+
if (carried === null)
|
|
1370
|
+
return "";
|
|
1371
|
+
return (` Running now as pid ${carried.pid}. ${carriedSentence(carried, home)} Sessions already running there are`
|
|
1372
|
+
+ ` still listed by \`pharos delegate --list\`, marked \`profile ${carried.profile}\`.`);
|
|
1373
|
+
}
|
|
1086
1374
|
function hostCheck(id, app) {
|
|
1087
1375
|
const spec = HOSTS[id];
|
|
1088
1376
|
const unlocks = {
|
|
@@ -1142,22 +1430,35 @@ async function extensionChecks(host, env, shippedBridge) {
|
|
|
1142
1430
|
* the one this pharos ships stays `ok:true` with a note, because `ok:false`
|
|
1143
1431
|
* should mean "something you asked for does not work" rather than "there is a
|
|
1144
1432
|
* newer one" — otherwise every future bump nags on every machine.
|
|
1433
|
+
*
|
|
1434
|
+
* **This row reports; it no longer has to be acted on to be fixed.** Since
|
|
1435
|
+
* #1394 `pharos delegate --host <flavour>` installs or upgrades the bridge on
|
|
1436
|
+
* its way to opening a URI, so an absent or older one is put right by the
|
|
1437
|
+
* first delegation. `pharos setup --install vscode-bridge` stays the way to
|
|
1438
|
+
* do it ahead of time — and the note says so, because a row that names no fix
|
|
1439
|
+
* reads as a row nobody can act on.
|
|
1145
1440
|
*/
|
|
1146
1441
|
const bridgeStale = bridge !== undefined && !bridgeAtLeast(bridge.version, SESSION_BRIDGE_VERSION);
|
|
1442
|
+
/** What now fixes an absent or outdated bridge, in both the words a person needs. */
|
|
1443
|
+
const onTheWay = `\`pharos delegate --host ${host}\` installs it on its way to the editor, and `
|
|
1444
|
+
+ "`pharos setup --install vscode-bridge` does it ahead of time.";
|
|
1147
1445
|
const bridgeNote = bridge === undefined
|
|
1148
1446
|
? shippedBridge === null
|
|
1149
1447
|
? `${where} This pharos ships no bridge either (${bundledVsixPath(env)} is absent) — the ship todo packs it; until then \`--install vscode-bridge\` refuses by name.`
|
|
1150
|
-
: `${where} This pharos ships ${shippedBridge}
|
|
1448
|
+
: `${where} This pharos ships ${shippedBridge}. ${onTheWay}`
|
|
1151
1449
|
: bridgeStale
|
|
1152
1450
|
? `${where} Installed ${bridge.version}, which predates \`pharos delegate --session\` (${SESSION_BRIDGE_VERSION}). `
|
|
1153
1451
|
+ "It does not ignore the session — it refuses the whole URI, because it rejects any query key it does "
|
|
1154
1452
|
+ `not know — so picking a session in ${flavour} sends nothing. Delegating without --session still works. `
|
|
1155
|
-
+
|
|
1453
|
+
+ (shippedBridge === null || !bridgeAtLeast(shippedBridge, SESSION_BRIDGE_VERSION)
|
|
1454
|
+
? `\`pharos setup --install vscode-bridge\` replaces it${shippedBridge === null ? "" : ` with ${shippedBridge}`}.`
|
|
1455
|
+
: `${onTheWay} A ${flavour} window that was already open keeps ${bridge.version} until it reloads, `
|
|
1456
|
+
+ "so the first --session delegation after the replacement is refused once and says so.")
|
|
1156
1457
|
: shippedBridge === null
|
|
1157
1458
|
? `${where} Installed ${bridge.version}; this pharos ships no bridge to compare it with.`
|
|
1158
1459
|
: bridge.version === shippedBridge
|
|
1159
1460
|
? `${where} Installed ${bridge.version}, which is the one this pharos ships.`
|
|
1160
|
-
: `${where} Installed ${bridge.version}, but this pharos ships ${shippedBridge}. It serves every verb this pharos has; the URI contract may still have moved.
|
|
1461
|
+
: `${where} Installed ${bridge.version}, but this pharos ships ${shippedBridge}. It serves every verb this pharos has; the URI contract may still have moved. ${onTheWay}`;
|
|
1161
1462
|
return [
|
|
1162
1463
|
{
|
|
1163
1464
|
id: `${host}-claude-ext`,
|
|
@@ -1188,7 +1489,7 @@ async function extensionChecks(host, env, shippedBridge) {
|
|
|
1188
1489
|
name: `Pharos bridge in ${flavour}`,
|
|
1189
1490
|
tier: 2,
|
|
1190
1491
|
unlocks: `\`pharos delegate --host ${host}\` at all: the bridge is what puts the prompt into a session inside ${flavour}. Nothing else can. From ${SESSION_BRIDGE_VERSION} it also honours \`--session\`, which is how a person picks between two sessions on one folder.`,
|
|
1191
|
-
without: `Delegating to ${flavour} is refused by name (bridge-not-installed)
|
|
1492
|
+
without: `Delegating to ${flavour} installs the bridge this pharos ships first, and is refused by name (bridge-not-installed) only when that install fails or there is none to install. \`--session\` against a bridge older than ${SESSION_BRIDGE_VERSION} is refused ONCE even after it is replaced — a window already open keeps the old one, and the old one refuses the whole URI rather than ignoring the session, which would otherwise report a prompt as sent.`,
|
|
1192
1493
|
ok: bridge !== undefined && !bridgeStale,
|
|
1193
1494
|
version: bridge?.version ?? null,
|
|
1194
1495
|
path: bridge?.path ?? null,
|