@4ge/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +228 -0
  3. package/bin/dev.js +3 -0
  4. package/bin/run.js +3 -0
  5. package/dist/base.js +398 -0
  6. package/dist/commands/auth/login.js +197 -0
  7. package/dist/commands/auth/logout.js +31 -0
  8. package/dist/commands/auth/profile/delete.js +34 -0
  9. package/dist/commands/auth/profile/list.js +32 -0
  10. package/dist/commands/auth/profile/use.js +30 -0
  11. package/dist/commands/auth/status.js +107 -0
  12. package/dist/commands/config/get.js +37 -0
  13. package/dist/commands/config/index.js +9 -0
  14. package/dist/commands/config/manage.js +165 -0
  15. package/dist/commands/config/set.js +42 -0
  16. package/dist/commands/config/show.js +46 -0
  17. package/dist/commands/epic/index.js +10 -0
  18. package/dist/commands/epic/list.js +91 -0
  19. package/dist/commands/epic/pull.js +137 -0
  20. package/dist/commands/epic/show.js +67 -0
  21. package/dist/commands/epic/update.js +104 -0
  22. package/dist/commands/feature/index.js +2 -0
  23. package/dist/commands/feature/list.js +93 -0
  24. package/dist/commands/feature/pull.js +122 -0
  25. package/dist/commands/feature/show.js +60 -0
  26. package/dist/commands/feature/update.js +103 -0
  27. package/dist/commands/idea/create.js +88 -0
  28. package/dist/commands/idea/graduate.js +89 -0
  29. package/dist/commands/idea/index.js +5 -0
  30. package/dist/commands/idea/list.js +83 -0
  31. package/dist/commands/idea/show.js +86 -0
  32. package/dist/commands/idea/update.js +108 -0
  33. package/dist/commands/init.js +354 -0
  34. package/dist/commands/login.js +5 -0
  35. package/dist/commands/plan/index.js +10 -0
  36. package/dist/commands/plan/pull.js +104 -0
  37. package/dist/commands/plan/versions.js +64 -0
  38. package/dist/commands/project/create.js +84 -0
  39. package/dist/commands/project/index.js +9 -0
  40. package/dist/commands/project/info.js +74 -0
  41. package/dist/commands/project/list.js +78 -0
  42. package/dist/commands/project/pull.js +127 -0
  43. package/dist/commands/project/update.js +104 -0
  44. package/dist/commands/root.js +13 -0
  45. package/dist/commands/skill/index.js +8 -0
  46. package/dist/commands/skill/install.js +71 -0
  47. package/dist/commands/story/index.js +10 -0
  48. package/dist/commands/story/list.js +81 -0
  49. package/dist/commands/story/pull.js +125 -0
  50. package/dist/commands/story/show.js +57 -0
  51. package/dist/commands/story/update.js +103 -0
  52. package/dist/commands/template/create.js +72 -0
  53. package/dist/commands/template/delete.js +62 -0
  54. package/dist/commands/template/feature/create.js +70 -0
  55. package/dist/commands/template/feature/delete.js +75 -0
  56. package/dist/commands/template/feature/list.js +69 -0
  57. package/dist/commands/template/feature/update.js +82 -0
  58. package/dist/commands/template/info.js +52 -0
  59. package/dist/commands/template/list.js +76 -0
  60. package/dist/commands/template/pull.js +99 -0
  61. package/dist/commands/template/update.js +71 -0
  62. package/dist/commands/whoami.js +5 -0
  63. package/dist/core/api/action-hints.js +17 -0
  64. package/dist/core/api/index.js +3 -0
  65. package/dist/core/api/retry.js +95 -0
  66. package/dist/core/api/validate-response.js +41 -0
  67. package/dist/core/auth/api-client.js +156 -0
  68. package/dist/core/auth/config.js +3 -0
  69. package/dist/core/auth/credentials.js +143 -0
  70. package/dist/core/auth/email-auth.js +30 -0
  71. package/dist/core/auth/errors.js +20 -0
  72. package/dist/core/auth/index.js +6 -0
  73. package/dist/core/auth/realtime.js +82 -0
  74. package/dist/core/auth/resolution.js +63 -0
  75. package/dist/core/auth/state.js +9 -0
  76. package/dist/core/auth/token-refresh.js +100 -0
  77. package/dist/core/config/defaults.js +31 -0
  78. package/dist/core/config/index.js +3 -0
  79. package/dist/core/config/parser.js +213 -0
  80. package/dist/core/config/schema.js +29 -0
  81. package/dist/core/project/markdown.js +37 -0
  82. package/dist/core/skill/installer.js +59 -0
  83. package/dist/core/skill/paths.js +52 -0
  84. package/dist/generated/api/client/client.gen.js +217 -0
  85. package/dist/generated/api/client/index.js +6 -0
  86. package/dist/generated/api/client/types.gen.js +2 -0
  87. package/dist/generated/api/client/utils.gen.js +231 -0
  88. package/dist/generated/api/client.gen.js +3 -0
  89. package/dist/generated/api/core/auth.gen.js +14 -0
  90. package/dist/generated/api/core/bodySerializer.gen.js +57 -0
  91. package/dist/generated/api/core/params.gen.js +103 -0
  92. package/dist/generated/api/core/pathSerializer.gen.js +114 -0
  93. package/dist/generated/api/core/queryKeySerializer.gen.js +99 -0
  94. package/dist/generated/api/core/serverSentEvents.gen.js +136 -0
  95. package/dist/generated/api/core/types.gen.js +2 -0
  96. package/dist/generated/api/core/utils.gen.js +87 -0
  97. package/dist/generated/api/index.js +2 -0
  98. package/dist/generated/api/sdk.gen.js +556 -0
  99. package/dist/generated/api/types.gen.js +2 -0
  100. package/dist/generated/api/zod.gen.js +1262 -0
  101. package/dist/ui/components/action-hints.js +33 -0
  102. package/dist/ui/components/cli-header.js +91 -0
  103. package/dist/ui/components/confirm-prompt.js +28 -0
  104. package/dist/ui/components/data-table.js +35 -0
  105. package/dist/ui/components/detail-view.js +5 -0
  106. package/dist/ui/components/error-display.js +5 -0
  107. package/dist/ui/components/prompt-layout.js +6 -0
  108. package/dist/ui/components/select-prompt.js +28 -0
  109. package/dist/ui/components/spinner.js +14 -0
  110. package/dist/ui/components/success-message.js +5 -0
  111. package/dist/ui/components/text-input.js +29 -0
  112. package/dist/ui/hooks/use-action-hints.js +21 -0
  113. package/dist/ui/hooks/use-status.js +22 -0
  114. package/dist/ui/index.js +15 -0
  115. package/dist/ui/prompt-helpers.js +55 -0
  116. package/dist/ui/render.js +9 -0
  117. package/package.json +89 -0
  118. package/skills/4ge-cli.md +113 -0
  119. package/usage.md +320 -0
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ export const ActionHints = ({ hints, interactive, onSelect, onDismiss, }) => {
5
+ const [selectedIndex, setSelectedIndex] = useState(0);
6
+ useInput((input, key) => {
7
+ if (!interactive)
8
+ return;
9
+ if (key.upArrow) {
10
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : hints.length - 1));
11
+ }
12
+ else if (key.downArrow) {
13
+ setSelectedIndex((prev) => (prev < hints.length - 1 ? prev + 1 : 0));
14
+ }
15
+ else if (key.return) {
16
+ onSelect?.(hints[selectedIndex]?.command ?? '');
17
+ }
18
+ else if (key.escape || input === 'q') {
19
+ onDismiss?.();
20
+ }
21
+ else if (key.ctrl && input === 'd') {
22
+ process.exit(0);
23
+ }
24
+ });
25
+ if (!hints || hints.length === 0) {
26
+ return null;
27
+ }
28
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "dim", children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "yellow", children: "\uD83D\uDCA1 Next steps:" }) }), hints.map((hint, index) => {
29
+ const isSelected = interactive && index === selectedIndex;
30
+ const prefix = interactive ? (isSelected ? '▶ ' : ' ') : ` ${index + 1}. `;
31
+ return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? 'cyan' : undefined, children: [prefix, hint.description] }), _jsxs(Text, { color: "dim", children: [" (", hint.command, ")"] })] }, index));
32
+ })] }));
33
+ };
@@ -0,0 +1,91 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import chalk from 'chalk';
5
+ const ART = [
6
+ '██╗ ██╗ ██████╗ ███████╗ ██████╗██╗ ██╗',
7
+ '██║ ██║██╔════╝ ██╔════╝ ██╔════╝██║ ██║',
8
+ '███████║██║ ███╗█████╗ ██║ ██║ ██║',
9
+ '╚════██║██║ ██║██╔══╝ ██║ ██║ ██║',
10
+ ' ██║╚██████╔╝███████╗ ╚██████╗███████╗██║',
11
+ ' ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝╚══════╝╚═╝',
12
+ ];
13
+ const MIDDY_ART = [
14
+ '██ ██ ▄▄▄▄ ▄▄▄▄▄ ▄█████ ██ ██ ',
15
+ '▀█████ ██ ▄▄ ██▄▄ ██ ██ ██ ',
16
+ ' ██ ▀███▀ ██▄▄▄ ▀█████ ██████ ██ ',
17
+ ];
18
+ const COMPACT_ART = [
19
+ '┏┓ ┏┓┓ ┳',
20
+ '┃┃┏┓┏┓ ┃ ┃ ┃',
21
+ '┗╋┗┫┗ ┗┛┗┛┻',
22
+ ' ┛ ',
23
+ ];
24
+ const START_COLOR = { r: 0, g: 229, b: 255 }; // #00e5ff
25
+ const END_COLOR = { r: 224, g: 64, b: 251 }; // #e040fb
26
+ const lerp = (start, end, t) => Math.round(start + (end - start) * t);
27
+ const applyGradient = (text, width) => {
28
+ return text
29
+ .split('')
30
+ .map((char, i) => {
31
+ const t = i / (width - 1);
32
+ const r = lerp(START_COLOR.r, END_COLOR.r, t);
33
+ const g = lerp(START_COLOR.g, END_COLOR.g, t);
34
+ const b = lerp(START_COLOR.b, END_COLOR.b, t);
35
+ return chalk.rgb(r, g, b)(char);
36
+ })
37
+ .join('');
38
+ };
39
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
40
+ const STATUS_CONFIG = {
41
+ 'checking-connectivity': { label: 'Checking Connectivity', icon: SPINNER_FRAMES, color: 'yellow' },
42
+ 'refreshing-token': { label: 'Refreshing Token', icon: SPINNER_FRAMES, color: 'yellow' },
43
+ ready: { label: 'Ready', icon: '✓', color: 'green' },
44
+ 'no-connection': { label: 'No Connection', icon: '✗', color: 'red' },
45
+ 'update-required': { label: 'Update Required', icon: '⚠', color: 'magenta' },
46
+ };
47
+ const getArt = (variant) => {
48
+ const art = variant === 'compact' ? MIDDY_ART : ART;
49
+ const width = Math.max(...art.map((line) => line.length));
50
+ return { art, width };
51
+ };
52
+ export function renderHeaderString(version, status = 'ready', headerVariant = 'full') {
53
+ const { art, width } = getArt(headerVariant);
54
+ const borderTop = chalk.dim(`╔${'═'.repeat(width + 2)}╗`);
55
+ const borderBottom = chalk.dim(`╚${'═'.repeat(width + 2)}╝`);
56
+ const artLines = art
57
+ .map((line) => {
58
+ const padding = ' '.repeat(width - line.length);
59
+ return `${chalk.dim('║ ')}${applyGradient(line + padding, width)}${chalk.dim(' ║')}`;
60
+ })
61
+ .join('\n');
62
+ const currentStatus = STATUS_CONFIG[status];
63
+ const icon = Array.isArray(currentStatus.icon) ? currentStatus.icon[0] : currentStatus.icon;
64
+ const statusLabel = chalk[currentStatus.color](`${currentStatus.label} ${icon}`);
65
+ const statusBar = chalk.dim(`[ 4ge CLI | v${version} | Status: ${statusLabel} ]`);
66
+ return `\n${borderTop}\n${artLines}\n${borderBottom}\n\n${statusBar}\n`;
67
+ }
68
+ export default function CliHeader({ version, status = 'ready', headerVariant = 'full' }) {
69
+ const [frame, setFrame] = useState(0);
70
+ useEffect(() => {
71
+ setFrame(0);
72
+ }, [status]);
73
+ useEffect(() => {
74
+ const isAnimated = Array.isArray(STATUS_CONFIG[status].icon);
75
+ if (!isAnimated)
76
+ return;
77
+ const timer = setInterval(() => {
78
+ setFrame((prev) => (prev + 1) % SPINNER_FRAMES.length);
79
+ }, 80);
80
+ return () => clearInterval(timer);
81
+ }, [status]);
82
+ const { art, width } = getArt(headerVariant);
83
+ const borderTop = `╔${'═'.repeat(width + 2)}╗`;
84
+ const borderBottom = `╚${'═'.repeat(width + 2)}╝`;
85
+ const currentStatus = STATUS_CONFIG[status];
86
+ const icon = Array.isArray(currentStatus.icon) ? currentStatus.icon[frame] : currentStatus.icon;
87
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: borderTop }), art.map((line, i) => {
88
+ const padding = ' '.repeat(width - line.length);
89
+ return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "\u2551 " }), _jsx(Text, { children: applyGradient(line + padding, width) }), _jsx(Text, { dimColor: true, children: " \u2551" })] }, i));
90
+ }), _jsx(Text, { dimColor: true, children: borderBottom })] }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["[ 4ge CLI | v", version, " | Status:", ' ', _jsxs(Text, { color: currentStatus.color, children: [currentStatus.label, " ", icon] }), ' ', "]"] }) })] }));
91
+ }
@@ -0,0 +1,28 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { PromptLayout } from './prompt-layout.js';
5
+ export function ConfirmPrompt({ message, onConfirm, defaultYes = false, context, helpText, headerVariant }) {
6
+ const [activeValue, setActiveValue] = useState(defaultYes);
7
+ useInput((input, key) => {
8
+ if (input === 'y' || input === 'Y') {
9
+ onConfirm(true);
10
+ }
11
+ else if (input === 'n' || input === 'N') {
12
+ onConfirm(false);
13
+ }
14
+ else if (key.leftArrow || key.rightArrow) {
15
+ setActiveValue((prev) => !prev);
16
+ }
17
+ else if (key.return) {
18
+ onConfirm(activeValue);
19
+ }
20
+ else if (key.escape) {
21
+ onConfirm(false);
22
+ }
23
+ else if (key.ctrl && input === 'd') {
24
+ process.exit(0);
25
+ }
26
+ });
27
+ return (_jsx(PromptLayout, { message: message, context: context, helpText: helpText, headerVariant: headerVariant, children: _jsxs(Box, { marginLeft: 1, children: [_jsxs(Text, { color: activeValue ? 'cyan' : undefined, bold: activeValue, children: ["[", activeValue ? 'Y' : 'y', "]es"] }), _jsx(Text, { children: " " }), _jsxs(Text, { color: !activeValue ? 'cyan' : undefined, bold: !activeValue, children: ["[", !activeValue ? 'N' : 'n', "]o"] })] }) }));
28
+ }
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ const getStatusColor = (value) => {
4
+ if (typeof value !== 'string')
5
+ return undefined;
6
+ const upper = value.toUpperCase();
7
+ if (upper.includes('COMPLETE') || upper === 'DONE' || upper === 'SUCCESS')
8
+ return 'green';
9
+ if (upper.includes('IN_PROGRESS') || upper === 'PENDING' || upper === 'DOING')
10
+ return 'yellow';
11
+ if (upper.includes('FAILED') || upper === 'ERROR' || upper === 'REJECTED')
12
+ return 'red';
13
+ return undefined;
14
+ };
15
+ export function DataTable({ data, columns, title, emptyMessage = 'No data available' }) {
16
+ if (data.length === 0) {
17
+ return (_jsxs(Box, { flexDirection: "column", paddingY: 1, children: [title && _jsx(Text, { bold: true, underline: true, children: title }), _jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: emptyMessage }) })] }));
18
+ }
19
+ // Calculate widths
20
+ const columnWidths = columns.map((col) => {
21
+ const headerLen = (col.header ?? col.key).length;
22
+ const maxContentLen = data.reduce((max, item) => {
23
+ const val = item[col.key];
24
+ const formatted = col.format ? col.format(val) : String(val ?? '');
25
+ return Math.max(max, formatted.length);
26
+ }, 0);
27
+ return Math.max(col.minWidth ?? 0, headerLen, maxContentLen) + 2;
28
+ });
29
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [title && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, underline: true, children: title }) })), _jsx(Box, { children: columns.map((col, i) => (_jsx(Box, { width: columnWidths[i], children: _jsx(Text, { bold: true, children: (col.header ?? col.key).toUpperCase() }) }, col.key))) }), _jsx(Box, { children: columns.map((col, i) => (_jsx(Box, { width: columnWidths[i], children: _jsx(Text, { dimColor: true, children: '-'.repeat(columnWidths[i] - 1) }) }, col.key))) }), data.map((item, rowIndex) => (_jsx(Box, { children: columns.map((col, i) => {
30
+ const val = item[col.key];
31
+ const formatted = col.format ? col.format(val) : String(val ?? '');
32
+ const color = col.color === 'auto' ? getStatusColor(val) : col.color || undefined;
33
+ return (_jsx(Box, { width: columnWidths[i], children: _jsx(Text, { color: color, children: formatted }) }, col.key));
34
+ }) }, rowIndex)))] }));
35
+ }
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function DetailView({ title, rows }) {
4
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, underline: true, color: "cyan", children: title }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: '─'.repeat(title.length) }) }), rows.map((row, index) => (_jsxs(Box, { children: [_jsx(Box, { width: 20, children: _jsxs(Text, { dimColor: true, children: [row.label, ":"] }) }), _jsx(Box, { flexGrow: 1, children: row.value ? (_jsx(Text, { color: row.color, children: row.value })) : (_jsx(Text, { dimColor: true, children: "\u2014" })) })] }, index)))] }));
5
+ }
@@ -0,0 +1,5 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function ErrorDisplay({ title, message, suggestion }) {
4
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, padding: 1, borderStyle: "round", borderColor: "red", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", title] }) }), _jsx(Box, { marginBottom: suggestion ? 1 : 0, children: _jsx(Text, { color: "red", children: message }) }), suggestion && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: ["\uD83D\uDCA1 ", suggestion] }) }))] }));
5
+ }
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import CliHeader from './cli-header.js';
4
+ export function PromptLayout({ message, context, helpText, children, version, status, headerVariant }) {
5
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [version && (_jsx(CliHeader, { version: version, status: status ?? 'ready', headerVariant: headerVariant })), context && context.length > 0 && (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: context.map((item, i) => (_jsxs(Text, { dimColor: true, children: [item.label, ": ", item.value] }, i))) })), _jsx(Box, { marginBottom: children ? 1 : 0, children: _jsx(Text, { bold: true, children: message }) }), children, helpText && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: helpText }) }))] }));
6
+ }
@@ -0,0 +1,28 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { PromptLayout } from './prompt-layout.js';
5
+ export function SelectPrompt({ message, options, onSelect, context, helpText, headerVariant }) {
6
+ const [selectedIndex, setSelectedIndex] = useState(0);
7
+ useInput((input, key) => {
8
+ if (key.upArrow) {
9
+ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : options.length - 1));
10
+ }
11
+ else if (key.downArrow) {
12
+ setSelectedIndex((prev) => (prev < options.length - 1 ? prev + 1 : 0));
13
+ }
14
+ else if (key.return) {
15
+ onSelect(options[selectedIndex].value);
16
+ }
17
+ else if (key.escape) {
18
+ onSelect(null);
19
+ }
20
+ else if (key.ctrl && input === 'd') {
21
+ process.exit(0);
22
+ }
23
+ });
24
+ return (_jsx(PromptLayout, { message: message, context: context, helpText: helpText, headerVariant: headerVariant, children: options.map((option, index) => {
25
+ const isSelected = index === selectedIndex;
26
+ return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [_jsxs(Text, { color: isSelected ? 'cyan' : undefined, children: [isSelected ? '▶ ' : ' ', option.label] }), option.description && (_jsx(Box, { marginLeft: 4, children: _jsx(Text, { dimColor: true, children: option.description }) }))] }, option.value));
27
+ }) }));
28
+ }
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Text } from 'ink';
4
+ export function Spinner({ message }) {
5
+ const [frame, setFrame] = useState(0);
6
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
7
+ useEffect(() => {
8
+ const timer = setInterval(() => {
9
+ setFrame((prev) => (prev + 1) % frames.length);
10
+ }, 80);
11
+ return () => clearInterval(timer);
12
+ }, [frames.length]);
13
+ return (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: frames[frame] }), " ", message] }));
14
+ }
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function SuccessMessage({ message, path }) {
4
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "green", children: "\u2713 " }), _jsx(Text, { children: message })] }), path && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u2192 ", path] }) }))] }));
5
+ }
@@ -0,0 +1,29 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ export const TextInput = ({ label, defaultValue = '', placeholder = '', mask = false, onSubmit, onCancel }) => {
5
+ const [value, setValue] = useState(defaultValue);
6
+ useInput((input, key) => {
7
+ if (key.escape) {
8
+ onCancel(null);
9
+ return;
10
+ }
11
+ if (key.ctrl && input === 'd') {
12
+ process.exit(0);
13
+ }
14
+ if (key.return) {
15
+ onSubmit(value);
16
+ return;
17
+ }
18
+ if (key.backspace || key.delete) {
19
+ setValue(prev => prev.slice(0, -1));
20
+ return;
21
+ }
22
+ if (input && !key.ctrl && !key.meta) {
23
+ setValue(prev => prev + input);
24
+ }
25
+ });
26
+ const displayValue = mask ? '•'.repeat(value.length) : value;
27
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { color: "cyan", children: [label, ": "] }), _jsx(Text, { children: displayValue || (placeholder ? _jsx(Text, { dimColor: true, children: placeholder }) : '') }), _jsx(Text, { color: "cyan", children: "\u258C" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Esc to cancel" }) })] }));
28
+ };
29
+ export default TextInput;
@@ -0,0 +1,21 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { parseActionHints } from '../../core/api/action-hints.js';
3
+ /**
4
+ * Hook to manage action hints state.
5
+ * Parses raw hints from API metadata and tracks user selection.
6
+ */
7
+ export function useActionHints(rawHints) {
8
+ const hints = parseActionHints(rawHints);
9
+ const [selectedCommand, setSelectedCommand] = useState(null);
10
+ const [dismissed, setDismissed] = useState(false);
11
+ const reset = useCallback(() => {
12
+ setSelectedCommand(null);
13
+ setDismissed(false);
14
+ }, []);
15
+ return {
16
+ hints,
17
+ selectedCommand,
18
+ dismissed,
19
+ reset
20
+ };
21
+ }
@@ -0,0 +1,22 @@
1
+ import { useState, useCallback } from 'react';
2
+ /**
3
+ * Hook to manage CLI status state.
4
+ * Tracks the current status and provides helper methods to update it.
5
+ */
6
+ export function useStatus(initialStatus = 'checking-connectivity') {
7
+ const [status, setStatus] = useState(initialStatus);
8
+ const setCheckingConnectivity = useCallback(() => setStatus('checking-connectivity'), []);
9
+ const setRefreshingToken = useCallback(() => setStatus('refreshing-token'), []);
10
+ const setReady = useCallback(() => setStatus('ready'), []);
11
+ const setNoConnection = useCallback(() => setStatus('no-connection'), []);
12
+ const setUpdateRequired = useCallback(() => setStatus('update-required'), []);
13
+ return {
14
+ status,
15
+ setStatus,
16
+ setCheckingConnectivity,
17
+ setRefreshingToken,
18
+ setReady,
19
+ setNoConnection,
20
+ setUpdateRequired,
21
+ };
22
+ }
@@ -0,0 +1,15 @@
1
+ export { renderTui } from './render.js';
2
+ export { Spinner } from './components/spinner.js';
3
+ export { DataTable } from './components/data-table.js';
4
+ export { DetailView } from './components/detail-view.js';
5
+ export { SelectPrompt } from './components/select-prompt.js';
6
+ export { ConfirmPrompt } from './components/confirm-prompt.js';
7
+ export { TextInput } from './components/text-input.js';
8
+ export { SuccessMessage } from './components/success-message.js';
9
+ export { ErrorDisplay } from './components/error-display.js';
10
+ export { ActionHints as ActionHintsComponent } from './components/action-hints.js';
11
+ export { selectOption, confirmPrompt } from './prompt-helpers.js';
12
+ // Hooks
13
+ export { useActionHints } from './hooks/use-action-hints.js';
14
+ // Core API Hints
15
+ export { parseActionHints, ActionHintSchema, ActionHintsSchema } from '../core/api/action-hints.js';
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink';
3
+ import { SelectPrompt } from './components/select-prompt.js';
4
+ import { ConfirmPrompt } from './components/confirm-prompt.js';
5
+ import { TextInput } from './components/text-input.js';
6
+ import { ActionHints } from './components/action-hints.js';
7
+ import chalk from 'chalk';
8
+ /**
9
+ * Encapsulates the common pattern for interactive prompts:
10
+ * 1. Create Ink instance
11
+ * 2. Return Promise that unmounts and resolves with null or the value
12
+ */
13
+ async function withPromptWrapper(renderFn) {
14
+ return new Promise((resolve) => {
15
+ let instance;
16
+ let settled = false;
17
+ const cleanup = (value) => {
18
+ if (settled)
19
+ return;
20
+ settled = true;
21
+ if (instance) {
22
+ instance.clear?.();
23
+ instance.unmount();
24
+ }
25
+ resolve(value);
26
+ };
27
+ instance = render(renderFn(cleanup));
28
+ // When Ink handles Ctrl+C (default behavior) or is unmounted,
29
+ // we resolve with null to indicate cancellation/exit.
30
+ instance.waitUntilExit().then(() => {
31
+ if (settled)
32
+ return;
33
+ settled = true;
34
+ if (instance) {
35
+ instance.clear?.();
36
+ }
37
+ resolve(null);
38
+ });
39
+ });
40
+ }
41
+ export async function selectOption(message, options, context, helpText, headerVariant) {
42
+ return withPromptWrapper((resolve) => (_jsx(SelectPrompt, { message: message, options: options, context: context, helpText: helpText, onSelect: resolve, headerVariant: headerVariant })));
43
+ }
44
+ export async function confirmPrompt(message, defaultYes = false, context, helpText, headerVariant) {
45
+ return withPromptWrapper((resolve) => (_jsx(ConfirmPrompt, { message: message, context: context, helpText: helpText, onConfirm: resolve, defaultYes: defaultYes, headerVariant: headerVariant })));
46
+ }
47
+ export async function textInput(message, options) {
48
+ return withPromptWrapper((resolve) => (_jsx(TextInput, { label: message, placeholder: options?.placeholder, mask: options?.mask, defaultValue: options?.defaultValue, onSubmit: resolve, onCancel: () => resolve(null) })));
49
+ }
50
+ export async function renderActionHintsMenu(hints) {
51
+ return withPromptWrapper((resolve) => (_jsx(ActionHints, { hints: hints, interactive: true, onSelect: (command) => {
52
+ console.log(`\n 🚀 Run: ${chalk.cyan(command)}\n`);
53
+ resolve();
54
+ }, onDismiss: resolve })));
55
+ }
@@ -0,0 +1,9 @@
1
+ import { render } from 'ink';
2
+ /**
3
+ * Render an Ink component and wait for it to exit.
4
+ * Returns the Ink instance for advanced control.
5
+ */
6
+ export async function renderTui(element) {
7
+ const instance = render(element);
8
+ return instance;
9
+ }
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@4ge/cli",
3
+ "version": "0.1.0",
4
+ "description": "4ge Service Architecture Bridge & CLI",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "4ge": "bin/run.js"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/Conflate-AI/4ge-cli.git"
12
+ },
13
+ "homepage": "https://github.com/Conflate-AI/4ge-cli#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/Conflate-AI/4ge-cli/issues"
16
+ },
17
+ "license": "MIT",
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "provenance": false
21
+ },
22
+ "oclif": {
23
+ "commands": "./dist/commands",
24
+ "bin": "4ge",
25
+ "dirname": "4ge",
26
+ "plugins": [
27
+ "@oclif/plugin-help"
28
+ ],
29
+ "flexibleTaxonomy": true
30
+ },
31
+ "type": "module",
32
+ "engines": {
33
+ "node": ">=24.0.0"
34
+ },
35
+ "scripts": {
36
+ "build": "tsc",
37
+ "typecheck": "tsc --noEmit",
38
+ "dev": "node --env-file=.env.local ./bin/dev.js",
39
+ "test": "node --env-file=.env.test ./node_modules/vitest/vitest.mjs run",
40
+ "test:watch": "node --env-file=.env.test ./node_modules/vitest/vitest.mjs",
41
+ "lint": "eslint src/ --ext .ts",
42
+ "format": "prettier --write \"src/**/*.ts\"",
43
+ "sync-api": "openapi-ts && prettier --write \"src/generated/api/**/*.ts\"",
44
+ "prepublishOnly": "tsc"
45
+ },
46
+ "files": [
47
+ "bin",
48
+ "dist",
49
+ "skills",
50
+ "package.json",
51
+ "README.md",
52
+ "usage.md"
53
+ ],
54
+ "keywords": [
55
+ "4ge",
56
+ "cli",
57
+ "oclif",
58
+ "project-management",
59
+ "ai-agent"
60
+ ],
61
+ "author": "Conflate AI",
62
+ "packageManager": "pnpm@10.18.0",
63
+ "dependencies": {
64
+ "@oclif/core": "^4",
65
+ "@oclif/plugin-help": "^6.2.49",
66
+ "@supabase/supabase-js": "^2.106.2",
67
+ "chalk": "^5.6.2",
68
+ "ink": "^7.0.3",
69
+ "open": "^11.0.0",
70
+ "react": "^19.2.6",
71
+ "yaml": "^2.9.0",
72
+ "zod": "^4.4.3"
73
+ },
74
+ "devDependencies": {
75
+ "@hey-api/client-fetch": "^0.13.1",
76
+ "@hey-api/openapi-ts": "0.97.2",
77
+ "@types/node": "^24.12.4",
78
+ "@types/react": "^19.2.15",
79
+ "@vitest/coverage-v8": "^4.1.10",
80
+ "eslint": "^10.4.0",
81
+ "fast-glob": "^3.3.3",
82
+ "prettier": "^3.8.3",
83
+ "ts-node": "^10.9.2",
84
+ "tslib": "^2.8.1",
85
+ "tsx": "^4.22.3",
86
+ "typescript": "^5.5",
87
+ "vitest": "^4.1.7"
88
+ }
89
+ }
@@ -0,0 +1,113 @@
1
+ # 4ge CLI Skill
2
+
3
+ This document provides instructions for AI agents to interact with the 4ge
4
+ Product Management Platform using the `4ge` CLI.
5
+
6
+ ## 🚨 Preconditions (User Action Required)
7
+ Before using this CLI, the user **MUST** have completed:
8
+ - **Authentication**: Set the `4GE_API_KEY` environment variable or run `4ge auth:login`.
9
+ - **Initialization**: Run `4ge init` in the project root.
10
+ - **Configuration**: `.4ge/config.yaml` exists with a valid `project_id`.
11
+
12
+ **Do NOT attempt to run `auth:login`, `init`, or manual configuration commands.**
13
+ The CLI is already installed and configured in this project.
14
+
15
+ ## ⌨️ Command Syntax
16
+ All interactions follow this structure:
17
+
18
+ ```bash
19
+ 4ge <namespace>:<command> [options] --json
20
+ ```
21
+
22
+ - **Namespace:Command**: Colon-separated (e.g. `epic:list`, `story:show`, `auth:status`).
23
+ - **Required flag**: `--json` is **mandatory** in agent mode for machine-readable output.
24
+ - **Positional arguments**: A few commands take a direct arg without a flag (e.g. `idea:show <ID>`).
25
+
26
+ ## 🛠 Usage Guidelines
27
+
28
+ ### 1. Always use JSON mode
29
+ Append `--json` to **every** command.
30
+
31
+ ```bash
32
+ 4ge epic:list --json
33
+ ```
34
+
35
+ #### Response envelope
36
+ ```json
37
+ {
38
+ "data": { ... },
39
+ "error": null | { "message": string, "code"?: string, "details"?: any, "hint"?: string },
40
+ "metadata": { "timestamp": "ISO8601", "action_hints": [{ "description": string, "command": string }] }
41
+ }
42
+ ```
43
+ `--json` mode sets the `X-Client-Type: 4ge-agent` header on API calls.
44
+
45
+ ### 2. Action hints
46
+ `metadata.action_hints` provides **optional** next-step suggestions. Use judgment
47
+ to decide relevance. `action_hints` is only present when the API provides it —
48
+ do not assume it always exists.
49
+
50
+ ### 3. Error handling
51
+ If `error` is non-null:
52
+ - Inspect `error.message` and `error.code`.
53
+ - Follow `error.hint` for recovery.
54
+ - Run with `--help` if a flag issue is suspected.
55
+
56
+ ### 4. Discovery
57
+ When unsure of flags or subcommands, run `<command> --help` (e.g.
58
+ `4ge story:list --help`).
59
+
60
+ ### 5. Preview changes before applying (dry-run)
61
+ All mutation commands accept `--dry-run`. It prints the payload that **would** be
62
+ sent without executing it. Always use it before running a mutation for the first
63
+ time against a resource.
64
+
65
+ ```bash
66
+ 4ge story:update --story-id <id> --status COMPLETE --dry-run --json
67
+ ```
68
+
69
+ ### 6. Retry transient failures
70
+ All commands accept `--retry <n>` (attempts with exponential backoff + jitter).
71
+ Use it for network-flaky operations.
72
+
73
+ ```bash
74
+ 4ge epic:pull --id <id> --retry 3 --json
75
+ ```
76
+
77
+ ## 📋 Command Reference
78
+
79
+ | Namespace | Commands | Key Flags |
80
+ |-----------|----------|-----------|
81
+ | **project** | `create`, `info`, `list`, `pull`, `update` | `--workspace` (create), `--name`, `--description`, `--template`, `--force`, `--save`/`--stdout` (pull), `--dry-run`, `--retry` |
82
+ | **epic** | `list`, `show`, `pull`, `update` | `--epic-id` / `--id` (pull), `--status`, `--title`, `--recursive` (pull), `--roadmap` (pull, with `--recursive`), `--save`/`--stdout` (pull), `--dry-run`, `--retry` |
83
+ | **story** | `list`, `show`, `pull`, `update` | `--story-id` / `--id` (pull), `--status`, `--title`, `--recursive` (pull), `--roadmap` (pull, with `--recursive`), `--save`/`--stdout` (pull), `--dry-run`, `--retry` |
84
+ | **feature** | `list`, `show`, `pull`, `update` | `--feature-id` / `--id` (pull), `--status`, `--roadmap`, `--recursive` (pull), `--roadmap` (pull, with `--recursive`), `--save`/`--stdout` (pull), `--dry-run`, `--retry` |
85
+ | **idea** | `create`, `graduate`, `list`, `show`, `update` | `<ID>` (positional for show), `--title`, `--workspace`, `--dry-run`, `--retry` |
86
+ | **template** | `create`, `delete`, `info`, `list`, `pull`, `update` | `--id`, `--name`, `--workspace`, `--description`, `--force`, `--save`/`--stdout` (pull), `--dry-run`, `--retry` |
87
+ | **template:feature** | `create`, `delete`, `list`, `update` | `--id`, `--template-id`, `--name`, `--description`, `--status`, `--dry-run`, `--retry` |
88
+ | **plan** | `pull`, `versions` | `--roadmap` (now\|next\|later), `--status`, `--save`, `--stdout`, `--retry` |
89
+ | **auth** | `status`, `logout`, `profile:list`, `profile:use`, `profile:delete` | *(login/logout are user-only)* |
90
+ | **config** | `set`, `get`, `show`, `manage` | `manage` opens interactive config editor; `set`/`get` take `key` / `key=value` |
91
+ | **skill** | `install` | `--agent`, `--global`, `--force` (installs this skill file for an agent harness) |
92
+ | *(root)* | `init`, `whoami` | `init` sets up `.4ge/config.yaml`; `whoami` aliases `auth:status` |
93
+
94
+ ### Project context
95
+ Epic, story, and feature commands read the project from `.4ge/config.yaml`.
96
+ No `--project-id` flag is needed for those namespaces.
97
+
98
+ ### Hierarchy config defaults
99
+ `.4ge/config.yaml` may define:
100
+ - `hierarchy.default_roadmap_filter` (now\|next\|later\|all) — fall-back for `--roadmap` on pull commands.
101
+ - `hierarchy.recursive_pull` (bool) — fall-back for `--recursive` on pull commands.
102
+
103
+ Flags always override config; bare `4ge <ns>:pull --id <id>` honours the config defaults.
104
+
105
+ ## 🔄 Common Workflow Patterns
106
+
107
+ 1. **Explore projects**: `4ge project:list --json`
108
+ 2. **List epics** (honours config `default_roadmap_filter`): `4ge epic:list --json`
109
+ 3. **Drill down**: `4ge story:show --story-id <id> --json`
110
+ 4. **Pull full hierarchy tree**: `4ge epic:pull --id <id> --recursive --json`
111
+ 5. **Sync local plan assets**: `4ge plan:pull --json`
112
+ 6. **Preview a mutation**: `4ge story:update --story-id <id> --status COMPLETE --dry-run --json`
113
+ 7. **Apply the mutation**: drop `--dry-run` and re-run.