@4ge/cli 0.1.0

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 (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +228 -0
  3. package/bin/dev.js +3 -0
  4. package/bin/run.js +3 -0
  5. package/dist/base.js +398 -0
  6. package/dist/commands/auth/login.js +197 -0
  7. package/dist/commands/auth/logout.js +31 -0
  8. package/dist/commands/auth/profile/delete.js +34 -0
  9. package/dist/commands/auth/profile/list.js +32 -0
  10. package/dist/commands/auth/profile/use.js +30 -0
  11. package/dist/commands/auth/status.js +107 -0
  12. package/dist/commands/config/get.js +37 -0
  13. package/dist/commands/config/index.js +9 -0
  14. package/dist/commands/config/manage.js +165 -0
  15. package/dist/commands/config/set.js +42 -0
  16. package/dist/commands/config/show.js +46 -0
  17. package/dist/commands/epic/index.js +10 -0
  18. package/dist/commands/epic/list.js +91 -0
  19. package/dist/commands/epic/pull.js +137 -0
  20. package/dist/commands/epic/show.js +67 -0
  21. package/dist/commands/epic/update.js +104 -0
  22. package/dist/commands/feature/index.js +2 -0
  23. package/dist/commands/feature/list.js +93 -0
  24. package/dist/commands/feature/pull.js +122 -0
  25. package/dist/commands/feature/show.js +60 -0
  26. package/dist/commands/feature/update.js +103 -0
  27. package/dist/commands/idea/create.js +88 -0
  28. package/dist/commands/idea/graduate.js +89 -0
  29. package/dist/commands/idea/index.js +5 -0
  30. package/dist/commands/idea/list.js +83 -0
  31. package/dist/commands/idea/show.js +86 -0
  32. package/dist/commands/idea/update.js +108 -0
  33. package/dist/commands/init.js +354 -0
  34. package/dist/commands/login.js +5 -0
  35. package/dist/commands/plan/index.js +10 -0
  36. package/dist/commands/plan/pull.js +104 -0
  37. package/dist/commands/plan/versions.js +64 -0
  38. package/dist/commands/project/create.js +84 -0
  39. package/dist/commands/project/index.js +9 -0
  40. package/dist/commands/project/info.js +74 -0
  41. package/dist/commands/project/list.js +78 -0
  42. package/dist/commands/project/pull.js +127 -0
  43. package/dist/commands/project/update.js +104 -0
  44. package/dist/commands/root.js +13 -0
  45. package/dist/commands/skill/index.js +8 -0
  46. package/dist/commands/skill/install.js +71 -0
  47. package/dist/commands/story/index.js +10 -0
  48. package/dist/commands/story/list.js +81 -0
  49. package/dist/commands/story/pull.js +125 -0
  50. package/dist/commands/story/show.js +57 -0
  51. package/dist/commands/story/update.js +103 -0
  52. package/dist/commands/template/create.js +72 -0
  53. package/dist/commands/template/delete.js +62 -0
  54. package/dist/commands/template/feature/create.js +70 -0
  55. package/dist/commands/template/feature/delete.js +75 -0
  56. package/dist/commands/template/feature/list.js +69 -0
  57. package/dist/commands/template/feature/update.js +82 -0
  58. package/dist/commands/template/info.js +52 -0
  59. package/dist/commands/template/list.js +76 -0
  60. package/dist/commands/template/pull.js +99 -0
  61. package/dist/commands/template/update.js +71 -0
  62. package/dist/commands/whoami.js +5 -0
  63. package/dist/core/api/action-hints.js +17 -0
  64. package/dist/core/api/index.js +3 -0
  65. package/dist/core/api/retry.js +95 -0
  66. package/dist/core/api/validate-response.js +41 -0
  67. package/dist/core/auth/api-client.js +156 -0
  68. package/dist/core/auth/config.js +3 -0
  69. package/dist/core/auth/credentials.js +143 -0
  70. package/dist/core/auth/email-auth.js +30 -0
  71. package/dist/core/auth/errors.js +20 -0
  72. package/dist/core/auth/index.js +6 -0
  73. package/dist/core/auth/realtime.js +82 -0
  74. package/dist/core/auth/resolution.js +63 -0
  75. package/dist/core/auth/state.js +9 -0
  76. package/dist/core/auth/token-refresh.js +100 -0
  77. package/dist/core/config/defaults.js +31 -0
  78. package/dist/core/config/index.js +3 -0
  79. package/dist/core/config/parser.js +213 -0
  80. package/dist/core/config/schema.js +29 -0
  81. package/dist/core/project/markdown.js +37 -0
  82. package/dist/core/skill/installer.js +59 -0
  83. package/dist/core/skill/paths.js +52 -0
  84. package/dist/generated/api/client/client.gen.js +217 -0
  85. package/dist/generated/api/client/index.js +6 -0
  86. package/dist/generated/api/client/types.gen.js +2 -0
  87. package/dist/generated/api/client/utils.gen.js +231 -0
  88. package/dist/generated/api/client.gen.js +3 -0
  89. package/dist/generated/api/core/auth.gen.js +14 -0
  90. package/dist/generated/api/core/bodySerializer.gen.js +57 -0
  91. package/dist/generated/api/core/params.gen.js +103 -0
  92. package/dist/generated/api/core/pathSerializer.gen.js +114 -0
  93. package/dist/generated/api/core/queryKeySerializer.gen.js +99 -0
  94. package/dist/generated/api/core/serverSentEvents.gen.js +136 -0
  95. package/dist/generated/api/core/types.gen.js +2 -0
  96. package/dist/generated/api/core/utils.gen.js +87 -0
  97. package/dist/generated/api/index.js +2 -0
  98. package/dist/generated/api/sdk.gen.js +556 -0
  99. package/dist/generated/api/types.gen.js +2 -0
  100. package/dist/generated/api/zod.gen.js +1262 -0
  101. package/dist/ui/components/action-hints.js +33 -0
  102. package/dist/ui/components/cli-header.js +91 -0
  103. package/dist/ui/components/confirm-prompt.js +28 -0
  104. package/dist/ui/components/data-table.js +35 -0
  105. package/dist/ui/components/detail-view.js +5 -0
  106. package/dist/ui/components/error-display.js +5 -0
  107. package/dist/ui/components/prompt-layout.js +6 -0
  108. package/dist/ui/components/select-prompt.js +28 -0
  109. package/dist/ui/components/spinner.js +14 -0
  110. package/dist/ui/components/success-message.js +5 -0
  111. package/dist/ui/components/text-input.js +29 -0
  112. package/dist/ui/hooks/use-action-hints.js +21 -0
  113. package/dist/ui/hooks/use-status.js +22 -0
  114. package/dist/ui/index.js +15 -0
  115. package/dist/ui/prompt-helpers.js +55 -0
  116. package/dist/ui/render.js +9 -0
  117. package/package.json +89 -0
  118. package/skills/4ge-cli.md +113 -0
  119. package/usage.md +320 -0
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Args } from '@oclif/core';
3
+ import { BaseCommand } from '../../base.js';
4
+ import { updateConfig, readConfig } from '../../core/config/parser.js';
5
+ import { SuccessMessage } from '../../ui/index.js';
6
+ export default class ConfigSet extends BaseCommand {
7
+ static description = 'Set a configuration value';
8
+ static args = {
9
+ key: Args.string({ description: 'Config key (dot notation, e.g., cli.default_mode)', required: true }),
10
+ value: Args.string({ description: 'Config value', required: true }),
11
+ };
12
+ async run() {
13
+ const { args } = await this.parse(ConfigSet);
14
+ // Load local config
15
+ const config = await readConfig();
16
+ if (!config) {
17
+ this.handleError('No project config found. Run `4ge init` first.', { code: 'NOT_INITIALIZED' });
18
+ return;
19
+ }
20
+ await this.renderHeader('ready', 'compact');
21
+ // Parse dot-notation key and set value
22
+ const keys = args.key.split('.');
23
+ const update = {};
24
+ let current = update;
25
+ for (let i = 0; i < keys.length - 1; i++) {
26
+ current[keys[i]] = {};
27
+ current = current[keys[i]];
28
+ }
29
+ current[keys[keys.length - 1]] = args.value;
30
+ try {
31
+ await updateConfig(update);
32
+ if (this.isJsonMode) {
33
+ this.outputJson({ [args.key]: args.value }, { source: 'config set' });
34
+ return;
35
+ }
36
+ await this.renderInk(_jsx(SuccessMessage, { message: `${args.key} set to ${args.value}` }));
37
+ }
38
+ catch (err) {
39
+ this.handleError(`Invalid config value: ${err.message}`, { code: 'INVALID_CONFIG' });
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { BaseCommand } from '../../base.js';
3
+ import { readConfig } from '../../core/config/parser.js';
4
+ import { DataTable } from '../../ui/components/data-table.js';
5
+ export default class ConfigShow extends BaseCommand {
6
+ static description = 'Show all configuration values';
7
+ static flags = {
8
+ ...BaseCommand.baseFlags,
9
+ };
10
+ async run() {
11
+ const config = await readConfig();
12
+ if (!config) {
13
+ this.handleError('No project config found. Run `4ge init` first.', { code: 'NOT_INITIALIZED' });
14
+ return;
15
+ }
16
+ await this.renderHeader('ready', 'compact');
17
+ if (this.isJsonMode) {
18
+ this.outputJson(config);
19
+ return;
20
+ }
21
+ const flattened = this.flattenObject(config);
22
+ const data = Object.entries(flattened).map(([key, value]) => ({ key, value: String(value) }));
23
+ await this.renderInk(_jsx(DataTable, { data: data, columns: [
24
+ { key: 'key', header: 'Key' },
25
+ { key: 'value', header: 'Value' }
26
+ ] }));
27
+ if (this.isInteractiveMode) {
28
+ await this.handleActionHints([
29
+ { label: 'Set config', command: '4ge config set <key> <value>' },
30
+ { label: 'Get config', command: '4ge config get <key>' },
31
+ ]);
32
+ }
33
+ }
34
+ flattenObject(obj, prefix = '') {
35
+ return Object.keys(obj).reduce((acc, k) => {
36
+ const pre = prefix.length ? prefix + '.' : '';
37
+ if (typeof obj[k] === 'object' && obj[k] !== null && !Array.isArray(obj[k])) {
38
+ Object.assign(acc, this.flattenObject(obj[k], pre + k));
39
+ }
40
+ else {
41
+ acc[pre + k] = obj[k];
42
+ }
43
+ return acc;
44
+ }, {});
45
+ }
46
+ }
@@ -0,0 +1,10 @@
1
+ import { Help } from '@oclif/core';
2
+ import { BaseCommand } from '../../base.js';
3
+ export default class EpicIndex extends BaseCommand {
4
+ static description = 'Manage project epics';
5
+ async run() {
6
+ await this.renderHeader('ready', 'compact');
7
+ const help = new Help(this.config);
8
+ await help.showHelp(['epic']);
9
+ }
10
+ }
@@ -0,0 +1,91 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../base.js';
4
+ import { readConfig } from '../../core/config/index.js';
5
+ import { listEpics } from '../../generated/api/sdk.gen.js';
6
+ import { zListEpicsResponse } from '../../generated/api/zod.gen.js';
7
+ import { validateResponse } from '../../core/api/index.js';
8
+ import { DataTable } from '../../ui/index.js';
9
+ export default class EpicList extends BaseCommand {
10
+ static description = 'List epics for the current project';
11
+ static examples = [
12
+ '<%= config.bin %> epic:list',
13
+ '<%= config.bin %> epic:list --roadmap next --json',
14
+ '<%= config.bin %> epic:list --status IN_DEVELOPMENT',
15
+ ];
16
+ static flags = {
17
+ ...BaseCommand.baseFlags,
18
+ roadmap: Flags.string({
19
+ description: 'Filter by roadmap status (now|next|later)',
20
+ options: ['now', 'next', 'later'],
21
+ }),
22
+ status: Flags.string({
23
+ description: 'Filter by status (e.g. BACKLOG, IN_DEVELOPMENT, COMPLETE)',
24
+ }),
25
+ };
26
+ async run() {
27
+ const { flags } = await this.parse(EpicList);
28
+ await this.renderHeader('ready', 'compact');
29
+ // 1. Resolve Auth and Config
30
+ const client = await this.getClient();
31
+ const config = await readConfig();
32
+ if (!config?.project_id) {
33
+ const message = 'Project not initialized. Run `4ge init` first to set project context.';
34
+ if (this.isJsonMode) {
35
+ this.outputErrorAndExit({ message, code: 'NOT_INITIALIZED' });
36
+ }
37
+ else {
38
+ this.error(message);
39
+ }
40
+ return;
41
+ }
42
+ // 2. Call SDK
43
+ try {
44
+ const response = await this.withSpinner('Fetching epics...', () => listEpics({
45
+ client,
46
+ path: {
47
+ projectId: config.project_id,
48
+ },
49
+ query: {
50
+ ...(flags.roadmap ? { roadmap: flags.roadmap } : {}),
51
+ ...(flags.status ? { status: flags.status } : {}),
52
+ },
53
+ }));
54
+ const { data, error } = response;
55
+ const validated = validateResponse(data, error, zListEpicsResponse);
56
+ const items = validated.data;
57
+ const metadata = validated.metadata;
58
+ if (this.isJsonMode) {
59
+ this.outputJson(items, { source: 'epic list' });
60
+ }
61
+ else {
62
+ await this.renderInk(_jsx(DataTable, { data: items, columns: [
63
+ { key: 'label', header: 'Title', minWidth: 20 },
64
+ { key: 'id', header: 'ID' },
65
+ { key: 'status', header: 'Status', color: 'auto' },
66
+ { key: 'roadmap_status', header: 'Roadmap' },
67
+ ], title: `Epics in ${config.project_id}${flags.roadmap ? ` (${flags.roadmap})` : ''}`, emptyMessage: `No epics found${flags.roadmap ? ` for roadmap: ${flags.roadmap}` : ''}` }));
68
+ if (this.isInteractiveMode) {
69
+ if (items.length > 0) {
70
+ const options = items.map((item) => ({
71
+ label: item.label || item.id,
72
+ value: item.id,
73
+ description: item.status || '',
74
+ }));
75
+ const selectedId = await this.selectOption('Select an epic for details:', options, { headerVariant: 'compact' });
76
+ if (selectedId) {
77
+ const { default: chalk } = await import('chalk');
78
+ this.log(`\n 🚀 Run: ${chalk.cyan(`4ge epic show ${selectedId}`)}\n`);
79
+ }
80
+ }
81
+ if (metadata?.action_hints) {
82
+ await this.handleActionHints(metadata.action_hints);
83
+ }
84
+ }
85
+ }
86
+ }
87
+ catch (err) {
88
+ this.handleError('An unexpected error occurred', err);
89
+ }
90
+ }
91
+ }
@@ -0,0 +1,137 @@
1
+ import { Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import { BaseCommand } from '../../base.js';
4
+ import { readConfig, getCollectionDir } from '../../core/config/index.js';
5
+ import { resolveRecursive, resolveRoadmapFilter } from '../../core/config/defaults.js';
6
+ import { getEpicDetail, getEpicTree } from '../../generated/api/sdk.gen.js';
7
+ import { zGetEpicTreeResponse, zGetEpicDetailResponse } from '../../generated/api/zod.gen.js';
8
+ import { validateResponse } from '../../core/api/index.js';
9
+ import fs from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ export default class EpicPull extends BaseCommand {
12
+ static description = 'Download epic details and optionally its hierarchy';
13
+ static examples = [
14
+ '<%= config.bin %> epic:pull --id <uuid>',
15
+ '<%= config.bin %> epic:pull --id <uuid> --json',
16
+ '<%= config.bin %> epic:pull --id <uuid> --recursive',
17
+ '<%= config.bin %> epic:pull --id <uuid> --recursive --roadmap now --status BACKLOG',
18
+ ];
19
+ static flags = {
20
+ ...BaseCommand.baseFlags,
21
+ id: Flags.string({
22
+ description: 'Epic ID',
23
+ required: true,
24
+ }),
25
+ recursive: Flags.boolean({
26
+ description: 'Pull full hierarchy tree instead of just the epic detail',
27
+ allowNo: true,
28
+ }),
29
+ roadmap: Flags.string({
30
+ description: 'Filter hierarchy by roadmap status (now|next|later). Only used with --recursive.',
31
+ options: ['now', 'next', 'later'],
32
+ }),
33
+ status: Flags.string({
34
+ description: 'Filter hierarchy by status. Only used with --recursive.',
35
+ }),
36
+ save: Flags.boolean({
37
+ description: 'Force save to disk even in JSON mode',
38
+ default: false,
39
+ }),
40
+ stdout: Flags.boolean({
41
+ description: 'Print to stdout without saving',
42
+ default: false,
43
+ }),
44
+ };
45
+ async run() {
46
+ const { flags } = await this.parse(EpicPull);
47
+ if (flags.json && flags.stdout) {
48
+ this.error('Cannot use --json and --stdout together');
49
+ }
50
+ if (flags.save && flags.stdout) {
51
+ this.error('Cannot use --stdout and --save together');
52
+ }
53
+ await this.renderHeader('ready', 'compact');
54
+ await this.migrateAssetsIfNeeded();
55
+ const client = await this.getClient();
56
+ const config = await readConfig();
57
+ if (!config?.project_id) {
58
+ const message = 'Project not initialized. Run `4ge init` first to set project context.';
59
+ if (this.isJsonMode) {
60
+ this.outputErrorAndExit({ message, code: 'NOT_INITIALIZED' });
61
+ }
62
+ else {
63
+ this.error(message);
64
+ }
65
+ return;
66
+ }
67
+ const isRecursive = resolveRecursive(flags.recursive, config);
68
+ const spinnerMsg = isRecursive ? `Pulling epic ${flags.id} tree...` : `Pulling epic ${flags.id}...`;
69
+ const roadmapFilter = resolveRoadmapFilter(flags.roadmap, config);
70
+ const response = await this.withSpinner(spinnerMsg, () => {
71
+ if (isRecursive) {
72
+ return getEpicTree({
73
+ client,
74
+ path: {
75
+ projectId: config.project_id,
76
+ epicId: flags.id,
77
+ },
78
+ query: {
79
+ ...(roadmapFilter ? { roadmap: roadmapFilter } : {}),
80
+ ...(flags.status ? { status: flags.status } : {}),
81
+ },
82
+ });
83
+ }
84
+ else {
85
+ return getEpicDetail({
86
+ client,
87
+ path: {
88
+ projectId: config.project_id,
89
+ epicId: flags.id,
90
+ },
91
+ });
92
+ }
93
+ });
94
+ const { data, error } = response;
95
+ try {
96
+ let payload;
97
+ let metadata;
98
+ if (isRecursive) {
99
+ // The generated tree schema models `data` as optional (the tree payload
100
+ // is loosely typed). Cast to the envelope type — the validation still
101
+ // runs and the payload is treated as `unknown`.
102
+ const validated = validateResponse(data, error, zGetEpicTreeResponse);
103
+ payload = validated.data;
104
+ metadata = validated.metadata;
105
+ }
106
+ else {
107
+ const validated = validateResponse(data, error, zGetEpicDetailResponse);
108
+ payload = validated.data;
109
+ metadata = validated.metadata;
110
+ }
111
+ const savePath = path.join(getCollectionDir('epics'), `${flags.id}.json`);
112
+ if (this.isJsonMode) {
113
+ this.outputJson(payload);
114
+ if (flags.save) {
115
+ await this.saveToFile(savePath, payload);
116
+ }
117
+ }
118
+ else if (flags.stdout) {
119
+ this.log(JSON.stringify(payload, null, 2));
120
+ }
121
+ else {
122
+ await this.saveToFile(savePath, payload);
123
+ this.log(`${chalk.green('✓')} Epic saved to .4ge/assets/epics/${flags.id}.json`);
124
+ if (this.isInteractiveMode && metadata?.action_hints) {
125
+ await this.handleActionHints(metadata.action_hints);
126
+ }
127
+ }
128
+ }
129
+ catch (err) {
130
+ this.handleError(`Failed to pull epic: ${flags.id}`, err);
131
+ }
132
+ }
133
+ async saveToFile(filePath, data) {
134
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
135
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
136
+ }
137
+ }
@@ -0,0 +1,67 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base.js';
3
+ import { readConfig } from '../../core/config/index.js';
4
+ import { getEpicDetail } from '../../generated/api/sdk.gen.js';
5
+ import { zGetEpicDetailResponse } from '../../generated/api/zod.gen.js';
6
+ import { validateResponse } from '../../core/api/index.js';
7
+ export default class EpicShow extends BaseCommand {
8
+ static description = 'Show detailed information for a specific epic';
9
+ static examples = [
10
+ '<%= config.bin %> epic:show --id <uuid>',
11
+ '<%= config.bin %> epic:show --id <uuid> --json',
12
+ ];
13
+ static flags = {
14
+ ...BaseCommand.baseFlags,
15
+ id: Flags.string({
16
+ description: 'Epic ID',
17
+ required: true,
18
+ }),
19
+ };
20
+ async run() {
21
+ const { flags } = await this.parse(EpicShow);
22
+ const client = await this.getClient();
23
+ const config = await readConfig();
24
+ if (!config?.project_id) {
25
+ const message = 'Project not initialized. Run `4ge init` first to set project context.';
26
+ if (this.isJsonMode) {
27
+ this.outputErrorAndExit({ message, code: 'NOT_INITIALIZED' });
28
+ }
29
+ else {
30
+ this.error(message);
31
+ }
32
+ return;
33
+ }
34
+ await this.renderHeader('ready', 'compact');
35
+ const response = await this.withSpinner(`Fetching epic ${flags.id}...`, () => getEpicDetail({
36
+ client,
37
+ path: {
38
+ projectId: config.project_id,
39
+ epicId: flags.id
40
+ },
41
+ }));
42
+ const { data, error } = response;
43
+ try {
44
+ const validated = validateResponse(data, error, zGetEpicDetailResponse);
45
+ const payload = validated.data;
46
+ const metadata = validated.metadata;
47
+ if (this.isJsonMode) {
48
+ this.outputJson(payload, { source: 'epic show' });
49
+ }
50
+ else {
51
+ this.log(`\nEpic: ${payload.label}`);
52
+ this.log(`ID: ${payload.id}`);
53
+ this.log(`Status: ${payload.status}`);
54
+ this.log(`Roadmap: ${payload.roadmap_status || 'N/A'}`);
55
+ if (payload.description) {
56
+ this.log(`\nDescription:\n${payload.description}\n`);
57
+ }
58
+ if (this.isInteractiveMode && metadata?.action_hints) {
59
+ await this.handleActionHints(metadata.action_hints);
60
+ }
61
+ }
62
+ }
63
+ catch (err) {
64
+ this.handleError(`Failed to fetch epic detail for ID: ${flags.id}`, err);
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../base.js';
4
+ import { updateEpic } from '../../generated/api/sdk.gen.js';
5
+ import { zUpdateEpicResponse } from '../../generated/api/zod.gen.js';
6
+ import { validateResponse } from '../../core/api/index.js';
7
+ import { SuccessMessage } from '../../ui/index.js';
8
+ export default class EpicUpdate extends BaseCommand {
9
+ static description = 'Update an epic';
10
+ static examples = [
11
+ '<%= config.bin %> epic:update --id <uuid> --status IN_DEVELOPMENT',
12
+ '<%= config.bin %> epic:update --id <uuid> --status COMPLETE --title "New Title"',
13
+ ];
14
+ static flags = {
15
+ ...BaseCommand.baseFlags,
16
+ id: Flags.string({
17
+ description: 'Epic ID (UUID)',
18
+ required: true,
19
+ }),
20
+ status: Flags.string({
21
+ description: 'New status (BACKLOG|NOT_STARTED|IN_PLANNING|READY_FOR_DEVELOPMENT|IN_DEVELOPMENT|BLOCKED|REVIEW|COMPLETE)',
22
+ options: ['BACKLOG', 'NOT_STARTED', 'IN_PLANNING', 'READY_FOR_DEVELOPMENT', 'IN_DEVELOPMENT', 'BLOCKED', 'REVIEW', 'COMPLETE'],
23
+ }),
24
+ title: Flags.string({
25
+ description: 'New title',
26
+ }),
27
+ description: Flags.string({
28
+ description: 'New description',
29
+ }),
30
+ priority: Flags.string({
31
+ description: 'New priority',
32
+ }),
33
+ 'roadmap-status': Flags.string({
34
+ description: 'New roadmap status (now|next|later)',
35
+ options: ['now', 'next', 'later'],
36
+ }),
37
+ force: Flags.boolean({
38
+ description: 'Skip confirmation prompt',
39
+ default: false,
40
+ }),
41
+ };
42
+ async run() {
43
+ const { flags } = await this.parse(EpicUpdate);
44
+ const projectId = await this.getProjectIdOrExit();
45
+ await this.renderHeader('ready', 'compact');
46
+ const body = {};
47
+ if (flags.status !== undefined)
48
+ body.status = flags.status;
49
+ if (flags.title !== undefined)
50
+ body.label = flags.title;
51
+ if (flags.description !== undefined)
52
+ body.description = flags.description;
53
+ if (flags.priority !== undefined)
54
+ body.priority = flags.priority;
55
+ if (flags['roadmap-status'] !== undefined)
56
+ body.roadmap_status = flags['roadmap-status'];
57
+ if (Object.keys(body).length === 0) {
58
+ const message = 'At least one field must be provided to update (--status, --title, --description, --priority, --roadmap-status)';
59
+ if (this.isJsonMode) {
60
+ this.outputErrorAndExit({ message, code: 'MISSING_INPUT' });
61
+ }
62
+ else {
63
+ this.error(message);
64
+ }
65
+ }
66
+ const confirmed = await this.confirmAction(`Update epic ${flags.id}?`, flags.force, { headerVariant: 'compact' });
67
+ if (!confirmed)
68
+ return;
69
+ const payload = {
70
+ path: {
71
+ projectId,
72
+ epicId: flags.id,
73
+ },
74
+ body,
75
+ };
76
+ if (this.isDryRun) {
77
+ await this.handleDryRun(payload);
78
+ return;
79
+ }
80
+ const client = await this.getClient();
81
+ const response = await this.withSpinner(`Updating epic ${flags.id}...`, () => updateEpic({
82
+ client,
83
+ ...payload,
84
+ }));
85
+ const { data, error } = response;
86
+ try {
87
+ const validated = validateResponse(data, error, zUpdateEpicResponse);
88
+ const epic = validated.data;
89
+ const metadata = validated.metadata;
90
+ if (this.isJsonMode) {
91
+ this.outputJson(epic, { source: 'epic update', ...metadata });
92
+ }
93
+ else {
94
+ await this.renderInk(_jsx(SuccessMessage, { message: `Epic ${flags.id} updated successfully!` }));
95
+ if (this.isInteractiveMode && metadata?.action_hints) {
96
+ await this.handleActionHints(metadata.action_hints);
97
+ }
98
+ }
99
+ }
100
+ catch (err) {
101
+ this.handleError(`Failed to update epic: ${flags.id}`, err);
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,2 @@
1
+ export { default as list } from './list.js';
2
+ export { default as show } from './show.js';
@@ -0,0 +1,93 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../base.js';
4
+ import { readConfig } from '../../core/config/index.js';
5
+ import { listFeatures } from '../../generated/api/sdk.gen.js';
6
+ import { zListFeaturesResponse } from '../../generated/api/zod.gen.js';
7
+ import { validateResponse } from '../../core/api/index.js';
8
+ import { DataTable } from '../../ui/index.js';
9
+ export default class FeatureList extends BaseCommand {
10
+ static description = 'List features for the current project';
11
+ static examples = [
12
+ '<%= config.bin %> feature:list',
13
+ '<%= config.bin %> feature:list --roadmap next --json',
14
+ '<%= config.bin %> feature:list --status IN_DEVELOPMENT',
15
+ ];
16
+ static flags = {
17
+ ...BaseCommand.baseFlags,
18
+ roadmap: Flags.string({
19
+ description: 'Filter by roadmap status (now|next|later)',
20
+ options: ['now', 'next', 'later'],
21
+ }),
22
+ status: Flags.string({
23
+ description: 'Filter by status (e.g. BACKLOG, IN_DEVELOPMENT, COMPLETE)',
24
+ }),
25
+ };
26
+ async run() {
27
+ const { flags } = await this.parse(FeatureList);
28
+ await this.renderHeader('ready', 'compact');
29
+ // 1. Resolve Auth and Config
30
+ const client = await this.getClient();
31
+ const config = await readConfig();
32
+ if (!config?.project_id) {
33
+ const message = 'Project not initialized. Run `4ge init` first to set project context.';
34
+ if (this.isJsonMode) {
35
+ this.outputErrorAndExit({ message, code: 'NOT_INITIALIZED' });
36
+ }
37
+ else {
38
+ this.error(message);
39
+ }
40
+ return;
41
+ }
42
+ // 2. Call SDK
43
+ try {
44
+ const response = await this.withSpinner('Fetching features...', () => listFeatures({
45
+ client,
46
+ path: {
47
+ projectId: config.project_id,
48
+ },
49
+ query: {
50
+ ...(flags.roadmap ? { roadmap: flags.roadmap } : {}),
51
+ ...(flags.status ? { status: flags.status } : {}),
52
+ },
53
+ }));
54
+ const { data, error } = response;
55
+ const validated = validateResponse(data, error, zListFeaturesResponse);
56
+ const items = validated.data;
57
+ const metadata = validated.metadata;
58
+ // 3. Dual output
59
+ if (this.isJsonMode) {
60
+ this.outputJson(items, { source: 'feature list' });
61
+ }
62
+ else {
63
+ await this.renderInk(_jsx(DataTable, { data: items, columns: [
64
+ { key: 'label', header: 'Title', minWidth: 20 },
65
+ { key: 'id', header: 'ID' },
66
+ { key: 'status', header: 'Status', color: 'auto' },
67
+ { key: 'roadmap_status', header: 'Roadmap' },
68
+ { key: 'priority', header: 'Priority' },
69
+ ], title: `Features in ${config.project_id}${flags.roadmap ? ` (${flags.roadmap})` : ''}`, emptyMessage: `No features found${flags.roadmap ? ` for roadmap: ${flags.roadmap}` : ''}` }));
70
+ if (this.isInteractiveMode) {
71
+ if (items.length > 0) {
72
+ const options = items.map((item) => ({
73
+ label: item.label || item.id,
74
+ value: item.id,
75
+ description: item.status || '',
76
+ }));
77
+ const selectedId = await this.selectOption('Select a feature for details:', options, { headerVariant: 'compact' });
78
+ if (selectedId) {
79
+ const { default: chalk } = await import('chalk');
80
+ this.log(`\n 🚀 Run: ${chalk.cyan(`4ge feature show ${selectedId}`)}\n`);
81
+ }
82
+ }
83
+ if (metadata?.action_hints) {
84
+ await this.handleActionHints(metadata.action_hints);
85
+ }
86
+ }
87
+ }
88
+ }
89
+ catch (err) {
90
+ this.handleError('An unexpected error occurred', err);
91
+ }
92
+ }
93
+ }