@hyperdrive.bot/cli 1.0.12 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +360 -5
  116. package/dist/services/hyperdrive-sigv4.js +192 -24
  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,117 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { HyperdriveSigV4Service } from '../../../services/hyperdrive-sigv4.js';
5
+ import { printTable } from '../../../utils/table.js';
6
+ import { selectTrackerProject } from '../../../utils/tracker-project-flow.js';
7
+ export default class TrackerProjectListModules extends Command {
8
+ static description = 'List modules linked to a tracker project';
9
+ static examples = [
10
+ '<%= config.bin %> tracker project list-modules',
11
+ '<%= config.bin %> tracker project list-modules --tracker-project-id tp-abc123',
12
+ '<%= config.bin %> tracker project list-modules --tracker-project-id tp-abc123 --json',
13
+ '<%= config.bin %> tracker project list-modules --domain my-tenant.hyperdrivebot.dev',
14
+ ];
15
+ static flags = {
16
+ domain: Flags.string({
17
+ char: 'd',
18
+ description: 'Tenant domain (for multi-domain setups)',
19
+ }),
20
+ json: Flags.boolean({
21
+ default: false,
22
+ description: 'Output raw JSON response',
23
+ }),
24
+ 'tracker-project-id': Flags.string({
25
+ description: 'Tracker project ID (skips interactive selection)',
26
+ }),
27
+ };
28
+ async run() {
29
+ const { flags } = await this.parse(TrackerProjectListModules);
30
+ const isJson = flags.json;
31
+ const service = new HyperdriveSigV4Service(flags.domain);
32
+ try {
33
+ // Select tracker project (interactive or via flag)
34
+ const project = await selectTrackerProject(service, flags['tracker-project-id']);
35
+ const spinner = isJson ? null : ora('Fetching linked modules...').start();
36
+ // Fetch linked modules and all modules in parallel
37
+ const [linkedModules, allModules] = await Promise.all([
38
+ service.trackerProjectListModules(project.trackerProjectId),
39
+ service.moduleList(),
40
+ ]);
41
+ spinner?.stop();
42
+ // Build module name lookup map (projectId → name)
43
+ const moduleNameMap = new Map();
44
+ for (const mod of allModules) {
45
+ if (mod.projectId) {
46
+ moduleNameMap.set(mod.projectId, mod.name || mod.slug || mod.projectId);
47
+ }
48
+ }
49
+ // Enrich linked modules with human-readable names
50
+ const enriched = linkedModules.map(link => ({
51
+ ...link,
52
+ moduleName: moduleNameMap.get(link.moduleId) || link.moduleId,
53
+ }));
54
+ // Empty state
55
+ if (enriched.length === 0) {
56
+ if (isJson) {
57
+ this.log(JSON.stringify([], null, 2));
58
+ }
59
+ else {
60
+ this.log(chalk.yellow('\nNo modules linked to this tracker project.'));
61
+ }
62
+ return;
63
+ }
64
+ // JSON output
65
+ if (isJson) {
66
+ this.log(JSON.stringify(enriched, null, 2));
67
+ return;
68
+ }
69
+ // Table output
70
+ this.log('');
71
+ printTable(enriched, {
72
+ moduleName: {
73
+ header: 'Module',
74
+ minWidth: 20,
75
+ get: (row) => {
76
+ const r = row;
77
+ return chalk.cyan(r.moduleName);
78
+ },
79
+ },
80
+ role: {
81
+ header: 'Role',
82
+ minWidth: 14,
83
+ get: (row) => {
84
+ const r = row;
85
+ if (r.role === 'primary')
86
+ return chalk.green('primary');
87
+ return chalk.dim(r.role || 'supporting');
88
+ },
89
+ },
90
+ linkedAt: {
91
+ header: 'Linked At',
92
+ minWidth: 24,
93
+ get: (row) => {
94
+ const r = row;
95
+ return r.linkedAt || chalk.gray('-');
96
+ },
97
+ },
98
+ }, (msg) => this.log(msg));
99
+ // Count footer
100
+ this.log(`\n${enriched.length} modules linked to ${chalk.cyan(project.externalProjectKey)} (${project.externalProjectName})`);
101
+ // Hooks info footer
102
+ this.log(`ℹ All hooks on this tracker project apply to every linked module. Use 'hd tracker project link-module' or 'unlink-module' to manage.`);
103
+ }
104
+ catch (error) {
105
+ const axiosError = error;
106
+ const status = axiosError.response?.status;
107
+ if (status === 404) {
108
+ this.log(chalk.red('\n❌ Tracker project not found.'));
109
+ this.log(chalk.gray('Run "hd tracker project list" to see available projects.'));
110
+ }
111
+ else {
112
+ this.log(chalk.red(`\n❌ ${axiosError.response?.data?.message ?? axiosError.message}`));
113
+ }
114
+ this.exit(1);
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,10 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class TrackerProjectList 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
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ };
9
+ run(): Promise<void>;
10
+ }
@@ -0,0 +1,90 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { HyperdriveSigV4Service } from '../../../services/hyperdrive-sigv4.js';
5
+ import { printTable } from '../../../utils/table.js';
6
+ const PROVIDER_ICONS = {
7
+ 'github-issues': '⚫ GitHub',
8
+ jira: '🔷 Jira',
9
+ linear: '🟣 Linear',
10
+ };
11
+ export default class TrackerProjectList extends Command {
12
+ static description = 'List all configured tracker projects';
13
+ static examples = [
14
+ '<%= config.bin %> tracker project list',
15
+ '<%= config.bin %> tracker project list --json',
16
+ '<%= config.bin %> tracker project list --domain my-tenant.hyperdrivebot.dev',
17
+ ];
18
+ static flags = {
19
+ domain: Flags.string({
20
+ char: 'd',
21
+ description: 'Tenant domain (for multi-domain setups)',
22
+ }),
23
+ json: Flags.boolean({
24
+ default: false,
25
+ description: 'Output raw JSON response',
26
+ }),
27
+ };
28
+ async run() {
29
+ const { flags } = await this.parse(TrackerProjectList);
30
+ const isJson = flags.json;
31
+ const service = new HyperdriveSigV4Service(flags.domain);
32
+ const spinner = isJson ? null : ora('Fetching tracker projects...').start();
33
+ try {
34
+ const projects = await service.trackerProjectList();
35
+ spinner?.stop();
36
+ if (isJson) {
37
+ this.log(JSON.stringify(projects, null, 2));
38
+ return;
39
+ }
40
+ if (!projects || projects.length === 0) {
41
+ this.log(chalk.yellow('\nNo tracker projects found.'));
42
+ this.log(chalk.gray('Run "hd tracker project init" to create one.'));
43
+ return;
44
+ }
45
+ this.log(chalk.green(`\n${projects.length} tracker project(s) found:\n`));
46
+ printTable(projects, {
47
+ trackerProjectId: {
48
+ header: 'ID',
49
+ minWidth: 18,
50
+ get: (row) => {
51
+ const tp = row;
52
+ return chalk.cyan(tp.trackerProjectId.length > 16 ? tp.trackerProjectId.slice(0, 16) + '…' : tp.trackerProjectId);
53
+ },
54
+ },
55
+ provider: {
56
+ header: 'Provider',
57
+ minWidth: 14,
58
+ get: (row) => {
59
+ const tp = row;
60
+ return PROVIDER_ICONS[tp.provider] ?? tp.provider;
61
+ },
62
+ },
63
+ externalProjectKey: {
64
+ header: 'External Key',
65
+ minWidth: 14,
66
+ },
67
+ statusMappingCount: {
68
+ header: 'Statuses',
69
+ get: (row) => {
70
+ const tp = row;
71
+ return String(Object.keys(tp.statusMapping || {}).length);
72
+ },
73
+ },
74
+ createdAt: {
75
+ header: 'Created',
76
+ get: (row) => {
77
+ const tp = row;
78
+ return tp.createdAt ? new Date(tp.createdAt).toLocaleDateString() : chalk.gray('-');
79
+ },
80
+ },
81
+ }, (msg) => this.log(msg));
82
+ }
83
+ catch (error) {
84
+ spinner?.fail('Failed to fetch tracker projects');
85
+ const axiosError = error;
86
+ this.log(chalk.red(`\n❌ ${axiosError.response?.data?.message ?? axiosError.message}`));
87
+ this.exit(1);
88
+ }
89
+ }
90
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class TrackerProjectStatus extends Command {
3
+ static args: {
4
+ 'tracker-project': import("@oclif/core/interfaces").Arg<string, 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
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,168 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { HyperdriveSigV4Service } from '../../../services/hyperdrive-sigv4.js';
5
+ import { printHeader, printTable } from '../../../utils/table.js';
6
+ const PROVIDER_ICONS = {
7
+ 'github-issues': '⚫ GitHub',
8
+ jira: '🔷 Jira',
9
+ linear: '🟣 Linear',
10
+ };
11
+ function getSummaryFreshness(module) {
12
+ if (!module.lastSyncedAt || module.summaryStatus === 'not_generated')
13
+ return 'missing';
14
+ const daysSinceSync = (Date.now() - new Date(module.lastSyncedAt).getTime()) / (1000 * 60 * 60 * 24);
15
+ return daysSinceSync > 7 ? 'stale' : 'fresh';
16
+ }
17
+ function colorFreshness(status) {
18
+ if (status === 'fresh')
19
+ return chalk.green(status);
20
+ if (status === 'stale')
21
+ return chalk.yellow(status);
22
+ return chalk.red(status);
23
+ }
24
+ export default class TrackerProjectStatus extends Command {
25
+ static args = {
26
+ 'tracker-project': Args.string({
27
+ description: 'Tracker project ID',
28
+ required: true,
29
+ }),
30
+ };
31
+ static description = 'Show detailed status of a tracker project';
32
+ static examples = [
33
+ '<%= config.bin %> tracker project status tp-abc123',
34
+ '<%= config.bin %> tracker project status tp-abc123 --json',
35
+ '<%= config.bin %> tracker project status tp-abc123 --domain my-tenant.hyperdrivebot.dev',
36
+ ];
37
+ static flags = {
38
+ domain: Flags.string({
39
+ char: 'd',
40
+ description: 'Tenant domain (for multi-domain setups)',
41
+ }),
42
+ json: Flags.boolean({
43
+ default: false,
44
+ description: 'Output raw JSON response',
45
+ }),
46
+ };
47
+ async run() {
48
+ const { args, flags } = await this.parse(TrackerProjectStatus);
49
+ const trackerProjectId = args['tracker-project'];
50
+ const isJson = flags.json;
51
+ const service = new HyperdriveSigV4Service(flags.domain);
52
+ const spinner = isJson ? null : ora('Fetching tracker project context...').start();
53
+ try {
54
+ const data = await service.trackerProjectGetContext(trackerProjectId);
55
+ spinner?.succeed('Context retrieved');
56
+ if (isJson) {
57
+ this.log(JSON.stringify(data, null, 2));
58
+ return;
59
+ }
60
+ const tp = data.trackerProject;
61
+ // Tracker Info section
62
+ printHeader('Tracker Info', (msg) => this.log(msg));
63
+ this.log(` Provider: ${PROVIDER_ICONS[tp.provider] ?? tp.provider}`);
64
+ this.log(` External Project Key: ${chalk.cyan(tp.externalProjectKey)}`);
65
+ this.log(` External Project: ${tp.externalProjectName}`);
66
+ this.log(` Tracker ID: ${chalk.dim(tp.trackerId)}`);
67
+ this.log(` Tracker Project ID: ${chalk.dim(tp.trackerProjectId)}`);
68
+ // Linked Modules section
69
+ printHeader('Linked Modules', (msg) => this.log(msg));
70
+ if (data.linkedModules.length === 0) {
71
+ this.log(chalk.gray(' No modules linked.'));
72
+ }
73
+ else {
74
+ printTable(data.linkedModules, {
75
+ name: {
76
+ header: 'Name',
77
+ minWidth: 20,
78
+ get: (row) => {
79
+ const m = row;
80
+ return chalk.cyan(m.metadata.name);
81
+ },
82
+ },
83
+ summaryStatus: {
84
+ header: 'Summary Status',
85
+ minWidth: 16,
86
+ get: (row) => {
87
+ const m = row;
88
+ return colorFreshness(getSummaryFreshness(m));
89
+ },
90
+ },
91
+ lastSyncedAt: {
92
+ header: 'Last Synced',
93
+ minWidth: 22,
94
+ get: (row) => {
95
+ const m = row;
96
+ return m.lastSyncedAt ? new Date(m.lastSyncedAt).toLocaleString() : chalk.gray('never');
97
+ },
98
+ },
99
+ }, (msg) => this.log(msg));
100
+ }
101
+ // Status Mapping section
102
+ printHeader('Status Mapping', (msg) => this.log(msg));
103
+ const mappingEntries = Object.entries(tp.statusMapping || {});
104
+ if (mappingEntries.length === 0) {
105
+ this.log(chalk.gray(' No status mapping configured.'));
106
+ }
107
+ else {
108
+ const mappingRows = mappingEntries.map(([providerStatus, normalizedState]) => ({
109
+ normalizedState,
110
+ providerStatus,
111
+ }));
112
+ printTable(mappingRows, {
113
+ providerStatus: {
114
+ header: 'Provider Status',
115
+ minWidth: 22,
116
+ },
117
+ normalizedState: {
118
+ header: 'Normalized State',
119
+ minWidth: 22,
120
+ get: (row) => chalk.cyan(String(row.normalizedState)),
121
+ },
122
+ }, (msg) => this.log(msg));
123
+ }
124
+ // Hooks section
125
+ printHeader('Hooks', (msg) => this.log(msg));
126
+ const hookEntries = Object.entries(data.hooksSummary || {});
127
+ if (hookEntries.length === 0) {
128
+ this.log(chalk.gray(' No hooks configured.'));
129
+ }
130
+ else {
131
+ const hookRows = hookEntries.flatMap(([triggerStatus, actions]) => actions.map((action) => ({ action, triggerStatus })));
132
+ printTable(hookRows, {
133
+ triggerStatus: {
134
+ header: 'Trigger Status',
135
+ minWidth: 22,
136
+ },
137
+ action: {
138
+ header: 'Action Type',
139
+ minWidth: 22,
140
+ },
141
+ }, (msg) => this.log(msg));
142
+ }
143
+ // Warning banner for stale/missing modules
144
+ const issueModules = data.linkedModules.filter((m) => {
145
+ const freshness = getSummaryFreshness(m);
146
+ return freshness === 'stale' || freshness === 'missing';
147
+ });
148
+ if (issueModules.length > 0) {
149
+ this.log('');
150
+ this.log(chalk.yellow(`⚠ Warning: ${issueModules.length} module(s) have stale or missing architecture summaries. Run "hd module sync {module}" to refresh.`));
151
+ }
152
+ this.log('');
153
+ }
154
+ catch (error) {
155
+ spinner?.fail('Failed to fetch tracker project');
156
+ const axiosError = error;
157
+ const status = axiosError.response?.status;
158
+ if (status === 404) {
159
+ this.log(chalk.red('\n❌ Tracker project not found.'));
160
+ this.log(chalk.gray('Run "hd tracker project list" to see available projects.'));
161
+ }
162
+ else {
163
+ this.log(chalk.red(`\n❌ ${axiosError.response?.data?.message ?? axiosError.message}`));
164
+ }
165
+ this.exit(1);
166
+ }
167
+ }
168
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class TrackerProjectUnlinkModule 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
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ modules: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ 'tracker-project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ run(): Promise<void>;
13
+ }
@@ -0,0 +1,251 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import inquirer from 'inquirer';
4
+ import ora from 'ora';
5
+ import { HyperdriveSigV4Service } from '../../../services/hyperdrive-sigv4.js';
6
+ import { printTable } from '../../../utils/table.js';
7
+ import { selectTrackerProject } from '../../../utils/tracker-project-flow.js';
8
+ export default class TrackerProjectUnlinkModule extends Command {
9
+ static description = 'Unlink one or more modules from an existing tracker project';
10
+ static examples = [
11
+ '<%= config.bin %> tracker project unlink-module',
12
+ '<%= config.bin %> tracker project unlink-module --tracker-project-id abc123',
13
+ '<%= config.bin %> tracker project unlink-module --tracker-project-id abc123 --modules mod1,mod2 --yes',
14
+ '<%= config.bin %> tracker project unlink-module --json',
15
+ ];
16
+ static flags = {
17
+ domain: Flags.string({
18
+ char: 'd',
19
+ description: 'Tenant domain (for multi-domain setups)',
20
+ }),
21
+ json: Flags.boolean({
22
+ default: false,
23
+ description: 'Output raw JSON response',
24
+ }),
25
+ modules: Flags.string({
26
+ description: 'Comma-separated module IDs to unlink (non-interactive)',
27
+ }),
28
+ 'tracker-project-id': Flags.string({
29
+ description: 'Tracker project ID (skips interactive selection)',
30
+ }),
31
+ yes: Flags.boolean({
32
+ char: 'y',
33
+ default: false,
34
+ description: 'Skip confirmation prompt',
35
+ }),
36
+ };
37
+ async run() {
38
+ const { flags } = await this.parse(TrackerProjectUnlinkModule);
39
+ const isJson = flags.json;
40
+ const isNonInteractive = Boolean(flags.modules);
41
+ // Auth
42
+ let service;
43
+ const authSpinner = isJson ? null : ora('Checking authentication...').start();
44
+ try {
45
+ service = new HyperdriveSigV4Service(flags.domain);
46
+ authSpinner?.succeed('Authenticated');
47
+ }
48
+ catch (error) {
49
+ authSpinner?.fail('Not authenticated');
50
+ this.error(`${error.message}\n\n` +
51
+ `Please authenticate first with: ${chalk.cyan('hd auth login')}`);
52
+ }
53
+ try {
54
+ // Step 1: Select tracker project
55
+ const project = await selectTrackerProject(service, flags['tracker-project-id']);
56
+ if (!isJson) {
57
+ this.log(`\nTracker project: ${chalk.cyan(project.externalProjectKey)} — ${project.externalProjectName}\n`);
58
+ }
59
+ // Step 2: Fetch linked modules and enrich with names
60
+ const fetchSpinner = isJson ? null : ora('Fetching linked modules...').start();
61
+ const [linkedModules, allModules] = await Promise.all([
62
+ service.trackerProjectListModules(project.trackerProjectId),
63
+ service.moduleList(),
64
+ ]);
65
+ fetchSpinner?.succeed(`Found ${linkedModules.length} linked module(s)`);
66
+ // Build module name lookup map
67
+ const moduleNameMap = new Map();
68
+ for (const mod of allModules) {
69
+ if (mod.projectId) {
70
+ moduleNameMap.set(mod.projectId, mod.name || mod.slug || mod.projectId);
71
+ }
72
+ }
73
+ // Enrich linked modules with names
74
+ const enriched = linkedModules.map(link => ({
75
+ ...link,
76
+ moduleName: moduleNameMap.get(link.moduleId) || link.moduleId,
77
+ }));
78
+ // Empty state: no modules linked
79
+ if (enriched.length === 0) {
80
+ if (isJson) {
81
+ this.log(JSON.stringify({ message: 'No modules linked to this tracker project.', results: [] }, null, 2));
82
+ }
83
+ else {
84
+ this.log(chalk.yellow('\nNo modules linked to this tracker project.'));
85
+ }
86
+ return;
87
+ }
88
+ // Step 3: Select modules to unlink
89
+ let modulesToUnlink;
90
+ if (isNonInteractive) {
91
+ // Non-interactive: parse --modules flag
92
+ const moduleIds = flags.modules.split(',').map(s => s.trim()).filter(Boolean);
93
+ modulesToUnlink = [];
94
+ for (const id of moduleIds) {
95
+ const linked = enriched.find(m => m.moduleId === id);
96
+ if (!linked) {
97
+ this.warn(`Module "${id}" is not linked to this project — skipping`);
98
+ continue;
99
+ }
100
+ modulesToUnlink.push({
101
+ moduleId: linked.moduleId,
102
+ moduleName: linked.moduleName,
103
+ role: linked.role || 'supporting',
104
+ });
105
+ }
106
+ }
107
+ else {
108
+ // Interactive: checkbox prompt
109
+ const { selectedModules } = await inquirer.prompt([{
110
+ choices: enriched.map(m => ({
111
+ name: `${m.moduleName} [${m.role || 'supporting'}]`,
112
+ value: { moduleId: m.moduleId, moduleName: m.moduleName, role: m.role || 'supporting' },
113
+ })),
114
+ message: 'Select modules to unlink (space to select, enter to confirm):',
115
+ name: 'selectedModules',
116
+ type: 'checkbox',
117
+ validate: (input) => input.length > 0 ? true : 'Select at least one module',
118
+ }]);
119
+ modulesToUnlink = selectedModules;
120
+ }
121
+ if (modulesToUnlink.length === 0) {
122
+ this.log(chalk.yellow('\nNo modules to unlink.'));
123
+ return;
124
+ }
125
+ // Step 4: Primary module warning
126
+ const hasPrimary = modulesToUnlink.some(m => m.role === 'primary');
127
+ if (hasPrimary && !isJson) {
128
+ this.log('');
129
+ this.log(chalk.yellow('\u26A0 You are unlinking the primary module. No module will be marked as primary after this.'));
130
+ }
131
+ // Step 5: Unlink info banner
132
+ if (!isJson) {
133
+ this.log('');
134
+ this.log(chalk.blue('\u2139 Unlinking a module removes it from this tracker project\'s automation scope.'));
135
+ this.log(chalk.blue(' Existing hooks will no longer fire for issues related to the unlinked module.'));
136
+ this.log('');
137
+ // Display summary
138
+ this.log(chalk.bold('Modules to unlink:'));
139
+ for (const mod of modulesToUnlink) {
140
+ const roleTag = mod.role === 'primary' ? chalk.green(` [${mod.role}]`) : chalk.dim(` [${mod.role}]`);
141
+ this.log(` - ${mod.moduleName}${roleTag}`);
142
+ }
143
+ this.log('');
144
+ }
145
+ // Step 6: Confirmation
146
+ if (!flags.yes && !isNonInteractive && !isJson) {
147
+ const { confirmed } = await inquirer.prompt([{
148
+ default: true,
149
+ message: 'Unlink these modules?',
150
+ name: 'confirmed',
151
+ type: 'confirm',
152
+ }]);
153
+ if (!confirmed) {
154
+ this.log(chalk.yellow('Cancelled. No modules were unlinked.'));
155
+ return;
156
+ }
157
+ }
158
+ // Step 7: Execute unlinking
159
+ const results = [];
160
+ for (const mod of modulesToUnlink) {
161
+ const spinner = isJson ? null : ora(`Unlinking ${chalk.cyan(mod.moduleName)}...`).start();
162
+ try {
163
+ await service.trackerProjectUnlinkModule(project.trackerProjectId, mod.moduleId);
164
+ spinner?.succeed(`Module ${chalk.cyan(mod.moduleName)} unlinked`);
165
+ results.push({
166
+ moduleId: mod.moduleId,
167
+ moduleName: mod.moduleName,
168
+ role: mod.role,
169
+ status: 'unlinked',
170
+ });
171
+ }
172
+ catch (error) {
173
+ const status = error.response?.status;
174
+ if (status === 404) {
175
+ spinner?.warn(`Module ${chalk.cyan(mod.moduleName)} is not linked — skipping`);
176
+ this.warn(`Module "${mod.moduleName}" is not linked — skipping`);
177
+ results.push({
178
+ moduleId: mod.moduleId,
179
+ moduleName: mod.moduleName,
180
+ role: mod.role,
181
+ status: 'skipped',
182
+ warning: 'Not linked',
183
+ });
184
+ }
185
+ else {
186
+ const msg = error.response?.data?.message || error.response?.data?.error || error.message;
187
+ spinner?.warn(`Failed to unlink ${chalk.cyan(mod.moduleName)}: ${msg}`);
188
+ this.warn(`Failed to unlink "${mod.moduleName}": ${msg}`);
189
+ results.push({
190
+ moduleId: mod.moduleId,
191
+ moduleName: mod.moduleName,
192
+ role: mod.role,
193
+ status: 'skipped',
194
+ warning: msg,
195
+ });
196
+ }
197
+ }
198
+ }
199
+ // Step 8: Output results
200
+ if (isJson) {
201
+ this.log(JSON.stringify(results, null, 2));
202
+ return;
203
+ }
204
+ const unlinked = results.filter(r => r.status === 'unlinked');
205
+ const skipped = results.filter(r => r.status === 'skipped');
206
+ if (unlinked.length > 0) {
207
+ this.log('');
208
+ printTable(unlinked, {
209
+ moduleName: {
210
+ header: 'Module',
211
+ minWidth: 20,
212
+ },
213
+ role: {
214
+ get: (row) => {
215
+ const r = row;
216
+ return r.role === 'primary' ? chalk.green(r.role) : chalk.dim(r.role || 'supporting');
217
+ },
218
+ header: 'Role',
219
+ minWidth: 14,
220
+ },
221
+ status: {
222
+ get: () => chalk.green('unlinked'),
223
+ header: 'Status',
224
+ },
225
+ }, (msg) => this.log(msg));
226
+ }
227
+ this.log('');
228
+ this.log(chalk.green(`${unlinked.length} module(s) unlinked successfully.`));
229
+ if (skipped.length > 0) {
230
+ this.log(chalk.yellow(`${skipped.length} module(s) skipped (not linked or error).`));
231
+ }
232
+ this.log('');
233
+ }
234
+ catch (error) {
235
+ let errorMessage = error.message;
236
+ if (error.response) {
237
+ const data = error.response.data;
238
+ if (error.response.status === 401) {
239
+ errorMessage = 'Authentication failed — please run "hd auth login"';
240
+ }
241
+ else if (data?.error) {
242
+ errorMessage = data.error;
243
+ }
244
+ else if (data?.message) {
245
+ errorMessage = data.message;
246
+ }
247
+ }
248
+ this.error(errorMessage);
249
+ }
250
+ }
251
+ }