@crystalplatforms/adm 0.2.3 → 0.3.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 -21
- package/README.md +154 -161
- package/bin/adm +18 -21
- package/package.json +9 -22
- package/scripts/completion/adm.bash +12 -55
- package/scripts/completion/adm.zsh +21 -58
- package/scripts/install.js +8 -8
- package/src/commands/config.js +66 -0
- package/src/commands/dotfiles.js +49 -32
- package/src/commands/feedback.js +12 -0
- package/src/commands/help.js +33 -0
- package/src/commands/uninstall.js +73 -54
- package/src/commands/version.js +7 -0
- package/src/commands.js +26 -0
- package/src/config/index.js +32 -32
- package/src/data/extensions.json +358 -358
- package/src/engines.js +38 -0
- package/src/install/executor.js +263 -0
- package/src/install/hints.js +94 -0
- package/src/install/npm-prefix.js +110 -0
- package/src/install/plan.js +75 -0
- package/src/install/report.js +46 -0
- package/src/install/run.js +88 -0
- package/src/install/shell-paths.js +129 -0
- package/src/legacy-cleanup.js +96 -0
- package/src/pkg/builders.js +220 -0
- package/src/pkg/managers.js +59 -0
- package/src/pkg/package-names.js +229 -0
- package/src/pkg/script-urls.js +100 -0
- package/src/platform/index.js +129 -0
- package/src/platform/os-release.js +27 -0
- package/src/platform/shell.js +111 -0
- package/src/reports.js +37 -0
- package/src/router.js +157 -0
- package/src/setup/catalog.js +13 -13
- package/src/setup/git-step.js +128 -0
- package/src/setup/loader.js +108 -0
- package/src/setup/prompts.js +259 -0
- package/src/setup/registry.js +48 -0
- package/src/setup/spinner.js +67 -0
- package/src/setup/wizard.js +319 -195
- package/src/upgrade/index.js +499 -0
- package/src/utils/closest-match.js +36 -0
- package/src/utils/dotfiles-sync.js +89 -89
- package/src/utils/file-conflict-handler.js +30 -30
- package/bin/adm-tui +0 -13
- package/src/ai/knowledge.js +0 -61
- package/src/commands/connect.js +0 -76
- package/src/commands/issue.js +0 -80
- package/src/commands/pr.js +0 -55
- package/src/commands/theme.js +0 -103
- package/src/commands/upgrade.js +0 -114
- package/src/installer/git-config.js +0 -34
- package/src/installer/node-installer.js +0 -50
- package/src/installer/package-manager-installer.js +0 -31
- package/src/installer/ssh-setup.js +0 -31
- package/src/installer/system-packages.js +0 -49
- package/src/installers/index.js +0 -66
- package/src/integrations/ai-backend.js +0 -60
- package/src/integrations/ai-providers/registry.js +0 -261
- package/src/integrations/github.js +0 -107
- package/src/integrations/gitlab.js +0 -121
- package/src/plugins/context.js +0 -64
- package/src/plugins/loader.js +0 -50
- package/src/plugins/runner.js +0 -25
- package/src/setup/installer.js +0 -192
- package/src/shell/detector.js +0 -10
- package/src/tui/app-state.js +0 -374
- package/src/tui/app.js +0 -442
- package/src/tui/autocomplete.js +0 -57
- package/src/tui/commands/registry.js +0 -823
- package/src/tui/components/SetupScreen.js +0 -292
- package/src/ui/animations.js +0 -24
- package/src/ui/spinner.js +0 -34
- package/src/ui/theme/cyberpunk.js +0 -14
- package/src/ui/theme/dark.js +0 -14
- package/src/ui/theme/forest.js +0 -14
- package/src/ui/theme/light.js +0 -14
- package/src/ui/theme/monokai.js +0 -14
- package/src/ui/theme/nord.js +0 -14
- package/src/ui/theme.js +0 -49
- package/src/utils/command-history.js +0 -43
- package/src/utils/error-handler.js +0 -69
- package/src/utils/fuzzy-search.js +0 -20
- package/src/utils/github-error-handler.js +0 -26
- package/src/utils/keychain.js +0 -97
- package/src/utils/open-url.js +0 -15
- package/src/utils/terminal-detection.js +0 -26
- package/src/utils/token-encryption.js +0 -96
package/src/setup/installer.js
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
const { promisify } = require('util');
|
|
2
|
-
const execAsync = promisify(require('child_process').exec);
|
|
3
|
-
const execSync = require('child_process').execSync;
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
|
|
8
|
-
const SCRIPT_URLS = {
|
|
9
|
-
rustup: 'https://sh.rustup.rs',
|
|
10
|
-
bun: 'https://bun.sh/install',
|
|
11
|
-
conda: 'https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh',
|
|
12
|
-
gcloud: 'https://sdk.cloud.google.com',
|
|
13
|
-
'azure-cli': 'https://aka.ms/InstallAzureCli',
|
|
14
|
-
composer: 'https://getcomposer.org/installer',
|
|
15
|
-
vcpkg: 'https://github.com/microsoft/vcpkg',
|
|
16
|
-
'oh-my-zsh': 'https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh',
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// Ensure npm uses a user-writable directory (fixes EACCES without sudo)
|
|
20
|
-
let _npmFixed = false;
|
|
21
|
-
function ensureNpmPrefix() {
|
|
22
|
-
if (_npmFixed) return;
|
|
23
|
-
_npmFixed = true;
|
|
24
|
-
try {
|
|
25
|
-
const prefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
|
26
|
-
const userPrefix = path.join(os.homedir(), '.npm-global');
|
|
27
|
-
const userBin = path.join(userPrefix, 'bin');
|
|
28
|
-
const needsPrefixChange = prefix === '/usr/local' || prefix === '/usr';
|
|
29
|
-
const needsPathFix = !process.env.PATH.split(':').includes(userBin);
|
|
30
|
-
|
|
31
|
-
if (needsPrefixChange) {
|
|
32
|
-
if (!fs.existsSync(userPrefix)) {
|
|
33
|
-
fs.mkdirSync(userBin, { recursive: true });
|
|
34
|
-
}
|
|
35
|
-
execSync(`npm config set prefix '${userPrefix}'`, { stdio: 'pipe' });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Always ensure PATH is set — even if prefix was already changed before
|
|
39
|
-
if (needsPathFix || needsPrefixChange) {
|
|
40
|
-
addToShellPath(userBin);
|
|
41
|
-
}
|
|
42
|
-
} catch {
|
|
43
|
-
// Can't fix — will fail later with helpful message
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Add directory to PATH in shell config (.zshrc or .bashrc)
|
|
48
|
-
function addToShellPath(dir) {
|
|
49
|
-
const marker = '# adm-cli npm path';
|
|
50
|
-
const exportLine = `export PATH="${dir}:\$PATH" ${marker}`;
|
|
51
|
-
|
|
52
|
-
const shellRc = process.env.SHELL && process.env.SHELL.includes('zsh')
|
|
53
|
-
? path.join(os.homedir(), '.zshrc')
|
|
54
|
-
: path.join(os.homedir(), '.bashrc');
|
|
55
|
-
|
|
56
|
-
try {
|
|
57
|
-
let content = '';
|
|
58
|
-
if (fs.existsSync(shellRc)) {
|
|
59
|
-
content = fs.readFileSync(shellRc, 'utf8');
|
|
60
|
-
}
|
|
61
|
-
// Remove old adm path entries
|
|
62
|
-
const lines = content.split('\n').filter(l => !l.includes(marker));
|
|
63
|
-
lines.push(exportLine);
|
|
64
|
-
fs.writeFileSync(shellRc, lines.join('\n'), 'utf8');
|
|
65
|
-
} catch {
|
|
66
|
-
// Can't write to shell rc — user will need to add manually
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Also set in current process so subsequent installs find tools
|
|
70
|
-
process.env.PATH = `${dir}:${process.env.PATH}`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function getNpmBinPath() {
|
|
74
|
-
try {
|
|
75
|
-
const prefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim();
|
|
76
|
-
return path.join(prefix, 'bin');
|
|
77
|
-
} catch {
|
|
78
|
-
return '';
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function getInstallCommand(tool, platform) {
|
|
83
|
-
const method = tool.installMethod;
|
|
84
|
-
const id = tool.id;
|
|
85
|
-
|
|
86
|
-
const commands = {
|
|
87
|
-
npm: `npm install -g ${id}@latest`,
|
|
88
|
-
brew: platform === 'darwin' ? `brew install ${id}` : `sudo apt-get install -y ${id}`,
|
|
89
|
-
pip: `pip install --upgrade ${id}`,
|
|
90
|
-
cargo: `cargo install ${id}`,
|
|
91
|
-
go: `go install ${id}@latest`,
|
|
92
|
-
gem: `gem install ${id}`,
|
|
93
|
-
composer: `composer global require ${id}`,
|
|
94
|
-
dotnet: `dotnet tool install -g ${id}`,
|
|
95
|
-
script: `curl -fsSL ${SCRIPT_URLS[id] || 'https://example.com/install'} | sh`,
|
|
96
|
-
nvm: `curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.4/install.sh | bash`,
|
|
97
|
-
node: `npm install -g ${id}@latest`,
|
|
98
|
-
python: `python3 -m pip install --upgrade ${id}`,
|
|
99
|
-
rustup: `rustup component add ${id}`,
|
|
100
|
-
apt: `sudo apt-get install -y ${id}`,
|
|
101
|
-
manual: `# Manual install required for ${id}`,
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
if (method === 'link') {
|
|
105
|
-
return `echo 'Visit: ${tool.url || tool.id}'`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return commands[method] || `# Unknown install method: ${method}`;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function isToolInstalled(tool, checkFn) {
|
|
112
|
-
const check = checkFn || execSync;
|
|
113
|
-
const cmd = tool.command || tool.id;
|
|
114
|
-
try {
|
|
115
|
-
check(`command -v ${cmd} >/dev/null 2>&1`, { shell: '/bin/bash', stdio: 'pipe' });
|
|
116
|
-
return true;
|
|
117
|
-
} catch {
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function installToolAsync(tool, platform) {
|
|
123
|
-
if (tool.installMethod === 'link') {
|
|
124
|
-
return { id: tool.id, name: tool.name, status: 'link', message: `Visit marketplace: ${tool.url || 'N/A'}` };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (isToolInstalled(tool)) {
|
|
128
|
-
return { id: tool.id, name: tool.name, status: 'skipped', message: 'already installed' };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// For npm/node methods, fix permissions first
|
|
132
|
-
if (tool.installMethod === 'npm' || tool.installMethod === 'node') {
|
|
133
|
-
ensureNpmPrefix();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const cmd = getInstallCommand(tool, platform);
|
|
137
|
-
try {
|
|
138
|
-
await execAsync(cmd, { shell: '/bin/bash' });
|
|
139
|
-
// Refresh PATH so subsequent tools can find this one
|
|
140
|
-
refreshPath();
|
|
141
|
-
return { id: tool.id, name: tool.name, status: 'installed', message: 'installed' };
|
|
142
|
-
} catch (err) {
|
|
143
|
-
const stderr = err.stderr || '';
|
|
144
|
-
let hint = '';
|
|
145
|
-
if (stderr.includes('EACCES')) {
|
|
146
|
-
hint = 'Fix: npm config set prefix ~/.npm-global && add ~/.npm-global/bin to PATH';
|
|
147
|
-
}
|
|
148
|
-
return { id: tool.id, name: tool.name, status: 'failed', error: (stderr.split('\n')[0] || err.message), hint, command: cmd };
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Refresh PATH by reading shell rc files (picks up new tool paths like ~/.bun/bin)
|
|
153
|
-
function refreshPath() {
|
|
154
|
-
const home = os.homedir();
|
|
155
|
-
const extraDirs = [
|
|
156
|
-
path.join(home, '.npm-global', 'bin'),
|
|
157
|
-
path.join(home, '.bun', 'bin'),
|
|
158
|
-
path.join(home, '.cargo', 'bin'),
|
|
159
|
-
path.join(home, 'go', 'bin'),
|
|
160
|
-
'/opt/homebrew/bin',
|
|
161
|
-
'/usr/local/bin',
|
|
162
|
-
];
|
|
163
|
-
for (const dir of extraDirs) {
|
|
164
|
-
if (fs.existsSync(dir) && !process.env.PATH.split(':').includes(dir)) {
|
|
165
|
-
process.env.PATH = `${dir}:${process.env.PATH}`;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async function installTools(tools, platform, checkFn) {
|
|
171
|
-
const run = checkFn || execSync;
|
|
172
|
-
const results = [];
|
|
173
|
-
|
|
174
|
-
for (const tool of tools) {
|
|
175
|
-
if (isToolInstalled(tool, run)) {
|
|
176
|
-
results.push({ id: tool.id, name: tool.name, status: 'skipped', message: `${tool.name} is already installed` });
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const cmd = getInstallCommand(tool, platform);
|
|
181
|
-
try {
|
|
182
|
-
run(cmd, { shell: '/bin/bash', stdio: 'pipe' });
|
|
183
|
-
results.push({ id: tool.id, name: tool.name, status: 'installed', message: `${tool.name} installed` });
|
|
184
|
-
} catch (err) {
|
|
185
|
-
results.push({ id: tool.id, name: tool.name, status: 'failed', error: err.message });
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return results;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
module.exports = { getInstallCommand, isToolInstalled, installTools, installToolAsync };
|
package/src/shell/detector.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// Minimal shell detector stub for TDD
|
|
2
|
-
function detectShell(env) {
|
|
3
|
-
env = env || process.env;
|
|
4
|
-
const shell = env.SHELL || '';
|
|
5
|
-
if (shell.includes('zsh')) return 'zsh';
|
|
6
|
-
if (shell.includes('bash')) return 'bash';
|
|
7
|
-
if (env.TERM_PROGRAM && env.TERM_PROGRAM.includes('Apple_Terminal')) return 'bash';
|
|
8
|
-
return 'unknown';
|
|
9
|
-
}
|
|
10
|
-
module.exports = { detectShell };
|
package/src/tui/app-state.js
DELETED
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
const { createRegistry, getPlaceholderText } = require('./commands/registry');
|
|
2
|
-
const { resolveTheme } = require('../ui/theme');
|
|
3
|
-
const { readConfig, writeConfig } = require('../config');
|
|
4
|
-
const ai = require('../integrations/ai-backend');
|
|
5
|
-
const { getKnowledge } = require('../ai/knowledge');
|
|
6
|
-
const { createAutocomplete, updateAutocomplete, moveUp, moveDown, selectActive, closeAutocomplete } = require('./autocomplete');
|
|
7
|
-
const fs = require('fs');
|
|
8
|
-
const path = require('path');
|
|
9
|
-
const os = require('os');
|
|
10
|
-
|
|
11
|
-
const VERSION = `Crystal ADM-CLI ${require('../../package.json').version}`;
|
|
12
|
-
const WELCOME_TEXT = 'Welcome to ADM! Type /help for commands or /download to install developer tools.';
|
|
13
|
-
|
|
14
|
-
function createAppState() {
|
|
15
|
-
// Load saved config synchronously for correct initial state (no re-render needed)
|
|
16
|
-
let savedTheme = 'dark';
|
|
17
|
-
let savedProvider = 'glm';
|
|
18
|
-
try {
|
|
19
|
-
const cfg = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.adm', 'config.json'), 'utf8'));
|
|
20
|
-
if (cfg.theme) savedTheme = cfg.theme;
|
|
21
|
-
if (cfg.aiProvider) savedProvider = cfg.aiProvider;
|
|
22
|
-
} catch {}
|
|
23
|
-
|
|
24
|
-
const config = { theme: savedTheme };
|
|
25
|
-
const themeState = { current: savedTheme };
|
|
26
|
-
const theme = resolveTheme(config);
|
|
27
|
-
const registry = createRegistry({ theme: themeState, execSync: null });
|
|
28
|
-
|
|
29
|
-
const ac = createAutocomplete(
|
|
30
|
-
(partial) => registry.autocomplete(partial),
|
|
31
|
-
(name) => registry.getCommandInfo(name),
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const state = {
|
|
35
|
-
messages: [{ text: WELCOME_TEXT, type: 'system' }],
|
|
36
|
-
theme,
|
|
37
|
-
themeState,
|
|
38
|
-
config,
|
|
39
|
-
aiMode: false,
|
|
40
|
-
showSetup: false,
|
|
41
|
-
setupDryRun: false,
|
|
42
|
-
connectStep: null,
|
|
43
|
-
connectPlatform: null,
|
|
44
|
-
connectCursor: 0,
|
|
45
|
-
modelStep: null,
|
|
46
|
-
modelProvider: null,
|
|
47
|
-
aiLoading: false,
|
|
48
|
-
githubStep: null,
|
|
49
|
-
githubCursor: 0,
|
|
50
|
-
confirmStep: null,
|
|
51
|
-
upgradeLoading: false,
|
|
52
|
-
aiHistory: [],
|
|
53
|
-
activeProvider: savedProvider,
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
let knowledge = null;
|
|
57
|
-
try {
|
|
58
|
-
knowledge = getKnowledge(VERSION, process.cwd());
|
|
59
|
-
} catch {
|
|
60
|
-
// knowledge unavailable — continue without it
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function disableAI() {
|
|
64
|
-
state.aiMode = false;
|
|
65
|
-
state.aiHistory = [];
|
|
66
|
-
state.messages.push({ text: 'AI: off', type: 'system' });
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function processInput(input) {
|
|
70
|
-
const trimmed = input.trim();
|
|
71
|
-
|
|
72
|
-
// In AI mode, intercept /exit and /ai to leave/toggle AI mode
|
|
73
|
-
const cmd = trimmed.startsWith('/') ? trimmed.slice(1) : trimmed;
|
|
74
|
-
if (state.aiMode && (cmd === 'exit' || cmd === 'ai')) {
|
|
75
|
-
state.aiMode = false;
|
|
76
|
-
state.aiHistory = [];
|
|
77
|
-
state.messages.push({ text: 'Exited AI mode.', type: 'system' });
|
|
78
|
-
return { output: null, shouldExit: false, shouldClear: false };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// In AI mode, send non-command input to the active AI provider
|
|
82
|
-
if (state.aiMode) {
|
|
83
|
-
state.aiLoading = true;
|
|
84
|
-
try {
|
|
85
|
-
const messages = [];
|
|
86
|
-
if (knowledge) {
|
|
87
|
-
messages.push({ role: 'system', content: knowledge });
|
|
88
|
-
}
|
|
89
|
-
messages.push(...state.aiHistory);
|
|
90
|
-
messages.push({ role: 'user', content: input });
|
|
91
|
-
|
|
92
|
-
const cfg = await readConfig();
|
|
93
|
-
const providerId = cfg.aiProvider || 'glm';
|
|
94
|
-
state.activeProvider = providerId;
|
|
95
|
-
const apiKey = cfg[`ai.${providerId}Key`] || process.env.GLM_API_KEY;
|
|
96
|
-
|
|
97
|
-
if (!apiKey && providerId !== 'ollama') {
|
|
98
|
-
throw new Error(`AI not configured. No API key for ${providerId}. Use /model ${providerId} to set one.`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const { queryWithProvider } = require('../integrations/ai-providers/registry');
|
|
102
|
-
const response = await queryWithProvider(providerId, input, { apiKey, messages });
|
|
103
|
-
|
|
104
|
-
state.aiHistory.push({ role: 'user', content: input });
|
|
105
|
-
state.aiHistory.push({ role: 'assistant', content: response });
|
|
106
|
-
|
|
107
|
-
const prefix = providerId === 'glm' ? 'GLM' : providerId;
|
|
108
|
-
state.messages.push({ text: `${prefix}: ${response}`, type: 'ai' });
|
|
109
|
-
} catch (err) {
|
|
110
|
-
state.messages.push({ text: `AI error: ${err.message}`, type: 'ai-error' });
|
|
111
|
-
} finally {
|
|
112
|
-
state.aiLoading = false;
|
|
113
|
-
}
|
|
114
|
-
return { output: null, shouldExit: false, shouldClear: false };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Set upgrade loading before async dispatch
|
|
118
|
-
const cmdName = trimmed.startsWith('/') ? trimmed.slice(1).split(/\s+/)[0] : trimmed.split(/\s+/)[0];
|
|
119
|
-
if (cmdName === 'upgrade') {
|
|
120
|
-
state.upgradeLoading = true;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const result = await registry.dispatch(input);
|
|
124
|
-
state.upgradeLoading = false;
|
|
125
|
-
|
|
126
|
-
if (result.shouldClear) {
|
|
127
|
-
state.messages.length = 0; // mutate in-place so external refs stay valid
|
|
128
|
-
return result;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (result.shouldToggleAI) {
|
|
132
|
-
state.aiMode = !state.aiMode;
|
|
133
|
-
const label = state.aiMode
|
|
134
|
-
? `AI: ON (${state.activeProvider}) — type a question, /exit to leave AI mode`
|
|
135
|
-
: 'AI: off';
|
|
136
|
-
state.messages.push({ text: label, type: 'system' });
|
|
137
|
-
if (result.output) {
|
|
138
|
-
state.messages.push({ text: result.output, type: 'ai' });
|
|
139
|
-
}
|
|
140
|
-
return result;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (result.shouldShowSetup) {
|
|
144
|
-
state.showSetup = true;
|
|
145
|
-
state.setupDryRun = result.dryRun || false;
|
|
146
|
-
return result;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (result.shouldStartConnect) {
|
|
150
|
-
startConnect();
|
|
151
|
-
return result;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (result.shouldStartGithub) {
|
|
155
|
-
startGithub();
|
|
156
|
-
return result;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (result.shouldPromptModelToken) {
|
|
160
|
-
state.modelStep = 'token';
|
|
161
|
-
state.modelProvider = result.modelProvider;
|
|
162
|
-
state.messages.push({ text: `Enter your ${result.modelProvider} API key:`, type: 'system' });
|
|
163
|
-
return result;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (result.needsConfirm) {
|
|
167
|
-
state.confirmStep = {
|
|
168
|
-
message: result.confirmMessage,
|
|
169
|
-
onConfirm: result.onConfirm,
|
|
170
|
-
onCancel: result.onCancel,
|
|
171
|
-
};
|
|
172
|
-
state.messages.push({ text: result.output, type: 'system' });
|
|
173
|
-
return result;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (result.output) {
|
|
177
|
-
state.messages.push({ text: result.output, type: 'command' });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return result;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function exitAI() {
|
|
184
|
-
if (state.aiMode) {
|
|
185
|
-
disableAI();
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function exitSetup() {
|
|
190
|
-
state.showSetup = false;
|
|
191
|
-
state.setupDryRun = false;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function startConnect() {
|
|
195
|
-
state.connectStep = 'select';
|
|
196
|
-
state.connectCursor = 0;
|
|
197
|
-
state.messages.push({ text: 'Select a platform:', type: 'system' });
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function moveConnectCursor(direction) {
|
|
201
|
-
const options = ['GitHub', 'GitLab'];
|
|
202
|
-
state.connectCursor = (state.connectCursor + direction + options.length) % options.length;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function selectConnectPlatform() {
|
|
206
|
-
const options = ['github', 'gitlab'];
|
|
207
|
-
const names = ['GitHub', 'GitLab'];
|
|
208
|
-
state.connectPlatform = options[state.connectCursor];
|
|
209
|
-
state.connectStep = 'token';
|
|
210
|
-
state.messages.push({ text: `Enter your ${names[state.connectCursor]} PAT:`, type: 'system' });
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async function submitConnectToken(token) {
|
|
214
|
-
const platform = state.connectPlatform;
|
|
215
|
-
try {
|
|
216
|
-
if (platform === 'github') {
|
|
217
|
-
const gh = require('../integrations/github');
|
|
218
|
-
const result = await gh.connect(token);
|
|
219
|
-
state.messages.push({ text: `GitHub: connected as ${result.user.login}`, type: 'system' });
|
|
220
|
-
} else {
|
|
221
|
-
const gl = require('../integrations/gitlab');
|
|
222
|
-
const result = await gl.connect(token);
|
|
223
|
-
state.messages.push({ text: `GitLab: connected as ${result.user.username}`, type: 'system' });
|
|
224
|
-
}
|
|
225
|
-
} catch (err) {
|
|
226
|
-
state.messages.push({ text: err.message, type: 'ai-error' });
|
|
227
|
-
}
|
|
228
|
-
state.connectStep = null;
|
|
229
|
-
state.connectPlatform = null;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function cancelConnect() {
|
|
233
|
-
state.messages.push({ text: 'Cancelled.', type: 'system' });
|
|
234
|
-
state.connectStep = null;
|
|
235
|
-
state.connectPlatform = null;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const GITHUB_OPTIONS = 4;
|
|
239
|
-
|
|
240
|
-
function startGithub() {
|
|
241
|
-
state.githubStep = 'select';
|
|
242
|
-
state.githubCursor = 0;
|
|
243
|
-
state.messages.push({ text: 'Select an action:', type: 'system' });
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function moveGithubCursor(direction) {
|
|
247
|
-
state.githubCursor = (state.githubCursor + direction + GITHUB_OPTIONS) % GITHUB_OPTIONS;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async function selectGithubItem() {
|
|
251
|
-
const idx = state.githubCursor;
|
|
252
|
-
state.githubStep = null;
|
|
253
|
-
|
|
254
|
-
const subcommands = ['status', 'pr list', 'issue list', 'commit suggest'];
|
|
255
|
-
await processInput(`github ${subcommands[idx]}`);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function cancelGithub() {
|
|
259
|
-
state.messages.push({ text: 'Cancelled.', type: 'system' });
|
|
260
|
-
state.githubStep = null;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async function submitModelToken(token) {
|
|
264
|
-
const provider = state.modelProvider;
|
|
265
|
-
const { getProvider } = require('../integrations/ai-providers/registry');
|
|
266
|
-
const providerInfo = getProvider(provider);
|
|
267
|
-
const config = await readConfig();
|
|
268
|
-
config[`ai.${provider}Key`] = token;
|
|
269
|
-
config.aiProvider = provider;
|
|
270
|
-
await writeConfig(config);
|
|
271
|
-
const name = providerInfo ? providerInfo.name : provider;
|
|
272
|
-
state.messages.push({ text: `Provider switched to ${name} (${provider}).`, type: 'system' });
|
|
273
|
-
state.modelStep = null;
|
|
274
|
-
state.modelProvider = null;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function cancelModelToken() {
|
|
278
|
-
state.messages.push({ text: 'Cancelled.', type: 'system' });
|
|
279
|
-
state.modelStep = null;
|
|
280
|
-
state.modelProvider = null;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
async function confirmAction() {
|
|
284
|
-
if (!state.confirmStep) return { output: null, shouldExit: false, shouldClear: false };
|
|
285
|
-
const { onConfirm } = state.confirmStep;
|
|
286
|
-
state.confirmStep = null;
|
|
287
|
-
const result = await onConfirm();
|
|
288
|
-
if (result.output) {
|
|
289
|
-
state.messages.push({ text: result.output, type: 'system' });
|
|
290
|
-
}
|
|
291
|
-
return result;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function confirmCancel() {
|
|
295
|
-
if (!state.confirmStep) return;
|
|
296
|
-
const { onCancel } = state.confirmStep;
|
|
297
|
-
state.confirmStep = null;
|
|
298
|
-
const result = onCancel();
|
|
299
|
-
if (result.output) {
|
|
300
|
-
state.messages.push({ text: result.output, type: 'system' });
|
|
301
|
-
}
|
|
302
|
-
return result;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function markSetupDone() {
|
|
306
|
-
state.setupInstalled = true;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function getStatusBar() {
|
|
310
|
-
return {
|
|
311
|
-
themeName: themeState.current,
|
|
312
|
-
aiMode: state.aiMode,
|
|
313
|
-
version: VERSION,
|
|
314
|
-
activeProvider: state.activeProvider,
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function getSuggestions(input) {
|
|
319
|
-
const trimmed = input.trim();
|
|
320
|
-
const stripped = trimmed.startsWith('/') ? trimmed.slice(1) : trimmed;
|
|
321
|
-
return registry.autocomplete(stripped);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function getPlaceholder(input) {
|
|
325
|
-
return getPlaceholderText(input, registry.getSubcommands);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
return {
|
|
329
|
-
get messages() { return state.messages; },
|
|
330
|
-
theme,
|
|
331
|
-
themeState,
|
|
332
|
-
get aiMode() { return state.aiMode; },
|
|
333
|
-
get showSetup() { return state.showSetup; },
|
|
334
|
-
get setupDryRun() { return state.setupDryRun; },
|
|
335
|
-
get connectStep() { return state.connectStep; },
|
|
336
|
-
set connectStep(v) { state.connectStep = v; },
|
|
337
|
-
get connectPlatform() { return state.connectPlatform; },
|
|
338
|
-
get connectCursor() { return state.connectCursor; },
|
|
339
|
-
processInput,
|
|
340
|
-
getStatusBar,
|
|
341
|
-
exitAI,
|
|
342
|
-
exitSetup,
|
|
343
|
-
moveConnectCursor,
|
|
344
|
-
selectConnectPlatform,
|
|
345
|
-
submitConnectToken,
|
|
346
|
-
cancelConnect,
|
|
347
|
-
get githubStep() { return state.githubStep; },
|
|
348
|
-
get githubCursor() { return state.githubCursor; },
|
|
349
|
-
moveGithubCursor,
|
|
350
|
-
selectGithubItem,
|
|
351
|
-
cancelGithub,
|
|
352
|
-
submitModelToken,
|
|
353
|
-
cancelModelToken,
|
|
354
|
-
get modelStep() { return state.modelStep; },
|
|
355
|
-
get modelProvider() { return state.modelProvider; },
|
|
356
|
-
get aiLoading() { return state.aiLoading; },
|
|
357
|
-
get upgradeLoading() { return state.upgradeLoading; },
|
|
358
|
-
get confirmStep() { return state.confirmStep; },
|
|
359
|
-
confirmAction,
|
|
360
|
-
confirmCancel,
|
|
361
|
-
markSetupDone,
|
|
362
|
-
get setupInstalled() { return state.setupInstalled; },
|
|
363
|
-
getSuggestions,
|
|
364
|
-
getPlaceholder,
|
|
365
|
-
autocomplete: ac,
|
|
366
|
-
updateAutocomplete: (input) => updateAutocomplete(ac, input),
|
|
367
|
-
moveAutocompleteUp: () => moveUp(ac),
|
|
368
|
-
moveAutocompleteDown: () => moveDown(ac),
|
|
369
|
-
selectAutocompleteActive: () => selectActive(ac),
|
|
370
|
-
closeAutocomplete: () => closeAutocomplete(ac),
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
module.exports = { createAppState, VERSION, WELCOME_TEXT };
|