super-backlog 1.1.0 → 1.1.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/README.md +1 -1
- package/dist/bin.js +19 -0
- package/dist/cli.js +4 -17
- package/dist/commands/dashboard.js +26 -2
- package/dist/dashboard/hub.js +39 -15
- package/dist/lib/hub-state.js +8 -2
- package/dist/lib/version-check.js +41 -14
- package/package.json +3 -3
- package/dist/commands/backlog-alias.js +0 -18
package/README.md
CHANGED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/bin.ts
|
|
3
|
+
// Always-run CLI entry. Unlike src/cli.ts (a plain module that only exports
|
|
4
|
+
// HELP/runCli for tests), this file self-executes unconditionally so it
|
|
5
|
+
// works when invoked via a symlink (npm's POSIX global/npx/npm-link bins are
|
|
6
|
+
// symlinks, so comparing process.argv[1] against the module's own realpath
|
|
7
|
+
// -- as the old cli.ts guard did -- is false for every such install).
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
import { runCli } from './cli.js';
|
|
10
|
+
import { assertNode20 } from './lib/version.js';
|
|
11
|
+
assertNode20();
|
|
12
|
+
runCli(process.argv.slice(2))
|
|
13
|
+
.then((code) => {
|
|
14
|
+
process.exitCode = code;
|
|
15
|
+
})
|
|
16
|
+
.catch((err) => {
|
|
17
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
});
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
// src/cli.ts
|
|
2
|
+
// Pure library module: exports HELP/runCli for the always-run entry
|
|
3
|
+
// (src/bin.ts) and for tests. Never self-executes -- see src/bin.ts for why.
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
5
|
import { parseArgs } from 'node:util';
|
|
5
|
-
import { resolve } from 'node:path';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
6
|
import process from 'node:process';
|
|
8
7
|
import { runDashboard } from './commands/dashboard.js';
|
|
9
8
|
import { runDoctor } from './commands/doctor.js';
|
|
@@ -11,7 +10,7 @@ import { runInit } from './commands/init.js';
|
|
|
11
10
|
import { runModels } from './commands/models.js';
|
|
12
11
|
import { runUninstall } from './commands/uninstall.js';
|
|
13
12
|
import { runUpdate } from './commands/update.js';
|
|
14
|
-
import {
|
|
13
|
+
import { KIT_VERSION } from './lib/version.js';
|
|
15
14
|
import { applyVersionHint, defaultFetchLatest } from './lib/version-check.js';
|
|
16
15
|
export const HELP = `super-backlog (sbl) - equip any project with Backlog.md + Superpowers
|
|
17
16
|
|
|
@@ -64,7 +63,7 @@ export async function runCli(argv) {
|
|
|
64
63
|
console.log(HELP);
|
|
65
64
|
return 0;
|
|
66
65
|
}
|
|
67
|
-
|
|
66
|
+
await applyVersionHint(KIT_VERSION, {
|
|
68
67
|
home: homedir(),
|
|
69
68
|
now: () => new Date(),
|
|
70
69
|
fetchLatest: defaultFetchLatest,
|
|
@@ -143,15 +142,3 @@ export async function runCli(argv) {
|
|
|
143
142
|
return 1;
|
|
144
143
|
}
|
|
145
144
|
}
|
|
146
|
-
assertNode20();
|
|
147
|
-
const entry = process.argv[1];
|
|
148
|
-
if (entry && fileURLToPath(import.meta.url) === resolve(entry)) {
|
|
149
|
-
runCli(process.argv.slice(2))
|
|
150
|
-
.then((code) => {
|
|
151
|
-
process.exitCode = code;
|
|
152
|
-
})
|
|
153
|
-
.catch((err) => {
|
|
154
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
155
|
-
process.exitCode = 1;
|
|
156
|
-
});
|
|
157
|
-
}
|
|
@@ -81,6 +81,25 @@ function waitForClose(hub) {
|
|
|
81
81
|
hub.server.once('close', () => resolve());
|
|
82
82
|
});
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Builds the hub shutdown routine: close the hub handle, then clear the
|
|
86
|
+
* on-disk hub.json owned by `pid`. The returned function is idempotent --
|
|
87
|
+
* calling it more than once (e.g. once from a signal handler, once from the
|
|
88
|
+
* caller's own cleanup) only runs the underlying work once and every caller
|
|
89
|
+
* observes the same result.
|
|
90
|
+
*/
|
|
91
|
+
export function createShutdown(hub, home, pid) {
|
|
92
|
+
let done = null;
|
|
93
|
+
return function shutdown() {
|
|
94
|
+
if (done === null) {
|
|
95
|
+
done = (async () => {
|
|
96
|
+
await hub.close();
|
|
97
|
+
clearHubState(home, pid);
|
|
98
|
+
})();
|
|
99
|
+
}
|
|
100
|
+
return done;
|
|
101
|
+
};
|
|
102
|
+
}
|
|
84
103
|
async function attachToHub(opts) {
|
|
85
104
|
let res;
|
|
86
105
|
try {
|
|
@@ -150,6 +169,10 @@ export async function runDashboard(cwd, args, deps = {}) {
|
|
|
150
169
|
try {
|
|
151
170
|
const status = await attach(`http://127.0.0.1:${state.port}/api/hub/status?token=${encodeURIComponent(state.token)}`, undefined);
|
|
152
171
|
if (status.status === 200) {
|
|
172
|
+
if (values['port'] !== undefined && port !== state.port) {
|
|
173
|
+
console.error(`error: a hub is already running on ${state.port}`);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
153
176
|
return await attachToHub({
|
|
154
177
|
cwd,
|
|
155
178
|
port: state.port,
|
|
@@ -199,8 +222,9 @@ export async function runDashboard(cwd, args, deps = {}) {
|
|
|
199
222
|
console.log(`serving dashboard at ${result.url} (press Ctrl+C to stop)`);
|
|
200
223
|
if (!noOpen)
|
|
201
224
|
openBrowser(result.url);
|
|
225
|
+
const shutdown = createShutdown(hub, home, pid);
|
|
202
226
|
const onSignal = () => {
|
|
203
|
-
void
|
|
227
|
+
void shutdown();
|
|
204
228
|
};
|
|
205
229
|
process.once('SIGINT', onSignal);
|
|
206
230
|
process.once('SIGTERM', onSignal);
|
|
@@ -211,6 +235,6 @@ export async function runDashboard(cwd, args, deps = {}) {
|
|
|
211
235
|
finally {
|
|
212
236
|
process.removeListener('SIGINT', onSignal);
|
|
213
237
|
process.removeListener('SIGTERM', onSignal);
|
|
214
|
-
|
|
238
|
+
await shutdown();
|
|
215
239
|
}
|
|
216
240
|
}
|
package/dist/dashboard/hub.js
CHANGED
|
@@ -12,20 +12,17 @@ import { projectSlug, realpathKey } from '../lib/slug.js';
|
|
|
12
12
|
import { KIT_VERSION } from '../lib/version.js';
|
|
13
13
|
import { createModelApiHandler } from '../models/dashboard-api.js';
|
|
14
14
|
const WATCH_WARN = 'warning: live reload is disabled because Node 24+ on Windows cannot reliably watch directories recursively (libuv fs-event bug); use Node 22 or Linux/macOS for live reload';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
console.warn(WATCH_WARN);
|
|
28
|
-
return null;
|
|
15
|
+
const ALLOWED_HOST = /^(127\.0\.0\.1|localhost)(:\d+)?$/;
|
|
16
|
+
function isAllowedHost(headerValue) {
|
|
17
|
+
const value = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
18
|
+
if (typeof value !== 'string')
|
|
19
|
+
return false;
|
|
20
|
+
return ALLOWED_HOST.test(value.trim().toLowerCase());
|
|
21
|
+
}
|
|
22
|
+
function hasJsonContentType(req) {
|
|
23
|
+
const value = req.headers['content-type'];
|
|
24
|
+
const ct = Array.isArray(value) ? value[0] : value;
|
|
25
|
+
return typeof ct === 'string' && ct.toLowerCase().startsWith('application/json');
|
|
29
26
|
}
|
|
30
27
|
function projectUrl(port, slug) {
|
|
31
28
|
return `http://127.0.0.1:${port}/p/${slug}/`;
|
|
@@ -66,6 +63,25 @@ export async function startHubServer(opts) {
|
|
|
66
63
|
const projects = new Map();
|
|
67
64
|
const token = opts.token;
|
|
68
65
|
let port = 0;
|
|
66
|
+
let watchWarned = false;
|
|
67
|
+
function watchBacklog(cwd, reloader) {
|
|
68
|
+
const backlogDir = join(cwd, 'backlog');
|
|
69
|
+
if (recursiveWatchSupported(process.platform, process.versions.node)) {
|
|
70
|
+
try {
|
|
71
|
+
const watcher = watch(backlogDir, { persistent: true, recursive: true }, () => reloader.trigger());
|
|
72
|
+
watcher.on('error', () => { });
|
|
73
|
+
return watcher;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!watchWarned) {
|
|
80
|
+
watchWarned = true;
|
|
81
|
+
console.warn(WATCH_WARN);
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
69
85
|
function disposeEntry(entry) {
|
|
70
86
|
entry.reloader.cancel();
|
|
71
87
|
entry.broker.close();
|
|
@@ -82,7 +98,7 @@ export async function startHubServer(opts) {
|
|
|
82
98
|
if (!computed.ok) {
|
|
83
99
|
return { ok: false, code: 400, message: 'empty slug' };
|
|
84
100
|
}
|
|
85
|
-
const slug =
|
|
101
|
+
const slug = computed.slug;
|
|
86
102
|
if (slug === '') {
|
|
87
103
|
return { ok: false, code: 400, message: 'empty slug' };
|
|
88
104
|
}
|
|
@@ -124,7 +140,15 @@ export async function startHubServer(opts) {
|
|
|
124
140
|
return { ok: true, slug, url };
|
|
125
141
|
}
|
|
126
142
|
async function handle(req, res) {
|
|
143
|
+
if (!isAllowedHost(req.headers.host)) {
|
|
144
|
+
sendText(res, 403, 'forbidden');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
127
147
|
const method = req.method ?? 'GET';
|
|
148
|
+
if (method === 'POST' && !hasJsonContentType(req)) {
|
|
149
|
+
sendText(res, 415, 'unsupported media type: expected application/json');
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
128
152
|
const parsed = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
129
153
|
const pathname = parsed.pathname;
|
|
130
154
|
if (pathname === '/' && method === 'GET') {
|
package/dist/lib/hub-state.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
2
|
+
import { chmodSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import process from 'node:process';
|
|
5
5
|
import { atomicWrite } from './atomic.js';
|
|
@@ -25,7 +25,13 @@ export function readHubState(home) {
|
|
|
25
25
|
}
|
|
26
26
|
export function writeHubState(home, state) {
|
|
27
27
|
mkdirSync(join(home, '.super-backlog'), { recursive: true });
|
|
28
|
-
|
|
28
|
+
const path = hubStatePath(home);
|
|
29
|
+
atomicWrite(path, JSON.stringify(state));
|
|
30
|
+
// hub.json carries the hub's auth token; keep it off other local accounts.
|
|
31
|
+
// win32 has no POSIX mode bits (ACLs govern access there instead).
|
|
32
|
+
if (process.platform !== 'win32') {
|
|
33
|
+
chmodSync(path, 0o600);
|
|
34
|
+
}
|
|
29
35
|
}
|
|
30
36
|
export function clearHubState(home, pid) {
|
|
31
37
|
const current = readHubState(home);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import process from 'node:process';
|
|
5
|
-
import
|
|
5
|
+
import spawn from 'cross-spawn';
|
|
6
6
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
7
7
|
const FETCH_TIMEOUT_MS = 2000;
|
|
8
8
|
function cachePath(home) {
|
|
@@ -45,23 +45,50 @@ function isStale(checkedAt, now) {
|
|
|
45
45
|
const t = Date.parse(checkedAt);
|
|
46
46
|
if (Number.isNaN(t))
|
|
47
47
|
return true;
|
|
48
|
-
|
|
48
|
+
const nowMs = now.getTime();
|
|
49
|
+
if (t > nowMs)
|
|
50
|
+
return true; // clock skew: a future checkedAt can never be trusted
|
|
51
|
+
return nowMs - t > DAY_MS;
|
|
52
|
+
}
|
|
53
|
+
// child.stdout is typed as Readable, but the underlying pipe stream (a
|
|
54
|
+
// net.Socket on POSIX, a Pipe wrap on Windows) always exposes unref() at
|
|
55
|
+
// runtime; the DOM/Node stream typings just don't declare it.
|
|
56
|
+
function unrefStream(stream) {
|
|
57
|
+
stream?.unref?.();
|
|
49
58
|
}
|
|
50
59
|
export async function defaultFetchLatest() {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
const work = new Promise((resolvePromise) => {
|
|
61
|
+
let child;
|
|
62
|
+
try {
|
|
63
|
+
child = spawn('npm', ['view', 'super-backlog', 'version'], {
|
|
64
|
+
cwd: process.cwd(),
|
|
65
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
resolvePromise(null);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let out = '';
|
|
73
|
+
unrefStream(child.stdout);
|
|
74
|
+
child.stdout?.on('data', (chunk) => {
|
|
75
|
+
out += chunk.toString('utf8');
|
|
76
|
+
});
|
|
77
|
+
child.on('error', () => resolvePromise(null));
|
|
78
|
+
child.on('close', (code) => {
|
|
79
|
+
if (code !== 0) {
|
|
80
|
+
resolvePromise(null);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const line = out.split(/\r?\n/).find((l) => l.trim() !== '');
|
|
84
|
+
const v = line?.trim();
|
|
85
|
+
resolvePromise(v === undefined || v === '' ? null : v);
|
|
86
|
+
});
|
|
87
|
+
child.unref();
|
|
61
88
|
});
|
|
62
89
|
let timer;
|
|
63
|
-
const timeout = new Promise((
|
|
64
|
-
timer = setTimeout(() =>
|
|
90
|
+
const timeout = new Promise((resolveTimeout) => {
|
|
91
|
+
timer = setTimeout(() => resolveTimeout(null), FETCH_TIMEOUT_MS);
|
|
65
92
|
timer.unref();
|
|
66
93
|
});
|
|
67
94
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "super-backlog",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "One command to equip any project with Backlog.md + Superpowers, plus a Project Dashboard.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"node": ">=20"
|
|
13
13
|
},
|
|
14
14
|
"bin": {
|
|
15
|
-
"sbl": "dist/
|
|
16
|
-
"super-backlog": "dist/
|
|
15
|
+
"sbl": "dist/bin.js",
|
|
16
|
+
"super-backlog": "dist/bin.js"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import spawn from 'cross-spawn';
|
|
2
|
-
import { resolveBacklogBin } from '../lib/run.js';
|
|
3
|
-
/** Run a backlog.md subcommand by delegating to the resolved backlog binary. */
|
|
4
|
-
export function runBacklogSubcommand(cwd, subcommand, args = []) {
|
|
5
|
-
const bin = resolveBacklogBin(cwd);
|
|
6
|
-
if (!bin) {
|
|
7
|
-
console.error('error: backlog CLI not found; is backlog.md installed?');
|
|
8
|
-
return Promise.resolve(1);
|
|
9
|
-
}
|
|
10
|
-
return new Promise((resolve) => {
|
|
11
|
-
const child = spawn(bin, [subcommand, ...args], {
|
|
12
|
-
cwd,
|
|
13
|
-
stdio: 'inherit',
|
|
14
|
-
});
|
|
15
|
-
child.on('error', () => resolve(1));
|
|
16
|
-
child.on('exit', (code) => resolve(code ?? 1));
|
|
17
|
-
});
|
|
18
|
-
}
|