@traisetech/autopilot 2.4.0 β 2.5.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/CHANGELOG.md +25 -9
- package/README.md +215 -106
- package/bin/autopilot.js +1 -1
- package/docs/CONFIGURATION.md +103 -103
- package/docs/DESIGN_PRINCIPLES.md +114 -114
- package/docs/TEAM-MODE.md +51 -51
- package/docs/TROUBLESHOOTING.md +21 -21
- package/package.json +75 -69
- package/src/commands/config.js +110 -110
- package/src/commands/dashboard.mjs +151 -151
- package/src/commands/doctor.js +127 -153
- package/src/commands/init.js +8 -9
- package/src/commands/insights.js +237 -237
- package/src/commands/leaderboard.js +116 -116
- package/src/commands/pause.js +18 -18
- package/src/commands/preset.js +121 -121
- package/src/commands/resume.js +17 -17
- package/src/commands/start.js +41 -41
- package/src/commands/status.js +73 -39
- package/src/commands/stop.js +58 -50
- package/src/commands/undo.js +84 -84
- package/src/config/defaults.js +23 -16
- package/src/config/ignore.js +14 -31
- package/src/config/loader.js +80 -80
- package/src/core/commit.js +45 -52
- package/src/core/commitMessageGenerator.js +130 -0
- package/src/core/configValidator.js +92 -0
- package/src/core/events.js +110 -110
- package/src/core/focus.js +2 -1
- package/src/core/gemini.js +15 -15
- package/src/core/git.js +29 -2
- package/src/core/history.js +69 -69
- package/src/core/notifier.js +61 -0
- package/src/core/retryQueue.js +152 -0
- package/src/core/safety.js +224 -210
- package/src/core/state.js +69 -71
- package/src/core/watcher.js +193 -66
- package/src/index.js +70 -70
- package/src/utils/banner.js +6 -6
- package/src/utils/crypto.js +18 -18
- package/src/utils/identity.js +41 -41
- package/src/utils/logger.js +86 -68
- package/src/utils/paths.js +62 -62
- package/src/utils/process.js +141 -141
package/src/commands/config.js
CHANGED
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Config Command
|
|
3
|
-
* View and modify Autopilot configuration
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const logger = require('../utils/logger');
|
|
7
|
-
const { loadConfig, saveConfig, getGlobalConfigPath } = require('../config/loader');
|
|
8
|
-
const fs = require('fs-extra');
|
|
9
|
-
const { getConfigPath } = require('../utils/paths');
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Helper to get value by dot notation
|
|
13
|
-
*/
|
|
14
|
-
const getByDot = (obj, path) => {
|
|
15
|
-
return path.split('.').reduce((acc, part) => acc && acc[part], obj);
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Helper to set value by dot notation
|
|
20
|
-
*/
|
|
21
|
-
const setByDot = (obj, path, value) => {
|
|
22
|
-
const parts = path.split('.');
|
|
23
|
-
const last = parts.pop();
|
|
24
|
-
const target = parts.reduce((acc, part) => {
|
|
25
|
-
if (!acc[part]) acc[part] = {};
|
|
26
|
-
return acc[part];
|
|
27
|
-
}, obj);
|
|
28
|
-
target[last] = value;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Parse value string to appropriate type
|
|
33
|
-
*/
|
|
34
|
-
const parseValue = (val) => {
|
|
35
|
-
if (val === 'true') return true;
|
|
36
|
-
if (val === 'false') return false;
|
|
37
|
-
if (!isNaN(val) && val.trim() !== '') return Number(val);
|
|
38
|
-
return val;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
async function config(cmd, key, value, options) {
|
|
42
|
-
const repoPath = options?.cwd || process.cwd();
|
|
43
|
-
const isGlobal = options?.global || false;
|
|
44
|
-
|
|
45
|
-
if (cmd === 'list') {
|
|
46
|
-
// If list --global, show only global config?
|
|
47
|
-
// Or just show effective config?
|
|
48
|
-
// Let's show effective config, maybe annotated if we had time.
|
|
49
|
-
// But for now, just loadConfig which merges them.
|
|
50
|
-
const currentConfig = await loadConfig(repoPath);
|
|
51
|
-
console.log(JSON.stringify(currentConfig, null, 2));
|
|
52
|
-
if (isGlobal) {
|
|
53
|
-
logger.info('(Note: Showing effective merged config. Use --global to set global values.)');
|
|
54
|
-
}
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (cmd === 'get') {
|
|
59
|
-
if (!key) {
|
|
60
|
-
logger.error('Usage: autopilot config get <key>');
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
// Get always shows effective value
|
|
64
|
-
const currentConfig = await loadConfig(repoPath);
|
|
65
|
-
const val = getByDot(currentConfig, key);
|
|
66
|
-
if (val === undefined) {
|
|
67
|
-
logger.warn(`Key '${key}' not set`);
|
|
68
|
-
} else {
|
|
69
|
-
console.log(typeof val === 'object' ? JSON.stringify(val, null, 2) : val);
|
|
70
|
-
}
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (cmd === 'set') {
|
|
75
|
-
if (!key || value === undefined) {
|
|
76
|
-
logger.error('Usage: autopilot config set <key> <value>');
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const typedValue = parseValue(value);
|
|
81
|
-
|
|
82
|
-
// We need to read the specific file (local or global) to avoid merging defaults into it
|
|
83
|
-
let rawConfig = {};
|
|
84
|
-
let configPath;
|
|
85
|
-
|
|
86
|
-
if (isGlobal) {
|
|
87
|
-
configPath = getGlobalConfigPath();
|
|
88
|
-
} else {
|
|
89
|
-
configPath = getConfigPath(repoPath);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (await fs.pathExists(configPath)) {
|
|
93
|
-
try {
|
|
94
|
-
rawConfig = await fs.readJson(configPath);
|
|
95
|
-
} catch (e) {
|
|
96
|
-
// Ignore read errors, start fresh
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
setByDot(rawConfig, key, typedValue);
|
|
101
|
-
|
|
102
|
-
await saveConfig(repoPath, rawConfig, isGlobal);
|
|
103
|
-
logger.success(`Set ${key} = ${typedValue} ${isGlobal ? '(Global)' : '(Local)'}`);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
logger.error('Unknown config command. Use list, get, or set.');
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
module.exports = config;
|
|
1
|
+
/**
|
|
2
|
+
* Config Command
|
|
3
|
+
* View and modify Autopilot configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const logger = require('../utils/logger');
|
|
7
|
+
const { loadConfig, saveConfig, getGlobalConfigPath } = require('../config/loader');
|
|
8
|
+
const fs = require('fs-extra');
|
|
9
|
+
const { getConfigPath } = require('../utils/paths');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Helper to get value by dot notation
|
|
13
|
+
*/
|
|
14
|
+
const getByDot = (obj, path) => {
|
|
15
|
+
return path.split('.').reduce((acc, part) => acc && acc[part], obj);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Helper to set value by dot notation
|
|
20
|
+
*/
|
|
21
|
+
const setByDot = (obj, path, value) => {
|
|
22
|
+
const parts = path.split('.');
|
|
23
|
+
const last = parts.pop();
|
|
24
|
+
const target = parts.reduce((acc, part) => {
|
|
25
|
+
if (!acc[part]) acc[part] = {};
|
|
26
|
+
return acc[part];
|
|
27
|
+
}, obj);
|
|
28
|
+
target[last] = value;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse value string to appropriate type
|
|
33
|
+
*/
|
|
34
|
+
const parseValue = (val) => {
|
|
35
|
+
if (val === 'true') return true;
|
|
36
|
+
if (val === 'false') return false;
|
|
37
|
+
if (!isNaN(val) && val.trim() !== '') return Number(val);
|
|
38
|
+
return val;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
async function config(cmd, key, value, options) {
|
|
42
|
+
const repoPath = options?.cwd || process.cwd();
|
|
43
|
+
const isGlobal = options?.global || false;
|
|
44
|
+
|
|
45
|
+
if (cmd === 'list') {
|
|
46
|
+
// If list --global, show only global config?
|
|
47
|
+
// Or just show effective config?
|
|
48
|
+
// Let's show effective config, maybe annotated if we had time.
|
|
49
|
+
// But for now, just loadConfig which merges them.
|
|
50
|
+
const currentConfig = await loadConfig(repoPath);
|
|
51
|
+
console.log(JSON.stringify(currentConfig, null, 2));
|
|
52
|
+
if (isGlobal) {
|
|
53
|
+
logger.info('(Note: Showing effective merged config. Use --global to set global values.)');
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (cmd === 'get') {
|
|
59
|
+
if (!key) {
|
|
60
|
+
logger.error('Usage: autopilot config get <key>');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// Get always shows effective value
|
|
64
|
+
const currentConfig = await loadConfig(repoPath);
|
|
65
|
+
const val = getByDot(currentConfig, key);
|
|
66
|
+
if (val === undefined) {
|
|
67
|
+
logger.warn(`Key '${key}' not set`);
|
|
68
|
+
} else {
|
|
69
|
+
console.log(typeof val === 'object' ? JSON.stringify(val, null, 2) : val);
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (cmd === 'set') {
|
|
75
|
+
if (!key || value === undefined) {
|
|
76
|
+
logger.error('Usage: autopilot config set <key> <value>');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const typedValue = parseValue(value);
|
|
81
|
+
|
|
82
|
+
// We need to read the specific file (local or global) to avoid merging defaults into it
|
|
83
|
+
let rawConfig = {};
|
|
84
|
+
let configPath;
|
|
85
|
+
|
|
86
|
+
if (isGlobal) {
|
|
87
|
+
configPath = getGlobalConfigPath();
|
|
88
|
+
} else {
|
|
89
|
+
configPath = getConfigPath(repoPath);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (await fs.pathExists(configPath)) {
|
|
93
|
+
try {
|
|
94
|
+
rawConfig = await fs.readJson(configPath);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
// Ignore read errors, start fresh
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setByDot(rawConfig, key, typedValue);
|
|
101
|
+
|
|
102
|
+
await saveConfig(repoPath, rawConfig, isGlobal);
|
|
103
|
+
logger.success(`Set ${key} = ${typedValue} ${isGlobal ? '(Global)' : '(Local)'}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
logger.error('Unknown config command. Use list, get, or set.');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = config;
|
|
@@ -1,151 +1,151 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { render, Box, Text, useInput, useApp } from 'ink';
|
|
3
|
-
import Gradient from 'ink-gradient';
|
|
4
|
-
import BigText from 'ink-big-text';
|
|
5
|
-
import Spinner from 'ink-spinner';
|
|
6
|
-
import fs from 'fs-extra';
|
|
7
|
-
import path from 'path';
|
|
8
|
-
import StateManager from '../core/state.js';
|
|
9
|
-
import git from '../core/git.js';
|
|
10
|
-
import HistoryManager from '../core/history.js';
|
|
11
|
-
import processUtils from '../utils/process.js';
|
|
12
|
-
|
|
13
|
-
const { useState, useEffect } = React;
|
|
14
|
-
const { getRunningPid } = processUtils;
|
|
15
|
-
|
|
16
|
-
const e = React.createElement;
|
|
17
|
-
|
|
18
|
-
// Dashboard Component
|
|
19
|
-
const Dashboard = () => {
|
|
20
|
-
const { exit } = useApp();
|
|
21
|
-
const root = process.cwd();
|
|
22
|
-
|
|
23
|
-
const [status, setStatus] = useState('loading');
|
|
24
|
-
const [pid, setPid] = useState(null);
|
|
25
|
-
const [lastCommit, setLastCommit] = useState(null);
|
|
26
|
-
const [pendingFiles, setPendingFiles] = useState([]);
|
|
27
|
-
const [todayStats, setTodayStats] = useState({ commits: 0 });
|
|
28
|
-
const [pausedState, setPausedState] = useState(null);
|
|
29
|
-
|
|
30
|
-
// Poll for updates
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
const fetchData = async () => {
|
|
33
|
-
try {
|
|
34
|
-
// 1. Check process status
|
|
35
|
-
const currentPid = await getRunningPid(root);
|
|
36
|
-
setPid(currentPid);
|
|
37
|
-
|
|
38
|
-
// 2. Check Paused State
|
|
39
|
-
const stateManager = new StateManager(root);
|
|
40
|
-
if (stateManager.isPaused()) {
|
|
41
|
-
setStatus('paused');
|
|
42
|
-
setPausedState(stateManager.getState());
|
|
43
|
-
} else if (currentPid) {
|
|
44
|
-
setStatus('running');
|
|
45
|
-
setPausedState(null);
|
|
46
|
-
} else {
|
|
47
|
-
setStatus('stopped');
|
|
48
|
-
setPausedState(null);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// 3. Last Commit
|
|
52
|
-
const historyManager = new HistoryManager(root);
|
|
53
|
-
const last = historyManager.getLastCommit();
|
|
54
|
-
setLastCommit(last);
|
|
55
|
-
|
|
56
|
-
// 4. Pending Files
|
|
57
|
-
const statusObj = await git.getPorcelainStatus(root);
|
|
58
|
-
if (statusObj.ok) {
|
|
59
|
-
setPendingFiles(statusObj.files);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 5. Today Stats (Simple count from history)
|
|
63
|
-
const history = historyManager.getHistory();
|
|
64
|
-
const today = new Date().toDateString();
|
|
65
|
-
const count = history.filter(c => new Date(c.timestamp).toDateString() === today).length;
|
|
66
|
-
setTodayStats({ commits: count });
|
|
67
|
-
|
|
68
|
-
} catch (err) {
|
|
69
|
-
// ignore errors
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
fetchData();
|
|
74
|
-
const timer = setInterval(fetchData, 5000);
|
|
75
|
-
return () => clearInterval(timer);
|
|
76
|
-
}, []);
|
|
77
|
-
|
|
78
|
-
// Keyboard controls
|
|
79
|
-
useInput((input, key) => {
|
|
80
|
-
if (input === 'q') {
|
|
81
|
-
exit();
|
|
82
|
-
}
|
|
83
|
-
if (input === 'p') {
|
|
84
|
-
// Toggle pause
|
|
85
|
-
const stateManager = new StateManager(root);
|
|
86
|
-
if (stateManager.isPaused()) {
|
|
87
|
-
stateManager.resume();
|
|
88
|
-
} else {
|
|
89
|
-
stateManager.pause('Dashboard toggle');
|
|
90
|
-
}
|
|
91
|
-
// Immediate refresh
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
return e(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan" },
|
|
96
|
-
// Header
|
|
97
|
-
e(Box, { marginBottom: 1 },
|
|
98
|
-
e(Gradient, { name: "morning" },
|
|
99
|
-
e(BigText, { text: "Autopilot", font: "simple" })
|
|
100
|
-
)
|
|
101
|
-
),
|
|
102
|
-
|
|
103
|
-
// Status
|
|
104
|
-
e(Box, { marginBottom: 1 },
|
|
105
|
-
e(Text, { bold: true }, "Status: "),
|
|
106
|
-
status === 'running' && e(Text, { color: "green" }, `π’ Running (PID: ${pid})`),
|
|
107
|
-
status === 'paused' && e(Text, { color: "yellow" }, `βΈοΈ Paused (${pausedState?.reason})`),
|
|
108
|
-
status === 'stopped' && e(Text, { color: "red" }, "π΄ Stopped"),
|
|
109
|
-
status === 'loading' && e(Text, {}, e(Spinner, { type: "dots" }), " Loading...")
|
|
110
|
-
),
|
|
111
|
-
|
|
112
|
-
// Activity
|
|
113
|
-
e(Box, { flexDirection: "column", marginBottom: 1 },
|
|
114
|
-
e(Text, { underline: true }, "Activity"),
|
|
115
|
-
e(Box, {},
|
|
116
|
-
e(Text, {}, "Last Commit: "),
|
|
117
|
-
e(Text, { color: "cyan" }, lastCommit ? `${lastCommit.message} (${new Date(lastCommit.timestamp).toLocaleTimeString()})` : 'None')
|
|
118
|
-
),
|
|
119
|
-
e(Box, {},
|
|
120
|
-
e(Text, {}, "Today's Commits: "),
|
|
121
|
-
e(Text, { color: "green" }, todayStats.commits)
|
|
122
|
-
)
|
|
123
|
-
),
|
|
124
|
-
|
|
125
|
-
// Pending Changes
|
|
126
|
-
e(Box, { flexDirection: "column", marginBottom: 1 },
|
|
127
|
-
e(Text, { underline: true }, `Pending Changes (${pendingFiles.length})`),
|
|
128
|
-
e(Box, { flexDirection: "column" },
|
|
129
|
-
pendingFiles.length === 0 ?
|
|
130
|
-
e(Text, { color: "gray" }, "No pending changes") :
|
|
131
|
-
pendingFiles.slice(0, 5).map((f, idx) =>
|
|
132
|
-
e(Text, { key: `${f.file}-${idx}`, color: "yellow" }, ` ${f.status} ${f.file}`)
|
|
133
|
-
)
|
|
134
|
-
),
|
|
135
|
-
pendingFiles.length > 5 && e(Text, { color: "gray" }, ` ...and ${pendingFiles.length - 5} more`)
|
|
136
|
-
),
|
|
137
|
-
|
|
138
|
-
// Footer
|
|
139
|
-
e(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray" },
|
|
140
|
-
e(Text, {}, "Press 'p' to toggle pause, 'q' to quit dashboard")
|
|
141
|
-
)
|
|
142
|
-
);
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
export default function runDashboard() {
|
|
146
|
-
if (!process.stdin.isTTY && !process.env.AUTOPILOT_TEST_MODE) {
|
|
147
|
-
console.error('Error: Dashboard requires an interactive terminal (TTY).');
|
|
148
|
-
process.exit(1);
|
|
149
|
-
}
|
|
150
|
-
render(e(Dashboard));
|
|
151
|
-
}
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, Box, Text, useInput, useApp } from 'ink';
|
|
3
|
+
import Gradient from 'ink-gradient';
|
|
4
|
+
import BigText from 'ink-big-text';
|
|
5
|
+
import Spinner from 'ink-spinner';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import StateManager from '../core/state.js';
|
|
9
|
+
import git from '../core/git.js';
|
|
10
|
+
import HistoryManager from '../core/history.js';
|
|
11
|
+
import processUtils from '../utils/process.js';
|
|
12
|
+
|
|
13
|
+
const { useState, useEffect } = React;
|
|
14
|
+
const { getRunningPid } = processUtils;
|
|
15
|
+
|
|
16
|
+
const e = React.createElement;
|
|
17
|
+
|
|
18
|
+
// Dashboard Component
|
|
19
|
+
const Dashboard = () => {
|
|
20
|
+
const { exit } = useApp();
|
|
21
|
+
const root = process.cwd();
|
|
22
|
+
|
|
23
|
+
const [status, setStatus] = useState('loading');
|
|
24
|
+
const [pid, setPid] = useState(null);
|
|
25
|
+
const [lastCommit, setLastCommit] = useState(null);
|
|
26
|
+
const [pendingFiles, setPendingFiles] = useState([]);
|
|
27
|
+
const [todayStats, setTodayStats] = useState({ commits: 0 });
|
|
28
|
+
const [pausedState, setPausedState] = useState(null);
|
|
29
|
+
|
|
30
|
+
// Poll for updates
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const fetchData = async () => {
|
|
33
|
+
try {
|
|
34
|
+
// 1. Check process status
|
|
35
|
+
const currentPid = await getRunningPid(root);
|
|
36
|
+
setPid(currentPid);
|
|
37
|
+
|
|
38
|
+
// 2. Check Paused State
|
|
39
|
+
const stateManager = new StateManager(root);
|
|
40
|
+
if (stateManager.isPaused()) {
|
|
41
|
+
setStatus('paused');
|
|
42
|
+
setPausedState(stateManager.getState());
|
|
43
|
+
} else if (currentPid) {
|
|
44
|
+
setStatus('running');
|
|
45
|
+
setPausedState(null);
|
|
46
|
+
} else {
|
|
47
|
+
setStatus('stopped');
|
|
48
|
+
setPausedState(null);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. Last Commit
|
|
52
|
+
const historyManager = new HistoryManager(root);
|
|
53
|
+
const last = historyManager.getLastCommit();
|
|
54
|
+
setLastCommit(last);
|
|
55
|
+
|
|
56
|
+
// 4. Pending Files
|
|
57
|
+
const statusObj = await git.getPorcelainStatus(root);
|
|
58
|
+
if (statusObj.ok) {
|
|
59
|
+
setPendingFiles(statusObj.files);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 5. Today Stats (Simple count from history)
|
|
63
|
+
const history = historyManager.getHistory();
|
|
64
|
+
const today = new Date().toDateString();
|
|
65
|
+
const count = history.filter(c => new Date(c.timestamp).toDateString() === today).length;
|
|
66
|
+
setTodayStats({ commits: count });
|
|
67
|
+
|
|
68
|
+
} catch (err) {
|
|
69
|
+
// ignore errors
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
fetchData();
|
|
74
|
+
const timer = setInterval(fetchData, 5000);
|
|
75
|
+
return () => clearInterval(timer);
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
// Keyboard controls
|
|
79
|
+
useInput((input, key) => {
|
|
80
|
+
if (input === 'q') {
|
|
81
|
+
exit();
|
|
82
|
+
}
|
|
83
|
+
if (input === 'p') {
|
|
84
|
+
// Toggle pause
|
|
85
|
+
const stateManager = new StateManager(root);
|
|
86
|
+
if (stateManager.isPaused()) {
|
|
87
|
+
stateManager.resume();
|
|
88
|
+
} else {
|
|
89
|
+
stateManager.pause('Dashboard toggle');
|
|
90
|
+
}
|
|
91
|
+
// Immediate refresh
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return e(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan" },
|
|
96
|
+
// Header
|
|
97
|
+
e(Box, { marginBottom: 1 },
|
|
98
|
+
e(Gradient, { name: "morning" },
|
|
99
|
+
e(BigText, { text: "Autopilot", font: "simple" })
|
|
100
|
+
)
|
|
101
|
+
),
|
|
102
|
+
|
|
103
|
+
// Status
|
|
104
|
+
e(Box, { marginBottom: 1 },
|
|
105
|
+
e(Text, { bold: true }, "Status: "),
|
|
106
|
+
status === 'running' && e(Text, { color: "green" }, `π’ Running (PID: ${pid})`),
|
|
107
|
+
status === 'paused' && e(Text, { color: "yellow" }, `βΈοΈ Paused (${pausedState?.reason})`),
|
|
108
|
+
status === 'stopped' && e(Text, { color: "red" }, "π΄ Stopped"),
|
|
109
|
+
status === 'loading' && e(Text, {}, e(Spinner, { type: "dots" }), " Loading...")
|
|
110
|
+
),
|
|
111
|
+
|
|
112
|
+
// Activity
|
|
113
|
+
e(Box, { flexDirection: "column", marginBottom: 1 },
|
|
114
|
+
e(Text, { underline: true }, "Activity"),
|
|
115
|
+
e(Box, {},
|
|
116
|
+
e(Text, {}, "Last Commit: "),
|
|
117
|
+
e(Text, { color: "cyan" }, lastCommit ? `${lastCommit.message} (${new Date(lastCommit.timestamp).toLocaleTimeString()})` : 'None')
|
|
118
|
+
),
|
|
119
|
+
e(Box, {},
|
|
120
|
+
e(Text, {}, "Today's Commits: "),
|
|
121
|
+
e(Text, { color: "green" }, todayStats.commits)
|
|
122
|
+
)
|
|
123
|
+
),
|
|
124
|
+
|
|
125
|
+
// Pending Changes
|
|
126
|
+
e(Box, { flexDirection: "column", marginBottom: 1 },
|
|
127
|
+
e(Text, { underline: true }, `Pending Changes (${pendingFiles.length})`),
|
|
128
|
+
e(Box, { flexDirection: "column" },
|
|
129
|
+
pendingFiles.length === 0 ?
|
|
130
|
+
e(Text, { color: "gray" }, "No pending changes") :
|
|
131
|
+
pendingFiles.slice(0, 5).map((f, idx) =>
|
|
132
|
+
e(Text, { key: `${f.file}-${idx}`, color: "yellow" }, ` ${f.status} ${f.file}`)
|
|
133
|
+
)
|
|
134
|
+
),
|
|
135
|
+
pendingFiles.length > 5 && e(Text, { color: "gray" }, ` ...and ${pendingFiles.length - 5} more`)
|
|
136
|
+
),
|
|
137
|
+
|
|
138
|
+
// Footer
|
|
139
|
+
e(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray" },
|
|
140
|
+
e(Text, {}, "Press 'p' to toggle pause, 'q' to quit dashboard")
|
|
141
|
+
)
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export default function runDashboard() {
|
|
146
|
+
if (!process.stdin.isTTY && !process.env.AUTOPILOT_TEST_MODE) {
|
|
147
|
+
console.error('Error: Dashboard requires an interactive terminal (TTY).');
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
render(e(Dashboard));
|
|
151
|
+
}
|