@bvdm/delano 0.2.1 → 0.2.3
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 +52 -6
- package/assets/payload/.agents/skills/prototype-skill/SKILL.md +1 -1
- package/package.json +2 -2
- package/src/cli/commands/install.js +21 -1
- package/src/cli/lib/install.js +389 -8
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.3`
|
|
18
19
|
- Binary: `delano`
|
|
19
20
|
- Commands: `onboarding`, `install`, `viewer`, `init`, `validate`, `status`, `next`
|
|
20
|
-
- Primary
|
|
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
|
|
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
|
-
##
|
|
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
|
|
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.
|
|
257
|
+
After trusted publishing is configured, publish by pushing a matching version tag such as `v0.2.3`, 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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: prototype-skill
|
|
3
|
-
description: Run a time-boxed Prototype Probe to retire material uncertainty before spec approval. Use when `spec.md` is still draft, `probe_required
|
|
3
|
+
description: Run a time-boxed Prototype Probe to retire material uncertainty before spec approval. Use when `spec.md` is still draft, `probe_required` is true, or a narrow experiment is needed to bound technical or delivery risk before planning.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# prototype-skill
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/src/cli/lib/install.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
595
|
+
const prompt = createPrompter(stdin, stdout);
|
|
224
596
|
try {
|
|
225
|
-
const
|
|
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
|
|
600
|
+
const answer = await prompt.ask(promptText);
|
|
229
601
|
return /^[Yy](es)?$/.test(answer.trim());
|
|
230
602
|
} finally {
|
|
231
|
-
|
|
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,
|