@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,125 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../base.js';
4
+ import { getCollectionDir } from '../../core/config/index.js';
5
+ import { resolveRecursive, resolveRoadmapFilter } from '../../core/config/defaults.js';
6
+ import { getStoryDetail, getStoryTree } from '../../generated/api/sdk.gen.js';
7
+ import { zGetStoryTreeResponse, zGetStoryDetailResponse } from '../../generated/api/zod.gen.js';
8
+ import { validateResponse } from '../../core/api/index.js';
9
+ import { SuccessMessage } from '../../ui/index.js';
10
+ import fs from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ export default class StoryPull extends BaseCommand {
13
+ static description = 'Download story details and optionally its hierarchy';
14
+ static examples = [
15
+ '<%= config.bin %> story:pull --id <uuid>',
16
+ '<%= config.bin %> story:pull --id <uuid> --json',
17
+ '<%= config.bin %> story:pull --id <uuid> --recursive',
18
+ '<%= config.bin %> story:pull --id <uuid> --recursive --roadmap now --status BACKLOG',
19
+ ];
20
+ static flags = {
21
+ ...BaseCommand.baseFlags,
22
+ id: Flags.string({
23
+ description: 'Story ID',
24
+ required: true,
25
+ }),
26
+ recursive: Flags.boolean({
27
+ description: 'Pull full hierarchy tree instead of just the story detail',
28
+ allowNo: true,
29
+ }),
30
+ roadmap: Flags.string({
31
+ description: 'Filter hierarchy by roadmap status (now|next|later). Only used with --recursive.',
32
+ options: ['now', 'next', 'later'],
33
+ }),
34
+ status: Flags.string({
35
+ description: 'Filter hierarchy by status. Only used with --recursive.',
36
+ }),
37
+ save: Flags.boolean({
38
+ description: 'Force save to disk even in JSON mode',
39
+ default: false,
40
+ }),
41
+ stdout: Flags.boolean({
42
+ description: 'Print to stdout without saving',
43
+ default: false,
44
+ }),
45
+ };
46
+ async run() {
47
+ const { flags } = await this.parse(StoryPull);
48
+ if (flags.json && flags.stdout) {
49
+ this.error('Cannot use --json and --stdout together');
50
+ }
51
+ if (flags.save && flags.stdout) {
52
+ this.error('Cannot use --stdout and --save together');
53
+ }
54
+ await this.renderHeader('ready', 'compact');
55
+ await this.migrateAssetsIfNeeded();
56
+ const client = await this.getClient();
57
+ const projectId = await this.getProjectIdOrExit();
58
+ const isRecursive = resolveRecursive(flags.recursive, this.localConfig);
59
+ const spinnerMsg = isRecursive ? `Pulling story ${flags.id} tree...` : `Pulling story ${flags.id}...`;
60
+ const roadmapFilter = resolveRoadmapFilter(flags.roadmap, this.localConfig);
61
+ const response = await this.withSpinner(spinnerMsg, () => {
62
+ if (isRecursive) {
63
+ return getStoryTree({
64
+ client,
65
+ path: {
66
+ projectId,
67
+ storyId: flags.id,
68
+ },
69
+ query: {
70
+ ...(roadmapFilter ? { roadmap: roadmapFilter } : {}),
71
+ ...(flags.status ? { status: flags.status } : {}),
72
+ },
73
+ });
74
+ }
75
+ else {
76
+ return getStoryDetail({
77
+ client,
78
+ path: {
79
+ projectId,
80
+ storyId: flags.id,
81
+ },
82
+ });
83
+ }
84
+ });
85
+ const { data, error } = response;
86
+ try {
87
+ let payload;
88
+ let metadata;
89
+ if (isRecursive) {
90
+ // The generated tree schema models `data` as optional (the tree payload
91
+ // is loosely typed). Cast to the envelope type — the validation still
92
+ // runs and the payload is treated as `unknown`.
93
+ const validated = validateResponse(data, error, zGetStoryTreeResponse);
94
+ payload = validated.data;
95
+ metadata = validated.metadata;
96
+ }
97
+ else {
98
+ const validated = validateResponse(data, error, zGetStoryDetailResponse);
99
+ payload = validated.data;
100
+ metadata = validated.metadata;
101
+ }
102
+ const savePath = path.join(getCollectionDir('stories'), `${flags.id}.json`);
103
+ if (this.isJsonMode) {
104
+ this.outputJson(payload);
105
+ if (flags.save) {
106
+ await this.saveToFile(savePath, payload);
107
+ }
108
+ }
109
+ else if (flags.stdout) {
110
+ this.log(JSON.stringify(payload, null, 2));
111
+ }
112
+ else {
113
+ await this.saveToFile(savePath, payload);
114
+ await this.renderInk(_jsx(SuccessMessage, { message: `Story saved to .4ge/assets/stories/${flags.id}.json` }));
115
+ }
116
+ }
117
+ catch (err) {
118
+ this.handleError(`Failed to pull story: ${flags.id}`, err);
119
+ }
120
+ }
121
+ async saveToFile(filePath, data) {
122
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
123
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
124
+ }
125
+ }
@@ -0,0 +1,57 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { BaseCommand } from '../../base.js';
3
+ import { getStoryDetail } from '../../generated/api/sdk.gen.js';
4
+ import { zGetStoryDetailResponse } from '../../generated/api/zod.gen.js';
5
+ import { validateResponse } from '../../core/api/index.js';
6
+ export default class StoryShow extends BaseCommand {
7
+ static description = 'Show detailed information for a specific story';
8
+ static examples = [
9
+ '<%= config.bin %> story:show --id <uuid>',
10
+ '<%= config.bin %> story:show --id <uuid> --json',
11
+ ];
12
+ static flags = {
13
+ ...BaseCommand.baseFlags,
14
+ id: Flags.string({
15
+ description: 'Story ID',
16
+ required: true,
17
+ }),
18
+ };
19
+ async run() {
20
+ const { flags } = await this.parse(StoryShow);
21
+ const client = await this.getClient();
22
+ const projectId = await this.getProjectIdOrExit();
23
+ await this.renderHeader('ready', 'compact');
24
+ const response = await this.withSpinner(`Fetching story ${flags.id}...`, () => getStoryDetail({
25
+ client,
26
+ path: {
27
+ projectId,
28
+ storyId: flags.id
29
+ },
30
+ }));
31
+ const { data, error } = response;
32
+ try {
33
+ const validated = validateResponse(data, error, zGetStoryDetailResponse);
34
+ const payload = validated.data;
35
+ const metadata = validated.metadata;
36
+ if (this.isJsonMode) {
37
+ this.outputJson(payload, { source: 'story show' });
38
+ }
39
+ else {
40
+ this.log(`\nStory: ${payload.label}`);
41
+ this.log(`ID: ${payload.id}`);
42
+ this.log(`Status: ${payload.status}`);
43
+ this.log(`Roadmap: ${payload.roadmap_status}`);
44
+ this.log(`Epic ID: ${payload.parent_id || 'None'}`);
45
+ if (payload.description) {
46
+ this.log(`\nDescription:\n${payload.description}\n`);
47
+ }
48
+ if (this.isInteractiveMode && metadata?.action_hints) {
49
+ await this.handleActionHints(metadata.action_hints);
50
+ }
51
+ }
52
+ }
53
+ catch (err) {
54
+ this.handleError(`Failed to fetch story detail for ID: ${flags.id}`, err);
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,103 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../base.js';
4
+ import { updateStory } from '../../generated/api/sdk.gen.js';
5
+ import { zUpdateStoryResponse } 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 StoryUpdate extends BaseCommand {
9
+ static description = 'Update a story';
10
+ static examples = [
11
+ '<%= config.bin %> story:update --id <uuid> --status IN_DEVELOPMENT',
12
+ '<%= config.bin %> story:update --id <uuid> --status COMPLETE --title "New Title"',
13
+ ];
14
+ static flags = {
15
+ ...BaseCommand.baseFlags,
16
+ id: Flags.string({
17
+ description: 'Story 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(StoryUpdate);
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 story ${flags.id}?`, flags.force, { headerVariant: 'compact' });
67
+ if (!confirmed)
68
+ return;
69
+ const payload = {
70
+ path: {
71
+ projectId,
72
+ storyId: 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 { data, error } = await this.withSpinner(`Updating story ${flags.id}...`, () => updateStory({
82
+ client,
83
+ ...payload,
84
+ }));
85
+ try {
86
+ const validated = validateResponse(data, error, zUpdateStoryResponse);
87
+ const story = validated.data;
88
+ const metadata = validated.metadata;
89
+ if (this.isJsonMode) {
90
+ this.outputJson(story, { source: 'story update', ...metadata });
91
+ }
92
+ else {
93
+ await this.renderInk(_jsx(SuccessMessage, { message: `Story ${flags.id} updated successfully!` }));
94
+ if (this.isInteractiveMode && metadata?.action_hints) {
95
+ await this.handleActionHints(metadata.action_hints);
96
+ }
97
+ }
98
+ }
99
+ catch (err) {
100
+ this.handleError(`Failed to update story: ${flags.id}`, err);
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../base.js';
4
+ import { createTemplate } from '../../generated/api/sdk.gen.js';
5
+ import { zCreateTemplateResponse } 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 TemplateCreate extends BaseCommand {
9
+ static description = 'Create a new project template';
10
+ static flags = {
11
+ ...BaseCommand.baseFlags,
12
+ name: Flags.string({
13
+ description: 'Template name',
14
+ required: true,
15
+ }),
16
+ 'workspace-id': Flags.string({
17
+ description: 'Workspace ID',
18
+ required: true,
19
+ }),
20
+ description: Flags.string({
21
+ description: 'Template description',
22
+ }),
23
+ force: Flags.boolean({
24
+ description: 'Skip confirmation prompt',
25
+ default: false,
26
+ }),
27
+ };
28
+ static examples = [
29
+ '<%= config.bin %> template:create --name "My Template" --workspace-id <workspace_id>',
30
+ '<%= config.bin %> template:create --name "My Template" --workspace-id <workspace_id> --description "A great template"',
31
+ ];
32
+ async run() {
33
+ const { flags } = await this.parse(TemplateCreate);
34
+ await this.renderHeader('ready', 'compact');
35
+ const confirmed = await this.confirmAction(`Create new template "${flags.name}"?`, flags.force, { headerVariant: 'compact' });
36
+ if (!confirmed)
37
+ return;
38
+ const payload = {
39
+ body: {
40
+ name: flags.name,
41
+ workspace_id: flags['workspace-id'],
42
+ description: flags.description,
43
+ },
44
+ };
45
+ if (this.isDryRun) {
46
+ await this.handleDryRun(payload);
47
+ return;
48
+ }
49
+ const client = await this.getClient();
50
+ const { data, error } = await this.withSpinner('Creating template...', () => createTemplate({
51
+ client,
52
+ ...payload,
53
+ }));
54
+ try {
55
+ const validated = validateResponse(data, error, zCreateTemplateResponse);
56
+ const template = validated.data;
57
+ const metadata = validated.metadata;
58
+ if (this.isJsonMode) {
59
+ this.outputJson(template, { source: 'template create', ...metadata });
60
+ }
61
+ else {
62
+ await this.renderInk(_jsx(SuccessMessage, { message: `Template "${template.name}" (id: ${template.id}) has been created successfully.` }));
63
+ if (this.isInteractiveMode && metadata?.action_hints) {
64
+ await this.handleActionHints(metadata.action_hints);
65
+ }
66
+ }
67
+ }
68
+ catch (err) {
69
+ this.handleError('Failed to create template', err);
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../base.js';
4
+ import { deleteTemplate } from '../../generated/api/sdk.gen.js';
5
+ import { zDeleteTemplateResponse } 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 TemplateDelete extends BaseCommand {
9
+ static description = 'Delete a project template';
10
+ static flags = {
11
+ ...BaseCommand.baseFlags,
12
+ id: Flags.string({
13
+ description: 'Template ID',
14
+ required: true,
15
+ }),
16
+ force: Flags.boolean({
17
+ description: 'Skip confirmation prompt',
18
+ char: 'f',
19
+ }),
20
+ };
21
+ static examples = [
22
+ '<%= config.bin %> template:delete --id <template_id>',
23
+ '<%= config.bin %> template:delete --id <template_id> --force',
24
+ ];
25
+ async run() {
26
+ const { flags } = await this.parse(TemplateDelete);
27
+ await this.renderHeader('ready', 'compact');
28
+ if (!(await this.confirmAction(`Delete template ${flags.id}? This cannot be undone.`, flags.force, { headerVariant: 'compact' }))) {
29
+ return;
30
+ }
31
+ const payload = {
32
+ path: { templateId: flags.id },
33
+ };
34
+ if (this.isDryRun) {
35
+ await this.handleDryRun(payload);
36
+ return;
37
+ }
38
+ const client = await this.getClient();
39
+ const { data, error } = await this.withSpinner('Deleting template...', () => deleteTemplate({
40
+ client,
41
+ ...payload,
42
+ }));
43
+ let metadata;
44
+ try {
45
+ const validated = validateResponse(data, error, zDeleteTemplateResponse);
46
+ metadata = validated.metadata;
47
+ }
48
+ catch (err) {
49
+ this.handleError(`Failed to delete template: ${flags.id}`, err);
50
+ return;
51
+ }
52
+ if (this.isJsonMode) {
53
+ this.outputJson({ success: true }, { source: 'template delete', id: flags.id, ...metadata });
54
+ }
55
+ else {
56
+ await this.renderInk(_jsx(SuccessMessage, { message: `Template ${flags.id} has been deleted.` }));
57
+ if (this.isInteractiveMode && metadata?.action_hints) {
58
+ await this.handleActionHints(metadata.action_hints);
59
+ }
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,70 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../../base.js';
4
+ import { createTemplateFeature } from '../../../generated/api/sdk.gen.js';
5
+ import { zCreateTemplateFeatureResponse } 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 TemplateFeatureCreate extends BaseCommand {
9
+ static description = 'Create a feature for a template';
10
+ static flags = {
11
+ ...BaseCommand.baseFlags,
12
+ id: Flags.string({
13
+ description: 'Template ID',
14
+ required: true,
15
+ }),
16
+ name: Flags.string({
17
+ description: 'Feature name',
18
+ required: true,
19
+ }),
20
+ description: Flags.string({
21
+ description: 'Feature description',
22
+ }),
23
+ public: Flags.boolean({
24
+ description: 'Whether the feature is public',
25
+ default: false,
26
+ }),
27
+ };
28
+ static examples = [
29
+ '<%= config.bin %> template:feature:create --id <template_id> --name "Authentication"',
30
+ '<%= config.bin %> template:feature:create --id <template_id> --name "Auth" --description "Auth feature" --public',
31
+ ];
32
+ async run() {
33
+ const { flags } = await this.parse(TemplateFeatureCreate);
34
+ await this.renderHeader('ready', 'compact');
35
+ const payload = {
36
+ path: { templateId: flags.id },
37
+ body: {
38
+ name: flags.name,
39
+ ...(flags.description !== undefined && { description: flags.description }),
40
+ ...(flags.public !== undefined && { is_public: flags.public }),
41
+ },
42
+ };
43
+ if (this.isDryRun) {
44
+ await this.handleDryRun(payload);
45
+ return;
46
+ }
47
+ const client = await this.getClient();
48
+ const { data, error } = await this.withSpinner('Creating template feature...', () => createTemplateFeature({
49
+ client,
50
+ ...payload,
51
+ }));
52
+ try {
53
+ const validated = validateResponse(data, error, zCreateTemplateFeatureResponse);
54
+ const feature = validated.data;
55
+ const metadata = validated.metadata;
56
+ if (this.isJsonMode) {
57
+ this.outputJson(feature, { source: 'template feature create', templateId: flags.id, ...metadata });
58
+ }
59
+ else {
60
+ await this.renderInk(_jsx(SuccessMessage, { message: `Feature "${feature.name}" (id: ${feature.id}) has been created for template ${flags.id}.` }));
61
+ if (this.isInteractiveMode && metadata?.action_hints) {
62
+ await this.handleActionHints(metadata.action_hints);
63
+ }
64
+ }
65
+ }
66
+ catch (err) {
67
+ this.handleError(`Failed to create feature for template: ${flags.id}`, err);
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../../base.js';
4
+ import { deleteTemplateFeature } from '../../../generated/api/sdk.gen.js';
5
+ import { zDeleteTemplateFeatureResponse } 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 TemplateFeatureDelete extends BaseCommand {
9
+ static description = 'Delete a feature from a template';
10
+ static flags = {
11
+ ...BaseCommand.baseFlags,
12
+ id: Flags.string({
13
+ description: 'Template ID',
14
+ required: true,
15
+ }),
16
+ 'feature-id': Flags.string({
17
+ description: 'Feature ID',
18
+ required: true,
19
+ }),
20
+ force: Flags.boolean({
21
+ description: 'Skip confirmation prompt',
22
+ char: 'f',
23
+ }),
24
+ };
25
+ static examples = [
26
+ '<%= config.bin %> template:feature:delete --id <template_id> --feature-id <feature_id>',
27
+ '<%= config.bin %> template:feature:delete --id <template_id> --feature-id <feature_id> --force',
28
+ ];
29
+ async run() {
30
+ await this.renderHeader('ready', 'compact');
31
+ const { flags } = await this.parse(TemplateFeatureDelete);
32
+ const message = `Delete feature ${flags['feature-id']} from template ${flags.id}? This cannot be undone.`;
33
+ if (!(await this.confirmAction(message, flags.force, { headerVariant: 'compact' }))) {
34
+ return;
35
+ }
36
+ const payload = {
37
+ path: {
38
+ templateId: flags.id,
39
+ featureId: flags['feature-id'],
40
+ },
41
+ };
42
+ if (this.isDryRun) {
43
+ await this.handleDryRun(payload);
44
+ return;
45
+ }
46
+ const client = await this.getClient();
47
+ const { data, error } = await this.withSpinner('Deleting template feature...', () => deleteTemplateFeature({
48
+ client,
49
+ ...payload,
50
+ }));
51
+ let metadata;
52
+ try {
53
+ const validated = validateResponse(data, error, zDeleteTemplateFeatureResponse);
54
+ metadata = validated.metadata;
55
+ }
56
+ catch (err) {
57
+ this.handleError(`Failed to delete feature ${flags['feature-id']} from template: ${flags.id}`, err);
58
+ return;
59
+ }
60
+ if (this.isJsonMode) {
61
+ this.outputJson({ success: true }, {
62
+ source: 'template feature delete',
63
+ templateId: flags.id,
64
+ featureId: flags['feature-id'],
65
+ ...metadata,
66
+ });
67
+ }
68
+ else {
69
+ await this.renderInk(_jsx(SuccessMessage, { message: `Feature ${flags['feature-id']} has been deleted from template ${flags.id}.` }));
70
+ if (this.isInteractiveMode && metadata?.action_hints) {
71
+ await this.handleActionHints(metadata.action_hints);
72
+ }
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,69 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../../base.js';
4
+ import { getTemplateDetail } from '../../../generated/api/sdk.gen.js';
5
+ import { zGetTemplateDetailResponse } from '../../../generated/api/zod.gen.js';
6
+ import { validateResponse } from '../../../core/api/index.js';
7
+ import { DataTable } from '../../../ui/index.js';
8
+ export default class TemplateFeatureList extends BaseCommand {
9
+ static description = 'List features for a specific project template';
10
+ static flags = {
11
+ ...BaseCommand.baseFlags,
12
+ id: Flags.string({
13
+ description: 'Template ID',
14
+ required: true,
15
+ }),
16
+ };
17
+ static examples = [
18
+ '<%= config.bin %> template:feature:list --id <template_id>',
19
+ '<%= config.bin %> template:feature:list --id <template_id> --json',
20
+ ];
21
+ async run() {
22
+ const { flags } = await this.parse(TemplateFeatureList);
23
+ await this.renderHeader('ready', 'compact');
24
+ const client = await this.getClient();
25
+ let template;
26
+ try {
27
+ const { data, error } = await getTemplateDetail({
28
+ client,
29
+ path: { templateId: flags.id },
30
+ });
31
+ // NOTE: the generated zGetTemplateDetailResponse schema does not model a
32
+ // `features` array, and Zod strips unknown keys by default. We need the
33
+ // raw `features` from the envelope, so capture it from the unvalidated
34
+ // payload before validateResponse runs.
35
+ const rawFeatures = data?.data?.features;
36
+ const validated = validateResponse(data, error, zGetTemplateDetailResponse);
37
+ template = { ...validated.data, features: rawFeatures };
38
+ }
39
+ catch (err) {
40
+ const message = `Failed to fetch features for template: ${flags.id}`;
41
+ if (this.isJsonMode) {
42
+ this.outputErrorAndExit({ message, code: 'API_ERROR', details: err });
43
+ }
44
+ else {
45
+ this.error(`${message}: ${err.message || 'Unknown error'}`);
46
+ }
47
+ return;
48
+ }
49
+ const features = template.features || [];
50
+ if (this.isJsonMode) {
51
+ this.outputJson(features, {
52
+ source: 'template feature list',
53
+ templateId: flags.id,
54
+ });
55
+ }
56
+ else {
57
+ if (features.length === 0) {
58
+ this.log(`No features found for template: ${template.name}`);
59
+ return;
60
+ }
61
+ await this.renderInk(_jsx(DataTable, { data: features, columns: [
62
+ { key: 'name', header: 'Name', minWidth: 20 },
63
+ { key: 'id', header: 'ID' },
64
+ { key: 'description', header: 'Description' },
65
+ { key: 'is_public', header: 'Public', format: (v) => (v ? '✓' : '') },
66
+ ], title: `Features for template: ${template.name}`, emptyMessage: `No features found for template: ${template.name}` }));
67
+ }
68
+ }
69
+ }