@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.
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/bin/dev.js +3 -0
- package/bin/run.js +3 -0
- package/dist/base.js +398 -0
- package/dist/commands/auth/login.js +197 -0
- package/dist/commands/auth/logout.js +31 -0
- package/dist/commands/auth/profile/delete.js +34 -0
- package/dist/commands/auth/profile/list.js +32 -0
- package/dist/commands/auth/profile/use.js +30 -0
- package/dist/commands/auth/status.js +107 -0
- package/dist/commands/config/get.js +37 -0
- package/dist/commands/config/index.js +9 -0
- package/dist/commands/config/manage.js +165 -0
- package/dist/commands/config/set.js +42 -0
- package/dist/commands/config/show.js +46 -0
- package/dist/commands/epic/index.js +10 -0
- package/dist/commands/epic/list.js +91 -0
- package/dist/commands/epic/pull.js +137 -0
- package/dist/commands/epic/show.js +67 -0
- package/dist/commands/epic/update.js +104 -0
- package/dist/commands/feature/index.js +2 -0
- package/dist/commands/feature/list.js +93 -0
- package/dist/commands/feature/pull.js +122 -0
- package/dist/commands/feature/show.js +60 -0
- package/dist/commands/feature/update.js +103 -0
- package/dist/commands/idea/create.js +88 -0
- package/dist/commands/idea/graduate.js +89 -0
- package/dist/commands/idea/index.js +5 -0
- package/dist/commands/idea/list.js +83 -0
- package/dist/commands/idea/show.js +86 -0
- package/dist/commands/idea/update.js +108 -0
- package/dist/commands/init.js +354 -0
- package/dist/commands/login.js +5 -0
- package/dist/commands/plan/index.js +10 -0
- package/dist/commands/plan/pull.js +104 -0
- package/dist/commands/plan/versions.js +64 -0
- package/dist/commands/project/create.js +84 -0
- package/dist/commands/project/index.js +9 -0
- package/dist/commands/project/info.js +74 -0
- package/dist/commands/project/list.js +78 -0
- package/dist/commands/project/pull.js +127 -0
- package/dist/commands/project/update.js +104 -0
- package/dist/commands/root.js +13 -0
- package/dist/commands/skill/index.js +8 -0
- package/dist/commands/skill/install.js +71 -0
- package/dist/commands/story/index.js +10 -0
- package/dist/commands/story/list.js +81 -0
- package/dist/commands/story/pull.js +125 -0
- package/dist/commands/story/show.js +57 -0
- package/dist/commands/story/update.js +103 -0
- package/dist/commands/template/create.js +72 -0
- package/dist/commands/template/delete.js +62 -0
- package/dist/commands/template/feature/create.js +70 -0
- package/dist/commands/template/feature/delete.js +75 -0
- package/dist/commands/template/feature/list.js +69 -0
- package/dist/commands/template/feature/update.js +82 -0
- package/dist/commands/template/info.js +52 -0
- package/dist/commands/template/list.js +76 -0
- package/dist/commands/template/pull.js +99 -0
- package/dist/commands/template/update.js +71 -0
- package/dist/commands/whoami.js +5 -0
- package/dist/core/api/action-hints.js +17 -0
- package/dist/core/api/index.js +3 -0
- package/dist/core/api/retry.js +95 -0
- package/dist/core/api/validate-response.js +41 -0
- package/dist/core/auth/api-client.js +156 -0
- package/dist/core/auth/config.js +3 -0
- package/dist/core/auth/credentials.js +143 -0
- package/dist/core/auth/email-auth.js +30 -0
- package/dist/core/auth/errors.js +20 -0
- package/dist/core/auth/index.js +6 -0
- package/dist/core/auth/realtime.js +82 -0
- package/dist/core/auth/resolution.js +63 -0
- package/dist/core/auth/state.js +9 -0
- package/dist/core/auth/token-refresh.js +100 -0
- package/dist/core/config/defaults.js +31 -0
- package/dist/core/config/index.js +3 -0
- package/dist/core/config/parser.js +213 -0
- package/dist/core/config/schema.js +29 -0
- package/dist/core/project/markdown.js +37 -0
- package/dist/core/skill/installer.js +59 -0
- package/dist/core/skill/paths.js +52 -0
- package/dist/generated/api/client/client.gen.js +217 -0
- package/dist/generated/api/client/index.js +6 -0
- package/dist/generated/api/client/types.gen.js +2 -0
- package/dist/generated/api/client/utils.gen.js +231 -0
- package/dist/generated/api/client.gen.js +3 -0
- package/dist/generated/api/core/auth.gen.js +14 -0
- package/dist/generated/api/core/bodySerializer.gen.js +57 -0
- package/dist/generated/api/core/params.gen.js +103 -0
- package/dist/generated/api/core/pathSerializer.gen.js +114 -0
- package/dist/generated/api/core/queryKeySerializer.gen.js +99 -0
- package/dist/generated/api/core/serverSentEvents.gen.js +136 -0
- package/dist/generated/api/core/types.gen.js +2 -0
- package/dist/generated/api/core/utils.gen.js +87 -0
- package/dist/generated/api/index.js +2 -0
- package/dist/generated/api/sdk.gen.js +556 -0
- package/dist/generated/api/types.gen.js +2 -0
- package/dist/generated/api/zod.gen.js +1262 -0
- package/dist/ui/components/action-hints.js +33 -0
- package/dist/ui/components/cli-header.js +91 -0
- package/dist/ui/components/confirm-prompt.js +28 -0
- package/dist/ui/components/data-table.js +35 -0
- package/dist/ui/components/detail-view.js +5 -0
- package/dist/ui/components/error-display.js +5 -0
- package/dist/ui/components/prompt-layout.js +6 -0
- package/dist/ui/components/select-prompt.js +28 -0
- package/dist/ui/components/spinner.js +14 -0
- package/dist/ui/components/success-message.js +5 -0
- package/dist/ui/components/text-input.js +29 -0
- package/dist/ui/hooks/use-action-hints.js +21 -0
- package/dist/ui/hooks/use-status.js +22 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/prompt-helpers.js +55 -0
- package/dist/ui/render.js +9 -0
- package/package.json +89 -0
- package/skills/4ge-cli.md +113 -0
- package/usage.md +320 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../base.js';
|
|
4
|
+
import { createProject } from '../../generated/api/sdk.gen.js';
|
|
5
|
+
import { zCreateProjectResponse } 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 ProjectCreate extends BaseCommand {
|
|
9
|
+
static description = 'Create a new project';
|
|
10
|
+
static flags = {
|
|
11
|
+
...BaseCommand.baseFlags,
|
|
12
|
+
workspace: Flags.string({
|
|
13
|
+
description: 'The workspace ID to create the project in',
|
|
14
|
+
required: true,
|
|
15
|
+
}),
|
|
16
|
+
name: Flags.string({
|
|
17
|
+
description: 'Project name',
|
|
18
|
+
required: true,
|
|
19
|
+
}),
|
|
20
|
+
description: Flags.string({
|
|
21
|
+
description: 'Project description',
|
|
22
|
+
}),
|
|
23
|
+
'problem-statement': Flags.string({
|
|
24
|
+
description: 'Problem statement',
|
|
25
|
+
}),
|
|
26
|
+
'project-rules': Flags.string({
|
|
27
|
+
description: 'Project rules',
|
|
28
|
+
}),
|
|
29
|
+
template: Flags.string({
|
|
30
|
+
description: 'Template ID (maps to project_type_id)',
|
|
31
|
+
}),
|
|
32
|
+
force: Flags.boolean({
|
|
33
|
+
description: 'Skip confirmation prompt',
|
|
34
|
+
default: false,
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
static examples = [
|
|
38
|
+
'<%= config.bin %> project:create --workspace <uuid> --name "My Project"',
|
|
39
|
+
'<%= config.bin %> project:create --workspace <uuid> --name "My Project" --template <uuid>',
|
|
40
|
+
];
|
|
41
|
+
async run() {
|
|
42
|
+
const { flags } = await this.parse(ProjectCreate);
|
|
43
|
+
await this.renderHeader('ready', 'compact');
|
|
44
|
+
const confirmed = await this.confirmAction(`Create new project "${flags.name}"?`, flags.force, { headerVariant: 'compact' });
|
|
45
|
+
if (!confirmed)
|
|
46
|
+
return;
|
|
47
|
+
const payload = {
|
|
48
|
+
body: {
|
|
49
|
+
name: flags.name,
|
|
50
|
+
workspace_id: flags.workspace,
|
|
51
|
+
...(flags.description !== undefined && { description: flags.description }),
|
|
52
|
+
...(flags['problem-statement'] !== undefined && { problem_statement: flags['problem-statement'] }),
|
|
53
|
+
...(flags['project-rules'] !== undefined && { project_rules: flags['project-rules'] }),
|
|
54
|
+
...(flags.template !== undefined && { project_type_id: flags.template }),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
if (this.isDryRun) {
|
|
58
|
+
await this.handleDryRun(payload);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const client = await this.getClient();
|
|
62
|
+
const { data, error } = await this.withSpinner('Creating project...', () => createProject({
|
|
63
|
+
client,
|
|
64
|
+
...payload,
|
|
65
|
+
}));
|
|
66
|
+
try {
|
|
67
|
+
const validated = validateResponse(data, error, zCreateProjectResponse);
|
|
68
|
+
const project = validated.data;
|
|
69
|
+
const metadata = validated.metadata;
|
|
70
|
+
if (this.isJsonMode) {
|
|
71
|
+
this.outputJson(project, { source: 'project create', ...metadata });
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
await this.renderInk(_jsx(SuccessMessage, { message: `Created project "${project.name}" (id: ${project.id}) in workspace (id: ${flags.workspace})` }));
|
|
75
|
+
if (this.isInteractiveMode && metadata?.action_hints) {
|
|
76
|
+
await this.handleActionHints(metadata.action_hints);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
this.handleError('Failed to create project', err);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base.js';
|
|
2
|
+
export default class Project extends BaseCommand {
|
|
3
|
+
static description = 'Interact with projects';
|
|
4
|
+
static hidden = true;
|
|
5
|
+
async run() {
|
|
6
|
+
await this.renderHeader('ready', 'compact');
|
|
7
|
+
this.log('Use `4ge project list` to list projects.');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base.js';
|
|
2
|
+
import { readConfig } from '../../core/config/index.js';
|
|
3
|
+
import { getProjectDetail } from '../../generated/api/sdk.gen.js';
|
|
4
|
+
import { zGetProjectDetailResponse } from '../../generated/api/zod.gen.js';
|
|
5
|
+
import { validateResponse } from '../../core/api/index.js';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
export default class ProjectInfo extends BaseCommand {
|
|
8
|
+
static description = 'Get detailed information about the current project';
|
|
9
|
+
static flags = {
|
|
10
|
+
...BaseCommand.baseFlags,
|
|
11
|
+
};
|
|
12
|
+
static examples = [
|
|
13
|
+
'<%= config.bin %> project:info',
|
|
14
|
+
'<%= config.bin %> project:info --json',
|
|
15
|
+
];
|
|
16
|
+
async run() {
|
|
17
|
+
const config = await readConfig();
|
|
18
|
+
if (!config?.project_id) {
|
|
19
|
+
const message = 'Project not initialized. Run `4ge init` first to set project context.';
|
|
20
|
+
if (this.isJsonMode) {
|
|
21
|
+
this.outputErrorAndExit({ message, code: 'NOT_INITIALIZED' });
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
this.error(message);
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
await this.renderHeader('ready', 'compact');
|
|
29
|
+
const client = await this.getClient();
|
|
30
|
+
try {
|
|
31
|
+
const { data, error } = await this.withRetry(() => getProjectDetail({
|
|
32
|
+
client,
|
|
33
|
+
path: { projectId: config.project_id },
|
|
34
|
+
}));
|
|
35
|
+
const validated = validateResponse(data, error, zGetProjectDetailResponse);
|
|
36
|
+
const p = validated.data;
|
|
37
|
+
// 3. Output
|
|
38
|
+
if (this.isJsonMode) {
|
|
39
|
+
this.outputJson(p, {
|
|
40
|
+
source: 'project info',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
this.log('--- Project Information ---');
|
|
45
|
+
const rows = [
|
|
46
|
+
['Name', p.name],
|
|
47
|
+
['ID', p.id],
|
|
48
|
+
['Status', p.status?.toUpperCase()],
|
|
49
|
+
['Description', p.description || '-'],
|
|
50
|
+
['Problem Statement', p.problem_statement || '-'],
|
|
51
|
+
['Project Rules', p.project_rules || '-'],
|
|
52
|
+
['Workspace ID', p.workspace_id],
|
|
53
|
+
['Project Type ID', p.project_type_id],
|
|
54
|
+
['Created At', p.created_at ? new Date(p.created_at).toLocaleString() : '-'],
|
|
55
|
+
['Updated At', p.updated_at ? new Date(p.updated_at).toLocaleString() : '-'],
|
|
56
|
+
];
|
|
57
|
+
for (const [key, value] of rows) {
|
|
58
|
+
this.log(`${chalk.blue((key ?? '').padEnd(20))}: ${value}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (this.isJsonMode) {
|
|
64
|
+
this.outputErrorAndExit({
|
|
65
|
+
message: err.message || 'An unexpected error occurred',
|
|
66
|
+
code: 'UNKNOWN_ERROR',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
this.error(err.message || 'An unexpected error occurred');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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 { listProjects } from '../../generated/api/sdk.gen.js';
|
|
5
|
+
import { zListProjectsResponse } 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 ProjectList extends BaseCommand {
|
|
9
|
+
static description = 'List all projects in the current workspace';
|
|
10
|
+
static flags = {
|
|
11
|
+
...BaseCommand.baseFlags,
|
|
12
|
+
};
|
|
13
|
+
static examples = [
|
|
14
|
+
'<%= config.bin %> project:list',
|
|
15
|
+
'<%= config.bin %> project:list --json',
|
|
16
|
+
];
|
|
17
|
+
async run() {
|
|
18
|
+
const config = await readConfig();
|
|
19
|
+
await this.renderHeader('ready', 'compact');
|
|
20
|
+
if (!config?.workspace_id) {
|
|
21
|
+
if (this.isJsonMode) {
|
|
22
|
+
this.outputErrorAndExit({
|
|
23
|
+
message: 'Project not initialized. Run `4ge init` first to set workspace context.',
|
|
24
|
+
code: 'NOT_INITIALIZED',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
this.error('Project not initialized. Run `4ge init` first to set workspace context.');
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const client = await this.getClient();
|
|
33
|
+
try {
|
|
34
|
+
const projects = await this.withSpinner('Fetching projects...', () => listProjects({ client }));
|
|
35
|
+
const { data, error } = projects;
|
|
36
|
+
const validated = validateResponse(data, error, zListProjectsResponse);
|
|
37
|
+
const items = validated.data;
|
|
38
|
+
const metadata = validated.metadata;
|
|
39
|
+
if (this.isJsonMode) {
|
|
40
|
+
this.outputJson(items, {
|
|
41
|
+
source: 'project list',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
await this.renderInk(_jsx(DataTable, { data: items, columns: [
|
|
46
|
+
{ key: 'name', header: 'Name', minWidth: 20 },
|
|
47
|
+
{ key: 'id', header: 'ID' },
|
|
48
|
+
{ key: 'status', header: 'Status', color: 'auto' },
|
|
49
|
+
{
|
|
50
|
+
key: 'created_at',
|
|
51
|
+
header: 'Created',
|
|
52
|
+
format: (v) => (v ? new Date(v).toLocaleDateString() : '-'),
|
|
53
|
+
},
|
|
54
|
+
], title: `Projects in ${config.workspace_name || config.workspace_id}`, emptyMessage: "No projects found." }));
|
|
55
|
+
if (this.isInteractiveMode) {
|
|
56
|
+
if (items.length > 0) {
|
|
57
|
+
const options = items.map((item) => ({
|
|
58
|
+
label: item.name || item.id,
|
|
59
|
+
value: item.id,
|
|
60
|
+
description: item.status || '',
|
|
61
|
+
}));
|
|
62
|
+
const selectedId = await this.selectOption('Select a project for details:', options, { headerVariant: 'compact' });
|
|
63
|
+
if (selectedId) {
|
|
64
|
+
const { default: chalk } = await import('chalk');
|
|
65
|
+
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge project show ${selectedId}`)}\n`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (metadata?.action_hints) {
|
|
69
|
+
await this.handleActionHints(metadata.action_hints);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
this.handleError('An unexpected error occurred', err);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base.js';
|
|
2
|
+
import { readConfig, getAssetPath, getProjectDocsDir } from '../../core/config/index.js';
|
|
3
|
+
import { getProjectDetail } from '../../generated/api/sdk.gen.js';
|
|
4
|
+
import { zGetProjectDetailResponse } from '../../generated/api/zod.gen.js';
|
|
5
|
+
import { validateResponse } from '../../core/api/index.js';
|
|
6
|
+
import { generateProblemStatementMd, generateProjectRulesMd, generateReadmeMd, } from '../../core/project/markdown.js';
|
|
7
|
+
import { Flags } from '@oclif/core';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import fs from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
export default class ProjectPull extends BaseCommand {
|
|
12
|
+
static description = 'Pull project metadata from the cloud and save it locally';
|
|
13
|
+
static flags = {
|
|
14
|
+
...BaseCommand.baseFlags,
|
|
15
|
+
save: Flags.boolean({
|
|
16
|
+
description: 'Force saving to disk even in --json mode',
|
|
17
|
+
default: false,
|
|
18
|
+
}),
|
|
19
|
+
stdout: Flags.boolean({
|
|
20
|
+
description: 'Print to terminal without saving (human mode only)',
|
|
21
|
+
default: false,
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
static examples = [
|
|
25
|
+
'<%= config.bin %> project:pull',
|
|
26
|
+
'<%= config.bin %> project:pull --stdout',
|
|
27
|
+
'<%= config.bin %> project:pull --json',
|
|
28
|
+
'<%= config.bin %> project:pull --json --save',
|
|
29
|
+
];
|
|
30
|
+
async run() {
|
|
31
|
+
const { flags } = await this.parse(ProjectPull);
|
|
32
|
+
// Validate flag combination
|
|
33
|
+
if (flags.json && flags.stdout) {
|
|
34
|
+
this.error('Cannot use --json and --stdout together');
|
|
35
|
+
}
|
|
36
|
+
if (flags.save && flags.stdout) {
|
|
37
|
+
this.error('Cannot use --stdout and --save together');
|
|
38
|
+
}
|
|
39
|
+
await this.renderHeader('ready', 'compact');
|
|
40
|
+
// 1. Resolve Auth and Config
|
|
41
|
+
await this.migrateAssetsIfNeeded();
|
|
42
|
+
const client = await this.getClient();
|
|
43
|
+
const config = await readConfig();
|
|
44
|
+
if (!config?.project_id) {
|
|
45
|
+
const message = 'Project not initialized. Run `4ge init` first to set project context.';
|
|
46
|
+
if (this.isJsonMode) {
|
|
47
|
+
this.outputErrorAndExit({ message, code: 'NOT_INITIALIZED' });
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
this.error(message);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// 2. Call API
|
|
55
|
+
try {
|
|
56
|
+
const { data, error } = await this.withRetry(() => getProjectDetail({
|
|
57
|
+
client,
|
|
58
|
+
path: { projectId: config.project_id },
|
|
59
|
+
}));
|
|
60
|
+
const validated = validateResponse(data, error, zGetProjectDetailResponse);
|
|
61
|
+
const projectData = validated.data;
|
|
62
|
+
const savePath = getAssetPath('project.json');
|
|
63
|
+
// 3. Output Logic
|
|
64
|
+
if (this.isJsonMode) {
|
|
65
|
+
this.outputJson(projectData);
|
|
66
|
+
if (flags.save) {
|
|
67
|
+
await this.saveToFile(savePath, projectData);
|
|
68
|
+
await this.saveMarkdownFiles(projectData, false);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (flags.stdout) {
|
|
72
|
+
const problemStatement = generateProblemStatementMd(projectData);
|
|
73
|
+
const projectRules = generateProjectRulesMd(projectData);
|
|
74
|
+
const filesGenerated = [];
|
|
75
|
+
if (problemStatement)
|
|
76
|
+
filesGenerated.push('problem-statement.md');
|
|
77
|
+
if (projectRules)
|
|
78
|
+
filesGenerated.push('project-rules.md');
|
|
79
|
+
const readme = generateReadmeMd(projectData, filesGenerated);
|
|
80
|
+
this.log(readme);
|
|
81
|
+
if (problemStatement) {
|
|
82
|
+
this.log('\n---\n');
|
|
83
|
+
this.log(problemStatement);
|
|
84
|
+
}
|
|
85
|
+
if (projectRules) {
|
|
86
|
+
this.log('\n---\n');
|
|
87
|
+
this.log(projectRules);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
await this.saveToFile(savePath, projectData);
|
|
92
|
+
this.log(`${chalk.green('✓')} Project metadata saved to .4ge/assets/project.json`);
|
|
93
|
+
await this.saveMarkdownFiles(projectData, true);
|
|
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
|
+
async saveMarkdownFiles(project, verbose) {
|
|
105
|
+
const projectDir = getProjectDocsDir();
|
|
106
|
+
await fs.mkdir(projectDir, { recursive: true });
|
|
107
|
+
const filesToGenerate = [
|
|
108
|
+
{ name: 'problem-statement.md', content: generateProblemStatementMd(project) },
|
|
109
|
+
{ name: 'project-rules.md', content: generateProjectRulesMd(project) },
|
|
110
|
+
];
|
|
111
|
+
const actualFiles = [];
|
|
112
|
+
for (const { name, content } of filesToGenerate) {
|
|
113
|
+
if (content) {
|
|
114
|
+
await fs.writeFile(path.join(projectDir, name), content, 'utf-8');
|
|
115
|
+
actualFiles.push(name);
|
|
116
|
+
if (verbose) {
|
|
117
|
+
this.log(`${chalk.green('✓')} Created .4ge/assets/project/${name}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const readmeContent = generateReadmeMd(project, actualFiles);
|
|
122
|
+
await fs.writeFile(path.join(projectDir, 'README.md'), readmeContent, 'utf-8');
|
|
123
|
+
if (verbose) {
|
|
124
|
+
this.log(`${chalk.green('✓')} Created .4ge/assets/project/README.md`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../base.js';
|
|
4
|
+
import { updateProject } from '../../generated/api/sdk.gen.js';
|
|
5
|
+
import { zUpdateProjectResponse } from '../../generated/api/zod.gen.js';
|
|
6
|
+
import { validateResponse } from '../../core/api/index.js';
|
|
7
|
+
import { readConfig } from '../../core/config/parser.js';
|
|
8
|
+
import { SuccessMessage } from '../../ui/index.js';
|
|
9
|
+
export default class ProjectUpdate extends BaseCommand {
|
|
10
|
+
static description = 'Update the currently initialized project';
|
|
11
|
+
static flags = {
|
|
12
|
+
...BaseCommand.baseFlags,
|
|
13
|
+
name: Flags.string({
|
|
14
|
+
description: 'New project name',
|
|
15
|
+
}),
|
|
16
|
+
description: Flags.string({
|
|
17
|
+
description: 'New description',
|
|
18
|
+
}),
|
|
19
|
+
'problem-statement': Flags.string({
|
|
20
|
+
description: 'New problem statement',
|
|
21
|
+
}),
|
|
22
|
+
'project-rules': Flags.string({
|
|
23
|
+
description: 'New project rules',
|
|
24
|
+
}),
|
|
25
|
+
template: Flags.string({
|
|
26
|
+
description: 'New template ID (maps to project_type_id)',
|
|
27
|
+
}),
|
|
28
|
+
force: Flags.boolean({
|
|
29
|
+
description: 'Skip confirmation prompt',
|
|
30
|
+
default: false,
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
static examples = [
|
|
34
|
+
'<%= config.bin %> project:update --name "New Project Name"',
|
|
35
|
+
'<%= config.bin %> project:update --description "New description"',
|
|
36
|
+
'<%= config.bin %> project:update --template <uuid>',
|
|
37
|
+
];
|
|
38
|
+
async run() {
|
|
39
|
+
const { flags } = await this.parse(ProjectUpdate);
|
|
40
|
+
const config = await readConfig();
|
|
41
|
+
if (!config?.project_id) {
|
|
42
|
+
const message = 'No project initialized. Run `4ge init` first.';
|
|
43
|
+
if (this.isJsonMode) {
|
|
44
|
+
this.outputErrorAndExit({ message, code: 'NOT_INITIALIZED' });
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
this.error(message);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
await this.renderHeader('ready', 'compact');
|
|
52
|
+
const body = {
|
|
53
|
+
...(flags.name !== undefined && { name: flags.name }),
|
|
54
|
+
...(flags.description !== undefined && { description: flags.description }),
|
|
55
|
+
...(flags['problem-statement'] !== undefined && { problem_statement: flags['problem-statement'] }),
|
|
56
|
+
...(flags['project-rules'] !== undefined && { project_rules: flags['project-rules'] }),
|
|
57
|
+
...(flags.template !== undefined && { project_type_id: flags.template }),
|
|
58
|
+
};
|
|
59
|
+
// Check if at least one field is provided for update
|
|
60
|
+
if (Object.keys(body).length === 0) {
|
|
61
|
+
const message = 'Provide at least one field to update';
|
|
62
|
+
if (this.isJsonMode) {
|
|
63
|
+
this.outputErrorAndExit({ message, code: 'MISSING_INPUT' });
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
this.error(message);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const confirmed = await this.confirmAction(`Update project "${config.project_id}"?`, flags.force, { headerVariant: 'compact' });
|
|
71
|
+
if (!confirmed)
|
|
72
|
+
return;
|
|
73
|
+
const payload = {
|
|
74
|
+
path: { projectId: config.project_id },
|
|
75
|
+
body,
|
|
76
|
+
};
|
|
77
|
+
if (this.isDryRun) {
|
|
78
|
+
await this.handleDryRun(payload);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const client = await this.getClient();
|
|
82
|
+
const { data, error } = await this.withSpinner('Updating project...', () => updateProject({
|
|
83
|
+
client,
|
|
84
|
+
...payload,
|
|
85
|
+
}));
|
|
86
|
+
try {
|
|
87
|
+
const validated = validateResponse(data, error, zUpdateProjectResponse);
|
|
88
|
+
const project = validated.data;
|
|
89
|
+
const metadata = validated.metadata;
|
|
90
|
+
if (this.isJsonMode) {
|
|
91
|
+
this.outputJson(project, { source: 'project update', ...metadata });
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
await this.renderInk(_jsx(SuccessMessage, { message: `Updated project "${project.name}" (id: ${project.id})` }));
|
|
95
|
+
if (this.isInteractiveMode && metadata?.action_hints) {
|
|
96
|
+
await this.handleActionHints(metadata.action_hints);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
this.handleError(`Failed to update project: ${config.project_id}`, err);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BaseCommand } from '../base.js';
|
|
2
|
+
export default class Root extends BaseCommand {
|
|
3
|
+
static hidden = true;
|
|
4
|
+
static description = 'A Universal Interface for 4ge Resources';
|
|
5
|
+
static flags = {
|
|
6
|
+
...BaseCommand.baseFlags,
|
|
7
|
+
};
|
|
8
|
+
async run() {
|
|
9
|
+
// If no subcommand, show help
|
|
10
|
+
await this.renderHeader('ready', 'compact');
|
|
11
|
+
await this.config.runCommand('help');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class SkillIndex extends Command {
|
|
3
|
+
static description = 'Install and manage the 4ge CLI agent skill';
|
|
4
|
+
static hidden = true;
|
|
5
|
+
async run() {
|
|
6
|
+
this.log('Use `4ge skill:install` to install the 4ge CLI skill for AI agents.');
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { BaseCommand } from '../../base.js';
|
|
4
|
+
import { installSkill } from '../../core/skill/installer.js';
|
|
5
|
+
import { isKnownAgent } from '../../core/skill/paths.js';
|
|
6
|
+
import { SuccessMessage } from '../../ui/index.js';
|
|
7
|
+
export default class SkillInstall extends BaseCommand {
|
|
8
|
+
static description = 'Install the 4ge CLI agent skill into the current project (or globally). Mirrors the `npx skills` directory convention.';
|
|
9
|
+
static flags = {
|
|
10
|
+
...BaseCommand.baseFlags,
|
|
11
|
+
agent: Flags.string({
|
|
12
|
+
description: 'Target agent harness (default: universal -> .agents/skills/)',
|
|
13
|
+
options: ['universal', 'pi', 'claude-code', 'cursor', 'codex', 'gemini-cli', 'windsurf', 'continue'],
|
|
14
|
+
}),
|
|
15
|
+
global: Flags.boolean({
|
|
16
|
+
description: 'Install to user scope (~/.<agent>/skills/) instead of project scope',
|
|
17
|
+
default: false,
|
|
18
|
+
}),
|
|
19
|
+
force: Flags.boolean({
|
|
20
|
+
description: 'Overwrite an existing skill file',
|
|
21
|
+
default: false,
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
async run() {
|
|
25
|
+
const { flags } = await this.parse(SkillInstall);
|
|
26
|
+
if (flags.agent && !isKnownAgent(flags.agent)) {
|
|
27
|
+
this.outputErrorAndExit({
|
|
28
|
+
message: `Unknown agent "${flags.agent}".`,
|
|
29
|
+
code: 'UNKNOWN_AGENT',
|
|
30
|
+
hint: 'Use --help to see supported agents.',
|
|
31
|
+
});
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const force = !!flags.force;
|
|
35
|
+
const scope = flags.global ? 'user' : 'project';
|
|
36
|
+
const agent = flags.agent ?? 'universal';
|
|
37
|
+
try {
|
|
38
|
+
const result = await installSkill({ agent, scope, force });
|
|
39
|
+
if (this.isJsonMode) {
|
|
40
|
+
this.outputJson({
|
|
41
|
+
path: result.path,
|
|
42
|
+
agent: result.agent,
|
|
43
|
+
scope: result.scope,
|
|
44
|
+
overwritten: result.overwritten,
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
await this.renderInk(React.createElement(SuccessMessage, {
|
|
49
|
+
message: result.overwritten
|
|
50
|
+
? `Skill overwritten at ${result.path}`
|
|
51
|
+
: `Skill installed at ${result.path}`,
|
|
52
|
+
}));
|
|
53
|
+
this.log(`Agent: ${result.agent}`);
|
|
54
|
+
this.log(`Scope: ${result.scope}`);
|
|
55
|
+
this.log(`Path: ${result.path}`);
|
|
56
|
+
if (!result.overwritten) {
|
|
57
|
+
this.log('');
|
|
58
|
+
this.log('Reload your agent to pick up the new skill.');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const message = err?.message ?? String(err);
|
|
63
|
+
const isExists = /already exists/i.test(message);
|
|
64
|
+
this.outputErrorAndExit({
|
|
65
|
+
message,
|
|
66
|
+
code: isExists ? 'SKILL_EXISTS' : 'SKILL_INSTALL_FAILED',
|
|
67
|
+
hint: isExists ? 'Re-run with --force to overwrite.' : undefined,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Help } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base.js';
|
|
3
|
+
export default class StoryIndex extends BaseCommand {
|
|
4
|
+
static description = 'Manage stories in your project';
|
|
5
|
+
async run() {
|
|
6
|
+
await this.renderHeader('ready', 'compact');
|
|
7
|
+
const help = new Help(this.config);
|
|
8
|
+
await help.showHelp(['story']);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../base.js';
|
|
4
|
+
import { listStories } from '../../generated/api/sdk.gen.js';
|
|
5
|
+
import { zListStoriesResponse } 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 StoryList extends BaseCommand {
|
|
9
|
+
static description = 'List stories for the current project';
|
|
10
|
+
static examples = [
|
|
11
|
+
'<%= config.bin %> story:list',
|
|
12
|
+
'<%= config.bin %> story:list --roadmap next --json',
|
|
13
|
+
'<%= config.bin %> story:list --status IN_DEVELOPMENT',
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
...BaseCommand.baseFlags,
|
|
17
|
+
roadmap: Flags.string({
|
|
18
|
+
description: 'Filter by roadmap status (now|next|later)',
|
|
19
|
+
options: ['now', 'next', 'later'],
|
|
20
|
+
}),
|
|
21
|
+
status: Flags.string({
|
|
22
|
+
description: 'Filter by status (e.g. BACKLOG, IN_DEVELOPMENT, COMPLETE)',
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
async run() {
|
|
26
|
+
const { flags } = await this.parse(StoryList);
|
|
27
|
+
await this.renderHeader('ready', 'compact');
|
|
28
|
+
// 1. Resolve Auth and Config
|
|
29
|
+
const client = await this.getClient();
|
|
30
|
+
const projectId = await this.getProjectIdOrExit();
|
|
31
|
+
// 2. Call SDK
|
|
32
|
+
try {
|
|
33
|
+
const response = await this.withSpinner('Fetching stories...', () => listStories({
|
|
34
|
+
client,
|
|
35
|
+
path: {
|
|
36
|
+
projectId,
|
|
37
|
+
},
|
|
38
|
+
query: {
|
|
39
|
+
...(flags.roadmap ? { roadmap: flags.roadmap } : {}),
|
|
40
|
+
...(flags.status ? { status: flags.status } : {}),
|
|
41
|
+
},
|
|
42
|
+
}));
|
|
43
|
+
const { data, error } = response;
|
|
44
|
+
const validated = validateResponse(data, error, zListStoriesResponse);
|
|
45
|
+
const items = validated.data;
|
|
46
|
+
const metadata = validated.metadata;
|
|
47
|
+
if (this.isJsonMode) {
|
|
48
|
+
this.outputJson(items, { source: 'story list' });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
await this.renderInk(_jsx(DataTable, { data: items, columns: [
|
|
52
|
+
{ key: 'label', header: 'Title', minWidth: 20 },
|
|
53
|
+
{ key: 'id', header: 'ID' },
|
|
54
|
+
{ key: 'status', header: 'Status', color: 'auto' },
|
|
55
|
+
{ key: 'roadmap_status', header: 'Roadmap' },
|
|
56
|
+
{ key: 'parent_id', header: 'Epic ID' },
|
|
57
|
+
], title: `Stories in ${projectId}${flags.roadmap ? ` (${flags.roadmap})` : ''}`, emptyMessage: `No stories found${flags.roadmap ? ` for roadmap: ${flags.roadmap}` : ''}` }));
|
|
58
|
+
if (this.isInteractiveMode) {
|
|
59
|
+
if (items.length > 0) {
|
|
60
|
+
const options = items.map((item) => ({
|
|
61
|
+
label: item.label || item.id,
|
|
62
|
+
value: item.id,
|
|
63
|
+
description: item.status || '',
|
|
64
|
+
}));
|
|
65
|
+
const selectedId = await this.selectOption('Select a story for details:', options, { headerVariant: 'compact' });
|
|
66
|
+
if (selectedId) {
|
|
67
|
+
const { default: chalk } = await import('chalk');
|
|
68
|
+
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge story show ${selectedId}`)}\n`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (metadata?.action_hints) {
|
|
72
|
+
await this.handleActionHints(metadata.action_hints);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
this.handleError('An unexpected error occurred', err);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|