@polderlabs/bizar 2.3.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/LICENSE +21 -0
- package/README.md +364 -0
- package/cli/audit.mjs +144 -0
- package/cli/banner.mjs +41 -0
- package/cli/bin.mjs +186 -0
- package/cli/copy.mjs +508 -0
- package/cli/export.mjs +87 -0
- package/cli/init.mjs +147 -0
- package/cli/install.mjs +390 -0
- package/cli/plan-templates.mjs +523 -0
- package/cli/plan.mjs +2087 -0
- package/cli/prompts.mjs +163 -0
- package/cli/update.mjs +273 -0
- package/cli/utils.mjs +153 -0
- package/config/AGENTS.md +282 -0
- package/config/agents/baldr.md +148 -0
- package/config/agents/forseti.md +112 -0
- package/config/agents/frigg.md +101 -0
- package/config/agents/heimdall.md +157 -0
- package/config/agents/hermod.md +144 -0
- package/config/agents/mimir.md +115 -0
- package/config/agents/odin.md +309 -0
- package/config/agents/quick.md +78 -0
- package/config/agents/semble-search.md +44 -0
- package/config/agents/thor.md +97 -0
- package/config/agents/tyr.md +96 -0
- package/config/agents/vidarr.md +100 -0
- package/config/agents/vor.md +140 -0
- package/config/commands/audit.md +1 -0
- package/config/commands/explain.md +1 -0
- package/config/commands/init.md +1 -0
- package/config/commands/learn.md +1 -0
- package/config/commands/pr-review.md +1 -0
- package/config/commands/tailscale-serve.md +96 -0
- package/config/hooks/README.md +29 -0
- package/config/hooks/post-tool-use.md +16 -0
- package/config/hooks/pre-tool-use.md +16 -0
- package/config/opencode.json +52 -0
- package/config/opencode.json.template +52 -0
- package/config/rules/general.md +8 -0
- package/config/rules/git.md +11 -0
- package/config/rules/javascript.md +10 -0
- package/config/rules/python.md +10 -0
- package/config/rules/testing.md +10 -0
- package/config/skills/bizar/README.md +9 -0
- package/config/skills/bizar/SKILL.md +187 -0
- package/config/skills/cpp-coding-standards/README.md +28 -0
- package/config/skills/cpp-coding-standards/SKILL.md +634 -0
- package/config/skills/cpp-coding-standards/agents/openai.yaml +4 -0
- package/config/skills/cpp-coding-standards/references/concurrency.md +320 -0
- package/config/skills/cpp-coding-standards/references/error-handling.md +229 -0
- package/config/skills/cpp-coding-standards/references/memory-safety.md +216 -0
- package/config/skills/cpp-coding-standards/references/modern-idioms.md +282 -0
- package/config/skills/cpp-coding-standards/references/review-checklist.md +96 -0
- package/config/skills/cpp-testing/README.md +28 -0
- package/config/skills/cpp-testing/SKILL.md +304 -0
- package/config/skills/cpp-testing/agents/openai.yaml +4 -0
- package/config/skills/cpp-testing/references/coverage.md +370 -0
- package/config/skills/cpp-testing/references/framework-compare.md +175 -0
- package/config/skills/cpp-testing/references/host-test-for-embedded.md +499 -0
- package/config/skills/cpp-testing/references/mocking.md +364 -0
- package/config/skills/cpp-testing/references/tdd-workflow.md +308 -0
- package/config/skills/embedded-esp-idf/README.md +41 -0
- package/config/skills/embedded-esp-idf/SKILL.md +439 -0
- package/config/skills/embedded-esp-idf/agents/openai.yaml +4 -0
- package/config/skills/embedded-esp-idf/references/freertos-patterns.md +214 -0
- package/config/skills/embedded-esp-idf/references/host-tests.md +164 -0
- package/config/skills/embedded-esp-idf/references/idf-py-commands.md +157 -0
- package/config/skills/embedded-esp-idf/references/kconfig.md +159 -0
- package/config/skills/embedded-esp-idf/references/logging-discipline.md +118 -0
- package/config/skills/embedded-esp-idf/references/memory-and-iram.md +137 -0
- package/config/skills/embedded-esp-idf/references/nvs.md +121 -0
- package/config/skills/embedded-esp-idf/references/packed-structs.md +192 -0
- package/config/skills/embedded-esp-idf/scripts/idf_env.sh +47 -0
- package/config/skills/embedded-esp-idf/scripts/size_check.sh +77 -0
- package/config/skills/self-improvement/SKILL.md +64 -0
- package/package.json +47 -0
- package/templates/plan/htmx.min.js +1 -0
- package/templates/plan/library/bug-investigation.mdx +79 -0
- package/templates/plan/library/decision-record.mdx +71 -0
- package/templates/plan/library/feature-design.mdx +92 -0
- package/templates/plan/meta.json.template +8 -0
- package/templates/plan/plan.canvas.template +1711 -0
- package/templates/plan/plan.html.template +937 -0
- package/templates/plan/plan.mdx.template +46 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bizar plan-templates.mjs
|
|
3
|
+
*
|
|
4
|
+
* Built-in template library for the visual planner v2.
|
|
5
|
+
*
|
|
6
|
+
* Templates are stored in two places:
|
|
7
|
+
* 1. JS-embedded defaults (so the CLI works without filesystem reads)
|
|
8
|
+
* 2. templates/plan/library/*.mdx (so users can edit/add templates)
|
|
9
|
+
*
|
|
10
|
+
* At lookup time we prefer the on-disk .mdx file (if present) and fall
|
|
11
|
+
* back to the embedded string. The "blank" template is special: it
|
|
12
|
+
* delegates to the existing plan.mdx.template file via the CLI caller.
|
|
13
|
+
*
|
|
14
|
+
* Exports:
|
|
15
|
+
* - getTemplate(name) → { name, description, content } | null
|
|
16
|
+
* - getTemplateNames() → string[] (sorted)
|
|
17
|
+
* - listTemplates() → [{ name, description, source }, ...]
|
|
18
|
+
* - printTemplates() → writes a friendly CLI listing to stdout
|
|
19
|
+
*
|
|
20
|
+
* The names "blank", "feature-design", "bug-investigation", and
|
|
21
|
+
* "decision-record" are reserved (case-insensitive).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
25
|
+
import { dirname, join, resolve, basename } from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const PROJECT_ROOT = resolve(__dirname, '..');
|
|
30
|
+
const TEMPLATES_DIR = join(PROJECT_ROOT, 'templates', 'plan');
|
|
31
|
+
const LIBRARY_DIR = join(TEMPLATES_DIR, 'library');
|
|
32
|
+
|
|
33
|
+
// ─── Embedded defaults ──────────────────────────────────────────────────────
|
|
34
|
+
//
|
|
35
|
+
// These are kept in sync with templates/plan/library/*.mdx. If the
|
|
36
|
+
// library directory is missing a file, the embedded version is used.
|
|
37
|
+
// To change a built-in template, edit BOTH (or just the .mdx file if
|
|
38
|
+
// the file is present — the loader prefers files).
|
|
39
|
+
|
|
40
|
+
const BUILT_IN_TEMPLATES = {
|
|
41
|
+
'blank': {
|
|
42
|
+
description: 'Empty starter — the same template the v1 planner used',
|
|
43
|
+
content: null, // signals: caller should use plan.mdx.template
|
|
44
|
+
},
|
|
45
|
+
'feature-design': {
|
|
46
|
+
description: 'For designing a new feature (problem, goals, design, tradeoffs)',
|
|
47
|
+
content: `# Feature: {{title}}
|
|
48
|
+
|
|
49
|
+
**Status:** \`[STATUS:draft]\` · **Author:** {{author}} · **Created:** {{created}}
|
|
50
|
+
|
|
51
|
+
> [!INFO]
|
|
52
|
+
> This is a v2 plan with the **feature-design** template. It uses callouts
|
|
53
|
+
> (\`> [!INFO]\`, \`> [!WARNING]\`, etc.), status badges, and GFM task lists.
|
|
54
|
+
|
|
55
|
+
## Problem
|
|
56
|
+
|
|
57
|
+
_What problem are we solving? Why now? What happens if we don't?_
|
|
58
|
+
|
|
59
|
+
## Goals
|
|
60
|
+
|
|
61
|
+
- [ ] Goal 1 — a concrete, measurable outcome
|
|
62
|
+
- [ ] Goal 2 — another concrete, measurable outcome
|
|
63
|
+
- [ ] Goal 3 — and one more for good measure
|
|
64
|
+
|
|
65
|
+
## Non-goals
|
|
66
|
+
|
|
67
|
+
- This feature does NOT do X (deferred to a future plan)
|
|
68
|
+
- This feature does NOT cover Y (out of scope for this iteration)
|
|
69
|
+
|
|
70
|
+
## Design
|
|
71
|
+
|
|
72
|
+
The proposed approach. Walk through the high-level shape, then drill into specifics.
|
|
73
|
+
|
|
74
|
+
### Architecture
|
|
75
|
+
|
|
76
|
+
\`\`\`mermaid
|
|
77
|
+
graph LR
|
|
78
|
+
Client --> API --> DB
|
|
79
|
+
API --> Cache
|
|
80
|
+
\`\`\`
|
|
81
|
+
|
|
82
|
+
### Data model
|
|
83
|
+
|
|
84
|
+
Tables, schemas, types. Show diffs or new shapes.
|
|
85
|
+
|
|
86
|
+
### API
|
|
87
|
+
|
|
88
|
+
_Endpoints, request/response shapes, error handling._
|
|
89
|
+
|
|
90
|
+
### Edge cases
|
|
91
|
+
|
|
92
|
+
- What if X is empty?
|
|
93
|
+
- What if Y is malformed?
|
|
94
|
+
- What if the user is offline?
|
|
95
|
+
|
|
96
|
+
## Tradeoffs
|
|
97
|
+
|
|
98
|
+
| Option | Pros | Cons |
|
|
99
|
+
|--------|------|------|
|
|
100
|
+
| A | Simple, easy to roll back | Limited to use case 1 |
|
|
101
|
+
| B | Handles more cases | More complex, more tests |
|
|
102
|
+
|
|
103
|
+
## Files affected
|
|
104
|
+
|
|
105
|
+
- \`path/to/file.ts\` — what changes and why
|
|
106
|
+
- \`path/to/other.ts\` — what changes and why
|
|
107
|
+
- \`path/to/migration.sql\` — schema changes
|
|
108
|
+
|
|
109
|
+
## Open questions
|
|
110
|
+
|
|
111
|
+
1. Question 1?
|
|
112
|
+
2. Question 2?
|
|
113
|
+
3. Question 3?
|
|
114
|
+
|
|
115
|
+
## Test plan
|
|
116
|
+
|
|
117
|
+
- [ ] Unit tests for the new module
|
|
118
|
+
- [ ] Integration tests for the API surface
|
|
119
|
+
- [ ] E2E test for the user flow
|
|
120
|
+
- [ ] Manual QA in staging
|
|
121
|
+
|
|
122
|
+
## Rollout
|
|
123
|
+
|
|
124
|
+
- [ ] Dev
|
|
125
|
+
- [ ] Staging
|
|
126
|
+
- [ ] Production (% rollout)
|
|
127
|
+
|
|
128
|
+
## Decision
|
|
129
|
+
|
|
130
|
+
> [!TIP]
|
|
131
|
+
> Pick option **A** or **B** and state the rationale here. Reference the
|
|
132
|
+
> Tradeoffs table above. Link to relevant docs / RFCs.
|
|
133
|
+
|
|
134
|
+
## References
|
|
135
|
+
|
|
136
|
+
- [Design doc](https://example.com/design)
|
|
137
|
+
- [Related plan](#)
|
|
138
|
+
`,
|
|
139
|
+
},
|
|
140
|
+
'bug-investigation': {
|
|
141
|
+
description: 'For investigating a bug (repro, root cause, fix, regression test)',
|
|
142
|
+
content: `# Bug: {{title}}
|
|
143
|
+
|
|
144
|
+
**Status:** \`[STATUS:draft]\` · **Author:** {{author}} · **Created:** {{created}}
|
|
145
|
+
|
|
146
|
+
> [!DANGER]
|
|
147
|
+
> Severity: <high/medium/low> · Reported: <date> · Reporter: <name>
|
|
148
|
+
|
|
149
|
+
## Summary
|
|
150
|
+
|
|
151
|
+
_One-paragraph description of the bug._
|
|
152
|
+
|
|
153
|
+
## Reproduction
|
|
154
|
+
|
|
155
|
+
Steps to reproduce:
|
|
156
|
+
1. Step 1
|
|
157
|
+
2. Step 2
|
|
158
|
+
3. ...
|
|
159
|
+
|
|
160
|
+
## Expected vs Actual
|
|
161
|
+
|
|
162
|
+
**Expected:** What should happen.
|
|
163
|
+
|
|
164
|
+
**Actual:** What actually happens.
|
|
165
|
+
|
|
166
|
+
## Environment
|
|
167
|
+
|
|
168
|
+
- App version: <version>
|
|
169
|
+
- OS: <os>
|
|
170
|
+
- Browser: <browser>
|
|
171
|
+
- Account: <test-account-id> (if reproducible only on certain accounts)
|
|
172
|
+
|
|
173
|
+
## Investigation
|
|
174
|
+
|
|
175
|
+
> [!NOTE]
|
|
176
|
+
> Document the timeline of what you tried, what you found, and what
|
|
177
|
+
> you ruled out. Link to relevant logs, traces, or screenshots.
|
|
178
|
+
|
|
179
|
+
### What we tried
|
|
180
|
+
|
|
181
|
+
- Tried X — found Y
|
|
182
|
+
- Tried A — ruled out B
|
|
183
|
+
|
|
184
|
+
### What we found
|
|
185
|
+
|
|
186
|
+
- Observation 1
|
|
187
|
+
- Observation 2
|
|
188
|
+
|
|
189
|
+
## Root cause
|
|
190
|
+
|
|
191
|
+
> [!WARNING]
|
|
192
|
+
> State the underlying cause clearly. If the cause is unknown, say so
|
|
193
|
+
> and list the most likely candidates.
|
|
194
|
+
|
|
195
|
+
_The underlying issue._
|
|
196
|
+
|
|
197
|
+
## Fix
|
|
198
|
+
|
|
199
|
+
> [!TIP]
|
|
200
|
+
> Describe the proposed fix. Include a code diff or a sketch of the
|
|
201
|
+
> change. Note any follow-up work that's needed but out of scope here.
|
|
202
|
+
|
|
203
|
+
_The proposed fix._
|
|
204
|
+
|
|
205
|
+
## Test plan
|
|
206
|
+
|
|
207
|
+
- [ ] Test that the bug is fixed (regression test on the failing case)
|
|
208
|
+
- [ ] Test that the fix doesn't break anything (existing tests still pass)
|
|
209
|
+
- [ ] Add a regression test to the suite
|
|
210
|
+
- [ ] Manual verification in staging
|
|
211
|
+
|
|
212
|
+
## Rollback plan
|
|
213
|
+
|
|
214
|
+
_How do we revert if the fix makes things worse?_
|
|
215
|
+
|
|
216
|
+
## References
|
|
217
|
+
|
|
218
|
+
- [PR #X](https://example.com/pr/X)
|
|
219
|
+
- [Slack thread](https://example.com/slack)
|
|
220
|
+
- [Related incidents](#)
|
|
221
|
+
`,
|
|
222
|
+
},
|
|
223
|
+
'decision-record': {
|
|
224
|
+
description: 'Architecture Decision Record (ADR) — context, options, decision, consequences',
|
|
225
|
+
content: `# Decision: {{title}}
|
|
226
|
+
|
|
227
|
+
**Status:** \`[STATUS:draft]\` · **Author:** {{author}} · **Date:** {{created}}
|
|
228
|
+
|
|
229
|
+
> [!INFO]
|
|
230
|
+
> This is an **Architecture Decision Record (ADR)**. It captures a
|
|
231
|
+
> significant decision, the context that led to it, and the consequences
|
|
232
|
+
> that follow. Once accepted, ADRs are immutable — create a new ADR to
|
|
233
|
+
> supersede this one.
|
|
234
|
+
|
|
235
|
+
## Context
|
|
236
|
+
|
|
237
|
+
_What is the situation? What forces are at play? What problem are we
|
|
238
|
+
trying to solve? What constraints do we have?_
|
|
239
|
+
|
|
240
|
+
## Options considered
|
|
241
|
+
|
|
242
|
+
### Option A: <name>
|
|
243
|
+
|
|
244
|
+
_Brief description._
|
|
245
|
+
|
|
246
|
+
- Pros: …
|
|
247
|
+
- Cons: …
|
|
248
|
+
|
|
249
|
+
### Option B: <name>
|
|
250
|
+
|
|
251
|
+
_Brief description._
|
|
252
|
+
|
|
253
|
+
- Pros: …
|
|
254
|
+
- Cons: …
|
|
255
|
+
|
|
256
|
+
### Option C: <name>
|
|
257
|
+
|
|
258
|
+
_Brief description._
|
|
259
|
+
|
|
260
|
+
- Pros: …
|
|
261
|
+
- Cons: …
|
|
262
|
+
|
|
263
|
+
## Decision
|
|
264
|
+
|
|
265
|
+
> [!TIP]
|
|
266
|
+
> State the chosen option and the rationale. Quote relevant constraints
|
|
267
|
+
> from the Context section. Note any dissent.
|
|
268
|
+
|
|
269
|
+
We chose **Option X** because…
|
|
270
|
+
|
|
271
|
+
## Consequences
|
|
272
|
+
|
|
273
|
+
### Positive
|
|
274
|
+
|
|
275
|
+
- …
|
|
276
|
+
|
|
277
|
+
### Negative
|
|
278
|
+
|
|
279
|
+
- …
|
|
280
|
+
|
|
281
|
+
### Neutral
|
|
282
|
+
|
|
283
|
+
- …
|
|
284
|
+
|
|
285
|
+
## Follow-ups
|
|
286
|
+
|
|
287
|
+
- [ ] Follow-up 1
|
|
288
|
+
- [ ] Follow-up 2
|
|
289
|
+
- [ ] Follow-up 3
|
|
290
|
+
|
|
291
|
+
## References
|
|
292
|
+
|
|
293
|
+
- [Doc 1](https://example.com/doc-1)
|
|
294
|
+
- [Doc 2](https://example.com/doc-2)
|
|
295
|
+
- [Related ADRs](#)
|
|
296
|
+
`,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// ─── File loader ─────────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Read a .mdx file from the library directory if it exists.
|
|
304
|
+
* Returns null if the file is absent or unreadable.
|
|
305
|
+
*/
|
|
306
|
+
function readLibraryFile(slug) {
|
|
307
|
+
const path = join(LIBRARY_DIR, `${slug}.mdx`);
|
|
308
|
+
if (!existsSync(path)) return null;
|
|
309
|
+
try {
|
|
310
|
+
return readFileSync(path, 'utf-8');
|
|
311
|
+
} catch {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Discover user-added templates in the library directory that are not
|
|
318
|
+
* already in BUILT_IN_TEMPLATES. Any *.mdx file whose basename (without
|
|
319
|
+
* extension) isn't a reserved built-in name becomes a "custom" entry.
|
|
320
|
+
*/
|
|
321
|
+
function discoverCustomTemplates() {
|
|
322
|
+
if (!existsSync(LIBRARY_DIR)) return [];
|
|
323
|
+
const reserved = new Set(Object.keys(BUILT_IN_TEMPLATES));
|
|
324
|
+
const out = [];
|
|
325
|
+
for (const name of readdirSync(LIBRARY_DIR)) {
|
|
326
|
+
if (!name.endsWith('.mdx')) continue;
|
|
327
|
+
const slug = name.slice(0, -'.mdx'.length);
|
|
328
|
+
if (reserved.has(slug)) continue;
|
|
329
|
+
const path = join(LIBRARY_DIR, name);
|
|
330
|
+
try {
|
|
331
|
+
const content = readFileSync(path, 'utf-8');
|
|
332
|
+
out.push({
|
|
333
|
+
name: slug,
|
|
334
|
+
description: 'Custom template',
|
|
335
|
+
content,
|
|
336
|
+
source: 'library',
|
|
337
|
+
});
|
|
338
|
+
} catch {
|
|
339
|
+
// skip unreadable
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Return the template record for a given name (case-insensitive).
|
|
349
|
+
* For the "blank" template, content is null — the caller should use
|
|
350
|
+
* plan.mdx.template as the source.
|
|
351
|
+
*
|
|
352
|
+
* @param {string} name
|
|
353
|
+
* @returns {{ name: string, description: string, content: string|null, source: 'built-in'|'library' } | null}
|
|
354
|
+
*/
|
|
355
|
+
export function getTemplate(name) {
|
|
356
|
+
if (!name) return null;
|
|
357
|
+
const normalized = String(name).toLowerCase().trim();
|
|
358
|
+
if (!BUILT_IN_TEMPLATES[normalized]) return null;
|
|
359
|
+
|
|
360
|
+
// "blank" is special: caller should use the standard plan.mdx.template
|
|
361
|
+
if (normalized === 'blank') {
|
|
362
|
+
return {
|
|
363
|
+
name: 'blank',
|
|
364
|
+
description: BUILT_IN_TEMPLATES.blank.description,
|
|
365
|
+
content: null,
|
|
366
|
+
source: 'built-in',
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Try the on-disk library file first; fall back to the JS-embedded copy.
|
|
371
|
+
const fileContent = readLibraryFile(normalized);
|
|
372
|
+
if (fileContent != null) {
|
|
373
|
+
return {
|
|
374
|
+
name: normalized,
|
|
375
|
+
description: BUILT_IN_TEMPLATES[normalized].description,
|
|
376
|
+
content: fileContent,
|
|
377
|
+
source: 'library',
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
name: normalized,
|
|
383
|
+
description: BUILT_IN_TEMPLATES[normalized].description,
|
|
384
|
+
content: BUILT_IN_TEMPLATES[normalized].content,
|
|
385
|
+
source: 'built-in',
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* @returns {string[]} Sorted list of built-in template names.
|
|
391
|
+
*/
|
|
392
|
+
export function getTemplateNames() {
|
|
393
|
+
return Object.keys(BUILT_IN_TEMPLATES).sort();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* List all templates, including user-added ones in the library directory.
|
|
398
|
+
* @returns {Array<{ name: string, description: string, source: string }>}
|
|
399
|
+
*/
|
|
400
|
+
export function listTemplates() {
|
|
401
|
+
const builtIn = Object.keys(BUILT_IN_TEMPLATES).sort().map((name) => ({
|
|
402
|
+
name,
|
|
403
|
+
description: BUILT_IN_TEMPLATES[name].description,
|
|
404
|
+
source: name === 'blank' ? 'built-in' : (readLibraryFile(name) != null ? 'library' : 'built-in'),
|
|
405
|
+
}));
|
|
406
|
+
return [...builtIn, ...discoverCustomTemplates()];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Print a friendly CLI listing of all available templates.
|
|
411
|
+
*/
|
|
412
|
+
export function printTemplates() {
|
|
413
|
+
const all = listTemplates();
|
|
414
|
+
console.log(' Built-in templates:');
|
|
415
|
+
const nameWidth = Math.max(...all.map((t) => t.name.length), 8);
|
|
416
|
+
for (const t of all) {
|
|
417
|
+
const tag = t.source === 'library' ? ' (override)' : '';
|
|
418
|
+
console.log(` ${t.name.padEnd(nameWidth)} ${t.description}${tag}`);
|
|
419
|
+
}
|
|
420
|
+
console.log();
|
|
421
|
+
console.log(' Use: bizar plan new <slug> --template <name>');
|
|
422
|
+
console.log(' Built-in: ' + Object.keys(BUILT_IN_TEMPLATES).join(', '));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ─── Variable substitution ───────────────────────────────────────────────────
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Substitute {{key}} placeholders in template content. Mirrors the
|
|
429
|
+
* behavior of the existing plan.mjs replaceTemplate — kept here so
|
|
430
|
+
* the templates module is self-contained.
|
|
431
|
+
*
|
|
432
|
+
* @param {string} content
|
|
433
|
+
* @param {Record<string,string>} vars
|
|
434
|
+
*/
|
|
435
|
+
export function substitute(content, vars) {
|
|
436
|
+
let out = content;
|
|
437
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
438
|
+
out = out.replace(new RegExp(`\\{\\{${k}\\}\\}`, 'g'), v);
|
|
439
|
+
}
|
|
440
|
+
return out;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Build the standard variable set used to render a template.
|
|
445
|
+
*
|
|
446
|
+
* @param {{ slug: string, title?: string }} opts
|
|
447
|
+
*/
|
|
448
|
+
export function buildVars({ slug, title }) {
|
|
449
|
+
const now = new Date().toISOString();
|
|
450
|
+
return {
|
|
451
|
+
title: title || slug,
|
|
452
|
+
slug,
|
|
453
|
+
author: process.env.USER || 'unknown',
|
|
454
|
+
created: now,
|
|
455
|
+
lastEdited: now,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ─── CLI helpers (for `plan template save/list/delete`) ──────────────────────
|
|
460
|
+
//
|
|
461
|
+
// These are minimal — the spec for v2 calls for full user-saved templates
|
|
462
|
+
// in ~/.config/bizar/plan-templates/. v2.0 ships a stub for
|
|
463
|
+
// library-directory operations; the user-templates dir is deferred to
|
|
464
|
+
// a follow-up.
|
|
465
|
+
|
|
466
|
+
const USER_TEMPLATES_DIR = join(
|
|
467
|
+
process.env.HOME || process.env.USERPROFILE || '~',
|
|
468
|
+
'.config',
|
|
469
|
+
'bizar',
|
|
470
|
+
'plan-templates',
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Save the content of an existing plan as a library template.
|
|
475
|
+
* If a name is given, the file is written to templates/plan/library/.
|
|
476
|
+
* The plan is read from plans/<planSlug>/plan.mdx.
|
|
477
|
+
*
|
|
478
|
+
* @param {string} name template name (becomes the filename)
|
|
479
|
+
* @param {string} planSlug source plan to read content from
|
|
480
|
+
* @returns {string} absolute path to the saved file
|
|
481
|
+
*/
|
|
482
|
+
export function saveTemplate(name, planSlug) {
|
|
483
|
+
if (!name || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(name)) {
|
|
484
|
+
throw new Error(`Invalid template name "${name}". Use lowercase letters, digits, and hyphens.`);
|
|
485
|
+
}
|
|
486
|
+
if (!planSlug || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(planSlug)) {
|
|
487
|
+
throw new Error(`Invalid plan slug "${planSlug}".`);
|
|
488
|
+
}
|
|
489
|
+
const sourcePath = join(PROJECT_ROOT, 'plans', planSlug, 'plan.mdx');
|
|
490
|
+
if (!existsSync(sourcePath)) {
|
|
491
|
+
throw new Error(`Plan "${planSlug}" not found at ${sourcePath}`);
|
|
492
|
+
}
|
|
493
|
+
const content = readFileSync(sourcePath, 'utf-8');
|
|
494
|
+
mkdirSync(LIBRARY_DIR, { recursive: true });
|
|
495
|
+
const target = join(LIBRARY_DIR, `${name}.mdx`);
|
|
496
|
+
writeFileSync(target, content, 'utf-8');
|
|
497
|
+
return target;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Delete a user-added template from the library directory.
|
|
502
|
+
* Refuses to delete built-in templates.
|
|
503
|
+
*
|
|
504
|
+
* @param {string} name
|
|
505
|
+
*/
|
|
506
|
+
export function deleteTemplate(name) {
|
|
507
|
+
const normalized = String(name || '').toLowerCase();
|
|
508
|
+
if (BUILT_IN_TEMPLATES[normalized]) {
|
|
509
|
+
throw new Error(`"${name}" is a built-in template and cannot be deleted.`);
|
|
510
|
+
}
|
|
511
|
+
const path = join(LIBRARY_DIR, `${normalized}.mdx`);
|
|
512
|
+
if (!existsSync(path)) {
|
|
513
|
+
throw new Error(`Template "${name}" not found in library.`);
|
|
514
|
+
}
|
|
515
|
+
unlinkSync(path);
|
|
516
|
+
return path;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ─── Constants export (for tests) ────────────────────────────────────────────
|
|
520
|
+
|
|
521
|
+
export const BUILT_IN_TEMPLATE_NAMES = Object.keys(BUILT_IN_TEMPLATES);
|
|
522
|
+
export const LIBRARY_DIR_PATH = LIBRARY_DIR;
|
|
523
|
+
export const USER_TEMPLATES_DIR_PATH = USER_TEMPLATES_DIR;
|