@4ge/cli 0.1.0 → 0.2.1
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.
- package/dist/base.js +1 -1
- package/dist/commands/auth/status.js +5 -5
- package/dist/commands/config/index.js +2 -2
- package/dist/commands/config/show.js +2 -2
- package/dist/commands/epic/list.js +1 -1
- package/dist/commands/feature/list.js +1 -1
- package/dist/commands/flow/index.js +10 -0
- package/dist/commands/flow/list.js +85 -0
- package/dist/commands/flow/pull.js +109 -0
- package/dist/commands/flow/show.js +58 -0
- package/dist/commands/flow/update.js +105 -0
- package/dist/commands/idea/graduate.js +2 -2
- package/dist/commands/init.js +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/commands/project/index.js +1 -1
- package/dist/commands/project/list.js +1 -1
- package/dist/commands/story/list.js +1 -1
- package/dist/commands/template/list.js +4 -2
- package/dist/commands/template/pull.js +2 -2
- package/dist/commands/whoami.js +1 -1
- package/dist/core/auth/api-client.js +2 -2
- package/dist/core/auth/token-refresh.js +1 -1
- package/dist/core/project/markdown.js +1 -1
- package/dist/generated/api/index.js +1 -1
- package/dist/generated/api/sdk.gen.js +56 -0
- package/dist/generated/api/zod.gen.js +167 -0
- package/dist/ui/components/cli-header.js +9 -6
- package/dist/ui/components/data-table.js +6 -1
- package/package.json +2 -1
- package/skills/4ge-cli.md +90 -4
package/dist/base.js
CHANGED
|
@@ -117,7 +117,7 @@ export class BaseCommand extends Command {
|
|
|
117
117
|
options?.onStatusChange?.('refreshing-token');
|
|
118
118
|
const credential = await ensureFreshToken();
|
|
119
119
|
if (!credential) {
|
|
120
|
-
const message = 'Not authenticated. Run `4ge auth
|
|
120
|
+
const message = 'Not authenticated. Run `4ge auth:login --token <api_key>` first.';
|
|
121
121
|
if (this.isJsonMode) {
|
|
122
122
|
this.outputErrorAndExit({ message, code: 'NO_CREDENTIALS' });
|
|
123
123
|
}
|
|
@@ -12,7 +12,7 @@ export default class AuthStatus extends BaseCommand {
|
|
|
12
12
|
this.outputErrorAndExit({ message: 'Not authenticated', code: 'NO_CREDENTIALS' });
|
|
13
13
|
}
|
|
14
14
|
else {
|
|
15
|
-
this.log('Not authenticated. Run `4ge auth
|
|
15
|
+
this.log('Not authenticated. Run `4ge auth:login --token <api_key>` to authenticate.');
|
|
16
16
|
}
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
@@ -37,7 +37,7 @@ export default class AuthStatus extends BaseCommand {
|
|
|
37
37
|
this.outputErrorAndExit({
|
|
38
38
|
message,
|
|
39
39
|
code,
|
|
40
|
-
hint: refreshError.message || "Run '4ge auth
|
|
40
|
+
hint: refreshError.message || "Run '4ge auth:login' to re-authenticate."
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
else {
|
|
@@ -47,7 +47,7 @@ export default class AuthStatus extends BaseCommand {
|
|
|
47
47
|
else {
|
|
48
48
|
this.log('⚠ Session expired — token refresh failed.');
|
|
49
49
|
}
|
|
50
|
-
this.log(` ${refreshError.message || "Run '4ge auth
|
|
50
|
+
this.log(` ${refreshError.message || "Run '4ge auth:login' to re-authenticate."}`);
|
|
51
51
|
}
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
@@ -65,7 +65,7 @@ export default class AuthStatus extends BaseCommand {
|
|
|
65
65
|
this.outputErrorAndExit({
|
|
66
66
|
message: 'API validation failed for 4GE_API_KEY',
|
|
67
67
|
code: 'ENV_CREDENTIAL_OVERRIDE',
|
|
68
|
-
hint: 'The 4GE_API_KEY environment variable is overriding stored credentials but is invalid. Unset it to use credentials from "4ge auth
|
|
68
|
+
hint: 'The 4GE_API_KEY environment variable is overriding stored credentials but is invalid. Unset it to use credentials from "4ge auth:login".'
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
71
|
else {
|
|
@@ -77,7 +77,7 @@ export default class AuthStatus extends BaseCommand {
|
|
|
77
77
|
this.log('⚠ Using API key from 4GE_API_KEY environment variable, but server validation failed.');
|
|
78
78
|
this.log('');
|
|
79
79
|
this.log('The 4GE_API_KEY environment variable takes priority over stored credentials.');
|
|
80
|
-
this.log('FIX: Unset 4GE_API_KEY (e.g., `unset 4GE_API_KEY`) to use credentials from `4ge auth
|
|
80
|
+
this.log('FIX: Unset 4GE_API_KEY (e.g., `unset 4GE_API_KEY`) to use credentials from `4ge auth:login`.');
|
|
81
81
|
}
|
|
82
82
|
else {
|
|
83
83
|
this.log(`Authenticated via ${activeCredential.source}${activeCredential.profile ? ` (profile: ${activeCredential.profile})` : ''}, but API validation failed.`);
|
|
@@ -3,7 +3,7 @@ export default class ConfigIndex extends Command {
|
|
|
3
3
|
static description = 'Manage CLI configuration';
|
|
4
4
|
static hidden = true;
|
|
5
5
|
async run() {
|
|
6
|
-
this.log('Use `4ge config
|
|
7
|
-
this.log('Use `4ge config
|
|
6
|
+
this.log('Use `4ge config:manage` for interactive configuration.');
|
|
7
|
+
this.log('Use `4ge config:show`, `4ge config:get`, or `4ge config:set` for direct access.');
|
|
8
8
|
}
|
|
9
9
|
}
|
|
@@ -26,8 +26,8 @@ export default class ConfigShow extends BaseCommand {
|
|
|
26
26
|
] }));
|
|
27
27
|
if (this.isInteractiveMode) {
|
|
28
28
|
await this.handleActionHints([
|
|
29
|
-
{ label: 'Set config', command: '4ge config
|
|
30
|
-
{ label: 'Get config', command: '4ge config
|
|
29
|
+
{ label: 'Set config', command: '4ge config:set <key> <value>' },
|
|
30
|
+
{ label: 'Get config', command: '4ge config:get <key>' },
|
|
31
31
|
]);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -75,7 +75,7 @@ export default class EpicList extends BaseCommand {
|
|
|
75
75
|
const selectedId = await this.selectOption('Select an epic for details:', options, { headerVariant: 'compact' });
|
|
76
76
|
if (selectedId) {
|
|
77
77
|
const { default: chalk } = await import('chalk');
|
|
78
|
-
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge epic
|
|
78
|
+
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge epic:show ${selectedId}`)}\n`);
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
if (metadata?.action_hints) {
|
|
@@ -77,7 +77,7 @@ export default class FeatureList extends BaseCommand {
|
|
|
77
77
|
const selectedId = await this.selectOption('Select a feature for details:', options, { headerVariant: 'compact' });
|
|
78
78
|
if (selectedId) {
|
|
79
79
|
const { default: chalk } = await import('chalk');
|
|
80
|
-
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge feature
|
|
80
|
+
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge feature:show ${selectedId}`)}\n`);
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
if (metadata?.action_hints) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Help } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base.js';
|
|
3
|
+
export default class FlowIndex extends BaseCommand {
|
|
4
|
+
static description = 'Manage feature flows in your project';
|
|
5
|
+
async run() {
|
|
6
|
+
await this.renderHeader('ready', 'compact');
|
|
7
|
+
const help = new Help(this.config);
|
|
8
|
+
await help.showHelp(['flow']);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../base.js';
|
|
4
|
+
import { listFlows } from '../../generated/api/sdk.gen.js';
|
|
5
|
+
import { zListFlowsResponse } 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 FlowList extends BaseCommand {
|
|
9
|
+
static description = 'List flows for a feature in the current project';
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> flow:list --feature-id <uuid>',
|
|
12
|
+
'<%= config.bin %> flow:list --feature-id <uuid> --roadmap now --json',
|
|
13
|
+
'<%= config.bin %> flow:list --feature-id <uuid> --status IN_DEVELOPMENT',
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
...BaseCommand.baseFlags,
|
|
17
|
+
'feature-id': Flags.string({
|
|
18
|
+
description: 'Feature ID whose flows to list (a feature owns one implied flow)',
|
|
19
|
+
required: true,
|
|
20
|
+
}),
|
|
21
|
+
roadmap: Flags.string({
|
|
22
|
+
description: 'Filter by roadmap status (now|next|later)',
|
|
23
|
+
options: ['now', 'next', 'later'],
|
|
24
|
+
}),
|
|
25
|
+
status: Flags.string({
|
|
26
|
+
description: 'Filter by status (e.g. BACKLOG, IN_DEVELOPMENT, COMPLETE)',
|
|
27
|
+
}),
|
|
28
|
+
};
|
|
29
|
+
async run() {
|
|
30
|
+
const { flags } = await this.parse(FlowList);
|
|
31
|
+
await this.renderHeader('ready', 'compact');
|
|
32
|
+
const client = await this.getClient();
|
|
33
|
+
const projectId = await this.getProjectIdOrExit();
|
|
34
|
+
try {
|
|
35
|
+
const response = await this.withSpinner('Fetching flows...', () => listFlows({
|
|
36
|
+
client,
|
|
37
|
+
path: {
|
|
38
|
+
projectId,
|
|
39
|
+
featureId: flags['feature-id'],
|
|
40
|
+
},
|
|
41
|
+
query: {
|
|
42
|
+
...(flags.roadmap ? { roadmap: flags.roadmap } : {}),
|
|
43
|
+
...(flags.status ? { status: flags.status } : {}),
|
|
44
|
+
},
|
|
45
|
+
}));
|
|
46
|
+
const { data, error } = response;
|
|
47
|
+
const validated = validateResponse(data, error, zListFlowsResponse);
|
|
48
|
+
const items = validated.data;
|
|
49
|
+
const metadata = validated.metadata;
|
|
50
|
+
if (this.isJsonMode) {
|
|
51
|
+
this.outputJson(items, { source: 'flow list' });
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
await this.renderInk(_jsx(DataTable, { data: items, columns: [
|
|
55
|
+
{ key: 'label', header: 'Title', minWidth: 20 },
|
|
56
|
+
{ key: 'id', header: 'ID' },
|
|
57
|
+
{ key: 'status', header: 'Status', color: 'auto' },
|
|
58
|
+
{ key: 'roadmap_status', header: 'Roadmap' },
|
|
59
|
+
{ key: 'step_count', header: 'Steps' },
|
|
60
|
+
{ key: 'parent_feature_id', header: 'Feature ID' },
|
|
61
|
+
], title: `Flows in feature ${flags['feature-id']}${flags.roadmap ? ` (${flags.roadmap})` : ''}`, emptyMessage: `No flows found${flags.roadmap ? ` for roadmap: ${flags.roadmap}` : ''}` }));
|
|
62
|
+
if (this.isInteractiveMode) {
|
|
63
|
+
if (items.length > 0) {
|
|
64
|
+
const options = items.map((item) => ({
|
|
65
|
+
label: item.label || item.id,
|
|
66
|
+
value: item.id,
|
|
67
|
+
description: item.status || '',
|
|
68
|
+
}));
|
|
69
|
+
const selectedId = await this.selectOption('Select a flow for details:', options, { headerVariant: 'compact' });
|
|
70
|
+
if (selectedId) {
|
|
71
|
+
const { default: chalk } = await import('chalk');
|
|
72
|
+
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge flow:show ${selectedId}`)}\n`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (metadata?.action_hints) {
|
|
76
|
+
await this.handleActionHints(metadata.action_hints);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
this.handleError('An unexpected error occurred', err);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
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 } from '../../core/config/defaults.js';
|
|
6
|
+
import { getFlow, getFlowTree } from '../../generated/api/sdk.gen.js';
|
|
7
|
+
import { zGetFlowTreeResponse, zGetFlowResponse } 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 FlowPull extends BaseCommand {
|
|
13
|
+
static description = 'Download flow details';
|
|
14
|
+
static examples = [
|
|
15
|
+
'<%= config.bin %> flow:pull --id <uuid>',
|
|
16
|
+
'<%= config.bin %> flow:pull --id <uuid> --json',
|
|
17
|
+
'<%= config.bin %> flow:pull --id <uuid> --recursive',
|
|
18
|
+
];
|
|
19
|
+
static flags = {
|
|
20
|
+
...BaseCommand.baseFlags,
|
|
21
|
+
id: Flags.string({
|
|
22
|
+
description: 'Flow ID',
|
|
23
|
+
required: true,
|
|
24
|
+
}),
|
|
25
|
+
recursive: Flags.boolean({
|
|
26
|
+
description: 'Pull full flow tree (flow + step nodes) instead of just the flow detail',
|
|
27
|
+
allowNo: true,
|
|
28
|
+
}),
|
|
29
|
+
save: Flags.boolean({
|
|
30
|
+
description: 'Force save to disk even in JSON mode',
|
|
31
|
+
default: false,
|
|
32
|
+
}),
|
|
33
|
+
stdout: Flags.boolean({
|
|
34
|
+
description: 'Print to stdout without saving',
|
|
35
|
+
default: false,
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
async run() {
|
|
39
|
+
const { flags } = await this.parse(FlowPull);
|
|
40
|
+
if (flags.json && flags.stdout) {
|
|
41
|
+
this.error('Cannot use --json and --stdout together');
|
|
42
|
+
}
|
|
43
|
+
if (flags.save && flags.stdout) {
|
|
44
|
+
this.error('Cannot use --stdout and --save together');
|
|
45
|
+
}
|
|
46
|
+
await this.renderHeader('ready', 'compact');
|
|
47
|
+
await this.migrateAssetsIfNeeded();
|
|
48
|
+
const client = await this.getClient();
|
|
49
|
+
const projectId = await this.getProjectIdOrExit();
|
|
50
|
+
const isRecursive = resolveRecursive(flags.recursive, this.localConfig);
|
|
51
|
+
const spinnerMsg = isRecursive ? `Pulling flow ${flags.id} tree...` : `Pulling flow ${flags.id}...`;
|
|
52
|
+
const response = await this.withSpinner(spinnerMsg, () => {
|
|
53
|
+
if (isRecursive) {
|
|
54
|
+
return getFlowTree({
|
|
55
|
+
client,
|
|
56
|
+
path: {
|
|
57
|
+
projectId,
|
|
58
|
+
flowId: flags.id,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
return getFlow({
|
|
64
|
+
client,
|
|
65
|
+
path: {
|
|
66
|
+
projectId,
|
|
67
|
+
flowId: flags.id,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
const { data, error } = response;
|
|
73
|
+
try {
|
|
74
|
+
let flow;
|
|
75
|
+
let metadata;
|
|
76
|
+
if (isRecursive) {
|
|
77
|
+
const validated = validateResponse(data, error, zGetFlowTreeResponse);
|
|
78
|
+
flow = validated.data;
|
|
79
|
+
metadata = validated.metadata;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const validated = validateResponse(data, error, zGetFlowResponse);
|
|
83
|
+
flow = validated.data;
|
|
84
|
+
metadata = validated.metadata;
|
|
85
|
+
}
|
|
86
|
+
const savePath = path.join(getCollectionDir('flows'), `${flags.id}.json`);
|
|
87
|
+
if (this.isJsonMode) {
|
|
88
|
+
this.outputJson(flow);
|
|
89
|
+
if (flags.save) {
|
|
90
|
+
await this.saveToFile(savePath, flow);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (flags.stdout) {
|
|
94
|
+
this.log(JSON.stringify(flow, null, 2));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
await this.saveToFile(savePath, flow);
|
|
98
|
+
await this.renderInk(_jsx(SuccessMessage, { message: `Flow saved to .4ge/assets/flows/${flags.id}.json` }));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
this.handleError(`Failed to pull flow: ${flags.id}`, err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async saveToFile(filePath, data) {
|
|
106
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
107
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base.js';
|
|
3
|
+
import { getFlow } from '../../generated/api/sdk.gen.js';
|
|
4
|
+
import { zGetFlowResponse } from '../../generated/api/zod.gen.js';
|
|
5
|
+
import { validateResponse } from '../../core/api/index.js';
|
|
6
|
+
export default class FlowShow extends BaseCommand {
|
|
7
|
+
static description = 'Show detailed information for a specific flow';
|
|
8
|
+
static examples = [
|
|
9
|
+
'<%= config.bin %> flow:show --id <uuid>',
|
|
10
|
+
'<%= config.bin %> flow:show --id <uuid> --json',
|
|
11
|
+
];
|
|
12
|
+
static flags = {
|
|
13
|
+
...BaseCommand.baseFlags,
|
|
14
|
+
id: Flags.string({
|
|
15
|
+
description: 'Flow ID',
|
|
16
|
+
required: true,
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
async run() {
|
|
20
|
+
const { flags } = await this.parse(FlowShow);
|
|
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 flow ${flags.id}...`, () => getFlow({
|
|
25
|
+
client,
|
|
26
|
+
path: {
|
|
27
|
+
projectId,
|
|
28
|
+
flowId: flags.id,
|
|
29
|
+
},
|
|
30
|
+
}));
|
|
31
|
+
const { data, error } = response;
|
|
32
|
+
try {
|
|
33
|
+
const validated = validateResponse(data, error, zGetFlowResponse);
|
|
34
|
+
const payload = validated.data;
|
|
35
|
+
const metadata = validated.metadata;
|
|
36
|
+
if (this.isJsonMode) {
|
|
37
|
+
this.outputJson(payload, { source: 'flow show' });
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
this.log(`\nFlow: ${payload.label}`);
|
|
41
|
+
this.log(`ID: ${payload.id}`);
|
|
42
|
+
this.log(`Status: ${payload.status}`);
|
|
43
|
+
this.log(`Roadmap: ${payload.roadmap_status ?? 'None'}`);
|
|
44
|
+
this.log(`Feature ID: ${payload.parent_feature_id ?? payload.parent_id ?? 'None'}`);
|
|
45
|
+
this.log(`Steps: ${payload.step_count}`);
|
|
46
|
+
if (payload.description) {
|
|
47
|
+
this.log(`\nDescription:\n${payload.description}\n`);
|
|
48
|
+
}
|
|
49
|
+
if (this.isInteractiveMode && metadata?.action_hints) {
|
|
50
|
+
await this.handleActionHints(metadata.action_hints);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
this.handleError(`Failed to fetch flow detail for ID: ${flags.id}`, err);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../base.js';
|
|
4
|
+
import { updateFlow } from '../../generated/api/sdk.gen.js';
|
|
5
|
+
import { zUpdateFlowResponse } 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 FlowUpdate extends BaseCommand {
|
|
9
|
+
static description = 'Update a flow';
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> flow:update --id <uuid> --status IN_DEVELOPMENT',
|
|
12
|
+
'<%= config.bin %> flow:update --id <uuid> --status COMPLETE --title "New Title"',
|
|
13
|
+
'<%= config.bin %> flow:update --id <uuid> --roadmap-status next --priority MUST_HAVE',
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
...BaseCommand.baseFlags,
|
|
17
|
+
id: Flags.string({
|
|
18
|
+
description: 'Flow ID (UUID)',
|
|
19
|
+
required: true,
|
|
20
|
+
}),
|
|
21
|
+
status: Flags.string({
|
|
22
|
+
description: 'New status (BACKLOG|NOT_STARTED|IN_PLANNING|READY_FOR_DEVELOPMENT|IN_DEVELOPMENT|BLOCKED|REVIEW|COMPLETE)',
|
|
23
|
+
options: ['BACKLOG', 'NOT_STARTED', 'IN_PLANNING', 'READY_FOR_DEVELOPMENT', 'IN_DEVELOPMENT', 'BLOCKED', 'REVIEW', 'COMPLETE'],
|
|
24
|
+
}),
|
|
25
|
+
title: Flags.string({
|
|
26
|
+
description: 'New title',
|
|
27
|
+
}),
|
|
28
|
+
description: Flags.string({
|
|
29
|
+
description: 'New description',
|
|
30
|
+
}),
|
|
31
|
+
priority: Flags.string({
|
|
32
|
+
description: 'New priority (MUST_HAVE|SHOULD_HAVE|BACKLOG)',
|
|
33
|
+
options: ['MUST_HAVE', 'SHOULD_HAVE', 'BACKLOG'],
|
|
34
|
+
}),
|
|
35
|
+
'roadmap-status': Flags.string({
|
|
36
|
+
description: 'New roadmap status (now|next|later)',
|
|
37
|
+
options: ['now', 'next', 'later'],
|
|
38
|
+
}),
|
|
39
|
+
force: Flags.boolean({
|
|
40
|
+
description: 'Skip confirmation prompt',
|
|
41
|
+
default: false,
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
async run() {
|
|
45
|
+
const { flags } = await this.parse(FlowUpdate);
|
|
46
|
+
const projectId = await this.getProjectIdOrExit();
|
|
47
|
+
await this.renderHeader('ready', 'compact');
|
|
48
|
+
const body = {};
|
|
49
|
+
if (flags.status !== undefined)
|
|
50
|
+
body.status = flags.status;
|
|
51
|
+
if (flags.title !== undefined)
|
|
52
|
+
body.label = flags.title;
|
|
53
|
+
if (flags.description !== undefined)
|
|
54
|
+
body.description = flags.description;
|
|
55
|
+
if (flags.priority !== undefined)
|
|
56
|
+
body.priority = flags.priority;
|
|
57
|
+
if (flags['roadmap-status'] !== undefined)
|
|
58
|
+
body.roadmap_status = flags['roadmap-status'];
|
|
59
|
+
if (Object.keys(body).length === 0) {
|
|
60
|
+
const message = 'At least one field must be provided to update (--status, --title, --description, --priority, --roadmap-status)';
|
|
61
|
+
if (this.isJsonMode) {
|
|
62
|
+
this.outputErrorAndExit({ message, code: 'MISSING_INPUT' });
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
this.error(message);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const confirmed = await this.confirmAction(`Update flow ${flags.id}?`, flags.force, { headerVariant: 'compact' });
|
|
69
|
+
if (!confirmed)
|
|
70
|
+
return;
|
|
71
|
+
const payload = {
|
|
72
|
+
path: {
|
|
73
|
+
projectId,
|
|
74
|
+
flowId: flags.id,
|
|
75
|
+
},
|
|
76
|
+
body,
|
|
77
|
+
};
|
|
78
|
+
if (this.isDryRun) {
|
|
79
|
+
await this.handleDryRun(payload);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const client = await this.getClient();
|
|
83
|
+
const { data, error } = await this.withSpinner(`Updating flow ${flags.id}...`, () => updateFlow({
|
|
84
|
+
client,
|
|
85
|
+
...payload,
|
|
86
|
+
}));
|
|
87
|
+
try {
|
|
88
|
+
const validated = validateResponse(data, error, zUpdateFlowResponse);
|
|
89
|
+
const flow = validated.data;
|
|
90
|
+
const metadata = validated.metadata;
|
|
91
|
+
if (this.isJsonMode) {
|
|
92
|
+
this.outputJson(flow, { source: 'flow update', ...metadata });
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
await this.renderInk(_jsx(SuccessMessage, { message: `Flow ${flags.id} updated successfully!` }));
|
|
96
|
+
if (this.isInteractiveMode && metadata?.action_hints) {
|
|
97
|
+
await this.handleActionHints(metadata.action_hints);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
this.handleError(`Failed to update flow: ${flags.id}`, err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -74,8 +74,8 @@ export default class IdeaGraduate extends BaseCommand {
|
|
|
74
74
|
if (this.isInteractiveMode) {
|
|
75
75
|
const { default: chalk } = await import('chalk');
|
|
76
76
|
this.log(`\n 🚀 Next steps:`);
|
|
77
|
-
this.log(` - Project info: ${chalk.cyan(`4ge project
|
|
78
|
-
this.log(` - Pull plan: ${chalk.cyan(`4ge plan
|
|
77
|
+
this.log(` - Project info: ${chalk.cyan(`4ge project:info --id ${result.id}`)}`);
|
|
78
|
+
this.log(` - Pull plan: ${chalk.cyan(`4ge plan:pull --project ${result.id}`)}`);
|
|
79
79
|
if (metadata?.action_hints) {
|
|
80
80
|
await this.handleActionHints(metadata.action_hints);
|
|
81
81
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -326,7 +326,7 @@ export default class Init extends BaseCommand {
|
|
|
326
326
|
this.log(`Skill installed at: ${skillPath}`);
|
|
327
327
|
this.log(chalk.dim('Reload your AI agent to pick up the new skill.'));
|
|
328
328
|
}
|
|
329
|
-
this.log(chalk.dim('\nConfigure additional settings with: 4ge config
|
|
329
|
+
this.log(chalk.dim('\nConfigure additional settings with: 4ge config:manage'));
|
|
330
330
|
this.log(chalk.dim('Available: default mode, hierarchy settings, pull settings\n'));
|
|
331
331
|
}
|
|
332
332
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -62,7 +62,7 @@ export default class ProjectList extends BaseCommand {
|
|
|
62
62
|
const selectedId = await this.selectOption('Select a project for details:', options, { headerVariant: 'compact' });
|
|
63
63
|
if (selectedId) {
|
|
64
64
|
const { default: chalk } = await import('chalk');
|
|
65
|
-
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge project
|
|
65
|
+
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge project:show ${selectedId}`)}\n`);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
if (metadata?.action_hints) {
|
|
@@ -65,7 +65,7 @@ export default class StoryList extends BaseCommand {
|
|
|
65
65
|
const selectedId = await this.selectOption('Select a story for details:', options, { headerVariant: 'compact' });
|
|
66
66
|
if (selectedId) {
|
|
67
67
|
const { default: chalk } = await import('chalk');
|
|
68
|
-
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge story
|
|
68
|
+
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge story:show ${selectedId}`)}\n`);
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
if (metadata?.action_hints) {
|
|
@@ -5,6 +5,8 @@ import { listTemplates } from '../../generated/api/sdk.gen.js';
|
|
|
5
5
|
import { zListTemplatesResponse } from '../../generated/api/zod.gen.js';
|
|
6
6
|
import { validateResponse } from '../../core/api/index.js';
|
|
7
7
|
import { DataTable } from '../../ui/index.js';
|
|
8
|
+
/** Render a boolean as a checkmark or empty string. Pure — extracted from the DataTable column def so it's unit-testable. */
|
|
9
|
+
export const formatCheckmark = (v) => (v ? '✓' : '');
|
|
8
10
|
export default class TemplateList extends BaseCommand {
|
|
9
11
|
static description = 'List all available project templates';
|
|
10
12
|
static flags = {
|
|
@@ -52,7 +54,7 @@ export default class TemplateList extends BaseCommand {
|
|
|
52
54
|
{ key: 'framework', header: 'Framework' },
|
|
53
55
|
{ key: 'author', header: 'Author' },
|
|
54
56
|
{ key: 'featuresCount', header: 'Features' },
|
|
55
|
-
{ key: 'isOfficial', header: 'Official', format:
|
|
57
|
+
{ key: 'isOfficial', header: 'Official', format: formatCheckmark },
|
|
56
58
|
], title: "Available Templates", emptyMessage: flags.workspace ? `No templates found for workspace: ${flags.workspace}` : 'No templates found.' }));
|
|
57
59
|
if (this.isInteractiveMode) {
|
|
58
60
|
if (filteredTemplates.length > 0) {
|
|
@@ -64,7 +66,7 @@ export default class TemplateList extends BaseCommand {
|
|
|
64
66
|
const selectedId = await this.selectOption('Select a template for details:', options, { headerVariant: 'compact' });
|
|
65
67
|
if (selectedId) {
|
|
66
68
|
const { default: chalk } = await import('chalk');
|
|
67
|
-
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge template
|
|
69
|
+
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge template:info --id ${selectedId}`)}\n`);
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
if (metadata?.action_hints) {
|
|
@@ -87,8 +87,8 @@ export default class TemplatePull extends BaseCommand {
|
|
|
87
87
|
}
|
|
88
88
|
if (this.isInteractiveMode) {
|
|
89
89
|
await this.handleActionHints([
|
|
90
|
-
{ label: 'List template features', command: `4ge template
|
|
91
|
-
{ label: 'Project info', command: '4ge project
|
|
90
|
+
{ label: 'List template features', command: `4ge template:feature:list ${templateId}` },
|
|
91
|
+
{ label: 'Project info', command: '4ge project:info' },
|
|
92
92
|
]);
|
|
93
93
|
}
|
|
94
94
|
}
|
package/dist/commands/whoami.js
CHANGED
|
@@ -16,7 +16,7 @@ export function isAuthError(error) {
|
|
|
16
16
|
*/
|
|
17
17
|
export function getAuthErrorHint(error) {
|
|
18
18
|
if (error?.code === 'TOKEN_EXPIRED') {
|
|
19
|
-
return "Run '4ge auth
|
|
19
|
+
return "Run '4ge auth:login' to re-authenticate";
|
|
20
20
|
}
|
|
21
21
|
if (error?.code === 'INVALID_API_KEY') {
|
|
22
22
|
return 'Check your 4GE_API_KEY environment variable';
|
|
@@ -146,7 +146,7 @@ export function createApiClient(opts) {
|
|
|
146
146
|
code: isPatAuth ? 'INVALID_API_KEY' : 'TOKEN_EXPIRED',
|
|
147
147
|
hint: isPatAuth
|
|
148
148
|
? 'Check your 4GE_API_KEY environment variable'
|
|
149
|
-
: "Run '4ge auth
|
|
149
|
+
: "Run '4ge auth:login' to re-authenticate",
|
|
150
150
|
};
|
|
151
151
|
return authError;
|
|
152
152
|
}
|
|
@@ -66,7 +66,7 @@ export async function ensureFreshToken() {
|
|
|
66
66
|
}
|
|
67
67
|
if (result.error || !result.data) {
|
|
68
68
|
// Server is reachable and explicitly rejected the refresh token.
|
|
69
|
-
throw new Error(`Session expired. Please re-authenticate: run '4ge auth
|
|
69
|
+
throw new Error(`Session expired. Please re-authenticate: run '4ge auth:login'\n` +
|
|
70
70
|
`Details: ${result.error?.message || 'Refresh token is invalid or expired'}`);
|
|
71
71
|
}
|
|
72
72
|
try {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// This file is auto-generated by @hey-api/openapi-ts
|
|
2
|
-
export { cliAuthCallback, cliAuthEmailLogin, cliAuthLogin, cliAuthRefresh, cliAuthStatus, createIdea, createProject, createTemplate, createTemplateFeature, deleteTemplate, deleteTemplateFeature, getEpicDetail, getEpicTree, getFeatureDetail, getFeatureTree, getLatestPlanVersion, getPlanMetadata, getPlanTree, getPlanVersionDetail, getPlanVersions, getProjectDetail, getStoryDetail, getStoryTree, getTemplateDetail, graduateIdea, listEpics, listFeatures, listIdeas, listProjects, listStories, listTemplateFeatures, listTemplates, listWorkspaces, updateEpic, updateFeature, updateIdea, updateProject, updateStory, updateTemplate, updateTemplateFeature, } from "./sdk.gen.js";
|
|
2
|
+
export { cliAuthCallback, cliAuthEmailLogin, cliAuthLogin, cliAuthRefresh, cliAuthStatus, createIdea, createProject, createTemplate, createTemplateFeature, deleteTemplate, deleteTemplateFeature, getEpicDetail, getEpicTree, getFeatureDetail, getFeatureTree, getFlow, getFlowTree, getLatestPlanVersion, getPlanMetadata, getPlanTree, getPlanVersionDetail, getPlanVersions, getProjectDetail, getStoryDetail, getStoryTree, getTemplateDetail, graduateIdea, listEpics, listFeatures, listFlows, listIdeas, listProjects, listStories, listTemplateFeatures, listTemplates, listWorkspaces, updateEpic, updateFeature, updateFlow, updateIdea, updateProject, updateStory, updateTemplate, updateTemplateFeature, } from "./sdk.gen.js";
|
|
@@ -301,6 +301,62 @@ export const getFeatureTree = (options) => (options.client ?? client).get({
|
|
|
301
301
|
url: "/api/cli/v1/projects/{projectId}/features/{featureId}/tree",
|
|
302
302
|
...options,
|
|
303
303
|
});
|
|
304
|
+
/**
|
|
305
|
+
* list-flows
|
|
306
|
+
*
|
|
307
|
+
* List flows under a feature. A feature owns one implied flow (its ordered useraction/systemresponse/conditional steps), so this returns a 1-element array. roadmap/status filters apply at the feature level.
|
|
308
|
+
*/
|
|
309
|
+
export const listFlows = (options) => (options.client ?? client).get({
|
|
310
|
+
security: [
|
|
311
|
+
{ scheme: "bearer", type: "http" },
|
|
312
|
+
{ name: "Authorization", type: "apiKey" },
|
|
313
|
+
],
|
|
314
|
+
url: "/api/cli/v1/projects/{projectId}/features/{featureId}/flows",
|
|
315
|
+
...options,
|
|
316
|
+
});
|
|
317
|
+
/**
|
|
318
|
+
* get-flow
|
|
319
|
+
*
|
|
320
|
+
* Get a flow summary. flowId is the owning feature id (a flow is the implied collection of steps attached to a feature via parent_feature_id).
|
|
321
|
+
*/
|
|
322
|
+
export const getFlow = (options) => (options.client ?? client).get({
|
|
323
|
+
security: [
|
|
324
|
+
{ scheme: "bearer", type: "http" },
|
|
325
|
+
{ name: "Authorization", type: "apiKey" },
|
|
326
|
+
],
|
|
327
|
+
url: "/api/cli/v1/projects/{projectId}/flows/{flowId}",
|
|
328
|
+
...options,
|
|
329
|
+
});
|
|
330
|
+
/**
|
|
331
|
+
* update-flow
|
|
332
|
+
*
|
|
333
|
+
* Update a flow. flowId is the owning feature id; flow-level metadata IS the feature metadata (status/label/description/roadmap/priority). Invalid enums return 422.
|
|
334
|
+
*/
|
|
335
|
+
export const updateFlow = (options) => (options.client ?? client).patch({
|
|
336
|
+
security: [
|
|
337
|
+
{ scheme: "bearer", type: "http" },
|
|
338
|
+
{ name: "Authorization", type: "apiKey" },
|
|
339
|
+
],
|
|
340
|
+
url: "/api/cli/v1/projects/{projectId}/flows/{flowId}",
|
|
341
|
+
...options,
|
|
342
|
+
headers: {
|
|
343
|
+
"Content-Type": "application/json",
|
|
344
|
+
...options.headers,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
/**
|
|
348
|
+
* get-flow-tree
|
|
349
|
+
*
|
|
350
|
+
* Get a flow with its ordered step tree. Steps are topologically sorted over project_plan_edges; conditional branches nest via parent_id.
|
|
351
|
+
*/
|
|
352
|
+
export const getFlowTree = (options) => (options.client ?? client).get({
|
|
353
|
+
security: [
|
|
354
|
+
{ scheme: "bearer", type: "http" },
|
|
355
|
+
{ name: "Authorization", type: "apiKey" },
|
|
356
|
+
],
|
|
357
|
+
url: "/api/cli/v1/projects/{projectId}/flows/{flowId}/tree",
|
|
358
|
+
...options,
|
|
359
|
+
});
|
|
304
360
|
/**
|
|
305
361
|
* list-stories
|
|
306
362
|
*
|
|
@@ -748,6 +748,173 @@ export const zGetFeatureTreeResponse = z.object({
|
|
|
748
748
|
action_hints: z.array(z.unknown()).optional(),
|
|
749
749
|
}),
|
|
750
750
|
});
|
|
751
|
+
export const zListFlowsPath = z.object({
|
|
752
|
+
projectId: z.uuid(),
|
|
753
|
+
featureId: z.uuid(),
|
|
754
|
+
});
|
|
755
|
+
export const zListFlowsQuery = z.object({
|
|
756
|
+
roadmap: z.enum(["now", "next", "later"]).optional(),
|
|
757
|
+
status: z.string().optional(),
|
|
758
|
+
});
|
|
759
|
+
/**
|
|
760
|
+
* Successful response
|
|
761
|
+
*/
|
|
762
|
+
export const zListFlowsResponse = z.object({
|
|
763
|
+
data: z.array(z.object({
|
|
764
|
+
id: z.uuid(),
|
|
765
|
+
label: z.string(),
|
|
766
|
+
description: z.string().nullable(),
|
|
767
|
+
node_type: z.enum(["flow"]),
|
|
768
|
+
status: z.enum([
|
|
769
|
+
"BACKLOG",
|
|
770
|
+
"NOT_STARTED",
|
|
771
|
+
"IN_PLANNING",
|
|
772
|
+
"READY_FOR_DEVELOPMENT",
|
|
773
|
+
"IN_DEVELOPMENT",
|
|
774
|
+
"BLOCKED",
|
|
775
|
+
"REVIEW",
|
|
776
|
+
"COMPLETE",
|
|
777
|
+
]),
|
|
778
|
+
priority: z.string().nullable(),
|
|
779
|
+
roadmap_status: z
|
|
780
|
+
.enum(["now", "next", "later", "someday", "released"])
|
|
781
|
+
.optional(),
|
|
782
|
+
parent_id: z.uuid().nullable(),
|
|
783
|
+
parent_feature_id: z.uuid().nullish(),
|
|
784
|
+
project_id: z.uuid(),
|
|
785
|
+
workspace_id: z.uuid(),
|
|
786
|
+
position_x: z.number().optional(),
|
|
787
|
+
position_y: z.number().optional(),
|
|
788
|
+
step_count: z.int().gte(0),
|
|
789
|
+
})),
|
|
790
|
+
error: z.unknown(),
|
|
791
|
+
metadata: z.object({
|
|
792
|
+
timestamp: z.string(),
|
|
793
|
+
request_id: z.string(),
|
|
794
|
+
action_hints: z.array(z.unknown()).optional(),
|
|
795
|
+
}),
|
|
796
|
+
});
|
|
797
|
+
export const zGetFlowPath = z.object({
|
|
798
|
+
projectId: z.uuid(),
|
|
799
|
+
flowId: z.uuid(),
|
|
800
|
+
});
|
|
801
|
+
/**
|
|
802
|
+
* Successful response
|
|
803
|
+
*/
|
|
804
|
+
export const zGetFlowResponse = z.object({
|
|
805
|
+
data: z.object({
|
|
806
|
+
id: z.uuid(),
|
|
807
|
+
label: z.string(),
|
|
808
|
+
description: z.string().nullable(),
|
|
809
|
+
node_type: z.enum(["flow"]),
|
|
810
|
+
status: z.enum([
|
|
811
|
+
"BACKLOG",
|
|
812
|
+
"NOT_STARTED",
|
|
813
|
+
"IN_PLANNING",
|
|
814
|
+
"READY_FOR_DEVELOPMENT",
|
|
815
|
+
"IN_DEVELOPMENT",
|
|
816
|
+
"BLOCKED",
|
|
817
|
+
"REVIEW",
|
|
818
|
+
"COMPLETE",
|
|
819
|
+
]),
|
|
820
|
+
priority: z.string().nullable(),
|
|
821
|
+
roadmap_status: z
|
|
822
|
+
.enum(["now", "next", "later", "someday", "released"])
|
|
823
|
+
.optional(),
|
|
824
|
+
parent_id: z.uuid().nullable(),
|
|
825
|
+
parent_feature_id: z.uuid().nullish(),
|
|
826
|
+
project_id: z.uuid(),
|
|
827
|
+
workspace_id: z.uuid(),
|
|
828
|
+
position_x: z.number().optional(),
|
|
829
|
+
position_y: z.number().optional(),
|
|
830
|
+
step_count: z.int().gte(0),
|
|
831
|
+
}),
|
|
832
|
+
error: z.unknown(),
|
|
833
|
+
metadata: z.object({
|
|
834
|
+
timestamp: z.string(),
|
|
835
|
+
request_id: z.string(),
|
|
836
|
+
action_hints: z.array(z.unknown()).optional(),
|
|
837
|
+
}),
|
|
838
|
+
});
|
|
839
|
+
export const zUpdateFlowBody = z.object({
|
|
840
|
+
label: z.string().min(1).optional(),
|
|
841
|
+
description: z.string().optional(),
|
|
842
|
+
status: z
|
|
843
|
+
.enum([
|
|
844
|
+
"BACKLOG",
|
|
845
|
+
"NOT_STARTED",
|
|
846
|
+
"IN_PLANNING",
|
|
847
|
+
"READY_FOR_DEVELOPMENT",
|
|
848
|
+
"IN_DEVELOPMENT",
|
|
849
|
+
"BLOCKED",
|
|
850
|
+
"REVIEW",
|
|
851
|
+
"COMPLETE",
|
|
852
|
+
])
|
|
853
|
+
.optional(),
|
|
854
|
+
priority: z.enum(["MUST_HAVE", "SHOULD_HAVE", "BACKLOG"]).optional(),
|
|
855
|
+
roadmap_status: z
|
|
856
|
+
.enum(["now", "next", "later", "someday", "released"])
|
|
857
|
+
.optional(),
|
|
858
|
+
parent_id: z.uuid().optional(),
|
|
859
|
+
});
|
|
860
|
+
export const zUpdateFlowPath = z.object({
|
|
861
|
+
projectId: z.uuid(),
|
|
862
|
+
flowId: z.uuid(),
|
|
863
|
+
});
|
|
864
|
+
/**
|
|
865
|
+
* Successful response
|
|
866
|
+
*/
|
|
867
|
+
export const zUpdateFlowResponse = z.object({
|
|
868
|
+
data: z.object({
|
|
869
|
+
id: z.uuid(),
|
|
870
|
+
label: z.string(),
|
|
871
|
+
description: z.string().nullable(),
|
|
872
|
+
node_type: z.enum(["flow"]),
|
|
873
|
+
status: z.enum([
|
|
874
|
+
"BACKLOG",
|
|
875
|
+
"NOT_STARTED",
|
|
876
|
+
"IN_PLANNING",
|
|
877
|
+
"READY_FOR_DEVELOPMENT",
|
|
878
|
+
"IN_DEVELOPMENT",
|
|
879
|
+
"BLOCKED",
|
|
880
|
+
"REVIEW",
|
|
881
|
+
"COMPLETE",
|
|
882
|
+
]),
|
|
883
|
+
priority: z.string().nullable(),
|
|
884
|
+
roadmap_status: z
|
|
885
|
+
.enum(["now", "next", "later", "someday", "released"])
|
|
886
|
+
.optional(),
|
|
887
|
+
parent_id: z.uuid().nullable(),
|
|
888
|
+
parent_feature_id: z.uuid().nullish(),
|
|
889
|
+
project_id: z.uuid(),
|
|
890
|
+
workspace_id: z.uuid(),
|
|
891
|
+
position_x: z.number().optional(),
|
|
892
|
+
position_y: z.number().optional(),
|
|
893
|
+
step_count: z.int().gte(0),
|
|
894
|
+
}),
|
|
895
|
+
error: z.unknown(),
|
|
896
|
+
metadata: z.object({
|
|
897
|
+
timestamp: z.string(),
|
|
898
|
+
request_id: z.string(),
|
|
899
|
+
action_hints: z.array(z.unknown()).optional(),
|
|
900
|
+
}),
|
|
901
|
+
});
|
|
902
|
+
export const zGetFlowTreePath = z.object({
|
|
903
|
+
projectId: z.uuid(),
|
|
904
|
+
flowId: z.uuid(),
|
|
905
|
+
});
|
|
906
|
+
/**
|
|
907
|
+
* Successful response
|
|
908
|
+
*/
|
|
909
|
+
export const zGetFlowTreeResponse = z.object({
|
|
910
|
+
data: z.unknown().optional(),
|
|
911
|
+
error: z.unknown(),
|
|
912
|
+
metadata: z.object({
|
|
913
|
+
timestamp: z.string(),
|
|
914
|
+
request_id: z.string(),
|
|
915
|
+
action_hints: z.array(z.unknown()).optional(),
|
|
916
|
+
}),
|
|
917
|
+
});
|
|
751
918
|
export const zListStoriesPath = z.object({
|
|
752
919
|
projectId: z.uuid(),
|
|
753
920
|
});
|
|
@@ -23,8 +23,9 @@ const COMPACT_ART = [
|
|
|
23
23
|
];
|
|
24
24
|
const START_COLOR = { r: 0, g: 229, b: 255 }; // #00e5ff
|
|
25
25
|
const END_COLOR = { r: 224, g: 64, b: 251 }; // #e040fb
|
|
26
|
-
const lerp = (start, end, t) => Math.round(start + (end - start) * t);
|
|
27
|
-
|
|
26
|
+
export const lerp = (start, end, t) => Math.round(start + (end - start) * t);
|
|
27
|
+
/** Apply a linear RGB gradient over a single-line string. Pure. */
|
|
28
|
+
export const applyGradient = (text, width) => {
|
|
28
29
|
return text
|
|
29
30
|
.split('')
|
|
30
31
|
.map((char, i) => {
|
|
@@ -37,14 +38,16 @@ const applyGradient = (text, width) => {
|
|
|
37
38
|
.join('');
|
|
38
39
|
};
|
|
39
40
|
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
40
|
-
const STATUS_CONFIG = {
|
|
41
|
+
export const STATUS_CONFIG = {
|
|
41
42
|
'checking-connectivity': { label: 'Checking Connectivity', icon: SPINNER_FRAMES, color: 'yellow' },
|
|
42
43
|
'refreshing-token': { label: 'Refreshing Token', icon: SPINNER_FRAMES, color: 'yellow' },
|
|
43
44
|
ready: { label: 'Ready', icon: '✓', color: 'green' },
|
|
44
45
|
'no-connection': { label: 'No Connection', icon: '✗', color: 'red' },
|
|
45
46
|
'update-required': { label: 'Update Required', icon: '⚠', color: 'magenta' },
|
|
46
47
|
};
|
|
47
|
-
|
|
48
|
+
/** Resolve the status config (label/icon/color) for a CliStatus. Pure. */
|
|
49
|
+
export const getStatusConfig = (status) => STATUS_CONFIG[status];
|
|
50
|
+
export const getArt = (variant) => {
|
|
48
51
|
const art = variant === 'compact' ? MIDDY_ART : ART;
|
|
49
52
|
const width = Math.max(...art.map((line) => line.length));
|
|
50
53
|
return { art, width };
|
|
@@ -59,7 +62,7 @@ export function renderHeaderString(version, status = 'ready', headerVariant = 'f
|
|
|
59
62
|
return `${chalk.dim('║ ')}${applyGradient(line + padding, width)}${chalk.dim(' ║')}`;
|
|
60
63
|
})
|
|
61
64
|
.join('\n');
|
|
62
|
-
const currentStatus =
|
|
65
|
+
const currentStatus = getStatusConfig(status);
|
|
63
66
|
const icon = Array.isArray(currentStatus.icon) ? currentStatus.icon[0] : currentStatus.icon;
|
|
64
67
|
const statusLabel = chalk[currentStatus.color](`${currentStatus.label} ${icon}`);
|
|
65
68
|
const statusBar = chalk.dim(`[ 4ge CLI | v${version} | Status: ${statusLabel} ]`);
|
|
@@ -82,7 +85,7 @@ export default function CliHeader({ version, status = 'ready', headerVariant = '
|
|
|
82
85
|
const { art, width } = getArt(headerVariant);
|
|
83
86
|
const borderTop = `╔${'═'.repeat(width + 2)}╗`;
|
|
84
87
|
const borderBottom = `╚${'═'.repeat(width + 2)}╝`;
|
|
85
|
-
const currentStatus =
|
|
88
|
+
const currentStatus = getStatusConfig(status);
|
|
86
89
|
const icon = Array.isArray(currentStatus.icon) ? currentStatus.icon[frame] : currentStatus.icon;
|
|
87
90
|
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: borderTop }), art.map((line, i) => {
|
|
88
91
|
const padding = ' '.repeat(width - line.length);
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Map a status value to a chalk/ink color token.
|
|
5
|
+
* Pure: given a value, returns 'green' | 'yellow' | 'red' | undefined.
|
|
6
|
+
* Exported for unit testing — the mapping is logic, the JSX is wiring.
|
|
7
|
+
*/
|
|
8
|
+
export const getStatusColor = (value) => {
|
|
4
9
|
if (typeof value !== 'string')
|
|
5
10
|
return undefined;
|
|
6
11
|
const upper = value.toUpperCase();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4ge/cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "4ge Service Architecture Bridge & CLI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"dev": "node --env-file=.env.local ./bin/dev.js",
|
|
39
39
|
"test": "node --env-file=.env.test ./node_modules/vitest/vitest.mjs run",
|
|
40
40
|
"test:watch": "node --env-file=.env.test ./node_modules/vitest/vitest.mjs",
|
|
41
|
+
"test:coverage": "node --env-file=.env.test ./node_modules/vitest/vitest.mjs run --coverage",
|
|
41
42
|
"lint": "eslint src/ --ext .ts",
|
|
42
43
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
43
44
|
"sync-api": "openapi-ts && prettier --write \"src/generated/api/**/*.ts\"",
|
package/skills/4ge-cli.md
CHANGED
|
@@ -13,15 +13,30 @@ Before using this CLI, the user **MUST** have completed:
|
|
|
13
13
|
The CLI is already installed and configured in this project.
|
|
14
14
|
|
|
15
15
|
## ⌨️ Command Syntax
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
**Convention (hard rule):** commands are `4ge <namespace>:<command> [--params]` —
|
|
18
|
+
a colon between namespace and command, **never a space**.
|
|
19
|
+
|
|
20
|
+
| Correct | Wrong |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `4ge epic:list` | ~~`4ge epic list`~~ |
|
|
23
|
+
| `4ge project:pull --json` | ~~`4ge project pull --json`~~ |
|
|
24
|
+
| `4ge flow:show --id <id>` | ~~`4ge flow show --id <id>`~~ |
|
|
25
|
+
| `4ge auth:status` | ~~`4ge auth status`~~ |
|
|
26
|
+
|
|
27
|
+
Nested commands chain with more colons: `4ge template:feature:list`.
|
|
28
|
+
A few root commands take no colon (`4ge init`, `4ge whoami`, `4ge login`) —
|
|
29
|
+
single words with no nested command.
|
|
30
|
+
|
|
31
|
+
Full form:
|
|
17
32
|
|
|
18
33
|
```bash
|
|
19
34
|
4ge <namespace>:<command> [options] --json
|
|
20
35
|
```
|
|
21
36
|
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
|
|
37
|
+
- **`--json` is mandatory** in agent mode for machine-readable output.
|
|
38
|
+
- **Positional arguments**: a few commands take a direct arg without a flag
|
|
39
|
+
(e.g. `4ge idea:show <ID>`).
|
|
25
40
|
|
|
26
41
|
## 🛠 Usage Guidelines
|
|
27
42
|
|
|
@@ -80,6 +95,7 @@ Use it for network-flaky operations.
|
|
|
80
95
|
|-----------|----------|-----------|
|
|
81
96
|
| **project** | `create`, `info`, `list`, `pull`, `update` | `--workspace` (create), `--name`, `--description`, `--template`, `--force`, `--save`/`--stdout` (pull), `--dry-run`, `--retry` |
|
|
82
97
|
| **epic** | `list`, `show`, `pull`, `update` | `--epic-id` / `--id` (pull), `--status`, `--title`, `--recursive` (pull), `--roadmap` (pull, with `--recursive`), `--save`/`--stdout` (pull), `--dry-run`, `--retry` |
|
|
98
|
+
| **flow** | `list`, `show`, `pull`, `update` | `--id`, `--feature-id` (list), `--recursive` (pull — full flow step tree), `--status`, `--title` (update), `--description` (update), `--priority` (update), `--roadmap-status` (update), `--save`/`--stdout` (pull), `--dry-run`, `--retry` |
|
|
83
99
|
| **story** | `list`, `show`, `pull`, `update` | `--story-id` / `--id` (pull), `--status`, `--title`, `--recursive` (pull), `--roadmap` (pull, with `--recursive`), `--save`/`--stdout` (pull), `--dry-run`, `--retry` |
|
|
84
100
|
| **feature** | `list`, `show`, `pull`, `update` | `--feature-id` / `--id` (pull), `--status`, `--roadmap`, `--recursive` (pull), `--roadmap` (pull, with `--recursive`), `--save`/`--stdout` (pull), `--dry-run`, `--retry` |
|
|
85
101
|
| **idea** | `create`, `graduate`, `list`, `show`, `update` | `<ID>` (positional for show), `--title`, `--workspace`, `--dry-run`, `--retry` |
|
|
@@ -102,6 +118,76 @@ No `--project-id` flag is needed for those namespaces.
|
|
|
102
118
|
|
|
103
119
|
Flags always override config; bare `4ge <ns>:pull --id <id>` honours the config defaults.
|
|
104
120
|
|
|
121
|
+
### Plan data model — parent-link semantics
|
|
122
|
+
|
|
123
|
+
**Read this before reconstructing a hierarchy from `list`/`pull --json` output.
|
|
124
|
+
A wrong field will falsely make everything look orphaned.**
|
|
125
|
+
|
|
126
|
+
Each project plan node (epic / story / feature / flow step) carries TWO
|
|
127
|
+
parent-link fields. They coexist by design — neither is legacy — and they
|
|
128
|
+
serve different relationships:
|
|
129
|
+
|
|
130
|
+
| Field | Meaning | Populated on |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `parent_id` | The **hierarchy tree** edge. epic → story → feature, plus step → step containment inside flows. | epics, stories, features, flow steps |
|
|
133
|
+
| `parent_feature_id` | A **flow-ownership** edge. Used ONLY by flow-step nodes (`node_type`: `useraction` / `systemresponse` / `conditional`) to attach to their owning feature. These steps deliberately have `parent_id` null (or pointing at another step). | flow steps only |
|
|
134
|
+
|
|
135
|
+
**Canonical hierarchy is `epic → story → feature`** (parent points UP
|
|
136
|
+
toward the epic), and the tree read bottom-up is:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
feature.parent_id → a story id → (story.parent_id) → an epic
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Not feature → epic directly — the story is the mandatory intermediate node.
|
|
143
|
+
|
|
144
|
+
#### Rules agents must follow
|
|
145
|
+
|
|
146
|
+
1. **For features and stories, `parent_feature_id` is null by design.**
|
|
147
|
+
Features and stories are never flow steps, so they use `parent_id`
|
|
148
|
+
exclusively. A null `parent_feature_id` on a feature/story is NOT a
|
|
149
|
+
broken link and NOT an orphan. Aggregating `parent_feature_id` to
|
|
150
|
+
reconstruct the tree will falsely report "orphans everywhere" — do
|
|
151
|
+
not do this.
|
|
152
|
+
|
|
153
|
+
2. **To reconstruct the hierarchy from list output, use `parent_id`,
|
|
154
|
+
never `parent_feature_id`:**
|
|
155
|
+
- `feature.parent_id` → a story id (the story that owns the feature).
|
|
156
|
+
- `story.parent_id` → an epic id.
|
|
157
|
+
- Group features by epic: resolve `feature.parent_id` → story, then
|
|
158
|
+
`story.parent_id` → epic.
|
|
159
|
+
|
|
160
|
+
3. **`children: []` under a filtered query on a flow-bearing feature is
|
|
161
|
+
a known, narrow bug — not proof of corruption.** Under an active
|
|
162
|
+
roadmap/status/priority filter, flow-step nodes linked ONLY via
|
|
163
|
+
`parent_feature_id` (no usable `parent_id`) can be pruned in the tree
|
|
164
|
+
builder and surface as `children: []` on their owning feature. This
|
|
165
|
+
affects **flow steps only** — never the `parent_id` epic→story→feature
|
|
166
|
+
backbone. If you see empty `children` only under a filtered query on a
|
|
167
|
+
feature that bears flows, cite this caveat before claiming the
|
|
168
|
+
hierarchy is broken.
|
|
169
|
+
|
|
170
|
+
*Contract:* the CLI surfaces the server's tree as-is. There is no CLI
|
|
171
|
+
flag to work around the flow-step pruning case; if you hit it, query
|
|
172
|
+
without the filter, or pull the flow's own step tree instead:
|
|
173
|
+
`flow:list --feature-id <featureId> --json` to resolve the flow id, then
|
|
174
|
+
`flow:pull --id <flowId> --recursive --json` (the flow step tree is not
|
|
175
|
+
subject to the same pruning).
|
|
176
|
+
|
|
177
|
+
#### Quick check: is the hierarchy intact?
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Every feature's parent_id should resolve to a story id in story:list.
|
|
181
|
+
4ge feature:list --json # collect feature[*].parent_id
|
|
182
|
+
4ge story:list --json # every parent_id from above must appear here
|
|
183
|
+
# Every story's parent_id should resolve to an epic id in epic:list.
|
|
184
|
+
4ge story:list --json # collect story[*].parent_id
|
|
185
|
+
4ge epic:list --json # every parent_id from above must appear here
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
If those joins are complete, the hierarchy is intact regardless of how
|
|
189
|
+
many `parent_feature_id` fields are null.
|
|
190
|
+
|
|
105
191
|
## 🔄 Common Workflow Patterns
|
|
106
192
|
|
|
107
193
|
1. **Explore projects**: `4ge project:list --json`
|