@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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/bin/adm +26 -0
  4. package/bin/adm-tui +13 -0
  5. package/package.json +62 -0
  6. package/scripts/completion/adm.bash +81 -0
  7. package/scripts/completion/adm.zsh +89 -0
  8. package/scripts/install.js +8 -0
  9. package/src/ai/knowledge.js +61 -0
  10. package/src/commands/connect.js +76 -0
  11. package/src/commands/dotfiles.js +72 -0
  12. package/src/commands/issue.js +80 -0
  13. package/src/commands/pr.js +55 -0
  14. package/src/commands/theme.js +103 -0
  15. package/src/commands/uninstall.js +72 -0
  16. package/src/commands/upgrade.js +114 -0
  17. package/src/config/index.js +32 -0
  18. package/src/data/extensions.json +358 -0
  19. package/src/installer/git-config.js +34 -0
  20. package/src/installer/node-installer.js +50 -0
  21. package/src/installer/package-manager-installer.js +31 -0
  22. package/src/installer/ssh-setup.js +31 -0
  23. package/src/installer/system-packages.js +49 -0
  24. package/src/installers/index.js +66 -0
  25. package/src/integrations/ai-backend.js +60 -0
  26. package/src/integrations/ai-providers/registry.js +261 -0
  27. package/src/integrations/github.js +107 -0
  28. package/src/integrations/gitlab.js +121 -0
  29. package/src/plugins/context.js +64 -0
  30. package/src/plugins/loader.js +50 -0
  31. package/src/plugins/runner.js +25 -0
  32. package/src/setup/catalog.js +13 -0
  33. package/src/setup/installer.js +192 -0
  34. package/src/setup/wizard.js +220 -0
  35. package/src/shell/detector.js +10 -0
  36. package/src/tui/app-state.js +374 -0
  37. package/src/tui/app.js +442 -0
  38. package/src/tui/autocomplete.js +57 -0
  39. package/src/tui/commands/registry.js +823 -0
  40. package/src/tui/components/SetupScreen.js +292 -0
  41. package/src/ui/animations.js +24 -0
  42. package/src/ui/spinner.js +34 -0
  43. package/src/ui/theme/cyberpunk.js +14 -0
  44. package/src/ui/theme/dark.js +14 -0
  45. package/src/ui/theme/forest.js +14 -0
  46. package/src/ui/theme/light.js +14 -0
  47. package/src/ui/theme/monokai.js +14 -0
  48. package/src/ui/theme/nord.js +14 -0
  49. package/src/ui/theme.js +49 -0
  50. package/src/utils/command-history.js +43 -0
  51. package/src/utils/dotfiles-sync.js +89 -0
  52. package/src/utils/error-handler.js +69 -0
  53. package/src/utils/file-conflict-handler.js +30 -0
  54. package/src/utils/fuzzy-search.js +20 -0
  55. package/src/utils/github-error-handler.js +26 -0
  56. package/src/utils/keychain.js +97 -0
  57. package/src/utils/open-url.js +15 -0
  58. package/src/utils/terminal-detection.js +26 -0
  59. package/src/utils/token-encryption.js +96 -0
@@ -0,0 +1,13 @@
1
+ const data = require('../data/extensions.json');
2
+
3
+ function loadCatalog() {
4
+ return data.categories;
5
+ }
6
+
7
+ function getToolsByCategories(categoryIds) {
8
+ const categories = loadCatalog();
9
+ const selected = categories.filter(cat => categoryIds.includes(cat.id));
10
+ return selected.flatMap(cat => cat.tools);
11
+ }
12
+
13
+ module.exports = { loadCatalog, getToolsByCategories };
@@ -0,0 +1,192 @@
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 };
@@ -0,0 +1,220 @@
1
+ const inquirer = require('inquirer');
2
+ const chalk = require('chalk');
3
+ const ora = require('ora');
4
+ const { writeConfig, readConfig } = require('../config');
5
+ const { detectShell } = require('../shell/detector');
6
+ const { isNodeInstalled, installNode, getNodeVersion } = require('../installer/node-installer');
7
+ const { isPnpmInstalled, installPnpm } = require('../installer/package-manager-installer');
8
+ const { configureGit, getCurrentGitConfig } = require('../installer/git-config');
9
+ const { sshKeyExists, generateSshKey, getPublicKey } = require('../installer/ssh-setup');
10
+ const { detectPlatform, isBrewInstalled, installBrew, isAptInstalled, installAptPackages } = require('../installer/system-packages');
11
+
12
+ async function runSetup(options = {}) {
13
+ const dry = process.env.ADM_DRY_RUN === '1' || options.dryRun;
14
+ const shell = detectShell();
15
+
16
+ console.log(chalk.cyan('\n ADM Setup Wizard\n'));
17
+ console.log(chalk.gray(` Detected shell: ${shell}`));
18
+ console.log(chalk.gray(` Detected OS: ${detectPlatform()}\n`));
19
+
20
+ if (dry) {
21
+ return { dryRun: true, shell, platform: detectPlatform(), planned: getFullPlan() };
22
+ }
23
+
24
+ // Step 1: System package manager
25
+ const platform = detectPlatform();
26
+ if (platform === 'darwin' && !isBrewInstalled()) {
27
+ const { installBrew: wantBrew } = await inquirer.prompt([{
28
+ type: 'confirm', name: 'installBrew', message: 'Install Homebrew?', default: true,
29
+ }]);
30
+ if (wantBrew) {
31
+ const s = ora('Installing Homebrew...').start();
32
+ try { await installBrew(); s.succeed('Homebrew installed'); }
33
+ catch (e) { s.fail(`Homebrew install failed: ${e.message}`); }
34
+ }
35
+ } else if (platform === 'linux' && !isAptInstalled()) {
36
+ const { installApt } = await inquirer.prompt([{
37
+ type: 'confirm', name: 'installApt', message: 'Install base apt packages (git, curl, build-essential)?', default: true,
38
+ }]);
39
+ if (installApt) {
40
+ const s = ora('Installing apt packages...').start();
41
+ try { await installAptPackages(); s.succeed('Apt packages installed'); }
42
+ catch (e) { s.fail(`Apt install failed: ${e.message}`); }
43
+ }
44
+ }
45
+
46
+ // Step 2: Node.js
47
+ const nodeStatus = isNodeInstalled();
48
+ let nodeVersion = nodeStatus.version;
49
+ if (!nodeStatus.installed) {
50
+ const { nodeChoice } = await inquirer.prompt([{
51
+ type: 'list',
52
+ name: 'nodeChoice',
53
+ message: 'Node.js is not installed. How would you like to install it?',
54
+ choices: [
55
+ { name: 'Install latest LTS (via nvm)', value: 'lts' },
56
+ { name: 'Install specific version', value: 'custom' },
57
+ { name: 'Skip Node.js installation', value: 'skip' },
58
+ ],
59
+ default: 'lts',
60
+ }]);
61
+ let customVersion;
62
+ if (nodeChoice === 'custom') {
63
+ ({ customVersion } = await inquirer.prompt([{
64
+ type: 'input', name: 'customVersion', message: 'Enter Node.js version (e.g. 18.17.0):',
65
+ }]));
66
+ }
67
+ if (nodeChoice !== 'skip') {
68
+ const s = ora(`Installing Node.js ${nodeChoice === 'custom' ? customVersion : 'LTS'}...`).start();
69
+ try {
70
+ await installNode(nodeChoice === 'custom' ? customVersion : '--lts');
71
+ nodeVersion = getNodeVersion();
72
+ s.succeed(`Node.js installed: ${nodeVersion}`);
73
+ } catch (e) {
74
+ s.fail(`Node.js install failed: ${e.message}`);
75
+ }
76
+ }
77
+ } else {
78
+ console.log(chalk.green(` Node.js ${nodeVersion} already installed`));
79
+ }
80
+
81
+ // Step 3: Package managers
82
+ const pnpmStatus = isPnpmInstalled();
83
+ if (!pnpmStatus.installed) {
84
+ const { wantPnpm } = await inquirer.prompt([{
85
+ type: 'confirm', name: 'wantPnpm', message: 'Install pnpm?', default: true,
86
+ }]);
87
+ if (wantPnpm) {
88
+ const s = ora('Installing pnpm...').start();
89
+ try { await installPnpm(); s.succeed('pnpm installed'); }
90
+ catch (e) { s.fail(`pnpm install failed: ${e.message}`); }
91
+ }
92
+ } else {
93
+ console.log(chalk.green(` pnpm ${pnpmStatus.version} already installed`));
94
+ }
95
+
96
+ // Step 4: Optional tools
97
+ const { tools } = await inquirer.prompt([{
98
+ type: 'checkbox',
99
+ name: 'tools',
100
+ message: 'Select optional tools to install:',
101
+ choices: [
102
+ { name: 'Vite', value: 'vite', checked: false },
103
+ { name: 'esbuild', value: 'esbuild', checked: false },
104
+ { name: 'TypeScript (tsc)', value: 'typescript', checked: false },
105
+ { name: 'Docker (via system pkg)', value: 'docker', checked: false },
106
+ { name: 'GitHub CLI (gh)', value: 'gh', checked: false },
107
+ ],
108
+ }]);
109
+ if (tools.length > 0) {
110
+ const s = ora(`Installing: ${tools.join(', ')}...`).start();
111
+ try {
112
+ for (const tool of tools) {
113
+ if (['vite', 'esbuild', 'typescript'].includes(tool)) {
114
+ const { execSync } = require('child_process');
115
+ execSync(`npm install -g ${tool}`, { stdio: 'pipe' });
116
+ } else if (tool === 'docker') {
117
+ const { installSystemPackage } = require('../installer/system-packages');
118
+ await installSystemPackage('docker.io');
119
+ } else if (tool === 'gh') {
120
+ const { installSystemPackage } = require('../installer/system-packages');
121
+ await installSystemPackage('gh');
122
+ }
123
+ }
124
+ s.succeed(`Installed: ${tools.join(', ')}`);
125
+ } catch (e) {
126
+ s.fail(`Tool install failed: ${e.message}`);
127
+ }
128
+ }
129
+
130
+ // Step 5: Git config
131
+ const currentGit = getCurrentGitConfig();
132
+ const gitAnswers = await inquirer.prompt([
133
+ {
134
+ type: 'input', name: 'name', message: 'Git user.name:',
135
+ default: currentGit.name || undefined,
136
+ },
137
+ {
138
+ type: 'input', name: 'email', message: 'Git user.email:',
139
+ default: currentGit.email || undefined,
140
+ },
141
+ {
142
+ type: 'confirm', name: 'gpgsign', message: 'Enable GPG commit signing?',
143
+ default: currentGit.gpgsign === 'true',
144
+ },
145
+ ]);
146
+ await configureGit(gitAnswers);
147
+
148
+ // Step 6: SSH key
149
+ if (!sshKeyExists()) {
150
+ const { wantSsh } = await inquirer.prompt([{
151
+ type: 'confirm', name: 'wantSsh', message: 'Generate SSH key (ED25519)?', default: true,
152
+ }]);
153
+ if (wantSsh) {
154
+ const result = generateSshKey(gitAnswers.email || 'adm@local');
155
+ if (result.skipped) {
156
+ console.log(chalk.yellow(` ${result.reason}`));
157
+ } else {
158
+ console.log(chalk.green(` SSH key generated: ${result.keyPath}`));
159
+ console.log(chalk.gray(` Public key: ${getPublicKey(result.keyPath).substring(0, 60)}...`));
160
+ }
161
+ }
162
+ } else {
163
+ console.log(chalk.green(' SSH key already exists'));
164
+ }
165
+
166
+ // Step 7: Dotfiles (optional)
167
+ const { dotfilesUrl } = await inquirer.prompt([{
168
+ type: 'input', name: 'dotfilesUrl', message: 'Dotfiles repo URL (leave empty to skip):',
169
+ }]);
170
+ if (dotfilesUrl) {
171
+ const s = ora('Cloning dotfiles...').start();
172
+ try {
173
+ const { execSync } = require('child_process');
174
+ const dotfilesDir = require('path').join(require('os').homedir(), '.adm', 'dotfiles');
175
+ execSync(`git clone "${dotfilesUrl}" "${dotfilesDir}"`, { stdio: 'pipe' });
176
+ s.succeed(`Dotfiles cloned to ${dotfilesDir}`);
177
+ } catch (e) {
178
+ s.fail(`Dotfiles clone failed: ${e.message}`);
179
+ }
180
+ }
181
+
182
+ // Step 8: Assistant mode
183
+ const { assistant } = await inquirer.prompt([{
184
+ type: 'confirm', name: 'assistant', message: 'Enable assistant mode?', default: false,
185
+ }]);
186
+
187
+ // Save config
188
+ const config = {
189
+ installed: true,
190
+ setupCompletedAt: new Date().toISOString(),
191
+ shell,
192
+ platform,
193
+ nodeVersion,
194
+ pnpm: pnpmStatus.installed || (await isPnpmInstalled()).installed,
195
+ tools,
196
+ git: { name: gitAnswers.name, email: gitAnswers.email, gpgsign: gitAnswers.gpgsign },
197
+ assistant,
198
+ dotfiles: dotfilesUrl || null,
199
+ };
200
+ await writeConfig(config);
201
+
202
+ console.log(chalk.cyan('\n Setup complete! Config saved to ~/.adm/config.json\n'));
203
+ return config;
204
+ }
205
+
206
+ function getFullPlan() {
207
+ return [
208
+ 'Detect and install system package manager (brew/apt)',
209
+ 'Install Node.js via nvm',
210
+ 'Install pnpm',
211
+ 'Install optional tools (Vite, esbuild, TypeScript, Docker, gh)',
212
+ 'Configure git (user.name, user.email, gpgsign)',
213
+ 'Generate SSH key (ED25519)',
214
+ 'Clone dotfiles repo',
215
+ 'Enable assistant mode',
216
+ 'Save config to ~/.adm/config.json',
217
+ ];
218
+ }
219
+
220
+ module.exports = { runSetup };
@@ -0,0 +1,10 @@
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 };