super-backlog 0.3.1
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/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/cli.js +130 -0
- package/dist/commands/dashboard.js +69 -0
- package/dist/commands/doctor.js +65 -0
- package/dist/commands/init.js +138 -0
- package/dist/commands/uninstall.js +279 -0
- package/dist/commands/update.js +138 -0
- package/dist/dashboard/data.js +281 -0
- package/dist/dashboard/layering.js +94 -0
- package/dist/dashboard/regen.js +28 -0
- package/dist/dashboard/render.js +75 -0
- package/dist/dashboard/server.js +109 -0
- package/dist/init/execute.js +232 -0
- package/dist/init/planner.js +60 -0
- package/dist/lib/atomic.js +16 -0
- package/dist/lib/hooks.js +78 -0
- package/dist/lib/markers.js +41 -0
- package/dist/lib/opencode.js +17 -0
- package/dist/lib/ownership.js +15 -0
- package/dist/lib/pkgjson.js +41 -0
- package/dist/lib/pm.js +20 -0
- package/dist/lib/powershell.js +44 -0
- package/dist/lib/run.js +32 -0
- package/dist/lib/validate-task.js +22 -0
- package/dist/lib/version.js +12 -0
- package/dist/lib/yamlmini.js +18 -0
- package/dist/templates/claude-pointer.md +5 -0
- package/dist/templates/dashboard-refresh-hook.sh +18 -0
- package/dist/templates/dashboard.html +1001 -0
- package/dist/templates/guard-hook.sh +25 -0
- package/dist/templates/skill-backlog-status-report.md +31 -0
- package/dist/templates/skill-spec-to-backlog.md +32 -0
- package/dist/templates/skill-task-review-gate.md +30 -0
- package/dist/templates/workflow-block.md +31 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 super-backlog contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# super-backlog
|
|
2
|
+
|
|
3
|
+
[](https://github.com/adam-s-k-i/super-backlog/actions/workflows/ci.yml)
|
|
4
|
+
[](https://adam-s-k-i.github.io/super-backlog/)
|
|
5
|
+
|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
One command to equip any project with [Backlog.md](https://github.com/MrLesk/Backlog.md) + [Superpowers](https://github.com/obra/superpowers), plus a Project Dashboard.
|
|
9
|
+
|
|
10
|
+
## What & why
|
|
11
|
+
|
|
12
|
+
Superpowers and Backlog.md are strong on their own, but nothing wires them together: Superpowers defines *how* agents should work (brainstorming → plans → TDD → review), Backlog.md defines *what* is tracked (markdown tasks, Kanban board, browser UI). Connecting them today means hand-copying glue from project to project — workflow blocks in `AGENTS.md`, bridge skills like `spec-to-backlog`, npm scripts, plugin config. super-backlog is the glue orchestrator that installs and maintains all of it in one step, with a guaranteed-clean exit path (`sbl uninstall`).
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
- Node >= 20
|
|
17
|
+
- A package manager (npm, pnpm, or bun) for dependency installation
|
|
18
|
+
- Works with **OpenCode** and **Claude Code** (both configured by default)
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx super-backlog init # install everything into the current project
|
|
24
|
+
npm run board # open the Backlog.md kanban board
|
|
25
|
+
sbl dashboard --serve # live Project Dashboard on http://localhost:6428
|
|
26
|
+
# (or: npx super-backlog dashboard --serve)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
On Windows PowerShell, `npx super-backlog init` may fail with an execution-policy error before the CLI starts. Run `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` once in PowerShell, or run `sbl doctor` for the exact policy and fix.
|
|
30
|
+
|
|
31
|
+
`init` is idempotent — safe to re-run any time; re-running with a newer kit version is the upgrade path for all injected files.
|
|
32
|
+
|
|
33
|
+
## What gets installed
|
|
34
|
+
|
|
35
|
+
| Target | Content | Ownership model |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `package.json` devDependencies | `backlog.md@latest` + `super-backlog@latest` | merged |
|
|
38
|
+
| `backlog/` | created by `backlog init --defaults` (upstream owns it) | delegated |
|
|
39
|
+
| `opencode.json` | `plugin[] += superpowers@git+https://github.com/obra/superpowers.git` | merged; other keys untouched |
|
|
40
|
+
| `.claude/` | Superpowers via official marketplace — init prints the exact command to paste (`/plugin install superpowers@claude-plugins-official`); file-based skills work immediately | instructed/delegated |
|
|
41
|
+
| `AGENTS.md` | `<!-- SUPER-BACKLOG:x.y.z START -->` … `<!-- SUPER-BACKLOG END -->` workflow block | marker-scoped |
|
|
42
|
+
| `CLAUDE.md` | one-line pointer to the AGENTS.md block | marker-scoped |
|
|
43
|
+
| `.opencode/skill/<skill>/SKILL.md` (3 glue skills) | skill templates | fingerprint header line |
|
|
44
|
+
| `.claude/skills/<skill>/SKILL.md` | same templates | fingerprint header line |
|
|
45
|
+
| `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 |
|
|
46
|
+
| `dashboard.html` | generated Project Dashboard | regenerated wholesale |
|
|
47
|
+
| `.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 |
|
|
48
|
+
| `.git/hooks/pre-commit` | integrity guard hook — only with `--guard` (opt-in) | appended marker block |
|
|
49
|
+
|
|
50
|
+
Commands: `sbl init` · `sbl doctor` · `sbl uninstall [--with-backlog]` · `sbl update` · `sbl dashboard [--serve] [--port <n>] [--no-open] [--out <file>]`. See `sbl help` for every flag.
|
|
51
|
+
|
|
52
|
+
## Project Dashboard
|
|
53
|
+
|
|
54
|
+
`sbl dashboard` generates a single self-contained `dashboard.html`: a dark, HTS-style cockpit rendered from your Backlog data in seven sections — Board & Quick Actions, Status (donut), Milestones, Tasks (sortable/filterable table with a click-in detail panel per task), Feature Cycle (pipeline stepper), Activity (30-day sparkline), and Decisions & Docs. A layered dependency graph maps task `depends-on` relations (cycle- and dangling-ref-tolerant): hover highlights edges, click opens the task. 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, diffs cleanly in git, hostable anywhere. Use `--serve` for live mode: it watches `backlog/`, regenerates on change, and serves on port 6428 by default.
|
|
55
|
+
|
|
56
|
+
### Keeping it fresh
|
|
57
|
+
|
|
58
|
+
By default `init` installs a `post-commit` hook block that regenerates `dashboard.html` whenever the commit touched `backlog/`. The block is marker-delimited, so it composes with the guard hook and foreign hook content, `uninstall` removes exactly that block, and `update` refreshes it. It never blocks a commit: failures print a one-line stderr note and the exit status stays 0. Opt out with `--no-refresh-hook`.
|
|
59
|
+
|
|
60
|
+

|
|
61
|
+
|
|
62
|
+
## Uninstall guarantee
|
|
63
|
+
|
|
64
|
+
super-backlog uninstall removes only provably owned artifacts and keeps your Backlog task data unless you pass --with-backlog. Every removal decision is reported line by line as removed / kept / skipped; files whose ownership cannot be proven are left untouched.
|
|
65
|
+
|
|
66
|
+
## Harness support
|
|
67
|
+
|
|
68
|
+
- **OpenCode** — native: `opencode.json` plugin entry plus file-based skills under `.opencode/skill/` (spec-to-backlog, backlog-status-report, task-review-gate).
|
|
69
|
+
- **Claude Code** — file-based skills under `.claude/skills/` always work immediately; the marketplace plugin cannot be installed from a script, so init prints the exact command to paste (`/plugin install superpowers@claude-plugins-official`) and exits with a warning (exit code 4).
|
|
70
|
+
|
|
71
|
+
Details and matrix: [docs/guide/harness-support.md](docs/guide/harness-support.md).
|
|
72
|
+
|
|
73
|
+
## Troubleshooting
|
|
74
|
+
|
|
75
|
+
Windows OpenCode fallback, exit codes, and environment seams: [docs/guide/troubleshooting.md](docs/guide/troubleshooting.md). Architecture deep-dive: [docs/guide/architecture.md](docs/guide/architecture.md). Guard hook details: [docs/guide/guard.md](docs/guide/guard.md).
|
|
76
|
+
|
|
77
|
+
## Development
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm ci # install dependencies
|
|
81
|
+
npm test # build + vitest suite
|
|
82
|
+
npm run lint # markdownlint + cspell over all Markdown
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
[MIT](LICENSE)
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/cli.ts
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { runDashboard } from './commands/dashboard.js';
|
|
6
|
+
import { runDoctor } from './commands/doctor.js';
|
|
7
|
+
import { runInit } from './commands/init.js';
|
|
8
|
+
import { runUninstall } from './commands/uninstall.js';
|
|
9
|
+
import { runUpdate } from './commands/update.js';
|
|
10
|
+
import { assertNode20, KIT_VERSION } from './lib/version.js';
|
|
11
|
+
const HELP = `super-backlog (sbl) - equip any project with Backlog.md + Superpowers
|
|
12
|
+
|
|
13
|
+
Usage: sbl <command> [options]
|
|
14
|
+
|
|
15
|
+
Commands:
|
|
16
|
+
init Install the kit into the current project
|
|
17
|
+
uninstall Remove kit-managed files (project data kept unless --with-backlog)
|
|
18
|
+
update Refresh kit-managed files and report upstream versions
|
|
19
|
+
dashboard Generate the single-file project dashboard (--serve for live mode)
|
|
20
|
+
doctor Check the environment (node, PowerShell policy, backlog CLI)
|
|
21
|
+
|
|
22
|
+
init options:
|
|
23
|
+
--pm <auto|npm|pnpm|bun|skip> Package manager to use (default: auto)
|
|
24
|
+
--harness <opencode|claude> Target harness; repeatable or comma-separated (default: both)
|
|
25
|
+
--guard Install the integrity pre-commit hook (opt-in)
|
|
26
|
+
--no-dashboard Skip generating the project dashboard
|
|
27
|
+
--no-refresh-hook Skip the post-commit dashboard freshness hook
|
|
28
|
+
--dry-run Show what would be done without writing anything
|
|
29
|
+
|
|
30
|
+
uninstall options:
|
|
31
|
+
--with-backlog Also permanently delete the backlog/ data directory
|
|
32
|
+
|
|
33
|
+
update options:
|
|
34
|
+
(none) Refreshes injected files, skills, hook; prints upstream versions
|
|
35
|
+
|
|
36
|
+
dashboard options:
|
|
37
|
+
--serve Live mode: watch backlog/ and regenerate on changes
|
|
38
|
+
--port <n> Port for --serve (default: 6428)
|
|
39
|
+
--no-open With --serve: do not open the browser automatically
|
|
40
|
+
--out <file> Output file name or path (default: dashboard.html)
|
|
41
|
+
|
|
42
|
+
doctor options:
|
|
43
|
+
(none) Prints one [ok]/[warn]/[skip] line per check; exit 4 on any warn
|
|
44
|
+
|
|
45
|
+
Global options:
|
|
46
|
+
--version Print the super-backlog version and exit
|
|
47
|
+
|
|
48
|
+
Exit codes:
|
|
49
|
+
0 ok | 1 usage/detection failure | 2 ownership or merge refusal
|
|
50
|
+
3 upstream command failure | 4 success with warnings`;
|
|
51
|
+
async function main(argv) {
|
|
52
|
+
const [command, ...rest] = argv;
|
|
53
|
+
if (command === '--version' || command === '-v') {
|
|
54
|
+
console.log(KIT_VERSION);
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
if (command === undefined || command === 'help' || command === '--help' || command === '-h') {
|
|
58
|
+
console.log(HELP);
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
switch (command) {
|
|
62
|
+
case 'init': {
|
|
63
|
+
const parsed = parseArgs({
|
|
64
|
+
args: rest,
|
|
65
|
+
allowPositionals: true,
|
|
66
|
+
options: {
|
|
67
|
+
pm: { type: 'string' },
|
|
68
|
+
harness: { type: 'string', multiple: true },
|
|
69
|
+
guard: { type: 'boolean' },
|
|
70
|
+
'no-dashboard': { type: 'boolean' },
|
|
71
|
+
'no-refresh-hook': { type: 'boolean' },
|
|
72
|
+
'dry-run': { type: 'boolean' },
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
return await runInit(process.cwd(), {
|
|
76
|
+
values: parsed.values,
|
|
77
|
+
positionals: parsed.positionals,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
case 'uninstall': {
|
|
81
|
+
const parsed = parseArgs({
|
|
82
|
+
args: rest,
|
|
83
|
+
allowPositionals: true,
|
|
84
|
+
options: { 'with-backlog': { type: 'boolean' } },
|
|
85
|
+
});
|
|
86
|
+
return runUninstall(process.cwd(), {
|
|
87
|
+
values: parsed.values,
|
|
88
|
+
positionals: parsed.positionals,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
case 'update': {
|
|
92
|
+
const parsed = parseArgs({ args: rest, allowPositionals: true, options: {} });
|
|
93
|
+
return await runUpdate(process.cwd(), {
|
|
94
|
+
values: parsed.values,
|
|
95
|
+
positionals: parsed.positionals,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
case 'dashboard': {
|
|
99
|
+
const parsed = parseArgs({
|
|
100
|
+
args: rest,
|
|
101
|
+
allowPositionals: true,
|
|
102
|
+
options: {
|
|
103
|
+
serve: { type: 'boolean' },
|
|
104
|
+
port: { type: 'string' },
|
|
105
|
+
'no-open': { type: 'boolean' },
|
|
106
|
+
out: { type: 'string' },
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
return await runDashboard(process.cwd(), {
|
|
110
|
+
values: parsed.values,
|
|
111
|
+
positionals: parsed.positionals,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
case 'doctor':
|
|
115
|
+
return runDoctor(process.cwd());
|
|
116
|
+
default:
|
|
117
|
+
console.error(`Unknown command "${command}".\n`);
|
|
118
|
+
console.error(HELP);
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
assertNode20();
|
|
123
|
+
main(process.argv.slice(2))
|
|
124
|
+
.then((code) => {
|
|
125
|
+
process.exitCode = code;
|
|
126
|
+
})
|
|
127
|
+
.catch((err) => {
|
|
128
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/commands/dashboard.ts
|
|
2
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { collectDashboardData } from '../dashboard/data.js';
|
|
4
|
+
import { renderDashboard } from '../dashboard/render.js';
|
|
5
|
+
import { DASHBOARD_PORT, startServeServer } from '../dashboard/server.js';
|
|
6
|
+
import { atomicWrite } from '../lib/atomic.js';
|
|
7
|
+
import { KIT_VERSION } from '../lib/version.js';
|
|
8
|
+
async function regenerateInto(outPath, cwd) {
|
|
9
|
+
const data = collectDashboardData(cwd, { kitVersion: KIT_VERSION });
|
|
10
|
+
atomicWrite(outPath, renderDashboard(data));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Generate dashboard.html for the project; with `{ serve: true }`, start the
|
|
14
|
+
* live-reload server afterwards (the listening socket keeps the process up).
|
|
15
|
+
* Called by `sbl init` with serve disabled.
|
|
16
|
+
*/
|
|
17
|
+
export async function generateDashboard(cwd, o) {
|
|
18
|
+
const outPath = join(cwd, 'dashboard.html');
|
|
19
|
+
await regenerateInto(outPath, cwd);
|
|
20
|
+
if (o.serve) {
|
|
21
|
+
// Dynamic import keeps init resilient if the serve module is unavailable.
|
|
22
|
+
const specifier = '../dashboard/server.js';
|
|
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,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return outPath;
|
|
34
|
+
}
|
|
35
|
+
/** CLI entry for `sbl dashboard [--serve] [--port N] [--no-open] [--out FILE]`. */
|
|
36
|
+
export async function runDashboard(cwd, args) {
|
|
37
|
+
const values = args.values;
|
|
38
|
+
let port = DASHBOARD_PORT;
|
|
39
|
+
if (values['port'] !== undefined) {
|
|
40
|
+
const parsed = Number(values['port']);
|
|
41
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
42
|
+
console.error(`error: invalid --port "${String(values['port'])}" (expected 0-65535)`);
|
|
43
|
+
return 1;
|
|
44
|
+
}
|
|
45
|
+
port = parsed;
|
|
46
|
+
}
|
|
47
|
+
const serve = values['serve'] === true;
|
|
48
|
+
const noOpen = values['no-open'] === true;
|
|
49
|
+
const outFile = values['out'] === undefined ? 'dashboard.html' : String(values['out']);
|
|
50
|
+
const outPath = isAbsolute(outFile) ? outFile : resolve(cwd, outFile);
|
|
51
|
+
try {
|
|
52
|
+
await regenerateInto(outPath, cwd);
|
|
53
|
+
console.log(`dashboard written: ${outPath}`);
|
|
54
|
+
if (serve) {
|
|
55
|
+
console.log(`serving dashboard at http://127.0.0.1:${port}/ (press Ctrl+C to stop)`);
|
|
56
|
+
await startServeServer(cwd, {
|
|
57
|
+
port,
|
|
58
|
+
file: outPath,
|
|
59
|
+
regenerate: () => regenerateInto(outPath, cwd),
|
|
60
|
+
openBrowser: !noOpen,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
console.error(`error: dashboard generation failed (${err instanceof Error ? err.message : String(err)})`);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// src/commands/doctor.ts
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { getEffectiveExecutionPolicy, isBlockingExecutionPolicy, } from '../lib/powershell.js';
|
|
4
|
+
import { resolveBacklogBin } from '../lib/run.js';
|
|
5
|
+
const MARK = { ok: '[ok] ', warn: '[warn]', skip: '[skip]' };
|
|
6
|
+
export function runDoctor(cwd, deps = {}) {
|
|
7
|
+
const platform = deps.platform ?? process.platform;
|
|
8
|
+
const nodeVersion = deps.nodeVersion ?? process.versions.node;
|
|
9
|
+
const resolveBacklog = deps.resolveBacklog ?? resolveBacklogBin;
|
|
10
|
+
const log = deps.log ?? ((line) => console.log(line));
|
|
11
|
+
let okCount = 0;
|
|
12
|
+
let warnCount = 0;
|
|
13
|
+
let skipCount = 0;
|
|
14
|
+
const emit = (status, line, extra = []) => {
|
|
15
|
+
if (status === 'ok')
|
|
16
|
+
okCount += 1;
|
|
17
|
+
else if (status === 'warn')
|
|
18
|
+
warnCount += 1;
|
|
19
|
+
else
|
|
20
|
+
skipCount += 1;
|
|
21
|
+
log(`${MARK[status]} ${line}`);
|
|
22
|
+
for (const extraLine of extra)
|
|
23
|
+
log(` ${extraLine}`);
|
|
24
|
+
};
|
|
25
|
+
// check 1: node >= 20
|
|
26
|
+
const major = Number(nodeVersion.split('.')[0]);
|
|
27
|
+
if (Number.isNaN(major)) {
|
|
28
|
+
emit('warn', `node version unreadable ("${nodeVersion}") - super-backlog requires Node >= 20`);
|
|
29
|
+
}
|
|
30
|
+
else if (major >= 20) {
|
|
31
|
+
emit('ok', `node v${nodeVersion} (>= 20)`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
emit('warn', `node v${nodeVersion} is too old - super-backlog requires Node >= 20`, [
|
|
35
|
+
'fix: upgrade Node.js (https://nodejs.org)',
|
|
36
|
+
]);
|
|
37
|
+
}
|
|
38
|
+
// check 2: PowerShell execution policy (win32 only, fakeable via SBL_FAKE_POLICY)
|
|
39
|
+
const policy = getEffectiveExecutionPolicy({ platform, executor: deps.executor });
|
|
40
|
+
if (policy === null) {
|
|
41
|
+
emit('skip', 'PowerShell execution policy check (not Windows or undetectable)');
|
|
42
|
+
}
|
|
43
|
+
else if (isBlockingExecutionPolicy(policy)) {
|
|
44
|
+
emit('warn', `PowerShell execution policy "${policy}" blocks .ps1 shims - direct npm/npx/sbl calls in PowerShell will fail`, [
|
|
45
|
+
'fix: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned',
|
|
46
|
+
'alt: call the .cmd shims explicitly (npx.cmd, sbl.cmd) or run from cmd.exe',
|
|
47
|
+
'note: "npm run ..." scripts are unaffected (they execute via cmd.exe)',
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
emit('ok', `PowerShell execution policy: ${policy}`);
|
|
52
|
+
}
|
|
53
|
+
// check 3: backlog binary resolvability
|
|
54
|
+
const backlogBin = resolveBacklog(cwd);
|
|
55
|
+
if (backlogBin !== null) {
|
|
56
|
+
emit('ok', `backlog CLI resolvable at ${backlogBin}`);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
emit('warn', 'backlog CLI not found - run sbl init or npm install first', [
|
|
60
|
+
'fix: npx.cmd super-backlog init',
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
log(`doctor summary: ${okCount} ok, ${warnCount} warn, ${skipCount} skip`);
|
|
64
|
+
return warnCount > 0 ? 4 : 0;
|
|
65
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// src/commands/init.ts
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { basename, join, resolve } from 'node:path';
|
|
4
|
+
import { detectPackageManager } from '../lib/pm.js';
|
|
5
|
+
import { getEffectiveExecutionPolicy, isBlockingExecutionPolicy, policyWarningLines, } from '../lib/powershell.js';
|
|
6
|
+
import { KIT_VERSION } from '../lib/version.js';
|
|
7
|
+
import { executeActions, InvalidJsonError, RefusalError, UpstreamError } from '../init/execute.js';
|
|
8
|
+
import { planInit } from '../init/planner.js';
|
|
9
|
+
const HARNESS_VALUES = ['opencode', 'claude'];
|
|
10
|
+
const PM_VALUES = ['auto', 'npm', 'pnpm', 'bun', 'skip'];
|
|
11
|
+
function isHarness(value) {
|
|
12
|
+
return HARNESS_VALUES.includes(value);
|
|
13
|
+
}
|
|
14
|
+
function describeAction(action) {
|
|
15
|
+
switch (action.kind) {
|
|
16
|
+
case 'upstream-install':
|
|
17
|
+
return `upstream-install via ${action.pm}`;
|
|
18
|
+
case 'merge-json':
|
|
19
|
+
return `merge-json ${action.path} (${action.transform})`;
|
|
20
|
+
case 'inject-agents-block':
|
|
21
|
+
return 'inject-agents-block AGENTS.md';
|
|
22
|
+
case 'write-claude-pointer':
|
|
23
|
+
return 'write-claude-pointer CLAUDE.md';
|
|
24
|
+
case 'copy-skills':
|
|
25
|
+
return 'copy-skills (.opencode/skill + .claude/skills)';
|
|
26
|
+
case 'install-guard-hook':
|
|
27
|
+
return 'install-guard-hook .git/hooks/pre-commit';
|
|
28
|
+
case 'install-refresh-hook':
|
|
29
|
+
return 'install-refresh-hook .git/hooks/post-commit';
|
|
30
|
+
case 'generate-dashboard':
|
|
31
|
+
return 'generate-dashboard';
|
|
32
|
+
case 'write':
|
|
33
|
+
return `write ${action.path}`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function maybePrintPolicyWarning() {
|
|
37
|
+
const policy = getEffectiveExecutionPolicy();
|
|
38
|
+
if (isBlockingExecutionPolicy(policy)) {
|
|
39
|
+
for (const line of policyWarningLines(policy)) {
|
|
40
|
+
console.log(line);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function runInit(cwd, args) {
|
|
45
|
+
const harnesses = [];
|
|
46
|
+
const rawHarnesses = args.values.harness;
|
|
47
|
+
const listed = Array.isArray(rawHarnesses)
|
|
48
|
+
? rawHarnesses
|
|
49
|
+
: rawHarnesses === undefined
|
|
50
|
+
? []
|
|
51
|
+
: [rawHarnesses];
|
|
52
|
+
for (const entry of listed) {
|
|
53
|
+
for (const part of String(entry).split(',')) {
|
|
54
|
+
const name = part.trim();
|
|
55
|
+
if (name === '')
|
|
56
|
+
continue;
|
|
57
|
+
if (!isHarness(name)) {
|
|
58
|
+
console.error(`Invalid --harness "${name}" (expected: ${HARNESS_VALUES.join(', ')})`);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
if (!harnesses.includes(name))
|
|
62
|
+
harnesses.push(name);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (harnesses.length === 0)
|
|
66
|
+
harnesses.push('opencode', 'claude');
|
|
67
|
+
const rawPm = args.values.pm === undefined ? 'auto' : String(args.values.pm);
|
|
68
|
+
if (!PM_VALUES.includes(rawPm)) {
|
|
69
|
+
console.error(`Invalid --pm "${rawPm}" (expected: ${PM_VALUES.join(', ')})`);
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
const pm = rawPm;
|
|
73
|
+
const guard = args.values.guard === true; // opt-in per spec D8
|
|
74
|
+
const dashboard = args.values['no-dashboard'] !== true;
|
|
75
|
+
const refreshHook = args.values['no-refresh-hook'] !== true; // default on, opt-out flag
|
|
76
|
+
const dryRun = args.values['dry-run'] === true;
|
|
77
|
+
const projectName = args.positionals[0] ?? basename(resolve(cwd));
|
|
78
|
+
let opencodeConfig;
|
|
79
|
+
const opencodePath = join(cwd, 'opencode.json');
|
|
80
|
+
if (existsSync(opencodePath)) {
|
|
81
|
+
try {
|
|
82
|
+
opencodeConfig = JSON.parse(readFileSync(opencodePath, 'utf8'));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
console.error('error: opencode.json is not valid JSON - fix it manually, then re-run');
|
|
86
|
+
return 1; // detection failure, consistent with uninstall
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const state = {
|
|
90
|
+
cwd,
|
|
91
|
+
detectedPm: detectPackageManager(cwd),
|
|
92
|
+
hasBacklogConfig: existsSync(join(cwd, 'backlog', 'config.yml')),
|
|
93
|
+
agentsExists: existsSync(join(cwd, 'AGENTS.md')),
|
|
94
|
+
claudeMdExists: existsSync(join(cwd, 'CLAUDE.md')),
|
|
95
|
+
opencodeConfig,
|
|
96
|
+
pkgExists: existsSync(join(cwd, 'package.json')),
|
|
97
|
+
};
|
|
98
|
+
const opts = { projectName, harnesses, pm, guard, dashboard, refreshHook, skipInstall: false };
|
|
99
|
+
const plan = planInit(state, opts, KIT_VERSION);
|
|
100
|
+
if (dryRun) {
|
|
101
|
+
console.log(`dry-run for "${projectName}": ${plan.actions.length} action(s) planned, nothing written`);
|
|
102
|
+
for (const action of plan.actions)
|
|
103
|
+
console.log(` - ${describeAction(action)}`);
|
|
104
|
+
for (const warning of plan.warnings)
|
|
105
|
+
console.log(`warning: ${warning}`);
|
|
106
|
+
maybePrintPolicyWarning();
|
|
107
|
+
return plan.warnings.length > 0 ? 4 : 0;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const result = await executeActions(cwd, plan.actions, {
|
|
111
|
+
version: KIT_VERSION,
|
|
112
|
+
projectName,
|
|
113
|
+
hasBacklogConfig: state.hasBacklogConfig,
|
|
114
|
+
});
|
|
115
|
+
const warnings = [...plan.warnings, ...result.warnings];
|
|
116
|
+
console.log(`super-backlog init complete - planned ${plan.actions.length}, applied ${result.applied}, skipped ${result.skipped}`);
|
|
117
|
+
for (const warning of warnings)
|
|
118
|
+
console.log(`warning: ${warning}`);
|
|
119
|
+
maybePrintPolicyWarning();
|
|
120
|
+
return warnings.length > 0 ? 4 : 0;
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
if (err instanceof UpstreamError) {
|
|
124
|
+
console.error(`error: upstream command failed: ${err.message}`);
|
|
125
|
+
return 3;
|
|
126
|
+
}
|
|
127
|
+
if (err instanceof InvalidJsonError) {
|
|
128
|
+
// detection failure, not ownership refusal: exit 1 per contract
|
|
129
|
+
console.error(`error: ${err.message}`);
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
if (err instanceof RefusalError) {
|
|
133
|
+
console.error(`error: ${err.message}`);
|
|
134
|
+
return 2;
|
|
135
|
+
}
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
}
|