@hyperdrive.bot/cli 1.0.13 → 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.
Files changed (157) hide show
  1. package/README.md +1495 -474
  2. package/dist/commands/deploy.d.ts +18 -0
  3. package/dist/commands/deploy.js +239 -0
  4. package/dist/commands/deployment/create.js +10 -2
  5. package/dist/commands/domain/{switch.d.ts → set-production.d.ts} +1 -1
  6. package/dist/commands/domain/set-production.js +27 -0
  7. package/dist/commands/git/list-open-prs.d.ts +12 -0
  8. package/dist/commands/git/list-open-prs.js +87 -0
  9. package/dist/commands/hook/add.d.ts +22 -0
  10. package/dist/commands/hook/add.js +299 -0
  11. package/dist/commands/hook/list.d.ts +11 -0
  12. package/dist/commands/hook/list.js +111 -0
  13. package/dist/commands/hook/logs.d.ts +13 -0
  14. package/dist/commands/hook/logs.js +124 -0
  15. package/dist/commands/hook/remove.d.ts +12 -0
  16. package/dist/commands/hook/remove.js +115 -0
  17. package/dist/commands/hook/toggle.d.ts +12 -0
  18. package/dist/commands/hook/toggle.js +125 -0
  19. package/dist/commands/init.d.ts +1 -1
  20. package/dist/commands/init.js +49 -9
  21. package/dist/commands/module/bindings.d.ts +14 -0
  22. package/dist/commands/module/bindings.js +125 -0
  23. package/dist/commands/module/create.d.ts +3 -0
  24. package/dist/commands/module/create.js +156 -78
  25. package/dist/commands/module/list.d.ts +1 -0
  26. package/dist/commands/module/list.js +22 -1
  27. package/dist/commands/module/sync.d.ts +29 -0
  28. package/dist/commands/module/sync.js +409 -0
  29. package/dist/commands/module/unlink.d.ts +11 -0
  30. package/dist/commands/module/unlink.js +77 -0
  31. package/dist/commands/module/update.d.ts +10 -0
  32. package/dist/commands/module/update.js +168 -5
  33. package/dist/commands/network/discover.d.ts +12 -0
  34. package/dist/commands/network/discover.js +210 -0
  35. package/dist/commands/network/get.d.ts +13 -0
  36. package/dist/commands/network/get.js +90 -0
  37. package/dist/commands/{auth/logout.d.ts → network/list.d.ts} +2 -9
  38. package/dist/commands/network/list.js +71 -0
  39. package/dist/commands/network/register.d.ts +16 -0
  40. package/dist/commands/network/register.js +144 -0
  41. package/dist/commands/parameter/sync.d.ts +13 -0
  42. package/dist/commands/parameter/sync.js +69 -1
  43. package/dist/commands/project/sync.d.ts +5 -11
  44. package/dist/commands/project/sync.js +12 -381
  45. package/dist/commands/seed.d.ts +93 -0
  46. package/dist/commands/seed.js +324 -0
  47. package/dist/commands/service/backup.d.ts +17 -0
  48. package/dist/commands/service/backup.js +156 -0
  49. package/dist/commands/service/backups.d.ts +14 -0
  50. package/dist/commands/service/backups.js +110 -0
  51. package/dist/commands/service/bind.d.ts +16 -0
  52. package/dist/commands/service/bind.js +106 -0
  53. package/dist/commands/service/bindings.d.ts +13 -0
  54. package/dist/commands/service/bindings.js +78 -0
  55. package/dist/commands/service/clone.d.ts +19 -0
  56. package/dist/commands/service/clone.js +153 -0
  57. package/dist/commands/service/create.d.ts +16 -0
  58. package/dist/commands/service/create.js +212 -0
  59. package/dist/commands/service/get.d.ts +13 -0
  60. package/dist/commands/service/get.js +97 -0
  61. package/dist/commands/service/list.d.ts +12 -0
  62. package/dist/commands/service/list.js +86 -0
  63. package/dist/commands/service/register.d.ts +21 -0
  64. package/dist/commands/service/register.js +215 -0
  65. package/dist/commands/service/restore.d.ts +19 -0
  66. package/dist/commands/service/restore.js +158 -0
  67. package/dist/commands/service/seed.d.ts +17 -0
  68. package/dist/commands/service/seed.js +173 -0
  69. package/dist/commands/service/templates.d.ts +10 -0
  70. package/dist/commands/service/templates.js +66 -0
  71. package/dist/commands/service/unbind.d.ts +15 -0
  72. package/dist/commands/service/unbind.js +74 -0
  73. package/dist/commands/stage/create.d.ts +23 -0
  74. package/dist/commands/stage/create.js +145 -6
  75. package/dist/commands/stage/delete.d.ts +11 -0
  76. package/dist/commands/stage/delete.js +85 -0
  77. package/dist/commands/stage/deploy.d.ts +34 -0
  78. package/dist/commands/stage/deploy.js +294 -0
  79. package/dist/commands/stage/ensure-branches.d.ts +23 -0
  80. package/dist/commands/stage/ensure-branches.js +101 -0
  81. package/dist/commands/stage/list.js +4 -0
  82. package/dist/commands/stage/status.d.ts +14 -0
  83. package/dist/commands/stage/status.js +100 -0
  84. package/dist/commands/{jira → tracker}/connect.js +32 -23
  85. package/dist/commands/tracker/hook/add.d.ts +25 -0
  86. package/dist/commands/tracker/hook/add.js +284 -0
  87. package/dist/commands/{jira → tracker}/hook/list.js +20 -11
  88. package/dist/commands/{jira/hook/add.d.ts → tracker/hook/logs.d.ts} +2 -3
  89. package/dist/commands/tracker/hook/logs.js +126 -0
  90. package/dist/commands/{jira → tracker}/hook/remove.js +9 -8
  91. package/dist/commands/{jira → tracker}/hook/toggle.js +14 -12
  92. package/dist/commands/tracker/project/init.d.ts +17 -0
  93. package/dist/commands/tracker/project/init.js +178 -0
  94. package/dist/commands/tracker/project/link-module.d.ts +17 -0
  95. package/dist/commands/tracker/project/link-module.js +287 -0
  96. package/dist/commands/tracker/project/list-modules.d.ts +11 -0
  97. package/dist/commands/tracker/project/list-modules.js +117 -0
  98. package/dist/commands/tracker/project/list.d.ts +10 -0
  99. package/dist/commands/tracker/project/list.js +90 -0
  100. package/dist/commands/tracker/project/status.d.ts +13 -0
  101. package/dist/commands/tracker/project/status.js +168 -0
  102. package/dist/commands/tracker/project/unlink-module.d.ts +13 -0
  103. package/dist/commands/tracker/project/unlink-module.js +251 -0
  104. package/dist/commands/{jira → tracker}/status.js +3 -3
  105. package/dist/lib/ensure-branches.d.ts +53 -0
  106. package/dist/lib/ensure-branches.js +149 -0
  107. package/dist/lib/git-providers/github.d.ts +16 -0
  108. package/dist/lib/git-providers/github.js +157 -0
  109. package/dist/lib/git-providers/gitlab.d.ts +16 -0
  110. package/dist/lib/git-providers/gitlab.js +148 -0
  111. package/dist/lib/git-providers/index.d.ts +67 -0
  112. package/dist/lib/git-providers/index.js +39 -0
  113. package/dist/lib/lambda-warmer.d.ts +106 -0
  114. package/dist/lib/lambda-warmer.js +189 -0
  115. package/dist/services/hyperdrive-sigv4.d.ts +359 -5
  116. package/dist/services/hyperdrive-sigv4.js +177 -12
  117. package/dist/utils/hook-flow.d.ts +60 -3
  118. package/dist/utils/hook-flow.js +437 -2
  119. package/dist/utils/hook-normalize.d.ts +6 -0
  120. package/dist/utils/hook-normalize.js +33 -0
  121. package/dist/utils/lifecycle-poller.d.ts +32 -0
  122. package/dist/utils/lifecycle-poller.js +72 -0
  123. package/dist/utils/retry.d.ts +43 -0
  124. package/dist/utils/retry.js +88 -0
  125. package/dist/utils/summary-display.js +1 -1
  126. package/dist/utils/tracker-project-flow.d.ts +84 -0
  127. package/dist/utils/tracker-project-flow.js +564 -0
  128. package/package.json +35 -7
  129. package/dist/commands/auth/login.d.ts +0 -16
  130. package/dist/commands/auth/login.js +0 -179
  131. package/dist/commands/auth/logout.js +0 -116
  132. package/dist/commands/auth/refresh.d.ts +0 -6
  133. package/dist/commands/auth/refresh.js +0 -66
  134. package/dist/commands/auth/status.d.ts +0 -6
  135. package/dist/commands/auth/status.js +0 -63
  136. package/dist/commands/config/get.d.ts +0 -9
  137. package/dist/commands/config/get.js +0 -37
  138. package/dist/commands/config/set.d.ts +0 -10
  139. package/dist/commands/config/set.js +0 -48
  140. package/dist/commands/config/show.d.ts +0 -6
  141. package/dist/commands/config/show.js +0 -10
  142. package/dist/commands/domain/current.d.ts +0 -6
  143. package/dist/commands/domain/current.js +0 -18
  144. package/dist/commands/domain/list.d.ts +0 -6
  145. package/dist/commands/domain/list.js +0 -42
  146. package/dist/commands/domain/switch.js +0 -40
  147. package/dist/commands/jira/hook/add.js +0 -147
  148. package/dist/services/tenant-service.d.ts +0 -127
  149. package/dist/services/tenant-service.js +0 -396
  150. package/dist/utils/auth-flow.d.ts +0 -147
  151. package/dist/utils/auth-flow.js +0 -479
  152. package/oclif.manifest.json +0 -3519
  153. /package/dist/commands/{jira → tracker}/connect.d.ts +0 -0
  154. /package/dist/commands/{jira → tracker}/hook/list.d.ts +0 -0
  155. /package/dist/commands/{jira → tracker}/hook/remove.d.ts +0 -0
  156. /package/dist/commands/{jira → tracker}/hook/toggle.d.ts +0 -0
  157. /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
  }