@philogag/pi-tui-openspec-status 0.1.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 ADDED
@@ -0,0 +1,112 @@
1
+ # `@philogag/pi-tui-openspec-status`
2
+
3
+ A [pi coding agent](https://github.com/mattoopie/pi) extension that
4
+ shows the current locked **openspec** change as a single status-bar line:
5
+
6
+ ```
7
+ add-pi-tui-openspec-status (superpowers-bridge-cn) [P● D● S○ T○] Tasks: ███░░░░░░░ 2/7
8
+ ```
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pnpm add -D @philogag/pi-tui-openspec-status
14
+ ```
15
+
16
+ Then enable in your pi config (`~/.pi/settings.json` or
17
+ `<repo>/.pi/settings.json`):
18
+
19
+ ```json
20
+ {
21
+ "extensions": ["@philogag/pi-tui-openspec-status"]
22
+ }
23
+ ```
24
+
25
+ ## Activation mode
26
+
27
+ The extension is **TUI-only**. `ctx.mode` is unknown at factory time
28
+ (it's only available once the first event fires), so the gate runs at
29
+ `session_start` (the first event):
30
+
31
+ | Mode | Activates? | Notes |
32
+ |------------|------------|------------------------------------|
33
+ | `tui` | ✅ yes | Normal interactive operation |
34
+ | `rpc` | ❌ no | `ctx.hasUI === true` here too, but mode check excludes it |
35
+ | `json` | ❌ no | No event-stream output |
36
+ | `print` | ❌ no | `-p` one-shot mode |
37
+
38
+ Per `pi.dev/docs/latest/extensions#ctx-mode`, `ctx.mode` (not
39
+ `ctx.hasUI`) is the correct TUI feature gate. The `/tui-openspec-select`
40
+ command (which can run without a prior `session_start`) re-checks the
41
+ mode before rendering.
42
+
43
+ ## Behavior
44
+
45
+ - The status line appears when you (or the agent) invoke an
46
+ openspec command that **explicitly names a change** —
47
+ `new`, `status`, `apply`, `archive`, `verify`, `sync`,
48
+ `instructions`, `show`, `validate`, `context`, `view` — **or** when
49
+ you manually select a change with `/tui-openspec-select`.
50
+ - Browsing commands like `openspec list` / `openspec doctor` clear the
51
+ status line.
52
+ - The line refreshes 500ms after each matching `bash` tool call.
53
+
54
+ ## Manual tracking with `/tui-openspec-select`
55
+
56
+ In TUI mode you can take manual control of the status bar with the
57
+ `/tui-openspec-select` command:
58
+
59
+ - Opens an interactive picker listing every **active** change
60
+ (`openspec/changes/*/` minus `archive/`) plus a `None` option.
61
+ - Selecting a change manually **locks** the status bar to it: bash
62
+ `openspec` commands will NOT switch it away until you manually
63
+ re-select another change or pick `None` (manual overrides auto).
64
+ - Picking `None` clears the manual lock and restores automatic
65
+ tracking from bash commands.
66
+ - Cancelling the picker (Esc) changes nothing — the current tracking
67
+ state is left untouched.
68
+ - Archiving the manually tracked change (e.g. `openspec archive <name>`)
69
+ still auto-clears the status bar, as usual.
70
+
71
+ ## Lock persistence across restarts
72
+
73
+ The tracked spec, worktree, and lock type (manual vs auto) are
74
+ persisted into the session file via `pi.appendEntry()` (custom entries
75
+ — never sent to the LLM). On `session_start` — including `/resume`,
76
+ where pi reloads the extension with a fresh instance — the last
77
+ matching entry is read back and the status bar is rebuilt:
78
+
79
+ - A **manual** lock (`/tui-openspec-select`) is restored pinned, so
80
+ bash openspec commands don't switch it away.
81
+ - An **auto** lock (from a bash `openspec` command) is restored with
82
+ its auto semantics, so a later `openspec status --change X` still
83
+ updates the tracked spec.
84
+ - Clearing (`None` / auto-unlock on archive) writes an explicit empty
85
+ snapshot, so a stale lock is never restored.
86
+
87
+ This means the status bar survives `/resume` and extension reloads
88
+ instead of going empty.
89
+
90
+ ## Worktree support
91
+
92
+ When `openspec` is invoked inside a git worktree
93
+ (e.g. `.worktrees/feat/openspec-status/`), the extension reads
94
+ `tasks.md` from **both** the main repo and the worktree, then
95
+ deduplicates by task ID:
96
+
97
+ - A task is "done" if checked in either side.
98
+ - Total count is the union of unique task IDs.
99
+
100
+ This prevents the progress bar from regressing when the worktree is
101
+ ahead of the main repo (the common SDD apply scenario).
102
+
103
+ ## Limitations
104
+
105
+ - Only the schema's external artifacts (`proposal`, `design`, `specs`,
106
+ `tasks`) appear; planning-phase internal artifacts
107
+ (`brainstorm`, `verify`, `retrospective`) are hidden.
108
+ - Requires the `openspec` CLI on `$PATH`. Missing CLI silently disables
109
+ the extension.
110
+ - Does **not** render widgets, dialogs, or keyboard shortcuts — only
111
+ the bottom status bar (`ctx.ui.setStatus`).
112
+ - **Does not activate in non-TUI modes** (rpc/json/print) — by design.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * List active (non-archived) openspec change names under
3
+ * `<openspecRoot>/openspec/changes/`. Stable-sorted; excludes the
4
+ * `archive` directory. Returns [] when the directory is
5
+ * missing/unreadable.
6
+ */
7
+ export declare function listActiveChanges(openspecRoot: string): Promise<string[]>;
8
+ //# sourceMappingURL=discover.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,MAAM,EAAE,CAAC,CAYnB"}
@@ -0,0 +1,24 @@
1
+ // src/discover.ts
2
+ import { readdir } from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ /**
5
+ * List active (non-archived) openspec change names under
6
+ * `<openspecRoot>/openspec/changes/`. Stable-sorted; excludes the
7
+ * `archive` directory. Returns [] when the directory is
8
+ * missing/unreadable.
9
+ */
10
+ export async function listActiveChanges(openspecRoot) {
11
+ const changesDir = path.join(openspecRoot, "openspec", "changes");
12
+ let entries;
13
+ try {
14
+ entries = await readdir(changesDir, { withFileTypes: true });
15
+ }
16
+ catch {
17
+ return [];
18
+ }
19
+ return entries
20
+ .filter((e) => e.isDirectory() && e.name !== "archive")
21
+ .map((e) => e.name)
22
+ .sort();
23
+ }
24
+ //# sourceMappingURL=discover.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover.js","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"AAAA,kBAAkB;AAClB,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,YAAoB;IAEpB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC;IAClE,IAAI,OAAO,CAAC;IACZ,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,OAAO;SACX,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC;SACtD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;SAClB,IAAI,EAAE,CAAC;AACZ,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ export declare const EXTENSION_ID = "pi-tui-openspec-status";
3
+ export declare const SET_STATUS_DEBOUNCE_MS = 500;
4
+ export interface PiTuiOpenspecStatusOptions {
5
+ /**
6
+ * Override the render debounce window (ms). Defaults to
7
+ * {@link SET_STATUS_DEBOUNCE_MS}. Tests typically pass a small value
8
+ * (e.g. 0) to skip debouncing entirely.
9
+ */
10
+ debounceMs?: number;
11
+ }
12
+ /**
13
+ * Wire the OpenSpec status bar into pi.
14
+ *
15
+ * Thin wiring layer: all tracking state and the render pipeline live
16
+ * in {@link OpenSpecStatusRender}; this module only connects pi events
17
+ * to it. A single render instance is created at session_start (or
18
+ * lazily by the select command, which may run without session_start in
19
+ * tests) and reused for the whole session.
20
+ *
21
+ * Lock persistence: every authoritative state change (lock / auto-lock
22
+ * / worktree / clear / auto-unlock) is mirrored into the session file
23
+ * via `pi.appendEntry(LOCK_CUSTOM_TYPE, snapshot)` — custom entries do
24
+ * NOT participate in LLM context. On session_start (including
25
+ * `/resume`, where pi reloads the extension with a fresh instance) the
26
+ * last matching entry is read back via `findLastPersistedLock()` and
27
+ * the render is rebuilt with the same lock type (manual vs auto), so
28
+ * the status bar survives restarts instead of going empty.
29
+ *
30
+ * The extension factory receives ONLY `ExtensionAPI` (see
31
+ * https://pi.dev/docs/latest/extensions) — there is no ctx at load
32
+ * time, so the mode is unknown until the first event fires. Handlers
33
+ * are therefore registered unconditionally, and the TUI gate
34
+ * (`ctx.mode === "tui"`) runs inside session_start — the first event
35
+ * that carries a real ExtensionContext. Until then every event is a
36
+ * no-op because no render exists yet.
37
+ */
38
+ export default function (pi: ExtensionAPI, options?: PiTuiOpenspecStatusOptions): void;
39
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AASA,OAAO,EAEL,KAAK,YAAY,EAClB,MAAM,iCAAiC,CAAC;AAEzC,eAAO,MAAM,YAAY,2BAA2B,CAAC;AACrD,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,OAAO,WACZ,EAAE,EAAE,YAAY,EAChB,OAAO,GAAE,0BAA+B,GACvC,IAAI,CAsGN"}
package/dist/index.js ADDED
@@ -0,0 +1,136 @@
1
+ // src/index.ts
2
+ import { listActiveChanges } from "./discover.js";
3
+ import { findSpec, findWorkTree } from "./parser.js";
4
+ import { OpenSpecStatusRender } from "./render.js";
5
+ import { findLastPersistedLock, LOCK_CUSTOM_TYPE, } from "./state.js";
6
+ import { isToolCallEventType, } from "@earendil-works/pi-coding-agent";
7
+ export const EXTENSION_ID = "pi-tui-openspec-status";
8
+ export const SET_STATUS_DEBOUNCE_MS = 500;
9
+ /**
10
+ * Wire the OpenSpec status bar into pi.
11
+ *
12
+ * Thin wiring layer: all tracking state and the render pipeline live
13
+ * in {@link OpenSpecStatusRender}; this module only connects pi events
14
+ * to it. A single render instance is created at session_start (or
15
+ * lazily by the select command, which may run without session_start in
16
+ * tests) and reused for the whole session.
17
+ *
18
+ * Lock persistence: every authoritative state change (lock / auto-lock
19
+ * / worktree / clear / auto-unlock) is mirrored into the session file
20
+ * via `pi.appendEntry(LOCK_CUSTOM_TYPE, snapshot)` — custom entries do
21
+ * NOT participate in LLM context. On session_start (including
22
+ * `/resume`, where pi reloads the extension with a fresh instance) the
23
+ * last matching entry is read back via `findLastPersistedLock()` and
24
+ * the render is rebuilt with the same lock type (manual vs auto), so
25
+ * the status bar survives restarts instead of going empty.
26
+ *
27
+ * The extension factory receives ONLY `ExtensionAPI` (see
28
+ * https://pi.dev/docs/latest/extensions) — there is no ctx at load
29
+ * time, so the mode is unknown until the first event fires. Handlers
30
+ * are therefore registered unconditionally, and the TUI gate
31
+ * (`ctx.mode === "tui"`) runs inside session_start — the first event
32
+ * that carries a real ExtensionContext. Until then every event is a
33
+ * no-op because no render exists yet.
34
+ */
35
+ export default function (pi, options = {}) {
36
+ const debounceMs = options.debounceMs ?? SET_STATUS_DEBOUNCE_MS;
37
+ let render;
38
+ /**
39
+ * Persist a lock snapshot to the session file. `null` (or an
40
+ * all-cleared state) is stored as an explicit "no lock" entry so
41
+ * that a later resume restores the empty state instead of a stale
42
+ * lock. Persistence is best-effort: appendEntry failures must never
43
+ * propagate into the extension's render path (D12).
44
+ */
45
+ const persistLock = (state) => {
46
+ try {
47
+ pi.appendEntry(LOCK_CUSTOM_TYPE, state ?? {
48
+ spec: "",
49
+ manualLock: false,
50
+ version: 1,
51
+ });
52
+ }
53
+ catch {
54
+ // append-only session journal; a failure here is not fatal.
55
+ }
56
+ };
57
+ pi.on("session_start", (_event, ctx) => {
58
+ // D9: only activate under a real TUI. rpc/json/print never create
59
+ // a render, so the status bar is never touched.
60
+ if (ctx?.mode !== "tui")
61
+ return;
62
+ render = new OpenSpecStatusRender(EXTENSION_ID, ctx, debounceMs, persistLock);
63
+ // Resume/restart: rebuild the previous lock state from the session
64
+ // journal (D1/D2/D4). A manual lock is pinned via render.lock(); an
65
+ // auto lock keeps its auto semantics via setSpec + setWorkTree.
66
+ // Recovery is best-effort: any failure keeps the empty state and
67
+ // never throws into the session_start handler (D12).
68
+ try {
69
+ const restored = findLastPersistedLock(ctx.sessionManager.getEntries());
70
+ if (restored && restored.spec) {
71
+ if (restored.manualLock) {
72
+ render.lock(restored.spec);
73
+ if (restored.worktree)
74
+ render.setWorkTree(restored.worktree);
75
+ }
76
+ else {
77
+ render.setSpec(restored.spec);
78
+ if (restored.worktree)
79
+ render.setWorkTree(restored.worktree);
80
+ }
81
+ }
82
+ }
83
+ catch {
84
+ // dirty journal data → stay in the empty state.
85
+ }
86
+ // Explicit initial publish: status bar shows "no change" until the
87
+ // first lock. Kept out of the constructor so that creating a render
88
+ // lazily (select command) has no side effects. Published after the
89
+ // restore so a restored lock immediately supersedes it via refresh().
90
+ ctx.ui.setStatus(EXTENSION_ID, undefined);
91
+ });
92
+ pi.on("tool_call", (event) => {
93
+ if (!render)
94
+ return;
95
+ // Real event shape: { type, toolCallId, toolName: "bash", input: { command } }.
96
+ // The bash tool input has NO `type` field — use the official guard
97
+ // (checks event.toolName === "bash") instead of sniffing input.type.
98
+ if (!isToolCallEventType("bash", event))
99
+ return;
100
+ const cmd = event.input.command;
101
+ if (typeof cmd !== "string")
102
+ return;
103
+ const spec = findSpec(cmd);
104
+ if (spec)
105
+ render.setSpec(spec);
106
+ const worktree = findWorkTree(cmd);
107
+ if (worktree)
108
+ render.setWorkTree(worktree);
109
+ });
110
+ pi.on("tool_result", () => {
111
+ if (render)
112
+ render.refresh();
113
+ });
114
+ pi.registerCommand("tui-openspec-select", {
115
+ description: "Manually select which openspec change the status bar tracks (None to clear)",
116
+ handler: async (_args, cmdCtx) => {
117
+ // TUI-only, like the rest of the extension.
118
+ if (cmdCtx.mode !== "tui")
119
+ return;
120
+ const changes = await listActiveChanges(cmdCtx.cwd);
121
+ const choice = await cmdCtx.ui.select("Select spec to track:", [
122
+ ...changes,
123
+ "None",
124
+ ]);
125
+ if (choice === undefined)
126
+ return; // cancelled — no side effects
127
+ if (choice === "None") {
128
+ render?.clearLock();
129
+ return;
130
+ }
131
+ render ??= new OpenSpecStatusRender(EXTENSION_ID, cmdCtx, debounceMs, persistLock);
132
+ render.lock(choice);
133
+ },
134
+ });
135
+ }
136
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import type { ArtifactStatus, MergedTasks, StatusJson } from "./types.js";
2
+ export declare function parseTasksFile(text: string): Map<string, boolean>;
3
+ export declare function mergeTasks(main: Map<string, boolean>, worktree: Map<string, boolean>): MergedTasks;
4
+ /**
5
+ * Read tasks.md from both main repo and (optional) worktree,
6
+ * parse & merge.
7
+ *
8
+ * Each source is read independently — a missing tasks.md in main does
9
+ * NOT suppress the worktree's tasks (and vice versa). The caller
10
+ * (`render`) decides whether missing change folders overall mean
11
+ * "unlock"; this function only merges whatever it can find.
12
+ */
13
+ export declare function readMergedTasks(changeName: string, mainRepoRoot: string, worktreeCwd?: string): Promise<MergedTasks>;
14
+ /**
15
+ * Union multiple artifact-status lists. Per id:
16
+ * - "done" wins over any other status (union of completed work)
17
+ * - otherwise keep the first non-empty status we see
18
+ * Output is de-duped by id and emitted in canonical order, with any
19
+ * unknown ids appended at the end.
20
+ */
21
+ export declare function mergeArtifactStatuses(lists: ReadonlyArray<ReadonlyArray<ArtifactStatus> | undefined | null>): ArtifactStatus[];
22
+ /**
23
+ * Merge multiple StatusJson results (e.g. from main + worktree scans).
24
+ * - schemaName: first non-empty wins
25
+ * - applied: true if any source says true
26
+ * - artifacts: union via mergeArtifactStatuses
27
+ * Returns null when ALL inputs are null.
28
+ */
29
+ export declare function mergeStatusResults(results: ReadonlyArray<StatusJson | null>): StatusJson | null;
30
+ //# sourceMappingURL=merge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAM1E,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAmBjE;AAED,wBAAgB,UAAU,CACxB,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,WAAW,CAOb;AAgBD;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,MAAM,EACpB,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,WAAW,CAAC,CAUtB;AAUD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,aAAa,CAAC,aAAa,CAAC,cAAc,CAAC,GAAG,SAAS,GAAG,IAAI,CAAC,GACrE,cAAc,EAAE,CA6BlB;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,aAAa,CAAC,UAAU,GAAG,IAAI,CAAC,GACxC,UAAU,GAAG,IAAI,CAWnB"}
package/dist/merge.js ADDED
@@ -0,0 +1,134 @@
1
+ // src/merge.ts
2
+ import { readFile } from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ const CHECKED_RE = /^\s*-\s*\[(x|done)\]\s+(\S+)/i;
5
+ const UNCHECKED_RE = /^\s*-\s*\[( |)\]\s+(\S+)/;
6
+ const ANY_TASK_RE = /^\s*-\s*\[[ xX]\]?\s*(\S+)/;
7
+ export function parseTasksFile(text) {
8
+ const out = new Map();
9
+ for (const line of text.split(/\r?\n/)) {
10
+ let m;
11
+ if ((m = CHECKED_RE.exec(line))) {
12
+ out.set(m[2], true);
13
+ continue;
14
+ }
15
+ if ((m = UNCHECKED_RE.exec(line))) {
16
+ out.set(m[2], false);
17
+ continue;
18
+ }
19
+ if ((m = ANY_TASK_RE.exec(line))) {
20
+ // Fallback: line that looks like a task but didn't match above;
21
+ // default to unchecked if not already present.
22
+ if (!out.has(m[1]))
23
+ out.set(m[1], false);
24
+ }
25
+ }
26
+ return out;
27
+ }
28
+ export function mergeTasks(main, worktree) {
29
+ const keys = new Set([...main.keys(), ...worktree.keys()]);
30
+ let done = 0;
31
+ for (const k of keys) {
32
+ if (main.get(k) === true || worktree.get(k) === true)
33
+ done++;
34
+ }
35
+ return { done, total: keys.size };
36
+ }
37
+ /**
38
+ * Read tasks.md from `p`, returning an empty Map on any failure.
39
+ * Use this so that a missing file is "no tasks here", not a fatal
40
+ * error that aborts the whole merge.
41
+ */
42
+ async function readTasksMap(p) {
43
+ try {
44
+ const text = await readFile(p, "utf8");
45
+ return parseTasksFile(text);
46
+ }
47
+ catch {
48
+ return new Map();
49
+ }
50
+ }
51
+ /**
52
+ * Read tasks.md from both main repo and (optional) worktree,
53
+ * parse & merge.
54
+ *
55
+ * Each source is read independently — a missing tasks.md in main does
56
+ * NOT suppress the worktree's tasks (and vice versa). The caller
57
+ * (`render`) decides whether missing change folders overall mean
58
+ * "unlock"; this function only merges whatever it can find.
59
+ */
60
+ export async function readMergedTasks(changeName, mainRepoRoot, worktreeCwd) {
61
+ const mainMap = await readTasksMap(path.join(mainRepoRoot, "openspec", "changes", changeName, "tasks.md"));
62
+ const wtMap = worktreeCwd
63
+ ? await readTasksMap(path.join(worktreeCwd, "openspec", "changes", changeName, "tasks.md"))
64
+ : new Map();
65
+ return mergeTasks(mainMap, wtMap);
66
+ }
67
+ // Canonical artifact order in the status line.
68
+ const ARTIFACT_ORDER = [
69
+ "proposal",
70
+ "design",
71
+ "specs",
72
+ "tasks",
73
+ ];
74
+ /**
75
+ * Union multiple artifact-status lists. Per id:
76
+ * - "done" wins over any other status (union of completed work)
77
+ * - otherwise keep the first non-empty status we see
78
+ * Output is de-duped by id and emitted in canonical order, with any
79
+ * unknown ids appended at the end.
80
+ */
81
+ export function mergeArtifactStatuses(lists) {
82
+ const out = new Map();
83
+ const orderedIds = [...ARTIFACT_ORDER];
84
+ // Pass 1: canonical ids in canonical order
85
+ for (const id of ARTIFACT_ORDER) {
86
+ for (const list of lists) {
87
+ if (!list)
88
+ continue;
89
+ const hit = list.find((a) => a.id === id);
90
+ if (!hit)
91
+ continue;
92
+ const cur = out.get(id);
93
+ if (!cur || hit.status === "done")
94
+ out.set(id, hit.status);
95
+ }
96
+ }
97
+ // Pass 2: any non-canonical ids, in first-seen order
98
+ for (const list of lists) {
99
+ if (!list)
100
+ continue;
101
+ for (const a of list) {
102
+ if (orderedIds.includes(a.id))
103
+ continue;
104
+ orderedIds.push(a.id);
105
+ const cur = out.get(a.id);
106
+ if (!cur || a.status === "done")
107
+ out.set(a.id, a.status);
108
+ }
109
+ }
110
+ return orderedIds
111
+ .filter((id) => out.has(id))
112
+ .map((id) => ({ id, status: out.get(id) }));
113
+ }
114
+ /**
115
+ * Merge multiple StatusJson results (e.g. from main + worktree scans).
116
+ * - schemaName: first non-empty wins
117
+ * - applied: true if any source says true
118
+ * - artifacts: union via mergeArtifactStatuses
119
+ * Returns null when ALL inputs are null.
120
+ */
121
+ export function mergeStatusResults(results) {
122
+ const valid = results.filter((r) => r !== null);
123
+ if (valid.length === 0)
124
+ return null;
125
+ const schemaName = valid.find((r) => typeof r.schemaName === "string")
126
+ ?.schemaName;
127
+ const applied = valid.some((r) => r.applied === true);
128
+ return {
129
+ schemaName,
130
+ applied,
131
+ artifacts: mergeArtifactStatuses(valid.map((r) => r.artifacts)),
132
+ };
133
+ }
134
+ //# sourceMappingURL=merge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.js","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { StatusJson } from "./types.js";
2
+ export declare const OPENSPEC_STATUS_TIMEOUT_MS = 10000;
3
+ export declare function runOpenspecStatus(changeName: string, cwd: string): Promise<StatusJson | null>;
4
+ //# sourceMappingURL=openspec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openspec.d.ts","sourceRoot":"","sources":["../src/openspec.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAM7C,eAAO,MAAM,0BAA0B,QAAQ,CAAC;AAEhD,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAiD5B"}
@@ -0,0 +1,53 @@
1
+ // src/openspec.ts
2
+ import { spawn } from "node:child_process";
3
+ import { clearTimeout } from "node:timers";
4
+ // The openspec CLI needs ~1.2–2s to produce the full status JSON on a
5
+ // cold node/jiti start, and more under load. Keep generous headroom so
6
+ // a slow CLI never collapses the status line to "spec-driven".
7
+ export const OPENSPEC_STATUS_TIMEOUT_MS = 10000;
8
+ export async function runOpenspecStatus(changeName, cwd) {
9
+ return new Promise((resolve) => {
10
+ let settled = false;
11
+ const finish = (v) => {
12
+ if (settled)
13
+ return;
14
+ settled = true;
15
+ resolve(v);
16
+ };
17
+ let stdout = "";
18
+ let stderr = "";
19
+ let proc = null;
20
+ try {
21
+ proc = spawn("openspec", ["status", "--change", changeName, "--json"], { cwd: cwd || process.cwd(), stdio: ["ignore", "pipe", "pipe"] });
22
+ }
23
+ catch {
24
+ finish(null);
25
+ return;
26
+ }
27
+ const timer = setTimeout(() => {
28
+ proc?.kill();
29
+ finish(null);
30
+ }, OPENSPEC_STATUS_TIMEOUT_MS);
31
+ proc.stdout?.on("data", (b) => (stdout += b.toString("utf8")));
32
+ proc.stderr?.on("data", (b) => (stderr += b.toString("utf8")));
33
+ proc.on("error", () => {
34
+ clearTimeout(timer);
35
+ finish(null);
36
+ });
37
+ proc.on("close", (code) => {
38
+ clearTimeout(timer);
39
+ if (code !== 0) {
40
+ finish(null);
41
+ return;
42
+ }
43
+ try {
44
+ const parsed = JSON.parse(stdout);
45
+ finish(parsed);
46
+ }
47
+ catch {
48
+ finish(null);
49
+ }
50
+ });
51
+ });
52
+ }
53
+ //# sourceMappingURL=openspec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openspec.js","sourceRoot":"","sources":["../src/openspec.ts"],"names":[],"mappings":"AAAA,kBAAkB;AAClB,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,sEAAsE;AACtE,uEAAuE;AACvE,+DAA+D;AAC/D,MAAM,CAAC,MAAM,0BAA0B,GAAG,KAAK,CAAC;AAEhD,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,UAAkB,EAClB,GAAW;IAEX,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,MAAM,GAAG,CAAC,CAAoB,EAAE,EAAE;YACtC,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,OAAO,CAAC,CAAC,CAAC,CAAC;QACb,CAAC,CAAC;QAEF,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,IAAI,GAAoC,IAAI,CAAC;QACjD,IAAI,CAAC;YACH,IAAI,GAAG,KAAK,CACV,UAAU,EACV,CAAC,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,CAAC,EAC5C,EAAE,GAAG,EAAE,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CACjE,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,CAAC,IAAI,CAAC,CAAC;YACb,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,IAAI,EAAE,IAAI,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,CAAC;QACf,CAAC,EAAE,0BAA0B,CAAC,CAAC;QAE/B,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC/D,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAE/D,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACpB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC,CAAC;QACf,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;YACxB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;gBACf,MAAM,CAAC,IAAI,CAAC,CAAC;gBACb,OAAO;YACT,CAAC;YACD,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAe,CAAC;gBAChD,MAAM,CAAC,MAAM,CAAC,CAAC;YACjB,CAAC;YAAC,MAAM,CAAC;gBACP,MAAM,CAAC,IAAI,CAAC,CAAC;YACf,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,39 @@
1
+ import type { ParsedBashCommand } from "./types.js";
2
+ export declare function isLockingSubcommand(sub: string): boolean;
3
+ /**
4
+ * Split a bash command string into a flat token list.
5
+ * Supports quoted strings ("…" or '…') as single tokens.
6
+ * Honors && || | ; connectors by emitting them as separators (kept out).
7
+ */
8
+ export declare function tokenize(cmd: string): string[];
9
+ /**
10
+ * From a token list starting with the openspec subcommand
11
+ * (e.g. ["status", "--change", "add-foo", "--json"]),
12
+ * find the change name. Skips the leading subcommand token(s).
13
+ *
14
+ * Special-cases `new`: its sub-verb (`change` | `spec`) sits between the
15
+ * subcommand and the name, so we skip 2 tokens for `openspec new change foo`
16
+ * but only 1 for `openspec status --change foo` / `openspec archive foo`.
17
+ */
18
+ export declare function extractChangeName(tokens: string[]): string | undefined;
19
+ /**
20
+ * Parse a bash command line.
21
+ * Returns null when the command is not an `openspec` invocation.
22
+ * Recognizes `openspec` anywhere in the pipeline (after `cd X &&`, etc.)
23
+ * so that worktree prefixes are honored.
24
+ */
25
+ export declare function parseBashCommand(cmd: string): ParsedBashCommand | null;
26
+ /**
27
+ * Extract the change name from a bash command line, when the command
28
+ * is an openspec invocation that locks a change (e.g. `openspec status
29
+ * --change foo` / `openspec new change foo`). Returns null for
30
+ * non-locking commands (list, doctor, …) and non-openspec commands.
31
+ */
32
+ export declare function findSpec(cmd: string): string | null;
33
+ /**
34
+ * Extract the worktree cwd from a bash command line, when the command
35
+ * runs inside a worktree (`cd <wt> && openspec …`). Returns null
36
+ * otherwise.
37
+ */
38
+ export declare function findWorkTree(cmd: string): string | null;
39
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAWpD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAExD;AAID;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAyC9C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAqBtE;AAmBD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAyBtE;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAInD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIvD"}
package/dist/parser.js ADDED
@@ -0,0 +1,162 @@
1
+ const LOCKING_SUBCOMMANDS = new Set([
2
+ "new", "status", "apply", "archive", "verify",
3
+ "sync", "instructions", "show", "validate", "context", "view",
4
+ ]);
5
+ // `openspec new` takes a depth-2 verb: `new change <name>` or `new spec <name>`.
6
+ // `new` alone (e.g. `openspec new --help`) has no name token.
7
+ const NEW_SUBVERBS = new Set(["change", "spec"]);
8
+ export function isLockingSubcommand(sub) {
9
+ return LOCKING_SUBCOMMANDS.has(sub);
10
+ }
11
+ const CONNECTORS = new Set(["&&", "||", "|", ";"]);
12
+ /**
13
+ * Split a bash command string into a flat token list.
14
+ * Supports quoted strings ("…" or '…') as single tokens.
15
+ * Honors && || | ; connectors by emitting them as separators (kept out).
16
+ */
17
+ export function tokenize(cmd) {
18
+ const tokens = [];
19
+ let buf = "";
20
+ let quote = null;
21
+ for (let i = 0; i < cmd.length; i++) {
22
+ const c = cmd[i];
23
+ if (quote) {
24
+ if (c === quote) {
25
+ quote = null;
26
+ }
27
+ else {
28
+ buf += c;
29
+ }
30
+ continue;
31
+ }
32
+ if (c === '"' || c === "'") {
33
+ quote = c;
34
+ continue;
35
+ }
36
+ if (/\s/.test(c)) {
37
+ if (buf) {
38
+ tokens.push(buf);
39
+ buf = "";
40
+ }
41
+ continue;
42
+ }
43
+ if (CONNECTORS.has(buf + c) || CONNECTORS.has(c)) {
44
+ if (buf) {
45
+ tokens.push(buf);
46
+ buf = "";
47
+ }
48
+ // Peek two-char connectors
49
+ const next = cmd[i + 1];
50
+ if ((c === "&" || c === "|") && next === c) {
51
+ i++; // skip second char
52
+ }
53
+ continue;
54
+ }
55
+ buf += c;
56
+ }
57
+ if (buf)
58
+ tokens.push(buf);
59
+ return tokens;
60
+ }
61
+ /**
62
+ * From a token list starting with the openspec subcommand
63
+ * (e.g. ["status", "--change", "add-foo", "--json"]),
64
+ * find the change name. Skips the leading subcommand token(s).
65
+ *
66
+ * Special-cases `new`: its sub-verb (`change` | `spec`) sits between the
67
+ * subcommand and the name, so we skip 2 tokens for `openspec new change foo`
68
+ * but only 1 for `openspec status --change foo` / `openspec archive foo`.
69
+ */
70
+ export function extractChangeName(tokens) {
71
+ if (tokens.length === 0)
72
+ return undefined;
73
+ let start = 1;
74
+ if (tokens[0] === "new" &&
75
+ tokens.length >= 2 &&
76
+ NEW_SUBVERBS.has(tokens[1])) {
77
+ start = 2;
78
+ }
79
+ for (let i = start; i < tokens.length; i++) {
80
+ if (tokens[i] === "--change") {
81
+ const v = tokens[i + 1];
82
+ if (v && !v.startsWith("--"))
83
+ return v;
84
+ continue;
85
+ }
86
+ if (tokens[i]?.startsWith("--"))
87
+ continue; // other flag
88
+ // first positional
89
+ if (tokens[i])
90
+ return tokens[i];
91
+ }
92
+ return undefined;
93
+ }
94
+ /**
95
+ * Find the last `cd <path>` in a token sequence and return the path.
96
+ * Walks all tokens looking for "cd" followed by a non-flag arg.
97
+ */
98
+ function lastCdTarget(tokens) {
99
+ let last;
100
+ for (let i = 0; i < tokens.length; i++) {
101
+ if (tokens[i] === "cd" && tokens[i + 1] && !tokens[i + 1].startsWith("--")) {
102
+ last = tokens[i + 1];
103
+ i++; // skip path
104
+ }
105
+ }
106
+ return last;
107
+ }
108
+ const WORKTREE_RE = /\.worktrees\/([^/\s]+)/;
109
+ /**
110
+ * Parse a bash command line.
111
+ * Returns null when the command is not an `openspec` invocation.
112
+ * Recognizes `openspec` anywhere in the pipeline (after `cd X &&`, etc.)
113
+ * so that worktree prefixes are honored.
114
+ */
115
+ export function parseBashCommand(cmd) {
116
+ const tokens = tokenize(cmd);
117
+ if (tokens.length === 0)
118
+ return null;
119
+ const openspecIdx = tokens.indexOf("openspec");
120
+ if (openspecIdx === -1)
121
+ return null;
122
+ const subcommand = tokens[openspecIdx + 1] ?? "";
123
+ // rest includes the subcommand itself; extractChangeName will skip it.
124
+ const rest = tokens.slice(openspecIdx + 1);
125
+ // Scan the prefix (everything before `openspec`) for the last `cd <path>`.
126
+ const cd = lastCdTarget(tokens.slice(0, openspecIdx));
127
+ const effectiveCwd = cd ?? "";
128
+ const isWorktree = !!effectiveCwd && WORKTREE_RE.test(effectiveCwd);
129
+ const isLocking = isLockingSubcommand(subcommand);
130
+ const changeName = isLocking ? extractChangeName(rest) : undefined;
131
+ return {
132
+ subcommand,
133
+ changeName,
134
+ effectiveCwd,
135
+ isWorktree,
136
+ isLocking,
137
+ };
138
+ }
139
+ /**
140
+ * Extract the change name from a bash command line, when the command
141
+ * is an openspec invocation that locks a change (e.g. `openspec status
142
+ * --change foo` / `openspec new change foo`). Returns null for
143
+ * non-locking commands (list, doctor, …) and non-openspec commands.
144
+ */
145
+ export function findSpec(cmd) {
146
+ const parsed = parseBashCommand(cmd);
147
+ if (!parsed)
148
+ return null;
149
+ return parsed.isLocking && parsed.changeName ? parsed.changeName : null;
150
+ }
151
+ /**
152
+ * Extract the worktree cwd from a bash command line, when the command
153
+ * runs inside a worktree (`cd <wt> && openspec …`). Returns null
154
+ * otherwise.
155
+ */
156
+ export function findWorkTree(cmd) {
157
+ const parsed = parseBashCommand(cmd);
158
+ if (!parsed)
159
+ return null;
160
+ return parsed.isWorktree ? parsed.effectiveCwd : null;
161
+ }
162
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.js","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { PersistedLock } from "./state.js";
3
+ import type { ArtifactStatus, MergedTasks } from "./types.js";
4
+ export declare const ARTIFACT_INITIALS: Record<ArtifactStatus["id"], string>;
5
+ export declare function formatArtifactTokens(statuses: ArtifactStatus[]): string;
6
+ export declare function formatProgressBar(done: number, total: number): string;
7
+ export declare function renderLine(name: string, schemaName: string, statuses: ArtifactStatus[], tasks: MergedTasks): string;
8
+ /**
9
+ * Base class for debounced status-bar renders.
10
+ *
11
+ * The constructor is intentionally side-effect free: creating a render
12
+ * instance must not publish anything to the status bar. Callers decide
13
+ * when the initial "no status" publish happens (see the session_start
14
+ * handler in index.ts). This matters for the lazy render path — a
15
+ * cancelled `/tui-openspec-select` must leave the status bar untouched.
16
+ */
17
+ export declare class StatusRender {
18
+ id: string;
19
+ ctx: ExtensionContext;
20
+ timer: ReturnType<typeof setTimeout> | undefined;
21
+ debounce: number;
22
+ lastRendered: string;
23
+ constructor(id: string, ctx: ExtensionContext, debounce?: number);
24
+ render(): Promise<void>;
25
+ refresh(): void;
26
+ renderText(): Promise<string>;
27
+ }
28
+ /**
29
+ * Renders the openspec change status bar line.
30
+ *
31
+ * Owns all tracking state (spec, worktree, manual lock) and the full
32
+ * multi-source render pipeline: probe `openspec/changes/<name>` in each
33
+ * source, query `openspec status --json` for the alive ones, merge
34
+ * artifacts + tasks from main and worktree, then emit the line.
35
+ *
36
+ * The optional `onStateChange` callback receives a full
37
+ * {@link PersistedLock} snapshot (or `null` when all sources are
38
+ * gone) at every authoritative state transition. Fires occur after
39
+ * the dedupe early-returns (i.e. only when state actually changed)
40
+ * and from the auto-unlock branch of `renderText()`. Consumers
41
+ * (e.g. the session-persistence layer wired in index.ts) use this
42
+ * to publish the live lock state to pi's custom-message stream.
43
+ */
44
+ export declare class OpenSpecStatusRender extends StatusRender {
45
+ spec?: string;
46
+ worktree?: string;
47
+ manualLock: boolean;
48
+ private readonly onStateChange;
49
+ constructor(id: string, ctx: ExtensionContext, debounce?: number, onStateChange?: (state: PersistedLock | null) => void);
50
+ /**
51
+ * Auto-lock from a bash `openspec` command. No-op while a manual
52
+ * lock (via /tui-openspec-select) is active.
53
+ */
54
+ setSpec(spec: string): void;
55
+ /**
56
+ * Adopt a worktree cwd (from `cd <worktree> && openspec ...`).
57
+ * Always applies — even under a manual lock the worktree is an
58
+ * additional scan source, not a change of the tracked spec.
59
+ */
60
+ setWorkTree(worktree: string): void;
61
+ /** Manual lock via /tui-openspec-select: pins the tracked change. */
62
+ lock(change: string): void;
63
+ /** Release the lock and clear the status bar (if a line is showing). */
64
+ clearLock(): void;
65
+ renderText(): Promise<string>;
66
+ }
67
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAGxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9D,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,MAAM,CAKlE,CAAC;AAMF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,MAAM,CAKvE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIrE;AAED,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,cAAc,EAAE,EAC1B,KAAK,EAAE,WAAW,GACjB,MAAM,CAQR;AAED;;;;;;;;GAQG;AACH,qBAAa,YAAY;IACvB,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,gBAAgB,CAAA;IACrB,KAAK,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,GAAG,SAAS,CAAA;IAChD,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,SAAK;gBAEL,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,gBAAgB,EAAE,QAAQ,SAAM;IAMvD,MAAM;IAWZ,OAAO;IAMD,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;CACpC;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,UAAS;IACnB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAEhB;gBAGZ,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,gBAAgB,EACrB,QAAQ,SAAM,EACd,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,KAAK,IAAI;IAMvD;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM;IAapB;;;;OAIG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM;IAkB5B,qEAAqE;IACrE,IAAI,CAAC,MAAM,EAAE,MAAM;IAYnB,wEAAwE;IACxE,SAAS;IAWM,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC;CA8D7C"}
package/dist/render.js ADDED
@@ -0,0 +1,210 @@
1
+ // src/render.ts
2
+ import { access } from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import { runOpenspecStatus } from "./openspec.js";
5
+ import { mergeStatusResults, readMergedTasks } from "./merge.js";
6
+ export const ARTIFACT_INITIALS = {
7
+ proposal: "P",
8
+ design: "D",
9
+ specs: "S",
10
+ tasks: "T",
11
+ };
12
+ const BAR_WIDTH = 10;
13
+ const FILLED = "█";
14
+ const EMPTY = "░";
15
+ export function formatArtifactTokens(statuses) {
16
+ return statuses
17
+ .filter((s) => s.id in ARTIFACT_INITIALS)
18
+ .map((s) => `${ARTIFACT_INITIALS[s.id]}${s.status === "done" ? "●" : "○"}`)
19
+ .join(" ");
20
+ }
21
+ export function formatProgressBar(done, total) {
22
+ const d = Math.max(0, Math.min(done, total));
23
+ const filledCells = total === 0 ? 0 : Math.round((d / total) * BAR_WIDTH);
24
+ return FILLED.repeat(filledCells) + EMPTY.repeat(BAR_WIDTH - filledCells);
25
+ }
26
+ export function renderLine(name, schemaName, statuses, tasks) {
27
+ return [
28
+ name,
29
+ `(${schemaName})`,
30
+ `[${formatArtifactTokens(statuses)}]`,
31
+ "Tasks:",
32
+ `${formatProgressBar(tasks.done, tasks.total)} ${tasks.done}/${tasks.total}`,
33
+ ].join(" ");
34
+ }
35
+ /**
36
+ * Base class for debounced status-bar renders.
37
+ *
38
+ * The constructor is intentionally side-effect free: creating a render
39
+ * instance must not publish anything to the status bar. Callers decide
40
+ * when the initial "no status" publish happens (see the session_start
41
+ * handler in index.ts). This matters for the lazy render path — a
42
+ * cancelled `/tui-openspec-select` must leave the status bar untouched.
43
+ */
44
+ export class StatusRender {
45
+ id;
46
+ ctx;
47
+ timer;
48
+ debounce;
49
+ lastRendered = "";
50
+ constructor(id, ctx, debounce = 500) {
51
+ this.id = id;
52
+ this.ctx = ctx;
53
+ this.debounce = debounce;
54
+ }
55
+ async render() {
56
+ this.timer = undefined;
57
+ const text = (await this.renderText()).trim();
58
+ if (text === this.lastRendered)
59
+ return;
60
+ this.lastRendered = text;
61
+ if (text.length > 0)
62
+ this.ctx.ui.setStatus(this.id, text);
63
+ else
64
+ this.ctx.ui.setStatus(this.id, undefined);
65
+ }
66
+ refresh() {
67
+ if (!this.timer)
68
+ // Arrow keeps `this` bound when the timer fires.
69
+ this.timer = setTimeout(() => this.render(), this.debounce);
70
+ }
71
+ async renderText() { return ""; }
72
+ }
73
+ /**
74
+ * Renders the openspec change status bar line.
75
+ *
76
+ * Owns all tracking state (spec, worktree, manual lock) and the full
77
+ * multi-source render pipeline: probe `openspec/changes/<name>` in each
78
+ * source, query `openspec status --json` for the alive ones, merge
79
+ * artifacts + tasks from main and worktree, then emit the line.
80
+ *
81
+ * The optional `onStateChange` callback receives a full
82
+ * {@link PersistedLock} snapshot (or `null` when all sources are
83
+ * gone) at every authoritative state transition. Fires occur after
84
+ * the dedupe early-returns (i.e. only when state actually changed)
85
+ * and from the auto-unlock branch of `renderText()`. Consumers
86
+ * (e.g. the session-persistence layer wired in index.ts) use this
87
+ * to publish the live lock state to pi's custom-message stream.
88
+ */
89
+ export class OpenSpecStatusRender extends StatusRender {
90
+ spec;
91
+ worktree;
92
+ manualLock = false;
93
+ onStateChange;
94
+ constructor(id, ctx, debounce = 500, onStateChange) {
95
+ super(id, ctx, debounce);
96
+ this.onStateChange = onStateChange;
97
+ }
98
+ /**
99
+ * Auto-lock from a bash `openspec` command. No-op while a manual
100
+ * lock (via /tui-openspec-select) is active.
101
+ */
102
+ setSpec(spec) {
103
+ if (this.manualLock)
104
+ return;
105
+ if (this.spec === spec)
106
+ return;
107
+ this.spec = spec;
108
+ this.onStateChange?.({
109
+ spec: this.spec,
110
+ worktree: this.worktree,
111
+ manualLock: false,
112
+ version: 1,
113
+ });
114
+ this.refresh();
115
+ }
116
+ /**
117
+ * Adopt a worktree cwd (from `cd <worktree> && openspec ...`).
118
+ * Always applies — even under a manual lock the worktree is an
119
+ * additional scan source, not a change of the tracked spec.
120
+ */
121
+ setWorkTree(worktree) {
122
+ if (this.worktree === worktree)
123
+ return;
124
+ this.worktree = worktree;
125
+ // A worktree change without a tracked spec has no persistable
126
+ // full lock snapshot (PersistedLock.spec is required); skip the
127
+ // fire. The restore path only calls setWorkTree when a spec is
128
+ // present, so no persisted state is lost.
129
+ if (this.spec) {
130
+ this.onStateChange?.({
131
+ spec: this.spec,
132
+ worktree: this.worktree,
133
+ manualLock: this.manualLock,
134
+ version: 1,
135
+ });
136
+ }
137
+ this.refresh();
138
+ }
139
+ /** Manual lock via /tui-openspec-select: pins the tracked change. */
140
+ lock(change) {
141
+ this.manualLock = true;
142
+ this.spec = change;
143
+ this.onStateChange?.({
144
+ spec: this.spec,
145
+ worktree: this.worktree,
146
+ manualLock: true,
147
+ version: 1,
148
+ });
149
+ this.refresh();
150
+ }
151
+ /** Release the lock and clear the status bar (if a line is showing). */
152
+ clearLock() {
153
+ this.manualLock = false;
154
+ this.spec = undefined;
155
+ this.worktree = "";
156
+ this.onStateChange?.(null);
157
+ if (this.lastRendered !== "") {
158
+ this.lastRendered = "";
159
+ this.ctx.ui.setStatus(this.id, undefined);
160
+ }
161
+ }
162
+ async renderText() {
163
+ const name = this.spec;
164
+ if (!name)
165
+ return "";
166
+ const mainCwd = this.ctx.cwd;
167
+ const wtCwd = this.worktree ?? "";
168
+ const sources = [{ cwd: mainCwd, kind: "main" }];
169
+ if (wtCwd)
170
+ sources.push({ cwd: wtCwd, kind: "worktree" });
171
+ // Probe each source's change folder in parallel. A missing folder
172
+ // is "skip this source", not "fail the render". Only when ALL
173
+ // sources are gone do we treat the change as fully archived and
174
+ // unlock.
175
+ const aliveFlags = await Promise.all(sources.map(async (s) => {
176
+ const dir = path.join(s.cwd, "openspec", "changes", name);
177
+ try {
178
+ await access(dir);
179
+ return true;
180
+ }
181
+ catch {
182
+ return false;
183
+ }
184
+ }));
185
+ const aliveSources = sources.filter((_, i) => aliveFlags[i]);
186
+ // Cleared or re-locked mid-render — never unlock or publish for a
187
+ // stale change.
188
+ if (this.spec !== name)
189
+ return "";
190
+ if (aliveSources.length === 0) {
191
+ // Fully archived (or all worktrees removed): release the lock.
192
+ this.spec = undefined;
193
+ this.worktree = "";
194
+ this.manualLock = false;
195
+ this.onStateChange?.(null);
196
+ return "";
197
+ }
198
+ // Query each alive source in parallel; merge artifacts/schema.
199
+ const [statusResults, tasks] = await Promise.all([
200
+ Promise.all(aliveSources.map((s) => runOpenspecStatus(name, s.cwd))),
201
+ readMergedTasks(name, mainCwd, wtCwd || undefined),
202
+ ]);
203
+ // Cleared or re-locked while the queries were in flight.
204
+ if (this.spec !== name)
205
+ return "";
206
+ const status = mergeStatusResults(statusResults);
207
+ return renderLine(name, status?.schemaName || "spec-driven", (status?.artifacts ?? []), tasks);
208
+ }
209
+ }
210
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,gBAAgB;AAChB,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAIjE,MAAM,CAAC,MAAM,iBAAiB,GAAyC;IACrE,QAAQ,EAAE,GAAG;IACb,MAAM,EAAE,GAAG;IACX,KAAK,EAAE,GAAG;IACV,KAAK,EAAE,GAAG;CACX,CAAC;AAEF,MAAM,SAAS,GAAG,EAAE,CAAC;AACrB,MAAM,MAAM,GAAG,GAAG,CAAC;AACnB,MAAM,KAAK,GAAG,GAAG,CAAC;AAElB,MAAM,UAAU,oBAAoB,CAAC,QAA0B;IAC7D,OAAO,QAAQ;SACZ,MAAM,CAAC,CAAC,CAAC,EAAuB,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,iBAAiB,CAAC;SAC7D,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;SAC1E,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,KAAa;IAC3D,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;IAC7C,MAAM,WAAW,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,SAAS,CAAC,CAAC;IAC1E,OAAO,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,SAAS,GAAG,WAAW,CAAC,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,UAAU,CACxB,IAAY,EACZ,UAAkB,EAClB,QAA0B,EAC1B,KAAkB;IAElB,OAAO;QACL,IAAI;QACJ,IAAI,UAAU,GAAG;QACjB,IAAI,oBAAoB,CAAC,QAAQ,CAAC,GAAG;QACrC,QAAQ;QACR,GAAG,iBAAiB,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,KAAK,EAAE;KAC7E,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,OAAO,YAAY;IACvB,EAAE,CAAQ;IACV,GAAG,CAAkB;IACrB,KAAK,CAA2C;IAChD,QAAQ,CAAQ;IAChB,YAAY,GAAG,EAAE,CAAA;IAEjB,YAAY,EAAU,EAAE,GAAqB,EAAE,QAAQ,GAAG,GAAG;QAC3D,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,MAAM;QACV,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;QACvB,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC9C,IAAI,IAAI,KAAK,IAAI,CAAC,YAAY;YAAE,OAAO;QACvC,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;QACzB,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;YACjB,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;;YAErC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;IAC9C,CAAC;IAED,OAAO;QACL,IAAI,CAAC,IAAI,CAAC,KAAK;YACb,iDAAiD;YACjD,IAAI,CAAC,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IAChE,CAAC;IAED,KAAK,CAAC,UAAU,KAAsB,OAAO,EAAE,CAAC,CAAC,CAAC;CACnD;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,OAAO,oBAAqB,SAAQ,YAAY;IACpD,IAAI,CAAU;IACd,QAAQ,CAAU;IAClB,UAAU,GAAG,KAAK,CAAC;IACF,aAAa,CAEhB;IAEd,YACE,EAAU,EACV,GAAqB,EACrB,QAAQ,GAAG,GAAG,EACd,aAAqD;QAErD,KAAK,CAAC,EAAE,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;QACzB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;IACrC,CAAC;IAED;;;OAGG;IACH,OAAO,CAAC,IAAY;QAClB,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QAC5B,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI;YAAE,OAAO;QAC/B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,UAAU,EAAE,KAAK;YACjB,OAAO,EAAE,CAAC;SACX,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;IAED;;;;OAIG;IACH,WAAW,CAAC,QAAgB;QAC1B,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ;YAAE,OAAO;QACvC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,8DAA8D;QAC9D,gEAAgE;QAChE,+DAA+D;QAC/D,0CAA0C;QAC1C,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,IAAI,CAAC,aAAa,EAAE,CAAC;gBACnB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,OAAO,EAAE,CAAC;aACX,CAAC,CAAC;QACL,CAAC;QACD,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;IAED,qEAAqE;IACrE,IAAI,CAAC,MAAc;QACjB,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC;QACnB,IAAI,CAAC,aAAa,EAAE,CAAC;YACnB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE,CAAC;SACX,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;IAED,wEAAwE;IACxE,SAAS;QACP,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,SAAS,CAAC;QACtB,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;QACnB,IAAI,CAAC,aAAa,EAAE,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,IAAI,CAAC,YAAY,KAAK,EAAE,EAAE,CAAC;YAC7B,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC;YACvB,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;QAC5C,CAAC;IACH,CAAC;IAEQ,KAAK,CAAC,UAAU;QACvB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACvB,IAAI,CAAC,IAAI;YAAE,OAAO,EAAE,CAAC;QAErB,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC;QAOlC,MAAM,OAAO,GAAa,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QAC3D,IAAI,KAAK;YAAE,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;QAE1D,kEAAkE;QAClE,8DAA8D;QAC9D,gEAAgE;QAChE,UAAU;QACV,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,GAAG,CAClC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;YACtB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;YAC1D,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC;gBAClB,OAAO,IAAI,CAAC;YACd,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC,CAAC,CACH,CAAC;QACF,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;QAE7D,kEAAkE;QAClE,gBAAgB;QAChB,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI;YAAE,OAAO,EAAE,CAAC;QAElC,IAAI,YAAY,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC9B,+DAA+D;YAC/D,IAAI,CAAC,IAAI,GAAG,SAAS,CAAC;YACtB,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;YACnB,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;YACxB,IAAI,CAAC,aAAa,EAAE,CAAC,IAAI,CAAC,CAAC;YAC3B,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,+DAA+D;QAC/D,MAAM,CAAC,aAAa,EAAE,KAAK,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAC/C,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YACpE,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,SAAS,CAAC;SACnD,CAAC,CAAC;QAEH,yDAAyD;QACzD,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI;YAAE,OAAO,EAAE,CAAC;QAElC,MAAM,MAAM,GAAG,kBAAkB,CAAC,aAAa,CAAC,CAAC;QACjD,OAAO,UAAU,CACf,IAAI,EACH,MAAM,EAAE,UAAqB,IAAI,aAAa,EAC/C,CAAC,MAAM,EAAE,SAAS,IAAI,EAAE,CAAU,EAClC,KAAK,CACN,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,10 @@
1
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ export interface PersistedLock {
3
+ spec: string;
4
+ worktree?: string;
5
+ manualLock: boolean;
6
+ version: 1;
7
+ }
8
+ export declare const LOCK_CUSTOM_TYPE = "pi-tui-openspec-status";
9
+ export declare function findLastPersistedLock(entries: SessionEntry[]): PersistedLock | null;
10
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAEpE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,OAAO,CAAC;IACpB,OAAO,EAAE,CAAC,CAAC;CACZ;AAED,eAAO,MAAM,gBAAgB,2BAA2B,CAAC;AAQzD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,aAAa,GAAG,IAAI,CAWnF"}
package/dist/state.js ADDED
@@ -0,0 +1,22 @@
1
+ export const LOCK_CUSTOM_TYPE = "pi-tui-openspec-status";
2
+ function isPersistedLock(data) {
3
+ if (typeof data !== "object" || data === null)
4
+ return false;
5
+ const d = data;
6
+ return d.version === 1 && typeof d.spec === "string" && typeof d.manualLock === "boolean";
7
+ }
8
+ export function findLastPersistedLock(entries) {
9
+ for (let i = entries.length - 1; i >= 0; i--) {
10
+ const e = entries[i];
11
+ if (!e)
12
+ continue;
13
+ if (e.type === "custom" && e.customType === LOCK_CUSTOM_TYPE) {
14
+ const data = e.data;
15
+ if (isPersistedLock(data))
16
+ return data;
17
+ return null; // latest matching entry is dirty — stop, fall back to empty
18
+ }
19
+ }
20
+ return null;
21
+ }
22
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAUA,MAAM,CAAC,MAAM,gBAAgB,GAAG,wBAAwB,CAAC;AAEzD,SAAS,eAAe,CAAC,IAAa;IACpC,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC5D,MAAM,CAAC,GAAG,IAA+B,CAAC;IAC1C,OAAO,CAAC,CAAC,OAAO,KAAK,CAAC,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,UAAU,KAAK,SAAS,CAAC;AAC5F,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,OAAuB;IAC3D,KAAK,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7C,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QACrB,IAAI,CAAC,CAAC;YAAE,SAAS;QACjB,IAAI,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,UAAU,KAAK,gBAAgB,EAAE,CAAC;YAC7D,MAAM,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC;YACpB,IAAI,eAAe,CAAC,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC;YACvC,OAAO,IAAI,CAAC,CAAC,4DAA4D;QAC3E,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,22 @@
1
+ export interface ArtifactStatus {
2
+ id: "proposal" | "design" | "specs" | "tasks";
3
+ status: "done" | "ready" | "blocked" | "skipped";
4
+ }
5
+ export interface StatusJson {
6
+ artifacts?: ArtifactStatus[];
7
+ applied?: boolean;
8
+ schemaName?: string;
9
+ [key: string]: unknown;
10
+ }
11
+ export interface ParsedBashCommand {
12
+ subcommand: string;
13
+ changeName?: string;
14
+ effectiveCwd: string;
15
+ isWorktree: boolean;
16
+ isLocking: boolean;
17
+ }
18
+ export interface MergedTasks {
19
+ done: number;
20
+ total: number;
21
+ }
22
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;IAC9C,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;CAClD;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@philogag/pi-tui-openspec-status",
3
+ "version": "0.1.0",
4
+ "description": "pi extension: single-line status bar tracking openspec status",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "keywords": [
18
+ "pi",
19
+ "extension",
20
+ "tui",
21
+ "openspec"
22
+ ],
23
+ "dependencies": {},
24
+ "peerDependencies": {
25
+ "@earendil-works/pi-coding-agent": "^0.84.3",
26
+ "@earendil-works/pi-tui": "latest",
27
+ "typebox": "latest"
28
+ },
29
+ "devDependencies": {
30
+ "@earendil-works/pi-coding-agent": "^0.84.3",
31
+ "@earendil-works/pi-tui": "latest",
32
+ "typebox": "latest",
33
+ "typescript": "^5.6.3",
34
+ "vitest": "latest"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc -b",
38
+ "typecheck": "tsc -b --noEmit",
39
+ "test": "vitest run",
40
+ "lint": "echo \"no linter configured\""
41
+ }
42
+ }