@polderlabs/bizar 3.21.0 → 3.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/bin.mjs +21 -1
- package/cli/heads-up.mjs +295 -0
- package/cli/init.mjs +34 -0
- package/cli/update.mjs +218 -6
- package/config/agents/_shared/AGENT_BASELINE.md +80 -0
- package/config/opencode.json +11 -23
- package/config/skills/glyph/SKILL.md +174 -0
- package/config/skills/lightrag/SKILL.md +81 -0
- package/config/skills/obsidian/SKILL.md +101 -0
- package/config/skills/read-the-damn-docs/SKILL.md +113 -0
- package/install.sh +245 -2
- package/package.json +1 -1
package/cli/bin.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { runInit } from './init.mjs';
|
|
|
25
25
|
import { runExport } from './export.mjs';
|
|
26
26
|
import runArtifact from './artifact.mjs';
|
|
27
27
|
import { runUpdate } from './update.mjs';
|
|
28
|
+
import { runHeadsUp } from './heads-up.mjs';
|
|
28
29
|
import { ensureSetup, checkSetupStatus } from './bootstrap.mjs';
|
|
29
30
|
|
|
30
31
|
const args = process.argv.slice(2);
|
|
@@ -90,6 +91,7 @@ function showHelp() {
|
|
|
90
91
|
dev-link [src] Symlink the local plugin source into opencode's plugin dir
|
|
91
92
|
dev-unlink Remove the dev symlink and restore the deployed copy
|
|
92
93
|
doctor Check the BizarHarness install for health issues
|
|
94
|
+
heads-up <subcommand> Manage pre-push / pre-release heads-ups (list/check/archive)
|
|
93
95
|
mod <subcommand> Manage mods (install/upgrade/list via the dashboard API)
|
|
94
96
|
|
|
95
97
|
Examples:
|
|
@@ -660,6 +662,9 @@ async function main() {
|
|
|
660
662
|
const result = await runDoctor();
|
|
661
663
|
if (result.failed > 0) process.exit(1);
|
|
662
664
|
}
|
|
665
|
+
} else if (args[0] === 'heads-up') {
|
|
666
|
+
// v3.21.0 — Pre-push / pre-release heads-ups
|
|
667
|
+
await runHeadsUp(args[1], args.slice(2));
|
|
663
668
|
} else if (args[0] === 'plan') {
|
|
664
669
|
await runArtifact(args.slice(1), {});
|
|
665
670
|
} else if (args[0] === 'install') {
|
|
@@ -757,7 +762,22 @@ async function loadDashCli() {
|
|
|
757
762
|
// fall through to file probing
|
|
758
763
|
}
|
|
759
764
|
|
|
760
|
-
// Try 2:
|
|
765
|
+
// Try 2: dynamic npm root -g (most reliable for global installs)
|
|
766
|
+
try {
|
|
767
|
+
const { execFileSync } = await import('node:child_process');
|
|
768
|
+
const { pathToFileURL } = await import('node:url');
|
|
769
|
+
const { join } = await import('node:path');
|
|
770
|
+
const npmRoot = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', timeout: 5000 }).trim();
|
|
771
|
+
if (npmRoot) {
|
|
772
|
+
const cliPath = join(npmRoot, '@polderlabs', 'bizar-dash', 'src', 'cli.mjs');
|
|
773
|
+
const mod = await import(pathToFileURL(cliPath).href);
|
|
774
|
+
return mod;
|
|
775
|
+
}
|
|
776
|
+
} catch (_e) {
|
|
777
|
+
/* fall through */
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Try 3: file path probing
|
|
761
781
|
const { join } = await import('node:path');
|
|
762
782
|
const { homedir } = await import('node:os');
|
|
763
783
|
const candidates = [
|
package/cli/heads-up.mjs
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* heads-up.mjs — `bizar heads-up` subcommand.
|
|
3
|
+
*
|
|
4
|
+
* Reads and manages .bizar/PRE_PUSH_NOTES.md — a per-project file for
|
|
5
|
+
* tracking gotchas, risks, and things-to-verify before pushing/publishing.
|
|
6
|
+
*
|
|
7
|
+
* Subcommands:
|
|
8
|
+
* list Print active heads-up entries
|
|
9
|
+
* check Exit code 0 (no blockers) or 1 (blockers found); batches
|
|
10
|
+
* with warnings printed to stderr
|
|
11
|
+
* archive Move all active entries to the Resolved section
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Entry parsing
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse entries from the "Active heads-ups" section of a PRE_PUSH_NOTES.md.
|
|
24
|
+
* Returns an array of { title, severity, area, affected, description,
|
|
25
|
+
* mitigation, verified, lines } objects.
|
|
26
|
+
*/
|
|
27
|
+
function parseActiveEntries(content) {
|
|
28
|
+
const entries = [];
|
|
29
|
+
|
|
30
|
+
// Find the Active heads-ups section. We stop at the next h2 that starts
|
|
31
|
+
// with an uppercase letter (section header like "## Resolved") but NOT
|
|
32
|
+
// at entry titles that start with a date bracket "## [YYYY-MM-DD]".
|
|
33
|
+
const activeMatch = content.match(/## Active heads-ups\n([\s\S]*?)(?=\n## [A-Z]|$)/);
|
|
34
|
+
if (!activeMatch) return entries;
|
|
35
|
+
|
|
36
|
+
const section = activeMatch[1];
|
|
37
|
+
|
|
38
|
+
// Each entry starts with "## [YYYY-MM-DD] ..."
|
|
39
|
+
const entryBlocks = section.split(/(?=^## \[)/m);
|
|
40
|
+
for (const block of entryBlocks) {
|
|
41
|
+
const trimmed = block.trim();
|
|
42
|
+
if (!trimmed || trimmed.startsWith('<!--')) continue;
|
|
43
|
+
|
|
44
|
+
const titleMatch = trimmed.match(/^##\s+(\[.+?\]\s*.+)$/m);
|
|
45
|
+
if (!titleMatch) continue;
|
|
46
|
+
|
|
47
|
+
const severityMatch = trimmed.match(/^Severity:\s*(\S+)/m);
|
|
48
|
+
const areaMatch = trimmed.match(/^Area:\s*(.+)$/m);
|
|
49
|
+
const affectedMatch = trimmed.match(/^Affected versions:\s*(.+)$/m);
|
|
50
|
+
const descMatch = trimmed.match(/^Description:\s*(.+)$/m);
|
|
51
|
+
const mitigMatch = trimmed.match(/^Mitigation:\s*(.+)$/m);
|
|
52
|
+
const verifiedMatch = trimmed.match(/^Verified:\s*(.+)$/m);
|
|
53
|
+
|
|
54
|
+
entries.push({
|
|
55
|
+
title: titleMatch[1].trim(),
|
|
56
|
+
severity: severityMatch ? severityMatch[1].trim().toLowerCase() : 'info',
|
|
57
|
+
area: areaMatch ? areaMatch[1].trim() : null,
|
|
58
|
+
affected: affectedMatch ? affectedMatch[1].trim() : null,
|
|
59
|
+
description: descMatch ? descMatch[1].trim() : null,
|
|
60
|
+
mitigation: mitigMatch ? mitigMatch[1].trim() : null,
|
|
61
|
+
verified: verifiedMatch ? verifiedMatch[1].trim() : '☐',
|
|
62
|
+
lines: trimmed,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return entries;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// File helpers
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
function readPrePushNotes(bizarDir) {
|
|
74
|
+
const file = join(bizarDir, 'PRE_PUSH_NOTES.md');
|
|
75
|
+
if (!existsSync(file)) return null;
|
|
76
|
+
return readFileSync(file, 'utf8');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function findBizarDir(cwd) {
|
|
80
|
+
// Walk up from cwd looking for .bizar/
|
|
81
|
+
let dir = cwd;
|
|
82
|
+
for (let i = 0; i < 10; i++) {
|
|
83
|
+
const candidate = join(dir, '.bizar');
|
|
84
|
+
if (existsSync(candidate)) return candidate;
|
|
85
|
+
const parent = join(dir, '..');
|
|
86
|
+
if (parent === dir) break;
|
|
87
|
+
dir = parent;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Severity label helpers
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
function severityLabel(severity) {
|
|
97
|
+
switch (severity) {
|
|
98
|
+
case 'blocker': return chalk.bold.red('blocker');
|
|
99
|
+
case 'warning': return chalk.bold.yellow('warning');
|
|
100
|
+
case 'info': return chalk.bold.blue('info');
|
|
101
|
+
default: return severity;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Subcommands
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* `bizar heads-up list` — print active entries.
|
|
111
|
+
*/
|
|
112
|
+
async function listHeadsUps(bizarDir) {
|
|
113
|
+
const content = readPrePushNotes(bizarDir);
|
|
114
|
+
if (!content) {
|
|
115
|
+
console.log(chalk.yellow(' No .bizar/PRE_PUSH_NOTES.md found. Run `bizar init` first.'));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const entries = parseActiveEntries(content);
|
|
120
|
+
if (entries.length === 0) {
|
|
121
|
+
console.log(chalk.green(' ✓ No active heads-ups.'));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(chalk.bold('\n ⚠ Active heads-ups:\n'));
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
console.log(` ${chalk.bold(entry.title)} ${severityLabel(entry.severity)}`);
|
|
128
|
+
if (entry.area) console.log(` Area: ${entry.area}`);
|
|
129
|
+
if (entry.affected) console.log(` Affects: ${entry.affected}`);
|
|
130
|
+
if (entry.description) console.log(` ${chalk.dim(entry.description)}`);
|
|
131
|
+
if (entry.mitigation) console.log(` Mitigation: ${chalk.cyan(entry.mitigation)}`);
|
|
132
|
+
console.log(` Verified: ${entry.verified === '☑' ? chalk.green('☑') : chalk.dim('☐')}`);
|
|
133
|
+
console.log('');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* `bizar heads-up check` — exit 0 if clear, 1 if blockers.
|
|
139
|
+
* Used by update/publish flows as a pre-flight gate.
|
|
140
|
+
*/
|
|
141
|
+
async function checkHeadsUps(bizarDir) {
|
|
142
|
+
const content = readPrePushNotes(bizarDir);
|
|
143
|
+
if (!content) {
|
|
144
|
+
// No file = no pre-push notes = clear
|
|
145
|
+
return { ok: true, blockerCount: 0, warningCount: 0, entries: [] };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const entries = parseActiveEntries(content);
|
|
149
|
+
const blockers = entries.filter((e) => e.severity === 'blocker');
|
|
150
|
+
const warnings = entries.filter((e) => e.severity === 'warning');
|
|
151
|
+
|
|
152
|
+
if (blockers.length > 0) {
|
|
153
|
+
console.error(chalk.bold.red(`\n ✗ ${blockers.length} blocker(s) in active heads-ups:\n`));
|
|
154
|
+
for (const b of blockers) {
|
|
155
|
+
console.error(chalk.red(` • ${b.title}`));
|
|
156
|
+
if (b.description) console.error(chalk.dim(` ${b.description}`));
|
|
157
|
+
if (b.mitigation) console.error(chalk.cyan(` Fix: ${b.mitigation}`));
|
|
158
|
+
console.error('');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (warnings.length > 0) {
|
|
162
|
+
console.error(chalk.yellow(`\n ⚠ ${warnings.length} warning(s) in active heads-ups:\n`));
|
|
163
|
+
for (const w of warnings) {
|
|
164
|
+
console.error(chalk.yellow(` • ${w.title}`));
|
|
165
|
+
if (w.description) console.error(chalk.dim(` ${w.description}`));
|
|
166
|
+
console.error('');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
ok: blockers.length === 0,
|
|
172
|
+
blockerCount: blockers.length,
|
|
173
|
+
warningCount: warnings.length,
|
|
174
|
+
entries,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* `bizar heads-up archive` — move all active entries to the Resolved section.
|
|
180
|
+
*/
|
|
181
|
+
async function archiveHeadsUps(bizarDir) {
|
|
182
|
+
const file = join(bizarDir, 'PRE_PUSH_NOTES.md');
|
|
183
|
+
const content = readPrePushNotes(bizarDir);
|
|
184
|
+
if (!content) {
|
|
185
|
+
console.log(chalk.yellow(' No .bizar/PRE_PUSH_NOTES.md found. Nothing to archive.'));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const entries = parseActiveEntries(content);
|
|
190
|
+
if (entries.length === 0) {
|
|
191
|
+
console.log(chalk.dim(' No active entries to archive.'));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Build resolved entries block (with indented detail lines for readability)
|
|
196
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
197
|
+
const newResolvedEntries = entries
|
|
198
|
+
.map((e) => {
|
|
199
|
+
const lines = e.lines.split('\n');
|
|
200
|
+
const header = lines[0];
|
|
201
|
+
const body = lines.slice(1).map((l) => ` ${l}`).join('\n');
|
|
202
|
+
return `${header}\n${body}`;
|
|
203
|
+
})
|
|
204
|
+
.join('\n\n');
|
|
205
|
+
|
|
206
|
+
// Clean the Active section — strip entries, keep header + comment
|
|
207
|
+
const cleanActive = '## Active heads-ups\n\n<!--\n Entries archived. See "Resolved" below.\n-->\n';
|
|
208
|
+
|
|
209
|
+
let newContent;
|
|
210
|
+
|
|
211
|
+
// Check if a "## Resolved" section already exists
|
|
212
|
+
const resolvedMatch = content.match(/## Resolved[\s\S]*$/);
|
|
213
|
+
if (resolvedMatch) {
|
|
214
|
+
// Remove active entries from the Active section
|
|
215
|
+
newContent = content.replace(/## Active heads-ups[\s\S]*?(?=\n## Resolved|$)/, cleanActive);
|
|
216
|
+
|
|
217
|
+
// Prepend new resolved entries into the existing Resolved section,
|
|
218
|
+
// right after the header (first line "## Resolved").
|
|
219
|
+
const oldResolved = resolvedMatch[0];
|
|
220
|
+
const firstNewlineAfterHeader = oldResolved.indexOf('\n');
|
|
221
|
+
const prefix = oldResolved.slice(0, firstNewlineAfterHeader + 1); // "## Resolved\n"
|
|
222
|
+
const suffix = oldResolved.slice(firstNewlineAfterHeader + 1); // everything after
|
|
223
|
+
|
|
224
|
+
const updatedResolved = `${prefix}<!-- Resolved on ${today} -->\n\n${newResolvedEntries}\n\n${suffix.trimStart()}\n`;
|
|
225
|
+
|
|
226
|
+
newContent = newContent.replace(resolvedMatch[0], updatedResolved);
|
|
227
|
+
} else {
|
|
228
|
+
// No existing Resolved section — replace Active section and append Resolved
|
|
229
|
+
newContent = content.replace(/## Active heads-ups[\s\S]*?(?=\n## |$)/, cleanActive);
|
|
230
|
+
const resolvedSection = `## Resolved\n\n<!-- Resolved on ${today} -->\n\n${newResolvedEntries}\n`;
|
|
231
|
+
newContent = `${newContent.trim()}\n\n${resolvedSection}\n`;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
writeFileSync(file, newContent);
|
|
235
|
+
console.log(chalk.green(` ✓ Archived ${entries.length} heads-up(s) to Resolved section.`));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// Help
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
function showHeadsUpHelp() {
|
|
243
|
+
console.log(`
|
|
244
|
+
bizar heads-up — Manage pre-push / pre-release heads-ups
|
|
245
|
+
|
|
246
|
+
Usage:
|
|
247
|
+
bizar heads-up list Show active heads-up entries
|
|
248
|
+
bizar heads-up check Exit 0 if no blockers, 1 if blockers exist
|
|
249
|
+
bizar heads-up archive Move all active entries to Resolved
|
|
250
|
+
|
|
251
|
+
Description:
|
|
252
|
+
Reads .bizar/PRE_PUSH_NOTES.md in the current (or nearest parent)
|
|
253
|
+
project. The file is created automatically by \`bizar init\`.
|
|
254
|
+
|
|
255
|
+
Add entries as agent-discovered gotchas before pushing or publishing.
|
|
256
|
+
The \`check\` subcommand is invoked automatically by \`bizar update\`
|
|
257
|
+
to gate pushes when unresolved blockers exist.
|
|
258
|
+
`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// Main entry point
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
export async function runHeadsUp(sub, args = []) {
|
|
266
|
+
const bizarDir = findBizarDir(process.cwd());
|
|
267
|
+
|
|
268
|
+
if (!sub || sub === '--help' || sub === '-h') {
|
|
269
|
+
showHeadsUpHelp();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!bizarDir) {
|
|
274
|
+
console.log(chalk.yellow(' No .bizar/ directory found in this project.'));
|
|
275
|
+
console.log(chalk.dim(' Run `bizar init` to create one.'));
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (sub === 'list') {
|
|
280
|
+
await listHeadsUps(bizarDir);
|
|
281
|
+
} else if (sub === 'check') {
|
|
282
|
+
const result = await checkHeadsUps(bizarDir);
|
|
283
|
+
if (!result.ok) process.exit(1);
|
|
284
|
+
console.log(chalk.green(' ✓ No active blocker heads-ups.'));
|
|
285
|
+
} else if (sub === 'archive') {
|
|
286
|
+
await archiveHeadsUps(bizarDir);
|
|
287
|
+
} else {
|
|
288
|
+
console.error(chalk.red(` ✗ Unknown heads-up subcommand: ${sub}`));
|
|
289
|
+
showHeadsUpHelp();
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Also export utilities so update.mjs and other commands can import them
|
|
295
|
+
export { checkHeadsUps, parseActiveEntries, findBizarDir, readPrePushNotes };
|
package/cli/init.mjs
CHANGED
|
@@ -56,6 +56,37 @@ function detectStack(cwd) {
|
|
|
56
56
|
return stack;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Write the .bizar/PRE_PUSH_NOTES.md template file.
|
|
61
|
+
* Created during `bizar init` with empty active/resolved sections.
|
|
62
|
+
*/
|
|
63
|
+
export function writePrePushNotesFile(bizarDir) {
|
|
64
|
+
const ppnPath = join(bizarDir, 'PRE_PUSH_NOTES.md');
|
|
65
|
+
const template = `# Pre-push / Pre-release Heads-up
|
|
66
|
+
|
|
67
|
+
> File for tracking gotchas, risks, and things-to-verify before pushing or publishing.
|
|
68
|
+
> Each entry is archived (moved to \`_archive/\`) after the push that resolves it.
|
|
69
|
+
|
|
70
|
+
## Active heads-ups
|
|
71
|
+
|
|
72
|
+
<!--
|
|
73
|
+
## [YYYY-MM-DD] Short title
|
|
74
|
+
Severity: blocker | warning | info
|
|
75
|
+
Area: cli | dash | plugin | npm | docs | build
|
|
76
|
+
Affected versions: v3.20.0–v3.21.0
|
|
77
|
+
Description: What could go wrong. Be specific.
|
|
78
|
+
Mitigation: What to do before pushing.
|
|
79
|
+
Verified: ☐
|
|
80
|
+
-->
|
|
81
|
+
|
|
82
|
+
## Resolved
|
|
83
|
+
|
|
84
|
+
<!-- Entries moved here after the push that resolved them. Keep last 5. -->
|
|
85
|
+
`;
|
|
86
|
+
writeFileSync(ppnPath, template);
|
|
87
|
+
console.log(chalk.green(` ✓ Created ${ppnPath}`));
|
|
88
|
+
}
|
|
89
|
+
|
|
59
90
|
export async function runInit(cwd) {
|
|
60
91
|
console.log(chalk.bold.hex('#10b981')('\n ᛗ BIZARHARNESS INIT ᛗ\n'));
|
|
61
92
|
|
|
@@ -150,6 +181,9 @@ ${stack.runner ? `- Dev: \`${stack.runner}\`` : ''}
|
|
|
150
181
|
console.log(chalk.green(` ✓ Created ${siPath}`));
|
|
151
182
|
}
|
|
152
183
|
|
|
184
|
+
// Generate PRE_PUSH_NOTES.md
|
|
185
|
+
writePrePushNotesFile(bizarDir);
|
|
186
|
+
|
|
153
187
|
// Build per-project knowledge graph (graphify -> .bizar/graph/)
|
|
154
188
|
// Soft step: never fails init. If graphify is missing or build errors,
|
|
155
189
|
// the user can retry manually with `bizar graph build`.
|
package/cli/update.mjs
CHANGED
|
@@ -37,12 +37,15 @@
|
|
|
37
37
|
import chalk from 'chalk';
|
|
38
38
|
import { execSync, spawn, spawnSync } from 'node:child_process';
|
|
39
39
|
import { existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
40
|
+
import { cp } from 'node:fs/promises';
|
|
40
41
|
import { homedir } from 'node:os';
|
|
41
42
|
import { join, dirname } from 'node:path';
|
|
42
43
|
import { fileURLToPath } from 'node:url';
|
|
44
|
+
import { checkHeadsUps, findBizarDir } from './heads-up.mjs';
|
|
43
45
|
|
|
44
46
|
const __filename = fileURLToPath(import.meta.url);
|
|
45
47
|
const __dirname = dirname(__filename);
|
|
48
|
+
const REPO_ROOT = join(__dirname, '..');
|
|
46
49
|
|
|
47
50
|
const PKG_MAIN = '@polderlabs/bizar';
|
|
48
51
|
const PKG_DASH = '@polderlabs/bizar-dash';
|
|
@@ -454,6 +457,111 @@ function spawnFreshDashboard({ port } = {}) {
|
|
|
454
457
|
}
|
|
455
458
|
}
|
|
456
459
|
|
|
460
|
+
// ---------------------------------------------------------------------------
|
|
461
|
+
// Repo-level update helpers
|
|
462
|
+
// ---------------------------------------------------------------------------
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Pull the latest changes from the git origin. Exits the process if the
|
|
466
|
+
* pull fails (merge conflict etc.), matching the task flow — an update
|
|
467
|
+
* should not proceed when the working tree is dirty.
|
|
468
|
+
*/
|
|
469
|
+
function runGitPull({ dryRun = false } = {}) {
|
|
470
|
+
if (dryRun) {
|
|
471
|
+
console.log(chalk.dim(' [dry-run] would run: git pull --rebase'));
|
|
472
|
+
return { ok: true, message: '[dry-run] git pull --rebase' };
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
execSync('git pull --rebase', { stdio: 'inherit', cwd: REPO_ROOT, timeout: 60000 });
|
|
476
|
+
return { ok: true, message: 'git pull --rebase succeeded' };
|
|
477
|
+
} catch {
|
|
478
|
+
return { ok: false, message: 'git pull --rebase failed — resolve conflicts manually' };
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Copy bundled skills from config/skills/ to the user's .opencode/skills/
|
|
484
|
+
* directory. Currently installs: obsidian, glyph, read-the-damn-docs.
|
|
485
|
+
*/
|
|
486
|
+
async function installSkills({ dryRun = false } = {}) {
|
|
487
|
+
const skills = ['obsidian', 'glyph', 'read-the-damn-docs'];
|
|
488
|
+
const results = [];
|
|
489
|
+
for (const skill of skills) {
|
|
490
|
+
const src = join(REPO_ROOT, 'config', 'skills', skill);
|
|
491
|
+
const dst = join(homedir(), '.opencode', 'skills', skill);
|
|
492
|
+
if (dryRun) {
|
|
493
|
+
if (existsSync(src)) {
|
|
494
|
+
console.log(chalk.dim(` [dry-run] would install skill: ${skill}`));
|
|
495
|
+
}
|
|
496
|
+
} else if (existsSync(src)) {
|
|
497
|
+
try {
|
|
498
|
+
await cp(src, dst, { recursive: true });
|
|
499
|
+
console.log(chalk.green(` ✓ Installed skill: ${skill}`));
|
|
500
|
+
results.push({ skill, ok: true });
|
|
501
|
+
} catch (err) {
|
|
502
|
+
console.log(chalk.yellow(` ⚠ Failed to install skill: ${skill} — ${err.message}`));
|
|
503
|
+
results.push({ skill, ok: false });
|
|
504
|
+
}
|
|
505
|
+
} else {
|
|
506
|
+
console.log(chalk.dim(` — Source not found: config/skills/${skill} (skipping)`));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return results;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Rebuild the bizar-dash frontend with Vite.
|
|
514
|
+
*/
|
|
515
|
+
function rebuildDashboard({ dryRun = false } = {}) {
|
|
516
|
+
const dashDir = join(REPO_ROOT, 'bizar-dash');
|
|
517
|
+
const pkgJson = join(dashDir, 'package.json');
|
|
518
|
+
if (!existsSync(pkgJson)) {
|
|
519
|
+
return { ok: false, message: 'bizar-dash/package.json not found — is the dashboard cloned?' };
|
|
520
|
+
}
|
|
521
|
+
if (dryRun) {
|
|
522
|
+
console.log(chalk.dim(' [dry-run] would run: npx vite build (in bizar-dash/)'));
|
|
523
|
+
return { ok: true, message: '[dry-run] dashboard rebuild' };
|
|
524
|
+
}
|
|
525
|
+
console.log(chalk.dim('\n Rebuilding dashboard...'));
|
|
526
|
+
const r = spawnSync('npx', ['vite', 'build'], {
|
|
527
|
+
stdio: 'inherit', cwd: dashDir, timeout: 120000,
|
|
528
|
+
});
|
|
529
|
+
if (r.status === 0) {
|
|
530
|
+
return { ok: true, message: 'dashboard rebuilt' };
|
|
531
|
+
}
|
|
532
|
+
return { ok: false, message: 'dashboard rebuild failed (try: cd bizar-dash && npx vite build)' };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Detect and run the project's test suite. Checks for common test runners
|
|
537
|
+
* (npm test, pytest, cargo test, go test) and runs the first one found.
|
|
538
|
+
*/
|
|
539
|
+
function runTestGate({ dryRun = false } = {}) {
|
|
540
|
+
if (dryRun) {
|
|
541
|
+
console.log(chalk.dim(' [dry-run] would detect and run local test suite'));
|
|
542
|
+
return { ok: true, message: '[dry-run] test gate' };
|
|
543
|
+
}
|
|
544
|
+
const cwd = process.cwd();
|
|
545
|
+
const suites = [
|
|
546
|
+
{ cmd: 'npm test', check: 'package.json' },
|
|
547
|
+
{ cmd: 'pytest', check: 'pyproject.toml' },
|
|
548
|
+
{ cmd: 'cargo test', check: 'Cargo.toml' },
|
|
549
|
+
{ cmd: 'go test ./...', check: 'go.mod' },
|
|
550
|
+
];
|
|
551
|
+
for (const suite of suites) {
|
|
552
|
+
try {
|
|
553
|
+
if (existsSync(join(cwd, suite.check))) {
|
|
554
|
+
console.log(chalk.dim(`\n Running test suite: ${suite.cmd}...`));
|
|
555
|
+
execSync(suite.cmd, { stdio: 'inherit', timeout: 120000, cwd });
|
|
556
|
+
return { ok: true, message: `test gate passed (${suite.cmd})` };
|
|
557
|
+
}
|
|
558
|
+
} catch {
|
|
559
|
+
return { ok: false, message: `test gate failed (${suite.cmd})` };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return { ok: true, message: 'no test suite detected (skipped)' };
|
|
563
|
+
}
|
|
564
|
+
|
|
457
565
|
// ---------------------------------------------------------------------------
|
|
458
566
|
// Prompt helpers
|
|
459
567
|
// ---------------------------------------------------------------------------
|
|
@@ -553,6 +661,22 @@ export async function runUpdate(subargs = []) {
|
|
|
553
661
|
console.log(chalk.dim(' No running Bizar instances detected.'));
|
|
554
662
|
}
|
|
555
663
|
|
|
664
|
+
// ── Git pull ──────────────────────────────────────────────────────────
|
|
665
|
+
if (dryRun) {
|
|
666
|
+
console.log(chalk.dim('\n [dry-run] would pull latest from origin (git pull --rebase)'));
|
|
667
|
+
} else {
|
|
668
|
+
console.log(chalk.dim('\n Pulling latest from origin...'));
|
|
669
|
+
const pull = runGitPull();
|
|
670
|
+
if (pull.ok) {
|
|
671
|
+
console.log(chalk.green(` ✓ ${pull.message}`));
|
|
672
|
+
} else {
|
|
673
|
+
console.log(chalk.red(` ✗ ${pull.message}`));
|
|
674
|
+
console.log(chalk.yellow(' Update aborted — resolve git conflicts and retry.'));
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
console.log('');
|
|
679
|
+
|
|
556
680
|
// 2. Show installed vs. latest versions.
|
|
557
681
|
const cur = {
|
|
558
682
|
opencode: currentVersion('opencode-ai'),
|
|
@@ -582,6 +706,52 @@ export async function runUpdate(subargs = []) {
|
|
|
582
706
|
}
|
|
583
707
|
console.log('');
|
|
584
708
|
|
|
709
|
+
// ── Heads-up gate ───────────────────────────────────────────────────────
|
|
710
|
+
// Check .bizar/PRE_PUSH_NOTES.md for active blockers or warnings before
|
|
711
|
+
// allowing the update to proceed. Blocker entries require --force.
|
|
712
|
+
const bizarDir = findBizarDir(process.cwd());
|
|
713
|
+
if (bizarDir) {
|
|
714
|
+
const headsUp = await checkHeadsUps(bizarDir);
|
|
715
|
+
if (!headsUp.ok) {
|
|
716
|
+
if (dryRun) {
|
|
717
|
+
console.log(chalk.yellow(' Dry-run: found active blocker(s) — update would be blocked.'));
|
|
718
|
+
} else if (assumeYes || subargs.includes('--force')) {
|
|
719
|
+
console.log(chalk.yellow(' ⚠ Active blocker(s) present — proceeding due to --force.'));
|
|
720
|
+
} else {
|
|
721
|
+
console.error(chalk.red(' ✗ Active blocker(s) found in .bizar/PRE_PUSH_NOTES.md.'));
|
|
722
|
+
console.error(chalk.dim(' Archive them with `bizar heads-up archive` or'));
|
|
723
|
+
console.error(chalk.dim(' override with `--force`.'));
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
726
|
+
} else if (headsUp.warningCount > 0) {
|
|
727
|
+
console.log(chalk.yellow(` ⚠ ${headsUp.warningCount} warning(s) in active heads-ups.`));
|
|
728
|
+
// In automatic mode, print the warning count and continue.
|
|
729
|
+
// In interactive mode, ask for confirmation.
|
|
730
|
+
if (!assumeYes && !forceAll && process.stdin.isTTY) {
|
|
731
|
+
try {
|
|
732
|
+
const inquirer = await import('inquirer');
|
|
733
|
+
const { proceed } = await inquirer.default.prompt([
|
|
734
|
+
{
|
|
735
|
+
type: 'confirm',
|
|
736
|
+
name: 'proceed',
|
|
737
|
+
message: 'Heads-up warnings exist. Continue with update?',
|
|
738
|
+
default: true,
|
|
739
|
+
},
|
|
740
|
+
]);
|
|
741
|
+
if (!proceed) {
|
|
742
|
+
console.log(chalk.yellow(' Update cancelled by user.'));
|
|
743
|
+
process.exit(0);
|
|
744
|
+
}
|
|
745
|
+
} catch {
|
|
746
|
+
// inquirer unavailable — continue anyway
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
} else {
|
|
750
|
+
console.log(chalk.green(' ✓ Heads-ups: clear'));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
console.log('');
|
|
754
|
+
|
|
585
755
|
// 3. Decide what to update. Default = everything. Any missing package
|
|
586
756
|
// is automatically included so a partial install gets completed.
|
|
587
757
|
let selected;
|
|
@@ -648,15 +818,39 @@ export async function runUpdate(subargs = []) {
|
|
|
648
818
|
}
|
|
649
819
|
}
|
|
650
820
|
|
|
651
|
-
//
|
|
652
|
-
|
|
821
|
+
// ── Install bundled skills ──────────────────────────────────────────
|
|
822
|
+
console.log(chalk.bold('\n → Installing bundled skills...'));
|
|
823
|
+
const skillResults = await installSkills({ dryRun });
|
|
824
|
+
const skillOk = skillResults.length === 0 || skillResults.every((r) => r.ok);
|
|
825
|
+
|
|
826
|
+
// ── Rebuild dashboard ───────────────────────────────────────────────
|
|
827
|
+
console.log(chalk.bold('\n → Rebuilding dashboard...'));
|
|
828
|
+
const dashRebuild = rebuildDashboard({ dryRun });
|
|
829
|
+
if (dashRebuild.ok) {
|
|
830
|
+
console.log(chalk.green(` ✓ ${dashRebuild.message}`));
|
|
831
|
+
} else {
|
|
832
|
+
console.log(chalk.yellow(` ⚠ ${dashRebuild.message}`));
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// ── Test gate ───────────────────────────────────────────────────────
|
|
836
|
+
console.log(chalk.bold('\n → Running test gate...'));
|
|
837
|
+
const testResult = runTestGate({ dryRun });
|
|
838
|
+
if (testResult.ok) {
|
|
839
|
+
console.log(chalk.green(` ✓ ${testResult.message}`));
|
|
840
|
+
} else {
|
|
841
|
+
console.log(chalk.red(` ✗ ${testResult.message}`));
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// ── Restart dashboard ─────────────────────────────────────────────
|
|
845
|
+
// Restart if it was running before, or if the dashboard was just rebuilt.
|
|
846
|
+
if (restartAfter && (instances.dashboard || dashRebuild.ok)) {
|
|
653
847
|
if (dryRun) {
|
|
654
848
|
console.log('');
|
|
655
849
|
console.log(chalk.dim(' [dry-run] would restart dashboard with the new code'));
|
|
656
850
|
} else {
|
|
657
851
|
console.log('');
|
|
658
852
|
console.log(chalk.cyan(' Restarting dashboard with the new code...'));
|
|
659
|
-
const res = spawnFreshDashboard({ port: instances.dashboard
|
|
853
|
+
const res = spawnFreshDashboard({ port: instances.dashboard?.port || undefined });
|
|
660
854
|
if (res.ok) {
|
|
661
855
|
console.log(chalk.green(` ✓ ${res.message}`));
|
|
662
856
|
} else {
|
|
@@ -673,11 +867,29 @@ export async function runUpdate(subargs = []) {
|
|
|
673
867
|
const marker = r.ok ? chalk.green('✓') : chalk.red('✗');
|
|
674
868
|
console.log(` ${marker} ${name.padEnd(10)} ${r.message}`);
|
|
675
869
|
}
|
|
870
|
+
// Skills
|
|
871
|
+
for (const sr of skillResults) {
|
|
872
|
+
const marker = sr.ok ? chalk.green('✓') : chalk.red('✗');
|
|
873
|
+
console.log(` ${marker} ${'skill/skills'.padEnd(10)} ${sr.ok ? 'installed' : `failed: ${sr.skill}`}`);
|
|
874
|
+
}
|
|
875
|
+
if (!dashRebuild.ok) {
|
|
876
|
+
console.log(` ${chalk.yellow('⚠')} ${'dash'.padEnd(10)} ${dashRebuild.message}`);
|
|
877
|
+
}
|
|
878
|
+
const testMarker = testResult.ok ? chalk.green('✓') : chalk.red('✗');
|
|
879
|
+
console.log(` ${testMarker} ${'test-gate'.padEnd(10)} ${testResult.message}`);
|
|
676
880
|
|
|
677
|
-
const
|
|
881
|
+
const allResults = [
|
|
882
|
+
...results.map(([, r]) => r),
|
|
883
|
+
...skillResults,
|
|
884
|
+
dashRebuild,
|
|
885
|
+
testResult,
|
|
886
|
+
];
|
|
887
|
+
const anyFail = allResults.some((r) => !r.ok);
|
|
678
888
|
if (anyFail) {
|
|
679
|
-
console.log(chalk.yellow('\n Some
|
|
680
|
-
|
|
889
|
+
console.log(chalk.yellow('\n Some steps had issues. See messages above.'));
|
|
890
|
+
if (allResults.filter((r) => !r.ok).some((r) => r.message && r.message.includes('failed'))) {
|
|
891
|
+
process.exit(1);
|
|
892
|
+
}
|
|
681
893
|
}
|
|
682
894
|
if (dryRun) {
|
|
683
895
|
console.log(
|