@boopkit/cli 0.6.12 → 0.6.13
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 +26 -5
- package/dist/capability-versions.json +1 -1
- package/dist/cli.js +2 -1
- package/dist/commands/add.js +49 -2
- package/dist/commands/init.d.ts +17 -1
- package/dist/commands/init.js +149 -25
- package/dist/commands/loop-merge.d.ts +1 -0
- package/dist/commands/loop-merge.js +4 -2
- package/dist/commands/loop.d.ts +21 -2
- package/dist/commands/loop.js +105 -16
- package/dist/commands/loop.js.orig-pre-maxbuffer +1455 -0
- package/dist/lib/scaffold.d.ts +18 -7
- package/dist/lib/scaffold.js +31 -14
- package/docs/init.md +132 -86
- package/docs/loop.md +16 -0
- package/package.json +3 -2
- package/profiles/base/profile.md +105 -0
- package/profiles/base/templates/.github/copilot-instructions.md +3 -0
- package/profiles/base/templates/.gitignore.hbs +6 -0
- package/profiles/base/templates/AGENTS.md.hbs +50 -0
- package/profiles/base/templates/CLAUDE.md.hbs +3 -0
- package/profiles/base/templates/README.md.hbs +15 -0
- package/profiles/base/templates/docs/README.md +12 -0
- package/profiles/base/templates/package.json.hbs +7 -0
- package/profiles/base/templates/reference/README.md +16 -0
- package/profiles/base/templates/specs/README.md +12 -0
- package/profiles/development/profile.md +8 -1
package/README.md
CHANGED
|
@@ -24,7 +24,8 @@ boop init my-app
|
|
|
24
24
|
cd my-app
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
`boop init` creates the project directory, applies the `web-app` profile, installs capability packages from npm, and scaffolds files into your project — including a `.boop.json` manifest that records what profile and capabilities are installed.
|
|
27
|
+
`boop init` creates the project directory, applies the `web-app` profile, installs capability packages from npm, and scaffolds files into your project — including a `.boop/config.json` manifest that records what profile and capabilities are installed.
|
|
28
|
+
It finishes by committing the scaffold to a new git repository and setting up the project's beads issue tracker in Dolt server mode.
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
31
|
# See what was installed, what's configured, and what to do next
|
|
@@ -52,7 +53,9 @@ boop add users-and-accounts
|
|
|
52
53
|
|
|
53
54
|
Start simple, grow deliberately. That's the shape of a boop project: a strong starting point you can extend without collecting scaffolding debt.
|
|
54
55
|
|
|
55
|
-
A **profile** is a named starting point.
|
|
56
|
+
A **profile** is a named starting point.
|
|
57
|
+
It defines a set of default capabilities, and what `boop init` writes before they are installed:
|
|
58
|
+
either a kernel (TypeScript config, `package.json`, `src/server.ts`, deployment setup) or a `templates/` folder of its own.
|
|
56
59
|
|
|
57
60
|
A **capability** is a composable feature bundle. Each capability package ships:
|
|
58
61
|
|
|
@@ -67,7 +70,8 @@ Capabilities can declare `requires` relationships (e.g. `users-and-accounts` req
|
|
|
67
70
|
|
|
68
71
|
## Profiles
|
|
69
72
|
|
|
70
|
-
A profile names a set of capabilities.
|
|
73
|
+
A profile names a set of capabilities.
|
|
74
|
+
Three ship today, and `boop init` can scaffold from two of them.
|
|
71
75
|
|
|
72
76
|
### `web-app`
|
|
73
77
|
|
|
@@ -75,7 +79,19 @@ Full-stack web application — Express + Handlebars + Tailwind CSS + PostgreSQL
|
|
|
75
79
|
|
|
76
80
|
Ships with application capabilities (`web`, `api`, `services`, `database`), development skills (`coding`, `testing`, `project-mgmt`, `product-design`, `skills`, `workflows`), and the `hello` demo capability.
|
|
77
81
|
|
|
78
|
-
This is the profile `boop init
|
|
82
|
+
This is the default profile for `boop init`.
|
|
83
|
+
It carries a kernel, so there is an application to write.
|
|
84
|
+
|
|
85
|
+
### `base`
|
|
86
|
+
|
|
87
|
+
Planning, writing and issue tracking for a project that isn't an application: setting up an AWS environment, a research project, a migration plan.
|
|
88
|
+
Ships with `project-mgmt`, `product-design` and `skills`, and nothing code-shaped.
|
|
89
|
+
|
|
90
|
+
It carries no kernel, but it ships its own templates,
|
|
91
|
+
so `boop init my-project --profile base` writes a project with `specs/`, `reference/` and `docs/` folders,
|
|
92
|
+
agent instructions for work whose output is documents and tracked issues,
|
|
93
|
+
and a beads issue tracker ready to use.
|
|
94
|
+
When the project grows code of its own, `boop add coding` brings in the coding skills.
|
|
79
95
|
|
|
80
96
|
### `development`
|
|
81
97
|
|
|
@@ -83,7 +99,10 @@ The development capabilities on their own (`coding`, `testing`, `project-mgmt`,
|
|
|
83
99
|
|
|
84
100
|
For a project with no application domain boop has anything to say about: a CLI, a library, a data pipeline. Those share no application layers at all, but they are developed the same way.
|
|
85
101
|
|
|
86
|
-
It carries
|
|
102
|
+
It carries neither a kernel nor templates — no runtime, no port, no deploy target, and no files of its own to write —
|
|
103
|
+
so there is nothing for `boop init` to scaffold and `boop init --profile development` is refused.
|
|
104
|
+
Use it by naming it as the `profile` in an existing project's `.boop/config.json`, or reach for `boop add` on the capabilities you want.
|
|
105
|
+
Boop's own repository is the first project to declare it.
|
|
87
106
|
|
|
88
107
|
List profiles with `boop list profiles`.
|
|
89
108
|
|
|
@@ -173,6 +192,7 @@ Most days you'll use `init`, `add`, and `status`. The rest are there when you ne
|
|
|
173
192
|
Flags:
|
|
174
193
|
|
|
175
194
|
- `boop init --no-install` / `boop add --no-install` — skip `npm install`
|
|
195
|
+
- `boop init --no-beads` — skip the scaffold commit and `bd init`, so no git repository, beads database or Dolt server is created
|
|
176
196
|
- `boop add --dry-run` — preview changes without writing
|
|
177
197
|
- `boop update --force` — overwrite locally modified scaffold files (creates `.bak` backups)
|
|
178
198
|
- `boop update --prune` — remove scaffold files no longer shipped by the capability
|
|
@@ -214,6 +234,7 @@ Boop itself is light — Node and npm. The other requirements come from whicheve
|
|
|
214
234
|
- **Node.js 24+** (the `web-app` profile enforces this)
|
|
215
235
|
- **npm** (or any package manager that respects workspaces)
|
|
216
236
|
- **beads (`bd`) 1.1.2+** — if you use the agent workflows or `boop loop`. The tree-building workflows depend on `bd gate` and `bd epic close-eligible` (1.1.x) and on 1.1.2 dependency semantics (plain `blocks` edges; `waits-for` is not used), and `boop loop` needs beads in Dolt server mode. See [beads](https://github.com/gastownhall/beads).
|
|
237
|
+
- **`jq`** — optional, and only for `boop loop`. The loop projects its beads queue read through `jq` to keep it small; without `jq` it warns once and falls back to a larger read that still works. `brew install jq` on macOS.
|
|
217
238
|
- **PostgreSQL** — if the `database` capability is installed
|
|
218
239
|
- **RabbitMQ / CloudAMQP account** — if `background-jobs` is installed
|
|
219
240
|
- **SendGrid account** — if `email` is installed
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"@boopkit/cap-hello": "0.4.1",
|
|
9
9
|
"@boopkit/cap-noticeboard": "0.4.1",
|
|
10
10
|
"@boopkit/cap-oauth-client": "0.1.1",
|
|
11
|
-
"@boopkit/cap-oauth-server": "0.1.
|
|
11
|
+
"@boopkit/cap-oauth-server": "0.1.2",
|
|
12
12
|
"@boopkit/cap-product-design": "0.6.7",
|
|
13
13
|
"@boopkit/cap-project-mgmt": "0.4.9",
|
|
14
14
|
"@boopkit/cap-services": "0.4.2",
|
package/dist/cli.js
CHANGED
|
@@ -57,7 +57,8 @@ program
|
|
|
57
57
|
.argument('<name>', 'Project name')
|
|
58
58
|
.option('-p, --profile <profile>', 'Profile to use', 'web-app')
|
|
59
59
|
.option('--no-install', 'Skip npm install')
|
|
60
|
-
.
|
|
60
|
+
.option('--no-beads', 'Skip bd init, so no beads database or Dolt server is created')
|
|
61
|
+
.action((name, options) => (0, init_1.runInit)(name, options));
|
|
61
62
|
program
|
|
62
63
|
.command('list')
|
|
63
64
|
.description('List available profiles or capabilities')
|
package/dist/commands/add.js
CHANGED
|
@@ -1,12 +1,56 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.runAdd = runAdd;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
4
39
|
const child_process_1 = require("child_process");
|
|
5
40
|
const preflight_1 = require("../lib/preflight");
|
|
6
41
|
const execute_1 = require("../lib/execute");
|
|
7
42
|
const interpolate_1 = require("../lib/interpolate");
|
|
8
43
|
const list_1 = require("./list");
|
|
9
44
|
const capability_1 = require("../lib/capability");
|
|
45
|
+
function hasDevScript(projectDir) {
|
|
46
|
+
try {
|
|
47
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf-8'));
|
|
48
|
+
return Boolean(pkg.scripts?.dev);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
10
54
|
function runAdd(capabilityName, options) {
|
|
11
55
|
// 0. Validate capability argument is provided
|
|
12
56
|
if (!capabilityName) {
|
|
@@ -110,7 +154,10 @@ function runAdd(capabilityName, options) {
|
|
|
110
154
|
nextSteps.push({ text: `Run \`npm run ${first}\` — ${desc}` });
|
|
111
155
|
}
|
|
112
156
|
}
|
|
113
|
-
|
|
157
|
+
// A project with no application code, such as a base scaffold, has no dev script
|
|
158
|
+
if (hasDevScript(targetDir)) {
|
|
159
|
+
nextSteps.push({ text: 'Run `npm run dev` to start developing' });
|
|
160
|
+
}
|
|
114
161
|
if (options.json) {
|
|
115
162
|
console.log(JSON.stringify({
|
|
116
163
|
capability: capabilityName,
|
|
@@ -177,7 +224,7 @@ function runAdd(capabilityName, options) {
|
|
|
177
224
|
console.log(` ${(0, interpolate_1.interpolate)(pi.verify, vars)}`);
|
|
178
225
|
}
|
|
179
226
|
}
|
|
180
|
-
else {
|
|
227
|
+
else if (nextSteps.length > 0) {
|
|
181
228
|
console.log('\nNext steps:');
|
|
182
229
|
for (let i = 0; i < nextSteps.length; i++) {
|
|
183
230
|
console.log(` ${i + 1}. ${nextSteps[i].text}`);
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
export interface InitOptions {
|
|
2
2
|
profile?: string;
|
|
3
3
|
install?: boolean;
|
|
4
|
+
beads?: boolean;
|
|
4
5
|
}
|
|
5
|
-
|
|
6
|
+
type RunFn = (file: string, args: string[], opts: {
|
|
7
|
+
cwd: string;
|
|
8
|
+
stdio: 'inherit' | 'pipe';
|
|
9
|
+
}) => unknown;
|
|
10
|
+
/** Replaceable in tests, so no test runs a real git or `bd init`, or starts a Dolt server. */
|
|
11
|
+
export interface InitDeps {
|
|
12
|
+
findBdFn?: (projectDir: string) => string | null;
|
|
13
|
+
execFileSyncFn?: RunFn;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Locate the bd binary: the project's own install first, because project-mgmt
|
|
17
|
+
* pins @beads/bd as a devDependency, then whatever is on PATH.
|
|
18
|
+
*/
|
|
19
|
+
export declare function findBd(projectDir: string, searchPath?: string): string | null;
|
|
20
|
+
export declare function runInit(name: string, options: InitOptions, deps?: InitDeps): void;
|
|
21
|
+
export {};
|
package/dist/commands/init.js
CHANGED
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.findBd = findBd;
|
|
36
37
|
exports.runInit = runInit;
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
@@ -44,9 +45,105 @@ const scaffold_1 = require("../lib/scaffold");
|
|
|
44
45
|
const project_1 = require("../lib/project");
|
|
45
46
|
const env_1 = require("../lib/env");
|
|
46
47
|
const safe_io_1 = require("../lib/safe-io");
|
|
47
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Locate the bd binary: the project's own install first, because project-mgmt
|
|
50
|
+
* pins @beads/bd as a devDependency, then whatever is on PATH.
|
|
51
|
+
*/
|
|
52
|
+
function findBd(projectDir, searchPath = process.env.PATH ?? '') {
|
|
53
|
+
const dirs = [path.join(projectDir, 'node_modules', '.bin'), ...searchPath.split(path.delimiter).filter(Boolean)];
|
|
54
|
+
for (const dir of dirs) {
|
|
55
|
+
const candidate = path.join(dir, 'bd');
|
|
56
|
+
try {
|
|
57
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
58
|
+
return candidate;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// not in this directory — keep looking
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function git(run, cwd, args) {
|
|
67
|
+
return String(run('git', args, { cwd, stdio: 'pipe' })).trim();
|
|
68
|
+
}
|
|
69
|
+
/** The top level of the repository the directory sits inside, or null when it is in none. */
|
|
70
|
+
function findEnclosingRepo(dir, run) {
|
|
71
|
+
try {
|
|
72
|
+
return git(run, dir, ['rev-parse', '--show-toplevel']) || null;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Only an identity the user configured counts; git would otherwise invent one from the hostname. */
|
|
79
|
+
function hasGitIdentity(dir, run) {
|
|
80
|
+
try {
|
|
81
|
+
return ['user.name', 'user.email'].every(key => git(run, dir, ['config', key]) !== '');
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Commit the scaffold in a new repository, so that bd init's commit is the
|
|
89
|
+
* second one rather than the first, and git status is clean afterwards.
|
|
90
|
+
* bd init would otherwise create the repository and commit only the files
|
|
91
|
+
* it touched, leaving the rest of the scaffold untracked.
|
|
92
|
+
*/
|
|
93
|
+
function commitScaffold(targetDir, name, run) {
|
|
94
|
+
if (!hasGitIdentity(targetDir, run)) {
|
|
95
|
+
console.error('Warning: Did not commit the scaffold: git has no user.name and user.email configured, and boop does not set them for you. ' +
|
|
96
|
+
'Set them with git config --global, then commit the scaffold yourself.');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
console.log('\nCommitting the scaffold...');
|
|
101
|
+
git(run, targetDir, ['init']);
|
|
102
|
+
git(run, targetDir, ['add', '-A']);
|
|
103
|
+
git(run, targetDir, ['commit', '-q', '-m', `chore: scaffold ${name} with boop init`]);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.error(`Warning: Unable to commit the scaffold: ${err.message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Commit the scaffold, then run `bd init` in the new project; report and return false rather than throw. */
|
|
110
|
+
function initBeads(targetDir, name, args, deps) {
|
|
111
|
+
const findBdFn = deps.findBdFn ?? findBd;
|
|
112
|
+
const execFileSyncFn = deps.execFileSyncFn ?? child_process_1.execFileSync;
|
|
113
|
+
const bd = findBdFn(targetDir);
|
|
114
|
+
if (!bd) {
|
|
115
|
+
console.error('Warning: Unable to initialize beads: bd was not found on PATH or in node_modules/.bin. ' +
|
|
116
|
+
'Install it (npm install, or brew install beads) and run the bd init command below.');
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
// Observed with bd 1.1.2: inside an enclosing repository, bd init commits its
|
|
120
|
+
// files into that repository and points its core.hooksPath at a folder that
|
|
121
|
+
// does not exist, silently disabling the hooks it had.
|
|
122
|
+
const enclosingRepo = findEnclosingRepo(targetDir, execFileSyncFn);
|
|
123
|
+
if (enclosingRepo) {
|
|
124
|
+
console.error(`Warning: Skipped beads setup: the project is inside another git repository, ${enclosingRepo}. ` +
|
|
125
|
+
'bd init would commit into that repository and redirect its git hooks. ' +
|
|
126
|
+
'To give the project a repository of its own, run git init and commit the scaffold inside it, then run the bd init command below.');
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
commitScaffold(targetDir, name, execFileSyncFn);
|
|
130
|
+
try {
|
|
131
|
+
console.log('\nInitializing beads...');
|
|
132
|
+
execFileSyncFn(bd, args, { cwd: targetDir, stdio: 'inherit' });
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
console.error(`Warning: Unable to initialize beads: ${err.message}`);
|
|
137
|
+
console.error('The project was scaffolded and left in place; run the bd init command below from inside it.');
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function runInit(name, options, deps = {}) {
|
|
48
142
|
const profileName = options.profile ?? 'web-app';
|
|
49
143
|
const shouldInstall = options.install !== false;
|
|
144
|
+
// BOOP_SKIP_BEADS is set for the whole test suite (tests/jest.setup.ts), so the
|
|
145
|
+
// many tests that run init never start a Dolt server.
|
|
146
|
+
const shouldInitBeads = options.beads !== false && !process.env.BOOP_SKIP_BEADS;
|
|
50
147
|
// --- Validate ---
|
|
51
148
|
// Guard: project name must not be empty
|
|
52
149
|
if (!name || !name.trim()) {
|
|
@@ -55,6 +152,10 @@ function runInit(name, options) {
|
|
|
55
152
|
return;
|
|
56
153
|
}
|
|
57
154
|
const targetDir = path.resolve(process.cwd(), name);
|
|
155
|
+
// The name argument can be a path. The project is named after its folder,
|
|
156
|
+
// because a path is not a valid npm package name, a heading, or a beads
|
|
157
|
+
// prefix; the raw argument is kept only for messages about the folder.
|
|
158
|
+
const projectName = path.basename(targetDir);
|
|
58
159
|
// Guard: target directory must not exist or must be empty
|
|
59
160
|
if (fs.existsSync(targetDir)) {
|
|
60
161
|
const entries = fs.readdirSync(targetDir);
|
|
@@ -71,21 +172,22 @@ function runInit(name, options) {
|
|
|
71
172
|
process.exitCode = 1;
|
|
72
173
|
return;
|
|
73
174
|
}
|
|
74
|
-
// --- Guard: a profile
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
175
|
+
// --- Guard: a profile needs a kernel or templates of its own to scaffold ---
|
|
176
|
+
// The kernel templates render a running application, so a profile with no
|
|
177
|
+
// kernel (profiles/development/) has no runtime, port or deploy target to
|
|
178
|
+
// render them with. A kernel-less profile that ships its own templates/
|
|
179
|
+
// (profiles/base/) renders those instead; one with neither is refused here
|
|
180
|
+
// rather than emitting a project that cannot start.
|
|
181
|
+
const kernel = profile.kernel;
|
|
182
|
+
if (!kernel && !(0, scaffold_1.profileHasTemplates)(profileName)) {
|
|
183
|
+
console.error(`Error: Profile "${profileName}" carries neither a kernel nor a templates/ folder, so there is nothing to scaffold.`);
|
|
81
184
|
console.error('It declares capabilities for a project that already exists: name it as the "profile" in that');
|
|
82
185
|
console.error(`project's .boop/config.json, or run \`boop add\` for the capabilities you want.`);
|
|
83
186
|
process.exitCode = 1;
|
|
84
187
|
return;
|
|
85
188
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const requiredNodeVersion = kernel.nodeVersion;
|
|
189
|
+
// --- Node version check (a profile with no kernel names no runtime to check) ---
|
|
190
|
+
const requiredNodeVersion = kernel?.nodeVersion;
|
|
89
191
|
if (requiredNodeVersion && !process.env.BOOP_SKIP_VERSION_CHECK) {
|
|
90
192
|
const minVersion = requiredNodeVersion.replace(/^[>=^~]+/, '');
|
|
91
193
|
const currentMajor = parseInt(process.versions.node.split('.')[0], 10);
|
|
@@ -138,18 +240,23 @@ function runInit(name, options) {
|
|
|
138
240
|
}
|
|
139
241
|
// --- Scaffold kernel ---
|
|
140
242
|
const variables = {
|
|
141
|
-
projectName
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
243
|
+
projectName,
|
|
244
|
+
...(kernel && {
|
|
245
|
+
port: kernel.port,
|
|
246
|
+
scripts: kernel.scripts,
|
|
247
|
+
nodeVersion: kernel.nodeVersion ?? '>=18.0.0',
|
|
248
|
+
}),
|
|
145
249
|
...hookVars,
|
|
146
250
|
};
|
|
147
|
-
const
|
|
251
|
+
const templatesDir = (0, scaffold_1.getTemplatesDir)(profileName);
|
|
252
|
+
const kernelFiles = (0, scaffold_1.scaffoldKernel)(targetDir, variables, false, templatesDir);
|
|
148
253
|
// --- Merge kernel dependencies ---
|
|
149
|
-
|
|
254
|
+
if (kernel) {
|
|
255
|
+
(0, scaffold_1.mergePackageJson)(targetDir, kernel.dependencies, kernel.devDependencies, undefined, { skipBackup: true });
|
|
256
|
+
}
|
|
150
257
|
// --- Create minimal manifest so capability hash storage can find config.json ---
|
|
151
258
|
(0, project_1.writeProjectManifest)(targetDir, {
|
|
152
|
-
name,
|
|
259
|
+
name: projectName,
|
|
153
260
|
profile: profileName,
|
|
154
261
|
boopVersion: '0.1.0',
|
|
155
262
|
capabilities: [],
|
|
@@ -193,10 +300,12 @@ function runInit(name, options) {
|
|
|
193
300
|
variables.hookInit = hookVars.hookInit;
|
|
194
301
|
variables.hookPreRoutes = hookVars.hookPreRoutes;
|
|
195
302
|
variables.hookPostRoutes = hookVars.hookPostRoutes;
|
|
196
|
-
(0, scaffold_1.scaffoldKernel)(targetDir, variables, true);
|
|
303
|
+
(0, scaffold_1.scaffoldKernel)(targetDir, variables, true, templatesDir);
|
|
197
304
|
// Re-scaffolding overwrites package.json (template has empty deps),
|
|
198
305
|
// so re-merge kernel deps + cap package deps that were added earlier.
|
|
199
|
-
|
|
306
|
+
if (kernel) {
|
|
307
|
+
(0, scaffold_1.mergePackageJson)(targetDir, kernel.dependencies, kernel.devDependencies, undefined, { skipBackup: true });
|
|
308
|
+
}
|
|
200
309
|
(0, scaffold_1.mergePackageJson)(targetDir, undefined, undefined, capDeps, { skipBackup: true });
|
|
201
310
|
}
|
|
202
311
|
// Post-install conflict check (when preflight was deferred)
|
|
@@ -294,12 +403,12 @@ function runInit(name, options) {
|
|
|
294
403
|
}
|
|
295
404
|
// --- Generate .env and .env.example ---
|
|
296
405
|
if (envEntries.length > 0) {
|
|
297
|
-
(0, env_1.generateEnvFiles)(targetDir, envEntries,
|
|
406
|
+
(0, env_1.generateEnvFiles)(targetDir, envEntries, projectName);
|
|
298
407
|
}
|
|
299
408
|
// --- Write final manifest, preserving capabilityMeta from hash storage ---
|
|
300
409
|
const existingManifest = (0, project_1.readProjectManifest)(targetDir);
|
|
301
410
|
const manifest = {
|
|
302
|
-
name,
|
|
411
|
+
name: projectName,
|
|
303
412
|
profile: profileName,
|
|
304
413
|
boopVersion: '0.1.0',
|
|
305
414
|
capabilities: installedCapabilities,
|
|
@@ -309,10 +418,10 @@ function runInit(name, options) {
|
|
|
309
418
|
};
|
|
310
419
|
(0, project_1.writeProjectManifest)(targetDir, manifest);
|
|
311
420
|
// --- Print summary ---
|
|
312
|
-
console.log(`\nCreated project "${
|
|
421
|
+
console.log(`\nCreated project "${projectName}" with profile "${profileName}"\n`);
|
|
313
422
|
const scriptCount = Object.keys(allScripts).length;
|
|
314
423
|
const parts = [
|
|
315
|
-
`${kernelFiles.length} kernel files`,
|
|
424
|
+
`${kernelFiles.length} ${kernel ? 'kernel' : 'template'} files`,
|
|
316
425
|
`${installedCapabilities.length} capabilities`,
|
|
317
426
|
`${capabilityFiles.length} capability files`,
|
|
318
427
|
...(scriptCount > 0 ? [`${scriptCount} scripts`] : []),
|
|
@@ -375,11 +484,26 @@ function runInit(name, options) {
|
|
|
375
484
|
}
|
|
376
485
|
}
|
|
377
486
|
}
|
|
487
|
+
// --- Initialize beads (project-mgmt brings it) ---
|
|
488
|
+
// Last, because bd init appends to AGENTS.md, CLAUDE.md and .gitignore when
|
|
489
|
+
// they exist but creates them when they don't, and the templates never
|
|
490
|
+
// overwrite an existing file. Server mode, because boop loop refuses a
|
|
491
|
+
// project whose beads runs embedded. Git is touched only here, just before
|
|
492
|
+
// bd init, because that is when a repository gets created anyway. bd derives
|
|
493
|
+
// its database name from the prefix.
|
|
494
|
+
const bdInitArgs = ['init', '--non-interactive', '--server', '-p', projectName];
|
|
495
|
+
const beadsPending = installedCapabilities.includes('project-mgmt') &&
|
|
496
|
+
!(shouldInitBeads && initBeads(targetDir, projectName, bdInitArgs, deps));
|
|
378
497
|
console.log(`\nNext steps:`);
|
|
379
498
|
console.log(` cd ${name}`);
|
|
380
499
|
if (!shouldInstall) {
|
|
381
500
|
console.log(' npm install');
|
|
382
501
|
}
|
|
502
|
+
if (beadsPending) {
|
|
503
|
+
console.log(` bd ${bdInitArgs.join(' ')}`);
|
|
504
|
+
}
|
|
383
505
|
console.log(' boop status ← check what needs configuration');
|
|
384
|
-
|
|
506
|
+
if (kernel) {
|
|
507
|
+
console.log(' npm run dev ← start development server');
|
|
508
|
+
}
|
|
385
509
|
}
|
|
@@ -138,7 +138,9 @@ function defaultBdList(deps) {
|
|
|
138
138
|
const execSyncFn = deps.execSyncFn || defaultExec;
|
|
139
139
|
return (status) => {
|
|
140
140
|
try {
|
|
141
|
-
const raw = execSyncFn(`bd list --status=${status} --json 2>/dev/null
|
|
141
|
+
const raw = execSyncFn(`bd list --status=${status} --json 2>/dev/null`, {
|
|
142
|
+
maxBuffer: loop_1.BD_READ_MAX_BUFFER,
|
|
143
|
+
});
|
|
142
144
|
return JSON.parse(raw);
|
|
143
145
|
}
|
|
144
146
|
catch {
|
|
@@ -150,7 +152,7 @@ function defaultGetParent(deps) {
|
|
|
150
152
|
const execSyncFn = deps.execSyncFn || defaultExec;
|
|
151
153
|
return (id) => {
|
|
152
154
|
try {
|
|
153
|
-
const data = JSON.parse(execSyncFn(`bd show ${id} --json 2>/dev/null
|
|
155
|
+
const data = JSON.parse(execSyncFn(`bd show ${id} --json 2>/dev/null`, { maxBuffer: loop_1.BD_READ_MAX_BUFFER }));
|
|
154
156
|
return (data[0] || data).parent || null;
|
|
155
157
|
}
|
|
156
158
|
catch {
|
package/dist/commands/loop.d.ts
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output cap for every `bd … --json` read the loop makes.
|
|
3
|
+
*
|
|
4
|
+
* Node's execSync defaults to 1 MiB and throws ENOBUFS past it. `bd ready --json`
|
|
5
|
+
* returns each issue WITH its full description, and `bd show --children --json`
|
|
6
|
+
* on a large container is the same trap — a mature backlog crosses 1 MiB and the
|
|
7
|
+
* read dies. The ready-queue read also projects its output (see
|
|
8
|
+
* READY_QUEUE_CMD); this cap is the backstop for the reads that cannot (boop-fl22).
|
|
9
|
+
*/
|
|
10
|
+
export declare const BD_READ_MAX_BUFFER: number;
|
|
1
11
|
export interface LoopOptions {
|
|
2
12
|
run?: boolean;
|
|
3
13
|
prefix?: string;
|
|
@@ -111,7 +121,7 @@ export declare function writeWorktreeMetadata(ctx: WorktreeContext, meta: {
|
|
|
111
121
|
interface ReadyIssue {
|
|
112
122
|
id: string;
|
|
113
123
|
issue_type: string;
|
|
114
|
-
parent?: string;
|
|
124
|
+
parent?: string | null;
|
|
115
125
|
}
|
|
116
126
|
export declare function buildPlanningPrompt(workContent: string, flowContent: string, prefix?: string): string;
|
|
117
127
|
export type RunClaudeFn = (prompt: string, opts: {
|
|
@@ -135,6 +145,7 @@ export declare function planWork(opts: {
|
|
|
135
145
|
runClaudeFn?: RunClaudeFn;
|
|
136
146
|
execSyncFn?: (cmd: string, opts: {
|
|
137
147
|
encoding: string;
|
|
148
|
+
maxBuffer?: number;
|
|
138
149
|
}) => string;
|
|
139
150
|
}): Promise<PlanResult>;
|
|
140
151
|
export declare function findFallbackFeature(features: Array<{
|
|
@@ -165,7 +176,7 @@ export declare function resolveModel(optionModel: string | undefined, env?: Node
|
|
|
165
176
|
export declare function shouldResumeUnclosedIssue(status: string | undefined): boolean;
|
|
166
177
|
export declare function isIssueInScope(issue: {
|
|
167
178
|
id: string;
|
|
168
|
-
parent?: string;
|
|
179
|
+
parent?: string | null;
|
|
169
180
|
}, prefix: string, getParentById: (id: string) => string | null): boolean;
|
|
170
181
|
/**
|
|
171
182
|
* A workflow-owned feature root is a feature-type issue that owns child tasks
|
|
@@ -196,9 +207,17 @@ export declare function shouldCloseFeatureRoot(feature: {
|
|
|
196
207
|
} | null, children: {
|
|
197
208
|
status: string;
|
|
198
209
|
}[]): boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Read the ready queue, filtered to what this loop may claim.
|
|
212
|
+
*
|
|
213
|
+
* THROWS on a failed read. It must never answer a broken read with an empty
|
|
214
|
+
* array: the caller treats "no issues" as a clean finish, so a swallowed
|
|
215
|
+
* failure makes the loop exit 0 with work undone (boop-fl22).
|
|
216
|
+
*/
|
|
199
217
|
export declare function getMatchingReadyIssues(prefix: string, includeFeatures: boolean, deps?: {
|
|
200
218
|
execSyncFn?: (cmd: string, opts: {
|
|
201
219
|
encoding: string;
|
|
220
|
+
maxBuffer?: number;
|
|
202
221
|
}) => string;
|
|
203
222
|
}): ReadyIssue[];
|
|
204
223
|
/**
|