@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,197 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base.js';
|
|
3
|
+
import { generateState } from '../../core/auth/state.js';
|
|
4
|
+
import { getSupabaseConfig, subscribeForTokens } from '../../core/auth/realtime.js';
|
|
5
|
+
import { authenticateWithEmail } from '../../core/auth/email-auth.js';
|
|
6
|
+
import { saveCredential } from '../../core/auth/credentials.js';
|
|
7
|
+
import { createApiClient } from '../../core/auth/api-client.js';
|
|
8
|
+
import { getApiBaseUrl } from '../../core/auth/config.js';
|
|
9
|
+
import { cliAuthStatus } from '../../generated/api/sdk.gen.js';
|
|
10
|
+
export default class AuthLogin extends BaseCommand {
|
|
11
|
+
static description = 'Authenticate the CLI with your 4ge account';
|
|
12
|
+
static flags = {
|
|
13
|
+
...BaseCommand.baseFlags,
|
|
14
|
+
token: Flags.string({
|
|
15
|
+
char: 't',
|
|
16
|
+
description: 'Manually provide an API key or access token',
|
|
17
|
+
}),
|
|
18
|
+
email: Flags.boolean({
|
|
19
|
+
description: 'Force email/password authentication flow',
|
|
20
|
+
default: false,
|
|
21
|
+
}),
|
|
22
|
+
local: Flags.boolean({
|
|
23
|
+
char: 'l',
|
|
24
|
+
description: 'Save credentials to local .4ge/credentials.json instead of global',
|
|
25
|
+
default: false,
|
|
26
|
+
}),
|
|
27
|
+
profile: Flags.string({
|
|
28
|
+
char: 'p',
|
|
29
|
+
description: 'Name of the profile to save credentials to',
|
|
30
|
+
default: 'default',
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
async run() {
|
|
34
|
+
const { flags } = await this.parse(AuthLogin);
|
|
35
|
+
await this.renderHeader('ready');
|
|
36
|
+
// 1. Manual Token Flow
|
|
37
|
+
if (flags.token) {
|
|
38
|
+
await this.withSpinner('Validating and saving token...', async () => {
|
|
39
|
+
await this.validateAndSave({
|
|
40
|
+
apiKey: flags.token,
|
|
41
|
+
local: flags.local,
|
|
42
|
+
profile: flags.profile,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
if (this.isJsonMode) {
|
|
46
|
+
this.outputJson({ message: 'Successfully authenticated with manual token' });
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
this.log('Successfully authenticated with manual token.');
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// 2. Interactive Flow Selection (Skip if --email flag is present)
|
|
54
|
+
let method = flags.email ? 'email' : null;
|
|
55
|
+
if (!method) {
|
|
56
|
+
if (this.isJsonMode) {
|
|
57
|
+
// In JSON mode without --token or --email, route to browser flow (which will output JSON)
|
|
58
|
+
await this.executeBrowserFlow(flags.local, flags.profile);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
method = await this.selectOption('How would you like to authenticate?', [
|
|
62
|
+
{ label: 'Browser (Recommended)', value: 'browser', description: 'Open your browser to log in' },
|
|
63
|
+
{ label: 'Email/Password', value: 'email', description: 'Enter your credentials in the terminal' },
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
if (!method)
|
|
67
|
+
return;
|
|
68
|
+
if (method === 'browser') {
|
|
69
|
+
await this.executeBrowserFlow(flags.local, flags.profile);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
await this.executeEmailFlow(flags.local, flags.profile);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async validateAndSave(opts) {
|
|
76
|
+
// Validate with cliAuthStatus before saving
|
|
77
|
+
const client = createApiClient({
|
|
78
|
+
baseUrl: getApiBaseUrl(),
|
|
79
|
+
credential: {
|
|
80
|
+
api_key: (opts.apiKey || opts.accessToken),
|
|
81
|
+
source: opts.local ? 'local' : 'global',
|
|
82
|
+
profile: opts.profile
|
|
83
|
+
},
|
|
84
|
+
isJsonMode: this.isJsonMode
|
|
85
|
+
});
|
|
86
|
+
const { error } = await cliAuthStatus({ client });
|
|
87
|
+
if (error) {
|
|
88
|
+
const detail = error.message || error.error?.message || 'API validation failed';
|
|
89
|
+
throw new Error(`Invalid credentials: ${detail}`);
|
|
90
|
+
}
|
|
91
|
+
await saveCredential(opts);
|
|
92
|
+
}
|
|
93
|
+
async executeBrowserFlow(local, profile) {
|
|
94
|
+
const state = generateState();
|
|
95
|
+
const { supabaseUrl, supabaseAnonKey } = getSupabaseConfig();
|
|
96
|
+
// Construct the login URL
|
|
97
|
+
const baseUrl = getApiBaseUrl();
|
|
98
|
+
const loginUrl = `${baseUrl}/api/cli/v1/auth/login?state=${state}`;
|
|
99
|
+
if (this.isJsonMode) {
|
|
100
|
+
this.outputJson({
|
|
101
|
+
url: loginUrl,
|
|
102
|
+
state,
|
|
103
|
+
message: 'Open the URL in your browser to complete authentication. The CLI will not wait for completion in JSON mode unless specifically requested (not implemented).'
|
|
104
|
+
});
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.log('\nOpening your browser to authenticate...');
|
|
108
|
+
this.log(`URL: ${loginUrl}\n`);
|
|
109
|
+
const open = (await import('open')).default;
|
|
110
|
+
await open(loginUrl);
|
|
111
|
+
let result;
|
|
112
|
+
try {
|
|
113
|
+
result = await this.withSpinner('Waiting for authentication to complete...', () => subscribeForTokens({
|
|
114
|
+
supabaseUrl,
|
|
115
|
+
supabaseAnonKey,
|
|
116
|
+
state,
|
|
117
|
+
}));
|
|
118
|
+
const tokens = result.tokens;
|
|
119
|
+
await this.validateAndSave({
|
|
120
|
+
accessToken: tokens.access_token,
|
|
121
|
+
refreshToken: tokens.refresh_token,
|
|
122
|
+
expiresAt: tokens.expires_at,
|
|
123
|
+
user: tokens.user,
|
|
124
|
+
local,
|
|
125
|
+
profile,
|
|
126
|
+
});
|
|
127
|
+
if (this.isJsonMode) {
|
|
128
|
+
this.outputJson({
|
|
129
|
+
message: 'Successfully authenticated via browser',
|
|
130
|
+
user: tokens.user
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
this.log('\nSuccessfully authenticated via browser!');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (this.isJsonMode) {
|
|
139
|
+
this.outputErrorAndExit(error);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
this.error(`Authentication failed: ${error.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
result?.cleanup();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async executeEmailFlow(local, profile) {
|
|
150
|
+
if (this.isJsonMode) {
|
|
151
|
+
this.outputErrorAndExit({
|
|
152
|
+
message: 'Email authentication is not supported in JSON mode. Please use --token for headless authentication or run in non-JSON mode.',
|
|
153
|
+
code: 'INTERACTIVE_REQUIRED'
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const email = await this.textInput('Email');
|
|
157
|
+
if (!email)
|
|
158
|
+
return;
|
|
159
|
+
const password = await this.textInput('Password', { mask: true });
|
|
160
|
+
if (!password)
|
|
161
|
+
return;
|
|
162
|
+
try {
|
|
163
|
+
const result = await this.withSpinner('Authenticating...', async () => {
|
|
164
|
+
return authenticateWithEmail({
|
|
165
|
+
email,
|
|
166
|
+
password,
|
|
167
|
+
baseUrl: getApiBaseUrl(),
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
await this.validateAndSave({
|
|
171
|
+
accessToken: result.access_token,
|
|
172
|
+
refreshToken: result.refresh_token,
|
|
173
|
+
expiresAt: result.expires_at,
|
|
174
|
+
user: result.user,
|
|
175
|
+
local,
|
|
176
|
+
profile,
|
|
177
|
+
});
|
|
178
|
+
if (this.isJsonMode) {
|
|
179
|
+
this.outputJson({
|
|
180
|
+
message: 'Successfully authenticated with email',
|
|
181
|
+
user: result.user
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
this.log('\nSuccessfully authenticated with email!');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (this.isJsonMode) {
|
|
190
|
+
this.outputErrorAndExit(error);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
this.error(`Authentication failed: ${error.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base.js';
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { clearCredential, readCredentialsFile, getGlobalCredentialsPath, getLocalCredentialsPath } from '../../core/auth/credentials.js';
|
|
4
|
+
export default class AuthLogout extends BaseCommand {
|
|
5
|
+
static description = 'Clear authentication credentials';
|
|
6
|
+
static flags = {
|
|
7
|
+
...BaseCommand.baseFlags,
|
|
8
|
+
profile: Flags.string({ description: 'Named profile to clear' }),
|
|
9
|
+
local: Flags.boolean({ description: 'Clear credentials from local project' }),
|
|
10
|
+
};
|
|
11
|
+
async run() {
|
|
12
|
+
const { flags } = await this.parse(AuthLogout);
|
|
13
|
+
await this.renderHeader('ready', 'compact');
|
|
14
|
+
// Determine which profile will be cleared for the output
|
|
15
|
+
const filePath = flags.local ? getLocalCredentialsPath() : getGlobalCredentialsPath();
|
|
16
|
+
const credentials = await readCredentialsFile(filePath);
|
|
17
|
+
const profileToClear = flags.profile ?? credentials?.active_profile ?? 'default';
|
|
18
|
+
try {
|
|
19
|
+
await clearCredential({ profile: flags.profile, local: flags.local });
|
|
20
|
+
if (this.isJsonMode) {
|
|
21
|
+
this.outputJson({ success: true, profile: profileToClear, location: flags.local ? 'local' : 'global' });
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
this.log(`✓ Credentials cleared from ${flags.local ? 'local' : 'global'} profile '${profileToClear}'`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
this.isJsonMode ? this.outputErrorAndExit(error.message) : this.error(error.message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { BaseCommand } from '../../../base.js';
|
|
2
|
+
import { Args } from '@oclif/core';
|
|
3
|
+
import { deleteProfile } from '../../../core/auth/credentials.js';
|
|
4
|
+
export default class AuthProfileDelete extends BaseCommand {
|
|
5
|
+
static description = 'Delete an authentication profile';
|
|
6
|
+
static args = {
|
|
7
|
+
name: Args.string({ description: 'Profile name to delete', required: true }),
|
|
8
|
+
};
|
|
9
|
+
async run() {
|
|
10
|
+
const { args } = await this.parse(AuthProfileDelete);
|
|
11
|
+
await this.renderHeader('ready', 'compact');
|
|
12
|
+
if (this.isDryRun) {
|
|
13
|
+
await this.handleDryRun({ action: 'delete_profile', name: args.name });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
await deleteProfile(args.name);
|
|
18
|
+
if (this.isJsonMode) {
|
|
19
|
+
this.outputJson({ deleted_profile: args.name });
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
this.log(`✓ Profile '${args.name}' deleted`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (this.isJsonMode) {
|
|
27
|
+
this.outputErrorAndExit(error.message);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
this.error(error.message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BaseCommand } from '../../../base.js';
|
|
2
|
+
import { listProfiles } from '../../../core/auth/credentials.js';
|
|
3
|
+
import { ux } from '@oclif/core';
|
|
4
|
+
export default class AuthProfileList extends BaseCommand {
|
|
5
|
+
static description = 'List authentication profiles';
|
|
6
|
+
async run() {
|
|
7
|
+
await this.renderHeader('ready', 'compact');
|
|
8
|
+
try {
|
|
9
|
+
const data = await listProfiles();
|
|
10
|
+
if (this.isJsonMode) {
|
|
11
|
+
this.outputJson(data);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
if (data.profiles.length === 0) {
|
|
15
|
+
this.log('No profiles found.');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
ux.stdout(data.profiles
|
|
19
|
+
.map((p) => `${p.is_active ? '*' : ' '} ${p.name}`)
|
|
20
|
+
.join('\n'));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (this.isJsonMode) {
|
|
25
|
+
this.outputErrorAndExit(error.message);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
this.error(error.message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { BaseCommand } from '../../../base.js';
|
|
2
|
+
import { Args } from '@oclif/core';
|
|
3
|
+
import { setActiveProfile } from '../../../core/auth/credentials.js';
|
|
4
|
+
export default class AuthProfileUse extends BaseCommand {
|
|
5
|
+
static description = 'Set the active authentication profile';
|
|
6
|
+
static args = {
|
|
7
|
+
name: Args.string({ description: 'Profile name to activate', required: true }),
|
|
8
|
+
};
|
|
9
|
+
async run() {
|
|
10
|
+
const { args } = await this.parse(AuthProfileUse);
|
|
11
|
+
await this.renderHeader('ready', 'compact');
|
|
12
|
+
try {
|
|
13
|
+
await setActiveProfile(args.name);
|
|
14
|
+
if (this.isJsonMode) {
|
|
15
|
+
this.outputJson({ active_profile: args.name });
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
this.log(`✓ Active profile set to '${args.name}'`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (this.isJsonMode) {
|
|
23
|
+
this.outputErrorAndExit(error.message);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
this.error(error.message);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base.js';
|
|
2
|
+
import { resolveCredential } from '../../core/auth/resolution.js';
|
|
3
|
+
import { createApiClient, ConnectivityError } from '../../core/auth/api-client.js';
|
|
4
|
+
import { ensureFreshToken } from '../../core/auth/token-refresh.js';
|
|
5
|
+
import { cliAuthStatus } from '../../generated/api/sdk.gen.js';
|
|
6
|
+
export default class AuthStatus extends BaseCommand {
|
|
7
|
+
static description = 'Check authentication status';
|
|
8
|
+
async run() {
|
|
9
|
+
const credential = await resolveCredential();
|
|
10
|
+
if (!credential) {
|
|
11
|
+
if (this.isJsonMode) {
|
|
12
|
+
this.outputErrorAndExit({ message: 'Not authenticated', code: 'NO_CREDENTIALS' });
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
this.log('Not authenticated. Run `4ge auth login --token <api_key>` to authenticate.');
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// Proactively refresh the token if it's expired/expiring.
|
|
20
|
+
// We do this BEFORE creating the API client so that we send a fresh token.
|
|
21
|
+
// We still use resolveCredential() + createApiClient() directly (instead of getClient())
|
|
22
|
+
// because auth:status wants to show raw credential state for diagnostics.
|
|
23
|
+
let freshCredential = null;
|
|
24
|
+
try {
|
|
25
|
+
freshCredential = await ensureFreshToken();
|
|
26
|
+
}
|
|
27
|
+
catch (refreshError) {
|
|
28
|
+
// Distinguish connectivity failures (server unreachable) from genuine
|
|
29
|
+
// session expiry (refresh token rejected). A dead server is NOT a
|
|
30
|
+
// session-expiry and must not be reported as one.
|
|
31
|
+
const isConnectivity = refreshError instanceof ConnectivityError;
|
|
32
|
+
const code = isConnectivity ? 'CONNECTION_ERROR' : 'SESSION_EXPIRED';
|
|
33
|
+
const message = isConnectivity
|
|
34
|
+
? 'Cannot reach the 4ge API server to verify authentication.'
|
|
35
|
+
: 'Session expired. Token refresh failed.';
|
|
36
|
+
if (this.isJsonMode) {
|
|
37
|
+
this.outputErrorAndExit({
|
|
38
|
+
message,
|
|
39
|
+
code,
|
|
40
|
+
hint: refreshError.message || "Run '4ge auth login' to re-authenticate."
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
if (isConnectivity) {
|
|
45
|
+
this.log('⚠ Cannot reach the 4ge API server — token could not be refreshed.');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
this.log('⚠ Session expired — token refresh failed.');
|
|
49
|
+
}
|
|
50
|
+
this.log(` ${refreshError.message || "Run '4ge auth login' to re-authenticate."}`);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Use the fresh credential if available, otherwise fall back to the originally resolved one
|
|
55
|
+
const activeCredential = freshCredential || credential;
|
|
56
|
+
try {
|
|
57
|
+
if (!this.isJsonMode) {
|
|
58
|
+
await this.renderHeader('ready', 'compact');
|
|
59
|
+
}
|
|
60
|
+
const client = createApiClient({ credential: activeCredential, isJsonMode: this.isJsonMode });
|
|
61
|
+
const { data, error } = await cliAuthStatus({ client });
|
|
62
|
+
if (error) {
|
|
63
|
+
if (this.isJsonMode) {
|
|
64
|
+
if (activeCredential.source === 'environment') {
|
|
65
|
+
this.outputErrorAndExit({
|
|
66
|
+
message: 'API validation failed for 4GE_API_KEY',
|
|
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 login".'
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
this.outputErrorAndExit({ message: 'API validation failed', code: 'VALIDATION_FAILED' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
if (activeCredential.source === 'environment') {
|
|
77
|
+
this.log('⚠ Using API key from 4GE_API_KEY environment variable, but server validation failed.');
|
|
78
|
+
this.log('');
|
|
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 login`.');
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
this.log(`Authenticated via ${activeCredential.source}${activeCredential.profile ? ` (profile: ${activeCredential.profile})` : ''}, but API validation failed.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (this.isJsonMode) {
|
|
89
|
+
this.outputJson({ source: activeCredential.source, profile: activeCredential.profile, user: data });
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
if (activeCredential.source === 'environment') {
|
|
93
|
+
this.log('ℹ Authenticated via 4GE_API_KEY environment variable (overrides stored credentials).');
|
|
94
|
+
}
|
|
95
|
+
this.log('Authenticated');
|
|
96
|
+
this.log(`Source: ${activeCredential.source}`);
|
|
97
|
+
if (activeCredential.profile)
|
|
98
|
+
this.log(`Profile: ${activeCredential.profile}`);
|
|
99
|
+
if (data)
|
|
100
|
+
this.log(`User: ${data.email || data.id || 'Unknown'}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
this.isJsonMode ? this.outputErrorAndExit(error.message) : this.error(error.message);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Args } from '@oclif/core';
|
|
2
|
+
import { BaseCommand } from '../../base.js';
|
|
3
|
+
import { readConfig } from '../../core/config/parser.js';
|
|
4
|
+
export default class ConfigGet extends BaseCommand {
|
|
5
|
+
static description = 'Get a configuration value';
|
|
6
|
+
static flags = {
|
|
7
|
+
...BaseCommand.baseFlags,
|
|
8
|
+
};
|
|
9
|
+
static args = {
|
|
10
|
+
key: Args.string({ description: 'Config key (dot notation, e.g., cli.default_mode)', required: true }),
|
|
11
|
+
};
|
|
12
|
+
async run() {
|
|
13
|
+
const { args } = await this.parse(ConfigGet);
|
|
14
|
+
const config = await readConfig();
|
|
15
|
+
if (!config) {
|
|
16
|
+
this.handleError('No project config found. Run `4ge init` first.', { code: 'NOT_INITIALIZED' });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
await this.renderHeader('ready', 'compact');
|
|
20
|
+
const value = this.getDotNotationValue(config, args.key);
|
|
21
|
+
if (this.isJsonMode) {
|
|
22
|
+
this.outputJson({ key: args.key, value: value ?? null });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (value === undefined || value === null) {
|
|
26
|
+
this.log(`Key '${args.key}' not found in configuration.`);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
this.log(String(value));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
getDotNotationValue(obj, path) {
|
|
33
|
+
return path.split('.').reduce((prev, curr) => {
|
|
34
|
+
return prev ? prev[curr] : undefined;
|
|
35
|
+
}, obj);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class ConfigIndex extends Command {
|
|
3
|
+
static description = 'Manage CLI configuration';
|
|
4
|
+
static hidden = true;
|
|
5
|
+
async run() {
|
|
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
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { BaseCommand } from '../../base.js';
|
|
3
|
+
import { readConfig, writeConfig } from '../../core/config/parser.js';
|
|
4
|
+
import { ProjectConfigSchema } from '../../core/config/schema.js';
|
|
5
|
+
import { SuccessMessage } from '../../ui/components/success-message.js';
|
|
6
|
+
const FIELDS = [
|
|
7
|
+
// Project Section
|
|
8
|
+
{
|
|
9
|
+
key: 'project_id',
|
|
10
|
+
label: 'Project ID',
|
|
11
|
+
type: 'string',
|
|
12
|
+
getValue: (c) => c.project_id,
|
|
13
|
+
setValue: (c, v) => ({ ...c, project_id: v }),
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
key: 'workspace_id',
|
|
17
|
+
label: 'Workspace ID',
|
|
18
|
+
type: 'string',
|
|
19
|
+
getValue: (c) => c.workspace_id,
|
|
20
|
+
setValue: (c, v) => ({ ...c, workspace_id: v }),
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: 'project_name',
|
|
24
|
+
label: 'Project Name',
|
|
25
|
+
type: 'string',
|
|
26
|
+
getValue: (c) => c.project_name,
|
|
27
|
+
setValue: (c, v) => ({ ...c, project_name: v }),
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: 'workspace_name',
|
|
31
|
+
label: 'Workspace Name',
|
|
32
|
+
type: 'string',
|
|
33
|
+
getValue: (c) => c.workspace_name,
|
|
34
|
+
setValue: (c, v) => ({ ...c, workspace_name: v }),
|
|
35
|
+
},
|
|
36
|
+
// Hierarchy Section
|
|
37
|
+
{
|
|
38
|
+
key: 'hierarchy.default_roadmap_filter',
|
|
39
|
+
label: 'Default Roadmap Filter',
|
|
40
|
+
type: 'enum',
|
|
41
|
+
options: ['now', 'next', 'later', 'all'],
|
|
42
|
+
getValue: (c) => c.hierarchy.default_roadmap_filter,
|
|
43
|
+
setValue: (c, v) => ({ ...c, hierarchy: { ...c.hierarchy, default_roadmap_filter: v } }),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: 'hierarchy.recursive_pull',
|
|
47
|
+
label: 'Recursive Pull',
|
|
48
|
+
type: 'boolean',
|
|
49
|
+
getValue: (c) => c.hierarchy.recursive_pull,
|
|
50
|
+
setValue: (c, v) => ({ ...c, hierarchy: { ...c.hierarchy, recursive_pull: v } }),
|
|
51
|
+
},
|
|
52
|
+
// Pull Section
|
|
53
|
+
{
|
|
54
|
+
key: 'pull.generate_stubs',
|
|
55
|
+
label: 'Generate Stubs',
|
|
56
|
+
type: 'boolean',
|
|
57
|
+
getValue: (c) => c.pull.generate_stubs,
|
|
58
|
+
setValue: (c, v) => ({ ...c, pull: { ...c.pull, generate_stubs: v } }),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
key: 'pull.include_brief_deps',
|
|
62
|
+
label: 'Include Brief Deps',
|
|
63
|
+
type: 'boolean',
|
|
64
|
+
getValue: (c) => c.pull.include_brief_deps,
|
|
65
|
+
setValue: (c, v) => ({ ...c, pull: { ...c.pull, include_brief_deps: v } }),
|
|
66
|
+
},
|
|
67
|
+
// CLI Section
|
|
68
|
+
{
|
|
69
|
+
key: 'cli.default_mode',
|
|
70
|
+
label: 'Default Mode',
|
|
71
|
+
type: 'enum',
|
|
72
|
+
options: ['pretty', 'interactive'],
|
|
73
|
+
getValue: (c) => c.cli.default_mode,
|
|
74
|
+
setValue: (c, v) => ({ ...c, cli: { ...c.cli, default_mode: v } }),
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
const SECTIONS = [
|
|
78
|
+
{ label: 'Project Identity', value: 'project', keys: ['project_id', 'workspace_id', 'project_name', 'workspace_name'] },
|
|
79
|
+
{ label: 'Hierarchy', value: 'hierarchy', keys: ['hierarchy.default_roadmap_filter', 'hierarchy.recursive_pull'] },
|
|
80
|
+
{ label: 'Pull', value: 'pull', keys: ['pull.generate_stubs', 'pull.include_brief_deps'] },
|
|
81
|
+
{ label: 'CLI', value: 'cli', keys: ['cli.default_mode'] },
|
|
82
|
+
];
|
|
83
|
+
export default class ConfigManage extends BaseCommand {
|
|
84
|
+
static description = 'Interactive configuration management TUI';
|
|
85
|
+
static flags = {
|
|
86
|
+
...BaseCommand.baseFlags,
|
|
87
|
+
};
|
|
88
|
+
async run() {
|
|
89
|
+
const config = await readConfig();
|
|
90
|
+
if (!config) {
|
|
91
|
+
this.error('No configuration found. Run `4ge init` first.');
|
|
92
|
+
}
|
|
93
|
+
await this.renderHeader('ready');
|
|
94
|
+
if (this.isJsonMode) {
|
|
95
|
+
this.outputJson(config);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
while (true) {
|
|
99
|
+
const sectionValue = await this.selectOption('Select a section to edit:', [
|
|
100
|
+
...SECTIONS,
|
|
101
|
+
{ label: 'Done ← Exit', value: 'done' },
|
|
102
|
+
], {
|
|
103
|
+
helpText: 'Esc: exit • Ctrl+D: exit'
|
|
104
|
+
});
|
|
105
|
+
if (sectionValue === 'done')
|
|
106
|
+
break;
|
|
107
|
+
const section = SECTIONS.find(s => s.value === sectionValue);
|
|
108
|
+
const sectionFields = FIELDS.filter(f => section.keys.includes(f.key));
|
|
109
|
+
while (true) {
|
|
110
|
+
const currentConfig = await readConfig();
|
|
111
|
+
if (!currentConfig)
|
|
112
|
+
break;
|
|
113
|
+
const fieldKey = await this.selectOption(`Edit ${section.label} settings:`, [
|
|
114
|
+
...sectionFields.map(f => ({
|
|
115
|
+
label: `${f.label}: ${f.getValue(currentConfig)}`,
|
|
116
|
+
value: f.key,
|
|
117
|
+
})),
|
|
118
|
+
{ label: '← Back', value: 'back' },
|
|
119
|
+
], {
|
|
120
|
+
helpText: 'Esc: back • Ctrl+D: exit'
|
|
121
|
+
});
|
|
122
|
+
if (fieldKey === 'back')
|
|
123
|
+
break;
|
|
124
|
+
const field = sectionFields.find(f => f.key === fieldKey);
|
|
125
|
+
const newValue = await this.promptForValue(field, field.getValue(currentConfig));
|
|
126
|
+
if (newValue === undefined || newValue === null)
|
|
127
|
+
continue;
|
|
128
|
+
try {
|
|
129
|
+
const updatedConfig = field.setValue(currentConfig, newValue);
|
|
130
|
+
const validated = ProjectConfigSchema.parse(updatedConfig);
|
|
131
|
+
await writeConfig(validated);
|
|
132
|
+
await this.renderInk(_jsx(SuccessMessage, { message: `Updated ${field.label}` }));
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
this.warn(`Failed to update ${field.label}: ${error.message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
await this.renderInk(_jsx(SuccessMessage, { message: "Configuration saved" }));
|
|
140
|
+
}
|
|
141
|
+
async promptForValue(field, currentValue) {
|
|
142
|
+
if (field.type === 'enum') {
|
|
143
|
+
const choice = await this.selectOption(`Edit ${field.label} (current: ${currentValue})`, [
|
|
144
|
+
...field.options.map(o => ({ label: o, value: o })),
|
|
145
|
+
{ label: 'Cancel', value: 'cancel' },
|
|
146
|
+
], {
|
|
147
|
+
helpText: 'Esc: cancel • Ctrl+D: exit'
|
|
148
|
+
});
|
|
149
|
+
return choice === 'cancel' ? undefined : choice;
|
|
150
|
+
}
|
|
151
|
+
if (field.type === 'boolean') {
|
|
152
|
+
const choice = await this.selectOption(`Edit ${field.label} (current: ${currentValue})`, [
|
|
153
|
+
{ label: 'true', value: 'true' },
|
|
154
|
+
{ label: 'false', value: 'false' },
|
|
155
|
+
{ label: 'Cancel', value: 'cancel' },
|
|
156
|
+
], {
|
|
157
|
+
helpText: 'Esc: cancel • Ctrl+D: exit'
|
|
158
|
+
});
|
|
159
|
+
return choice === 'cancel' ? undefined : choice === 'true';
|
|
160
|
+
}
|
|
161
|
+
if (field.type === 'string') {
|
|
162
|
+
return this.textInput(field.label, { defaultValue: currentValue || '' });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|