@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 +112 -0
- package/dist/discover.d.ts +8 -0
- package/dist/discover.d.ts.map +1 -0
- package/dist/discover.js +24 -0
- package/dist/discover.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +136 -0
- package/dist/index.js.map +1 -0
- package/dist/merge.d.ts +30 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +134 -0
- package/dist/merge.js.map +1 -0
- package/dist/openspec.d.ts +4 -0
- package/dist/openspec.d.ts.map +1 -0
- package/dist/openspec.js +53 -0
- package/dist/openspec.js.map +1 -0
- package/dist/parser.d.ts +39 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +162 -0
- package/dist/parser.js.map +1 -0
- package/dist/render.d.ts +67 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +210 -0
- package/dist/render.js.map +1 -0
- package/dist/state.d.ts +10 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +22 -0
- package/dist/state.js.map +1 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +42 -0
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"}
|
package/dist/discover.js
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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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"}
|
package/dist/merge.d.ts
ADDED
|
@@ -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":"AAAA,eAAe;AACf,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAGlC,MAAM,UAAU,GAAG,+BAA+B,CAAC;AACnD,MAAM,YAAY,GAAG,0BAA0B,CAAC;AAChD,MAAM,WAAW,GAAG,4BAA4B,CAAC;AAEjD,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,MAAM,GAAG,GAAG,IAAI,GAAG,EAAmB,CAAC;IACvC,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACvC,IAAI,CAAyB,CAAC;QAC9B,IAAI,CAAC,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAChC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAE,EAAE,IAAI,CAAC,CAAC;YACrB,SAAS;QACX,CAAC;QACD,IAAI,CAAC,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YAClC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAE,EAAE,KAAK,CAAC,CAAC;YACtB,SAAS;QACX,CAAC;QACD,IAAI,CAAC,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;YACjC,gEAAgE;YAChE,+CAA+C;YAC/C,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC;gBAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAE,EAAE,KAAK,CAAC,CAAC;QAC7C,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,UAAU,CACxB,IAA0B,EAC1B,QAA8B;IAE9B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAS,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,EAAE,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACnE,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;QACrB,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI;YAAE,IAAI,EAAE,CAAC;IAC/D,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;AACpC,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,YAAY,CAAC,CAAS;IACnC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACvC,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,GAAG,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,UAAkB,EAClB,YAAoB,EACpB,WAAoB;IAEpB,MAAM,OAAO,GAAG,MAAM,YAAY,CAChC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,CAAC,CACvE,CAAC;IACF,MAAM,KAAK,GAAG,WAAW;QACvB,CAAC,CAAC,MAAM,YAAY,CAChB,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,CAAC,CACtE;QACH,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;IACd,OAAO,UAAU,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;AACpC,CAAC;AAED,+CAA+C;AAC/C,MAAM,cAAc,GAA2B;IAC7C,UAAU;IACV,QAAQ;IACR,OAAO;IACP,OAAO;CACR,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAAsE;IAEtE,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkD,CAAC;IACtE,MAAM,UAAU,GAA2B,CAAC,GAAG,cAAc,CAAC,CAAC;IAE/D,2CAA2C;IAC3C,KAAK,MAAM,EAAE,IAAI,cAAc,EAAE,CAAC;QAChC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,IAAI;gBAAE,SAAS;YACpB,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;YAC1C,IAAI,CAAC,GAAG;gBAAE,SAAS;YACnB,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACxB,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM;gBAAE,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IAED,qDAAqD;IACrD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACrB,IAAI,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;gBAAE,SAAS;YACxC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACtB,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC1B,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM;gBAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IAED,OAAO,UAAU;SACd,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;SAC3B,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,CAAE,EAAE,CAAC,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAChC,OAAyC;IAEzC,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAmB,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC;IACjE,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACpC,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,UAAU,KAAK,QAAQ,CAAC;QACpE,EAAE,UAAU,CAAC;IACf,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC;IACtD,OAAO;QACL,UAAU;QACV,OAAO;QACP,SAAS,EAAE,qBAAqB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;KAChE,CAAC;AACJ,CAAC"}
|
|
@@ -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"}
|
package/dist/openspec.js
ADDED
|
@@ -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"}
|
package/dist/parser.d.ts
ADDED
|
@@ -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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"}
|
package/dist/render.d.ts
ADDED
|
@@ -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":"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"}
|
package/dist/state.d.ts
ADDED
|
@@ -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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|