bmad-method 6.6.1-next.8 → 6.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/{tools/installer/modules/registry-fallback.yaml → bmad-modules.yaml} +29 -15
- package/package.json +4 -4
- package/src/bmm-skills/1-analysis/bmad-product-brief/SKILL.md +18 -11
- package/src/bmm-skills/1-analysis/bmad-product-brief/customize.toml +13 -8
- package/src/bmm-skills/2-plan-workflows/bmad-prd/SKILL.md +54 -57
- package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/headless-schemas.md +2 -2
- package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/prd-template.md +40 -30
- package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/prd-validation-checklist.md +126 -21
- package/src/bmm-skills/2-plan-workflows/bmad-prd/assets/validation-report-template.html +193 -58
- package/src/bmm-skills/2-plan-workflows/bmad-prd/customize.toml +47 -13
- package/src/bmm-skills/2-plan-workflows/bmad-prd/references/headless.md +27 -12
- package/src/bmm-skills/2-plan-workflows/bmad-prd/references/validate.md +97 -0
- package/src/bmm-skills/module.yaml +2 -2
- package/src/core-skills/module.yaml +1 -1
- package/tools/installer/core/installer.js +1 -22
- package/tools/installer/core/manifest.js +0 -22
- package/tools/installer/modules/channel-plan.js +1 -1
- package/tools/installer/modules/external-manager.js +9 -27
- package/tools/installer/modules/official-modules.js +9 -48
- package/tools/installer/prompts.js +149 -0
- package/tools/installer/ui.js +13 -197
- package/src/bmm-skills/2-plan-workflows/bmad-prd/references/facilitation-guide.md +0 -79
- package/src/bmm-skills/2-plan-workflows/bmad-prd/references/validation-render.md +0 -58
- package/src/bmm-skills/2-plan-workflows/bmad-prd/scripts/render-validation-html.py +0 -290
- package/tools/installer/modules/community-manager.js +0 -704
- package/tools/installer/modules/registry-client.js +0 -187
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Validate
|
|
2
|
+
|
|
3
|
+
The Validate intent playbook. Standalone — this intent critiques an existing PRD without changing it and ends after the user has seen the report; it does not run Finalize. The synthesis pipeline below is also reused for mid-session report requests during Create/Update.
|
|
4
|
+
|
|
5
|
+
## Orient
|
|
6
|
+
|
|
7
|
+
Source-extract against `.decision-log.md`, any original inputs, and the PRD/addendum themselves. Delegate to subagents per PRD Discipline → "Extract, don't ingest" (in SKILL.md); the parent assembles from extracts.
|
|
8
|
+
|
|
9
|
+
## Run the Reviewer Gate
|
|
10
|
+
|
|
11
|
+
Run the Reviewer Gate (see SKILL.md) against `prd.md` (and `addendum.md` if present). The rubric walker is the default entry in the gate menu; under Validate intent it additionally runs the synthesis pipeline below. The Finalize discipline pass during Create/Update does NOT render a report — findings stay in-conversation.
|
|
12
|
+
|
|
13
|
+
## Rubric-walker pipeline
|
|
14
|
+
|
|
15
|
+
The rubric walker is the primary review entry. Spawn it as a subagent with this prompt:
|
|
16
|
+
|
|
17
|
+
> You are validating a PRD against the quality rubric at `{workflow.validation_checklist_template}`. Read the full rubric first, then read `prd.md` (and `addendum.md` if present). Form a judgment per dimension — *strong / adequate / thin / broken* — and write findings only where they add information. Cite specific PRD locations and quote phrases. Severity ranks impact on the PRD's usefulness, not how easy the fix is. Write your review to `{doc_workspace}/review-rubric.md` in the format the rubric specifies. Return ONLY a compact summary (overall verdict, dimension verdicts, finding counts by severity, file path).
|
|
18
|
+
|
|
19
|
+
The Reviewer Gate may also dispatch additional reviewers from `{workflow.finalize_reviewers}` (adversarial-general by default) and any ad-hoc reviewers the parent judges warranted. Each writes its review to `{doc_workspace}/review-{slug}.md` and returns a compact summary. Run in parallel.
|
|
20
|
+
|
|
21
|
+
## Synthesis pipeline
|
|
22
|
+
|
|
23
|
+
Once every selected reviewer has returned, the parent synthesizes one consolidated report. **Do not skip this step under Validate intent** — it produces the persistent artifact the user opens.
|
|
24
|
+
|
|
25
|
+
### Inputs
|
|
26
|
+
|
|
27
|
+
- `{doc_workspace}/review-rubric.md` — primary, structured by the seven dimensions
|
|
28
|
+
- Zero or more `{doc_workspace}/review-{slug}.md` files — extra reviewers (adversarial, etc.)
|
|
29
|
+
- `{workflow.validation_report_template}` — the HTML skeleton
|
|
30
|
+
|
|
31
|
+
### What the synthesis pass does
|
|
32
|
+
|
|
33
|
+
1. Read every reviewer file in `{doc_workspace}/review-*.md`.
|
|
34
|
+
2. Fill the HTML skeleton:
|
|
35
|
+
- **Header.** PRD name, path. Grade derived from the rubric verdicts and severity counts: *Excellent* = all dimensions strong/adequate, no high/critical findings · *Good* = ≤1 thin dimension, no critical findings · *Fair* = multiple thin dimensions or any high finding · *Poor* = any broken dimension or any critical finding. Set the matching `grade-excellent | grade-good | grade-fair | grade-poor` class.
|
|
36
|
+
- **Synthesis block.** Lift the rubric's *Overall verdict* paragraph as the lead; if adversarial or ad-hoc reviewers materially shift the picture, add a second paragraph that names what they surfaced.
|
|
37
|
+
- **Dimension summary cards.** One per dimension that was assessed. Colored verdict text. Skip dimensions the rubric marked n/a for this PRD (e.g. downstream usability for a standalone PRD).
|
|
38
|
+
- **Dimension sections.** One `<section class="dimension">` per assessed dimension, in rubric order. `<details open>` for *thin* and *broken*; closed for *strong* and *adequate*. Each contains the dimension judgment (the prose from review-rubric.md) and the findings list.
|
|
39
|
+
- **Reviewer sections.** One `<section class="reviewer-section">` per extra reviewer that ran. The source file path goes in the `<span class="reviewer-source">`. Closed by default. Adversarial findings keep their adversarial voice — do not soften.
|
|
40
|
+
- **Mechanical notes.** Bullet list from the rubric's "Mechanical notes" section. Skip the block if empty.
|
|
41
|
+
- **Footer.** Rubric path, ISO timestamp.
|
|
42
|
+
3. Write the filled HTML to `{doc_workspace}/validation-report.html`.
|
|
43
|
+
4. Write the markdown twin to `{doc_workspace}/validation-report.md` (same content, grouped by severity rather than by dimension — see format below; this is the canonical form for downstream re-reading).
|
|
44
|
+
5. Open the HTML in the default browser:
|
|
45
|
+
```bash
|
|
46
|
+
python3 -c "import webbrowser, pathlib; webbrowser.open(pathlib.Path('{doc_workspace}/validation-report.html').resolve().as_uri())"
|
|
47
|
+
```
|
|
48
|
+
Skip the open step in headless mode (see `references/headless.md`).
|
|
49
|
+
|
|
50
|
+
### Markdown twin format
|
|
51
|
+
|
|
52
|
+
```markdown
|
|
53
|
+
# Validation Report — {prd_name}
|
|
54
|
+
|
|
55
|
+
- **PRD:** `{prd_path}`
|
|
56
|
+
- **Rubric:** `{rubric_path}`
|
|
57
|
+
- **Run at:** {ISO timestamp}
|
|
58
|
+
- **Grade:** {Excellent | Good | Fair | Poor}
|
|
59
|
+
|
|
60
|
+
## Overall verdict
|
|
61
|
+
{synthesis paragraphs}
|
|
62
|
+
|
|
63
|
+
## Dimension verdicts
|
|
64
|
+
- Decision-readiness — {verdict}
|
|
65
|
+
- Substance over theater — {verdict}
|
|
66
|
+
- (etc. for each assessed dimension)
|
|
67
|
+
|
|
68
|
+
## Findings by severity
|
|
69
|
+
|
|
70
|
+
### Critical (n)
|
|
71
|
+
**[Dimension or Reviewer]** — Title (§ location)
|
|
72
|
+
{Note}
|
|
73
|
+
Fix: {suggested fix}
|
|
74
|
+
|
|
75
|
+
### High (n)
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
### Medium (n)
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
### Low (n)
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
## Mechanical notes
|
|
85
|
+
- {bullet}
|
|
86
|
+
|
|
87
|
+
## Reviewer files
|
|
88
|
+
- `review-rubric.md`
|
|
89
|
+
- `review-adversarial-general.md` (if present)
|
|
90
|
+
- (etc.)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Re-running validation overwrites the consolidated report in place. The individual `review-*.md` files are preserved so the user can drill in.
|
|
94
|
+
|
|
95
|
+
## Close
|
|
96
|
+
|
|
97
|
+
Surface artifact paths; the rendered HTML/markdown is the persistent artifact. Always offer to roll findings into an Update.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
code: bmm
|
|
2
|
-
name: "BMad Method
|
|
3
|
-
description: "
|
|
2
|
+
name: "BMad Method"
|
|
3
|
+
description: "Full-lifecycle AI agile development: analysis, planning, architecture, implementation"
|
|
4
4
|
default_selected: true # This module will be selected by default for new installations
|
|
5
5
|
|
|
6
6
|
# Variables from Core Config inserted:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
code: core
|
|
2
2
|
name: "BMad Core Module"
|
|
3
|
-
description: "
|
|
3
|
+
description: "Shared utilities across modules"
|
|
4
4
|
|
|
5
5
|
header: "BMad Core Configuration"
|
|
6
6
|
subheader: "Configure the core settings for your BMad installation.\nThese settings will be used across all installed bmad skills, workflows, and agents."
|
|
@@ -640,13 +640,7 @@ class Installer {
|
|
|
640
640
|
const moduleInfo = sourcePath ? await officialModules.getModuleInfo(sourcePath, moduleName, '') : null;
|
|
641
641
|
const displayName = moduleInfo?.name || moduleName;
|
|
642
642
|
|
|
643
|
-
const
|
|
644
|
-
let communityResolution = null;
|
|
645
|
-
if (!externalResolution) {
|
|
646
|
-
const { CommunityModuleManager } = require('../modules/community-manager');
|
|
647
|
-
communityResolution = new CommunityModuleManager().getResolution(moduleName);
|
|
648
|
-
}
|
|
649
|
-
const resolution = externalResolution || communityResolution;
|
|
643
|
+
const resolution = officialModules.externalModuleManager.getResolution(moduleName);
|
|
650
644
|
const cachedResolution = CustomModuleManager._resolutionCache.get(moduleName);
|
|
651
645
|
const versionInfo = await resolveModuleVersion(moduleName, {
|
|
652
646
|
moduleSourcePath: sourcePath,
|
|
@@ -1178,21 +1172,6 @@ class Installer {
|
|
|
1178
1172
|
}
|
|
1179
1173
|
}
|
|
1180
1174
|
|
|
1181
|
-
// Add installed community modules to available modules
|
|
1182
|
-
const { CommunityModuleManager } = require('../modules/community-manager');
|
|
1183
|
-
const communityMgr = new CommunityModuleManager();
|
|
1184
|
-
const communityModules = await communityMgr.listAll();
|
|
1185
|
-
for (const communityModule of communityModules) {
|
|
1186
|
-
if (installedModules.includes(communityModule.code) && !availableModules.some((m) => m.id === communityModule.code)) {
|
|
1187
|
-
availableModules.push({
|
|
1188
|
-
id: communityModule.code,
|
|
1189
|
-
name: communityModule.displayName,
|
|
1190
|
-
isExternal: true,
|
|
1191
|
-
fromCommunity: true,
|
|
1192
|
-
});
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
1175
|
// Add installed custom modules to available modules
|
|
1197
1176
|
const { CustomModuleManager } = require('../modules/custom-module-manager');
|
|
1198
1177
|
const customMgr = new CustomModuleManager();
|
|
@@ -310,28 +310,6 @@ class Manifest {
|
|
|
310
310
|
};
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
-
// Check if this is a community module
|
|
314
|
-
const { CommunityModuleManager } = require('../modules/community-manager');
|
|
315
|
-
const communityMgr = new CommunityModuleManager();
|
|
316
|
-
const communityInfo = await communityMgr.getModuleByCode(moduleName);
|
|
317
|
-
if (communityInfo) {
|
|
318
|
-
const communityResolution = communityMgr.getResolution(moduleName);
|
|
319
|
-
const versionInfo = await resolveModuleVersion(moduleName, {
|
|
320
|
-
moduleSourcePath,
|
|
321
|
-
fallbackVersion: communityInfo.version,
|
|
322
|
-
});
|
|
323
|
-
return {
|
|
324
|
-
version: communityResolution?.version || versionInfo.version || communityInfo.version,
|
|
325
|
-
source: 'community',
|
|
326
|
-
npmPackage: communityInfo.npmPackage || null,
|
|
327
|
-
repoUrl: communityInfo.url || null,
|
|
328
|
-
channel: communityResolution?.channel || null,
|
|
329
|
-
sha: communityResolution?.sha || null,
|
|
330
|
-
registryApprovedTag: communityResolution?.registryApprovedTag || null,
|
|
331
|
-
registryApprovedSha: communityResolution?.registryApprovedSha || null,
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
|
|
335
313
|
// Check if this is a custom module (from user-provided URL or local path)
|
|
336
314
|
const { CustomModuleManager } = require('../modules/custom-module-manager');
|
|
337
315
|
const customMgr = new CustomModuleManager();
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* We build the plan from:
|
|
8
8
|
* 1. CLI flags (--channel / --all-* / --next=CODE / --pin CODE=TAG)
|
|
9
9
|
* 2. Interactive answers (the "all stable?" gate + per-module picker)
|
|
10
|
-
* 3. Registry defaults (default_channel from
|
|
10
|
+
* 3. Registry defaults (default_channel from bmad-modules.yaml)
|
|
11
11
|
* 4. Hardcoded fallback 'stable'
|
|
12
12
|
*
|
|
13
13
|
* Precedence: --pin > --next=CODE > --channel (global) > registry default > 'stable'.
|
|
@@ -4,9 +4,9 @@ const path = require('node:path');
|
|
|
4
4
|
const { execSync } = require('node:child_process');
|
|
5
5
|
const yaml = require('yaml');
|
|
6
6
|
const prompts = require('../prompts');
|
|
7
|
-
const { RegistryClient } = require('./registry-client');
|
|
8
7
|
const { resolveChannel, tagExists, parseGitHubRepo } = require('./channel-resolver');
|
|
9
8
|
const { decideChannelForModule } = require('./channel-plan');
|
|
9
|
+
const { getProjectRoot } = require('../project-root');
|
|
10
10
|
|
|
11
11
|
const VALID_CHANNELS = new Set(['stable', 'next', 'pinned']);
|
|
12
12
|
|
|
@@ -46,15 +46,12 @@ async function writeChannelMarker(markerPath, data) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const
|
|
50
|
-
const MARKETPLACE_REPO = 'bmad-plugins-marketplace';
|
|
51
|
-
const MARKETPLACE_REF = 'main';
|
|
52
|
-
const FALLBACK_CONFIG_PATH = path.join(__dirname, 'registry-fallback.yaml');
|
|
49
|
+
const REGISTRY_CONFIG_PATH = path.join(getProjectRoot(), 'bmad-modules.yaml');
|
|
53
50
|
|
|
54
51
|
/**
|
|
55
|
-
* Manages official modules from the
|
|
56
|
-
*
|
|
57
|
-
*
|
|
52
|
+
* Manages official modules from the bundled registry file. The remote
|
|
53
|
+
* marketplace fetch has been retired; this repo is the single source of truth
|
|
54
|
+
* for which official modules exist and how they are displayed.
|
|
58
55
|
*
|
|
59
56
|
* @class ExternalModuleManager
|
|
60
57
|
*/
|
|
@@ -65,9 +62,7 @@ class ExternalModuleManager {
|
|
|
65
62
|
// ExternalModuleManager) sees resolutions made during install.
|
|
66
63
|
static _resolutions = new Map();
|
|
67
64
|
|
|
68
|
-
constructor() {
|
|
69
|
-
this._client = new RegistryClient();
|
|
70
|
-
}
|
|
65
|
+
constructor() {}
|
|
71
66
|
|
|
72
67
|
/**
|
|
73
68
|
* Get the most recent channel resolution for a module (if any).
|
|
@@ -79,8 +74,7 @@ class ExternalModuleManager {
|
|
|
79
74
|
}
|
|
80
75
|
|
|
81
76
|
/**
|
|
82
|
-
* Load the official modules registry from
|
|
83
|
-
* bundled YAML file if the fetch fails.
|
|
77
|
+
* Load the official modules registry from the bundled YAML file.
|
|
84
78
|
* @returns {Object} Parsed YAML content with modules array
|
|
85
79
|
*/
|
|
86
80
|
async loadExternalModulesConfig() {
|
|
@@ -88,23 +82,10 @@ class ExternalModuleManager {
|
|
|
88
82
|
return this.cachedModules;
|
|
89
83
|
}
|
|
90
84
|
|
|
91
|
-
// Try remote registry first
|
|
92
|
-
try {
|
|
93
|
-
const config = await this._client.fetchGitHubYaml(MARKETPLACE_OWNER, MARKETPLACE_REPO, 'registry/official.yaml', MARKETPLACE_REF);
|
|
94
|
-
if (config?.modules?.length) {
|
|
95
|
-
this.cachedModules = config;
|
|
96
|
-
return config;
|
|
97
|
-
}
|
|
98
|
-
} catch {
|
|
99
|
-
// Fall through to local fallback
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Fallback to bundled file
|
|
103
85
|
try {
|
|
104
|
-
const content = await fs.readFile(
|
|
86
|
+
const content = await fs.readFile(REGISTRY_CONFIG_PATH, 'utf8');
|
|
105
87
|
const config = yaml.parse(content);
|
|
106
88
|
this.cachedModules = config;
|
|
107
|
-
await prompts.log.warn('Could not reach BMad registry; using bundled module list.');
|
|
108
89
|
return config;
|
|
109
90
|
} catch (error) {
|
|
110
91
|
await prompts.log.warn(`Failed to load modules config: ${error.message}`);
|
|
@@ -130,6 +111,7 @@ class ExternalModuleManager {
|
|
|
130
111
|
defaultSelected: mod.default_selected === true || mod.defaultSelected === true,
|
|
131
112
|
type: mod.type || 'bmad-org',
|
|
132
113
|
npmPackage: mod.npm_package || mod.npmPackage || null,
|
|
114
|
+
pluginName: mod.plugin_name || mod.pluginName || null,
|
|
133
115
|
defaultChannel: normalizeChannelName(mod.default_channel || mod.defaultChannel) || 'stable',
|
|
134
116
|
builtIn: mod.built_in === true,
|
|
135
117
|
isExternal: mod.built_in !== true,
|
|
@@ -231,14 +231,6 @@ class OfficialModules {
|
|
|
231
231
|
return externalSource;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
// Check community modules (pass channelOptions for --next/--pin overrides)
|
|
235
|
-
const { CommunityModuleManager } = require('./community-manager');
|
|
236
|
-
const communityMgr = new CommunityModuleManager();
|
|
237
|
-
const communitySource = await communityMgr.findModuleSource(moduleCode, options);
|
|
238
|
-
if (communitySource) {
|
|
239
|
-
return communitySource;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
234
|
// Check custom modules (from user-provided URLs, already cloned to cache)
|
|
243
235
|
const { CustomModuleManager } = require('./custom-module-manager');
|
|
244
236
|
const customMgr = new CustomModuleManager();
|
|
@@ -269,21 +261,6 @@ class OfficialModules {
|
|
|
269
261
|
return this.installFromResolution(resolved, bmadDir, fileTrackingCallback, options);
|
|
270
262
|
}
|
|
271
263
|
|
|
272
|
-
// Community modules whose cloned repo ships marketplace.json get the same
|
|
273
|
-
// skill-level install treatment as custom-source installs. If the in-process
|
|
274
|
-
// cache wasn't populated (e.g. caller skipped the pre-clone phase), fall
|
|
275
|
-
// back to resolving directly from `~/.bmad/cache/community-modules/<name>/`
|
|
276
|
-
// so we don't silently regress to the legacy half-install path.
|
|
277
|
-
const { CommunityModuleManager } = require('./community-manager');
|
|
278
|
-
const communityMgr = new CommunityModuleManager();
|
|
279
|
-
let communityResolved = communityMgr.getPluginResolution(moduleName);
|
|
280
|
-
if (!communityResolved) {
|
|
281
|
-
communityResolved = await communityMgr.resolveFromCache(moduleName);
|
|
282
|
-
}
|
|
283
|
-
if (communityResolved) {
|
|
284
|
-
return this.installFromResolution(communityResolved, bmadDir, fileTrackingCallback, options);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
264
|
const sourcePath = await this.findModuleSource(moduleName, {
|
|
288
265
|
silent: options.silent,
|
|
289
266
|
channelOptions: options.channelOptions,
|
|
@@ -310,14 +287,9 @@ class OfficialModules {
|
|
|
310
287
|
const manifestObj = new Manifest();
|
|
311
288
|
const versionInfo = await manifestObj.getModuleVersionInfo(moduleName, bmadDir, sourcePath);
|
|
312
289
|
|
|
313
|
-
// Pick up channel resolution recorded by
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (!externalResolution) {
|
|
317
|
-
const { CommunityModuleManager } = require('./community-manager');
|
|
318
|
-
communityResolution = new CommunityModuleManager().getResolution(moduleName);
|
|
319
|
-
}
|
|
320
|
-
const resolution = externalResolution || communityResolution;
|
|
290
|
+
// Pick up channel resolution recorded by the external manager (the only
|
|
291
|
+
// manager that does pre-clone resolution now that community is retired).
|
|
292
|
+
const resolution = this.externalModuleManager.getResolution(moduleName);
|
|
321
293
|
|
|
322
294
|
await manifestObj.addModule(bmadDir, moduleName, {
|
|
323
295
|
version: resolution?.version || versionInfo.version,
|
|
@@ -326,8 +298,6 @@ class OfficialModules {
|
|
|
326
298
|
repoUrl: versionInfo.repoUrl,
|
|
327
299
|
channel: resolution?.channel,
|
|
328
300
|
sha: resolution?.sha,
|
|
329
|
-
registryApprovedTag: communityResolution?.registryApprovedTag,
|
|
330
|
-
registryApprovedSha: communityResolution?.registryApprovedSha,
|
|
331
301
|
});
|
|
332
302
|
|
|
333
303
|
return { success: true, module: moduleName, path: targetPath, versionInfo };
|
|
@@ -375,27 +345,19 @@ class OfficialModules {
|
|
|
375
345
|
await this.createModuleDirectories(resolved.code, bmadDir, options);
|
|
376
346
|
}
|
|
377
347
|
|
|
378
|
-
// Update manifest. For
|
|
379
|
-
// CommunityModuleManager (stable/next/pinned) and propagate the registry's
|
|
380
|
-
// approved tag/sha. For custom-source installs we derive channel from the
|
|
348
|
+
// Update manifest. For custom-source installs we derive channel from the
|
|
381
349
|
// cloneRef (present → pinned, absent → next; local paths have no channel).
|
|
382
350
|
const { Manifest } = require('../core/manifest');
|
|
383
351
|
const manifestObj = new Manifest();
|
|
384
352
|
|
|
385
353
|
const hasGitClone = !!resolved.repoUrl;
|
|
386
|
-
const isCommunity = resolved.communitySource === true;
|
|
387
354
|
const manifestEntry = {
|
|
388
|
-
version: resolved.
|
|
389
|
-
source:
|
|
355
|
+
version: resolved.cloneRef || (hasGitClone ? 'main' : resolved.version || null),
|
|
356
|
+
source: 'custom',
|
|
390
357
|
npmPackage: null,
|
|
391
358
|
repoUrl: resolved.repoUrl || null,
|
|
392
359
|
};
|
|
393
|
-
if (
|
|
394
|
-
if (resolved.communityChannel) manifestEntry.channel = resolved.communityChannel;
|
|
395
|
-
if (resolved.cloneSha) manifestEntry.sha = resolved.cloneSha;
|
|
396
|
-
if (resolved.registryApprovedTag) manifestEntry.registryApprovedTag = resolved.registryApprovedTag;
|
|
397
|
-
if (resolved.registryApprovedSha) manifestEntry.registryApprovedSha = resolved.registryApprovedSha;
|
|
398
|
-
} else if (hasGitClone) {
|
|
360
|
+
if (hasGitClone) {
|
|
399
361
|
manifestEntry.channel = resolved.cloneRef ? 'pinned' : 'next';
|
|
400
362
|
if (resolved.cloneSha) manifestEntry.sha = resolved.cloneSha;
|
|
401
363
|
if (resolved.rawInput) manifestEntry.rawSource = resolved.rawInput;
|
|
@@ -408,11 +370,10 @@ class OfficialModules {
|
|
|
408
370
|
module: resolved.code,
|
|
409
371
|
path: targetPath,
|
|
410
372
|
// Mirror the manifestEntry.version precedence above so downstream summary
|
|
411
|
-
// lines show the same string we just wrote to disk (
|
|
412
|
-
// use the registry-approved tag via `communityVersion`; custom git-backed
|
|
373
|
+
// lines show the same string we just wrote to disk (custom git-backed
|
|
413
374
|
// installs show the cloned ref or 'main').
|
|
414
375
|
versionInfo: {
|
|
415
|
-
version: resolved.
|
|
376
|
+
version: resolved.cloneRef || (hasGitClone ? 'main' : resolved.version || ''),
|
|
416
377
|
},
|
|
417
378
|
};
|
|
418
379
|
}
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
let _clack = null;
|
|
11
11
|
let _clackCore = null;
|
|
12
12
|
let _picocolors = null;
|
|
13
|
+
const fs = require('node:fs');
|
|
14
|
+
const os = require('node:os');
|
|
15
|
+
const path = require('node:path');
|
|
13
16
|
|
|
14
17
|
/**
|
|
15
18
|
* Lazy-load @clack/prompts (ESM module)
|
|
@@ -575,6 +578,151 @@ async function autocomplete(options) {
|
|
|
575
578
|
return result;
|
|
576
579
|
}
|
|
577
580
|
|
|
581
|
+
function hasPathSeparator(value) {
|
|
582
|
+
return value.endsWith('/') || value.endsWith('\\');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function expandHome(input) {
|
|
586
|
+
if (!input) return input;
|
|
587
|
+
if (input === '~') return os.homedir();
|
|
588
|
+
if (input.startsWith('~/') || input.startsWith('~\\')) {
|
|
589
|
+
return path.join(os.homedir(), input.slice(2));
|
|
590
|
+
}
|
|
591
|
+
return input;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function toDirectoryOption(value, label = value, synthetic = false) {
|
|
595
|
+
return { value, label, synthetic };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function isExistingDirectory(value) {
|
|
599
|
+
try {
|
|
600
|
+
return fs.existsSync(value) && fs.statSync(value).isDirectory();
|
|
601
|
+
} catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function listDirectoryOptions(input, options) {
|
|
607
|
+
const cwd = options.cwd || process.cwd();
|
|
608
|
+
const rawInput = input.trim();
|
|
609
|
+
const expandedInput = expandHome(rawInput);
|
|
610
|
+
const trailingSep = hasPathSeparator(rawInput) || hasPathSeparator(expandedInput);
|
|
611
|
+
const resolvedInput = expandedInput ? path.resolve(cwd, expandedInput) : cwd;
|
|
612
|
+
const browseDir = expandedInput && !trailingSep && !isExistingDirectory(resolvedInput) ? path.dirname(resolvedInput) : resolvedInput;
|
|
613
|
+
const prefix = expandedInput && browseDir !== resolvedInput ? path.basename(resolvedInput).toLowerCase() : '';
|
|
614
|
+
const results = [];
|
|
615
|
+
|
|
616
|
+
if (!trailingSep && isExistingDirectory(resolvedInput)) {
|
|
617
|
+
results.push(toDirectoryOption(resolvedInput, `. (use this directory)`));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
if (isExistingDirectory(browseDir)) {
|
|
621
|
+
try {
|
|
622
|
+
for (const entry of fs.readdirSync(browseDir, { withFileTypes: true })) {
|
|
623
|
+
if (!entry.isDirectory()) continue;
|
|
624
|
+
if (prefix && !entry.name.toLowerCase().startsWith(prefix)) continue;
|
|
625
|
+
const fullPath = path.join(browseDir, entry.name);
|
|
626
|
+
if (!results.some((option) => option.value === fullPath)) {
|
|
627
|
+
results.push(toDirectoryOption(fullPath));
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
} catch {
|
|
631
|
+
// Skip unreadable directories; validation still reports path issues.
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const validation = options.validate?.(rawInput);
|
|
636
|
+
const hasMatchingOption = results.some((option) => option.value === resolvedInput);
|
|
637
|
+
if (expandedInput && !validation && !hasMatchingOption) {
|
|
638
|
+
results.unshift(toDirectoryOption(resolvedInput, `Create/use: ${resolvedInput}`, true));
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return results;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Directory prompt with autocomplete candidates and create-directory support.
|
|
646
|
+
* Uses @clack/core directly so typed paths that do not exist yet can still be
|
|
647
|
+
* submitted when validation allows creating them.
|
|
648
|
+
* @param {Object} options - Prompt options
|
|
649
|
+
* @param {string} options.message - Prompt message
|
|
650
|
+
* @param {string} [options.default] - Default directory
|
|
651
|
+
* @param {string} [options.placeholder] - Placeholder text
|
|
652
|
+
* @param {Function} [options.validate] - Sync validation function
|
|
653
|
+
* @returns {Promise<string>} Selected or typed directory path
|
|
654
|
+
*/
|
|
655
|
+
async function directory(options) {
|
|
656
|
+
const core = await getClackCore();
|
|
657
|
+
const color = await getPicocolors();
|
|
658
|
+
const tabCompletion = {
|
|
659
|
+
prefix: '',
|
|
660
|
+
index: -1,
|
|
661
|
+
options: [],
|
|
662
|
+
lastValue: '',
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
let prompt;
|
|
666
|
+
prompt = new core.AutocompletePrompt({
|
|
667
|
+
initialValue: options.default,
|
|
668
|
+
options: () => listDirectoryOptions(prompt?.userInput || '', options),
|
|
669
|
+
filter: () => true,
|
|
670
|
+
validate: (value) => options.validate?.(value ?? prompt.userInput),
|
|
671
|
+
render() {
|
|
672
|
+
const title = `${color.gray('◆')} ${options.message}`;
|
|
673
|
+
const bar = color.gray('│');
|
|
674
|
+
const barEnd = color.gray('└');
|
|
675
|
+
const userInput = this.userInput;
|
|
676
|
+
const placeholder = options.placeholder || options.default;
|
|
677
|
+
const inputDisplay = userInput ? this.userInputWithCursor : `${color.inverse(color.hidden('_'))}${color.dim(placeholder || '')}`;
|
|
678
|
+
const errorLine = this.state === 'error' ? [`${color.yellow('│')} ${color.yellow(this.error)}`] : [];
|
|
679
|
+
|
|
680
|
+
switch (this.state) {
|
|
681
|
+
case 'submit': {
|
|
682
|
+
return `${color.gray('◇')} ${options.message}\n${bar} ${color.dim(this.value || '')}`;
|
|
683
|
+
}
|
|
684
|
+
case 'cancel': {
|
|
685
|
+
return `${color.gray('◇')} ${options.message}\n${bar} ${color.strikethrough(color.dim(userInput || ''))}`;
|
|
686
|
+
}
|
|
687
|
+
default: {
|
|
688
|
+
return [title, `${bar} ${inputDisplay}`, ...errorLine, barEnd].join('\n');
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
const hasSetUserInput = typeof prompt._setUserInput === 'function';
|
|
695
|
+
const hasClearUserInput = typeof prompt._clearUserInput === 'function';
|
|
696
|
+
|
|
697
|
+
prompt.on('key', (_, key) => {
|
|
698
|
+
if (key?.name !== 'tab') return;
|
|
699
|
+
if (!hasSetUserInput) return; // @clack/core API surface changed — skip Tab silently.
|
|
700
|
+
const currentInput = prompt.userInput;
|
|
701
|
+
const isContinuingCycle = tabCompletion.lastValue && currentInput === tabCompletion.lastValue;
|
|
702
|
+
const completionOptions = isContinuingCycle ? tabCompletion.options : prompt.filteredOptions.filter((option) => !option.synthetic);
|
|
703
|
+
if (completionOptions.length === 0) return;
|
|
704
|
+
|
|
705
|
+
if (isContinuingCycle) {
|
|
706
|
+
tabCompletion.index = (tabCompletion.index + 1) % completionOptions.length;
|
|
707
|
+
} else {
|
|
708
|
+
tabCompletion.prefix = currentInput;
|
|
709
|
+
tabCompletion.options = completionOptions;
|
|
710
|
+
tabCompletion.index = 0;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const focusedOption = completionOptions[tabCompletion.index];
|
|
714
|
+
if (!focusedOption) return;
|
|
715
|
+
const completedValue = focusedOption.value;
|
|
716
|
+
tabCompletion.lastValue = completedValue;
|
|
717
|
+
if (hasClearUserInput) prompt._clearUserInput();
|
|
718
|
+
prompt._setUserInput(completedValue, true);
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
const result = await prompt.prompt();
|
|
722
|
+
await handleCancel(result);
|
|
723
|
+
return result;
|
|
724
|
+
}
|
|
725
|
+
|
|
578
726
|
/**
|
|
579
727
|
* Get the color utility (picocolors instance from @clack/prompts)
|
|
580
728
|
* @returns {Promise<Object>} The color utility (picocolors)
|
|
@@ -694,6 +842,7 @@ module.exports = {
|
|
|
694
842
|
multiselect,
|
|
695
843
|
autocompleteMultiselect,
|
|
696
844
|
autocomplete,
|
|
845
|
+
directory,
|
|
697
846
|
confirm,
|
|
698
847
|
text,
|
|
699
848
|
password,
|