@hyperdrive.bot/cli 1.0.13 → 1.0.17

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 +4526 -780
  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 +41 -13
  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
@@ -1,78 +1,20 @@
1
1
  /**
2
- * Project Sync Command
2
+ * Project Sync Command (DEPRECATED)
3
3
  *
4
- * Generates structured architecture summaries for repos in a project.
5
- * For each repo: clone/pull detect _bmad or invoke Claude validate YAML → upload S3 → update DynamoDB.
4
+ * This command has been replaced by `hd module sync`.
5
+ * Kept in place to show deprecation warning to users of the old workflow.
6
6
  */
7
7
  import { Args, Command, Flags } from '@oclif/core';
8
8
  import chalk from 'chalk';
9
- import { execFileSync, spawn } from 'child_process';
10
- import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync } from 'fs';
11
- import yaml from 'js-yaml';
12
- import ora from 'ora';
13
- import { tmpdir } from 'os';
14
- import { join } from 'path';
15
- import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
16
- // ============================================================================
17
- // Architecture Summary YAML Schema & Validation (inline for CLI — no server deps)
18
- // ============================================================================
19
- const VALID_ENTITY_TYPES = ['client', 'company', 'delivery', 'initiative', 'module', 'service', 'system', 'tool'];
20
- const REQUIRED_SUMMARY_KEYS = ['repo', 'domains', 'patterns', 'modules', 'entity_registry', 'tech_stack'];
21
- const ARCHITECTURE_YAML_SCHEMA = `repo:
22
- name: "<repo-name>"
23
- description: "<one-line description>"
24
-
25
- domains:
26
- - name: "<domain-name>"
27
- modules: [<module1>, <module2>]
28
- key_files: [<path1>, <path2>]
29
-
30
- patterns:
31
- handler_pattern: "<glob pattern for handlers>"
32
- service_pattern: "<glob pattern for services>"
33
- module_config: "<glob pattern for module config>"
34
- test_pattern: "<glob pattern for tests>"
35
-
36
- modules: [<module1>, <module2>, ...]
37
-
38
- entity_registry:
39
- - { name: "<entity-name>", type: "<client|company|delivery|initiative|module|service|system|tool>", path: "<relative-path>" }
40
-
41
- tech_stack:
42
- runtime: "<e.g. nodejs-22>"
43
- framework: "<e.g. serverless-v4>"
44
- language: "<e.g. typescript>"
45
- database: "<e.g. dynamodb>"
46
- infrastructure: "<e.g. aws-lambda>"`;
47
- const ARCHITECTURE_ANALYSIS_PROMPT = `Analyze this code repository and produce a structured architecture summary in YAML format. Output ONLY valid YAML — no markdown fences, no explanations, no commentary.
48
-
49
- Analyze the following:
50
- 1. Directory structure and key files
51
- 2. Functional domains (groups of related modules)
52
- 3. Code patterns (handler paths, service paths, test paths, module config paths)
53
- 4. List of modules/packages
54
- 5. Entity registry (named entities with type: client|company|delivery|initiative|module|service|system|tool)
55
- 6. Technology stack (runtime, framework, language, database, infrastructure)
56
-
57
- Required YAML schema:
58
- ${ARCHITECTURE_YAML_SCHEMA}
59
-
60
- Now analyze the repository and produce the YAML summary.`;
61
- const VALID_REPO_NAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
62
- const MAX_BMAD_DOC_SIZE = 100 * 1024; // 100KB cap for _bmad docs (SEC-002 mitigation)
63
9
  export default class ProjectSync extends Command {
64
10
  static args = {
65
11
  project: Args.string({
66
- description: 'Project slug or ID',
67
- required: true,
12
+ description: 'Project slug or ID (deprecated)',
13
+ required: false,
68
14
  }),
69
15
  };
70
- static description = 'Generate architecture summaries for project repos via Claude analysis';
71
- static examples = [
72
- '<%= config.bin %> project sync my-project',
73
- '<%= config.bin %> project sync my-project --repo api',
74
- '<%= config.bin %> project sync my-project --json',
75
- ];
16
+ static description = '[DEPRECATED] Use "module sync" instead';
17
+ static hidden = true;
76
18
  static flags = {
77
19
  domain: Flags.string({
78
20
  char: 'd',
@@ -86,321 +28,10 @@ export default class ProjectSync extends Command {
86
28
  }),
87
29
  };
88
30
  async run() {
89
- const { args, flags } = await this.parse(ProjectSync);
90
- // Authenticate
91
- let service;
92
- const authSpinner = ora('Checking authentication...').start();
93
- try {
94
- service = new HyperdriveSigV4Service(flags.domain);
95
- authSpinner.succeed('Authenticated');
96
- }
97
- catch (error) {
98
- authSpinner.fail('Not authenticated');
99
- this.error(`${error.message}\n\nPlease authenticate first with: ${chalk.cyan('hd auth login')}`);
100
- }
101
- // Resolve project by slug
102
- const projectSpinner = ora(`Resolving project ${chalk.cyan(args.project)}...`).start();
103
- let project;
104
- try {
105
- const result = await service.moduleGet({ slug: args.project });
106
- project = { name: result.name || args.project, projectId: result.projectId || args.project, slug: result.slug || args.project };
107
- projectSpinner.succeed(`Project: ${chalk.cyan(project.name)} (${project.slug})`);
108
- }
109
- catch (error) {
110
- projectSpinner.fail('Project not found');
111
- this.error(`Could not find project "${args.project}": ${error.response?.data?.message || error.message}`);
112
- }
113
- // List repos
114
- const repoSpinner = ora('Fetching repositories...').start();
115
- let repos;
116
- try {
117
- repos = await service.projectListRepos(project.projectId);
118
- repoSpinner.succeed(`Found ${repos.length} repo(s)`);
119
- }
120
- catch (error) {
121
- repoSpinner.fail('Failed to fetch repos');
122
- this.error(`Could not list repos: ${error.response?.data?.message || error.message}`);
123
- }
124
- // Filter by --repo flag
125
- if (flags.repo) {
126
- repos = repos.filter(r => r.name === flags.repo);
127
- if (repos.length === 0) {
128
- this.error(`No repo named "${flags.repo}" found in project "${project.slug}"`);
129
- }
130
- }
131
- if (repos.length === 0) {
132
- this.log(chalk.yellow('No repositories to sync.'));
133
- return;
134
- }
135
- this.log('');
136
- this.log(chalk.blue(`Syncing ${repos.length} repo(s)...`));
137
- this.log('');
138
- // Process each repo
139
- const results = [];
140
- for (let i = 0; i < repos.length; i++) {
141
- const repo = repos[i];
142
- const prefix = chalk.dim(`[${i + 1}/${repos.length}]`);
143
- const result = await this.syncRepo(service, project, repo, prefix);
144
- results.push(result);
145
- }
146
- // Summary
147
- this.log('');
148
- const succeeded = results.filter(r => r.success).length;
149
- const failed = results.filter(r => !r.success).length;
150
- if (flags.json) {
151
- this.log(JSON.stringify({ failed, project: project.slug, results, succeeded, total: repos.length }, null, 2));
152
- return;
153
- }
154
- if (failed === 0) {
155
- this.log(chalk.green(`Synced ${succeeded}/${repos.length} repos (0 failed)`));
156
- }
157
- else {
158
- this.log(chalk.yellow(`Synced ${succeeded}/${repos.length} repos (${failed} failed)`));
159
- for (const r of results.filter(r => !r.success)) {
160
- this.log(chalk.red(` ${r.name}: ${r.error}`));
161
- }
162
- }
163
- }
164
- async syncRepo(service, project, repo, prefix) {
165
- const spinner = ora(`${prefix} ${chalk.cyan(repo.name)} — cloning...`).start();
166
- let tmpDir = null;
167
- try {
168
- // Step 1: Validate repo name and clone
169
- if (!VALID_REPO_NAME_REGEX.test(repo.name)) {
170
- throw new Error(`Invalid repo name "${repo.name}" — must match ${VALID_REPO_NAME_REGEX}`);
171
- }
172
- tmpDir = mkdtempSync(join(tmpdir(), `hd-sync-${repo.name}-`));
173
- const clonePath = join(tmpDir, repo.name);
174
- try {
175
- execFileSync('git', ['clone', '--depth', '1', '--branch', repo.defaultBranch, repo.gitRemote, clonePath], {
176
- stdio: 'pipe',
177
- timeout: 120_000, // 2 min clone timeout
178
- });
179
- }
180
- catch (cloneError) {
181
- throw new Error(`git clone failed: ${cloneError.stderr?.toString() || cloneError.message}`);
182
- }
183
- // Step 2: Detect _bmad or generate via Claude
184
- spinner.text = `${prefix} ${chalk.cyan(repo.name)} — analyzing...`;
185
- let yamlOutput = null;
186
- let lastError = null;
187
- const MAX_ATTEMPTS = 3;
188
- // Check for _bmad docs
189
- const bmadDocPath = this.detectBmadDocs(clonePath);
190
- for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
191
- try {
192
- if (bmadDocPath) {
193
- // Use _bmad doc with simpler restructure prompt
194
- const docStat = statSync(bmadDocPath);
195
- if (docStat.size > MAX_BMAD_DOC_SIZE) {
196
- throw new Error(`_bmad doc exceeds ${MAX_BMAD_DOC_SIZE / 1024}KB size limit (${Math.round(docStat.size / 1024)}KB) — skipping to full analysis`);
197
- }
198
- const docContent = readFileSync(bmadDocPath, 'utf-8');
199
- 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>`;
200
- yamlOutput = await this.runClaude(prompt, clonePath);
201
- }
202
- else {
203
- // Full codebase analysis
204
- yamlOutput = await this.runClaude(ARCHITECTURE_ANALYSIS_PROMPT, clonePath);
205
- }
206
- // Validate
207
- this.validateYaml(yamlOutput);
208
- break; // Validation passed
209
- }
210
- catch (error) {
211
- lastError = error.message;
212
- this.log(chalk.dim(` ${repo.name} attempt ${attempt}/${MAX_ATTEMPTS} failed: ${lastError}`));
213
- yamlOutput = null;
214
- if (attempt === MAX_ATTEMPTS) {
215
- throw new Error(`Validation failed after ${MAX_ATTEMPTS} attempts: ${lastError}`);
216
- }
217
- }
218
- }
219
- if (!yamlOutput) {
220
- throw new Error(`Generation failed: ${lastError}`);
221
- }
222
- // Step 2b: Extract entity registry from validated YAML and merge with gut config
223
- spinner.text = `${prefix} ${chalk.cyan(repo.name)} — extracting entities...`;
224
- const parsedYaml = yaml.load(yamlOutput);
225
- let entityRegistry = (parsedYaml.entity_registry || []);
226
- // Check for .gut/config.json and merge
227
- const gutEntities = this.readGutEntities(clonePath);
228
- if (gutEntities.length > 0) {
229
- entityRegistry = this.mergeEntityRegistries(entityRegistry, gutEntities);
230
- this.log(chalk.dim(` ${repo.name}: merged ${entityRegistry.length} entities (${gutEntities.length} from gut)`));
231
- }
232
- else if (entityRegistry.length > 0) {
233
- this.log(chalk.dim(` ${repo.name}: found ${entityRegistry.length} entities from analysis`));
234
- }
235
- // Step 3: Upload to S3 via API
236
- spinner.text = `${prefix} ${chalk.cyan(repo.name)} — uploading...`;
237
- // The API handles S3 upload and DynamoDB update — call updateRepo with the summary content
238
- await service.projectUpdateRepo(project.projectId, repo.repoId, {
239
- architectureSummary: yamlOutput,
240
- lastSyncedAt: new Date().toISOString(),
241
- });
242
- // Step 4: Update entity registry via dedicated endpoint
243
- if (entityRegistry.length > 0) {
244
- try {
245
- await service.projectUpdateEntities(project.projectId, repo.repoId, entityRegistry);
246
- }
247
- catch (entityError) {
248
- this.log(chalk.yellow(` ${repo.name}: entity registry update failed: ${entityError.message}`));
249
- }
250
- }
251
- spinner.succeed(`${prefix} ${chalk.cyan(repo.name)} — ${chalk.green('done')}`);
252
- return { name: repo.name, success: true };
253
- }
254
- catch (error) {
255
- spinner.fail(`${prefix} ${chalk.cyan(repo.name)} — ${chalk.red('failed')}`);
256
- this.log(chalk.red(` Error: ${error.message}`));
257
- return { error: error.message, name: repo.name, success: false };
258
- }
259
- finally {
260
- // Cleanup temp directory
261
- if (tmpDir && existsSync(tmpDir)) {
262
- try {
263
- rmSync(tmpDir, { force: true, recursive: true });
264
- }
265
- catch {
266
- // Ignore cleanup errors
267
- }
268
- }
269
- }
270
- }
271
- detectBmadDocs(repoPath) {
272
- const bmadDir = join(repoPath, '_bmad');
273
- if (!existsSync(bmadDir))
274
- return null;
275
- try {
276
- if (!statSync(bmadDir).isDirectory())
277
- return null;
278
- const files = readdirSync(bmadDir);
279
- const archPatterns = [/^architecture.*\.md$/i, /^arch-.*\.md$/i];
280
- for (const file of files) {
281
- for (const pattern of archPatterns) {
282
- if (pattern.test(file)) {
283
- return join(bmadDir, file);
284
- }
285
- }
286
- }
287
- }
288
- catch {
289
- // Ignore fs errors
290
- }
291
- return null;
292
- }
293
- runClaude(prompt, cwd) {
294
- return new Promise((resolve, reject) => {
295
- const TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
296
- const child = spawn('claude', ['-p', prompt, '--output-format', 'text'], {
297
- cwd,
298
- stdio: ['pipe', 'pipe', 'pipe'],
299
- timeout: TIMEOUT_MS,
300
- });
301
- let stdout = '';
302
- let stderr = '';
303
- child.stdout.on('data', (data) => {
304
- stdout += data.toString();
305
- });
306
- child.stderr.on('data', (data) => {
307
- stderr += data.toString();
308
- });
309
- child.on('error', (error) => {
310
- reject(new Error(`Claude CLI failed to start: ${error.message}`));
311
- });
312
- child.on('close', (code) => {
313
- if (code !== 0) {
314
- reject(new Error(`Claude CLI exited with code ${code}: ${stderr.slice(0, 500)}`));
315
- return;
316
- }
317
- // Strip markdown code fences if present
318
- let output = stdout.trim();
319
- if (output.startsWith('```yaml')) {
320
- output = output.replace(/^```yaml\n?/, '').replace(/\n?```$/, '');
321
- }
322
- else if (output.startsWith('```')) {
323
- output = output.replace(/^```\n?/, '').replace(/\n?```$/, '');
324
- }
325
- resolve(output);
326
- });
327
- });
328
- }
329
- validateYaml(yamlString) {
330
- const parsed = yaml.load(yamlString);
331
- if (!parsed || typeof parsed !== 'object') {
332
- throw new Error('YAML must be a non-null object');
333
- }
334
- for (const key of REQUIRED_SUMMARY_KEYS) {
335
- if (!(key in parsed)) {
336
- throw new Error(`Missing required top-level key: '${key}'`);
337
- }
338
- }
339
- const repo = parsed.repo;
340
- if (!repo || typeof repo !== 'object' || !repo.name || !repo.description) {
341
- throw new Error("'repo' must have 'name' and 'description' string fields");
342
- }
343
- if (!Array.isArray(parsed.domains)) {
344
- throw new Error("'domains' must be an array");
345
- }
346
- if (!parsed.patterns || typeof parsed.patterns !== 'object') {
347
- throw new Error("'patterns' must be an object");
348
- }
349
- if (!Array.isArray(parsed.modules)) {
350
- throw new Error("'modules' must be an array");
351
- }
352
- if (!Array.isArray(parsed.entity_registry)) {
353
- throw new Error("'entity_registry' must be an array");
354
- }
355
- for (let i = 0; i < parsed.entity_registry.length; i++) {
356
- const entry = parsed.entity_registry[i];
357
- if (!entry.name)
358
- throw new Error(`entity_registry[${i}]: 'name' is required`);
359
- if (!entry.type)
360
- throw new Error(`entity_registry[${i}]: 'type' is required`);
361
- if (!VALID_ENTITY_TYPES.includes(entry.type)) {
362
- throw new Error(`entity_registry[${i}]: invalid type '${entry.type}'`);
363
- }
364
- if (!entry.path)
365
- throw new Error(`entity_registry[${i}]: 'path' is required`);
366
- }
367
- const techStack = parsed.tech_stack;
368
- if (!techStack || typeof techStack !== 'object') {
369
- throw new Error("'tech_stack' must be an object");
370
- }
371
- for (const field of ['runtime', 'framework', 'language', 'database', 'infrastructure']) {
372
- if (!techStack[field]) {
373
- throw new Error(`tech_stack.${field} is required`);
374
- }
375
- }
376
- }
377
- readGutEntities(repoPath) {
378
- const gutConfigPath = join(repoPath, '.gut', 'config.json');
379
- if (!existsSync(gutConfigPath))
380
- return [];
381
- try {
382
- const content = readFileSync(gutConfigPath, 'utf-8');
383
- const config = JSON.parse(content);
384
- if (!config.entities || !Array.isArray(config.entities))
385
- return [];
386
- return config.entities
387
- .filter((e) => e && typeof e === 'object' && e.name && e.type && e.path &&
388
- VALID_ENTITY_TYPES.includes(e.type))
389
- .map((e) => ({
390
- name: e.name,
391
- type: e.type,
392
- path: e.path,
393
- ...(e.repository && typeof e.repository === 'string' ? { repository: e.repository } : {}),
394
- ...(e.description && typeof e.description === 'string' ? { description: e.description } : {}),
395
- }));
396
- }
397
- catch {
398
- return [];
399
- }
400
- }
401
- mergeEntityRegistries(claudeEntities, gutEntities) {
402
- const gutNameSet = new Set(gutEntities.map(e => e.name.toLowerCase()));
403
- const uniqueClaude = claudeEntities.filter(e => !gutNameSet.has(e.name.toLowerCase()));
404
- return [...gutEntities, ...uniqueClaude];
31
+ this.warn(`${chalk.yellow("⚠️ 'hd project sync' is deprecated.")} Use ${chalk.cyan("'hd module sync {module}'")} instead.\n\n` +
32
+ `Examples:\n` +
33
+ ` ${chalk.cyan('hd module sync my-module')} Sync a single module\n` +
34
+ ` ${chalk.cyan('hd module sync --all')} Sync all modules\n` +
35
+ ` ${chalk.cyan('hd module sync my-module --json')} JSON output\n`);
405
36
  }
406
37
  }
@@ -0,0 +1,93 @@
1
+ import { Command } from '@oclif/core';
2
+ /**
3
+ * Context passed to every per-app seed script function.
4
+ *
5
+ * Apps own per-app schema knowledge — this CLI is the orchestrator. Each app
6
+ * declares its seed phases by exporting a default object from
7
+ * `apps/<app>/scripts/seed.ts` shaped like {@link AppSeedScript}.
8
+ */
9
+ export interface AppSeedContext {
10
+ /** Stage slug (e.g. `feat-e2e-expansion`). */
11
+ stage: string;
12
+ /** Source Postgres schema to copy data from (e.g. `public`). */
13
+ fromSchema: string;
14
+ /** Target Postgres schema to migrate + seed (defaults to `stage`). */
15
+ toSchema: string;
16
+ /** Per-stage CloudFront hostname for the FE Lambda Container (used for `professores.domain` etc.). */
17
+ cfDomain: string;
18
+ /** App slug currently running (one of `--apps`). */
19
+ app: string;
20
+ }
21
+ /** Result of a single phase. `sql` empty string => phase is a no-op for this app. */
22
+ export interface AppSeedPhaseResult {
23
+ sql: string;
24
+ /** Optional human-readable description for `--dry-run` output. */
25
+ description?: string;
26
+ }
27
+ /**
28
+ * Contract every app's `apps/<app>/scripts/seed.ts` must export as `default`.
29
+ *
30
+ * Each phase returns SQL that the orchestrator submits to the `hd service seed`
31
+ * Lambda (in-VPC psql runner). Apps that don't need a phase should return
32
+ * `{ sql: '' }`.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * // apps/core/scripts/seed.ts
37
+ * import type { AppSeedContext, AppSeedScript } from '@hyperdrive.bot/cli/seed'
38
+ *
39
+ * const seed: AppSeedScript = {
40
+ * async runMigrations(ctx) {
41
+ * return { sql: `CREATE SCHEMA IF NOT EXISTS "${ctx.toSchema}"; ...` }
42
+ * },
43
+ * async copyData(ctx) {
44
+ * const tables = ['alunos', 'professores', 'cursos']
45
+ * const sql = tables.map(t =>
46
+ * `INSERT INTO "${ctx.toSchema}"."${t}" SELECT * FROM "${ctx.fromSchema}"."${t}";`
47
+ * ).join('\n')
48
+ * return { sql }
49
+ * },
50
+ * async resetSequences(ctx) {
51
+ * return { sql: `SELECT setval(pg_get_serial_sequence('${ctx.toSchema}.alunos','id'), COALESCE(MAX(id),1)) FROM "${ctx.toSchema}".alunos;` }
52
+ * },
53
+ * async seedFixtures(ctx) {
54
+ * return { sql: `UPDATE "${ctx.toSchema}".professores SET domain='${ctx.cfDomain}' WHERE id=9901;` }
55
+ * },
56
+ * }
57
+ * export default seed
58
+ * ```
59
+ */
60
+ export interface AppSeedScript {
61
+ runMigrations?(ctx: AppSeedContext): Promise<AppSeedPhaseResult> | AppSeedPhaseResult;
62
+ copyData?(ctx: AppSeedContext): Promise<AppSeedPhaseResult> | AppSeedPhaseResult;
63
+ resetSequences?(ctx: AppSeedContext): Promise<AppSeedPhaseResult> | AppSeedPhaseResult;
64
+ seedFixtures?(ctx: AppSeedContext): Promise<AppSeedPhaseResult> | AppSeedPhaseResult;
65
+ }
66
+ export default class Seed extends Command {
67
+ static description: string;
68
+ static examples: string[];
69
+ static flags: {
70
+ stage: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
71
+ apps: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
72
+ 'from-schema': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
73
+ 'to-schema': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
74
+ 'cf-domain': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
75
+ service: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
76
+ 'apps-dir': import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
77
+ 'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
78
+ 'allow-production': import("@oclif/core/interfaces").BooleanFlag<boolean>;
79
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
80
+ 'no-wait': import("@oclif/core/interfaces").BooleanFlag<boolean>;
81
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
82
+ };
83
+ run(): Promise<void>;
84
+ /**
85
+ * Resolve the DB service slug to seed against.
86
+ *
87
+ * If `--service` was passed, trust it. Otherwise list services and pick the
88
+ * single rds-postgres service registered for this stage. Fail loudly when
89
+ * resolution is ambiguous — never guess silently.
90
+ */
91
+ private resolveServiceSlug;
92
+ private printDryRun;
93
+ }