@hyperdrive.bot/cli 1.0.12 → 1.0.16
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 +1495 -474
- package/dist/commands/deploy.d.ts +18 -0
- package/dist/commands/deploy.js +239 -0
- package/dist/commands/deployment/create.js +10 -2
- package/dist/commands/domain/{switch.d.ts → set-production.d.ts} +1 -1
- package/dist/commands/domain/set-production.js +27 -0
- package/dist/commands/git/list-open-prs.d.ts +12 -0
- package/dist/commands/git/list-open-prs.js +87 -0
- package/dist/commands/hook/add.d.ts +22 -0
- package/dist/commands/hook/add.js +299 -0
- package/dist/commands/hook/list.d.ts +11 -0
- package/dist/commands/hook/list.js +111 -0
- package/dist/commands/hook/logs.d.ts +13 -0
- package/dist/commands/hook/logs.js +124 -0
- package/dist/commands/hook/remove.d.ts +12 -0
- package/dist/commands/hook/remove.js +115 -0
- package/dist/commands/hook/toggle.d.ts +12 -0
- package/dist/commands/hook/toggle.js +125 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +49 -9
- package/dist/commands/module/bindings.d.ts +14 -0
- package/dist/commands/module/bindings.js +125 -0
- package/dist/commands/module/create.d.ts +3 -0
- package/dist/commands/module/create.js +156 -78
- package/dist/commands/module/list.d.ts +1 -0
- package/dist/commands/module/list.js +22 -1
- package/dist/commands/module/sync.d.ts +29 -0
- package/dist/commands/module/sync.js +409 -0
- package/dist/commands/module/unlink.d.ts +11 -0
- package/dist/commands/module/unlink.js +77 -0
- package/dist/commands/module/update.d.ts +10 -0
- package/dist/commands/module/update.js +168 -5
- package/dist/commands/network/discover.d.ts +12 -0
- package/dist/commands/network/discover.js +210 -0
- package/dist/commands/network/get.d.ts +13 -0
- package/dist/commands/network/get.js +90 -0
- package/dist/commands/{auth/logout.d.ts → network/list.d.ts} +2 -9
- package/dist/commands/network/list.js +71 -0
- package/dist/commands/network/register.d.ts +16 -0
- package/dist/commands/network/register.js +144 -0
- package/dist/commands/parameter/sync.d.ts +13 -0
- package/dist/commands/parameter/sync.js +69 -1
- package/dist/commands/project/sync.d.ts +5 -11
- package/dist/commands/project/sync.js +12 -381
- package/dist/commands/seed.d.ts +93 -0
- package/dist/commands/seed.js +324 -0
- package/dist/commands/service/backup.d.ts +17 -0
- package/dist/commands/service/backup.js +156 -0
- package/dist/commands/service/backups.d.ts +14 -0
- package/dist/commands/service/backups.js +110 -0
- package/dist/commands/service/bind.d.ts +16 -0
- package/dist/commands/service/bind.js +106 -0
- package/dist/commands/service/bindings.d.ts +13 -0
- package/dist/commands/service/bindings.js +78 -0
- package/dist/commands/service/clone.d.ts +19 -0
- package/dist/commands/service/clone.js +153 -0
- package/dist/commands/service/create.d.ts +16 -0
- package/dist/commands/service/create.js +212 -0
- package/dist/commands/service/get.d.ts +13 -0
- package/dist/commands/service/get.js +97 -0
- package/dist/commands/service/list.d.ts +12 -0
- package/dist/commands/service/list.js +86 -0
- package/dist/commands/service/register.d.ts +21 -0
- package/dist/commands/service/register.js +215 -0
- package/dist/commands/service/restore.d.ts +19 -0
- package/dist/commands/service/restore.js +158 -0
- package/dist/commands/service/seed.d.ts +17 -0
- package/dist/commands/service/seed.js +173 -0
- package/dist/commands/service/templates.d.ts +10 -0
- package/dist/commands/service/templates.js +66 -0
- package/dist/commands/service/unbind.d.ts +15 -0
- package/dist/commands/service/unbind.js +74 -0
- package/dist/commands/stage/create.d.ts +23 -0
- package/dist/commands/stage/create.js +145 -6
- package/dist/commands/stage/delete.d.ts +11 -0
- package/dist/commands/stage/delete.js +85 -0
- package/dist/commands/stage/deploy.d.ts +34 -0
- package/dist/commands/stage/deploy.js +294 -0
- package/dist/commands/stage/ensure-branches.d.ts +23 -0
- package/dist/commands/stage/ensure-branches.js +101 -0
- package/dist/commands/stage/list.js +4 -0
- package/dist/commands/stage/status.d.ts +14 -0
- package/dist/commands/stage/status.js +100 -0
- package/dist/commands/{jira → tracker}/connect.js +32 -23
- package/dist/commands/tracker/hook/add.d.ts +25 -0
- package/dist/commands/tracker/hook/add.js +284 -0
- package/dist/commands/{jira → tracker}/hook/list.js +20 -11
- package/dist/commands/{jira/hook/add.d.ts → tracker/hook/logs.d.ts} +2 -3
- package/dist/commands/tracker/hook/logs.js +126 -0
- package/dist/commands/{jira → tracker}/hook/remove.js +9 -8
- package/dist/commands/{jira → tracker}/hook/toggle.js +14 -12
- package/dist/commands/tracker/project/init.d.ts +17 -0
- package/dist/commands/tracker/project/init.js +178 -0
- package/dist/commands/tracker/project/link-module.d.ts +17 -0
- package/dist/commands/tracker/project/link-module.js +287 -0
- package/dist/commands/tracker/project/list-modules.d.ts +11 -0
- package/dist/commands/tracker/project/list-modules.js +117 -0
- package/dist/commands/tracker/project/list.d.ts +10 -0
- package/dist/commands/tracker/project/list.js +90 -0
- package/dist/commands/tracker/project/status.d.ts +13 -0
- package/dist/commands/tracker/project/status.js +168 -0
- package/dist/commands/tracker/project/unlink-module.d.ts +13 -0
- package/dist/commands/tracker/project/unlink-module.js +251 -0
- package/dist/commands/{jira → tracker}/status.js +3 -3
- package/dist/lib/ensure-branches.d.ts +53 -0
- package/dist/lib/ensure-branches.js +149 -0
- package/dist/lib/git-providers/github.d.ts +16 -0
- package/dist/lib/git-providers/github.js +157 -0
- package/dist/lib/git-providers/gitlab.d.ts +16 -0
- package/dist/lib/git-providers/gitlab.js +148 -0
- package/dist/lib/git-providers/index.d.ts +67 -0
- package/dist/lib/git-providers/index.js +39 -0
- package/dist/lib/lambda-warmer.d.ts +106 -0
- package/dist/lib/lambda-warmer.js +189 -0
- package/dist/services/hyperdrive-sigv4.d.ts +360 -5
- package/dist/services/hyperdrive-sigv4.js +192 -24
- package/dist/utils/hook-flow.d.ts +60 -3
- package/dist/utils/hook-flow.js +437 -2
- package/dist/utils/hook-normalize.d.ts +6 -0
- package/dist/utils/hook-normalize.js +33 -0
- package/dist/utils/lifecycle-poller.d.ts +32 -0
- package/dist/utils/lifecycle-poller.js +72 -0
- package/dist/utils/retry.d.ts +43 -0
- package/dist/utils/retry.js +88 -0
- package/dist/utils/summary-display.js +1 -1
- package/dist/utils/tracker-project-flow.d.ts +84 -0
- package/dist/utils/tracker-project-flow.js +564 -0
- package/package.json +35 -7
- package/dist/commands/auth/login.d.ts +0 -16
- package/dist/commands/auth/login.js +0 -179
- package/dist/commands/auth/logout.js +0 -116
- package/dist/commands/auth/refresh.d.ts +0 -6
- package/dist/commands/auth/refresh.js +0 -66
- package/dist/commands/auth/status.d.ts +0 -6
- package/dist/commands/auth/status.js +0 -63
- package/dist/commands/config/get.d.ts +0 -9
- package/dist/commands/config/get.js +0 -37
- package/dist/commands/config/set.d.ts +0 -10
- package/dist/commands/config/set.js +0 -48
- package/dist/commands/config/show.d.ts +0 -6
- package/dist/commands/config/show.js +0 -10
- package/dist/commands/domain/current.d.ts +0 -6
- package/dist/commands/domain/current.js +0 -18
- package/dist/commands/domain/list.d.ts +0 -6
- package/dist/commands/domain/list.js +0 -42
- package/dist/commands/domain/switch.js +0 -40
- package/dist/commands/jira/hook/add.js +0 -147
- package/dist/services/tenant-service.d.ts +0 -127
- package/dist/services/tenant-service.js +0 -396
- package/dist/utils/auth-flow.d.ts +0 -147
- package/dist/utils/auth-flow.js +0 -479
- package/oclif.manifest.json +0 -3519
- /package/dist/commands/{jira → tracker}/connect.d.ts +0 -0
- /package/dist/commands/{jira → tracker}/hook/list.d.ts +0 -0
- /package/dist/commands/{jira → tracker}/hook/remove.d.ts +0 -0
- /package/dist/commands/{jira → tracker}/hook/toggle.d.ts +0 -0
- /package/dist/commands/{jira → tracker}/status.d.ts +0 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module Sync Command
|
|
3
|
+
*
|
|
4
|
+
* Generates structured architecture summaries for modules.
|
|
5
|
+
* For each module: clone → detect _bmad or invoke Claude → validate YAML → upload via API.
|
|
6
|
+
* Replaces the deprecated `project sync` command with module-level ownership.
|
|
7
|
+
*/
|
|
8
|
+
import { Args, Command, Flags } from '@oclif/core';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { execFileSync, spawn } from 'child_process';
|
|
11
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'fs';
|
|
12
|
+
import yaml from 'js-yaml';
|
|
13
|
+
import ora from 'ora';
|
|
14
|
+
import { tmpdir } from 'os';
|
|
15
|
+
import { join } from 'path';
|
|
16
|
+
import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Architecture Summary YAML Schema & Validation (inline for CLI — no server deps)
|
|
19
|
+
// ============================================================================
|
|
20
|
+
const VALID_ENTITY_TYPES = ['client', 'company', 'delivery', 'initiative', 'module', 'service', 'system', 'tool'];
|
|
21
|
+
const REQUIRED_SUMMARY_KEYS = ['module', 'domains', 'patterns', 'tech_stack'];
|
|
22
|
+
const ARCHITECTURE_YAML_SCHEMA = `module:
|
|
23
|
+
name: "<module-name>"
|
|
24
|
+
description: "<one-line description>"
|
|
25
|
+
|
|
26
|
+
domains:
|
|
27
|
+
- name: "<domain-name>"
|
|
28
|
+
modules: [<module1>, <module2>]
|
|
29
|
+
key_files: [<path1>, <path2>]
|
|
30
|
+
|
|
31
|
+
patterns:
|
|
32
|
+
handler_pattern: "<glob pattern for handlers>"
|
|
33
|
+
service_pattern: "<glob pattern for services>"
|
|
34
|
+
module_config: "<glob pattern for module config>"
|
|
35
|
+
test_pattern: "<glob pattern for tests>"
|
|
36
|
+
|
|
37
|
+
entity_registry:
|
|
38
|
+
- { name: "<entity-name>", type: "<client|company|delivery|initiative|module|service|system|tool>", path: "<relative-path>" }
|
|
39
|
+
|
|
40
|
+
tech_stack:
|
|
41
|
+
runtime: "<e.g. nodejs-22>"
|
|
42
|
+
framework: "<e.g. serverless-v4>"
|
|
43
|
+
language: "<e.g. typescript>"
|
|
44
|
+
database: "<e.g. dynamodb>"`;
|
|
45
|
+
const ARCHITECTURE_ANALYSIS_PROMPT = `Analyze this code repository and produce a structured module architecture summary in YAML format. Output ONLY valid YAML — no markdown fences, no explanations, no commentary.
|
|
46
|
+
|
|
47
|
+
Analyze the following:
|
|
48
|
+
1. Directory structure and key files
|
|
49
|
+
2. Functional domains (groups of related modules)
|
|
50
|
+
3. Code patterns (handler paths, service paths, test paths, module config paths)
|
|
51
|
+
4. Entity registry (named entities with type: client|company|delivery|initiative|module|service|system|tool)
|
|
52
|
+
5. Technology stack (runtime, framework, language, database)
|
|
53
|
+
|
|
54
|
+
Required YAML schema:
|
|
55
|
+
${ARCHITECTURE_YAML_SCHEMA}
|
|
56
|
+
|
|
57
|
+
Now analyze the repository and produce the YAML summary.`;
|
|
58
|
+
const VALID_REPO_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
59
|
+
const MAX_BMAD_DOC_SIZE = 100 * 1024; // 100KB cap for _bmad docs (SEC-002 mitigation)
|
|
60
|
+
export default class ModuleSync extends Command {
|
|
61
|
+
static args = {
|
|
62
|
+
module: Args.string({
|
|
63
|
+
description: 'Module slug',
|
|
64
|
+
required: false,
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
static description = 'Generate architecture summaries for modules via Claude analysis';
|
|
68
|
+
static examples = [
|
|
69
|
+
'<%= config.bin %> module sync my-module',
|
|
70
|
+
'<%= config.bin %> module sync my-module --json',
|
|
71
|
+
'<%= config.bin %> module sync --all',
|
|
72
|
+
'<%= config.bin %> module sync --all --json',
|
|
73
|
+
];
|
|
74
|
+
static flags = {
|
|
75
|
+
all: Flags.boolean({
|
|
76
|
+
description: 'Sync all modules in the tenant',
|
|
77
|
+
exclusive: ['module'],
|
|
78
|
+
}),
|
|
79
|
+
domain: Flags.string({
|
|
80
|
+
char: 'd',
|
|
81
|
+
description: 'Tenant domain (for multi-domain setups)',
|
|
82
|
+
}),
|
|
83
|
+
json: Flags.boolean({
|
|
84
|
+
description: 'Output result as JSON',
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
async run() {
|
|
88
|
+
const { args, flags } = await this.parse(ModuleSync);
|
|
89
|
+
if (!args.module && !flags.all) {
|
|
90
|
+
this.error('Provide a module slug or use --all to sync all modules.');
|
|
91
|
+
}
|
|
92
|
+
// Authenticate
|
|
93
|
+
let service;
|
|
94
|
+
const authSpinner = ora('Checking authentication...').start();
|
|
95
|
+
try {
|
|
96
|
+
service = new HyperdriveSigV4Service(flags.domain);
|
|
97
|
+
authSpinner.succeed('Authenticated');
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
authSpinner.fail('Not authenticated');
|
|
101
|
+
this.error(`${error.message}\n\nPlease authenticate first with: ${chalk.cyan('hd auth login')}`);
|
|
102
|
+
}
|
|
103
|
+
if (flags.all) {
|
|
104
|
+
// --all mode: sync all modules
|
|
105
|
+
await this.syncAllModules(service, flags);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Single module sync
|
|
109
|
+
await this.syncSingleModule(service, args.module, flags);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async syncAllModules(service, flags) {
|
|
113
|
+
const listSpinner = ora('Fetching modules...').start();
|
|
114
|
+
let modules;
|
|
115
|
+
try {
|
|
116
|
+
modules = await service.moduleList();
|
|
117
|
+
listSpinner.succeed(`Found ${modules.length} module(s)`);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
listSpinner.fail('Failed to fetch modules');
|
|
121
|
+
this.error(`Could not list modules: ${error.message}`);
|
|
122
|
+
}
|
|
123
|
+
if (modules.length === 0) {
|
|
124
|
+
if (flags.json) {
|
|
125
|
+
this.log(JSON.stringify({ results: [], summary: { failed: 0, succeeded: 0, total: 0 } }, null, 2));
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
this.log(chalk.yellow('No modules to sync.'));
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.log('');
|
|
133
|
+
this.log(chalk.blue(`Syncing ${modules.length} module(s)...`));
|
|
134
|
+
this.log('');
|
|
135
|
+
const results = [];
|
|
136
|
+
for (let i = 0; i < modules.length; i++) {
|
|
137
|
+
const mod = modules[i];
|
|
138
|
+
const slug = mod.slug || mod.name || `module-${i}`;
|
|
139
|
+
const prefix = chalk.dim(`[${i + 1}/${modules.length}]`);
|
|
140
|
+
const result = await this.syncModule(service, slug, prefix);
|
|
141
|
+
results.push(result);
|
|
142
|
+
}
|
|
143
|
+
// Summary
|
|
144
|
+
this.log('');
|
|
145
|
+
const succeeded = results.filter(r => r.status === 'success').length;
|
|
146
|
+
const failed = results.filter(r => r.status === 'failed').length;
|
|
147
|
+
if (flags.json) {
|
|
148
|
+
this.log(JSON.stringify({ results, summary: { failed, succeeded, total: modules.length } }, null, 2));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (failed === 0) {
|
|
152
|
+
this.log(chalk.green(`Synced ${succeeded}/${modules.length} modules (0 failed)`));
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
this.log(chalk.yellow(`Synced ${succeeded}/${modules.length} modules (${failed} failed)`));
|
|
156
|
+
for (const r of results.filter(r => r.status === 'failed')) {
|
|
157
|
+
this.log(chalk.red(` ${r.slug}: ${r.error}`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async syncSingleModule(service, moduleSlug, flags) {
|
|
162
|
+
const result = await this.syncModule(service, moduleSlug, '');
|
|
163
|
+
if (flags.json) {
|
|
164
|
+
this.log(JSON.stringify(result, null, 2));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (result.status === 'failed') {
|
|
168
|
+
// Error already displayed by syncModule
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
async syncModule(service, moduleSlug, prefix) {
|
|
172
|
+
const label = prefix ? `${prefix} ${chalk.cyan(moduleSlug)}` : chalk.cyan(moduleSlug);
|
|
173
|
+
const spinner = ora(`${label} — resolving...`).start();
|
|
174
|
+
let tmpDir = null;
|
|
175
|
+
try {
|
|
176
|
+
// Step 1: Resolve module by slug
|
|
177
|
+
const mod = await service.moduleGet({ slug: moduleSlug });
|
|
178
|
+
const sourceLocation = mod.sourceLocation;
|
|
179
|
+
if (!sourceLocation) {
|
|
180
|
+
throw new Error('Module has no sourceLocation configured — cannot clone');
|
|
181
|
+
}
|
|
182
|
+
// Validate the slug for path safety
|
|
183
|
+
const safeName = mod.slug || moduleSlug;
|
|
184
|
+
if (!VALID_REPO_NAME_REGEX.test(safeName)) {
|
|
185
|
+
throw new Error(`Invalid module slug "${safeName}" — must match ${VALID_REPO_NAME_REGEX}`);
|
|
186
|
+
}
|
|
187
|
+
// Step 2: Clone the module repo
|
|
188
|
+
spinner.text = `${label} — cloning...`;
|
|
189
|
+
tmpDir = mkdtempSync(join(tmpdir(), `hd-sync-${safeName}-`));
|
|
190
|
+
const clonePath = join(tmpDir, safeName);
|
|
191
|
+
try {
|
|
192
|
+
execFileSync('git', ['clone', '--depth', '1', sourceLocation, clonePath], {
|
|
193
|
+
stdio: 'pipe',
|
|
194
|
+
timeout: 120_000, // 2 min clone timeout
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch (cloneError) {
|
|
198
|
+
throw new Error(`git clone failed: ${cloneError.stderr?.toString() || cloneError.message}`);
|
|
199
|
+
}
|
|
200
|
+
// Step 3: Detect _bmad or generate via Claude
|
|
201
|
+
spinner.text = `${label} — analyzing...`;
|
|
202
|
+
let yamlOutput = null;
|
|
203
|
+
let lastError = null;
|
|
204
|
+
const MAX_ATTEMPTS = 3;
|
|
205
|
+
// Check for _bmad docs
|
|
206
|
+
const bmadDocPath = this.detectBmadDocs(clonePath);
|
|
207
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
208
|
+
try {
|
|
209
|
+
if (bmadDocPath) {
|
|
210
|
+
// Use _bmad doc with simpler restructure prompt
|
|
211
|
+
const docStat = statSync(bmadDocPath);
|
|
212
|
+
if (docStat.size > MAX_BMAD_DOC_SIZE) {
|
|
213
|
+
throw new Error(`_bmad doc exceeds ${MAX_BMAD_DOC_SIZE / 1024}KB size limit (${Math.round(docStat.size / 1024)}KB) — skipping to full analysis`);
|
|
214
|
+
}
|
|
215
|
+
const docContent = readFileSync(bmadDocPath, 'utf-8');
|
|
216
|
+
const prompt = `You are given an existing architecture documentation file. Convert it into the following YAML format. Output ONLY valid YAML, no markdown fences, no explanations.\n\nRequired YAML schema:\n${ARCHITECTURE_YAML_SCHEMA}\n\nHere is the architecture document to convert:\n\n<architecture-doc>\n${docContent}\n</architecture-doc>`;
|
|
217
|
+
yamlOutput = await this.runClaude(prompt, clonePath);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// Full codebase analysis
|
|
221
|
+
yamlOutput = await this.runClaude(ARCHITECTURE_ANALYSIS_PROMPT, clonePath);
|
|
222
|
+
}
|
|
223
|
+
// Validate
|
|
224
|
+
this.validateYaml(yamlOutput);
|
|
225
|
+
break; // Validation passed
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
lastError = error.message;
|
|
229
|
+
this.log(chalk.dim(` ${safeName} attempt ${attempt}/${MAX_ATTEMPTS} failed: ${lastError}`));
|
|
230
|
+
yamlOutput = null;
|
|
231
|
+
if (attempt === MAX_ATTEMPTS) {
|
|
232
|
+
throw new Error(`Validation failed after ${MAX_ATTEMPTS} attempts: ${lastError}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (!yamlOutput) {
|
|
237
|
+
throw new Error(`Generation failed: ${lastError}`);
|
|
238
|
+
}
|
|
239
|
+
// Step 4: Extract entity registry and merge with gut config
|
|
240
|
+
spinner.text = `${label} — extracting entities...`;
|
|
241
|
+
const parsedYaml = yaml.load(yamlOutput);
|
|
242
|
+
let entityRegistry = (parsedYaml.entity_registry || []);
|
|
243
|
+
// Check for .gut/config.json and merge
|
|
244
|
+
const gutEntities = this.readGutEntities(clonePath);
|
|
245
|
+
if (gutEntities.length > 0) {
|
|
246
|
+
entityRegistry = this.mergeEntityRegistries(entityRegistry, gutEntities);
|
|
247
|
+
this.log(chalk.dim(` ${safeName}: merged ${entityRegistry.length} entities (${gutEntities.length} from gut)`));
|
|
248
|
+
}
|
|
249
|
+
else if (entityRegistry.length > 0) {
|
|
250
|
+
this.log(chalk.dim(` ${safeName}: found ${entityRegistry.length} entities from analysis`));
|
|
251
|
+
}
|
|
252
|
+
// Step 5: Upload via moduleUpdateContext API
|
|
253
|
+
spinner.text = `${label} — uploading...`;
|
|
254
|
+
await service.moduleUpdateContext(moduleSlug, {
|
|
255
|
+
architectureSummary: yamlOutput,
|
|
256
|
+
entityRegistry,
|
|
257
|
+
lastSyncedAt: new Date().toISOString(),
|
|
258
|
+
});
|
|
259
|
+
spinner.succeed(`${label} — ${chalk.green('done')}`);
|
|
260
|
+
return { slug: moduleSlug, status: 'success' };
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
spinner.fail(`${label} — ${chalk.red('failed')}`);
|
|
264
|
+
this.log(chalk.red(` Error: ${error.message}`));
|
|
265
|
+
return { error: error.message, slug: moduleSlug, status: 'failed' };
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
// Cleanup temp directory
|
|
269
|
+
if (tmpDir && existsSync(tmpDir)) {
|
|
270
|
+
try {
|
|
271
|
+
rmSync(tmpDir, { force: true, recursive: true });
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
// Ignore cleanup errors
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
detectBmadDocs(repoPath) {
|
|
280
|
+
const bmadDir = join(repoPath, '_bmad');
|
|
281
|
+
if (!existsSync(bmadDir))
|
|
282
|
+
return null;
|
|
283
|
+
try {
|
|
284
|
+
if (!statSync(bmadDir).isDirectory())
|
|
285
|
+
return null;
|
|
286
|
+
const files = readdirSync(bmadDir);
|
|
287
|
+
const archPatterns = [/^architecture.*\.md$/i, /^arch-.*\.md$/i];
|
|
288
|
+
for (const file of files) {
|
|
289
|
+
for (const pattern of archPatterns) {
|
|
290
|
+
if (pattern.test(file)) {
|
|
291
|
+
return join(bmadDir, file);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
// Ignore fs errors
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
runClaude(prompt, cwd) {
|
|
302
|
+
return new Promise((resolve, reject) => {
|
|
303
|
+
const TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
304
|
+
const child = spawn('claude', ['-p', prompt, '--output-format', 'text'], {
|
|
305
|
+
cwd,
|
|
306
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
307
|
+
timeout: TIMEOUT_MS,
|
|
308
|
+
});
|
|
309
|
+
let stdout = '';
|
|
310
|
+
let stderr = '';
|
|
311
|
+
child.stdout.on('data', (data) => {
|
|
312
|
+
stdout += data.toString();
|
|
313
|
+
});
|
|
314
|
+
child.stderr.on('data', (data) => {
|
|
315
|
+
stderr += data.toString();
|
|
316
|
+
});
|
|
317
|
+
child.on('error', (error) => {
|
|
318
|
+
reject(new Error(`Claude CLI failed to start: ${error.message}`));
|
|
319
|
+
});
|
|
320
|
+
child.on('close', (code) => {
|
|
321
|
+
if (code !== 0) {
|
|
322
|
+
reject(new Error(`Claude CLI exited with code ${code}: ${stderr.slice(0, 500)}`));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
// Strip markdown code fences if present
|
|
326
|
+
let output = stdout.trim();
|
|
327
|
+
if (output.startsWith('```yaml')) {
|
|
328
|
+
output = output.replace(/^```yaml\n?/, '').replace(/\n?```$/, '');
|
|
329
|
+
}
|
|
330
|
+
else if (output.startsWith('```')) {
|
|
331
|
+
output = output.replace(/^```\n?/, '').replace(/\n?```$/, '');
|
|
332
|
+
}
|
|
333
|
+
resolve(output);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
validateYaml(yamlString) {
|
|
338
|
+
const parsed = yaml.load(yamlString);
|
|
339
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
340
|
+
throw new Error('YAML must be a non-null object');
|
|
341
|
+
}
|
|
342
|
+
for (const key of REQUIRED_SUMMARY_KEYS) {
|
|
343
|
+
if (!(key in parsed)) {
|
|
344
|
+
throw new Error(`Missing required top-level key: '${key}'`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const mod = parsed.module;
|
|
348
|
+
if (!mod || typeof mod !== 'object' || !mod.name || !mod.description) {
|
|
349
|
+
throw new Error("'module' must have 'name' and 'description' string fields");
|
|
350
|
+
}
|
|
351
|
+
if (!Array.isArray(parsed.domains)) {
|
|
352
|
+
throw new Error("'domains' must be an array");
|
|
353
|
+
}
|
|
354
|
+
if (!parsed.patterns || typeof parsed.patterns !== 'object') {
|
|
355
|
+
throw new Error("'patterns' must be an object");
|
|
356
|
+
}
|
|
357
|
+
// entity_registry is optional but must be an array if present
|
|
358
|
+
if ('entity_registry' in parsed) {
|
|
359
|
+
if (!Array.isArray(parsed.entity_registry)) {
|
|
360
|
+
throw new Error("'entity_registry' must be an array");
|
|
361
|
+
}
|
|
362
|
+
for (let i = 0; i < parsed.entity_registry.length; i++) {
|
|
363
|
+
const entry = parsed.entity_registry[i];
|
|
364
|
+
if (!entry.name)
|
|
365
|
+
throw new Error(`entity_registry[${i}]: 'name' is required`);
|
|
366
|
+
if (!entry.type)
|
|
367
|
+
throw new Error(`entity_registry[${i}]: 'type' is required`);
|
|
368
|
+
if (!VALID_ENTITY_TYPES.includes(entry.type)) {
|
|
369
|
+
throw new Error(`entity_registry[${i}]: invalid type '${entry.type}'`);
|
|
370
|
+
}
|
|
371
|
+
if (!entry.path)
|
|
372
|
+
throw new Error(`entity_registry[${i}]: 'path' is required`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const techStack = parsed.tech_stack;
|
|
376
|
+
if (!techStack || typeof techStack !== 'object') {
|
|
377
|
+
throw new Error("'tech_stack' must be an object");
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
readGutEntities(repoPath) {
|
|
381
|
+
const gutConfigPath = join(repoPath, '.gut', 'config.json');
|
|
382
|
+
if (!existsSync(gutConfigPath))
|
|
383
|
+
return [];
|
|
384
|
+
try {
|
|
385
|
+
const content = readFileSync(gutConfigPath, 'utf-8');
|
|
386
|
+
const config = JSON.parse(content);
|
|
387
|
+
if (!config.entities || !Array.isArray(config.entities))
|
|
388
|
+
return [];
|
|
389
|
+
return config.entities
|
|
390
|
+
.filter((e) => e && typeof e === 'object' && e.name && e.type && e.path &&
|
|
391
|
+
VALID_ENTITY_TYPES.includes(e.type))
|
|
392
|
+
.map((e) => ({
|
|
393
|
+
name: e.name,
|
|
394
|
+
path: e.path,
|
|
395
|
+
type: e.type,
|
|
396
|
+
...(e.repository && typeof e.repository === 'string' ? { repository: e.repository } : {}),
|
|
397
|
+
...(e.description && typeof e.description === 'string' ? { description: e.description } : {}),
|
|
398
|
+
}));
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
mergeEntityRegistries(claudeEntities, gutEntities) {
|
|
405
|
+
const gutNameSet = new Set(gutEntities.map(e => e.name.toLowerCase()));
|
|
406
|
+
const uniqueClaude = claudeEntities.filter(e => !gutNameSet.has(e.name.toLowerCase()));
|
|
407
|
+
return [...gutEntities, ...uniqueClaude];
|
|
408
|
+
}
|
|
409
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class ModuleUnlink extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static examples: string[];
|
|
5
|
+
static flags: {
|
|
6
|
+
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
originSlug: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
targetSlug: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
|
|
4
|
+
export default class ModuleUnlink extends Command {
|
|
5
|
+
static description = 'Remove a dependency link between two modules';
|
|
6
|
+
static examples = [
|
|
7
|
+
'<%= config.bin %> <%= command.id %> --originSlug="my-frontend" --targetSlug="my-api"',
|
|
8
|
+
];
|
|
9
|
+
static flags = {
|
|
10
|
+
domain: Flags.string({
|
|
11
|
+
char: 'd',
|
|
12
|
+
description: 'Tenant domain (for multi-domain setups)',
|
|
13
|
+
}),
|
|
14
|
+
originSlug: Flags.string({
|
|
15
|
+
description: 'Slug of the module to remove the dependency from',
|
|
16
|
+
required: true,
|
|
17
|
+
}),
|
|
18
|
+
targetSlug: Flags.string({
|
|
19
|
+
description: 'Slug of the dependency to remove',
|
|
20
|
+
required: true,
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
async run() {
|
|
24
|
+
const { flags } = await this.parse(ModuleUnlink);
|
|
25
|
+
if (flags.originSlug === flags.targetSlug) {
|
|
26
|
+
this.log(chalk.red('❌ Error: originSlug and targetSlug must be different'));
|
|
27
|
+
this.exit(1);
|
|
28
|
+
}
|
|
29
|
+
this.log(chalk.yellow(`🔗 Unlinking ${flags.originSlug} -/-> ${flags.targetSlug}...`));
|
|
30
|
+
try {
|
|
31
|
+
const service = new HyperdriveSigV4Service(flags.domain);
|
|
32
|
+
const result = await service.moduleUnlink({
|
|
33
|
+
originSlug: flags.originSlug,
|
|
34
|
+
targetSlug: flags.targetSlug,
|
|
35
|
+
});
|
|
36
|
+
this.log('');
|
|
37
|
+
this.log(chalk.green(`✅ Module unlink successful!`));
|
|
38
|
+
this.log('');
|
|
39
|
+
this.log(chalk.gray(`Removed dependency: ${chalk.cyan(flags.originSlug)} no longer depends on ${chalk.cyan(flags.targetSlug)}`));
|
|
40
|
+
this.log(chalk.gray(`Deployment waves will be recalculated on next mission launch.`));
|
|
41
|
+
this.log('');
|
|
42
|
+
// Show remaining targets if any
|
|
43
|
+
const targets = result.targets || {};
|
|
44
|
+
const remaining = Object.keys(targets);
|
|
45
|
+
if (remaining.length > 0) {
|
|
46
|
+
this.log(chalk.gray('Remaining dependencies:'));
|
|
47
|
+
for (const target of remaining) {
|
|
48
|
+
const params = typeof targets[target] === 'object' ? Object.keys(targets[target]).join(', ') : '';
|
|
49
|
+
this.log(chalk.gray(` → ${chalk.cyan(target)} (${params})`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
this.log(chalk.gray('No remaining dependencies for this module.'));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.log('');
|
|
58
|
+
this.log(chalk.red('❌ Error unlinking modules'));
|
|
59
|
+
if (error.response?.data?.message) {
|
|
60
|
+
this.log(chalk.red('Error: ' + error.response.data.message));
|
|
61
|
+
}
|
|
62
|
+
else if (error.message) {
|
|
63
|
+
this.log(chalk.red('Error: ' + error.message));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.log(chalk.red('Error: ' + error.toString()));
|
|
67
|
+
}
|
|
68
|
+
if (error.response?.status === 404) {
|
|
69
|
+
this.log('');
|
|
70
|
+
this.log(chalk.yellow('💡 Tip: Make sure the link exists'));
|
|
71
|
+
this.log(chalk.gray(` Check with: hd module get --slug=${flags.originSlug}`));
|
|
72
|
+
}
|
|
73
|
+
this.log('');
|
|
74
|
+
this.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -10,18 +10,28 @@ export default class ModuleUpdate extends Command {
|
|
|
10
10
|
buildRuntimeVersion: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
11
|
ciService: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
defaultBranch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
|
+
dependsOn: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
14
|
deploymentStrategy: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
|
+
dockerBuild: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
|
+
'deploy-command': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
17
|
domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
18
|
framework: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
19
|
installCommand: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
|
+
postDeployCommand: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
|
+
postDeployFailureMode: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
22
|
+
'module-type': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
23
|
name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
24
|
routeDiscovery: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
25
|
+
'runtime-mode': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
26
|
runCommand: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
27
|
runtime: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
28
|
+
'runtime-config': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
29
|
runtimeVersion: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
30
|
+
show: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
22
31
|
slug: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
23
32
|
sourceDirectory: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
24
33
|
sourceLocation: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
34
|
+
subdomain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
25
35
|
};
|
|
26
36
|
run(): Promise<void>;
|
|
27
37
|
}
|