@bvdm/delano 0.2.0 → 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.
Files changed (39) hide show
  1. package/.delano/viewer/public/explorer.svg +2 -2
  2. package/.delano/viewer/public/index.html +21 -21
  3. package/.delano/viewer/public/markdown.svg +5 -5
  4. package/.delano/viewer/public/styles.css +1042 -1042
  5. package/.delano/viewer/public/vscode.svg +23 -23
  6. package/LICENSE +21 -0
  7. package/README.md +68 -5
  8. package/assets/payload/.agents/hooks/bash-worktree-fix.sh +0 -0
  9. package/assets/payload/.agents/hooks/post-tool-logger.js +0 -0
  10. package/assets/payload/.agents/hooks/session-tracker.js +0 -0
  11. package/assets/payload/.agents/hooks/user-prompt-logger.js +0 -0
  12. package/assets/payload/.agents/scripts/check-log-safety.sh +0 -0
  13. package/assets/payload/.agents/scripts/check-path-standards.sh +0 -0
  14. package/assets/payload/.agents/scripts/fix-path-standards.sh +0 -0
  15. package/assets/payload/.agents/scripts/git-sparse-download.sh +0 -0
  16. package/assets/payload/.agents/scripts/log-event.js +0 -0
  17. package/assets/payload/.agents/scripts/log-event.sh +0 -0
  18. package/assets/payload/.agents/scripts/pm/blocked.sh +0 -0
  19. package/assets/payload/.agents/scripts/pm/epic-list.sh +0 -0
  20. package/assets/payload/.agents/scripts/pm/in-progress.sh +0 -0
  21. package/assets/payload/.agents/scripts/pm/init.sh +0 -0
  22. package/assets/payload/.agents/scripts/pm/next.sh +0 -0
  23. package/assets/payload/.agents/scripts/pm/prd-list.sh +0 -0
  24. package/assets/payload/.agents/scripts/pm/search.sh +0 -0
  25. package/assets/payload/.agents/scripts/pm/standup.sh +0 -0
  26. package/assets/payload/.agents/scripts/pm/status.sh +0 -0
  27. package/assets/payload/.agents/scripts/pm/validate.sh +0 -0
  28. package/assets/payload/.agents/scripts/query-log.sh +0 -0
  29. package/assets/payload/.agents/scripts/test-and-log.sh +0 -0
  30. package/assets/payload/.delano/viewer/public/explorer.svg +2 -2
  31. package/assets/payload/.delano/viewer/public/index.html +21 -21
  32. package/assets/payload/.delano/viewer/public/markdown.svg +5 -5
  33. package/assets/payload/.delano/viewer/public/styles.css +1042 -1042
  34. package/assets/payload/.delano/viewer/public/vscode.svg +23 -23
  35. package/assets/payload/install-delano.sh +0 -0
  36. package/install-delano.sh +0 -0
  37. package/package.json +10 -2
  38. package/src/cli/commands/install.js +21 -1
  39. package/src/cli/lib/install.js +389 -8
@@ -1,24 +1,24 @@
1
- <?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
2
- <svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
3
- <path d="M21.0016 3.11679C21.0016 2.23783 20.0175 2.23782 19.5801 2.34769C20.1924 1.86426 20.9105 1.98147 21.1656 2.12796L27.079 5.02747C27.6424 5.30375 27.9998 5.8786 27.9998 6.50857V25.5831C27.9998 26.2215 27.6329 26.8025 27.058 27.0743L21.4937 29.7054C21.1109 29.8701 20.2799 30.2767 19.5801 29.7053C20.4549 29.8702 20.9287 29.2476 21.0016 28.8264V3.11679Z" fill="url(#paint0_linear_87_8101)"/>
4
- <path d="M19.6512 2.3319C20.1154 2.24017 21.0018 2.28271 21.0018 3.11685V9.68254L3.07359 23.2453C2.76022 23.4824 2.3192 23.443 2.05229 23.1542L0.204532 21.1548C-0.0849358 20.8416 -0.0646824 20.3513 0.249624 20.0633L19.5802 2.34775L19.6512 2.3319Z" fill="url(#paint1_linear_87_8101)"/>
5
- <path d="M21.0018 22.3708L3.07359 8.80801C2.76022 8.57094 2.3192 8.61028 2.05229 8.8991L0.204532 10.8985C-0.0849358 11.2117 -0.0646824 11.702 0.249624 11.9901L19.5802 29.7056C20.455 29.8704 20.9289 29.2478 21.0018 28.8266V22.3708Z" fill="url(#paint2_linear_87_8101)"/>
6
- <defs>
7
- <linearGradient id="paint0_linear_87_8101" x1="23.79" y1="2" x2="23.79" y2="30" gradientUnits="userSpaceOnUse">
8
- <stop stop-color="#32B5F1"/>
9
- <stop offset="1" stop-color="#2B9FED"/>
10
- </linearGradient>
11
- <linearGradient id="paint1_linear_87_8101" x1="21.0018" y1="5.53398" x2="1.0217" y2="22.3051" gradientUnits="userSpaceOnUse">
12
- <stop stop-color="#0F6FB3"/>
13
- <stop offset="0.270551" stop-color="#1279B7"/>
14
- <stop offset="0.421376" stop-color="#1176B5"/>
15
- <stop offset="0.618197" stop-color="#0E69AC"/>
16
- <stop offset="0.855344" stop-color="#0F70AF"/>
17
- <stop offset="1" stop-color="#0F6DAD"/>
18
- </linearGradient>
19
- <linearGradient id="paint2_linear_87_8101" x1="1.15522" y1="9.98389" x2="21.0791" y2="26.4808" gradientUnits="userSpaceOnUse">
20
- <stop stop-color="#1791D2"/>
21
- <stop offset="1" stop-color="#1173C5"/>
22
- </linearGradient>
23
- </defs>
1
+ <?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
2
+ <svg width="800px" height="800px" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
3
+ <path d="M21.0016 3.11679C21.0016 2.23783 20.0175 2.23782 19.5801 2.34769C20.1924 1.86426 20.9105 1.98147 21.1656 2.12796L27.079 5.02747C27.6424 5.30375 27.9998 5.8786 27.9998 6.50857V25.5831C27.9998 26.2215 27.6329 26.8025 27.058 27.0743L21.4937 29.7054C21.1109 29.8701 20.2799 30.2767 19.5801 29.7053C20.4549 29.8702 20.9287 29.2476 21.0016 28.8264V3.11679Z" fill="url(#paint0_linear_87_8101)"/>
4
+ <path d="M19.6512 2.3319C20.1154 2.24017 21.0018 2.28271 21.0018 3.11685V9.68254L3.07359 23.2453C2.76022 23.4824 2.3192 23.443 2.05229 23.1542L0.204532 21.1548C-0.0849358 20.8416 -0.0646824 20.3513 0.249624 20.0633L19.5802 2.34775L19.6512 2.3319Z" fill="url(#paint1_linear_87_8101)"/>
5
+ <path d="M21.0018 22.3708L3.07359 8.80801C2.76022 8.57094 2.3192 8.61028 2.05229 8.8991L0.204532 10.8985C-0.0849358 11.2117 -0.0646824 11.702 0.249624 11.9901L19.5802 29.7056C20.455 29.8704 20.9289 29.2478 21.0018 28.8266V22.3708Z" fill="url(#paint2_linear_87_8101)"/>
6
+ <defs>
7
+ <linearGradient id="paint0_linear_87_8101" x1="23.79" y1="2" x2="23.79" y2="30" gradientUnits="userSpaceOnUse">
8
+ <stop stop-color="#32B5F1"/>
9
+ <stop offset="1" stop-color="#2B9FED"/>
10
+ </linearGradient>
11
+ <linearGradient id="paint1_linear_87_8101" x1="21.0018" y1="5.53398" x2="1.0217" y2="22.3051" gradientUnits="userSpaceOnUse">
12
+ <stop stop-color="#0F6FB3"/>
13
+ <stop offset="0.270551" stop-color="#1279B7"/>
14
+ <stop offset="0.421376" stop-color="#1176B5"/>
15
+ <stop offset="0.618197" stop-color="#0E69AC"/>
16
+ <stop offset="0.855344" stop-color="#0F70AF"/>
17
+ <stop offset="1" stop-color="#0F6DAD"/>
18
+ </linearGradient>
19
+ <linearGradient id="paint2_linear_87_8101" x1="1.15522" y1="9.98389" x2="21.0791" y2="26.4808" gradientUnits="userSpaceOnUse">
20
+ <stop stop-color="#1791D2"/>
21
+ <stop offset="1" stop-color="#1173C5"/>
22
+ </linearGradient>
23
+ </defs>
24
24
  </svg>
File without changes
package/install-delano.sh CHANGED
File without changes
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "@bvdm/delano",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "CLI for the Delano delivery runtime.",
5
- "license": "UNLICENSED",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/MajesteitBart/delano"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/MajesteitBart/delano/issues"
12
+ },
13
+ "homepage": "https://github.com/MajesteitBart/delano#readme",
6
14
  "bin": {
7
15
  "delano": "bin/delano.js"
8
16
  },
@@ -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,