@hyperdrive.bot/cli 1.0.6 → 1.0.8

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 (97) hide show
  1. package/README.md +415 -67
  2. package/dist/commands/account/add.d.ts +6 -6
  3. package/dist/commands/account/list.d.ts +3 -0
  4. package/dist/commands/account/list.js +9 -2
  5. package/dist/commands/account/remove.d.ts +3 -3
  6. package/dist/commands/auth/login.d.ts +4 -4
  7. package/dist/commands/auth/login.js +1 -0
  8. package/dist/commands/ci/account/create.d.ts +7 -6
  9. package/dist/commands/ci/account/create.js +49 -3
  10. package/dist/commands/ci/account/delete.d.ts +3 -3
  11. package/dist/commands/ci/account/list.d.ts +2 -2
  12. package/dist/commands/config/get.d.ts +1 -1
  13. package/dist/commands/config/set.d.ts +2 -2
  14. package/dist/commands/deployment/create.d.ts +10 -10
  15. package/dist/commands/deployment/get.d.ts +4 -4
  16. package/dist/commands/deployment/launch.d.ts +6 -6
  17. package/dist/commands/deployment/list.d.ts +3 -3
  18. package/dist/commands/deployment/list.js +17 -17
  19. package/dist/commands/domain/switch.d.ts +1 -1
  20. package/dist/commands/example.d.ts +3 -3
  21. package/dist/commands/git/connect.d.ts +2 -2
  22. package/dist/commands/git/connect.js +1 -0
  23. package/dist/commands/git/disconnect.d.ts +3 -3
  24. package/dist/commands/git/list.d.ts +2 -2
  25. package/dist/commands/git/sync.d.ts +7 -7
  26. package/dist/commands/git/sync.js +24 -23
  27. package/dist/commands/init.d.ts +1 -0
  28. package/dist/commands/init.js +20 -19
  29. package/dist/commands/jira/connect.d.ts +2 -1
  30. package/dist/commands/jira/connect.js +17 -6
  31. package/dist/commands/jira/hook/add.d.ts +17 -0
  32. package/dist/commands/jira/hook/add.js +147 -0
  33. package/dist/commands/jira/hook/list.d.ts +14 -0
  34. package/dist/commands/jira/hook/list.js +105 -0
  35. package/dist/commands/jira/hook/remove.d.ts +15 -0
  36. package/dist/commands/jira/hook/remove.js +119 -0
  37. package/dist/commands/jira/hook/toggle.d.ts +15 -0
  38. package/dist/commands/jira/hook/toggle.js +136 -0
  39. package/dist/commands/jira/status.d.ts +1 -1
  40. package/dist/commands/module/analyze.d.ts +5 -5
  41. package/dist/commands/module/create.d.ts +17 -17
  42. package/dist/commands/module/create.js +9 -1
  43. package/dist/commands/module/destroy.d.ts +3 -3
  44. package/dist/commands/module/get.d.ts +2 -2
  45. package/dist/commands/module/link.d.ts +4 -4
  46. package/dist/commands/module/list.d.ts +1 -1
  47. package/dist/commands/module/list.js +12 -11
  48. package/dist/commands/module/reanalyze.d.ts +6 -6
  49. package/dist/commands/module/update.d.ts +19 -19
  50. package/dist/commands/parameter/add.d.ts +7 -7
  51. package/dist/commands/parameter/backfill.d.ts +4 -4
  52. package/dist/commands/parameter/backfill.js +4 -3
  53. package/dist/commands/parameter/clear.d.ts +6 -6
  54. package/dist/commands/parameter/list.d.ts +6 -6
  55. package/dist/commands/parameter/list.js +4 -3
  56. package/dist/commands/parameter/pull.d.ts +6 -6
  57. package/dist/commands/parameter/remove.d.ts +7 -7
  58. package/dist/commands/parameter/sync.d.ts +6 -6
  59. package/dist/commands/parameter/update.d.ts +7 -7
  60. package/dist/commands/project/init.d.ts +21 -0
  61. package/dist/commands/project/init.js +576 -0
  62. package/dist/commands/project/list.d.ts +10 -0
  63. package/dist/commands/project/list.js +119 -0
  64. package/dist/commands/project/status.d.ts +13 -0
  65. package/dist/commands/project/status.js +163 -0
  66. package/dist/commands/project/sync.d.ts +26 -0
  67. package/dist/commands/project/sync.js +388 -0
  68. package/dist/commands/stage/access.d.ts +15 -0
  69. package/dist/commands/stage/access.js +130 -0
  70. package/dist/commands/stage/create.d.ts +11 -11
  71. package/dist/commands/stage/list.d.ts +1 -1
  72. package/dist/commands/stage/list.js +21 -20
  73. package/dist/commands/stage/revoke.d.ts +18 -0
  74. package/dist/commands/stage/revoke.js +171 -0
  75. package/dist/commands/stage/share.d.ts +23 -0
  76. package/dist/commands/stage/share.js +292 -0
  77. package/dist/commands/test-api.d.ts +1 -1
  78. package/dist/services/auth-service.d.ts +15 -82
  79. package/dist/services/auth-service.js +24 -237
  80. package/dist/services/hyperdrive-sigv4.d.ts +162 -24
  81. package/dist/services/hyperdrive-sigv4.js +107 -193
  82. package/dist/services/tenant-service.d.ts +6 -0
  83. package/dist/services/tenant-service.js +13 -0
  84. package/dist/utils/account-flow.d.ts +2 -2
  85. package/dist/utils/account-flow.js +4 -4
  86. package/dist/utils/auth-flow.d.ts +1 -0
  87. package/dist/utils/auth-flow.js +2 -0
  88. package/dist/utils/git-flow.d.ts +1 -0
  89. package/dist/utils/git-flow.js +2 -2
  90. package/dist/utils/hook-flow.d.ts +21 -0
  91. package/dist/utils/hook-flow.js +154 -0
  92. package/dist/utils/jira-flow.d.ts +2 -2
  93. package/dist/utils/jira-flow.js +4 -4
  94. package/dist/utils/table.d.ts +17 -0
  95. package/dist/utils/table.js +41 -0
  96. package/oclif.manifest.json +844 -154
  97. package/package.json +59 -15
@@ -0,0 +1,388 @@
1
+ /**
2
+ * Project Sync Command
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.
6
+ */
7
+ import { Args, Command, Flags } from '@oclif/core';
8
+ import chalk from 'chalk';
9
+ import { execSync, 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
+ export default class ProjectSync extends Command {
62
+ static args = {
63
+ project: Args.string({
64
+ description: 'Project slug or ID',
65
+ required: true,
66
+ }),
67
+ };
68
+ static description = 'Generate architecture summaries for project repos via Claude analysis';
69
+ static examples = [
70
+ '<%= config.bin %> project sync my-project',
71
+ '<%= config.bin %> project sync my-project --repo api',
72
+ '<%= config.bin %> project sync my-project --json',
73
+ ];
74
+ static flags = {
75
+ domain: Flags.string({
76
+ char: 'd',
77
+ description: 'Tenant domain (for multi-domain setups)',
78
+ }),
79
+ json: Flags.boolean({
80
+ description: 'Output result as JSON',
81
+ }),
82
+ repo: Flags.string({
83
+ description: 'Sync only a specific repo by name',
84
+ }),
85
+ };
86
+ async run() {
87
+ const { args, flags } = await this.parse(ProjectSync);
88
+ // Authenticate
89
+ let service;
90
+ const authSpinner = ora('Checking authentication...').start();
91
+ try {
92
+ service = new HyperdriveSigV4Service(flags.domain);
93
+ authSpinner.succeed('Authenticated');
94
+ }
95
+ catch (error) {
96
+ authSpinner.fail('Not authenticated');
97
+ this.error(`${error.message}\n\nPlease authenticate first with: ${chalk.cyan('hd auth login')}`);
98
+ }
99
+ // Resolve project by slug
100
+ const projectSpinner = ora(`Resolving project ${chalk.cyan(args.project)}...`).start();
101
+ let project;
102
+ try {
103
+ const result = await service.moduleGet({ slug: args.project });
104
+ project = { name: result.name || args.project, projectId: result.projectId || args.project, slug: result.slug || args.project };
105
+ projectSpinner.succeed(`Project: ${chalk.cyan(project.name)} (${project.slug})`);
106
+ }
107
+ catch (error) {
108
+ projectSpinner.fail('Project not found');
109
+ this.error(`Could not find project "${args.project}": ${error.response?.data?.message || error.message}`);
110
+ }
111
+ // List repos
112
+ const repoSpinner = ora('Fetching repositories...').start();
113
+ let repos;
114
+ try {
115
+ repos = await service.projectListRepos(project.projectId);
116
+ repoSpinner.succeed(`Found ${repos.length} repo(s)`);
117
+ }
118
+ catch (error) {
119
+ repoSpinner.fail('Failed to fetch repos');
120
+ this.error(`Could not list repos: ${error.response?.data?.message || error.message}`);
121
+ }
122
+ // Filter by --repo flag
123
+ if (flags.repo) {
124
+ repos = repos.filter(r => r.name === flags.repo);
125
+ if (repos.length === 0) {
126
+ this.error(`No repo named "${flags.repo}" found in project "${project.slug}"`);
127
+ }
128
+ }
129
+ if (repos.length === 0) {
130
+ this.log(chalk.yellow('No repositories to sync.'));
131
+ return;
132
+ }
133
+ this.log('');
134
+ this.log(chalk.blue(`Syncing ${repos.length} repo(s)...`));
135
+ this.log('');
136
+ // Process each repo
137
+ const results = [];
138
+ for (let i = 0; i < repos.length; i++) {
139
+ const repo = repos[i];
140
+ const prefix = chalk.dim(`[${i + 1}/${repos.length}]`);
141
+ const result = await this.syncRepo(service, project, repo, prefix);
142
+ results.push(result);
143
+ }
144
+ // Summary
145
+ this.log('');
146
+ const succeeded = results.filter(r => r.success).length;
147
+ const failed = results.filter(r => !r.success).length;
148
+ if (flags.json) {
149
+ this.log(JSON.stringify({ failed, project: project.slug, results, succeeded, total: repos.length }, null, 2));
150
+ return;
151
+ }
152
+ if (failed === 0) {
153
+ this.log(chalk.green(`Synced ${succeeded}/${repos.length} repos (0 failed)`));
154
+ }
155
+ else {
156
+ this.log(chalk.yellow(`Synced ${succeeded}/${repos.length} repos (${failed} failed)`));
157
+ for (const r of results.filter(r => !r.success)) {
158
+ this.log(chalk.red(` ${r.name}: ${r.error}`));
159
+ }
160
+ }
161
+ }
162
+ async syncRepo(service, project, repo, prefix) {
163
+ const spinner = ora(`${prefix} ${chalk.cyan(repo.name)} — cloning...`).start();
164
+ let tmpDir = null;
165
+ try {
166
+ // Step 1: Clone
167
+ tmpDir = mkdtempSync(join(tmpdir(), `hd-sync-${repo.name}-`));
168
+ const clonePath = join(tmpDir, repo.name);
169
+ try {
170
+ execSync(`git clone --depth 1 --branch ${repo.defaultBranch} ${repo.gitRemote} ${clonePath}`, {
171
+ stdio: 'pipe',
172
+ timeout: 120_000, // 2 min clone timeout
173
+ });
174
+ }
175
+ catch (cloneError) {
176
+ throw new Error(`git clone failed: ${cloneError.stderr?.toString() || cloneError.message}`);
177
+ }
178
+ // Step 2: Detect _bmad or generate via Claude
179
+ spinner.text = `${prefix} ${chalk.cyan(repo.name)} — analyzing...`;
180
+ let yamlOutput = null;
181
+ let lastError = null;
182
+ const MAX_ATTEMPTS = 3;
183
+ // Check for _bmad docs
184
+ const bmadDocPath = this.detectBmadDocs(clonePath);
185
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
186
+ try {
187
+ if (bmadDocPath) {
188
+ // Use _bmad doc with simpler restructure prompt
189
+ const docContent = readFileSync(bmadDocPath, 'utf-8');
190
+ 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${docContent}`;
191
+ yamlOutput = await this.runClaude(prompt, clonePath);
192
+ }
193
+ else {
194
+ // Full codebase analysis
195
+ yamlOutput = await this.runClaude(ARCHITECTURE_ANALYSIS_PROMPT, clonePath);
196
+ }
197
+ // Validate
198
+ this.validateYaml(yamlOutput);
199
+ break; // Validation passed
200
+ }
201
+ catch (error) {
202
+ lastError = error.message;
203
+ this.log(chalk.dim(` ${repo.name} attempt ${attempt}/${MAX_ATTEMPTS} failed: ${lastError}`));
204
+ yamlOutput = null;
205
+ if (attempt === MAX_ATTEMPTS) {
206
+ throw new Error(`Validation failed after ${MAX_ATTEMPTS} attempts: ${lastError}`);
207
+ }
208
+ }
209
+ }
210
+ if (!yamlOutput) {
211
+ throw new Error(`Generation failed: ${lastError}`);
212
+ }
213
+ // Step 2b: Extract entity registry from validated YAML and merge with gut config
214
+ spinner.text = `${prefix} ${chalk.cyan(repo.name)} — extracting entities...`;
215
+ const parsedYaml = yaml.load(yamlOutput);
216
+ let entityRegistry = (parsedYaml.entity_registry || []);
217
+ // Check for .gut/config.json and merge
218
+ const gutEntities = this.readGutEntities(clonePath);
219
+ if (gutEntities.length > 0) {
220
+ entityRegistry = this.mergeEntityRegistries(entityRegistry, gutEntities);
221
+ this.log(chalk.dim(` ${repo.name}: merged ${entityRegistry.length} entities (${gutEntities.length} from gut)`));
222
+ }
223
+ else if (entityRegistry.length > 0) {
224
+ this.log(chalk.dim(` ${repo.name}: found ${entityRegistry.length} entities from analysis`));
225
+ }
226
+ // Step 3: Upload to S3 via API
227
+ spinner.text = `${prefix} ${chalk.cyan(repo.name)} — uploading...`;
228
+ // The API handles S3 upload and DynamoDB update — call updateRepo with the summary content
229
+ await service.projectUpdateRepo(project.projectId, repo.repoId, {
230
+ architectureSummary: yamlOutput,
231
+ lastSyncedAt: new Date().toISOString(),
232
+ });
233
+ // Step 4: Update entity registry via dedicated endpoint
234
+ if (entityRegistry.length > 0) {
235
+ try {
236
+ await service.projectUpdateEntities(project.projectId, repo.repoId, entityRegistry);
237
+ }
238
+ catch (entityError) {
239
+ this.log(chalk.yellow(` ${repo.name}: entity registry update failed: ${entityError.message}`));
240
+ }
241
+ }
242
+ spinner.succeed(`${prefix} ${chalk.cyan(repo.name)} — ${chalk.green('done')}`);
243
+ return { name: repo.name, success: true };
244
+ }
245
+ catch (error) {
246
+ spinner.fail(`${prefix} ${chalk.cyan(repo.name)} — ${chalk.red('failed')}`);
247
+ this.log(chalk.red(` Error: ${error.message}`));
248
+ return { error: error.message, name: repo.name, success: false };
249
+ }
250
+ finally {
251
+ // Cleanup temp directory
252
+ if (tmpDir && existsSync(tmpDir)) {
253
+ try {
254
+ rmSync(tmpDir, { force: true, recursive: true });
255
+ }
256
+ catch {
257
+ // Ignore cleanup errors
258
+ }
259
+ }
260
+ }
261
+ }
262
+ detectBmadDocs(repoPath) {
263
+ const bmadDir = join(repoPath, '_bmad');
264
+ if (!existsSync(bmadDir))
265
+ return null;
266
+ try {
267
+ if (!statSync(bmadDir).isDirectory())
268
+ return null;
269
+ const files = readdirSync(bmadDir);
270
+ const archPatterns = [/^architecture.*\.md$/i, /^arch-.*\.md$/i];
271
+ for (const file of files) {
272
+ for (const pattern of archPatterns) {
273
+ if (pattern.test(file)) {
274
+ return join(bmadDir, file);
275
+ }
276
+ }
277
+ }
278
+ }
279
+ catch {
280
+ // Ignore fs errors
281
+ }
282
+ return null;
283
+ }
284
+ runClaude(prompt, cwd) {
285
+ return new Promise((resolve, reject) => {
286
+ const TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
287
+ const child = spawn('claude', ['-p', prompt, '--output-format', 'text'], {
288
+ cwd,
289
+ stdio: ['pipe', 'pipe', 'pipe'],
290
+ timeout: TIMEOUT_MS,
291
+ });
292
+ let stdout = '';
293
+ let stderr = '';
294
+ child.stdout.on('data', (data) => {
295
+ stdout += data.toString();
296
+ });
297
+ child.stderr.on('data', (data) => {
298
+ stderr += data.toString();
299
+ });
300
+ child.on('error', (error) => {
301
+ reject(new Error(`Claude CLI failed to start: ${error.message}`));
302
+ });
303
+ child.on('close', (code) => {
304
+ if (code !== 0) {
305
+ reject(new Error(`Claude CLI exited with code ${code}: ${stderr.slice(0, 500)}`));
306
+ return;
307
+ }
308
+ // Strip markdown code fences if present
309
+ let output = stdout.trim();
310
+ if (output.startsWith('```yaml')) {
311
+ output = output.replace(/^```yaml\n?/, '').replace(/\n?```$/, '');
312
+ }
313
+ else if (output.startsWith('```')) {
314
+ output = output.replace(/^```\n?/, '').replace(/\n?```$/, '');
315
+ }
316
+ resolve(output);
317
+ });
318
+ });
319
+ }
320
+ validateYaml(yamlString) {
321
+ const parsed = yaml.load(yamlString);
322
+ if (!parsed || typeof parsed !== 'object') {
323
+ throw new Error('YAML must be a non-null object');
324
+ }
325
+ for (const key of REQUIRED_SUMMARY_KEYS) {
326
+ if (!(key in parsed)) {
327
+ throw new Error(`Missing required top-level key: '${key}'`);
328
+ }
329
+ }
330
+ const repo = parsed.repo;
331
+ if (!repo || typeof repo !== 'object' || !repo.name || !repo.description) {
332
+ throw new Error("'repo' must have 'name' and 'description' string fields");
333
+ }
334
+ if (!Array.isArray(parsed.entity_registry)) {
335
+ throw new Error("'entity_registry' must be an array");
336
+ }
337
+ for (let i = 0; i < parsed.entity_registry.length; i++) {
338
+ const entry = parsed.entity_registry[i];
339
+ if (!entry.name)
340
+ throw new Error(`entity_registry[${i}]: 'name' is required`);
341
+ if (!entry.type)
342
+ throw new Error(`entity_registry[${i}]: 'type' is required`);
343
+ if (!VALID_ENTITY_TYPES.includes(entry.type)) {
344
+ throw new Error(`entity_registry[${i}]: invalid type '${entry.type}'`);
345
+ }
346
+ if (!entry.path)
347
+ throw new Error(`entity_registry[${i}]: 'path' is required`);
348
+ }
349
+ const techStack = parsed.tech_stack;
350
+ if (!techStack || typeof techStack !== 'object') {
351
+ throw new Error("'tech_stack' must be an object");
352
+ }
353
+ for (const field of ['runtime', 'framework', 'language', 'database', 'infrastructure']) {
354
+ if (!techStack[field]) {
355
+ throw new Error(`tech_stack.${field} is required`);
356
+ }
357
+ }
358
+ }
359
+ readGutEntities(repoPath) {
360
+ const gutConfigPath = join(repoPath, '.gut', 'config.json');
361
+ if (!existsSync(gutConfigPath))
362
+ return [];
363
+ try {
364
+ const content = readFileSync(gutConfigPath, 'utf-8');
365
+ const config = JSON.parse(content);
366
+ if (!config.entities || !Array.isArray(config.entities))
367
+ return [];
368
+ return config.entities
369
+ .filter((e) => e && typeof e === 'object' && e.name && e.type && e.path &&
370
+ VALID_ENTITY_TYPES.includes(e.type))
371
+ .map((e) => ({
372
+ name: e.name,
373
+ type: e.type,
374
+ path: e.path,
375
+ ...(e.repository && typeof e.repository === 'string' ? { repository: e.repository } : {}),
376
+ ...(e.description && typeof e.description === 'string' ? { description: e.description } : {}),
377
+ }));
378
+ }
379
+ catch {
380
+ return [];
381
+ }
382
+ }
383
+ mergeEntityRegistries(claudeEntities, gutEntities) {
384
+ const gutNameSet = new Set(gutEntities.map(e => e.name.toLowerCase()));
385
+ const uniqueClaude = claudeEntities.filter(e => !gutNameSet.has(e.name.toLowerCase()));
386
+ return [...gutEntities, ...uniqueClaude];
387
+ }
388
+ }
@@ -0,0 +1,15 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class StageAccess extends Command {
3
+ static args: {
4
+ stage: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ private formatRole;
14
+ private promptForStage;
15
+ }
@@ -0,0 +1,130 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
5
+ import { printTable } from '../../utils/table.js';
6
+ export default class StageAccess extends Command {
7
+ static args = {
8
+ stage: Args.string({
9
+ description: 'Stage name to view access for',
10
+ required: false,
11
+ }),
12
+ };
13
+ static description = 'View who has access to a stage';
14
+ static examples = [
15
+ '<%= config.bin %> <%= command.id %>',
16
+ '<%= config.bin %> <%= command.id %> develop',
17
+ '<%= config.bin %> <%= command.id %> develop --format json',
18
+ ];
19
+ static flags = {
20
+ domain: Flags.string({
21
+ char: 'd',
22
+ description: 'Tenant domain (for multi-domain setups)',
23
+ }),
24
+ format: Flags.string({
25
+ char: 'f',
26
+ default: 'table',
27
+ description: 'Output format',
28
+ options: ['table', 'json'],
29
+ }),
30
+ };
31
+ async run() {
32
+ const { args, flags } = await this.parse(StageAccess);
33
+ const service = new HyperdriveSigV4Service(flags.domain);
34
+ // Resolve stage
35
+ let stageName;
36
+ if (args.stage) {
37
+ stageName = args.stage;
38
+ }
39
+ else {
40
+ stageName = await this.promptForStage(service);
41
+ }
42
+ try {
43
+ this.log(chalk.blue(`\n🔍 Fetching access for stage '${stageName}'...\n`));
44
+ const result = await service.stageAccessGet(stageName);
45
+ if (flags.format === 'json') {
46
+ this.log(JSON.stringify(result, null, 2));
47
+ return;
48
+ }
49
+ // Display header
50
+ this.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
51
+ this.log(chalk.cyan(`📋 Access for stage: ${result.stageName}`));
52
+ this.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
53
+ this.log('');
54
+ if (!result.access || result.access.length === 0) {
55
+ this.log(chalk.yellow('⚠️ No access entries found (only the owner has access)'));
56
+ return;
57
+ }
58
+ // Group by type
59
+ const users = result.access.filter((a) => a.targetType === 'user');
60
+ const groups = result.access.filter((a) => a.targetType === 'group');
61
+ if (users.length > 0) {
62
+ this.log(chalk.white(`Users (${users.length}):`));
63
+ printTable(users, {
64
+ targetId: {
65
+ header: 'User ID / Email',
66
+ minWidth: 30,
67
+ },
68
+ role: {
69
+ header: 'Role',
70
+ get: (row) => this.formatRole(row.role),
71
+ },
72
+ }, (msg) => this.log(msg));
73
+ this.log('');
74
+ }
75
+ if (groups.length > 0) {
76
+ this.log(chalk.white(`Groups (${groups.length}):`));
77
+ printTable(groups, {
78
+ targetId: {
79
+ header: 'Group ID',
80
+ minWidth: 30,
81
+ },
82
+ role: {
83
+ header: 'Role',
84
+ get: (row) => this.formatRole(row.role),
85
+ },
86
+ }, (msg) => this.log(msg));
87
+ this.log('');
88
+ }
89
+ // Summary
90
+ this.log(chalk.gray(`Total: ${result.access.length} access entries`));
91
+ }
92
+ catch (error) {
93
+ const err = error;
94
+ const message = err.response?.data?.message || err.message;
95
+ this.error(`Failed to get stage access: ${message}`);
96
+ }
97
+ }
98
+ formatRole(role) {
99
+ switch (role) {
100
+ case 'manager':
101
+ return chalk.red('Manager');
102
+ case 'deployer':
103
+ return chalk.yellow('Deployer');
104
+ case 'viewer':
105
+ return chalk.blue('Viewer');
106
+ default:
107
+ return role;
108
+ }
109
+ }
110
+ async promptForStage(service) {
111
+ this.log(chalk.gray(' Fetching stages...'));
112
+ const stages = await service.stageList();
113
+ if (!stages || stages.length === 0) {
114
+ this.error('No stages found');
115
+ }
116
+ const stageChoices = stages.map(s => ({
117
+ name: s.production
118
+ ? `${s.name} ${chalk.red('(PROD)')}`
119
+ : `${s.name} ${chalk.blue('(dev)')}`,
120
+ value: s.slug || s.name,
121
+ }));
122
+ const { stage } = await inquirer.prompt([{
123
+ choices: stageChoices,
124
+ message: chalk.yellow('Select a stage:'),
125
+ name: 'stage',
126
+ type: 'list',
127
+ }]);
128
+ return stage;
129
+ }
130
+ }
@@ -2,17 +2,17 @@ import { Command } from '@oclif/core';
2
2
  export default class StageCreate extends Command {
3
3
  static description: string;
4
4
  static flags: {
5
- accountId: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
6
- autoLaunch: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
7
- branchName: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
8
- defaultStage: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
9
- deletionProtection: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
10
- domain: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
11
- name: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
12
- production: import("@oclif/core/lib/interfaces/parser.js").BooleanFlag<boolean>;
13
- project: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
14
- provider: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
15
- region: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
5
+ accountId: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
6
+ autoLaunch: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ branchName: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
8
+ defaultStage: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ deletionProtection: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ name: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ production: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ project: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ provider: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
15
+ region: import("@oclif/core/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/interfaces").CustomOptions>;
16
16
  };
17
17
  run(): Promise<void>;
18
18
  /**
@@ -3,7 +3,7 @@ export default class StageList extends Command {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
- domain: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
6
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
7
7
  };
8
8
  run(): Promise<void>;
9
9
  }
@@ -1,6 +1,7 @@
1
- import { Command, Flags, ux } from '@oclif/core';
1
+ import { Command, Flags } from '@oclif/core';
2
2
  import chalk from 'chalk';
3
3
  import { HyperdriveSigV4Service } from '../../services/hyperdrive-sigv4.js';
4
+ import { printTable } from '../../utils/table.js';
4
5
  export default class StageList extends Command {
5
6
  static description = 'List all stages';
6
7
  static examples = [
@@ -24,36 +25,36 @@ export default class StageList extends Command {
24
25
  }
25
26
  this.log(chalk.green(`✅ Found ${stages.length} stage(s)\n`));
26
27
  // Display stages in a table
27
- ux.table(stages, {
28
- branchName: {
29
- header: 'Branch',
30
- },
31
- createdAt: {
32
- get: (row) => new Date(row.createdAt).toLocaleDateString(),
33
- header: 'Created',
34
- },
35
- defaultStage: {
36
- get: (row) => row.defaultStage ? chalk.green('✓') : '',
37
- header: 'Default',
38
- },
28
+ printTable(stages, {
39
29
  name: {
40
30
  header: 'Name',
41
31
  minWidth: 15,
42
32
  },
33
+ slug: {
34
+ header: 'Slug',
35
+ minWidth: 15,
36
+ get: (row) => chalk.cyan(row.slug),
37
+ },
43
38
  production: {
44
- get: (row) => row.production ? chalk.red('PROD') : chalk.blue('DEV'),
45
39
  header: 'Type',
40
+ get: (row) => row.production ? chalk.red('PROD') : chalk.blue('DEV'),
41
+ },
42
+ branchName: {
43
+ header: 'Branch',
46
44
  },
47
45
  regions: {
48
- get: (row) => (row.regions || []).join(', '),
49
46
  header: 'Regions',
47
+ get: (row) => (row.regions || []).join(', '),
50
48
  },
51
- slug: {
52
- get: (row) => chalk.cyan(row.slug),
53
- header: 'Slug',
54
- minWidth: 15,
49
+ defaultStage: {
50
+ header: 'Default',
51
+ get: (row) => row.defaultStage ? chalk.green('') : '',
52
+ },
53
+ createdAt: {
54
+ header: 'Created',
55
+ get: (row) => new Date(row.createdAt).toLocaleDateString(),
55
56
  },
56
- });
57
+ }, (msg) => this.log(msg));
57
58
  }
58
59
  catch (error) {
59
60
  console.error('Error:', error);
@@ -0,0 +1,18 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class StageRevoke extends Command {
3
+ static args: {
4
+ stage: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ ci: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ domain: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ user: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ };
14
+ run(): Promise<void>;
15
+ private findCIAccount;
16
+ private promptForStage;
17
+ private promptForTarget;
18
+ }