@crouton-kit/crouter 0.3.17 → 0.3.19
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/dist/build-root.js +6 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
- package/dist/builtin-memory/crouter-development/personas.md +117 -0
- package/dist/builtin-memory/crouter-development/plugins.md +163 -0
- package/dist/builtin-memory/design.md +60 -0
- package/dist/builtin-memory/development.md +116 -0
- package/dist/builtin-memory/planning.md +66 -0
- package/dist/builtin-memory/spec.md +99 -0
- package/dist/builtin-personas/design/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/design/orchestrator.md +1 -1
- package/dist/builtin-personas/developer/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/explore/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/general/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/lifecycle/resident.md +1 -1
- package/dist/builtin-personas/lifecycle/terminal.md +5 -2
- package/dist/builtin-personas/orchestration-kernel.md +15 -24
- package/dist/builtin-personas/plan/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/plan/reviewers/architecture-fit/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/code-smells/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/security/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/review/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/spec/orchestrator.md +1 -1
- package/dist/builtin-personas/waiting.md +8 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +24 -14
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +4 -4
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
- package/dist/builtin-views/_lib/states.mjs +161 -0
- package/dist/builtin-views/canvas/client.mjs +303 -0
- package/dist/builtin-views/canvas/view.mjs +576 -0
- package/dist/builtin-views/git-pr/client.mjs +440 -0
- package/dist/builtin-views/git-pr/view.mjs +675 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
- package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
- package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
- package/dist/builtin-views/inbox/view.mjs +889 -0
- package/dist/builtin-views/linkedin/client.mjs +610 -0
- package/dist/builtin-views/linkedin/view.mjs +1171 -0
- package/dist/clients/attach/attach-cmd.d.ts +2 -0
- package/dist/clients/attach/attach-cmd.js +354 -0
- package/dist/clients/attach/chat-view.d.ts +77 -0
- package/dist/clients/attach/chat-view.js +450 -0
- package/dist/clients/attach/clipboard-image.d.ts +16 -0
- package/dist/clients/attach/clipboard-image.js +113 -0
- package/dist/clients/attach/config-load.d.ts +31 -0
- package/dist/clients/attach/config-load.js +113 -0
- package/dist/clients/attach/extension-dialogs.d.ts +29 -0
- package/dist/clients/attach/extension-dialogs.js +101 -0
- package/dist/clients/attach/input-controller.d.ts +54 -0
- package/dist/clients/attach/input-controller.js +204 -0
- package/dist/clients/attach/slash-commands.d.ts +36 -0
- package/dist/clients/attach/slash-commands.js +200 -0
- package/dist/clients/attach/view-socket.d.ts +48 -0
- package/dist/clients/attach/view-socket.js +126 -0
- package/dist/commands/attention.js +3 -3
- package/dist/commands/canvas-prune.js +1 -1
- package/dist/commands/daemon.js +4 -3
- package/dist/commands/human/prompts.js +4 -10
- package/dist/commands/human/queue.js +43 -8
- package/dist/commands/human/shared.d.ts +28 -1
- package/dist/commands/human/shared.js +48 -10
- package/dist/commands/memory/find.d.ts +1 -0
- package/dist/commands/memory/find.js +180 -0
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +140 -0
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +79 -0
- package/dist/commands/memory/read.js +103 -0
- package/dist/commands/memory/shared.d.ts +30 -0
- package/dist/commands/memory/shared.js +122 -0
- package/dist/commands/memory/write.d.ts +1 -0
- package/dist/commands/memory/write.js +85 -0
- package/dist/commands/memory.d.ts +2 -0
- package/dist/commands/memory.js +27 -0
- package/dist/commands/node.d.ts +3 -2
- package/dist/commands/node.js +660 -65
- package/dist/commands/pkg/market-manage.js +1 -1
- package/dist/commands/push.js +6 -6
- package/dist/commands/revive.js +1 -1
- package/dist/commands/skill/author.js +1 -1
- package/dist/commands/skill/shared.d.ts +1 -8
- package/dist/commands/skill/shared.js +2 -55
- package/dist/commands/skill.js +9 -14
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/update.js +1 -1
- package/dist/commands/view-cycle.d.ts +2 -0
- package/dist/commands/view-cycle.js +125 -0
- package/dist/commands/view-list.d.ts +2 -0
- package/dist/commands/view-list.js +66 -0
- package/dist/commands/view-new.d.ts +2 -0
- package/dist/commands/view-new.js +70 -0
- package/dist/commands/view-pick.d.ts +2 -0
- package/dist/commands/view-pick.js +119 -0
- package/dist/commands/view-run.d.ts +2 -0
- package/dist/commands/view-run.js +191 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +29 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
- package/dist/core/__tests__/cascade-close.test.js +12 -2
- package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
- package/dist/core/__tests__/child-death-wake.test.js +245 -0
- package/dist/core/__tests__/context-intro.test.js +76 -62
- package/dist/core/__tests__/daemon-boot.test.js +14 -5
- package/dist/core/__tests__/daemon-liveness.test.js +34 -9
- package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
- package/dist/core/__tests__/detach-focus.test.js +206 -0
- package/dist/core/__tests__/draw-style.test.d.ts +1 -0
- package/dist/core/__tests__/draw-style.test.js +258 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
- package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
- package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
- package/dist/core/__tests__/helpers/harness.d.ts +9 -0
- package/dist/core/__tests__/helpers/harness.js +111 -1
- package/dist/core/__tests__/home-session.test.js +41 -8
- package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
- package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
- package/dist/core/__tests__/human-surface-target.test.d.ts +1 -0
- package/dist/core/__tests__/human-surface-target.test.js +98 -0
- package/dist/core/__tests__/kickoff.test.js +37 -3
- package/dist/core/__tests__/live-mutation.test.js +50 -33
- package/dist/core/__tests__/memory.test.js +23 -115
- package/dist/core/__tests__/persona-subkind.test.js +18 -15
- package/dist/core/__tests__/placement-focus.test.js +5 -0
- package/dist/core/__tests__/placement-teardown.test.js +54 -11
- package/dist/core/__tests__/relaunch.test.js +4 -3
- package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
- package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
- package/dist/core/__tests__/spawn-root.test.js +10 -0
- package/dist/core/__tests__/spike-harness.test.js +1 -0
- package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
- package/dist/core/__tests__/wake-bearings.test.js +156 -0
- package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
- package/dist/core/__tests__/wake-origin.test.js +110 -0
- package/dist/core/bootstrap.js +1 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +24 -2
- package/dist/core/canvas/browse/model.d.ts +38 -2
- package/dist/core/canvas/browse/model.js +134 -10
- package/dist/core/canvas/browse/render.d.ts +6 -12
- package/dist/core/canvas/browse/render.js +72 -104
- package/dist/core/canvas/canvas.js +15 -22
- package/dist/core/canvas/db.js +46 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/paths.d.ts +4 -1
- package/dist/core/canvas/paths.js +10 -4
- package/dist/core/canvas/pid.d.ts +4 -0
- package/dist/core/canvas/pid.js +23 -0
- package/dist/core/canvas/render.d.ts +11 -2
- package/dist/core/canvas/render.js +69 -0
- package/dist/core/canvas/types.d.ts +85 -3
- package/dist/core/canvas/types.js +2 -2
- package/dist/core/canvas/wakeups.d.ts +76 -0
- package/dist/core/canvas/wakeups.js +185 -0
- package/dist/core/config.js +4 -1
- package/dist/core/frontmatter.js +37 -124
- package/dist/core/help.d.ts +6 -0
- package/dist/core/help.js +7 -0
- package/dist/core/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/index.d.ts +4 -3
- package/dist/core/personas/index.js +3 -2
- package/dist/core/personas/loader.d.ts +41 -16
- package/dist/core/personas/loader.js +133 -29
- package/dist/core/personas/resolve.d.ts +4 -4
- package/dist/core/personas/resolve.js +28 -16
- package/dist/core/predicate.d.ts +63 -0
- package/dist/core/predicate.js +189 -0
- package/dist/core/resolver.js +26 -5
- package/dist/core/runtime/bearings.d.ts +53 -12
- package/dist/core/runtime/bearings.js +132 -59
- package/dist/core/runtime/broker-cli.d.ts +1 -0
- package/dist/core/runtime/broker-cli.js +46 -0
- package/dist/core/runtime/broker-protocol.d.ts +332 -0
- package/dist/core/runtime/broker-protocol.js +153 -0
- package/dist/core/runtime/broker-sdk.d.ts +48 -0
- package/dist/core/runtime/broker-sdk.js +72 -0
- package/dist/core/runtime/broker.d.ts +55 -0
- package/dist/core/runtime/broker.js +1128 -0
- package/dist/core/runtime/close.js +35 -6
- package/dist/core/runtime/host.d.ts +53 -0
- package/dist/core/runtime/host.js +186 -0
- package/dist/core/runtime/kickoff.d.ts +2 -1
- package/dist/core/runtime/kickoff.js +91 -5
- package/dist/core/runtime/launch.d.ts +45 -2
- package/dist/core/runtime/launch.js +65 -2
- package/dist/core/runtime/lifecycle.js +23 -6
- package/dist/core/runtime/memory.d.ts +2 -42
- package/dist/core/runtime/memory.js +11 -162
- package/dist/core/runtime/nodes.d.ts +33 -0
- package/dist/core/runtime/nodes.js +59 -1
- package/dist/core/runtime/persona.js +21 -11
- package/dist/core/runtime/pi-vendored.d.ts +18 -0
- package/dist/core/runtime/pi-vendored.js +49 -0
- package/dist/core/runtime/placement.d.ts +42 -14
- package/dist/core/runtime/placement.js +228 -38
- package/dist/core/runtime/promote.d.ts +0 -6
- package/dist/core/runtime/promote.js +1 -12
- package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
- package/dist/core/runtime/{demote.js → recycle.js} +27 -11
- package/dist/core/runtime/reset.js +8 -6
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +19 -0
- package/dist/core/runtime/spawn.js +75 -22
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +28 -0
- package/dist/core/runtime/tmux.js +80 -6
- package/dist/core/scope.d.ts +11 -0
- package/dist/core/scope.js +39 -0
- package/dist/core/spawn.d.ts +25 -1
- package/dist/core/spawn.js +72 -7
- package/dist/core/substrate/gate.d.ts +13 -0
- package/dist/core/substrate/gate.js +21 -0
- package/dist/core/substrate/index.d.ts +7 -0
- package/dist/core/substrate/index.js +18 -0
- package/dist/core/substrate/on-read.d.ts +14 -0
- package/dist/core/substrate/on-read.js +292 -0
- package/dist/core/substrate/render.d.ts +25 -0
- package/dist/core/substrate/render.js +256 -0
- package/dist/core/substrate/schema.d.ts +76 -0
- package/dist/core/substrate/schema.js +124 -0
- package/dist/core/substrate/session-cache.d.ts +30 -0
- package/dist/core/substrate/session-cache.js +77 -0
- package/dist/core/substrate/subject.d.ts +41 -0
- package/dist/core/substrate/subject.js +54 -0
- package/dist/core/tui/contract.d.ts +83 -0
- package/dist/core/tui/contract.js +8 -0
- package/dist/core/tui/draw.d.ts +96 -0
- package/dist/core/tui/draw.js +339 -0
- package/dist/core/tui/host.d.ts +29 -0
- package/dist/core/tui/host.js +379 -0
- package/dist/core/tui/loader.d.ts +16 -0
- package/dist/core/tui/loader.js +94 -0
- package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
- package/dist/core/wake.d.ts +86 -0
- package/dist/core/wake.js +308 -0
- package/dist/daemon/crtrd.d.ts +29 -4
- package/dist/daemon/crtrd.js +662 -46
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
- package/dist/pi-extensions/canvas-commands.d.ts +3 -0
- package/dist/pi-extensions/canvas-commands.js +10 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
- package/dist/pi-extensions/canvas-context-intro.js +55 -15
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
- package/dist/pi-extensions/canvas-nav.d.ts +3 -0
- package/dist/pi-extensions/canvas-nav.js +145 -34
- package/dist/pi-extensions/canvas-stophook.js +17 -8
- package/dist/pi-extensions/canvas-view.d.ts +21 -0
- package/dist/pi-extensions/canvas-view.js +75 -0
- package/dist/prompts/skill.js +19 -26
- package/dist/prompts/view.d.ts +7 -0
- package/dist/prompts/view.js +101 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/package.json +9 -4
- package/dist/commands/skill/find.d.ts +0 -4
- package/dist/commands/skill/find.js +0 -257
- package/dist/commands/skill/read.js +0 -91
- /package/dist/commands/{skill → memory}/read.d.ts +0 -0
- /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Git / PR board data layer for the crtr `git-pr` view (a monitor archetype).
|
|
4
|
+
*
|
|
5
|
+
* Self-contained ESM, Node-builtins-only. Imports NOTHING from crtr so it ships
|
|
6
|
+
* verbatim (`cp -R src/builtin-views dist/builtin-views`) and is dynamically
|
|
7
|
+
* `import()`ed by the view at runtime where there is no TS toolchain.
|
|
8
|
+
*
|
|
9
|
+
* It shells `git` (local state) and `gh` (GitHub PR/CI state) over the view's
|
|
10
|
+
* cwd — exactly as the canvas view shells `crtr` and the LinkedIn view shells
|
|
11
|
+
* `capture`. Two independent domains:
|
|
12
|
+
* - `fetchGit()` → branch / upstream ahead·behind / working-tree status +
|
|
13
|
+
* churn / last commit. The PRIMARY instrument.
|
|
14
|
+
* - `fetchPrs(branch)` → open PRs for this repo (review decision + CI rollup),
|
|
15
|
+
* the current branch's PR first. BEST-EFFORT — its failure is
|
|
16
|
+
* a guided note in the PR section, never a crash; the git
|
|
17
|
+
* section still renders (graceful partial failure).
|
|
18
|
+
*
|
|
19
|
+
* NOTHING here throws. Every exported function returns a `Result<T>`; failures
|
|
20
|
+
* surface as a typed `ClientError` so the view renders guidance, not a crash.
|
|
21
|
+
* The error taxonomy (each → a view state):
|
|
22
|
+
* git: git-missing · not-a-repo · git-failed (empty-repo / no-upstream are
|
|
23
|
+
* NOT errors — they degrade to null fields the header renders).
|
|
24
|
+
* gh: gh-missing · gh-unauthed · gh-no-remote · gh-network · gh-failed.
|
|
25
|
+
*
|
|
26
|
+
* @module git-pr/client
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { execFile } from 'node:child_process';
|
|
30
|
+
|
|
31
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One changed file, flattened from `git status --porcelain=v1 -z`.
|
|
35
|
+
* @typedef {Object} ChangedFile
|
|
36
|
+
* @property {string} path Display path (destination on a rename).
|
|
37
|
+
* @property {string} xy The two-char porcelain code (e.g. "M ", " M", "??", "UU").
|
|
38
|
+
* @property {'staged'|'modified'|'untracked'|'conflict'} cls Primary class (drives glyph + hue).
|
|
39
|
+
* @property {number} add Lines added (numstat; 0 if unknown/binary/untracked).
|
|
40
|
+
* @property {number} del Lines removed.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Local git state. `lastCommit` / `upstream` are null on an empty repo / a
|
|
45
|
+
* branch with no upstream — the header renders the degraded form, not an error.
|
|
46
|
+
* @typedef {Object} GitState
|
|
47
|
+
* @property {string} branch Branch name, or "(detached <sha>)" / "(no branch)".
|
|
48
|
+
* @property {boolean} detached True ⇒ detached HEAD.
|
|
49
|
+
* @property {string|null} upstream Tracking ref (e.g. "origin/main"), or null.
|
|
50
|
+
* @property {number} ahead Commits ahead of upstream.
|
|
51
|
+
* @property {number} behind Commits behind upstream.
|
|
52
|
+
* @property {{sha:string, subject:string, when:string}|null} lastCommit
|
|
53
|
+
* @property {ChangedFile[]} files Changed files, problems-first then path.
|
|
54
|
+
* @property {{staged:number, modified:number, untracked:number, conflict:number}} counts
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* One open pull request, flattened from `gh pr list --json …`.
|
|
59
|
+
* @typedef {Object} Pr
|
|
60
|
+
* @property {number} number
|
|
61
|
+
* @property {string} title
|
|
62
|
+
* @property {string} headRefName
|
|
63
|
+
* @property {boolean} isDraft
|
|
64
|
+
* @property {boolean} current True ⇒ this PR's head is the checked-out branch.
|
|
65
|
+
* @property {'approved'|'changes'|'review'} review Normalized reviewDecision.
|
|
66
|
+
* @property {'pass'|'fail'|'pending'|'none'} ci Rolled-up statusCheckRollup.
|
|
67
|
+
* @property {string} updatedAt ISO 8601 (drives the right-flush age).
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Typed failure. `kind` drives the view state / guidance.
|
|
72
|
+
* @typedef {{kind:'git-missing', message:string}
|
|
73
|
+
* | {kind:'not-a-repo', message:string}
|
|
74
|
+
* | {kind:'git-failed', message:string}} GitError
|
|
75
|
+
*/
|
|
76
|
+
/**
|
|
77
|
+
* @typedef {{kind:'gh-missing', message:string}
|
|
78
|
+
* | {kind:'gh-unauthed', message:string}
|
|
79
|
+
* | {kind:'gh-no-remote', message:string}
|
|
80
|
+
* | {kind:'gh-network', message:string}
|
|
81
|
+
* | {kind:'gh-failed', message:string}} GhError
|
|
82
|
+
*/
|
|
83
|
+
/** @typedef {GitError|GhError} ClientError */
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Never-throw return contract.
|
|
87
|
+
* @template T
|
|
88
|
+
* @typedef {{ok:true, data:T} | {ok:false, error:ClientError}} Result
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
// ── Result helpers ───────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/** @template T @param {T} data @returns {{ok:true, data:T}} */
|
|
94
|
+
function ok(data) {
|
|
95
|
+
return { ok: true, data };
|
|
96
|
+
}
|
|
97
|
+
/** @param {ClientError} error @returns {{ok:false, error:ClientError}} */
|
|
98
|
+
function fail(error) {
|
|
99
|
+
return { ok: false, error };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Process runner (never throws) ────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @typedef {Object} RunResult
|
|
106
|
+
* @property {boolean} spawned False ⇒ the binary is missing (ENOENT).
|
|
107
|
+
* @property {number} exitCode 0 on success; -1 if not spawned.
|
|
108
|
+
* @property {string} stdout
|
|
109
|
+
* @property {string} stderr
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Run a binary. Resolves (never rejects). ENOENT ⇒ `spawned:false`. Runs in the
|
|
114
|
+
* view's cwd (the repo under inspection) so `git`/`gh` operate on the right tree.
|
|
115
|
+
* @param {string} bin
|
|
116
|
+
* @param {string[]} argv
|
|
117
|
+
* @returns {Promise<RunResult>}
|
|
118
|
+
*/
|
|
119
|
+
function run(bin, argv) {
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
execFile(
|
|
122
|
+
bin,
|
|
123
|
+
argv,
|
|
124
|
+
{ maxBuffer: 32 * 1024 * 1024, encoding: 'utf8', cwd: process.cwd() },
|
|
125
|
+
(err, stdout, stderr) => {
|
|
126
|
+
const out = typeof stdout === 'string' ? stdout : '';
|
|
127
|
+
const errOut = typeof stderr === 'string' ? stderr : '';
|
|
128
|
+
if (err && /** @type {any} */ (err).code === 'ENOENT') {
|
|
129
|
+
resolve({ spawned: false, exitCode: -1, stdout: out, stderr: errOut });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const code = err
|
|
133
|
+
? typeof /** @type {any} */ (err).code === 'number'
|
|
134
|
+
? /** @type {any} */ (err).code
|
|
135
|
+
: 1
|
|
136
|
+
: 0;
|
|
137
|
+
resolve({ spawned: true, exitCode: code, stdout: out, stderr: errOut });
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** @param {string} s @returns {string} */
|
|
144
|
+
function firstLine(s) {
|
|
145
|
+
const lines = String(s || '')
|
|
146
|
+
.split(/\r?\n/)
|
|
147
|
+
.map((l) => l.trim())
|
|
148
|
+
.filter(Boolean);
|
|
149
|
+
return lines.length ? lines[0] : '';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** @param {string} v @returns {number} */
|
|
153
|
+
function toInt(v) {
|
|
154
|
+
const n = parseInt(String(v), 10);
|
|
155
|
+
return Number.isFinite(n) ? n : 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── git: status parsing ───────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Classify a porcelain XY pair into the file's primary class. A file can be both
|
|
162
|
+
* staged AND modified (e.g. `MM`); the worktree change wins the glyph (it's the
|
|
163
|
+
* freshest, un-committed edit), but `counts` tallies both columns independently.
|
|
164
|
+
* @param {string} x Index column.
|
|
165
|
+
* @param {string} y Worktree column.
|
|
166
|
+
* @returns {'staged'|'modified'|'untracked'|'conflict'}
|
|
167
|
+
*/
|
|
168
|
+
function classify(x, y) {
|
|
169
|
+
if (x === 'U' || y === 'U' || (x === 'A' && y === 'A') || (x === 'D' && y === 'D')) return 'conflict';
|
|
170
|
+
if (x === '?' || y === '?') return 'untracked';
|
|
171
|
+
if (y !== ' ' && y !== '') return 'modified'; // worktree change (incl. D)
|
|
172
|
+
if (x !== ' ' && x !== '') return 'staged'; // index-only change
|
|
173
|
+
return 'modified';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse `git status --porcelain=v1 -z -uall` into changed files + column tallies.
|
|
178
|
+
* The `-z` form is NUL-terminated; a rename/copy entry is followed by an extra
|
|
179
|
+
* NUL field (the original path) which we consume.
|
|
180
|
+
* @param {string} stdout
|
|
181
|
+
* @returns {{files: ChangedFile[], counts: GitState['counts']}}
|
|
182
|
+
*/
|
|
183
|
+
function parseStatus(stdout) {
|
|
184
|
+
const parts = String(stdout || '').split('\0');
|
|
185
|
+
/** @type {ChangedFile[]} */
|
|
186
|
+
const files = [];
|
|
187
|
+
const counts = { staged: 0, modified: 0, untracked: 0, conflict: 0 };
|
|
188
|
+
for (let i = 0; i < parts.length; i++) {
|
|
189
|
+
const entry = parts[i];
|
|
190
|
+
if (!entry || entry.length < 3) continue;
|
|
191
|
+
const x = entry[0];
|
|
192
|
+
const y = entry[1];
|
|
193
|
+
const path = entry.slice(3);
|
|
194
|
+
// A rename/copy carries the source path in the next NUL field — skip it.
|
|
195
|
+
if (x === 'R' || x === 'C' || y === 'R' || y === 'C') i++;
|
|
196
|
+
const cls = classify(x, y);
|
|
197
|
+
if (cls === 'conflict') counts.conflict++;
|
|
198
|
+
else if (cls === 'untracked') counts.untracked++;
|
|
199
|
+
else {
|
|
200
|
+
if (x !== ' ' && x !== '?' && x !== '') counts.staged++;
|
|
201
|
+
if (y !== ' ' && y !== '?' && y !== '') counts.modified++;
|
|
202
|
+
}
|
|
203
|
+
files.push({ path, xy: `${x}${y}`, cls, add: 0, del: 0 });
|
|
204
|
+
}
|
|
205
|
+
return { files, counts };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Parse `git diff [--cached] --numstat` into a path→churn map. Binary files
|
|
210
|
+
* (`-\t-`) and renames (`old => new`) are skipped — churn is best-effort.
|
|
211
|
+
* @param {string} stdout
|
|
212
|
+
* @param {Map<string,{add:number,del:number}>} into
|
|
213
|
+
*/
|
|
214
|
+
function accChurn(stdout, into) {
|
|
215
|
+
for (const line of String(stdout || '').split(/\r?\n/)) {
|
|
216
|
+
if (!line.trim()) continue;
|
|
217
|
+
const m = line.split('\t');
|
|
218
|
+
if (m.length < 3) continue;
|
|
219
|
+
const add = m[0];
|
|
220
|
+
const del = m[1];
|
|
221
|
+
const path = m.slice(2).join('\t');
|
|
222
|
+
if (add === '-' || del === '-' || path.includes(' => ')) continue;
|
|
223
|
+
const prev = into.get(path) || { add: 0, del: 0 };
|
|
224
|
+
prev.add += toInt(add);
|
|
225
|
+
prev.del += toInt(del);
|
|
226
|
+
into.set(path, prev);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const CLASS_RANK = { conflict: 0, staged: 1, modified: 2, untracked: 3 };
|
|
231
|
+
|
|
232
|
+
// ── git: public API ───────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Read local git state for the view's cwd. Returns a typed error for the three
|
|
236
|
+
* hard cases (no git binary, not a repo, a git command failing); empty-repo and
|
|
237
|
+
* no-upstream degrade to null fields, NOT errors.
|
|
238
|
+
* @returns {Promise<Result<GitState>>}
|
|
239
|
+
*/
|
|
240
|
+
export async function fetchGit() {
|
|
241
|
+
const inside = await run('git', ['rev-parse', '--is-inside-work-tree']);
|
|
242
|
+
if (!inside.spawned) {
|
|
243
|
+
return fail({ kind: 'git-missing', message: 'git was not found on PATH — install git to use this view.' });
|
|
244
|
+
}
|
|
245
|
+
if (inside.exitCode !== 0 || inside.stdout.trim() !== 'true') {
|
|
246
|
+
return fail({ kind: 'not-a-repo', message: 'The current directory is not a git repository.' });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Branch (empty ⇒ detached HEAD; works even before the first commit).
|
|
250
|
+
const br = await run('git', ['branch', '--show-current']);
|
|
251
|
+
let branch = br.stdout.trim();
|
|
252
|
+
let detached = false;
|
|
253
|
+
if (branch === '') {
|
|
254
|
+
detached = true;
|
|
255
|
+
const head = await run('git', ['rev-parse', '--short', 'HEAD']);
|
|
256
|
+
branch = head.exitCode === 0 ? `(detached ${head.stdout.trim()})` : '(no branch)';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Upstream + ahead/behind (no upstream ⇒ null, ahead/behind 0).
|
|
260
|
+
let upstream = null;
|
|
261
|
+
let ahead = 0;
|
|
262
|
+
let behind = 0;
|
|
263
|
+
const up = await run('git', ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{upstream}']);
|
|
264
|
+
if (up.exitCode === 0) {
|
|
265
|
+
upstream = up.stdout.trim();
|
|
266
|
+
const counts = await run('git', ['rev-list', '--left-right', '--count', '@{upstream}...HEAD']);
|
|
267
|
+
if (counts.exitCode === 0) {
|
|
268
|
+
const nums = counts.stdout.trim().split(/\s+/);
|
|
269
|
+
behind = toInt(nums[0]); // left = in upstream, not HEAD
|
|
270
|
+
ahead = toInt(nums[1]); // right = in HEAD, not upstream
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Last commit (null on an empty repo).
|
|
275
|
+
let lastCommit = null;
|
|
276
|
+
const log = await run('git', ['log', '-1', '--pretty=%h%x1f%s%x1f%cI']);
|
|
277
|
+
if (log.exitCode === 0 && log.stdout.trim() !== '') {
|
|
278
|
+
const [sha, subject, when] = log.stdout.replace(/\n$/, '').split('\x1f');
|
|
279
|
+
lastCommit = { sha: sha || '', subject: subject || '', when: when || '' };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Working-tree status. A failure here (rare once we know it's a repo) is the
|
|
283
|
+
// one genuine git-failed case.
|
|
284
|
+
const st = await run('git', ['status', '--porcelain=v1', '-z', '--untracked-files=all']);
|
|
285
|
+
if (!st.spawned) return fail({ kind: 'git-missing', message: 'git disappeared mid-read.' });
|
|
286
|
+
if (st.exitCode !== 0) {
|
|
287
|
+
return fail({ kind: 'git-failed', message: firstLine(st.stderr) || 'git status failed.' });
|
|
288
|
+
}
|
|
289
|
+
const { files, counts } = parseStatus(st.stdout);
|
|
290
|
+
|
|
291
|
+
// Churn (best-effort; never fatal).
|
|
292
|
+
/** @type {Map<string,{add:number,del:number}>} */
|
|
293
|
+
const churn = new Map();
|
|
294
|
+
const unstaged = await run('git', ['diff', '--numstat']);
|
|
295
|
+
if (unstaged.exitCode === 0) accChurn(unstaged.stdout, churn);
|
|
296
|
+
const staged = await run('git', ['diff', '--cached', '--numstat']);
|
|
297
|
+
if (staged.exitCode === 0) accChurn(staged.stdout, churn);
|
|
298
|
+
for (const f of files) {
|
|
299
|
+
const c = churn.get(f.path);
|
|
300
|
+
if (c) {
|
|
301
|
+
f.add = c.add;
|
|
302
|
+
f.del = c.del;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Problems first (conflict → staged → modified → untracked), then by path.
|
|
307
|
+
files.sort((a, b) => {
|
|
308
|
+
const r = CLASS_RANK[a.cls] - CLASS_RANK[b.cls];
|
|
309
|
+
return r !== 0 ? r : a.path < b.path ? -1 : a.path > b.path ? 1 : 0;
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
return ok({ branch, detached, upstream, ahead, behind, lastCommit, files, counts });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── gh: rollups + classification ───────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
const CI_BAD = new Set(['FAILURE', 'ERROR', 'CANCELLED', 'TIMED_OUT', 'ACTION_REQUIRED', 'STARTUP_FAILURE']);
|
|
318
|
+
const CI_GOOD = new Set(['SUCCESS', 'NEUTRAL', 'SKIPPED']);
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Roll a PR's statusCheckRollup array into one verdict. CheckRun entries carry
|
|
322
|
+
* `status` (QUEUED/IN_PROGRESS/COMPLETED) + `conclusion`; StatusContext entries
|
|
323
|
+
* carry `state`. Any bad → fail; else any in-flight/unknown → pending; else pass.
|
|
324
|
+
* @param {any[]|undefined} items
|
|
325
|
+
* @returns {'pass'|'fail'|'pending'|'none'}
|
|
326
|
+
*/
|
|
327
|
+
function rollupCi(items) {
|
|
328
|
+
if (!Array.isArray(items) || items.length === 0) return 'none';
|
|
329
|
+
let pending = false;
|
|
330
|
+
for (const it of items) {
|
|
331
|
+
const concl = String((it && it.conclusion) || '').toUpperCase();
|
|
332
|
+
const state = String((it && it.state) || '').toUpperCase();
|
|
333
|
+
const status = String((it && it.status) || '').toUpperCase();
|
|
334
|
+
if (CI_BAD.has(concl) || CI_BAD.has(state)) return 'fail';
|
|
335
|
+
if (status && status !== 'COMPLETED') pending = true;
|
|
336
|
+
else if (state && !CI_GOOD.has(state) && !CI_BAD.has(state)) pending = true; // PENDING/EXPECTED/QUEUED
|
|
337
|
+
else if (!concl && !state && !status) pending = true; // shapeless ⇒ treat as in-flight
|
|
338
|
+
}
|
|
339
|
+
return pending ? 'pending' : 'pass';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** @param {string} d @returns {'approved'|'changes'|'review'} */
|
|
343
|
+
function normReview(d) {
|
|
344
|
+
const s = String(d || '').toUpperCase();
|
|
345
|
+
if (s === 'APPROVED') return 'approved';
|
|
346
|
+
if (s === 'CHANGES_REQUESTED') return 'changes';
|
|
347
|
+
return 'review'; // REVIEW_REQUIRED / "" / null
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Classify a failed `gh` invocation. Auth first (the most common), then
|
|
352
|
+
* network, then a missing/!GitHub remote, else generic.
|
|
353
|
+
* @param {string} stderr
|
|
354
|
+
* @returns {GhError}
|
|
355
|
+
*/
|
|
356
|
+
function classifyGh(stderr) {
|
|
357
|
+
const s = String(stderr || '').toLowerCase();
|
|
358
|
+
if (/auth|logged in|gh auth login|authentication|not logged/.test(s)) {
|
|
359
|
+
return { kind: 'gh-unauthed', message: 'gh is not authenticated — run `gh auth login`.' };
|
|
360
|
+
}
|
|
361
|
+
if (/could not resolve host|dial tcp|network is unreachable|no such host|timeout|temporary failure|connection refused/.test(s)) {
|
|
362
|
+
return { kind: 'gh-network', message: 'Cannot reach GitHub (offline?).' };
|
|
363
|
+
}
|
|
364
|
+
if (/could not resolve to a repository|no git remote|none of the git remotes|no default remote|not a github|head branch could not/.test(s)) {
|
|
365
|
+
return { kind: 'gh-no-remote', message: 'No GitHub remote for this repository.' };
|
|
366
|
+
}
|
|
367
|
+
return { kind: 'gh-failed', message: firstLine(stderr) || 'gh command failed.' };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ── gh: public API ─────────────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Open PRs for this repo, the current branch's PR first then most-recently
|
|
374
|
+
* updated. BEST-EFFORT: a typed error means the PR section degrades to a guided
|
|
375
|
+
* note — the git section is unaffected.
|
|
376
|
+
* @param {string} branch The checked-out branch (to mark the current PR).
|
|
377
|
+
* @returns {Promise<Result<Pr[]>>}
|
|
378
|
+
*/
|
|
379
|
+
export async function fetchPrs(branch) {
|
|
380
|
+
const r = await run('gh', [
|
|
381
|
+
'pr',
|
|
382
|
+
'list',
|
|
383
|
+
'--state',
|
|
384
|
+
'open',
|
|
385
|
+
'--limit',
|
|
386
|
+
'30',
|
|
387
|
+
'--json',
|
|
388
|
+
'number,title,headRefName,reviewDecision,statusCheckRollup,updatedAt,isDraft',
|
|
389
|
+
]);
|
|
390
|
+
if (!r.spawned) {
|
|
391
|
+
return fail({ kind: 'gh-missing', message: 'gh (GitHub CLI) not found — install it to see PRs.' });
|
|
392
|
+
}
|
|
393
|
+
if (r.exitCode !== 0) return fail(classifyGh(r.stderr || r.stdout));
|
|
394
|
+
|
|
395
|
+
let arr;
|
|
396
|
+
try {
|
|
397
|
+
arr = JSON.parse(r.stdout.trim() || '[]');
|
|
398
|
+
} catch {
|
|
399
|
+
return fail({ kind: 'gh-failed', message: 'could not parse gh output as JSON.' });
|
|
400
|
+
}
|
|
401
|
+
if (!Array.isArray(arr)) arr = [];
|
|
402
|
+
|
|
403
|
+
/** @type {Pr[]} */
|
|
404
|
+
const prs = arr.map((p) => {
|
|
405
|
+
// Guard the whole element — gh shouldn't emit null array entries, but this
|
|
406
|
+
// file's contract is never-throw, so dereference only off a safe object.
|
|
407
|
+
const o = p || {};
|
|
408
|
+
const head = String(o.headRefName || '');
|
|
409
|
+
return {
|
|
410
|
+
number: typeof o.number === 'number' ? o.number : toInt(o.number),
|
|
411
|
+
title: String(o.title || '(untitled)'),
|
|
412
|
+
headRefName: head,
|
|
413
|
+
isDraft: !!o.isDraft,
|
|
414
|
+
current: head !== '' && head === branch,
|
|
415
|
+
review: normReview(o.reviewDecision),
|
|
416
|
+
ci: rollupCi(o.statusCheckRollup),
|
|
417
|
+
updatedAt: String(o.updatedAt || ''),
|
|
418
|
+
};
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
prs.sort((a, b) => {
|
|
422
|
+
if (a.current !== b.current) return a.current ? -1 : 1; // current branch first
|
|
423
|
+
return a.updatedAt < b.updatedAt ? 1 : a.updatedAt > b.updatedAt ? -1 : 0; // newest first
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
return ok(prs);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Introspection helper (not used at runtime): the exact shell-outs this client
|
|
431
|
+
* makes, so the shape can be eyeballed without a live repo.
|
|
432
|
+
* @returns {Record<string,string>}
|
|
433
|
+
*/
|
|
434
|
+
export function describeCommands() {
|
|
435
|
+
return {
|
|
436
|
+
fetchGit:
|
|
437
|
+
'git rev-parse --is-inside-work-tree · branch --show-current · rev-list --left-right --count @{u}...HEAD · log -1 · status --porcelain=v1 -z · diff [--cached] --numstat',
|
|
438
|
+
fetchPrs: 'gh pr list --state open --json number,title,headRefName,reviewDecision,statusCheckRollup,updatedAt,isDraft',
|
|
439
|
+
};
|
|
440
|
+
}
|