@crystalplatforms/adm 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +210 -0
- package/bin/adm +26 -0
- package/bin/adm-tui +13 -0
- package/package.json +62 -0
- package/scripts/completion/adm.bash +81 -0
- package/scripts/completion/adm.zsh +89 -0
- package/scripts/install.js +8 -0
- package/src/ai/knowledge.js +61 -0
- package/src/commands/connect.js +76 -0
- package/src/commands/dotfiles.js +72 -0
- package/src/commands/issue.js +80 -0
- package/src/commands/pr.js +55 -0
- package/src/commands/theme.js +103 -0
- package/src/commands/uninstall.js +72 -0
- package/src/commands/upgrade.js +114 -0
- package/src/config/index.js +32 -0
- package/src/data/extensions.json +358 -0
- package/src/installer/git-config.js +34 -0
- package/src/installer/node-installer.js +50 -0
- package/src/installer/package-manager-installer.js +31 -0
- package/src/installer/ssh-setup.js +31 -0
- package/src/installer/system-packages.js +49 -0
- package/src/installers/index.js +66 -0
- package/src/integrations/ai-backend.js +60 -0
- package/src/integrations/ai-providers/registry.js +261 -0
- package/src/integrations/github.js +107 -0
- package/src/integrations/gitlab.js +121 -0
- package/src/plugins/context.js +64 -0
- package/src/plugins/loader.js +50 -0
- package/src/plugins/runner.js +25 -0
- package/src/setup/catalog.js +13 -0
- package/src/setup/installer.js +192 -0
- package/src/setup/wizard.js +220 -0
- package/src/shell/detector.js +10 -0
- package/src/tui/app-state.js +374 -0
- package/src/tui/app.js +442 -0
- package/src/tui/autocomplete.js +57 -0
- package/src/tui/commands/registry.js +823 -0
- package/src/tui/components/SetupScreen.js +292 -0
- package/src/ui/animations.js +24 -0
- package/src/ui/spinner.js +34 -0
- package/src/ui/theme/cyberpunk.js +14 -0
- package/src/ui/theme/dark.js +14 -0
- package/src/ui/theme/forest.js +14 -0
- package/src/ui/theme/light.js +14 -0
- package/src/ui/theme/monokai.js +14 -0
- package/src/ui/theme/nord.js +14 -0
- package/src/ui/theme.js +49 -0
- package/src/utils/command-history.js +43 -0
- package/src/utils/dotfiles-sync.js +89 -0
- package/src/utils/error-handler.js +69 -0
- package/src/utils/file-conflict-handler.js +30 -0
- package/src/utils/fuzzy-search.js +20 -0
- package/src/utils/github-error-handler.js +26 -0
- package/src/utils/keychain.js +97 -0
- package/src/utils/open-url.js +15 -0
- package/src/utils/terminal-detection.js +26 -0
- package/src/utils/token-encryption.js +96 -0
|
@@ -0,0 +1,374 @@
|
|
|
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 };
|