@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,108 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../base.js';
4
+ import { readConfig } from '../../core/config/index.js';
5
+ import { updateIdea } from '../../generated/api/sdk.gen.js';
6
+ import { zUpdateIdeaResponse } from '../../generated/api/zod.gen.js';
7
+ import { validateResponse } from '../../core/api/index.js';
8
+ import { SuccessMessage } from '../../ui/index.js';
9
+ export default class IdeaUpdate extends BaseCommand {
10
+ static description = 'Update an existing idea';
11
+ static examples = [
12
+ '<%= config.bin %> idea:update <id> --workspace <id> --title "Updated Title"',
13
+ '<%= config.bin %> idea:update <id> --workspace <id> --description "Updated description"',
14
+ ];
15
+ static args = {
16
+ id: Args.string({
17
+ description: 'Idea ID',
18
+ required: true,
19
+ }),
20
+ };
21
+ static flags = {
22
+ ...BaseCommand.baseFlags,
23
+ force: Flags.boolean({ description: 'Skip confirmation prompt', default: false }),
24
+ workspace: Flags.string({
25
+ description: 'Workspace ID',
26
+ char: 'w',
27
+ }),
28
+ title: Flags.string({
29
+ description: 'New title',
30
+ char: 't',
31
+ }),
32
+ description: Flags.string({
33
+ description: 'New description',
34
+ char: 'd',
35
+ }),
36
+ status: Flags.string({
37
+ description: 'New status',
38
+ char: 's',
39
+ }),
40
+ };
41
+ async run() {
42
+ const { args, flags } = await this.parse(IdeaUpdate);
43
+ const config = await readConfig();
44
+ const workspaceId = flags.workspace || config?.workspace_id;
45
+ if (!workspaceId) {
46
+ this.handleError('Workspace ID is required. Use --workspace or run in an initialized project.', { code: 'MISSING_WORKSPACE' });
47
+ return;
48
+ }
49
+ await this.renderHeader('ready', 'compact');
50
+ if (!flags.title && !flags.description && !flags.status) {
51
+ const message = 'At least one field to update must be provided (--title, --description, or --status).';
52
+ if (this.isJsonMode) {
53
+ this.outputErrorAndExit({ message, code: 'MISSING_INPUT' });
54
+ }
55
+ else {
56
+ this.error(message);
57
+ }
58
+ return;
59
+ }
60
+ const confirmed = await this.confirmAction(`Update idea ${args.id} in workspace ${workspaceId}?`, flags.force, { headerVariant: 'compact' });
61
+ if (!confirmed) {
62
+ this.log('Operation cancelled.');
63
+ return;
64
+ }
65
+ const payload = {
66
+ path: {
67
+ workspaceId,
68
+ ideaId: args.id,
69
+ },
70
+ body: {
71
+ ...(flags.title !== undefined && { title: flags.title }),
72
+ ...(flags.description !== undefined && { description: flags.description }),
73
+ ...(flags.status !== undefined && { status: flags.status }),
74
+ },
75
+ };
76
+ if (this.isDryRun) {
77
+ await this.handleDryRun(payload);
78
+ return;
79
+ }
80
+ const client = await this.getClient();
81
+ try {
82
+ const { data, error } = await this.withSpinner('Updating idea...', () => updateIdea({
83
+ client,
84
+ ...payload,
85
+ }));
86
+ const validated = validateResponse(data, error, zUpdateIdeaResponse);
87
+ const result = validated.data;
88
+ const metadata = validated.metadata;
89
+ if (this.isJsonMode) {
90
+ this.outputJson(result, { source: 'idea:update', ...metadata });
91
+ }
92
+ else {
93
+ await this.renderInk(_jsx(SuccessMessage, { message: `Idea ${args.id} updated successfully.` }));
94
+ if (this.isInteractiveMode) {
95
+ const { default: chalk } = await import('chalk');
96
+ this.log(`\n 🚀 Actions:`);
97
+ this.log(` - Show details: ${chalk.cyan(`4ge idea:show ${args.id} --workspace ${workspaceId}`)}`);
98
+ if (metadata?.action_hints) {
99
+ await this.handleActionHints(metadata.action_hints);
100
+ }
101
+ }
102
+ }
103
+ }
104
+ catch (err) {
105
+ this.handleError(`Failed to update idea: ${args.id}`, err);
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,354 @@
1
+ import { Flags } from '@oclif/core';
2
+ import chalk from 'chalk';
3
+ import React from 'react';
4
+ import * as ink from 'ink';
5
+ import { readFile, writeFile } from 'node:fs/promises';
6
+ import { join } from 'node:path';
7
+ import { BaseCommand } from '../base.js';
8
+ import CliHeader from '../ui/components/cli-header.js';
9
+ import { isProjectInitialized, writeConfig, getGitignoreEntries, getLocalConfigPath, hasLocal4geDir, readConfig, getLocal4geDir } from '../core/config/index.js';
10
+ import { listWorkspaces, listProjects } from '../generated/api/sdk.gen.js';
11
+ import { extractData, ApiValidationError } from '../core/api/index.js';
12
+ import { zListWorkspacesResponse, zListProjectsResponse } from '../generated/api/zod.gen.js';
13
+ import { installSkill } from '../core/skill/installer.js';
14
+ var InitStep;
15
+ (function (InitStep) {
16
+ InitStep["DETECTION"] = "DETECTION";
17
+ InitStep["WORKSPACE"] = "WORKSPACE";
18
+ InitStep["PROJECT"] = "PROJECT";
19
+ InitStep["COMPLETE"] = "COMPLETE";
20
+ InitStep["CANCELLED"] = "CANCELLED";
21
+ })(InitStep || (InitStep = {}));
22
+ function renderCliHeader(version) {
23
+ let statusSetter = null;
24
+ const StatusDrivenHeader = () => {
25
+ const [status, setStatus] = React.useState('checking-connectivity');
26
+ statusSetter = setStatus;
27
+ return React.createElement(CliHeader, { version, status });
28
+ };
29
+ const instance = ink.render(React.createElement(StatusDrivenHeader));
30
+ return {
31
+ instance,
32
+ setStatus: (newStatus) => {
33
+ if (statusSetter) {
34
+ statusSetter(newStatus);
35
+ }
36
+ },
37
+ };
38
+ }
39
+ export default class Init extends BaseCommand {
40
+ static description = 'Initialize a new 4ge project';
41
+ static flags = {
42
+ ...BaseCommand.baseFlags,
43
+ workspace: Flags.string({ description: 'Workspace ID to use' }),
44
+ project: Flags.string({ description: 'Project ID to use' }),
45
+ skill: Flags.boolean({
46
+ description: 'Install the 4ge CLI agent skill after initialization (non-interactive)',
47
+ default: false,
48
+ }),
49
+ };
50
+ async run() {
51
+ try {
52
+ const { flags } = await this.parse(Init);
53
+ let currentStep = InitStep.DETECTION;
54
+ let selectedWorkspaceId = flags.workspace;
55
+ let selectedProjectId = flags.project;
56
+ let workspaceName = '';
57
+ let projectName = '';
58
+ let client;
59
+ if (!this.isJsonMode) {
60
+ const { instance, setStatus } = renderCliHeader(this.config.version);
61
+ // Yield to allow Ink to mount the component before we start updating status
62
+ await new Promise(resolve => setTimeout(resolve, 0));
63
+ try {
64
+ client = await this.getClient({
65
+ onStatusChange: (status) => setStatus(status),
66
+ });
67
+ instance.clear();
68
+ instance.unmount();
69
+ // Small delay to let Ink flush the final frame
70
+ await new Promise(resolve => setTimeout(resolve, 50));
71
+ await this.renderHeader('ready');
72
+ }
73
+ catch (error) {
74
+ instance.clear();
75
+ instance.unmount();
76
+ // Small delay to let Ink flush the final frame
77
+ await new Promise(resolve => setTimeout(resolve, 50));
78
+ await this.renderHeader('no-connection');
79
+ throw error;
80
+ }
81
+ }
82
+ else {
83
+ client = await this.getClient();
84
+ }
85
+ while (currentStep !== InitStep.COMPLETE && currentStep !== InitStep.CANCELLED) {
86
+ switch (currentStep) {
87
+ case InitStep.DETECTION: {
88
+ const isInitialized = await isProjectInitialized();
89
+ const hasDir = await hasLocal4geDir();
90
+ if (isInitialized) {
91
+ const existingConfig = await readConfig();
92
+ if (this.isJsonMode) {
93
+ this.outputJson({ initialized: true, config: existingConfig });
94
+ return;
95
+ }
96
+ if (existingConfig) {
97
+ const { data: projectsData, error: projectsError } = await listProjects({
98
+ client,
99
+ headers: { 'x-workspace-id': existingConfig.workspace_id }
100
+ });
101
+ const existingProjects = extractData(projectsData, projectsError, zListProjectsResponse);
102
+ const projectBelongsToWorkspace = existingProjects.some((p) => p.id === existingConfig.project_id);
103
+ if (!projectBelongsToWorkspace) {
104
+ this.warn(`⚠ Project "${existingConfig.project_name || existingConfig.project_id}" does not belong to workspace "${existingConfig.workspace_name || existingConfig.workspace_id}"`);
105
+ }
106
+ }
107
+ if (!(await this.confirmPrompt('Project already initialized. Reinitialize?', false, { helpText: 'Ctrl+D: exit' }))) {
108
+ this.log('Aborted.');
109
+ return;
110
+ }
111
+ }
112
+ else if (hasDir) {
113
+ if (this.isJsonMode) {
114
+ this.outputErrorAndExit({ message: 'Incomplete .4ge configuration found. Re-initialize to complete setup.', code: 'INCOMPLETE_CONFIG' });
115
+ return;
116
+ }
117
+ this.log('⚠ Found an incomplete .4ge configuration. Let\'s set it up properly.');
118
+ }
119
+ if (this.isJsonMode) {
120
+ if (!selectedWorkspaceId || !selectedProjectId) {
121
+ this.outputErrorAndExit({
122
+ message: '--workspace and --project are required in --json mode',
123
+ code: 'MISSING_REQUIRED_FLAGS'
124
+ });
125
+ return;
126
+ }
127
+ // Validate workspace
128
+ const { data: wsData, error: wsError } = await listWorkspaces({ client });
129
+ const workspaces = extractData(wsData, wsError, zListWorkspacesResponse);
130
+ const workspace = workspaces.find((w) => w.id === selectedWorkspaceId);
131
+ if (!workspace) {
132
+ this.outputErrorAndExit({ message: `Workspace "${selectedWorkspaceId}" not found.`, code: 'INVALID_WORKSPACE' });
133
+ return;
134
+ }
135
+ workspaceName = workspace.name;
136
+ // Validate project
137
+ const { data: projectsData, error: projectsError } = await listProjects({
138
+ client,
139
+ headers: { 'x-workspace-id': selectedWorkspaceId }
140
+ });
141
+ const projects = extractData(projectsData, projectsError, zListProjectsResponse);
142
+ const project = projects.find((p) => p.id === selectedProjectId);
143
+ if (!project) {
144
+ this.outputErrorAndExit({ message: `Project "${selectedProjectId}" not found in workspace "${selectedWorkspaceId}".`, code: 'INVALID_PROJECT' });
145
+ return;
146
+ }
147
+ projectName = project.name;
148
+ currentStep = InitStep.COMPLETE;
149
+ }
150
+ else {
151
+ currentStep = InitStep.WORKSPACE;
152
+ }
153
+ break;
154
+ }
155
+ case InitStep.WORKSPACE: {
156
+ const { data: wsData, error: wsError } = await listWorkspaces({ client });
157
+ const workspaces = extractData(wsData, wsError, zListWorkspacesResponse);
158
+ if (workspaces.length === 0)
159
+ this.error('No workspaces found for this account.');
160
+ const workspace = flags.workspace
161
+ ? workspaces.find(w => w.id === flags.workspace)
162
+ : workspaces.length === 1
163
+ ? workspaces[0]
164
+ : null;
165
+ if (workspace) {
166
+ if (!flags.workspace && workspaces.length === 1) {
167
+ this.log(`Using only available workspace: ${workspace.name} (${workspace.id})`);
168
+ }
169
+ selectedWorkspaceId = workspace.id;
170
+ workspaceName = workspace.name;
171
+ currentStep = InitStep.PROJECT;
172
+ break;
173
+ }
174
+ if (flags.workspace)
175
+ this.error(`Workspace "${flags.workspace}" not found.`);
176
+ const options = workspaces.map((w) => ({ label: w.name, value: w.id, description: w.id }));
177
+ const result = await this.selectOption('Select workspace:', options, {
178
+ helpText: 'Esc: exit • Ctrl+D: exit'
179
+ });
180
+ if (result === null) {
181
+ currentStep = InitStep.CANCELLED;
182
+ }
183
+ else {
184
+ const selected = workspaces.find((w) => w.id === result);
185
+ selectedWorkspaceId = result;
186
+ workspaceName = selected?.name || '';
187
+ currentStep = InitStep.PROJECT;
188
+ }
189
+ break;
190
+ }
191
+ case InitStep.PROJECT: {
192
+ const { data: projectsData, error: projectsError } = await listProjects({
193
+ client,
194
+ headers: { 'x-workspace-id': selectedWorkspaceId }
195
+ });
196
+ const projects = extractData(projectsData, projectsError, zListProjectsResponse);
197
+ if (projects.length === 0)
198
+ this.error(`No projects found in workspace ${selectedWorkspaceId}`);
199
+ const project = flags.project
200
+ ? projects.find(p => p.id === flags.project)
201
+ : projects.length === 1
202
+ ? projects[0]
203
+ : null;
204
+ if (project) {
205
+ if (!flags.project && projects.length === 1) {
206
+ this.log(`Using only available project: ${project.name} (${project.id})`);
207
+ }
208
+ selectedProjectId = project.id;
209
+ projectName = project.name;
210
+ currentStep = InitStep.COMPLETE;
211
+ break;
212
+ }
213
+ if (flags.project)
214
+ this.error(`Project "${flags.project}" not found in workspace "${selectedWorkspaceId}".`);
215
+ const options = projects.map((p) => ({ label: p.name, value: p.id, description: p.id }));
216
+ const result = await this.selectOption('Select project:', options, {
217
+ context: [{ label: 'Workspace', value: workspaceName }],
218
+ helpText: flags.workspace ? 'Esc: exit • Ctrl+D: exit' : 'Esc: back • Ctrl+D: exit'
219
+ });
220
+ if (result === null) {
221
+ currentStep = flags.workspace ? InitStep.CANCELLED : InitStep.WORKSPACE;
222
+ }
223
+ else {
224
+ const selected = projects.find((p) => p.id === result);
225
+ selectedProjectId = result;
226
+ projectName = selected?.name || '';
227
+ currentStep = InitStep.COMPLETE;
228
+ }
229
+ break;
230
+ }
231
+ }
232
+ }
233
+ if (currentStep === InitStep.CANCELLED) {
234
+ this.log('Initialization cancelled.');
235
+ return;
236
+ }
237
+ if (!selectedProjectId || !selectedWorkspaceId) {
238
+ this.error('Unexpected state: missing project or workspace selection.');
239
+ return;
240
+ }
241
+ // 4. Write Config
242
+ await writeConfig({
243
+ project_id: selectedProjectId,
244
+ workspace_id: selectedWorkspaceId,
245
+ project_name: projectName || undefined,
246
+ workspace_name: workspaceName || undefined,
247
+ hierarchy: {
248
+ default_roadmap_filter: 'now',
249
+ recursive_pull: true,
250
+ },
251
+ pull: {
252
+ generate_stubs: false,
253
+ include_brief_deps: false,
254
+ },
255
+ cli: {
256
+ default_mode: 'pretty',
257
+ }
258
+ });
259
+ // 5. Update .4ge/.gitignore
260
+ let gitignoreUpdated = false;
261
+ const entries = getGitignoreEntries();
262
+ try {
263
+ const gitignorePath = join(getLocal4geDir(), '.gitignore');
264
+ let content = '';
265
+ try {
266
+ content = await readFile(gitignorePath, 'utf-8');
267
+ }
268
+ catch (e) {
269
+ if (e.code !== 'ENOENT')
270
+ throw e;
271
+ }
272
+ const lines = content.split('\n').map(l => l.trim());
273
+ const toAdd = entries.filter(e => !lines.includes(e));
274
+ if (toAdd.length > 0) {
275
+ const newContent = content + (content.endsWith('\n') || content === '' ? '' : '\n') + toAdd.join('\n') + '\n';
276
+ await writeFile(gitignorePath, newContent, 'utf-8');
277
+ gitignoreUpdated = true;
278
+ }
279
+ }
280
+ catch (e) {
281
+ this.warn(`Failed to update .4ge/.gitignore: ${e instanceof Error ? e.message : String(e)}`);
282
+ }
283
+ // 6. Skill install (optional)
284
+ let skillInstalled = false;
285
+ let skillPath;
286
+ const shouldInstallSkill = flags.skill || (!this.isJsonMode && await this.confirmPrompt('Install the 4ge CLI skill for AI agents here?', false, { helpText: 'Ctrl+D: exit' }));
287
+ if (shouldInstallSkill) {
288
+ try {
289
+ const result = await installSkill({ agent: 'universal', scope: 'project', force: false });
290
+ skillInstalled = true;
291
+ skillPath = result.path;
292
+ }
293
+ catch (err) {
294
+ if (this.isJsonMode) {
295
+ this.outputErrorAndExit({
296
+ message: err?.message ?? String(err),
297
+ code: 'SKILL_INSTALL_FAILED',
298
+ hint: /already exists/i.test(err?.message ?? '') ? 'Re-run `4ge skill:install --force`.' : undefined,
299
+ });
300
+ return;
301
+ }
302
+ this.warn(`Failed to install skill: ${err instanceof Error ? err.message : String(err)}`);
303
+ this.warn('Run `4ge skill:install` later to add it.');
304
+ }
305
+ }
306
+ // 7. Success Output
307
+ if (this.isJsonMode) {
308
+ this.outputJson({
309
+ project_id: selectedProjectId,
310
+ workspace_id: selectedWorkspaceId,
311
+ config_path: getLocalConfigPath(),
312
+ gitignore_updated: gitignoreUpdated,
313
+ skill_installed: skillInstalled,
314
+ ...(skillPath ? { skill_path: skillPath } : {}),
315
+ });
316
+ }
317
+ else {
318
+ const { SuccessMessage } = await import('../ui/index.js');
319
+ const React = await import('react');
320
+ await this.renderInk(React.createElement(SuccessMessage, { message: 'Project initialized successfully!' }));
321
+ this.log(`Config written to: ${getLocalConfigPath()}`);
322
+ if (gitignoreUpdated) {
323
+ this.log('.4ge/.gitignore updated with private entries.');
324
+ }
325
+ if (skillInstalled && skillPath) {
326
+ this.log(`Skill installed at: ${skillPath}`);
327
+ this.log(chalk.dim('Reload your AI agent to pick up the new skill.'));
328
+ }
329
+ this.log(chalk.dim('\nConfigure additional settings with: 4ge config manage'));
330
+ this.log(chalk.dim('Available: default mode, hierarchy settings, pull settings\n'));
331
+ }
332
+ }
333
+ catch (err) {
334
+ if (err instanceof ApiValidationError) {
335
+ if (this.isJsonMode) {
336
+ this.outputError({
337
+ message: err.message,
338
+ code: 'API_VALIDATION_ERROR',
339
+ details: err.details
340
+ });
341
+ this.exit(1);
342
+ return;
343
+ }
344
+ else {
345
+ const detail = err.details instanceof Error ? err.details.message : String(err.details ?? '');
346
+ this.log(chalk.red('\nAPI response format changed — please update your CLI with `npm update -g @4ge/cli`'));
347
+ this.error(`${err.message}: ${detail}`);
348
+ }
349
+ return;
350
+ }
351
+ this.handleError('Initialization failed', err);
352
+ }
353
+ }
354
+ }
@@ -0,0 +1,5 @@
1
+ import AuthLogin from './auth/login.js';
2
+ export default class Login extends AuthLogin {
3
+ static description = 'Alias for `4ge auth login`';
4
+ static hidden = false;
5
+ }
@@ -0,0 +1,10 @@
1
+ import { Help } from '@oclif/core';
2
+ import { BaseCommand } from '../../base.js';
3
+ export default class PlanIndex extends BaseCommand {
4
+ static description = 'Manage project plans';
5
+ async run() {
6
+ await this.renderHeader('ready', 'compact');
7
+ const help = new Help(this.config);
8
+ await help.showHelp(['plan']);
9
+ }
10
+ }
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { BaseCommand } from '../../base.js';
3
+ import { readConfig, getAssetPath } from '../../core/config/index.js';
4
+ import { resolveRoadmapFilter } from '../../core/config/defaults.js';
5
+ import { getPlanTree } from '../../generated/api/sdk.gen.js';
6
+ import { zGetPlanTreeResponse } from '../../generated/api/zod.gen.js';
7
+ import { validateResponse } from '../../core/api/index.js';
8
+ import { Flags } from '@oclif/core';
9
+ import { SuccessMessage } from '../../ui/index.js';
10
+ import fs from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ export default class PlanPull extends BaseCommand {
13
+ static description = 'Downloads the complete plan tree snapshot for the current project';
14
+ static flags = {
15
+ ...BaseCommand.baseFlags,
16
+ roadmap: Flags.string({
17
+ description: 'Filter by roadmap status (now|next|later)',
18
+ options: ['now', 'next', 'later'],
19
+ }),
20
+ status: Flags.string({
21
+ description: 'Filter by status (BACKLOG|NOT_STARTED|IN_PLANNING|READY_FOR_DEVELOPMENT|IN_DEVELOPMENT|BLOCKED|REVIEW|COMPLETE)',
22
+ }),
23
+ save: Flags.boolean({
24
+ description: 'Force save to disk even in JSON mode',
25
+ default: false,
26
+ }),
27
+ stdout: Flags.boolean({
28
+ description: 'Print to stdout without saving',
29
+ default: false,
30
+ }),
31
+ };
32
+ static examples = [
33
+ '<%= config.bin %> plan:pull',
34
+ '<%= config.bin %> plan:pull --roadmap next',
35
+ '<%= config.bin %> plan:pull --status COMPLETE',
36
+ '<%= config.bin %> plan:pull --roadmap now --status IN_DEVELOPMENT',
37
+ '<%= config.bin %> plan:pull --json',
38
+ '<%= config.bin %> plan:pull --json --save',
39
+ '<%= config.bin %> plan:pull --stdout',
40
+ ];
41
+ async run() {
42
+ const { flags } = await this.parse(PlanPull);
43
+ // Validate flag combination
44
+ if (flags.json && flags.stdout) {
45
+ this.error('Cannot use --json and --stdout together');
46
+ }
47
+ if (flags.save && flags.stdout) {
48
+ this.error('Cannot use --stdout and --save together');
49
+ }
50
+ await this.renderHeader('ready', 'compact');
51
+ await this.migrateAssetsIfNeeded();
52
+ const client = await this.getClient();
53
+ const config = await readConfig();
54
+ if (!config?.project_id) {
55
+ const message = 'Project not initialized. Run `4ge init` first to set project context.';
56
+ if (this.isJsonMode) {
57
+ this.outputErrorAndExit({ message, code: 'NOT_INITIALIZED' });
58
+ }
59
+ else {
60
+ this.error(message);
61
+ }
62
+ return;
63
+ }
64
+ try {
65
+ const roadmapFilter = resolveRoadmapFilter(flags.roadmap, config);
66
+ const { data, error } = await this.withRetry(() => getPlanTree({
67
+ client,
68
+ path: { projectId: config.project_id },
69
+ query: {
70
+ ...(roadmapFilter ? { roadmap: roadmapFilter } : {}),
71
+ ...(flags.status ? { status: flags.status } : {}),
72
+ },
73
+ }));
74
+ const validated = validateResponse(data, error, zGetPlanTreeResponse);
75
+ const payload = validated.data;
76
+ const metadata = validated.metadata;
77
+ const savePath = getAssetPath('plan.json');
78
+ if (this.isJsonMode) {
79
+ this.outputJson(payload, metadata);
80
+ if (flags.save) {
81
+ await this.saveToFile(savePath, payload);
82
+ this.log('Saved to .4ge/assets/plan.json');
83
+ }
84
+ }
85
+ else if (flags.stdout) {
86
+ this.log(JSON.stringify(payload, null, 2));
87
+ }
88
+ else {
89
+ await this.saveToFile(savePath, payload);
90
+ await this.renderInk(_jsx(SuccessMessage, { message: "Plan saved to .4ge/assets/plan.json" }));
91
+ if (this.isInteractiveMode && metadata?.action_hints) {
92
+ await this.handleActionHints(metadata.action_hints);
93
+ }
94
+ }
95
+ }
96
+ catch (err) {
97
+ this.handleError('An unexpected error occurred', err);
98
+ }
99
+ }
100
+ async saveToFile(filePath, data) {
101
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
102
+ await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
103
+ }
104
+ }
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { BaseCommand } from '../../base.js';
3
+ import { readConfig } from '../../core/config/index.js';
4
+ import { getPlanVersions } from '../../generated/api/sdk.gen.js';
5
+ import { zGetPlanVersionsResponse } 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 PlanVersions extends BaseCommand {
9
+ static description = 'Lists available plan versions';
10
+ static flags = {
11
+ ...BaseCommand.baseFlags,
12
+ };
13
+ static examples = [
14
+ '<%= config.bin %> plan:versions',
15
+ '<%= config.bin %> plan:versions --json',
16
+ ];
17
+ async run() {
18
+ const { flags } = await this.parse(PlanVersions);
19
+ await this.renderHeader('ready', 'compact');
20
+ const client = await this.getClient();
21
+ const config = await readConfig();
22
+ if (!config?.project_id) {
23
+ const message = 'Project not initialized. Run `4ge init` first to set project context.';
24
+ if (this.isJsonMode) {
25
+ this.outputErrorAndExit({ message, code: 'NOT_INITIALIZED' });
26
+ }
27
+ else {
28
+ this.error(message);
29
+ }
30
+ return;
31
+ }
32
+ try {
33
+ const { data, error } = await this.withRetry(() => getPlanVersions({
34
+ client,
35
+ path: { projectId: config.project_id },
36
+ }));
37
+ const validated = validateResponse(data, error, zGetPlanVersionsResponse);
38
+ const payload = validated.data;
39
+ const metadata = validated.metadata;
40
+ if (this.isJsonMode) {
41
+ this.outputJson(payload, metadata);
42
+ }
43
+ else {
44
+ const versions = payload.versions || [];
45
+ const latestVersion = payload.latestVersion;
46
+ if (latestVersion !== undefined) {
47
+ this.log(`Latest Version: ${latestVersion}`);
48
+ }
49
+ await this.renderInk(_jsx(DataTable, { data: versions, columns: [
50
+ { key: 'id', header: 'ID' },
51
+ { key: 'versionNumber', header: 'Version' },
52
+ { key: 'changeDescription', header: 'Change Description', format: (v) => v || '-' },
53
+ { key: 'createdAt', header: 'Created', format: (v) => new Date(v).toLocaleString() },
54
+ ], title: "Plan Versions", emptyMessage: "No versions found" }));
55
+ if (this.isInteractiveMode && metadata?.action_hints) {
56
+ await this.handleActionHints(metadata.action_hints);
57
+ }
58
+ }
59
+ }
60
+ catch (err) {
61
+ this.handleError('An unexpected error occurred', err);
62
+ }
63
+ }
64
+ }