@bvdm/delano 0.2.1 → 0.2.2

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bart van der Meeren
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -15,9 +15,19 @@ The npm package is intentionally thin. It distributes the approved runtime paylo
15
15
  ## Delano CLI
16
16
 
17
17
  - Package: `@bvdm/delano`
18
+ - Current package version: `0.2.2`
18
19
  - Binary: `delano`
19
20
  - Commands: `onboarding`, `install`, `viewer`, `init`, `validate`, `status`, `next`
20
- - Primary v1.1 goal: bootstrap a repo safely, then stay out of the way
21
+ - Primary goal: bootstrap a repo safely, expose local delivery state clearly, and keep runtime gates verifiable
22
+
23
+ ## Recent main changes
24
+
25
+ The latest main merges moved Delano beyond a thin install wrapper:
26
+
27
+ - PR #4, `feat/delano-vnext-runtime-upgrade`, merged on 2026-05-04. This added the v0.2 runtime layer: schema-backed `.project` artifact validation, operating-mode and status-transition checks, evidence mapping, strict validation fixtures, privacy-safe logging defaults, package/payload drift checks, dry-run sync inspection, apply-gated repair planning, lease-based multi-agent coordination, worktree health checks, delivery metrics, context audits, skill-output eval fixtures, and compact root/adapter agent instructions.
28
+ - PR #3, `delano-viewer-design-overhaul`, merged on 2026-04-29. This added the packaged read-only Delano viewer under `.delano/viewer`, including the local Node server, static UI, `.project` indexing APIs, project outlines, workstream/task navigation, rendered markdown, context-aware filters, guarded open actions, and visual/browser smoke evidence.
29
+
30
+ The current runtime still treats `HANDBOOK.md` and `.project/` as the source of truth. The new pieces make that model easier to inspect and harder to bypass accidentally.
21
31
 
22
32
  ## One-command bootstrap
23
33
 
@@ -123,11 +133,11 @@ bash .agents/scripts/pm/status.sh
123
133
  bash .agents/scripts/pm/next.sh --all
124
134
  ```
125
135
 
126
- The viewer is packaged with `@bvdm/delano` and serves the selected repository's `.project` files read-only. It defaults to `http://127.0.0.1:3977`; set `DELANO_VIEWER_PORT` or `PORT` to use another port.
136
+ The viewer is packaged with `@bvdm/delano` and serves the selected repository's `.project` files read-only. It defaults to `http://127.0.0.1:3977`; set `DELANO_VIEWER_PORT` or `PORT` to use another port. It indexes `.project/context`, `.project/templates`, and `.project/projects`, then derives artifact roles, statuses, project outlines, task/workstream relationships, snippets, and rendered markdown for local inspection.
127
137
 
128
138
  ## Required dependencies
129
139
 
130
- Delano v1.1 assumes these tools are available:
140
+ Delano assumes these tools are available:
131
141
 
132
142
  - `node` 18 or newer
133
143
  - `bash`
@@ -144,6 +154,7 @@ The CLI does not bundle its own shell or Python runtime.
144
154
  - it aborts if an approved target path already exists
145
155
  - it reports each conflict as file, directory, or symlink
146
156
  - it only overwrites approved allowlist paths when `--force` is used
157
+ - it can narrow updates with `--only`, `--exclude`, `--no-project-context`, and `--no-project-state`
147
158
  - it does not touch unrelated files outside the allowlist
148
159
  - it does not install or overwrite repo-root Git config files such as `.gitignore` or `.gitattributes`
149
160
 
@@ -152,6 +163,19 @@ The base install payload includes `.delano/`, including the read-only viewer UI.
152
163
  The installable `.project/context/` pack is seeded from generic templates during packaging; it does not ship Delano's own repo-specific context files into consumer repositories.
153
164
  After install, the recommended first step is `delano onboarding`, which requires explicit approval before it reviews `AGENTS.md`.
154
165
 
166
+ For an update-safe refresh that avoids repo-owned project state, narrow the plan before forcing overwrites:
167
+
168
+ ```bash
169
+ delano install --interactive
170
+ delano install --only skills,project-templates --force --yes
171
+ delano install --exclude project-context,project-projects,project-registry --force --yes
172
+ delano install --no-project-state --force --yes
173
+ ```
174
+
175
+ The interactive installer presents presets for updating the runtime while preserving project state, updating only skills and project templates, full install or repair, and custom category selection.
176
+
177
+ Install categories are `agent-runtime`, `skills`, `viewer`, `project-context`, `project-templates`, `project-registry`, `project-projects`, `handbook`, and `legacy-installer`. The `--no-project-state` shortcut excludes `.project/context`, `.project/projects`, and `.project/registry`.
178
+
155
179
  ## Optional AGENTS.md / CLAUDE.md snippet
156
180
 
157
181
  If you want explicit Delano instructions in a repo-root `AGENTS.md` or `CLAUDE.md`, copy and paste this yourself:
@@ -174,16 +198,38 @@ When working in this repository:
174
198
  - use `delano viewer` to inspect `.project/` through the read-only local UI
175
199
  ```
176
200
 
177
- ## v1.1 boundaries
201
+ ## Runtime boundaries
178
202
 
179
203
  This package is deliberately narrow:
180
204
 
181
205
  - npm is the distribution surface
182
206
  - `.project` remains repo-owned after install
183
207
  - `.project/context/` installs as generic starter context that the target repo must replace with its own reality
208
+ - `.project/projects/` and `.project/registry/` are repo-owned state and should normally be excluded from forced refreshes
184
209
  - `.agents` remains the runtime surface
185
- - wrapper commands stay thin in v1.1
210
+ - wrapper commands stay thin
186
211
  - `install-delano.sh` remains available as the legacy bridge installer
212
+ - remote GitHub/Linear writes remain outside the default flow; current sync tooling is dry-run and repair-plan oriented unless an operator explicitly approves an apply-capable workflow
213
+
214
+ ## Runtime validation
215
+
216
+ The v0.2 runtime upgrade expanded `delano validate` and `bash .agents/scripts/pm/validate.sh` with local gates for:
217
+
218
+ - artifact schemas, artifact scope, operating modes, status transitions, dependencies, blockers, and acceptance/evidence mapping
219
+ - privacy-safe prompt/log defaults and path-output safety
220
+ - package manifest and install payload drift
221
+ - local/GitHub/Linear sync inspection, drift reporting, and apply-gated repair planning
222
+ - lease contracts, conflict zones, stream-aware next-task selection, handoff summaries, and worktree health
223
+ - delivery metrics, project metrics summaries, context audit scoring, skill-output eval fixtures, and closeout learning proposals
224
+
225
+ For release readiness, run:
226
+
227
+ ```bash
228
+ npm run build:assets
229
+ npm run check:package-manifest
230
+ bash .agents/scripts/pm/validate.sh
231
+ npm test
232
+ ```
187
233
 
188
234
  ## Local development
189
235
 
@@ -208,7 +254,7 @@ Before the first Actions publish, configure npm trusted publishing for `@bvdm/de
208
254
 
209
255
  The package metadata must keep `repository.url` set to `https://github.com/MajesteitBart/delano`; npm validates that value against the GitHub Actions provenance bundle.
210
256
 
211
- After trusted publishing is configured, publish by pushing a matching version tag such as `v0.2.0`, or run the `Publish package to npm` workflow manually from `main`. The workflow rebuilds the package payload, checks manifest drift, runs tests, dry-runs the package contents, verifies the version is not already published, and then runs `npm publish --access public` from GitHub Actions using OIDC. A manual `dry_run` input is available to run the same checks without publishing.
257
+ After trusted publishing is configured, publish by pushing a matching version tag such as `v0.2.2`, or run the `Publish package to npm` workflow manually from `main`. The workflow rebuilds the package payload, checks manifest drift, runs tests, dry-runs the package contents, verifies the version is not already published, and then runs `npm publish --access public` from GitHub Actions using OIDC. A manual `dry_run` input is available to run the same checks without publishing.
212
258
 
213
259
  If npm publish fails after the package checks pass, verify that the npm trusted publisher settings match the repository and workflow filename exactly, and that the workflow has `id-token: write`.
214
260
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@bvdm/delano",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "CLI for the Delano delivery runtime.",
5
- "license": "UNLICENSED",
5
+ "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/MajesteitBart/delano"
@@ -2,6 +2,7 @@ const {
2
2
  applyInstallPlan,
3
3
  buildInstallPlan,
4
4
  collectConflicts,
5
+ configureInteractiveInstall,
5
6
  confirmInstall,
6
7
  parseInstallArgs,
7
8
  printConflicts,
@@ -17,24 +18,43 @@ function getInstallHelp() {
17
18
  "Options:",
18
19
  " --target <dir> Install into the given directory. Defaults to the current working directory.",
19
20
  " --agents <list> Comma-separated agent list for future opt-in adapter docs: claude,codex,opencode,pi.",
21
+ " --only <list> Install only selected categories.",
22
+ " --exclude <list> Omit selected categories from the install plan.",
23
+ " --no-project-state Omit .project/context, .project/projects, and .project/registry.",
24
+ " --no-project-context",
25
+ " Omit .project/context starter templates.",
26
+ " --interactive Choose an install preset and target in the terminal.",
27
+ " --tui Alias for --interactive.",
20
28
  " --force Overwrite existing allowlisted target paths. Does not override parent-path blockers.",
21
29
  " --yes Skip the final confirmation prompt.",
22
30
  " -h, --help Show command help.",
23
31
  "",
32
+ "Categories:",
33
+ " agent-runtime, skills, viewer, project-context, project-templates,",
34
+ " project-registry, project-projects, handbook, legacy-installer",
35
+ "",
24
36
  "Behavior:",
25
37
  " - Computes the full install plan before writing files.",
26
38
  " - Aborts on conflicts by default.",
39
+ " - Filters the plan before conflict detection when --only or --exclude is used.",
40
+ " - Treats .project/context, .project/projects, and .project/registry as repo-owned state after install.",
27
41
  " - Only installs the approved base payload; top-level adapter entry docs remain opt-in and are not installed in v1.",
28
42
  "",
29
43
  "Examples:",
44
+ " delano install --interactive",
30
45
  " delano install --target ../repo --yes",
46
+ " delano install --only skills,project-templates --force --yes",
47
+ " delano install --exclude project-context,project-projects,project-registry --force --yes",
31
48
  " delano --yes",
32
49
  " npx -y @bvdm/delano@latest --yes"
33
50
  ].join("\n");
34
51
  }
35
52
 
36
53
  async function runInstall(args) {
37
- const options = parseInstallArgs(args);
54
+ let options = parseInstallArgs(args);
55
+ if (options.interactive) {
56
+ options = await configureInteractiveInstall(options);
57
+ }
38
58
  const plan = buildInstallPlan(options);
39
59
  const conflicts = collectConflicts(plan);
40
60
  const unforceableConflicts = conflicts.filter((conflict) => !conflict.forceable);
@@ -8,13 +8,110 @@ const {
8
8
  statSync,
9
9
  } = require("node:fs");
10
10
  const path = require("node:path");
11
- const readline = require("node:readline/promises");
11
+ const readline = require("node:readline");
12
12
  const { stdin, stdout } = require("node:process");
13
13
 
14
14
  const { CliError } = require("./errors");
15
15
  const { getPackageRoot, getPathType } = require("./runtime");
16
16
 
17
17
  const SUPPORTED_AGENTS = ["claude", "codex", "opencode", "pi"];
18
+ const INSTALL_CATEGORIES = [
19
+ {
20
+ name: "agent-runtime",
21
+ description: ".agents runtime except skills",
22
+ matches: (target) => target.startsWith(".agents/") && !target.startsWith(".agents/skills/")
23
+ },
24
+ {
25
+ name: "skills",
26
+ description: ".agents/skills",
27
+ matches: (target) => target.startsWith(".agents/skills/")
28
+ },
29
+ {
30
+ name: "viewer",
31
+ description: ".delano viewer files",
32
+ matches: (target) => target.startsWith(".delano/")
33
+ },
34
+ {
35
+ name: "project-context",
36
+ description: ".project/context starter templates",
37
+ matches: (target) => target.startsWith(".project/context/")
38
+ },
39
+ {
40
+ name: "project-templates",
41
+ description: ".project/templates",
42
+ matches: (target) => target.startsWith(".project/templates/")
43
+ },
44
+ {
45
+ name: "project-registry",
46
+ description: ".project/registry",
47
+ matches: (target) => target.startsWith(".project/registry/")
48
+ },
49
+ {
50
+ name: "project-projects",
51
+ description: ".project/projects seed files",
52
+ matches: (target) => target.startsWith(".project/projects/")
53
+ },
54
+ {
55
+ name: "handbook",
56
+ description: "HANDBOOK.md",
57
+ matches: (target) => target === "HANDBOOK.md"
58
+ },
59
+ {
60
+ name: "legacy-installer",
61
+ description: "install-delano.sh",
62
+ matches: (target) => target === "install-delano.sh"
63
+ }
64
+ ];
65
+
66
+ const INSTALL_CATEGORY_ALIASES = new Map([
67
+ ["agent-skills", "skills"],
68
+ ["agents", "agent-runtime"],
69
+ ["runtime", "agent-runtime"],
70
+ ["context", "project-context"],
71
+ ["templates", "project-templates"],
72
+ ["project-state", "project-projects"],
73
+ ["projects", "project-projects"],
74
+ ["registry", "project-registry"],
75
+ ["delano", "viewer"],
76
+ ["installer", "legacy-installer"]
77
+ ]);
78
+
79
+ const INSTALL_CATEGORY_NAMES = INSTALL_CATEGORIES.map((category) => category.name);
80
+ const PROJECT_STATE_CATEGORIES = ["project-context", "project-projects", "project-registry"];
81
+ const INSTALL_PRESETS = [
82
+ {
83
+ id: "update-safe",
84
+ label: "Update Delano runtime, preserve project state",
85
+ description: "Refresh runtime files while excluding .project/context, .project/projects, and .project/registry.",
86
+ only: null,
87
+ exclude: PROJECT_STATE_CATEGORIES,
88
+ force: true
89
+ },
90
+ {
91
+ id: "skills-templates",
92
+ label: "Update skills and project templates",
93
+ description: "Refresh .agents/skills and .project/templates only.",
94
+ only: ["skills", "project-templates"],
95
+ exclude: [],
96
+ force: true
97
+ },
98
+ {
99
+ id: "full",
100
+ label: "Full install or repair",
101
+ description: "Install every allowlisted category. This includes project starter state.",
102
+ only: null,
103
+ exclude: [],
104
+ force: false
105
+ },
106
+ {
107
+ id: "custom",
108
+ label: "Choose categories",
109
+ description: "Pick exact categories and force behavior.",
110
+ only: null,
111
+ exclude: [],
112
+ force: false
113
+ }
114
+ ];
18
115
 
19
116
  function getMissingPackagedAssetMessage(relativePath) {
20
117
  return [
@@ -64,12 +161,47 @@ function parseAgentList(rawValue) {
64
161
  return selected;
65
162
  }
66
163
 
164
+ function parseCategoryList(rawValue, optionName) {
165
+ if (!rawValue) {
166
+ throw new CliError(`Missing value for ${optionName}.`, 1);
167
+ }
168
+
169
+ const selected = [];
170
+ for (const chunk of rawValue.split(",")) {
171
+ const value = chunk.trim().toLowerCase();
172
+ if (!value) {
173
+ continue;
174
+ }
175
+
176
+ const categoryName = INSTALL_CATEGORY_ALIASES.get(value) || value;
177
+ if (!INSTALL_CATEGORY_NAMES.includes(categoryName)) {
178
+ throw new CliError(
179
+ `Unknown install category '${value}'. Supported values: ${INSTALL_CATEGORY_NAMES.join(", ")}.`,
180
+ 1
181
+ );
182
+ }
183
+
184
+ if (!selected.includes(categoryName)) {
185
+ selected.push(categoryName);
186
+ }
187
+ }
188
+
189
+ if (selected.length === 0) {
190
+ throw new CliError(`No install categories selected for ${optionName}.`, 1);
191
+ }
192
+
193
+ return selected;
194
+ }
195
+
67
196
  function parseInstallArgs(args) {
68
197
  const options = {
69
198
  target: process.cwd(),
70
199
  force: false,
71
200
  yes: false,
72
- agents: [...SUPPORTED_AGENTS]
201
+ interactive: false,
202
+ agents: [...SUPPORTED_AGENTS],
203
+ only: null,
204
+ exclude: []
73
205
  };
74
206
 
75
207
  for (let index = 0; index < args.length; index += 1) {
@@ -103,6 +235,42 @@ function parseInstallArgs(args) {
103
235
  continue;
104
236
  }
105
237
 
238
+ if (arg === "--only") {
239
+ index += 1;
240
+ options.only = parseCategoryList(args[index], "--only");
241
+ continue;
242
+ }
243
+
244
+ if (arg.startsWith("--only=")) {
245
+ options.only = parseCategoryList(arg.slice("--only=".length), "--only");
246
+ continue;
247
+ }
248
+
249
+ if (arg === "--exclude") {
250
+ index += 1;
251
+ options.exclude = parseCategoryList(args[index], "--exclude");
252
+ continue;
253
+ }
254
+
255
+ if (arg.startsWith("--exclude=")) {
256
+ options.exclude = parseCategoryList(arg.slice("--exclude=".length), "--exclude");
257
+ continue;
258
+ }
259
+
260
+ if (arg === "--no-project-context") {
261
+ options.exclude = mergeCategoryLists(options.exclude, ["project-context"]);
262
+ continue;
263
+ }
264
+
265
+ if (arg === "--no-project-state") {
266
+ options.exclude = mergeCategoryLists(options.exclude, [
267
+ "project-context",
268
+ "project-projects",
269
+ "project-registry"
270
+ ]);
271
+ continue;
272
+ }
273
+
106
274
  if (arg === "--force") {
107
275
  options.force = true;
108
276
  continue;
@@ -113,6 +281,11 @@ function parseInstallArgs(args) {
113
281
  continue;
114
282
  }
115
283
 
284
+ if (arg === "--interactive" || arg === "--tui") {
285
+ options.interactive = true;
286
+ continue;
287
+ }
288
+
116
289
  throw new CliError(`Unknown install option: ${arg}`, 1);
117
290
  }
118
291
 
@@ -120,9 +293,170 @@ function parseInstallArgs(args) {
120
293
  return options;
121
294
  }
122
295
 
296
+ function applyInstallPreset(options, presetId) {
297
+ const preset = INSTALL_PRESETS.find((candidate) => candidate.id === presetId);
298
+ if (!preset) {
299
+ throw new CliError(`Unknown install preset: ${presetId}`, 1);
300
+ }
301
+
302
+ return {
303
+ ...options,
304
+ only: preset.only ? [...preset.only] : null,
305
+ exclude: [...preset.exclude],
306
+ force: preset.force
307
+ };
308
+ }
309
+
310
+ async function configureInteractiveInstall(options) {
311
+ const prompt = createPrompter(stdin, stdout);
312
+ try {
313
+ console.log("Delano install");
314
+ console.log("==============");
315
+ console.log("");
316
+ console.log("Choose what to install:");
317
+ INSTALL_PRESETS.forEach((preset, index) => {
318
+ console.log(` ${index + 1}. ${preset.label}`);
319
+ console.log(` ${preset.description}`);
320
+ });
321
+ console.log("");
322
+
323
+ const presetAnswer = await prompt.ask("Selection [1]: ");
324
+ const presetIndex = parseSelectionNumber(presetAnswer, 1, INSTALL_PRESETS.length, 1) - 1;
325
+ const preset = INSTALL_PRESETS[presetIndex];
326
+ let configured = applyInstallPreset(options, preset.id);
327
+
328
+ const targetAnswer = await prompt.ask(`Target [${configured.target}]: `);
329
+ if (targetAnswer.trim()) {
330
+ configured.target = path.resolve(targetAnswer.trim());
331
+ }
332
+
333
+ if (preset.id === "custom") {
334
+ configured = await configureCustomInstallSelection(configured, prompt);
335
+ } else {
336
+ const forceDefault = configured.force ? "Y/n" : "y/N";
337
+ const forceAnswer = await prompt.ask(`Overwrite selected existing files with --force? [${forceDefault}] `);
338
+ configured.force = parseYesNo(forceAnswer, configured.force);
339
+ }
340
+
341
+ return configured;
342
+ } finally {
343
+ prompt.close();
344
+ }
345
+ }
346
+
347
+ async function configureCustomInstallSelection(options, prompt) {
348
+ console.log("");
349
+ console.log("Categories:");
350
+ INSTALL_CATEGORIES.forEach((category, index) => {
351
+ console.log(` ${index + 1}. ${category.name} - ${category.description}`);
352
+ });
353
+ console.log("");
354
+ console.log("Enter category numbers or names separated by commas.");
355
+ console.log("Use 'all' for every category.");
356
+
357
+ const categoryAnswer = await prompt.ask("Categories [all]: ");
358
+ const only = parseInteractiveCategorySelection(categoryAnswer);
359
+ const forceAnswer = await prompt.ask("Overwrite selected existing files with --force? [y/N] ");
360
+
361
+ return {
362
+ ...options,
363
+ only,
364
+ exclude: [],
365
+ force: parseYesNo(forceAnswer, false)
366
+ };
367
+ }
368
+
369
+ function createPrompter(input, output) {
370
+ const rl = readline.createInterface({ input, crlfDelay: Infinity });
371
+ const iterator = rl[Symbol.asyncIterator]();
372
+
373
+ return {
374
+ async ask(promptText) {
375
+ output.write(promptText);
376
+ const next = await iterator.next();
377
+ const answer = next.done ? "" : next.value;
378
+ output.write("\n");
379
+ return answer;
380
+ },
381
+ close() {
382
+ rl.close();
383
+ }
384
+ };
385
+ }
386
+
387
+
388
+ function parseSelectionNumber(rawValue, min, max, defaultValue) {
389
+ const value = rawValue.trim();
390
+ if (!value) {
391
+ return defaultValue;
392
+ }
393
+
394
+ const parsed = Number(value);
395
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
396
+ throw new CliError(`Selection must be a number from ${min} to ${max}.`, 1);
397
+ }
398
+ return parsed;
399
+ }
400
+
401
+ function parseYesNo(rawValue, defaultValue) {
402
+ const value = rawValue.trim().toLowerCase();
403
+ if (!value) {
404
+ return defaultValue;
405
+ }
406
+ if (value === "y" || value === "yes") {
407
+ return true;
408
+ }
409
+ if (value === "n" || value === "no") {
410
+ return false;
411
+ }
412
+ throw new CliError("Answer must be yes or no.", 1);
413
+ }
414
+
415
+ function parseInteractiveCategorySelection(rawValue) {
416
+ const value = rawValue.trim();
417
+ if (!value || value.toLowerCase() === "all") {
418
+ return null;
419
+ }
420
+
421
+ const selected = [];
422
+ for (const chunk of value.split(",")) {
423
+ const token = chunk.trim();
424
+ if (!token) {
425
+ continue;
426
+ }
427
+
428
+ const numeric = Number(token);
429
+ const categoryName = Number.isInteger(numeric)
430
+ ? INSTALL_CATEGORIES[numeric - 1]?.name
431
+ : (INSTALL_CATEGORY_ALIASES.get(token.toLowerCase()) || token.toLowerCase());
432
+
433
+ if (!categoryName || !INSTALL_CATEGORY_NAMES.includes(categoryName)) {
434
+ throw new CliError(
435
+ `Unknown install category '${token}'. Supported values: ${INSTALL_CATEGORY_NAMES.join(", ")}.`,
436
+ 1
437
+ );
438
+ }
439
+
440
+ if (!selected.includes(categoryName)) {
441
+ selected.push(categoryName);
442
+ }
443
+ }
444
+
445
+ if (selected.length === 0) {
446
+ throw new CliError("No install categories selected.", 1);
447
+ }
448
+
449
+ return selected;
450
+ }
451
+
123
452
  function buildInstallPlan(options) {
124
453
  const { manifest, entries, payloadRoot } = readInstallManifest();
125
- const items = entries.map((entry) => {
454
+ const selectedEntries = filterManifestEntries(entries, options);
455
+ if (selectedEntries.length === 0) {
456
+ throw new CliError("Install selection matched no files.", 1);
457
+ }
458
+
459
+ const items = selectedEntries.map((entry) => {
126
460
  const sourcePath = path.join(payloadRoot, entry.target);
127
461
  if (!existsSync(sourcePath)) {
128
462
  throw new CliError(getMissingPackagedAssetMessage(entry.target), 1);
@@ -137,11 +471,41 @@ function buildInstallPlan(options) {
137
471
 
138
472
  return {
139
473
  manifest,
474
+ selectedEntries,
475
+ skippedCount: entries.length - selectedEntries.length,
140
476
  items,
141
477
  targetRoot: options.target
142
478
  };
143
479
  }
144
480
 
481
+ function mergeCategoryLists(left, right) {
482
+ const merged = [...left];
483
+ for (const item of right) {
484
+ if (!merged.includes(item)) {
485
+ merged.push(item);
486
+ }
487
+ }
488
+ return merged;
489
+ }
490
+
491
+ function getInstallCategory(target) {
492
+ const normalizedTarget = target.replace(/\\/g, "/");
493
+ return INSTALL_CATEGORIES.find((category) => category.matches(normalizedTarget))?.name || "uncategorized";
494
+ }
495
+
496
+ function filterManifestEntries(entries, options) {
497
+ const only = options.only ? new Set(options.only) : null;
498
+ const exclude = new Set(options.exclude || []);
499
+
500
+ return entries.filter((entry) => {
501
+ const category = getInstallCategory(entry.target);
502
+ if (only && !only.has(category)) {
503
+ return false;
504
+ }
505
+ return !exclude.has(category);
506
+ });
507
+ }
508
+
145
509
  function collectConflicts(plan) {
146
510
  const conflicts = [];
147
511
 
@@ -191,10 +555,18 @@ function printPlanSummary(plan, options) {
191
555
  console.log("------------");
192
556
  console.log(`Target: ${options.target}`);
193
557
  console.log(`Files: ${plan.items.length}`);
558
+ if (plan.skippedCount > 0) {
559
+ console.log(`Skipped by selection: ${plan.skippedCount}`);
560
+ }
194
561
  console.log(`Agents: ${options.agents.join(", ")}`);
562
+ console.log(`Only: ${options.only ? options.only.join(", ") : "all categories"}`);
563
+ if (options.exclude.length > 0) {
564
+ console.log(`Exclude: ${options.exclude.join(", ")}`);
565
+ }
195
566
  console.log(`Force: ${options.force ? "yes" : "no"}`);
196
567
  console.log("");
197
568
  console.log("Note: --agents is accepted now for forward compatibility, but v1 base install still excludes top-level adapter entry docs by default.");
569
+ console.log("Note: .project/context, .project/projects, and .project/registry are repo-owned after install; use --no-project-state or --only for update-safe refreshes.");
198
570
  }
199
571
 
200
572
  function printConflicts(conflicts, options) {
@@ -211,7 +583,7 @@ function printConflicts(conflicts, options) {
211
583
  if (options.force) {
212
584
  console.error("Install cannot continue because at least one conflict is not forceable.");
213
585
  } else {
214
- console.error("Install aborted before writing files. Re-run with --force to overwrite only allowlisted target paths.");
586
+ console.error("Install aborted before writing files. Re-run with --force to overwrite only selected allowlisted target paths, or narrow the plan with --only/--exclude.");
215
587
  }
216
588
  }
217
589
 
@@ -220,15 +592,15 @@ async function confirmInstall(plan, options) {
220
592
  return true;
221
593
  }
222
594
 
223
- const rl = readline.createInterface({ input: stdin, output: stdout });
595
+ const prompt = createPrompter(stdin, stdout);
224
596
  try {
225
- const prompt = options.force
597
+ const promptText = options.force
226
598
  ? `Proceed with force-installing ${plan.items.length} files into ${options.target}? [y/N] `
227
599
  : `Proceed with installing ${plan.items.length} files into ${options.target}? [y/N] `;
228
- const answer = await rl.question(prompt);
600
+ const answer = await prompt.ask(promptText);
229
601
  return /^[Yy](es)?$/.test(answer.trim());
230
602
  } finally {
231
- rl.close();
603
+ prompt.close();
232
604
  }
233
605
  }
234
606
 
@@ -294,12 +666,21 @@ function validateManifestPath(relativePath, fieldName) {
294
666
  }
295
667
 
296
668
  module.exports = {
669
+ INSTALL_CATEGORIES,
670
+ INSTALL_PRESETS,
297
671
  SUPPORTED_AGENTS,
298
672
  applyInstallPlan,
673
+ applyInstallPreset,
299
674
  buildInstallPlan,
300
675
  collectConflicts,
301
676
  confirmInstall,
677
+ configureInteractiveInstall,
678
+ createPrompter,
679
+ filterManifestEntries,
680
+ getInstallCategory,
302
681
  normalizeManifestEntries,
682
+ parseInteractiveCategorySelection,
683
+ parseCategoryList,
303
684
  parseAgentList,
304
685
  parseInstallArgs,
305
686
  printConflicts,