@4ge/cli 0.2.0 → 0.2.2
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 +18 -8
- 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/list.js +1 -1
- 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 +1 -1
- 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/realtime.js +11 -10
- package/dist/core/auth/token-refresh.js +1 -1
- package/dist/core/project/markdown.js +1 -1
- package/dist/core/version-check.js +134 -0
- package/dist/generated/api/zod.gen.js +14 -1
- package/dist/hooks/postrun.js +25 -0
- package/package.json +4 -1
- package/skills/4ge-cli.md +35 -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
|
}
|
|
@@ -3,6 +3,8 @@ import { resolveCredential } from '../../core/auth/resolution.js';
|
|
|
3
3
|
import { createApiClient, ConnectivityError } from '../../core/auth/api-client.js';
|
|
4
4
|
import { ensureFreshToken } from '../../core/auth/token-refresh.js';
|
|
5
5
|
import { cliAuthStatus } from '../../generated/api/sdk.gen.js';
|
|
6
|
+
import { zCliAuthStatusResponse } from '../../generated/api/zod.gen.js';
|
|
7
|
+
import { validateResponse } from '../../core/api/index.js';
|
|
6
8
|
export default class AuthStatus extends BaseCommand {
|
|
7
9
|
static description = 'Check authentication status';
|
|
8
10
|
async run() {
|
|
@@ -12,7 +14,7 @@ export default class AuthStatus extends BaseCommand {
|
|
|
12
14
|
this.outputErrorAndExit({ message: 'Not authenticated', code: 'NO_CREDENTIALS' });
|
|
13
15
|
}
|
|
14
16
|
else {
|
|
15
|
-
this.log('Not authenticated. Run `4ge auth
|
|
17
|
+
this.log('Not authenticated. Run `4ge auth:login --token <api_key>` to authenticate.');
|
|
16
18
|
}
|
|
17
19
|
return;
|
|
18
20
|
}
|
|
@@ -37,7 +39,7 @@ export default class AuthStatus extends BaseCommand {
|
|
|
37
39
|
this.outputErrorAndExit({
|
|
38
40
|
message,
|
|
39
41
|
code,
|
|
40
|
-
hint: refreshError.message || "Run '4ge auth
|
|
42
|
+
hint: refreshError.message || "Run '4ge auth:login' to re-authenticate."
|
|
41
43
|
});
|
|
42
44
|
}
|
|
43
45
|
else {
|
|
@@ -47,7 +49,7 @@ export default class AuthStatus extends BaseCommand {
|
|
|
47
49
|
else {
|
|
48
50
|
this.log('⚠ Session expired — token refresh failed.');
|
|
49
51
|
}
|
|
50
|
-
this.log(` ${refreshError.message || "Run '4ge auth
|
|
52
|
+
this.log(` ${refreshError.message || "Run '4ge auth:login' to re-authenticate."}`);
|
|
51
53
|
}
|
|
52
54
|
return;
|
|
53
55
|
}
|
|
@@ -65,7 +67,7 @@ export default class AuthStatus extends BaseCommand {
|
|
|
65
67
|
this.outputErrorAndExit({
|
|
66
68
|
message: 'API validation failed for 4GE_API_KEY',
|
|
67
69
|
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
|
|
70
|
+
hint: 'The 4GE_API_KEY environment variable is overriding stored credentials but is invalid. Unset it to use credentials from "4ge auth:login".'
|
|
69
71
|
});
|
|
70
72
|
}
|
|
71
73
|
else {
|
|
@@ -77,7 +79,7 @@ export default class AuthStatus extends BaseCommand {
|
|
|
77
79
|
this.log('⚠ Using API key from 4GE_API_KEY environment variable, but server validation failed.');
|
|
78
80
|
this.log('');
|
|
79
81
|
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
|
|
82
|
+
this.log('FIX: Unset 4GE_API_KEY (e.g., `unset 4GE_API_KEY`) to use credentials from `4ge auth:login`.');
|
|
81
83
|
}
|
|
82
84
|
else {
|
|
83
85
|
this.log(`Authenticated via ${activeCredential.source}${activeCredential.profile ? ` (profile: ${activeCredential.profile})` : ''}, but API validation failed.`);
|
|
@@ -85,8 +87,15 @@ export default class AuthStatus extends BaseCommand {
|
|
|
85
87
|
}
|
|
86
88
|
return;
|
|
87
89
|
}
|
|
90
|
+
// Validate the envelope against the generated schema and extract the
|
|
91
|
+
// typed user. Uses the same boundary-validation seam as every other
|
|
92
|
+
// command (the generated zCliAuthStatusResponse now types data.user,
|
|
93
|
+
// data.workspaces, and the authenticated_as enum in lockstep with the
|
|
94
|
+
// app's CLIAuthStatusSchema — see docs/cli-api-contract.pin.md).
|
|
95
|
+
const validated = validateResponse(data, null, zCliAuthStatusResponse);
|
|
96
|
+
const user = validated.data?.user;
|
|
88
97
|
if (this.isJsonMode) {
|
|
89
|
-
this.outputJson({ source: activeCredential.source, profile: activeCredential.profile, user
|
|
98
|
+
this.outputJson({ source: activeCredential.source, profile: activeCredential.profile, user });
|
|
90
99
|
}
|
|
91
100
|
else {
|
|
92
101
|
if (activeCredential.source === 'environment') {
|
|
@@ -96,8 +105,9 @@ export default class AuthStatus extends BaseCommand {
|
|
|
96
105
|
this.log(`Source: ${activeCredential.source}`);
|
|
97
106
|
if (activeCredential.profile)
|
|
98
107
|
this.log(`Profile: ${activeCredential.profile}`);
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
// Display precedence per contract §5.8: full_name → username → email → id.
|
|
109
|
+
if (user)
|
|
110
|
+
this.log(`User: ${user.full_name || user.username || user.email || user.id || 'Unknown'}`);
|
|
101
111
|
}
|
|
102
112
|
}
|
|
103
113
|
catch (error) {
|
|
@@ -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) {
|
|
@@ -69,7 +69,7 @@ export default class FlowList extends BaseCommand {
|
|
|
69
69
|
const selectedId = await this.selectOption('Select a flow for details:', options, { headerVariant: 'compact' });
|
|
70
70
|
if (selectedId) {
|
|
71
71
|
const { default: chalk } = await import('chalk');
|
|
72
|
-
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge flow
|
|
72
|
+
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge flow:show ${selectedId}`)}\n`);
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
if (metadata?.action_hints) {
|
|
@@ -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) {
|
|
@@ -66,7 +66,7 @@ export default class TemplateList extends BaseCommand {
|
|
|
66
66
|
const selectedId = await this.selectOption('Select a template for details:', options, { headerVariant: 'compact' });
|
|
67
67
|
if (selectedId) {
|
|
68
68
|
const { default: chalk } = await import('chalk');
|
|
69
|
-
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge template
|
|
69
|
+
this.log(`\n 🚀 Run: ${chalk.cyan(`4ge template:info --id ${selectedId}`)}\n`);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
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,17 +66,18 @@ export async function subscribeForTokens(opts) {
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
* Get the Supabase URL and anon key
|
|
70
|
-
*
|
|
69
|
+
* Get the Supabase URL and anon key.
|
|
70
|
+
*
|
|
71
|
+
* The anon key is public by design (RLS enforces all security), so the
|
|
72
|
+
* production values are baked in as defaults — a globally-installed CLI has
|
|
73
|
+
* no `.env.local` and must still support the OAuth browser flow with zero
|
|
74
|
+
* user configuration. `SUPABASE_URL` / `SUPABASE_ANON_KEY` env vars override
|
|
75
|
+
* the defaults (used by `4ge/app` for local dev against a local Supabase).
|
|
71
76
|
*/
|
|
72
77
|
export function getSupabaseConfig() {
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
if (!supabaseAnonKey) {
|
|
79
|
-
throw new Error('SUPABASE_ANON_KEY environment variable is required for OAuth login. Set it in your .env file or environment.');
|
|
80
|
-
}
|
|
78
|
+
// Production Supabase project backing app.4ge.dev.
|
|
79
|
+
const supabaseUrl = process.env.SUPABASE_URL || 'https://ujhqusbkvaaczncgzsyo.supabase.co';
|
|
80
|
+
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY ||
|
|
81
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InVqaHF1c2JrdmFhY3puY2d6c3lvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTU4NTQ1OTAsImV4cCI6MjA3MTQzMDU5MH0.A0c0oG-Dj0EvEuH8eNOa8y0o81uCNPkjlOFqoUMx170';
|
|
81
82
|
return { supabaseUrl, supabaseAnonKey };
|
|
82
83
|
}
|
|
@@ -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 {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort npm registry update check for the 4ge CLI.
|
|
3
|
+
*
|
|
4
|
+
* Design constraints:
|
|
5
|
+
* - Never throws. A failed/timeout/missing registry response is a no-op.
|
|
6
|
+
* - Never blocks the user: hits the registry at most once per `cacheTtlMs`
|
|
7
|
+
* (default 24h), persisting the result in a cache file alongside the
|
|
8
|
+
* credentials directory.
|
|
9
|
+
* - Silent in machine/JSON contexts — the caller (the postrun hook) is
|
|
10
|
+
* responsible for suppressing output in `--json` mode.
|
|
11
|
+
*/
|
|
12
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
export const DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
16
|
+
export const DEFAULT_REGISTRY_TIMEOUT_MS = 3_000;
|
|
17
|
+
export const NPM_REGISTRY_URL = 'https://registry.npmjs.org';
|
|
18
|
+
export const PACKAGE_NAME = '@4ge/cli';
|
|
19
|
+
/**
|
|
20
|
+
* Default cache path: `~/.4ge/.update-check.json` (sibling of credentials.json).
|
|
21
|
+
*/
|
|
22
|
+
export function getDefaultCachePath() {
|
|
23
|
+
return join(homedir(), '.4ge', '.update-check.json');
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Compare two semver strings by their numeric major.minor.patch components.
|
|
27
|
+
* Prerelease suffixes are ignored (treated as equal to the release).
|
|
28
|
+
*
|
|
29
|
+
* @returns -1 if `a < b`, 0 if equal, 1 if `a > b`. Non-parseable parts are
|
|
30
|
+
* treated as 0.
|
|
31
|
+
*/
|
|
32
|
+
export function compareVersions(a, b) {
|
|
33
|
+
const pa = parseSemver(a);
|
|
34
|
+
const pb = parseSemver(b);
|
|
35
|
+
for (let i = 0; i < 3; i++) {
|
|
36
|
+
const x = pa[i];
|
|
37
|
+
const y = pb[i];
|
|
38
|
+
if (x < y)
|
|
39
|
+
return -1;
|
|
40
|
+
if (x > y)
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
function parseSemver(v) {
|
|
46
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(v ?? '');
|
|
47
|
+
return [
|
|
48
|
+
m ? Number(m[1]) : 0,
|
|
49
|
+
m ? Number(m[2]) : 0,
|
|
50
|
+
m ? Number(m[3]) : 0,
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Check the npm registry for a newer published version of the package.
|
|
55
|
+
*
|
|
56
|
+
* @returns an {@link UpdateInfo} when an update is available, otherwise `null`.
|
|
57
|
+
* Always resolves (never rejects) — safe to call fire-and-forget.
|
|
58
|
+
*/
|
|
59
|
+
export async function checkForUpdate(opts) {
|
|
60
|
+
const { currentVersion, packageName = PACKAGE_NAME, cachePath = getDefaultCachePath(), cacheTtlMs = DEFAULT_CACHE_TTL_MS, timeoutMs = DEFAULT_REGISTRY_TIMEOUT_MS, registryUrl = NPM_REGISTRY_URL, fetchImpl = globalThis.fetch, } = opts;
|
|
61
|
+
if (!currentVersion)
|
|
62
|
+
return null;
|
|
63
|
+
// 1. Try the cache first.
|
|
64
|
+
let cached = null;
|
|
65
|
+
try {
|
|
66
|
+
const raw = await readFile(cachePath, 'utf-8');
|
|
67
|
+
cached = JSON.parse(raw);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Missing or corrupt cache is fine — fall through to a live fetch.
|
|
71
|
+
cached = null;
|
|
72
|
+
}
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
// "Fresh" means we checked recently, regardless of whether we learned a
|
|
75
|
+
// version. This gives us negative caching: a failed/offline check is retried
|
|
76
|
+
// at most once per TTL window instead of adding a network timeout to every
|
|
77
|
+
// subsequent command.
|
|
78
|
+
const cacheFresh = cached && typeof cached.lastCheck === 'number' &&
|
|
79
|
+
(now - cached.lastCheck) < cacheTtlMs;
|
|
80
|
+
let latest;
|
|
81
|
+
if (cacheFresh) {
|
|
82
|
+
// Either reuse a cached version, or (if the last check found nothing)
|
|
83
|
+
// honour the negative cache and skip a redundant fetch.
|
|
84
|
+
latest = cached.latest;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
latest = await fetchLatestVersion(packageName, registryUrl, timeoutMs, fetchImpl);
|
|
88
|
+
// Persist for next time — including a negative cache entry when fetch
|
|
89
|
+
// returned nothing, so we don't hammer the registry while offline.
|
|
90
|
+
await safeWriteCache(cachePath, { lastCheck: now, latest });
|
|
91
|
+
if (!latest)
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
if (!latest)
|
|
95
|
+
return null;
|
|
96
|
+
const updateAvailable = compareVersions(currentVersion, latest) < 0;
|
|
97
|
+
if (!updateAvailable)
|
|
98
|
+
return null;
|
|
99
|
+
return { current: currentVersion, latest, updateAvailable };
|
|
100
|
+
}
|
|
101
|
+
async function fetchLatestVersion(packageName, registryUrl, timeoutMs, fetchImpl) {
|
|
102
|
+
if (typeof fetchImpl !== 'function')
|
|
103
|
+
return undefined;
|
|
104
|
+
const encoded = encodeURIComponent(packageName);
|
|
105
|
+
const url = `${registryUrl}/${encoded}`;
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
108
|
+
try {
|
|
109
|
+
const res = await fetchImpl(url, {
|
|
110
|
+
signal: controller.signal,
|
|
111
|
+
headers: { accept: 'application/json' },
|
|
112
|
+
});
|
|
113
|
+
if (!res.ok)
|
|
114
|
+
return undefined;
|
|
115
|
+
const body = (await res.json());
|
|
116
|
+
return body?.['dist-tags']?.latest;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Network error, timeout, malformed JSON — all treated as "no info".
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function safeWriteCache(cachePath, data) {
|
|
127
|
+
try {
|
|
128
|
+
await mkdir(dirname(cachePath), { recursive: true });
|
|
129
|
+
await writeFile(cachePath, JSON.stringify(data), 'utf-8');
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Cache write is best-effort; never propagate.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -57,7 +57,20 @@ export const zCliAuthCallbackResponse = z.object({
|
|
|
57
57
|
* Successful response
|
|
58
58
|
*/
|
|
59
59
|
export const zCliAuthStatusResponse = z.object({
|
|
60
|
-
data: z.
|
|
60
|
+
data: z.object({
|
|
61
|
+
user: z.object({
|
|
62
|
+
id: z.uuid(),
|
|
63
|
+
email: z.email().nullable(),
|
|
64
|
+
full_name: z.string().nullish(),
|
|
65
|
+
username: z.string().nullish(),
|
|
66
|
+
}),
|
|
67
|
+
workspaces: z.array(z.object({
|
|
68
|
+
id: z.uuid(),
|
|
69
|
+
name: z.string(),
|
|
70
|
+
role: z.enum(["OWNER", "MEMBER"]),
|
|
71
|
+
})),
|
|
72
|
+
authenticated_as: z.enum(["session", "api_key"]),
|
|
73
|
+
}),
|
|
61
74
|
error: z.unknown(),
|
|
62
75
|
metadata: z.object({
|
|
63
76
|
timestamp: z.string(),
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { checkForUpdate } from '../core/version-check.js';
|
|
2
|
+
const hook = async (options) => {
|
|
3
|
+
const { argv, config } = options;
|
|
4
|
+
// Silence in JSON / machine mode so stdout stays clean.
|
|
5
|
+
if (argv.includes('--json') || argv.includes('-j'))
|
|
6
|
+
return;
|
|
7
|
+
// Unlikely for a published CLI, but don't attempt without a version.
|
|
8
|
+
const currentVersion = config.version;
|
|
9
|
+
if (!currentVersion)
|
|
10
|
+
return;
|
|
11
|
+
try {
|
|
12
|
+
const info = await checkForUpdate({ currentVersion });
|
|
13
|
+
if (info?.updateAvailable) {
|
|
14
|
+
// Write directly to stderr — oclif's context.warn() also emits a stack
|
|
15
|
+
// trace via the logger, which is noisy for an advisory notice.
|
|
16
|
+
const chalk = (await import('chalk')).default;
|
|
17
|
+
process.stderr.write(chalk.yellow(`\nUpdate available: ${info.current} → ${info.latest}\n` +
|
|
18
|
+
` Run \`npm i -g @4ge/cli\` to update.\n`));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// Never let an update check interfere with the user's command.
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
export default hook;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4ge/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "4ge Service Architecture Bridge & CLI",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
"plugins": [
|
|
27
27
|
"@oclif/plugin-help"
|
|
28
28
|
],
|
|
29
|
+
"hooks": {
|
|
30
|
+
"postrun": "./dist/hooks/postrun.js"
|
|
31
|
+
},
|
|
29
32
|
"flexibleTaxonomy": true
|
|
30
33
|
},
|
|
31
34
|
"type": "module",
|
package/skills/4ge-cli.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 4ge-cli
|
|
3
|
+
description: >
|
|
4
|
+
Instructions for AI agents to interact with the 4ge Product Management
|
|
5
|
+
Platform via the `4ge` CLI. Covers command syntax (namespace:command with a
|
|
6
|
+
colon), mandatory `--json` agent mode, the response envelope, error handling,
|
|
7
|
+
discovery, dry-run previews, retry, and the full command reference for the
|
|
8
|
+
project / epic / story / feature / flow / idea / template / plan / auth /
|
|
9
|
+
config / skill namespaces. Use when an agent needs to read or mutate 4ge plan
|
|
10
|
+
assets, work with pull/list/show/update commands, or install the 4ge CLI skill
|
|
11
|
+
for an agent harness.
|
|
12
|
+
user-invocable: true
|
|
13
|
+
risk: safe
|
|
14
|
+
source: https://github.com/Conflate-AI/4ge-cli/blob/main/skills/4ge-cli.md
|
|
15
|
+
---
|
|
16
|
+
|
|
1
17
|
# 4ge CLI Skill
|
|
2
18
|
|
|
3
19
|
This document provides instructions for AI agents to interact with the 4ge
|
|
@@ -13,15 +29,30 @@ Before using this CLI, the user **MUST** have completed:
|
|
|
13
29
|
The CLI is already installed and configured in this project.
|
|
14
30
|
|
|
15
31
|
## ⌨️ Command Syntax
|
|
16
|
-
|
|
32
|
+
|
|
33
|
+
**Convention (hard rule):** commands are `4ge <namespace>:<command> [--params]` —
|
|
34
|
+
a colon between namespace and command, **never a space**.
|
|
35
|
+
|
|
36
|
+
| Correct | Wrong |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `4ge epic:list` | ~~`4ge epic list`~~ |
|
|
39
|
+
| `4ge project:pull --json` | ~~`4ge project pull --json`~~ |
|
|
40
|
+
| `4ge flow:show --id <id>` | ~~`4ge flow show --id <id>`~~ |
|
|
41
|
+
| `4ge auth:status` | ~~`4ge auth status`~~ |
|
|
42
|
+
|
|
43
|
+
Nested commands chain with more colons: `4ge template:feature:list`.
|
|
44
|
+
A few root commands take no colon (`4ge init`, `4ge whoami`, `4ge login`) —
|
|
45
|
+
single words with no nested command.
|
|
46
|
+
|
|
47
|
+
Full form:
|
|
17
48
|
|
|
18
49
|
```bash
|
|
19
50
|
4ge <namespace>:<command> [options] --json
|
|
20
51
|
```
|
|
21
52
|
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
|
|
53
|
+
- **`--json` is mandatory** in agent mode for machine-readable output.
|
|
54
|
+
- **Positional arguments**: a few commands take a direct arg without a flag
|
|
55
|
+
(e.g. `4ge idea:show <ID>`).
|
|
25
56
|
|
|
26
57
|
## 🛠 Usage Guidelines
|
|
27
58
|
|