bmad-method 6.2.3-next.24 → 6.2.3-next.26
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/package.json +1 -1
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-01-clarify-and-route.md +4 -5
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-02-plan.md +7 -7
- package/src/bmm-skills/4-implementation/bmad-quick-dev/step-oneshot.md +0 -1
- package/src/bmm-skills/4-implementation/bmad-quick-dev/workflow.md +1 -5
- package/tools/installer/commands/install.js +0 -1
- package/tools/installer/core/existing-install.js +2 -8
- package/tools/installer/core/install-paths.js +0 -3
- package/tools/installer/core/installer.js +5 -396
- package/tools/installer/core/manifest-generator.js +0 -9
- package/tools/installer/core/manifest.js +1 -70
- package/tools/installer/modules/official-modules.js +14 -64
- package/tools/installer/ui.js +1 -613
- package/tools/installer/core/custom-module-cache.js +0 -260
- package/tools/installer/custom-handler.js +0 -112
- package/tools/installer/modules/custom-modules.js +0 -302
package/package.json
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
---
|
|
2
|
-
wipFile: '{implementation_artifacts}/spec-wip.md'
|
|
3
2
|
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
|
4
3
|
spec_file: '' # set at runtime for both routes before leaving this step
|
|
5
4
|
---
|
|
@@ -21,7 +20,7 @@ Before listing artifacts or prompting the user, check whether you already know t
|
|
|
21
20
|
|
|
22
21
|
1. Explicit argument
|
|
23
22
|
Did the user pass a specific file path, spec name, or clear instruction this message?
|
|
24
|
-
- If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: ready-for-dev, in-progress,
|
|
23
|
+
- If it points to a file that matches the spec template (has `status` frontmatter with a recognized value: draft, ready-for-dev, in-progress, in-review, or done) → set `spec_file` and **EARLY EXIT** to the appropriate step (step-02 for draft, step-03 for ready/in-progress, step-04 for review). For `done`, ingest as context and proceed to INSTRUCTIONS — do not resume.
|
|
25
24
|
- Anything else (intent files, external docs, plans, descriptions) → ingest it as starting intent and proceed to INSTRUCTIONS. Do not attempt to infer a workflow state from it.
|
|
26
25
|
|
|
27
26
|
2. Recent conversation
|
|
@@ -29,8 +28,8 @@ Before listing artifacts or prompting the user, check whether you already know t
|
|
|
29
28
|
Use the same routing as above.
|
|
30
29
|
|
|
31
30
|
3. Otherwise — scan artifacts and ask
|
|
32
|
-
- `{
|
|
33
|
-
|
|
31
|
+
- Active specs (`draft`, `ready-for-dev`, `in-progress`, `in-review`) in `{implementation_artifacts}`? → List them and HALT. Ask user which to resume (or `[N]` for new).
|
|
32
|
+
- If `draft` selected: Set `spec_file`. **EARLY EXIT** → `./step-02-plan.md` (resume planning from the draft)
|
|
34
33
|
- If `ready-for-dev` or `in-progress` selected: Set `spec_file`. **EARLY EXIT** → `./step-03-implement.md`
|
|
35
34
|
- If `in-review` selected: Set `spec_file`. **EARLY EXIT** → `./step-04-review.md`
|
|
36
35
|
- Unformatted spec or intent file lacking `status` frontmatter? → Suggest treating its contents as the starting intent. Do NOT attempt to infer a state and resume it.
|
|
@@ -65,7 +64,7 @@ Never ask extra questions if you already understand what the user intends.
|
|
|
65
64
|
- On **K**: Proceed as-is.
|
|
66
65
|
5. Route — choose exactly one:
|
|
67
66
|
|
|
68
|
-
Derive a valid kebab-case slug from the clarified intent. If the intent references a tracking identifier (story number, issue number, ticket ID), lead the slug with it (e.g. `3-2-digest-delivery`, `gh-47-fix-auth`). If `{implementation_artifacts}/spec-{slug}.md` already exists, append `-2`, `-3`, etc. Set `spec_file` = `{implementation_artifacts}/spec-{slug}.md`.
|
|
67
|
+
Derive a valid kebab-case slug from the clarified intent. If the intent references a tracking identifier (story number, issue number, ticket ID), lead the slug with it (e.g. `3-2-digest-delivery`, `gh-47-fix-auth`). If `{implementation_artifacts}/spec-{slug}.md` already exists: if its status is `draft`, treat it as the same work and resume it (set `spec_file` to that path, **EARLY EXIT** → `./step-02-plan.md`); otherwise append `-2`, `-3`, etc. Set `spec_file` = `{implementation_artifacts}/spec-{slug}.md`.
|
|
69
68
|
|
|
70
69
|
**a) One-shot** — zero blast radius: no plausible path by which this change causes unintended consequences elsewhere. Clear intent, no architectural decisions.
|
|
71
70
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
---
|
|
2
|
-
wipFile: '{implementation_artifacts}/spec-wip.md'
|
|
3
2
|
deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
|
4
3
|
---
|
|
5
4
|
|
|
@@ -12,11 +11,12 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
|
|
12
11
|
|
|
13
12
|
## INSTRUCTIONS
|
|
14
13
|
|
|
15
|
-
1.
|
|
16
|
-
2.
|
|
17
|
-
3.
|
|
18
|
-
4.
|
|
19
|
-
5.
|
|
14
|
+
1. Draft resume check. If `{spec_file}` exists with `status: draft`, read it and capture the verbatim `<frozen-after-approval>...</frozen-after-approval>` block as `preserved_intent`. Otherwise `preserved_intent` is empty.
|
|
15
|
+
2. Investigate codebase. _Isolate deep exploration in sub-agents/tasks where available. To prevent context snowballing, instruct subagents to give you distilled summaries only._
|
|
16
|
+
3. Read `./spec-template.md` fully. Fill it out based on the intent and investigation. If `{preserved_intent}` is non-empty, substitute it for the `<frozen-after-approval>` block in your filled spec before writing. Write the result to `{spec_file}`.
|
|
17
|
+
4. Self-review against READY FOR DEVELOPMENT standard.
|
|
18
|
+
5. If intent gaps exist, do not fantasize, do not leave open questions, HALT and ask the human.
|
|
19
|
+
6. Token count check (see SCOPE STANDARD). If spec exceeds 1600 tokens:
|
|
20
20
|
- Show user the token count.
|
|
21
21
|
- HALT and ask human: `[S] Split — carve off secondary goals` | `[K] Keep full spec — accept the risks`
|
|
22
22
|
- On **S**: Propose the split — name each secondary goal. Append deferred goals to `{deferred_work_file}`. Rewrite the current spec to cover only the main goal — do not surgically carve sections out; regenerate the spec for the narrowed scope. Continue to checkpoint.
|
|
@@ -26,7 +26,7 @@ deferred_work_file: '{implementation_artifacts}/deferred-work.md'
|
|
|
26
26
|
|
|
27
27
|
Present summary. If token count exceeded 1600 and user chose [K], include the token count and explain why it may be a problem. HALT and ask human: `[A] Approve` | `[E] Edit`
|
|
28
28
|
|
|
29
|
-
- **A**:
|
|
29
|
+
- **A**: Set status `ready-for-dev` in `{spec_file}`. Everything inside `<frozen-after-approval>` is now locked — only the human can change it. Display the finalized spec path to the user as a CWD-relative path (no leading `/`) so it is clickable in the terminal. → Step 3.
|
|
30
30
|
- **E**: Apply changes, then return to CHECKPOINT 1.
|
|
31
31
|
|
|
32
32
|
|
|
@@ -70,10 +70,6 @@ Load and read full config from `{main_config}` and resolve:
|
|
|
70
70
|
|
|
71
71
|
YOU MUST ALWAYS SPEAK OUTPUT in your Agent communication style with the config `{communication_language}`.
|
|
72
72
|
|
|
73
|
-
### 2.
|
|
74
|
-
|
|
75
|
-
- `wipFile` = `{implementation_artifacts}/spec-wip.md`
|
|
76
|
-
|
|
77
|
-
### 3. First Step Execution
|
|
73
|
+
### 2. First Step Execution
|
|
78
74
|
|
|
79
75
|
Read fully and follow: `./step-01-clarify-and-route.md` to begin the workflow.
|
|
@@ -17,7 +17,6 @@ module.exports = {
|
|
|
17
17
|
'--tools <tools>',
|
|
18
18
|
'Comma-separated list of tool/IDE IDs to configure (e.g., "claude-code,cursor"). Use "none" to skip tool configuration.',
|
|
19
19
|
],
|
|
20
|
-
['--custom-content <paths>', 'Comma-separated list of paths to custom modules/agents/workflows'],
|
|
21
20
|
['--action <type>', 'Action type for existing installations: install, update, or quick-update'],
|
|
22
21
|
['--user-name <name>', 'Name for agents to use (default: system username)'],
|
|
23
22
|
['--communication-language <lang>', 'Language for agent communication (default: English)'],
|
|
@@ -10,14 +10,13 @@ const { Manifest } = require('./manifest');
|
|
|
10
10
|
class ExistingInstall {
|
|
11
11
|
#version;
|
|
12
12
|
|
|
13
|
-
constructor({ installed, version, hasCore, modules, ides
|
|
13
|
+
constructor({ installed, version, hasCore, modules, ides }) {
|
|
14
14
|
this.installed = installed;
|
|
15
15
|
this.#version = version;
|
|
16
16
|
this.hasCore = hasCore;
|
|
17
17
|
this.modules = Object.freeze(modules.map((m) => Object.freeze({ ...m })));
|
|
18
18
|
this.moduleIds = Object.freeze(this.modules.map((m) => m.id));
|
|
19
19
|
this.ides = Object.freeze([...ides]);
|
|
20
|
-
this.customModules = Object.freeze([...customModules]);
|
|
21
20
|
Object.freeze(this);
|
|
22
21
|
}
|
|
23
22
|
|
|
@@ -35,7 +34,6 @@ class ExistingInstall {
|
|
|
35
34
|
hasCore: false,
|
|
36
35
|
modules: [],
|
|
37
36
|
ides: [],
|
|
38
|
-
customModules: [],
|
|
39
37
|
});
|
|
40
38
|
}
|
|
41
39
|
|
|
@@ -53,15 +51,11 @@ class ExistingInstall {
|
|
|
53
51
|
let hasCore = false;
|
|
54
52
|
const modules = [];
|
|
55
53
|
let ides = [];
|
|
56
|
-
let customModules = [];
|
|
57
54
|
|
|
58
55
|
const manifest = new Manifest();
|
|
59
56
|
const manifestData = await manifest.read(bmadDir);
|
|
60
57
|
if (manifestData) {
|
|
61
58
|
version = manifestData.version;
|
|
62
|
-
if (manifestData.customModules) {
|
|
63
|
-
customModules = manifestData.customModules;
|
|
64
|
-
}
|
|
65
59
|
if (manifestData.ides) {
|
|
66
60
|
ides = manifestData.ides.filter((ide) => ide && typeof ide === 'string');
|
|
67
61
|
}
|
|
@@ -120,7 +114,7 @@ class ExistingInstall {
|
|
|
120
114
|
return ExistingInstall.empty();
|
|
121
115
|
}
|
|
122
116
|
|
|
123
|
-
return new ExistingInstall({ installed, version, hasCore, modules, ides
|
|
117
|
+
return new ExistingInstall({ installed, version, hasCore, modules, ides });
|
|
124
118
|
}
|
|
125
119
|
}
|
|
126
120
|
|
|
@@ -20,14 +20,12 @@ class InstallPaths {
|
|
|
20
20
|
|
|
21
21
|
const configDir = path.join(bmadDir, '_config');
|
|
22
22
|
const agentsDir = path.join(configDir, 'agents');
|
|
23
|
-
const customCacheDir = path.join(configDir, 'custom');
|
|
24
23
|
const coreDir = path.join(bmadDir, 'core');
|
|
25
24
|
|
|
26
25
|
for (const [dir, label] of [
|
|
27
26
|
[bmadDir, 'bmad directory'],
|
|
28
27
|
[configDir, 'config directory'],
|
|
29
28
|
[agentsDir, 'agents config directory'],
|
|
30
|
-
[customCacheDir, 'custom modules cache'],
|
|
31
29
|
[coreDir, 'core module directory'],
|
|
32
30
|
]) {
|
|
33
31
|
await ensureWritableDir(dir, label);
|
|
@@ -40,7 +38,6 @@ class InstallPaths {
|
|
|
40
38
|
bmadDir,
|
|
41
39
|
configDir,
|
|
42
40
|
agentsDir,
|
|
43
|
-
customCacheDir,
|
|
44
41
|
coreDir,
|
|
45
42
|
isUpdate,
|
|
46
43
|
});
|
|
@@ -2,7 +2,6 @@ const path = require('node:path');
|
|
|
2
2
|
const fs = require('fs-extra');
|
|
3
3
|
const { Manifest } = require('./manifest');
|
|
4
4
|
const { OfficialModules } = require('../modules/official-modules');
|
|
5
|
-
const { CustomModules } = require('../modules/custom-modules');
|
|
6
5
|
const { IdeManager } = require('../ide/manager');
|
|
7
6
|
const { FileOps } = require('../file-ops');
|
|
8
7
|
const { Config } = require('./config');
|
|
@@ -19,7 +18,6 @@ class Installer {
|
|
|
19
18
|
constructor() {
|
|
20
19
|
this.externalModuleManager = new ExternalModuleManager();
|
|
21
20
|
this.manifest = new Manifest();
|
|
22
|
-
this.customModules = new CustomModules();
|
|
23
21
|
this.ideManager = new IdeManager();
|
|
24
22
|
this.fileOps = new FileOps();
|
|
25
23
|
this.installedFiles = new Set(); // Track all installed files
|
|
@@ -80,8 +78,6 @@ class Installer {
|
|
|
80
78
|
const officialModules = await OfficialModules.build(config, paths);
|
|
81
79
|
const existingInstall = await ExistingInstall.detect(paths.bmadDir);
|
|
82
80
|
|
|
83
|
-
await this.customModules.discoverPaths(originalConfig, paths);
|
|
84
|
-
|
|
85
81
|
if (existingInstall.installed) {
|
|
86
82
|
await this._removeDeselectedModules(existingInstall, config, paths);
|
|
87
83
|
updateState = await this._prepareUpdateState(paths, config, existingInstall, officialModules);
|
|
@@ -121,14 +117,9 @@ class Installer {
|
|
|
121
117
|
}
|
|
122
118
|
}
|
|
123
119
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// Compute module lists: official = selected minus custom, all = both
|
|
127
|
-
const customModuleIds = new Set(this.customModules.paths.keys());
|
|
128
|
-
const officialModuleIds = (config.modules || []).filter((m) => !customModuleIds.has(m));
|
|
129
|
-
const allModules = [...officialModuleIds, ...[...customModuleIds].filter((id) => !officialModuleIds.includes(id))];
|
|
120
|
+
const allModules = config.modules || [];
|
|
130
121
|
|
|
131
|
-
await this._installAndConfigure(config, originalConfig, paths,
|
|
122
|
+
await this._installAndConfigure(config, originalConfig, paths, allModules, allModules, addResult, officialModules);
|
|
132
123
|
|
|
133
124
|
await this._setupIdes(config, allModules, paths, addResult, previousSkillIds);
|
|
134
125
|
|
|
@@ -242,26 +233,6 @@ class Installer {
|
|
|
242
233
|
}
|
|
243
234
|
}
|
|
244
235
|
|
|
245
|
-
/**
|
|
246
|
-
* Cache custom modules into the local cache directory.
|
|
247
|
-
* Updates this.customModules.paths in place with cached locations.
|
|
248
|
-
*/
|
|
249
|
-
async _cacheCustomModules(paths, addResult) {
|
|
250
|
-
if (!this.customModules.paths || this.customModules.paths.size === 0) return;
|
|
251
|
-
|
|
252
|
-
const { CustomModuleCache } = require('./custom-module-cache');
|
|
253
|
-
const customCache = new CustomModuleCache(paths.bmadDir);
|
|
254
|
-
|
|
255
|
-
for (const [moduleId, sourcePath] of this.customModules.paths) {
|
|
256
|
-
const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, {
|
|
257
|
-
sourcePath: sourcePath,
|
|
258
|
-
});
|
|
259
|
-
this.customModules.paths.set(moduleId, cachedInfo.cachePath);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
addResult('Custom modules cached', 'ok');
|
|
263
|
-
}
|
|
264
|
-
|
|
265
236
|
/**
|
|
266
237
|
* Install modules, create directories, generate configs and manifests.
|
|
267
238
|
*/
|
|
@@ -284,11 +255,6 @@ class Installer {
|
|
|
284
255
|
installedModuleNames,
|
|
285
256
|
});
|
|
286
257
|
|
|
287
|
-
await this._installCustomModules(config, paths, addResult, officialModules, {
|
|
288
|
-
message,
|
|
289
|
-
installedModuleNames,
|
|
290
|
-
});
|
|
291
|
-
|
|
292
258
|
return `${allModules.length} module(s) ${isQuickUpdate ? 'updated' : 'installed'}`;
|
|
293
259
|
},
|
|
294
260
|
});
|
|
@@ -515,48 +481,7 @@ class Installer {
|
|
|
515
481
|
}
|
|
516
482
|
|
|
517
483
|
/**
|
|
518
|
-
*
|
|
519
|
-
* that aren't already known from the manifest or external module list.
|
|
520
|
-
* @param {Object} paths - InstallPaths instance
|
|
521
|
-
*/
|
|
522
|
-
async _scanCachedCustomModules(paths) {
|
|
523
|
-
const cacheDir = paths.customCacheDir;
|
|
524
|
-
if (!(await fs.pathExists(cacheDir))) {
|
|
525
|
-
return;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
|
529
|
-
|
|
530
|
-
for (const cachedModule of cachedModules) {
|
|
531
|
-
const moduleId = cachedModule.name;
|
|
532
|
-
const cachedPath = path.join(cacheDir, moduleId);
|
|
533
|
-
|
|
534
|
-
// Skip if path doesn't exist (broken symlink, deleted dir) - avoids lstat ENOENT
|
|
535
|
-
if (!(await fs.pathExists(cachedPath)) || !cachedModule.isDirectory()) {
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Skip if we already have this module from manifest
|
|
540
|
-
if (this.customModules.paths.has(moduleId)) {
|
|
541
|
-
continue;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Check if this is an external official module - skip cache for those
|
|
545
|
-
const isExternal = await this.externalModuleManager.hasModule(moduleId);
|
|
546
|
-
if (isExternal) {
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
// Check if this is actually a custom module (has module.yaml)
|
|
551
|
-
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
|
|
552
|
-
if (await fs.pathExists(moduleYamlPath)) {
|
|
553
|
-
this.customModules.paths.set(moduleId, cachedPath);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
/**
|
|
559
|
-
* Common update preparation: detect files, preserve core config, scan cache, back up.
|
|
484
|
+
* Common update preparation: detect files, preserve core config, back up.
|
|
560
485
|
* @param {Object} paths - InstallPaths instance
|
|
561
486
|
* @param {Object} config - Clean config (may have coreConfig updated)
|
|
562
487
|
* @param {Object} existingInstall - Detection result
|
|
@@ -584,8 +509,6 @@ class Installer {
|
|
|
584
509
|
}
|
|
585
510
|
}
|
|
586
511
|
|
|
587
|
-
await this._scanCachedCustomModules(paths);
|
|
588
|
-
|
|
589
512
|
const backupDirs = await this._backupUserFiles(paths, customFiles, modifiedFiles);
|
|
590
513
|
|
|
591
514
|
return {
|
|
@@ -677,38 +600,6 @@ class Installer {
|
|
|
677
600
|
}
|
|
678
601
|
}
|
|
679
602
|
|
|
680
|
-
/**
|
|
681
|
-
* Install custom modules using CustomModules.install().
|
|
682
|
-
* Source paths come from this.customModules.paths (populated by discoverPaths).
|
|
683
|
-
*/
|
|
684
|
-
async _installCustomModules(config, paths, addResult, officialModules, ctx) {
|
|
685
|
-
const { message, installedModuleNames } = ctx;
|
|
686
|
-
const isQuickUpdate = config.isQuickUpdate();
|
|
687
|
-
|
|
688
|
-
for (const [moduleName, sourcePath] of this.customModules.paths) {
|
|
689
|
-
if (installedModuleNames.has(moduleName)) continue;
|
|
690
|
-
installedModuleNames.add(moduleName);
|
|
691
|
-
|
|
692
|
-
message(`${isQuickUpdate ? 'Updating' : 'Installing'} ${moduleName}...`);
|
|
693
|
-
|
|
694
|
-
const collectedModuleConfig = officialModules.moduleConfigs[moduleName] || {};
|
|
695
|
-
const result = await this.customModules.install(moduleName, paths.bmadDir, (filePath) => this.installedFiles.add(filePath), {
|
|
696
|
-
moduleConfig: collectedModuleConfig,
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
// Generate runtime config.yaml with merged values
|
|
700
|
-
await this.generateModuleConfigs(paths.bmadDir, {
|
|
701
|
-
[moduleName]: { ...config.coreConfig, ...result.moduleConfig, ...collectedModuleConfig },
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
// Get display name from source module.yaml; version from marketplace.json
|
|
705
|
-
const moduleInfo = await officialModules.getModuleInfo(sourcePath, moduleName, '');
|
|
706
|
-
const displayName = moduleInfo?.name || moduleName;
|
|
707
|
-
const version = await this._getMarketplaceVersion(sourcePath);
|
|
708
|
-
addResult(displayName, 'ok', '', { moduleCode: moduleName, newVersion: version });
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
603
|
/**
|
|
713
604
|
* Read files-manifest.csv
|
|
714
605
|
* @param {string} bmadDir - BMAD installation directory
|
|
@@ -1253,16 +1144,9 @@ class Installer {
|
|
|
1253
1144
|
const configuredIdes = existingInstall.ides;
|
|
1254
1145
|
const projectRoot = path.dirname(bmadDir);
|
|
1255
1146
|
|
|
1256
|
-
const customModuleSources = await this.customModules.assembleQuickUpdateSources(
|
|
1257
|
-
config,
|
|
1258
|
-
existingInstall,
|
|
1259
|
-
bmadDir,
|
|
1260
|
-
this.externalModuleManager,
|
|
1261
|
-
);
|
|
1262
|
-
|
|
1263
1147
|
// Get available modules (what we have source for)
|
|
1264
1148
|
const availableModulesData = await new OfficialModules().listAvailable();
|
|
1265
|
-
const availableModules = [...availableModulesData.modules
|
|
1149
|
+
const availableModules = [...availableModulesData.modules];
|
|
1266
1150
|
|
|
1267
1151
|
// Add external official modules to available modules
|
|
1268
1152
|
const externalModules = await this.externalModuleManager.listAvailable();
|
|
@@ -1277,52 +1161,12 @@ class Installer {
|
|
|
1277
1161
|
}
|
|
1278
1162
|
}
|
|
1279
1163
|
|
|
1280
|
-
|
|
1281
|
-
for (const [moduleId, customModule] of customModuleSources) {
|
|
1282
|
-
const sourcePath = customModule.sourcePath;
|
|
1283
|
-
if (sourcePath && (await fs.pathExists(sourcePath)) && !availableModules.some((m) => m.id === moduleId)) {
|
|
1284
|
-
availableModules.push({
|
|
1285
|
-
id: moduleId,
|
|
1286
|
-
name: customModule.name || moduleId,
|
|
1287
|
-
path: sourcePath,
|
|
1288
|
-
isCustom: true,
|
|
1289
|
-
fromManifest: true,
|
|
1290
|
-
});
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
// Handle missing custom module sources
|
|
1295
|
-
const customModuleResult = await this.handleMissingCustomSources(
|
|
1296
|
-
customModuleSources,
|
|
1297
|
-
bmadDir,
|
|
1298
|
-
projectRoot,
|
|
1299
|
-
'update',
|
|
1300
|
-
installedModules,
|
|
1301
|
-
config.skipPrompts || false,
|
|
1302
|
-
);
|
|
1303
|
-
|
|
1304
|
-
const { validCustomModules, keptModulesWithoutSources } = customModuleResult;
|
|
1305
|
-
|
|
1306
|
-
const customModulesFromManifest = validCustomModules.map((m) => ({
|
|
1307
|
-
...m,
|
|
1308
|
-
isCustom: true,
|
|
1309
|
-
hasUpdate: true,
|
|
1310
|
-
}));
|
|
1311
|
-
|
|
1312
|
-
const allAvailableModules = [...availableModules, ...customModulesFromManifest];
|
|
1313
|
-
const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
|
|
1164
|
+
const availableModuleIds = new Set(availableModules.map((m) => m.id));
|
|
1314
1165
|
|
|
1315
1166
|
// Only update modules that are BOTH installed AND available (we have source for)
|
|
1316
1167
|
const modulesToUpdate = installedModules.filter((id) => availableModuleIds.has(id));
|
|
1317
1168
|
const skippedModules = installedModules.filter((id) => !availableModuleIds.has(id));
|
|
1318
1169
|
|
|
1319
|
-
// Add custom modules that were kept without sources to the skipped modules
|
|
1320
|
-
for (const keptModule of keptModulesWithoutSources) {
|
|
1321
|
-
if (!skippedModules.includes(keptModule)) {
|
|
1322
|
-
skippedModules.push(keptModule);
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
1170
|
if (skippedModules.length > 0) {
|
|
1327
1171
|
await prompts.log.warn(`Skipping ${skippedModules.length} module(s) - no source available: ${skippedModules.join(', ')}`);
|
|
1328
1172
|
}
|
|
@@ -1367,9 +1211,7 @@ class Installer {
|
|
|
1367
1211
|
actionType: 'install',
|
|
1368
1212
|
_quickUpdate: true,
|
|
1369
1213
|
_preserveModules: skippedModules,
|
|
1370
|
-
_customModuleSources: customModuleSources,
|
|
1371
1214
|
_existingModules: installedModules,
|
|
1372
|
-
customContent: config.customContent,
|
|
1373
1215
|
};
|
|
1374
1216
|
|
|
1375
1217
|
await this.install(installConfig);
|
|
@@ -1504,239 +1346,6 @@ class Installer {
|
|
|
1504
1346
|
return this._readOutputFolder(bmadDir);
|
|
1505
1347
|
}
|
|
1506
1348
|
|
|
1507
|
-
/**
|
|
1508
|
-
* Handle missing custom module sources interactively
|
|
1509
|
-
* @param {Map} customModuleSources - Map of custom module ID to info
|
|
1510
|
-
* @param {string} bmadDir - BMAD directory
|
|
1511
|
-
* @param {string} projectRoot - Project root directory
|
|
1512
|
-
* @param {string} operation - Current operation ('update', 'compile', etc.)
|
|
1513
|
-
* @param {Array} installedModules - Array of installed module IDs (will be modified)
|
|
1514
|
-
* @param {boolean} [skipPrompts=false] - Skip interactive prompts and keep all modules with missing sources
|
|
1515
|
-
* @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array
|
|
1516
|
-
*/
|
|
1517
|
-
async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules, skipPrompts = false) {
|
|
1518
|
-
const validCustomModules = [];
|
|
1519
|
-
const keptModulesWithoutSources = []; // Track modules kept without sources
|
|
1520
|
-
const customModulesWithMissingSources = [];
|
|
1521
|
-
|
|
1522
|
-
// Check which sources exist
|
|
1523
|
-
for (const [moduleId, customInfo] of customModuleSources) {
|
|
1524
|
-
if (await fs.pathExists(customInfo.sourcePath)) {
|
|
1525
|
-
validCustomModules.push({
|
|
1526
|
-
id: moduleId,
|
|
1527
|
-
name: customInfo.name,
|
|
1528
|
-
path: customInfo.sourcePath,
|
|
1529
|
-
info: customInfo,
|
|
1530
|
-
});
|
|
1531
|
-
} else {
|
|
1532
|
-
// For cached modules that are missing, we just skip them without prompting
|
|
1533
|
-
if (customInfo.cached) {
|
|
1534
|
-
// Skip cached modules without prompting
|
|
1535
|
-
keptModulesWithoutSources.push({
|
|
1536
|
-
id: moduleId,
|
|
1537
|
-
name: customInfo.name,
|
|
1538
|
-
cached: true,
|
|
1539
|
-
});
|
|
1540
|
-
} else {
|
|
1541
|
-
customModulesWithMissingSources.push({
|
|
1542
|
-
id: moduleId,
|
|
1543
|
-
name: customInfo.name,
|
|
1544
|
-
sourcePath: customInfo.sourcePath,
|
|
1545
|
-
relativePath: customInfo.relativePath,
|
|
1546
|
-
info: customInfo,
|
|
1547
|
-
});
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
// If no missing sources, return immediately
|
|
1553
|
-
if (customModulesWithMissingSources.length === 0) {
|
|
1554
|
-
return {
|
|
1555
|
-
validCustomModules,
|
|
1556
|
-
keptModulesWithoutSources: [],
|
|
1557
|
-
};
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
// Non-interactive mode: keep all modules with missing sources
|
|
1561
|
-
if (skipPrompts) {
|
|
1562
|
-
for (const missing of customModulesWithMissingSources) {
|
|
1563
|
-
keptModulesWithoutSources.push(missing.id);
|
|
1564
|
-
}
|
|
1565
|
-
return { validCustomModules, keptModulesWithoutSources };
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
await prompts.log.warn(`Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`);
|
|
1569
|
-
|
|
1570
|
-
let keptCount = 0;
|
|
1571
|
-
let updatedCount = 0;
|
|
1572
|
-
let removedCount = 0;
|
|
1573
|
-
|
|
1574
|
-
for (const missing of customModulesWithMissingSources) {
|
|
1575
|
-
await prompts.log.message(
|
|
1576
|
-
`${missing.name} (${missing.id})\n Original source: ${missing.relativePath}\n Full path: ${missing.sourcePath}`,
|
|
1577
|
-
);
|
|
1578
|
-
|
|
1579
|
-
const choices = [
|
|
1580
|
-
{
|
|
1581
|
-
name: 'Keep installed (will not be processed)',
|
|
1582
|
-
value: 'keep',
|
|
1583
|
-
hint: 'Keep',
|
|
1584
|
-
},
|
|
1585
|
-
{
|
|
1586
|
-
name: 'Specify new source location',
|
|
1587
|
-
value: 'update',
|
|
1588
|
-
hint: 'Update',
|
|
1589
|
-
},
|
|
1590
|
-
];
|
|
1591
|
-
|
|
1592
|
-
// Only add remove option if not just compiling agents
|
|
1593
|
-
if (operation !== 'compile-agents') {
|
|
1594
|
-
choices.push({
|
|
1595
|
-
name: '⚠️ REMOVE module completely (destructive!)',
|
|
1596
|
-
value: 'remove',
|
|
1597
|
-
hint: 'Remove',
|
|
1598
|
-
});
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
const action = await prompts.select({
|
|
1602
|
-
message: `How would you like to handle "${missing.name}"?`,
|
|
1603
|
-
choices,
|
|
1604
|
-
});
|
|
1605
|
-
|
|
1606
|
-
switch (action) {
|
|
1607
|
-
case 'update': {
|
|
1608
|
-
// Use sync validation because @clack/prompts doesn't support async validate
|
|
1609
|
-
const newSourcePath = await prompts.text({
|
|
1610
|
-
message: 'Enter the new path to the custom module:',
|
|
1611
|
-
default: missing.sourcePath,
|
|
1612
|
-
validate: (input) => {
|
|
1613
|
-
if (!input || input.trim() === '') {
|
|
1614
|
-
return 'Please enter a path';
|
|
1615
|
-
}
|
|
1616
|
-
const expandedPath = path.resolve(input.trim());
|
|
1617
|
-
if (!fs.pathExistsSync(expandedPath)) {
|
|
1618
|
-
return 'Path does not exist';
|
|
1619
|
-
}
|
|
1620
|
-
// Check if it looks like a valid module
|
|
1621
|
-
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
|
1622
|
-
const agentsPath = path.join(expandedPath, 'agents');
|
|
1623
|
-
const workflowsPath = path.join(expandedPath, 'workflows');
|
|
1624
|
-
|
|
1625
|
-
if (!fs.pathExistsSync(moduleYamlPath) && !fs.pathExistsSync(agentsPath) && !fs.pathExistsSync(workflowsPath)) {
|
|
1626
|
-
return 'Path does not appear to contain a valid custom module';
|
|
1627
|
-
}
|
|
1628
|
-
return; // clack expects undefined for valid input
|
|
1629
|
-
},
|
|
1630
|
-
});
|
|
1631
|
-
|
|
1632
|
-
// Defensive: handleCancel should have exited, but guard against symbol propagation
|
|
1633
|
-
if (typeof newSourcePath !== 'string') {
|
|
1634
|
-
keptCount++;
|
|
1635
|
-
keptModulesWithoutSources.push(missing.id);
|
|
1636
|
-
continue;
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
// Update the source in manifest
|
|
1640
|
-
const resolvedPath = path.resolve(newSourcePath.trim());
|
|
1641
|
-
missing.info.sourcePath = resolvedPath;
|
|
1642
|
-
// Remove relativePath - we only store absolute sourcePath now
|
|
1643
|
-
delete missing.info.relativePath;
|
|
1644
|
-
await this.manifest.addCustomModule(bmadDir, missing.info);
|
|
1645
|
-
|
|
1646
|
-
validCustomModules.push({
|
|
1647
|
-
id: missing.id,
|
|
1648
|
-
name: missing.name,
|
|
1649
|
-
path: resolvedPath,
|
|
1650
|
-
info: missing.info,
|
|
1651
|
-
});
|
|
1652
|
-
|
|
1653
|
-
updatedCount++;
|
|
1654
|
-
await prompts.log.success('Updated source location');
|
|
1655
|
-
|
|
1656
|
-
break;
|
|
1657
|
-
}
|
|
1658
|
-
case 'remove': {
|
|
1659
|
-
// Extra confirmation for destructive remove
|
|
1660
|
-
await prompts.log.error(
|
|
1661
|
-
`WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!\n Module location: ${path.join(bmadDir, missing.id)}`,
|
|
1662
|
-
);
|
|
1663
|
-
|
|
1664
|
-
const confirmDelete = await prompts.confirm({
|
|
1665
|
-
message: 'Are you absolutely sure you want to delete this module?',
|
|
1666
|
-
default: false,
|
|
1667
|
-
});
|
|
1668
|
-
|
|
1669
|
-
if (confirmDelete) {
|
|
1670
|
-
const typedConfirm = await prompts.text({
|
|
1671
|
-
message: 'Type "DELETE" to confirm permanent deletion:',
|
|
1672
|
-
validate: (input) => {
|
|
1673
|
-
if (input !== 'DELETE') {
|
|
1674
|
-
return 'You must type "DELETE" exactly to proceed';
|
|
1675
|
-
}
|
|
1676
|
-
return; // clack expects undefined for valid input
|
|
1677
|
-
},
|
|
1678
|
-
});
|
|
1679
|
-
|
|
1680
|
-
if (typedConfirm === 'DELETE') {
|
|
1681
|
-
// Remove the module from filesystem and manifest
|
|
1682
|
-
const modulePath = path.join(bmadDir, missing.id);
|
|
1683
|
-
if (await fs.pathExists(modulePath)) {
|
|
1684
|
-
const fsExtra = require('fs-extra');
|
|
1685
|
-
await fsExtra.remove(modulePath);
|
|
1686
|
-
await prompts.log.warn(`Deleted module directory: ${path.relative(projectRoot, modulePath)}`);
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
await this.manifest.removeModule(bmadDir, missing.id);
|
|
1690
|
-
await this.manifest.removeCustomModule(bmadDir, missing.id);
|
|
1691
|
-
await prompts.log.warn('Removed from manifest');
|
|
1692
|
-
|
|
1693
|
-
// Also remove from installedModules list
|
|
1694
|
-
if (installedModules && installedModules.includes(missing.id)) {
|
|
1695
|
-
const index = installedModules.indexOf(missing.id);
|
|
1696
|
-
if (index !== -1) {
|
|
1697
|
-
installedModules.splice(index, 1);
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
removedCount++;
|
|
1702
|
-
await prompts.log.error(`"${missing.name}" has been permanently removed`);
|
|
1703
|
-
} else {
|
|
1704
|
-
await prompts.log.message('Removal cancelled - module will be kept');
|
|
1705
|
-
keptCount++;
|
|
1706
|
-
}
|
|
1707
|
-
} else {
|
|
1708
|
-
await prompts.log.message('Removal cancelled - module will be kept');
|
|
1709
|
-
keptCount++;
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
break;
|
|
1713
|
-
}
|
|
1714
|
-
case 'keep': {
|
|
1715
|
-
keptCount++;
|
|
1716
|
-
keptModulesWithoutSources.push(missing.id);
|
|
1717
|
-
await prompts.log.message('Module will be kept as-is');
|
|
1718
|
-
|
|
1719
|
-
break;
|
|
1720
|
-
}
|
|
1721
|
-
// No default
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
// Show summary
|
|
1726
|
-
if (keptCount > 0 || updatedCount > 0 || removedCount > 0) {
|
|
1727
|
-
let summary = 'Summary for custom modules with missing sources:';
|
|
1728
|
-
if (keptCount > 0) summary += `\n • ${keptCount} module(s) kept as-is`;
|
|
1729
|
-
if (updatedCount > 0) summary += `\n • ${updatedCount} module(s) updated with new sources`;
|
|
1730
|
-
if (removedCount > 0) summary += `\n • ${removedCount} module(s) permanently deleted`;
|
|
1731
|
-
await prompts.log.message(summary);
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
return {
|
|
1735
|
-
validCustomModules,
|
|
1736
|
-
keptModulesWithoutSources,
|
|
1737
|
-
};
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
1349
|
/**
|
|
1741
1350
|
* Find the bmad installation directory in a project
|
|
1742
1351
|
* Always uses the standard _bmad folder name
|