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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/lib/hooks.ts
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
export const GUARD_MARKER = 'super-backlog guard';
|
|
6
|
+
export const REFRESH_MARKER = 'super-backlog dashboard-refresh';
|
|
7
|
+
function markerBlockRe(marker) {
|
|
8
|
+
return new RegExp(`^# >>> ${marker} [\\d.]+ >>>[\\s\\S]*?# <<< ${marker} <<<\\n?`, 'm');
|
|
9
|
+
}
|
|
10
|
+
export const GUARD_RE = markerBlockRe(GUARD_MARKER);
|
|
11
|
+
export const REFRESH_RE = markerBlockRe(REFRESH_MARKER);
|
|
12
|
+
function hookTemplate(file) {
|
|
13
|
+
const here = dirname(fileURLToPath(import.meta.url)); // dist/lib at runtime, src/lib under vitest
|
|
14
|
+
const candidates = [join(here, '..', 'templates', file), join(here, 'templates', file)];
|
|
15
|
+
for (const c of candidates)
|
|
16
|
+
if (existsSync(c))
|
|
17
|
+
return readFileSync(c, 'utf8');
|
|
18
|
+
throw new Error(`${file} template not found`);
|
|
19
|
+
}
|
|
20
|
+
function renderBlock(templateFile, version) {
|
|
21
|
+
return hookTemplate(templateFile).replace('{{VERSION}}', version);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Shared marker-block installer: swaps this kind's own block in place when
|
|
25
|
+
* present, otherwise appends after the current contents (a fresh hook file
|
|
26
|
+
* gets `#!/bin/sh` + block). Foreign content - including other super-backlog
|
|
27
|
+
* blocks - is always preserved.
|
|
28
|
+
*/
|
|
29
|
+
function installBlock(gitDir, hookName, re, block) {
|
|
30
|
+
const path = join(gitDir, 'hooks', hookName);
|
|
31
|
+
let next;
|
|
32
|
+
if (existsSync(path)) {
|
|
33
|
+
const cur = readFileSync(path, 'utf8');
|
|
34
|
+
next = re.test(cur) ? cur.replace(re, block) : cur.replace(/\n?$/, '\n') + block;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
next = '#!/bin/sh\n' + block;
|
|
38
|
+
}
|
|
39
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
40
|
+
writeFileSync(path, next);
|
|
41
|
+
try {
|
|
42
|
+
chmodSync(path, 0o755);
|
|
43
|
+
}
|
|
44
|
+
catch { /* best effort on Windows */ }
|
|
45
|
+
}
|
|
46
|
+
/** Shared marker-block remover: strips the own block; deletes the file when nothing but a shebang remains. */
|
|
47
|
+
function removeBlock(gitDir, hookName, re) {
|
|
48
|
+
const path = join(gitDir, 'hooks', hookName);
|
|
49
|
+
if (!existsSync(path))
|
|
50
|
+
return false;
|
|
51
|
+
const cur = readFileSync(path, 'utf8');
|
|
52
|
+
if (!re.test(cur))
|
|
53
|
+
return false;
|
|
54
|
+
const rest = cur.replace(re, '').trim();
|
|
55
|
+
if (rest === '' || rest === '#!/bin/sh')
|
|
56
|
+
rmSync(path);
|
|
57
|
+
else
|
|
58
|
+
writeFileSync(path, rest + '\n');
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
export function renderGuardHook(version) {
|
|
62
|
+
return renderBlock('guard-hook.sh', version);
|
|
63
|
+
}
|
|
64
|
+
export function installGuardHook(gitDir, version) {
|
|
65
|
+
installBlock(gitDir, 'pre-commit', GUARD_RE, renderGuardHook(version));
|
|
66
|
+
}
|
|
67
|
+
export function removeGuardHook(gitDir) {
|
|
68
|
+
return removeBlock(gitDir, 'pre-commit', GUARD_RE);
|
|
69
|
+
}
|
|
70
|
+
export function renderRefreshHook(version) {
|
|
71
|
+
return renderBlock('dashboard-refresh-hook.sh', version);
|
|
72
|
+
}
|
|
73
|
+
export function installRefreshHook(gitDir, version) {
|
|
74
|
+
installBlock(gitDir, 'post-commit', REFRESH_RE, renderRefreshHook(version));
|
|
75
|
+
}
|
|
76
|
+
export function removeRefreshHook(gitDir) {
|
|
77
|
+
return removeBlock(gitDir, 'post-commit', REFRESH_RE);
|
|
78
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/lib/markers.ts
|
|
2
|
+
const START_RE = /<!--\s*SUPER-BACKLOG:(\d+\.\d+\.\d+)\s*START\s*-->/;
|
|
3
|
+
export function markerStart(version) {
|
|
4
|
+
return `<!-- SUPER-BACKLOG:${version} START -->`;
|
|
5
|
+
}
|
|
6
|
+
export const MARKER_END = '<!-- SUPER-BACKLOG END -->';
|
|
7
|
+
function ownedSpan(content) {
|
|
8
|
+
const m = START_RE.exec(content);
|
|
9
|
+
if (!m || m.index === -1)
|
|
10
|
+
return null;
|
|
11
|
+
const start = m.index;
|
|
12
|
+
const endIdx = content.indexOf(MARKER_END, start);
|
|
13
|
+
if (endIdx === -1)
|
|
14
|
+
return null;
|
|
15
|
+
return { start, end: endIdx + MARKER_END.length };
|
|
16
|
+
}
|
|
17
|
+
export function injectBlock(content, version, block) {
|
|
18
|
+
const fresh = `${markerStart(version)}\n${block}\n${MARKER_END}`;
|
|
19
|
+
const span = ownedSpan(content);
|
|
20
|
+
if (!span) {
|
|
21
|
+
const sep = content.length === 0 ? '' : content.endsWith('\n') ? '' : '\n';
|
|
22
|
+
return { content: content + sep + fresh + '\n', action: 'created' };
|
|
23
|
+
}
|
|
24
|
+
const existing = content.slice(span.start, span.end);
|
|
25
|
+
if (existing === fresh)
|
|
26
|
+
return { content, action: 'unchanged' };
|
|
27
|
+
return {
|
|
28
|
+
content: content.slice(0, span.start) + fresh + content.slice(span.end),
|
|
29
|
+
action: 'replaced',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function stripOwned(content) {
|
|
33
|
+
const span = ownedSpan(content);
|
|
34
|
+
if (!span)
|
|
35
|
+
return { content, removed: false };
|
|
36
|
+
const before = content.slice(0, span.start);
|
|
37
|
+
let after = content.slice(span.end);
|
|
38
|
+
if (before.endsWith('\n') && after.startsWith('\n'))
|
|
39
|
+
after = after.slice(1);
|
|
40
|
+
return { content: before + after, removed: true };
|
|
41
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { OwnershipError } from './ownership.js';
|
|
2
|
+
export const PLUGIN_SPEC = 'superpowers@git+https://github.com/obra/superpowers.git';
|
|
3
|
+
export function applyPluginEntry(config) {
|
|
4
|
+
const base = config && typeof config === 'object' && !Array.isArray(config)
|
|
5
|
+
? { ...config }
|
|
6
|
+
: {};
|
|
7
|
+
const raw = base.plugin;
|
|
8
|
+
const list = Array.isArray(raw) ? [...raw] : raw === undefined ? [] : [raw];
|
|
9
|
+
if (list.includes(PLUGIN_SPEC))
|
|
10
|
+
return { config: base, changed: false };
|
|
11
|
+
const suspicious = list.find((e) => typeof e === 'string' && e.startsWith('superpowers@'));
|
|
12
|
+
if (suspicious !== undefined) {
|
|
13
|
+
throw new OwnershipError(`refusing to modify existing superpowers plugin entry "${String(suspicious)}" — resolve manually, then re-run`);
|
|
14
|
+
}
|
|
15
|
+
list.push(PLUGIN_SPEC);
|
|
16
|
+
return { config: { ...base, plugin: list }, changed: true };
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class OwnershipError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export const FINGERPRINT_RE = /<!--\s*managed-by:\s*super-backlog\s*v?\d+\.\d+\.\d+\s*-->/;
|
|
4
|
+
export function renderSkill(templateContent, version) {
|
|
5
|
+
const line = `<!-- managed-by: super-backlog ${version} -->`;
|
|
6
|
+
if (/^---\r?\n/.test(templateContent)) {
|
|
7
|
+
const close = templateContent.indexOf('\n---', 3);
|
|
8
|
+
const insertAt = templateContent.indexOf('\n', close + 1) + 1;
|
|
9
|
+
return (templateContent.slice(0, insertAt) + `\n${line}` + templateContent.slice(insertAt));
|
|
10
|
+
}
|
|
11
|
+
return `${line}\n${templateContent}`;
|
|
12
|
+
}
|
|
13
|
+
export function isOwnedSkillFile(content) {
|
|
14
|
+
return FINGERPRINT_RE.test(content);
|
|
15
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// src/lib/pkgjson.ts
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export const WANTED_SCRIPTS = {
|
|
5
|
+
tasks: 'backlog task list',
|
|
6
|
+
board: 'backlog board',
|
|
7
|
+
browser: 'backlog browser',
|
|
8
|
+
dashboard: 'super-backlog dashboard',
|
|
9
|
+
};
|
|
10
|
+
export const WANTED_DEVS = {
|
|
11
|
+
'backlog.md': 'latest',
|
|
12
|
+
'super-backlog': 'latest',
|
|
13
|
+
};
|
|
14
|
+
export function readPkgJson(cwd) {
|
|
15
|
+
const p = join(cwd, 'package.json');
|
|
16
|
+
if (!existsSync(p))
|
|
17
|
+
return null;
|
|
18
|
+
return JSON.parse(readFileSync(p, 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
export function mergeScripts(pkg, wanted) {
|
|
21
|
+
const scripts = { ...(pkg.scripts ?? {}) };
|
|
22
|
+
const added = [];
|
|
23
|
+
for (const [name, cmd] of Object.entries(wanted)) {
|
|
24
|
+
if (!(name in scripts)) {
|
|
25
|
+
scripts[name] = cmd;
|
|
26
|
+
added.push(name);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { pkg: { ...pkg, ...(added.length ? { scripts } : {}) }, added };
|
|
30
|
+
}
|
|
31
|
+
export function addDevDependencies(pkg, deps) {
|
|
32
|
+
const devDependencies = { ...(pkg.devDependencies ?? {}) };
|
|
33
|
+
const added = [];
|
|
34
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
35
|
+
if (!(name in devDependencies)) {
|
|
36
|
+
devDependencies[name] = spec;
|
|
37
|
+
added.push(name);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { pkg: { ...pkg, ...(added.length ? { devDependencies } : {}) }, added };
|
|
41
|
+
}
|
package/dist/lib/pm.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/lib/pm.ts
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
export function detectPackageManager(cwd) {
|
|
5
|
+
const has = (f) => existsSync(join(cwd, f));
|
|
6
|
+
if (!has('package.json'))
|
|
7
|
+
return null;
|
|
8
|
+
if (has('pnpm-lock.yaml'))
|
|
9
|
+
return 'pnpm';
|
|
10
|
+
if (has('bun.lockb') || has('bun.lock'))
|
|
11
|
+
return 'bun';
|
|
12
|
+
return 'npm';
|
|
13
|
+
}
|
|
14
|
+
export function installCmdsFor(pm, pkgs) {
|
|
15
|
+
switch (pm) {
|
|
16
|
+
case 'pnpm': return { cmd: 'pnpm', args: ['add', '-D', ...pkgs] };
|
|
17
|
+
case 'bun': return { cmd: 'bun', args: ['add', '-d', ...pkgs] };
|
|
18
|
+
default: return { cmd: 'npm', args: ['install', '--save-dev', ...pkgs] };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/lib/powershell.ts
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
const defaultExecutor = (cmd, args) => {
|
|
5
|
+
const r = spawnSync(cmd, args, { encoding: 'utf8', windowsHide: true });
|
|
6
|
+
if (r.error)
|
|
7
|
+
return { status: null, stdout: '', stderr: String(r.error.message) };
|
|
8
|
+
return { status: r.status, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
|
|
9
|
+
};
|
|
10
|
+
export function getEffectiveExecutionPolicy(deps = {}) {
|
|
11
|
+
const fake = (deps.fakePolicy ?? process.env.SBL_FAKE_POLICY)?.trim();
|
|
12
|
+
if (fake)
|
|
13
|
+
return fake;
|
|
14
|
+
const platform = deps.platform ?? process.platform;
|
|
15
|
+
if (platform !== 'win32')
|
|
16
|
+
return null;
|
|
17
|
+
const executor = deps.executor ?? defaultExecutor;
|
|
18
|
+
const r = executor('powershell.exe', [
|
|
19
|
+
'-NoProfile',
|
|
20
|
+
'-NonInteractive',
|
|
21
|
+
'-Command',
|
|
22
|
+
'Get-ExecutionPolicy',
|
|
23
|
+
]);
|
|
24
|
+
if ((r.status === null || r.status !== 0))
|
|
25
|
+
return null;
|
|
26
|
+
const policy = r.stdout.trim();
|
|
27
|
+
if (policy === '')
|
|
28
|
+
return null;
|
|
29
|
+
return policy;
|
|
30
|
+
}
|
|
31
|
+
export function isBlockingExecutionPolicy(policy) {
|
|
32
|
+
if (!policy)
|
|
33
|
+
return false;
|
|
34
|
+
const normalized = policy.trim().toLowerCase();
|
|
35
|
+
return normalized === 'restricted' || normalized === 'allsigned';
|
|
36
|
+
}
|
|
37
|
+
export function policyWarningLines(policy) {
|
|
38
|
+
return [
|
|
39
|
+
`warning: PowerShell execution policy "${policy}" blocks direct npx/npm/sbl calls in PowerShell (.ps1 shims are not loadable)`,
|
|
40
|
+
' fix: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned (one-time, no admin needed)',
|
|
41
|
+
' alt: call the .cmd shims explicitly (npx.cmd super-backlog init) or run from cmd.exe',
|
|
42
|
+
' note: npm scripts like "npm run board" are unaffected (they execute via cmd.exe)',
|
|
43
|
+
];
|
|
44
|
+
}
|
package/dist/lib/run.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
export function runCapture(cmd, args, cwd) {
|
|
6
|
+
// shell on win32 — safe only because callers pass constant args; never pass user input here.
|
|
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 });
|
|
10
|
+
if (r.error && (r.status === null || r.status === undefined)) {
|
|
11
|
+
return { status: 127, stdout: '', stderr: String(r.error.message) };
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
status: r.status ?? 1,
|
|
15
|
+
stdout: (r.stdout ?? '').toString(),
|
|
16
|
+
stderr: (r.stderr ?? '').toString(),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const EXT = process.platform === 'win32' ? '.cmd' : '';
|
|
20
|
+
export function resolveBacklogBin(cwd) {
|
|
21
|
+
const local = join(cwd, 'node_modules', '.bin', `backlog${EXT}`);
|
|
22
|
+
if (existsSync(local))
|
|
23
|
+
return local;
|
|
24
|
+
const probe = process.platform === 'win32' ? 'where' : 'which';
|
|
25
|
+
const w = runCapture(probe, ['backlog'], cwd);
|
|
26
|
+
if (w.status === 0) {
|
|
27
|
+
const first = w.stdout.split(/\r?\n/).find(Boolean);
|
|
28
|
+
if (first)
|
|
29
|
+
return first.trim().replace(/\.ps1$/i, '.cmd');
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function validateTaskMarkdown(filename, content) {
|
|
2
|
+
const errors = [];
|
|
3
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
4
|
+
if (!m)
|
|
5
|
+
return [`backlog/tasks/${filename}: missing YAML frontmatter (edit tasks via the backlog CLI instead)`];
|
|
6
|
+
const fm = m[1];
|
|
7
|
+
const field = (name) => {
|
|
8
|
+
const fmMatch = new RegExp(`^${name}:\\s*(.*?)\\s*$`, 'm').exec(fm);
|
|
9
|
+
return fmMatch ? fmMatch[1].replace(/^["']|["']$/g, '') : null;
|
|
10
|
+
};
|
|
11
|
+
const stem = filename.replace(/\.md$/, '');
|
|
12
|
+
const base = stem.includes(' - ') ? stem.split(' - ')[0] : stem;
|
|
13
|
+
const id = field('id');
|
|
14
|
+
if (!id)
|
|
15
|
+
errors.push(`backlog/tasks/${filename}: missing 'id' field`);
|
|
16
|
+
else if (id.toLowerCase() !== base.toLowerCase())
|
|
17
|
+
errors.push(`backlog/tasks/${filename}: id '${id}' does not match filename stem '${base}'`);
|
|
18
|
+
const title = field('title');
|
|
19
|
+
if (!title)
|
|
20
|
+
errors.push(`backlog/tasks/${filename}: empty or missing 'title'`);
|
|
21
|
+
return errors;
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/lib/version.ts
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
export const KIT_VERSION = require('../../package.json').version ?? '0.0.0';
|
|
6
|
+
export function assertNode20() {
|
|
7
|
+
const major = Number(process.versions.node.split('.')[0]);
|
|
8
|
+
if (major < 20) {
|
|
9
|
+
console.error(`super-backlog requires Node >= 20 (found ${process.versions.node}).`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/lib/yamlmini.ts
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
export function readSimpleKeys(filePath, keys) {
|
|
4
|
+
const out = {};
|
|
5
|
+
for (const k of keys)
|
|
6
|
+
out[k] = undefined;
|
|
7
|
+
if (!existsSync(filePath))
|
|
8
|
+
return out;
|
|
9
|
+
const wanted = new Set(keys);
|
|
10
|
+
for (const line of readFileSync(filePath, 'utf8').split(/\r?\n/)) {
|
|
11
|
+
const m = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line.trim());
|
|
12
|
+
if (!m || !wanted.has(m[1]))
|
|
13
|
+
continue;
|
|
14
|
+
const raw = m[2].trim();
|
|
15
|
+
out[m[1]] = raw.replace(/^["'](.*)["']$/, '$1');
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# >>> super-backlog dashboard-refresh {{VERSION}} >>>
|
|
2
|
+
# Regenerates dashboard.html when the just-created commit touched backlog/.
|
|
3
|
+
# Post-commit only: this block NEVER blocks a commit - any failure is just a
|
|
4
|
+
# stderr note and the exit status stays 0.
|
|
5
|
+
root=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
6
|
+
if [ -n "$root" ] && [ -f "$root/node_modules/super-backlog/dist/dashboard/regen.js" ]; then
|
|
7
|
+
touch_backlog=0
|
|
8
|
+
if ! git rev-parse --verify --quiet HEAD~1 >/dev/null 2>&1; then
|
|
9
|
+
touch_backlog=1 # initial commit: nothing to diff against - regenerate
|
|
10
|
+
elif [ -n "$(git diff --name-only HEAD~1 HEAD -- 'backlog/*' 2>/dev/null)" ]; then
|
|
11
|
+
touch_backlog=1
|
|
12
|
+
fi
|
|
13
|
+
if [ "$touch_backlog" -eq 1 ]; then
|
|
14
|
+
node "$root/node_modules/super-backlog/dist/dashboard/regen.js" >/dev/null 2>&1 || echo "super-backlog: dashboard regeneration failed (see npm run dashboard)" >&2
|
|
15
|
+
fi
|
|
16
|
+
fi
|
|
17
|
+
exit 0
|
|
18
|
+
# <<< super-backlog dashboard-refresh <<<
|