super-backlog 0.10.1 → 1.0.3
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 +4 -5
- package/dist/cli.js +6 -15
- package/dist/commands/backlog-alias.js +1 -4
- package/dist/commands/dashboard.js +34 -36
- package/dist/commands/init.js +1 -7
- package/dist/commands/serve.js +2 -69
- package/dist/commands/uninstall.js +3 -6
- package/dist/commands/update.js +2 -16
- package/dist/dashboard/data.js +1 -1
- package/dist/dashboard/server.js +8 -3
- package/dist/init/execute.js +1 -32
- package/dist/init/planner.js +0 -4
- package/dist/lib/preflight.js +4 -13
- package/dist/lib/run.js +2 -5
- package/dist/templates/dashboard.html +28 -11
- package/package.json +6 -1
- package/dist/dashboard/regen.js +0 -28
package/README.md
CHANGED
|
@@ -71,11 +71,10 @@ sbl dashboard --serve # live Project Dashboard on http://localhost:6
|
|
|
71
71
|
| `.opencode/skill/<skill>/SKILL.md` (3 glue skills) | skill templates | fingerprint header line |
|
|
72
72
|
| `.claude/skills/<skill>/SKILL.md` | same templates | fingerprint header line |
|
|
73
73
|
| `package.json` scripts | `tasks` → `backlog task list`, `board` → `backlog board`, `browser` → `backlog browser`, `dashboard` → `super-backlog dashboard` (never overwrite existing values) | merged, add-only-if-absent |
|
|
74
|
-
| `dashboard.html` | generated Project Dashboard |
|
|
75
|
-
| `.git/hooks/post-commit` | dashboard freshness block — regenerates `dashboard.html` after commits that touch `backlog/` (default; opt out with `--no-refresh-hook`) | appended marker block |
|
|
74
|
+
| `dashboard.html` | generated Project Dashboard | not installed in user projects; generated on demand by `sbl dashboard` |
|
|
76
75
|
| `.git/hooks/pre-commit` | integrity guard hook — only with `--guard` (opt-in) | appended marker block |
|
|
77
76
|
|
|
78
|
-
Commands: `sbl init` · `sbl models` · `sbl doctor` · `sbl uninstall [--with-backlog]` · `sbl update` · `sbl dashboard [--
|
|
77
|
+
Commands: `sbl init` · `sbl models` · `sbl doctor` · `sbl uninstall [--with-backlog]` · `sbl update` · `sbl dashboard [--port <n>] [--no-open]` · `sbl serve [--port <n>] [--no-open]`. See `sbl help` for every flag.
|
|
79
78
|
|
|
80
79
|
## Model router (opt-in)
|
|
81
80
|
|
|
@@ -99,11 +98,11 @@ The router is fully owned by super-backlog and removed by `sbl uninstall`. See t
|
|
|
99
98
|
|
|
100
99
|
## Project Dashboard
|
|
101
100
|
|
|
102
|
-
`sbl dashboard`
|
|
101
|
+
`sbl dashboard` starts a local server that serves a dark, HTS-style cockpit rendered from your Backlog data in seven sections — Board & Quick Actions, Status (donut), Milestones, Tasks (sortable/filterable table; click a row to open a modal detail view with acceptance criteria and dependencies), Feature Cycle (pipeline stepper plus an Up Next / Blocked flow view from task dependencies), Activity (30-day sparkline), and Decisions & Docs. Glossary tooltips explain domain terms inline; extend or override them project-wide via `backlog/docs/glossary.md` (`## Term` heading plus the text below it). No CDNs, no external fonts — works offline when served locally. The server watches `backlog/`, regenerates on change, and serves on port `6428`; connected browser tabs reload automatically via Server-Sent Events. `sbl serve` is a deprecated alias that behaves identically. `sbl dashboard` also launches the Backlog browser alongside the server so you can edit tasks while the dashboard updates.
|
|
103
102
|
|
|
104
103
|
### Keeping it fresh
|
|
105
104
|
|
|
106
|
-
|
|
105
|
+
Run `sbl dashboard` whenever you want a live view of the board. The server regenerates the dashboard while it runs; stop it with `Ctrl+C`. There is no static `dashboard.html` installed in your project.
|
|
107
106
|
|
|
108
107
|

|
|
109
108
|
|
package/dist/cli.js
CHANGED
|
@@ -19,21 +19,19 @@ Commands:
|
|
|
19
19
|
init Install the kit into the current project
|
|
20
20
|
uninstall Remove kit-managed files (project data kept unless --with-backlog)
|
|
21
21
|
update Refresh kit-managed files and report upstream versions
|
|
22
|
-
dashboard
|
|
23
|
-
serve
|
|
22
|
+
dashboard Start the project dashboard server (live-reload + Backlog browser)
|
|
23
|
+
serve Deprecated alias for 'sbl dashboard'
|
|
24
24
|
browser Open the Backlog.md browser (delegates to backlog browser)
|
|
25
25
|
board Show the Backlog.md board (delegates to backlog board)
|
|
26
26
|
models Manage the model router (show, enable, disable, discover)
|
|
27
27
|
doctor Check the environment (node, PowerShell policy, backlog CLI)
|
|
28
28
|
|
|
29
|
-
init options:
|
|
29
|
+
init options:
|
|
30
30
|
--pm <auto|npm|pnpm|bun|skip> Package manager to use (default: auto)
|
|
31
31
|
--harness <opencode|claude> Target harness; repeatable or comma-separated (default: both)
|
|
32
32
|
--guard Install the integrity pre-commit hook (opt-in)
|
|
33
33
|
--models Install the model router config during init (opt-in)
|
|
34
34
|
--no-models Explicitly opt out of the model router
|
|
35
|
-
--no-dashboard Skip generating the project dashboard
|
|
36
|
-
--no-refresh-hook Skip the post-commit dashboard freshness hook
|
|
37
35
|
--fix-all Repair environment problems automatically (no prompts)
|
|
38
36
|
--dry-run Show what would be done without writing anything
|
|
39
37
|
|
|
@@ -44,11 +42,9 @@ uninstall options:
|
|
|
44
42
|
update options:
|
|
45
43
|
(none) Refreshes injected files, skills, hook; prints upstream versions
|
|
46
44
|
|
|
47
|
-
dashboard options:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
--no-open With --serve: do not open the browser automatically
|
|
51
|
-
--out <file> Output file name or path (default: dashboard.html)
|
|
45
|
+
dashboard options:
|
|
46
|
+
--port <n> Port for the dashboard server (default: 6428)
|
|
47
|
+
--no-open Do not open the dashboard browser automatically
|
|
52
48
|
|
|
53
49
|
serve options:
|
|
54
50
|
--port <n> Port for the dashboard server (default: 6428)
|
|
@@ -84,8 +80,6 @@ async function main(argv) {
|
|
|
84
80
|
guard: { type: 'boolean' },
|
|
85
81
|
models: { type: 'boolean' },
|
|
86
82
|
'no-models': { type: 'boolean' },
|
|
87
|
-
'no-dashboard': { type: 'boolean' },
|
|
88
|
-
'no-refresh-hook': { type: 'boolean' },
|
|
89
83
|
'fix-all': { type: 'boolean' },
|
|
90
84
|
'dry-run': { type: 'boolean' },
|
|
91
85
|
},
|
|
@@ -118,10 +112,8 @@ async function main(argv) {
|
|
|
118
112
|
args: rest,
|
|
119
113
|
allowPositionals: true,
|
|
120
114
|
options: {
|
|
121
|
-
serve: { type: 'boolean' },
|
|
122
115
|
port: { type: 'string' },
|
|
123
116
|
'no-open': { type: 'boolean' },
|
|
124
|
-
out: { type: 'string' },
|
|
125
117
|
},
|
|
126
118
|
});
|
|
127
119
|
return await runDashboard(process.cwd(), {
|
|
@@ -136,7 +128,6 @@ async function main(argv) {
|
|
|
136
128
|
options: {
|
|
137
129
|
port: { type: 'string' },
|
|
138
130
|
'no-open': { type: 'boolean' },
|
|
139
|
-
out: { type: 'string' },
|
|
140
131
|
},
|
|
141
132
|
});
|
|
142
133
|
return await runServe(process.cwd(), {
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { spawn } from 'node:child_process';
|
|
3
|
-
import process from 'node:process';
|
|
1
|
+
import spawn from 'cross-spawn';
|
|
4
2
|
import { resolveBacklogBin } from '../lib/run.js';
|
|
5
3
|
/** Run a backlog.md subcommand by delegating to the resolved backlog binary. */
|
|
6
4
|
export function runBacklogSubcommand(cwd, subcommand, args = []) {
|
|
@@ -13,7 +11,6 @@ export function runBacklogSubcommand(cwd, subcommand, args = []) {
|
|
|
13
11
|
const child = spawn(bin, [subcommand, ...args], {
|
|
14
12
|
cwd,
|
|
15
13
|
stdio: 'inherit',
|
|
16
|
-
shell: process.platform === 'win32',
|
|
17
14
|
});
|
|
18
15
|
child.on('error', () => resolve(1));
|
|
19
16
|
child.on('exit', (code) => resolve(code ?? 1));
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
import { tmpdir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import spawn from 'cross-spawn';
|
|
3
4
|
import { collectDashboardData } from '../dashboard/data.js';
|
|
4
5
|
import { renderDashboard } from '../dashboard/render.js';
|
|
5
6
|
import { DASHBOARD_PORT, startServeServer } from '../dashboard/server.js';
|
|
6
7
|
import { atomicWrite } from '../lib/atomic.js';
|
|
8
|
+
import { resolveBacklogBin } from '../lib/run.js';
|
|
7
9
|
import { KIT_VERSION } from '../lib/version.js';
|
|
8
10
|
async function regenerateInto(outPath, cwd) {
|
|
9
11
|
const data = collectDashboardData(cwd, { kitVersion: KIT_VERSION });
|
|
10
12
|
atomicWrite(outPath, renderDashboard(data));
|
|
11
13
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const mod = (await import(specifier));
|
|
24
|
-
if (typeof mod.startServeServer !== 'function') {
|
|
25
|
-
throw new Error('serve module does not export startServeServer');
|
|
26
|
-
}
|
|
27
|
-
await mod.startServeServer(cwd, {
|
|
28
|
-
port: DASHBOARD_PORT,
|
|
29
|
-
regenerate: () => regenerateInto(outPath, cwd),
|
|
30
|
-
openBrowser: true,
|
|
14
|
+
function spawnBacklogBrowser(cwd) {
|
|
15
|
+
const bin = resolveBacklogBin(cwd);
|
|
16
|
+
if (!bin) {
|
|
17
|
+
console.warn('warning: backlog CLI not found; dashboard will serve without the Backlog browser');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const child = spawn(bin, ['browser', '--no-open', '--non-interactive'], {
|
|
22
|
+
cwd,
|
|
23
|
+
detached: true,
|
|
24
|
+
stdio: 'ignore',
|
|
31
25
|
});
|
|
26
|
+
child.on('error', () => { });
|
|
27
|
+
child.unref();
|
|
28
|
+
console.log('started Backlog browser (dashboard still serves if browser fails)');
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
console.warn('warning: failed to start Backlog browser; dashboard still serves');
|
|
32
32
|
}
|
|
33
|
-
return outPath;
|
|
34
33
|
}
|
|
35
|
-
/** CLI entry for `sbl dashboard [--
|
|
34
|
+
/** CLI entry for `sbl dashboard [--port N] [--no-open]`. Starts a local server
|
|
35
|
+
* that watches backlog/ and regenerates a temp dashboard file on changes. */
|
|
36
36
|
export async function runDashboard(cwd, args) {
|
|
37
37
|
const values = args.values;
|
|
38
38
|
let port = DASHBOARD_PORT;
|
|
@@ -44,26 +44,24 @@ export async function runDashboard(cwd, args) {
|
|
|
44
44
|
}
|
|
45
45
|
port = parsed;
|
|
46
46
|
}
|
|
47
|
-
const serve = values['serve'] === true;
|
|
48
47
|
const noOpen = values['no-open'] === true;
|
|
49
|
-
const
|
|
50
|
-
const outPath = isAbsolute(outFile) ? outFile : resolve(cwd, outFile);
|
|
48
|
+
const outPath = join(tmpdir(), `sbl-dashboard-${Date.now()}.html`);
|
|
51
49
|
try {
|
|
52
50
|
await regenerateInto(outPath, cwd);
|
|
53
51
|
console.log(`dashboard written: ${outPath}`);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
52
|
+
console.log(`serving dashboard at http://127.0.0.1:${port}/ (press Ctrl+C to stop)`);
|
|
53
|
+
// Start backlog browser in parallel; don't await so the dashboard server can listen immediately.
|
|
54
|
+
spawnBacklogBrowser(cwd);
|
|
55
|
+
await startServeServer(cwd, {
|
|
56
|
+
port,
|
|
57
|
+
file: outPath,
|
|
58
|
+
regenerate: () => regenerateInto(outPath, cwd),
|
|
59
|
+
openBrowser: !noOpen,
|
|
60
|
+
});
|
|
63
61
|
return 0;
|
|
64
62
|
}
|
|
65
63
|
catch (err) {
|
|
66
|
-
console.error(`error: dashboard
|
|
64
|
+
console.error(`error: dashboard serve failed (${err instanceof Error ? err.message : String(err)})`);
|
|
67
65
|
return 1;
|
|
68
66
|
}
|
|
69
67
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -34,10 +34,6 @@ function describeAction(action) {
|
|
|
34
34
|
return 'install-model-router .super-backlog/models.json';
|
|
35
35
|
case 'install-guard-hook':
|
|
36
36
|
return 'install-guard-hook .git/hooks/pre-commit';
|
|
37
|
-
case 'install-refresh-hook':
|
|
38
|
-
return 'install-refresh-hook .git/hooks/post-commit';
|
|
39
|
-
case 'generate-dashboard':
|
|
40
|
-
return 'generate-dashboard';
|
|
41
37
|
case 'write':
|
|
42
38
|
return `write ${action.path}`;
|
|
43
39
|
}
|
|
@@ -96,8 +92,6 @@ export async function runInit(cwd, args, deps = {}) {
|
|
|
96
92
|
}
|
|
97
93
|
const pm = rawPm;
|
|
98
94
|
const guard = args.values.guard === true; // opt-in per spec D8
|
|
99
|
-
const dashboard = args.values['no-dashboard'] !== true;
|
|
100
|
-
const refreshHook = args.values['no-refresh-hook'] !== true; // default on, opt-out flag
|
|
101
95
|
const dryRun = args.values['dry-run'] === true;
|
|
102
96
|
const models = args.values.models === true ? true : args.values['no-models'] === true ? false : undefined;
|
|
103
97
|
const projectName = args.positionals[0] ?? basename(resolve(cwd));
|
|
@@ -121,7 +115,7 @@ export async function runInit(cwd, args, deps = {}) {
|
|
|
121
115
|
opencodeConfig,
|
|
122
116
|
pkgExists: existsSync(join(cwd, 'package.json')),
|
|
123
117
|
};
|
|
124
|
-
const opts = { projectName, harnesses, pm, guard,
|
|
118
|
+
const opts = { projectName, harnesses, pm, guard, skipInstall: false, models };
|
|
125
119
|
const plan = planInit(state, opts, KIT_VERSION);
|
|
126
120
|
if (dryRun) {
|
|
127
121
|
console.log(`dry-run for "${projectName}": ${plan.actions.length} action(s) planned, nothing written`);
|
package/dist/commands/serve.js
CHANGED
|
@@ -1,70 +1,3 @@
|
|
|
1
1
|
// src/commands/serve.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import process from 'node:process';
|
|
5
|
-
import { collectDashboardData } from '../dashboard/data.js';
|
|
6
|
-
import { renderDashboard } from '../dashboard/render.js';
|
|
7
|
-
import { DASHBOARD_PORT, startServeServer } from '../dashboard/server.js';
|
|
8
|
-
import { atomicWrite } from '../lib/atomic.js';
|
|
9
|
-
import { resolveBacklogBin } from '../lib/run.js';
|
|
10
|
-
import { KIT_VERSION } from '../lib/version.js';
|
|
11
|
-
async function regenerateInto(outPath, cwd) {
|
|
12
|
-
const data = collectDashboardData(cwd, { kitVersion: KIT_VERSION });
|
|
13
|
-
atomicWrite(outPath, renderDashboard(data));
|
|
14
|
-
}
|
|
15
|
-
function spawnBacklogBrowser(cwd, port) {
|
|
16
|
-
const bin = resolveBacklogBin(cwd);
|
|
17
|
-
if (!bin) {
|
|
18
|
-
console.warn('warning: backlog CLI not found; dashboard will serve without the Backlog browser');
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
const url = `http://127.0.0.1:${port}/`;
|
|
22
|
-
try {
|
|
23
|
-
const child = spawn(bin, ['browser', '--no-open', '--non-interactive'], {
|
|
24
|
-
cwd,
|
|
25
|
-
detached: true,
|
|
26
|
-
stdio: 'ignore',
|
|
27
|
-
shell: process.platform === 'win32',
|
|
28
|
-
});
|
|
29
|
-
child.on('error', () => { });
|
|
30
|
-
child.unref();
|
|
31
|
-
console.log('started Backlog browser (dashboard still serves if browser fails)');
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
console.warn('warning: failed to start Backlog browser; dashboard still serves');
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
/** CLI entry for `sbl serve [--port N] [--no-open]`. */
|
|
38
|
-
export async function runServe(cwd, args) {
|
|
39
|
-
const values = args.values;
|
|
40
|
-
let port = DASHBOARD_PORT;
|
|
41
|
-
if (values['port'] !== undefined) {
|
|
42
|
-
const parsed = Number(values['port']);
|
|
43
|
-
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
44
|
-
console.error(`error: invalid --port "${String(values['port'])}" (expected 0-65535)`);
|
|
45
|
-
return 1;
|
|
46
|
-
}
|
|
47
|
-
port = parsed;
|
|
48
|
-
}
|
|
49
|
-
const noOpen = values['no-open'] === true;
|
|
50
|
-
const outFile = values['out'] === undefined ? 'dashboard.html' : String(values['out']);
|
|
51
|
-
const outPath = isAbsolute(outFile) ? outFile : resolve(cwd, outFile);
|
|
52
|
-
try {
|
|
53
|
-
await regenerateInto(outPath, cwd);
|
|
54
|
-
console.log(`dashboard written: ${outPath}`);
|
|
55
|
-
console.log(`serving dashboard at http://127.0.0.1:${port}/ (press Ctrl+C to stop)`);
|
|
56
|
-
// Start backlog browser in parallel; don't await so the dashboard server can listen immediately.
|
|
57
|
-
spawnBacklogBrowser(cwd, port);
|
|
58
|
-
await startServeServer(cwd, {
|
|
59
|
-
port,
|
|
60
|
-
file: outPath,
|
|
61
|
-
regenerate: () => regenerateInto(outPath, cwd),
|
|
62
|
-
openBrowser: !noOpen,
|
|
63
|
-
});
|
|
64
|
-
return 0;
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
console.error(`error: serve failed (${err instanceof Error ? err.message : String(err)})`);
|
|
68
|
-
return 1;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
2
|
+
// Deprecated alias: `sbl serve` behaves exactly like `sbl dashboard`.
|
|
3
|
+
export { runDashboard as runServe } from './dashboard.js';
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
1
|
+
import spawn from 'cross-spawn';
|
|
3
2
|
import { existsSync, readFileSync, rmSync } from 'node:fs';
|
|
4
3
|
import { join } from 'node:path';
|
|
5
|
-
import process from 'node:process';
|
|
6
4
|
import { findGitDir, POINTER_HEADING_RE } from '../init/execute.js';
|
|
7
5
|
import { atomicWrite } from '../lib/atomic.js';
|
|
8
6
|
import { GUARD_RE, REFRESH_RE, removeGuardHook, removeRefreshHook } from '../lib/hooks.js';
|
|
@@ -195,11 +193,10 @@ export function verifyRemnants(cwd) {
|
|
|
195
193
|
return remnants;
|
|
196
194
|
}
|
|
197
195
|
function defaultRemoveGlobal() {
|
|
198
|
-
const cmd =
|
|
199
|
-
const r =
|
|
196
|
+
const cmd = 'npm';
|
|
197
|
+
const r = spawn.sync(cmd, ['uninstall', '-g', 'super-backlog'], {
|
|
200
198
|
encoding: 'utf8',
|
|
201
199
|
windowsHide: true,
|
|
202
|
-
shell: process.platform === 'win32',
|
|
203
200
|
});
|
|
204
201
|
if (r.error)
|
|
205
202
|
return 1;
|
package/dist/commands/update.js
CHANGED
|
@@ -4,7 +4,7 @@ import { basename, join, resolve } from 'node:path';
|
|
|
4
4
|
import process from 'node:process';
|
|
5
5
|
import { executeActions, findGitDir, InvalidJsonError, validateJsonFile, RefusalError, UpstreamError } from '../init/execute.js';
|
|
6
6
|
import { planInit } from '../init/planner.js';
|
|
7
|
-
import { GUARD_RE
|
|
7
|
+
import { GUARD_RE } from '../lib/hooks.js';
|
|
8
8
|
import { detectPackageManager } from '../lib/pm.js';
|
|
9
9
|
import { resolveBacklogBin, runCapture } from '../lib/run.js';
|
|
10
10
|
import { KIT_VERSION } from '../lib/version.js';
|
|
@@ -13,8 +13,6 @@ const REFRESH_KINDS = new Set([
|
|
|
13
13
|
'write-claude-pointer',
|
|
14
14
|
'copy-skills',
|
|
15
15
|
'install-guard-hook',
|
|
16
|
-
'install-refresh-hook',
|
|
17
|
-
'generate-dashboard',
|
|
18
16
|
]);
|
|
19
17
|
export function refreshActions(all) {
|
|
20
18
|
return all.filter((action) => REFRESH_KINDS.has(action.kind));
|
|
@@ -31,15 +29,6 @@ function guardHookInstalled(cwd) {
|
|
|
31
29
|
return false;
|
|
32
30
|
return GUARD_RE.test(readFileSync(hookPath, 'utf8'));
|
|
33
31
|
}
|
|
34
|
-
function refreshHookInstalled(cwd) {
|
|
35
|
-
const gitDir = findGitDir(cwd);
|
|
36
|
-
if (!gitDir)
|
|
37
|
-
return false;
|
|
38
|
-
const hookPath = join(gitDir, 'hooks', 'post-commit');
|
|
39
|
-
if (!existsSync(hookPath))
|
|
40
|
-
return false;
|
|
41
|
-
return REFRESH_RE.test(readFileSync(hookPath, 'utf8'));
|
|
42
|
-
}
|
|
43
32
|
export async function runUpdate(cwd, _args) {
|
|
44
33
|
// Up-front detection-failure check (mirrors uninstall): refuse before mutating anything.
|
|
45
34
|
for (const f of ['package.json', 'opencode.json']) {
|
|
@@ -72,8 +61,6 @@ export async function runUpdate(cwd, _args) {
|
|
|
72
61
|
harnesses: ['opencode', 'claude'],
|
|
73
62
|
pm: 'auto',
|
|
74
63
|
guard: guardHookInstalled(cwd),
|
|
75
|
-
refreshHook: refreshHookInstalled(cwd),
|
|
76
|
-
dashboard: existsSync(join(cwd, 'dashboard.html')),
|
|
77
64
|
skipInstall: false,
|
|
78
65
|
};
|
|
79
66
|
const plan = planInit(state, opts, KIT_VERSION);
|
|
@@ -113,13 +100,12 @@ export async function runUpdate(cwd, _args) {
|
|
|
113
100
|
warnings.push(`\`${bin} --version\` failed with exit code ${local.status}`);
|
|
114
101
|
}
|
|
115
102
|
}
|
|
116
|
-
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
117
103
|
let published = null;
|
|
118
104
|
try {
|
|
119
105
|
// test seam: SBL_FORCE_OFFLINE makes e2e runs take the offline path deterministically
|
|
120
106
|
if (process.env.SBL_FORCE_OFFLINE)
|
|
121
107
|
throw new Error('forced offline');
|
|
122
|
-
const view = runCapture(npm, ['view', 'backlog.md', 'version'], cwd);
|
|
108
|
+
const view = runCapture('npm', ['view', 'backlog.md', 'version'], cwd);
|
|
123
109
|
if (view.status === 0)
|
|
124
110
|
published = firstLine(view.stdout);
|
|
125
111
|
}
|
package/dist/dashboard/data.js
CHANGED
|
@@ -159,7 +159,7 @@ export const BUILT_IN_GLOSSARY = [
|
|
|
159
159
|
{ term: 'Backlog.md', definition: 'File-based task management CLI owning specs, statuses and history under backlog/.' },
|
|
160
160
|
{ term: 'Superpowers', definition: 'The methodology skill set that decides HOW the work is done.' },
|
|
161
161
|
{ term: 'Pipeline', definition: 'The nine workflow phases from Idea to Merge & archive.' },
|
|
162
|
-
{ term: 'Freshness Hook', definition: '
|
|
162
|
+
{ term: 'Freshness Hook', definition: 'Run `sbl dashboard` to serve a live dashboard that reloads automatically while the server is running.' },
|
|
163
163
|
];
|
|
164
164
|
/** Split `## Term` headings plus their following non-heading block into entries; empty sections are skipped. */
|
|
165
165
|
export function parseGlossaryMarkdown(content) {
|
package/dist/dashboard/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
5
5
|
import { createServer } from 'node:http';
|
|
6
6
|
import { isAbsolute, join } from 'node:path';
|
|
7
7
|
import process from 'node:process';
|
|
8
|
+
import crossSpawn from 'cross-spawn';
|
|
8
9
|
import { createModelApiHandler } from '../models/dashboard-api.js';
|
|
9
10
|
import { resolveBacklogBin } from '../lib/run.js';
|
|
10
11
|
export const DASHBOARD_PORT = 6428;
|
|
@@ -25,7 +26,12 @@ async function readBody(req) {
|
|
|
25
26
|
/** Safe /api/run handler: only whitelisted backlog subcommands may be spawned. */
|
|
26
27
|
export function createRunApiHandler(cwd) {
|
|
27
28
|
return async (req, res) => {
|
|
28
|
-
if (req.
|
|
29
|
+
if (req.url !== '/api/run') {
|
|
30
|
+
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
31
|
+
res.end('not found');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (req.method !== 'POST') {
|
|
29
35
|
res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
|
|
30
36
|
res.end('not found');
|
|
31
37
|
return;
|
|
@@ -61,11 +67,10 @@ export function createRunApiHandler(cwd) {
|
|
|
61
67
|
}
|
|
62
68
|
const args = WHITELIST.get(payload.command);
|
|
63
69
|
try {
|
|
64
|
-
const child =
|
|
70
|
+
const child = crossSpawn(bin, args, {
|
|
65
71
|
cwd,
|
|
66
72
|
detached: true,
|
|
67
73
|
stdio: 'ignore',
|
|
68
|
-
shell: process.platform === 'win32',
|
|
69
74
|
});
|
|
70
75
|
child.on('error', () => { });
|
|
71
76
|
child.unref();
|
package/dist/init/execute.js
CHANGED
|
@@ -4,7 +4,7 @@ import { dirname, basename, join, resolve } from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import process from 'node:process';
|
|
6
6
|
import { atomicWrite } from '../lib/atomic.js';
|
|
7
|
-
import { installGuardHook
|
|
7
|
+
import { installGuardHook } from '../lib/hooks.js';
|
|
8
8
|
import { injectBlock } from '../lib/markers.js';
|
|
9
9
|
import { applyPluginEntry } from '../lib/opencode.js';
|
|
10
10
|
import { OwnershipError, renderSkill } from '../lib/ownership.js';
|
|
@@ -150,22 +150,6 @@ function applyCopySkills(cwd, version) {
|
|
|
150
150
|
}
|
|
151
151
|
return true;
|
|
152
152
|
}
|
|
153
|
-
async function applyGenerateDashboard(cwd, warnings) {
|
|
154
|
-
try {
|
|
155
|
-
// Non-literal specifier: the dashboard module lands in a later batch; a missing
|
|
156
|
-
// module must degrade to a warning here, never crash init.
|
|
157
|
-
const specifier = '../commands/dashboard.js';
|
|
158
|
-
const mod = (await import(specifier));
|
|
159
|
-
if (typeof mod.generateDashboard !== 'function') {
|
|
160
|
-
throw new Error('dashboard module does not export generateDashboard');
|
|
161
|
-
}
|
|
162
|
-
await mod.generateDashboard(cwd, { serve: false });
|
|
163
|
-
}
|
|
164
|
-
catch (err) {
|
|
165
|
-
warnings.push(`dashboard generation skipped (${err instanceof Error ? err.message : String(err)})`);
|
|
166
|
-
}
|
|
167
|
-
return true;
|
|
168
|
-
}
|
|
169
153
|
export async function executeActions(cwd, actions, ctx) {
|
|
170
154
|
const warnings = [];
|
|
171
155
|
let applied = 0;
|
|
@@ -214,21 +198,6 @@ export async function executeActions(cwd, actions, ctx) {
|
|
|
214
198
|
}
|
|
215
199
|
break;
|
|
216
200
|
}
|
|
217
|
-
case 'install-refresh-hook': {
|
|
218
|
-
const gitDir = findGitDir(cwd);
|
|
219
|
-
if (!gitDir) {
|
|
220
|
-
warnings.push('no .git directory found - dashboard refresh hook not installed');
|
|
221
|
-
skipped++;
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
installRefreshHook(gitDir, ctx.version);
|
|
225
|
-
applied++;
|
|
226
|
-
}
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
case 'generate-dashboard':
|
|
230
|
-
(await applyGenerateDashboard(cwd, warnings)) ? applied++ : skipped++;
|
|
231
|
-
break;
|
|
232
201
|
case 'install-model-router': {
|
|
233
202
|
const { installOpenCodeAdapter } = await import('../models/opencode.js');
|
|
234
203
|
const { installClaudeAdapter } = await import('../models/claude.js');
|
package/dist/init/planner.js
CHANGED
|
@@ -58,10 +58,6 @@ export function planInit(state, opts, _version) {
|
|
|
58
58
|
actions.push({ kind: 'write-claude-pointer' });
|
|
59
59
|
if (opts.guard)
|
|
60
60
|
actions.push({ kind: 'install-guard-hook' });
|
|
61
|
-
if (opts.refreshHook ?? true)
|
|
62
|
-
actions.push({ kind: 'install-refresh-hook' });
|
|
63
|
-
if (opts.dashboard)
|
|
64
|
-
actions.push({ kind: 'generate-dashboard' });
|
|
65
61
|
if (opts.models === true)
|
|
66
62
|
actions.push({ kind: 'install-model-router', enabled: true });
|
|
67
63
|
return { actions, warnings };
|
package/dist/lib/preflight.js
CHANGED
|
@@ -3,19 +3,17 @@
|
|
|
3
3
|
// System-changing fixes (node install, execution policy) require consent or fixAll;
|
|
4
4
|
// safe fixes run unconditionally. Every fix is verified and reports a manual
|
|
5
5
|
// fallback command on failure.
|
|
6
|
-
import { spawnSync } from 'node:child_process';
|
|
7
6
|
import { existsSync } from 'node:fs';
|
|
8
7
|
import { delimiter, join } from 'node:path';
|
|
9
8
|
import process from 'node:process';
|
|
9
|
+
import spawn from 'cross-spawn';
|
|
10
10
|
import { getEffectiveExecutionPolicy, isBlockingExecutionPolicy, } from './powershell.js';
|
|
11
11
|
import { resolveBacklogBin } from './run.js';
|
|
12
12
|
const defaultExecutor = (cmd, args) => {
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
const r = spawnSync(cmd, args, {
|
|
13
|
+
// cross-spawn resolves .cmd shims on Windows without shell: true.
|
|
14
|
+
const r = spawn.sync(cmd, args, {
|
|
16
15
|
encoding: 'utf8',
|
|
17
16
|
windowsHide: true,
|
|
18
|
-
shell: process.platform === 'win32',
|
|
19
17
|
});
|
|
20
18
|
if (r.error)
|
|
21
19
|
return { status: null, stdout: '', stderr: String(r.error.message) };
|
|
@@ -99,13 +97,6 @@ function checkNpmCommand(ctx) {
|
|
|
99
97
|
ctx.npmCmd = 'npm';
|
|
100
98
|
return { id, status: 'ok', detail: `npm ${probe.stdout.trim()}` };
|
|
101
99
|
}
|
|
102
|
-
if (ctx.platform === 'win32') {
|
|
103
|
-
const fallback = ctx.executor('npm.cmd', ['--version']);
|
|
104
|
-
if (fallback.status === 0) {
|
|
105
|
-
ctx.npmCmd = 'npm.cmd';
|
|
106
|
-
return { id, status: 'fixed', detail: 'npm not directly callable; using npm.cmd shim' };
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
100
|
return {
|
|
110
101
|
id,
|
|
111
102
|
status: 'failed',
|
|
@@ -206,7 +197,7 @@ export function runPreflight(cwd, deps = {}) {
|
|
|
206
197
|
log: deps.log ?? ((line) => console.log(line)),
|
|
207
198
|
confirm: deps.confirm,
|
|
208
199
|
fixAll: deps.fixAll ?? false,
|
|
209
|
-
npmCmd:
|
|
200
|
+
npmCmd: 'npm',
|
|
210
201
|
};
|
|
211
202
|
const unitNames = [
|
|
212
203
|
['node-version', checkNodeVersion],
|
package/dist/lib/run.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { spawnSync } from 'node:child_process';
|
|
3
2
|
import { join } from 'node:path';
|
|
4
3
|
import process from 'node:process';
|
|
4
|
+
import spawn from 'cross-spawn';
|
|
5
5
|
export function runCapture(cmd, args, cwd) {
|
|
6
|
-
|
|
7
|
-
const winShell = process.platform === 'win32';
|
|
8
|
-
const file = winShell && /\s/.test(cmd) ? `"${cmd}"` : cmd;
|
|
9
|
-
const r = spawnSync(file, args, { cwd, encoding: 'utf8', shell: winShell });
|
|
6
|
+
const r = spawn.sync(cmd, args, { cwd, encoding: 'utf8' });
|
|
10
7
|
if (r.error && (r.status === null || r.status === undefined)) {
|
|
11
8
|
return { status: 127, stdout: '', stderr: String(r.error.message) };
|
|
12
9
|
}
|
|
@@ -163,15 +163,18 @@
|
|
|
163
163
|
|
|
164
164
|
/* ---------- Quick actions ---------- */
|
|
165
165
|
.cmd-row { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
|
|
166
|
+
.cmd-row { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
166
167
|
.cmd-btn {
|
|
167
|
-
|
|
168
|
+
display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
|
|
169
|
+
padding: 10px 18px; border-radius: 10px; min-width: 180px; text-align: left;
|
|
168
170
|
background: var(--surface); border: 1px solid var(--line-strong); color: var(--text);
|
|
169
171
|
cursor: pointer;
|
|
170
172
|
}
|
|
171
|
-
.cmd-btn:hover { background: var(--surface-2); border-color: var(--accent);
|
|
173
|
+
.cmd-btn:hover { background: var(--surface-2); border-color: var(--accent); }
|
|
172
174
|
.cmd-btn:active { transform: translateY(1px); }
|
|
173
|
-
.cmd-
|
|
174
|
-
.cmd-btn
|
|
175
|
+
.cmd-title { font-size: .95rem; font-weight: 600; color: var(--text); }
|
|
176
|
+
.cmd-btn:hover .cmd-title { color: var(--accent); }
|
|
177
|
+
.cmd-line { font-family: var(--mono); font-size: .72rem; color: var(--dim); }
|
|
175
178
|
.hint { color: var(--dim); font-size: .8rem; margin-top: 12px; }
|
|
176
179
|
.drafts-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
|
|
177
180
|
.drafts-list li { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 8px 12px; font-size: .88rem; }
|
|
@@ -322,9 +325,9 @@
|
|
|
322
325
|
<div class="sec-head"><span class="sec-num">01</span><h2>Board & Quick Actions</h2><span class="tagline">commands · board mirror only</span></div>
|
|
323
326
|
<div id="quickactions" class="mount">
|
|
324
327
|
<div class="cmd-row" id="cmd-buttons">
|
|
325
|
-
<button type="button" class="cmd-btn" data-cmd="browser">backlog browser</button>
|
|
326
|
-
<button type="button" class="cmd-btn" data-cmd="board">backlog board</button>
|
|
327
|
-
<button type="button" class="cmd-btn" data-copy="sbl dashboard
|
|
328
|
+
<button type="button" class="cmd-btn" data-cmd="browser"><span class="cmd-title">Backlog Browser</span><span class="cmd-line">backlog browser</span></button>
|
|
329
|
+
<button type="button" class="cmd-btn" data-cmd="board"><span class="cmd-title">Backlog Board</span><span class="cmd-line">backlog board</span></button>
|
|
330
|
+
<button type="button" class="cmd-btn" data-copy="sbl dashboard"><span class="cmd-title">Live Dashboard</span><span class="cmd-line">sbl dashboard</span></button>
|
|
328
331
|
</div>
|
|
329
332
|
<p class="hint">The board mirrors Backlog.md — it never writes execution state.</p>
|
|
330
333
|
</div>
|
|
@@ -389,7 +392,7 @@
|
|
|
389
392
|
|
|
390
393
|
<footer class="footer">
|
|
391
394
|
Generated __GENERATED_AT__ · super-backlog v__KIT_VERSION__ · <span id="source-badge"></span> ·
|
|
392
|
-
|
|
395
|
+
served live by <code class="inline">sbl dashboard</code> — reloads automatically while the server is running.
|
|
393
396
|
</footer>
|
|
394
397
|
|
|
395
398
|
</main>
|
|
@@ -506,12 +509,26 @@
|
|
|
506
509
|
renderTasks();
|
|
507
510
|
|
|
508
511
|
/* ---------- Quick action buttons ---------- */
|
|
512
|
+
function cmdFeedback(btn, text) {
|
|
513
|
+
var title = btn.querySelector('.cmd-title');
|
|
514
|
+
if (!title || btn.getAttribute('data-busy')) return;
|
|
515
|
+
btn.setAttribute('data-busy', '1');
|
|
516
|
+
var orig = title.textContent;
|
|
517
|
+
title.textContent = text;
|
|
518
|
+
setTimeout(function () { title.textContent = orig; btn.removeAttribute('data-busy'); }, 1200);
|
|
519
|
+
}
|
|
520
|
+
function copyCommand(btn, command) {
|
|
521
|
+
if (navigator.clipboard) navigator.clipboard.writeText(command).catch(function () {});
|
|
522
|
+
cmdFeedback(btn, 'copied \u2713');
|
|
523
|
+
}
|
|
509
524
|
document.querySelectorAll('#cmd-buttons .cmd-btn').forEach(function (b) {
|
|
510
525
|
b.addEventListener('click', function () {
|
|
511
526
|
var copy = b.getAttribute('data-copy');
|
|
512
|
-
if (copy) {
|
|
513
|
-
|
|
514
|
-
|
|
527
|
+
if (copy) { copyCommand(b, copy); return; }
|
|
528
|
+
var cmd = b.getAttribute('data-cmd');
|
|
529
|
+
fetch('/api/run', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ command: cmd }) })
|
|
530
|
+
.then(function (res) { if (!res.ok) throw new Error('run failed'); cmdFeedback(b, 'started \u2713'); })
|
|
531
|
+
.catch(function () {});
|
|
515
532
|
});
|
|
516
533
|
});
|
|
517
534
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "super-backlog",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "One command to equip any project with Backlog.md + Superpowers, plus a Project Dashboard.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"screenshot": "npm run build && node scripts/capture-dashboard.mjs"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
|
+
"@types/cross-spawn": "^6.0.6",
|
|
35
36
|
"@types/node": "^26.2.0",
|
|
36
37
|
"backlog.md": "^1.50.1",
|
|
37
38
|
"cspell": "^10.1.1",
|
|
@@ -52,9 +53,13 @@
|
|
|
52
53
|
},
|
|
53
54
|
"vitest": {
|
|
54
55
|
"vite": "^8.2.2",
|
|
56
|
+
"esbuild": "^0.25.0",
|
|
55
57
|
"@vitest/mocker": {
|
|
56
58
|
"vite": "^8.2.2"
|
|
57
59
|
}
|
|
58
60
|
}
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"cross-spawn": "^7.0.6"
|
|
59
64
|
}
|
|
60
65
|
}
|
package/dist/dashboard/regen.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
// src/dashboard/regen.ts
|
|
2
|
-
// Standalone regeneration entry invoked by the post-commit freshness hook:
|
|
3
|
-
// node "<repo>/node_modules/super-backlog/dist/dashboard/regen.js"
|
|
4
|
-
// Contract: never throw, never exit non-zero - a broken dashboard generation
|
|
5
|
-
// must never block or fail a user's commit. All errors go to console.error.
|
|
6
|
-
import { join, resolve } from 'node:path';
|
|
7
|
-
import { pathToFileURL } from 'node:url';
|
|
8
|
-
import process from 'node:process';
|
|
9
|
-
import { collectDashboardData } from './data.js';
|
|
10
|
-
import { renderDashboard } from './render.js';
|
|
11
|
-
import { atomicWrite } from '../lib/atomic.js';
|
|
12
|
-
import { KIT_VERSION } from '../lib/version.js';
|
|
13
|
-
/** Regenerate <cwd>/dashboard.html. Throws on failure; the direct-run wrapper swallows. */
|
|
14
|
-
export function regenerateDashboard(cwd) {
|
|
15
|
-
const html = renderDashboard(collectDashboardData(cwd, { kitVersion: KIT_VERSION }));
|
|
16
|
-
atomicWrite(join(cwd, 'dashboard.html'), html);
|
|
17
|
-
}
|
|
18
|
-
const invokedDirectly = process.argv[1] !== undefined &&
|
|
19
|
-
import.meta.url === pathToFileURL(resolve(process.argv[1])).href;
|
|
20
|
-
if (invokedDirectly) {
|
|
21
|
-
try {
|
|
22
|
-
regenerateDashboard(process.cwd());
|
|
23
|
-
}
|
|
24
|
-
catch (err) {
|
|
25
|
-
console.error(err instanceof Error ? (err.stack ?? err.message) : err);
|
|
26
|
-
}
|
|
27
|
-
process.exit(0); // always - success or failure
|
|
28
|
-
}
|