@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.
Files changed (89) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +154 -161
  3. package/bin/adm +18 -21
  4. package/package.json +9 -22
  5. package/scripts/completion/adm.bash +12 -55
  6. package/scripts/completion/adm.zsh +21 -58
  7. package/scripts/install.js +8 -8
  8. package/src/commands/config.js +66 -0
  9. package/src/commands/dotfiles.js +49 -32
  10. package/src/commands/feedback.js +12 -0
  11. package/src/commands/help.js +33 -0
  12. package/src/commands/uninstall.js +73 -54
  13. package/src/commands/version.js +7 -0
  14. package/src/commands.js +26 -0
  15. package/src/config/index.js +32 -32
  16. package/src/data/extensions.json +358 -358
  17. package/src/engines.js +38 -0
  18. package/src/install/executor.js +263 -0
  19. package/src/install/hints.js +94 -0
  20. package/src/install/npm-prefix.js +110 -0
  21. package/src/install/plan.js +75 -0
  22. package/src/install/report.js +46 -0
  23. package/src/install/run.js +88 -0
  24. package/src/install/shell-paths.js +129 -0
  25. package/src/legacy-cleanup.js +96 -0
  26. package/src/pkg/builders.js +220 -0
  27. package/src/pkg/managers.js +59 -0
  28. package/src/pkg/package-names.js +229 -0
  29. package/src/pkg/script-urls.js +100 -0
  30. package/src/platform/index.js +129 -0
  31. package/src/platform/os-release.js +27 -0
  32. package/src/platform/shell.js +111 -0
  33. package/src/reports.js +37 -0
  34. package/src/router.js +157 -0
  35. package/src/setup/catalog.js +13 -13
  36. package/src/setup/git-step.js +128 -0
  37. package/src/setup/loader.js +108 -0
  38. package/src/setup/prompts.js +259 -0
  39. package/src/setup/registry.js +48 -0
  40. package/src/setup/spinner.js +67 -0
  41. package/src/setup/wizard.js +319 -195
  42. package/src/upgrade/index.js +499 -0
  43. package/src/utils/closest-match.js +36 -0
  44. package/src/utils/dotfiles-sync.js +89 -89
  45. package/src/utils/file-conflict-handler.js +30 -30
  46. package/bin/adm-tui +0 -13
  47. package/src/ai/knowledge.js +0 -61
  48. package/src/commands/connect.js +0 -76
  49. package/src/commands/issue.js +0 -80
  50. package/src/commands/pr.js +0 -55
  51. package/src/commands/theme.js +0 -103
  52. package/src/commands/upgrade.js +0 -114
  53. package/src/installer/git-config.js +0 -34
  54. package/src/installer/node-installer.js +0 -50
  55. package/src/installer/package-manager-installer.js +0 -31
  56. package/src/installer/ssh-setup.js +0 -31
  57. package/src/installer/system-packages.js +0 -49
  58. package/src/installers/index.js +0 -66
  59. package/src/integrations/ai-backend.js +0 -60
  60. package/src/integrations/ai-providers/registry.js +0 -261
  61. package/src/integrations/github.js +0 -107
  62. package/src/integrations/gitlab.js +0 -121
  63. package/src/plugins/context.js +0 -64
  64. package/src/plugins/loader.js +0 -50
  65. package/src/plugins/runner.js +0 -25
  66. package/src/setup/installer.js +0 -192
  67. package/src/shell/detector.js +0 -10
  68. package/src/tui/app-state.js +0 -374
  69. package/src/tui/app.js +0 -442
  70. package/src/tui/autocomplete.js +0 -57
  71. package/src/tui/commands/registry.js +0 -823
  72. package/src/tui/components/SetupScreen.js +0 -292
  73. package/src/ui/animations.js +0 -24
  74. package/src/ui/spinner.js +0 -34
  75. package/src/ui/theme/cyberpunk.js +0 -14
  76. package/src/ui/theme/dark.js +0 -14
  77. package/src/ui/theme/forest.js +0 -14
  78. package/src/ui/theme/light.js +0 -14
  79. package/src/ui/theme/monokai.js +0 -14
  80. package/src/ui/theme/nord.js +0 -14
  81. package/src/ui/theme.js +0 -49
  82. package/src/utils/command-history.js +0 -43
  83. package/src/utils/error-handler.js +0 -69
  84. package/src/utils/fuzzy-search.js +0 -20
  85. package/src/utils/github-error-handler.js +0 -26
  86. package/src/utils/keychain.js +0 -97
  87. package/src/utils/open-url.js +0 -15
  88. package/src/utils/terminal-detection.js +0 -26
  89. package/src/utils/token-encryption.js +0 -96
@@ -1,27 +1,17 @@
1
1
  #compdef adm
2
+ # v0.3 CLI surface — src/commands.js is the source of truth
2
3
 
3
4
  _adm() {
4
- local -a commands subcommands opts
5
+ local -a commands
5
6
 
6
7
  commands=(
7
- 'help:Show command reference'
8
- 'exit:Exit ADM'
9
- 'clear:Clear message history'
10
- 'theme:List or switch themes'
11
- 'config:Show current configuration'
12
- 'status:Show git status'
13
- 'ai:Toggle AI mode or ask a question'
14
- 'model:Show or switch AI provider'
15
- 'setup:Launch extension setup wizard'
16
- 'connect:Connect to GitHub or GitLab'
17
- 'pr:Pull request operations'
18
- 'mr:Merge request operations (GitLab)'
19
- 'issue:List issues from connected platform'
20
- 'commit:Commit subcommands'
21
- 'clock:Show ASCII clock'
22
- 'dotfiles:Sync dotfiles from repo'
23
- 'uninstall:Remove ADM CLI config'
24
- 'plugins:List loaded plugins'
8
+ 'help:Show available commands'
9
+ '-v:Print ADM version'
10
+ 'feedback:Show how to report bugs and ideas'
11
+ 'config:Show ADM config and detected environment'
12
+ 'uninstall:Remove ADM data (~/.adm) and shell PATH entries'
13
+ 'upgrade:Self-update ADM (alias: update)'
14
+ 'dotfiles:Sync dotfiles from a git repo'
25
15
  )
26
16
 
27
17
  _arguments -C \
@@ -34,53 +24,26 @@ _adm() {
34
24
  ;;
35
25
  args)
36
26
  case $words[1] in
37
- setup)
38
- _arguments '--dry-run[Preview planned actions without executing]'
27
+ config)
28
+ _arguments '--json[Machine-readable output]'
39
29
  ;;
40
- connect)
41
- local -a connect_subs
42
- connect_subs=('github:Connect GitHub account' 'gitlab:Connect GitLab account' 'list:List connected services' 'disconnect:Disconnect a service')
43
- if (( CURRENT == 2 )); then
44
- _describe 'subcommand' connect_subs
45
- else
46
- case $words[2] in
47
- github|gitlab) _arguments '--token[Access token]:token:' ;;
48
- esac
49
- fi
50
- ;;
51
- pr)
52
- local -a pr_subs
53
- pr_subs=('list:List open pull requests' 'draft:Create a draft PR' 'comment:Comment on a pull request')
54
- if (( CURRENT == 2 )); then
55
- _describe 'subcommand' pr_subs
56
- fi
57
- ;;
58
- mr)
59
- local -a mr_subs
60
- mr_subs=('list:List open merge requests' 'draft:Create a draft MR' 'comment:Comment on a merge request')
61
- if (( CURRENT == 2 )); then
62
- _describe 'subcommand' mr_subs
63
- fi
64
- ;;
65
- issue)
66
- _arguments '1:subcommand:(list)'
67
- ;;
68
- commit)
69
- _arguments '1:subcommand:(suggest)'
30
+ upgrade|update)
31
+ _arguments \
32
+ '--check[Only check whether a newer version exists]' \
33
+ '--dry-run[Print the upgrade plan without changing anything]'
70
34
  ;;
71
35
  dotfiles)
72
36
  local -a dotfiles_subs
73
- dotfiles_subs=('sync:Clone/pull dotfiles repo and symlink files')
37
+ dotfiles_subs=('sync:Sync dotfiles from a git repo into your home dir')
74
38
  if (( CURRENT == 2 )); then
75
39
  _describe 'subcommand' dotfiles_subs
40
+ else
41
+ _arguments \
42
+ '--repo[Dotfiles git repo URL]:url:' \
43
+ '--copy[Copy files instead of symlinking]' \
44
+ '--only[Comma-separated file list]:names:'
76
45
  fi
77
46
  ;;
78
- theme)
79
- _arguments '1:theme:(dark light cyberpunk nord forest monokai)'
80
- ;;
81
- model)
82
- _arguments '1:provider:(glm-free glm-pro openai anthropic ollama list)'
83
- ;;
84
47
  esac
85
48
  ;;
86
49
  esac
@@ -1,8 +1,8 @@
1
- #!/usr/bin/env node
2
- const chalk = require('chalk');
3
- console.log('');
4
- console.log(chalk.green(' ADM CLI installed successfully!'));
5
- console.log('');
6
- console.log(' Run ' + chalk.bold('adm') + ' to launch the TUI.');
7
- console.log(' Type ' + chalk.bold('/download') + ' inside ADM to set up your dev environment.');
8
- console.log('');
1
+ #!/usr/bin/env node
2
+ const chalk = require('chalk');
3
+ console.log('');
4
+ console.log(chalk.green(' ADM CLI installed successfully!'));
5
+ console.log('');
6
+ console.log(' Run ' + chalk.bold('adm') + ' to launch the TUI.');
7
+ console.log(' Type ' + chalk.bold('/download') + ' inside ADM to set up your dev environment.');
8
+ console.log('');
@@ -0,0 +1,66 @@
1
+ const { detectEnvironment, formatEnvironment } = require('../platform');
2
+ const { configDir, readConfig } = require('../config');
3
+ const { getVersion } = require('./version');
4
+
5
+ // `adm config` — human-readable view.
6
+ // `adm config --json` — machine-readable view with a frozen key set:
7
+ // version, configDir, configFile, os, distro, distroVersion,
8
+ // manager, arch, isWSL, shell, rcFile
9
+ async function runConfig({ json, log, err } = {}) {
10
+ log = log || (s => process.stdout.write(s + '\n'));
11
+ err = err || (s => process.stderr.write(s + '\n'));
12
+
13
+ let saved = {};
14
+ try {
15
+ saved = await readConfig();
16
+ } catch {
17
+ saved = {};
18
+ }
19
+
20
+ const dir = configDir();
21
+ const file = require('path').join(dir, 'config.json');
22
+ const env = detectEnvironment();
23
+
24
+ if (json) {
25
+ log(JSON.stringify({
26
+ version: getVersion(),
27
+ configDir: dir,
28
+ configFile: file,
29
+ os: env.os,
30
+ distro: env.distro,
31
+ distroVersion: env.distroVersion,
32
+ manager: env.manager,
33
+ arch: env.arch,
34
+ isWSL: env.isWSL,
35
+ shell: env.shell,
36
+ rcFile: env.rcFile,
37
+ }, null, 2));
38
+ return 0;
39
+ }
40
+
41
+ const lines = [
42
+ `ADM v${getVersion()}`,
43
+ '',
44
+ 'Paths:',
45
+ ` Config dir: ${dir}`,
46
+ ` Config file: ${file}`,
47
+ '',
48
+ 'Environment:',
49
+ formatEnvironment(env),
50
+ '',
51
+ 'Saved config:',
52
+ ];
53
+ const keys = Object.keys(saved);
54
+ if (keys.length === 0) {
55
+ lines.push(' (empty)');
56
+ } else {
57
+ for (const key of keys) {
58
+ const value = JSON.stringify(saved[key]);
59
+ lines.push(` ${key}: ${value.length > 80 ? value.slice(0, 77) + '...' : value}`);
60
+ }
61
+ }
62
+ log(lines.join('\n'));
63
+ return 0;
64
+ }
65
+
66
+ module.exports = { runConfig };
@@ -2,71 +2,88 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const chalk = require('chalk');
4
4
  const ora = require('ora');
5
- const child_process = require('child_process');
6
- const { readConfig, ensureConfigDir } = require('../config');
5
+ const { readConfig } = require('../config');
7
6
  const { syncDotfiles } = require('../utils/dotfiles-sync');
8
7
 
9
- /**
10
- * /dotfiles sync — clones/pulls dotfiles repo and synchronizes files
11
- *
12
- * Options:
13
- * repo: string — dotfiles repo URL (overrides config)
14
- * copy: boolean — copy instead of symlinking
15
- * only: string[] — file filter (e.g. ['bashrc', 'zshrc'])
16
- */
17
- async function syncDotfilesCommand(options = {}) {
8
+ // `adm dotfiles sync [--repo <url>] [--copy] [--only <name,name>]`
9
+ // Port of the old logic: clones/pulls the dotfiles repo into ~/.adm/dotfiles,
10
+ // then symlinks (or copies) known files into the home directory.
11
+ async function runDotfilesSync(flags = [], { log, err } = {}) {
12
+ log = log || (s => process.stdout.write(s + '\n'));
13
+ err = err || (s => process.stderr.write(s + '\n'));
18
14
  const configDir = process.env.ADM_CONFIG_DIR || path.join(require('os').homedir(), '.adm');
19
15
  const dotfilesDir = path.join(configDir, 'dotfiles');
20
16
 
21
- // Get repo URL
17
+ // ── parse flags ──────────────────────────────────────────────
18
+ const options = {};
19
+ const bad = [];
20
+ for (let i = 0; i < flags.length; i++) {
21
+ const f = flags[i];
22
+ if (f === '--help' || f === '-h') {
23
+ log('Usage: adm dotfiles sync [--repo <url>] [--copy] [--only <name,name>]');
24
+ log('');
25
+ log(' --repo <url> dotfiles git repo (or set dotfiles.repo in the config)');
26
+ log(' --copy copy files instead of symlinking');
27
+ log(' --only <name,name> sync only these files (e.g. --only bashrc,zshrc)');
28
+ return 0;
29
+ }
30
+ if (f === '--repo') options.repo = flags[++i];
31
+ else if (f === '--copy') options.copy = true;
32
+ else if (f === '--only') options.only = String(flags[++i] || '').split(',').map(s => s.trim()).filter(Boolean);
33
+ else bad.push(f);
34
+ }
35
+ if (bad.length > 0) {
36
+ err(`Unknown flags for "dotfiles sync": ${bad.join(', ')}\nValid flags: --repo <url>, --copy, --only <name,name>`);
37
+ return 1;
38
+ }
39
+
40
+ // ── resolve repo URL ─────────────────────────────────────────
22
41
  let repoUrl = options.repo;
23
42
  if (!repoUrl) {
24
43
  const config = await readConfig();
25
44
  repoUrl = config?.dotfiles?.repo;
26
45
  }
27
46
  if (!repoUrl) {
28
- throw new Error('No dotfiles repo URL. Use --repo or set in config (dotfiles.repo).');
47
+ err('No dotfiles repo URL. Pass --repo <url> or set it in the config (dotfiles.repo).\nExample: adm dotfiles sync --repo https://github.com/you/dotfiles');
48
+ return 1;
29
49
  }
30
50
 
31
- // Clone or pull repo
51
+ // ── clone or pull ────────────────────────────────────────────
52
+ const { execFileSync } = require('child_process');
32
53
  const s = ora('Syncing dotfiles...').start();
33
54
  try {
34
55
  if (fs.existsSync(path.join(dotfilesDir, '.git'))) {
35
56
  s.text = 'Pulling updates...';
36
- child_process.execSync(`git pull`, { cwd: dotfilesDir, stdio: 'pipe' });
57
+ execFileSync('git', ['pull'], { cwd: dotfilesDir, stdio: 'pipe' });
37
58
  } else {
38
- // Remove empty directory if it exists
39
59
  if (fs.existsSync(dotfilesDir)) {
40
60
  fs.rmSync(dotfilesDir, { recursive: true, force: true });
41
61
  }
42
62
  s.text = 'Cloning dotfiles repo...';
43
- child_process.execSync(`git clone ${repoUrl} ${dotfilesDir}`, { stdio: 'pipe' });
63
+ fs.mkdirSync(configDir, { recursive: true });
64
+ execFileSync('git', ['clone', repoUrl, dotfilesDir], { stdio: 'pipe' });
44
65
  }
45
66
  s.succeed('Dotfiles repo ready');
46
- } catch (err) {
47
- s.fail(`Git error: ${err.message}`);
48
- throw err;
67
+ } catch (e) {
68
+ s.fail(`Git error: ${e.message}`);
69
+ return 2;
49
70
  }
50
71
 
51
- // Sync files
52
- console.log(chalk.bold('\n Syncing files:'));
72
+ // ── sync files ───────────────────────────────────────────────
73
+ log(chalk.bold('\n Syncing files:'));
53
74
  const result = await syncDotfiles({
54
75
  repo: repoUrl,
55
76
  copy: options.copy,
56
77
  only: options.only,
57
78
  });
58
79
 
59
- // Summary
60
80
  const total = result.symlinked.length + result.copied.length;
61
- console.log(chalk.bold(`\n Done: ${total} files synced`));
62
- if (result.skipped.length > 0) {
63
- console.log(chalk.yellow(` Skipped: ${result.skipped.join(', ')}`));
64
- }
65
- if (result.errors.length > 0) {
66
- console.log(chalk.red(` Errors: ${result.errors.map(e => e.file).join(', ')}`));
67
- }
81
+ log(chalk.bold(`\n Done: ${total} file${total === 1 ? '' : 's'} synced`));
82
+ if (result.skipped.length > 0) log(chalk.yellow(` Skipped: ${result.skipped.join(', ')}`));
83
+ if (result.backedUp.length > 0) log(chalk.blue(` Backed up: ${result.backedUp.join(', ')}`));
84
+ if (result.errors.length > 0) log(chalk.red(` Errors: ${result.errors.map(e => e.file).join(', ')}`));
68
85
 
69
- return result;
86
+ return result.errors.length > 0 ? 2 : 0;
70
87
  }
71
88
 
72
- module.exports = { syncDotfilesCommand };
89
+ module.exports = { runDotfilesSync };
@@ -0,0 +1,12 @@
1
+ const REPO_ISSUES = 'https://github.com/CrystalPlatforms/ADM-CLI/issues';
2
+
3
+ function runFeedback({ log } = {}) {
4
+ log = log || (s => process.stdout.write(s + '\n'));
5
+ log('Found a bug or an idea? Open an issue:');
6
+ log(` ${REPO_ISSUES}`);
7
+ log('');
8
+ log('Include: OS + distro, shell, the command you ran, and the full output.');
9
+ return 0;
10
+ }
11
+
12
+ module.exports = { runFeedback, REPO_ISSUES };
@@ -0,0 +1,33 @@
1
+ const { COMMANDS, AUTOMATION_HINTS } = require('../commands');
2
+
3
+ // Renders one " <name> <summary>" line per COMMANDS table entry.
4
+ // This is the only place help text is produced — the table is the single
5
+ // source of truth (anti-drift).
6
+ function renderCommandLines() {
7
+ const width = Math.max(...COMMANDS.map(c => c.name.length));
8
+ const flagsWidth = Math.max(...COMMANDS.map(c => c.flags ? c.flags.join('|').length : 0));
9
+ const labelWidth = width + (flagsWidth > 0 ? flagsWidth + 3 : 0);
10
+ return COMMANDS.map(c => {
11
+ const flags = c.flags ? ` [${c.flags.join('|')}]` : '';
12
+ return ` ${(c.name + flags).padEnd(labelWidth)} ${c.summary}`;
13
+ });
14
+ }
15
+
16
+ function showHelp() {
17
+ const lines = [
18
+ 'ADM — lightweight, multi-platform developer environment installer',
19
+ '',
20
+ 'Usage:',
21
+ ' adm <command> [flags]',
22
+ '',
23
+ 'Commands:',
24
+ ...renderCommandLines(),
25
+ '',
26
+ ...AUTOMATION_HINTS,
27
+ '',
28
+ 'Docs: https://github.com/CrystalPlatforms/ADM-CLI#readme',
29
+ ];
30
+ return lines.join('\n');
31
+ }
32
+
33
+ module.exports = { showHelp, renderCommandLines };
@@ -1,72 +1,91 @@
1
- const fs = require('fs').promises;
1
+ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
- const { spawn } = require('child_process');
5
- const chalk = require('chalk');
4
+ const { saveReport } = require('../reports');
5
+ const { getVersion } = require('../commands/version');
6
6
 
7
- async function uninstall() {
8
- const configDir = process.env.ADM_CONFIG_DIR || path.join(os.homedir(), '.adm');
7
+ // Removes ADM's footprint:
8
+ // 1. the config dir (~/.adm, or ADM_CONFIG_DIR)
9
+ // 2. PATH entries ADM wrote into shell rc files (marker lines)
10
+ // Idempotent by contract: removing nothing still exits 0 ("already removed").
11
+ const RC_FILES = ['.bashrc', '.bash_profile', '.zprofile', '.zshrc', '.profile'];
12
+ const RC_MARKERS = ['# adm-cli', 'adm completion', 'adm-cli npm path'];
9
13
 
10
- console.log(chalk.yellow(' Uninstalling ADM CLI...\n'));
11
-
12
- // Remove config directory
14
+ function cleanRcFile(rcPath) {
15
+ let content;
13
16
  try {
14
- await fs.rm(configDir, { recursive: true, force: true });
15
- console.log(chalk.green(` Removed config directory: ${configDir}`));
16
- } catch (err) {
17
- if (err.code !== 'ENOENT') {
18
- console.log(chalk.red(` Failed to remove config: ${err.message}`));
17
+ content = fs.readFileSync(rcPath, 'utf8');
18
+ } catch {
19
+ return { cleaned: false, lines: 0 };
20
+ }
21
+ const lines = content.split('\n');
22
+ const kept = [];
23
+ let removed = 0;
24
+ for (let i = 0; i < lines.length; i++) {
25
+ if (RC_MARKERS.some(m => lines[i].includes(m))) {
26
+ removed++;
27
+ // The PATH line ADM appends right after its marker goes too —
28
+ // leave zero trace (v0.3 installer layout).
29
+ if (lines[i + 1] && /^\s*export\s+PATH=/.test(lines[i + 1])) {
30
+ i++;
31
+ removed++;
32
+ }
33
+ continue;
19
34
  }
35
+ kept.push(lines[i]);
20
36
  }
37
+ if (removed === 0) {
38
+ return { cleaned: false, lines: 0 };
39
+ }
40
+ fs.writeFileSync(rcPath, kept.join('\n'), 'utf8');
41
+ return { cleaned: true, lines: removed };
42
+ }
21
43
 
22
- // Uninstall npm package
23
- await new Promise((resolve) => {
24
- const child = spawn('npm', ['uninstall', '-g', '@crystalgames/adm'], { stdio: 'pipe' });
25
- child.on('close', (code) => {
26
- if (code === 0) {
27
- console.log(chalk.green(' Removed npm package: @crystalgames/adm'));
28
- } else {
29
- console.log(chalk.yellow(' npm uninstall failed — package may have been installed differently'));
30
- }
31
- resolve();
32
- });
33
- });
44
+ async function runUninstall({ log, err, configDir: cfgDir, home } = {}) {
45
+ log = log || (s => process.stdout.write(s + '\n'));
46
+ err = err || (s => process.stderr.write(s + '\n'));
47
+ const dir = cfgDir || process.env.ADM_CONFIG_DIR || path.join(home || os.homedir(), '.adm');
48
+ const homeDir = home || os.homedir();
34
49
 
35
- // Remove binary (curl-installed)
36
- const binaryPath = '/usr/local/bin/adm';
37
- try {
38
- await fs.rm(binaryPath, { force: true });
39
- console.log(chalk.green(` Removed binary: ${binaryPath}`));
40
- } catch (err) {
41
- if (err.code !== 'ENOENT') {
42
- console.log(chalk.yellow(` Could not remove binary: ${err.message}`));
50
+ const dirExisted = fs.existsSync(dir);
51
+ if (dirExisted) {
52
+ try {
53
+ fs.rmSync(dir, { recursive: true, force: true });
54
+ } catch (e) {
55
+ err(`ADM uninstall: failed to remove ${dir}: ${e.message}`);
56
+ return 2;
43
57
  }
44
58
  }
45
59
 
46
- // Remove shell completions references
47
- const shell = process.env.SHELL || '';
48
- const rcFiles = [];
49
- if (shell.includes('bash')) {
50
- rcFiles.push(path.join(os.homedir(), '.bashrc'), path.join(os.homedir(), '.bash_profile'));
51
- } else if (shell.includes('zsh')) {
52
- rcFiles.push(path.join(os.homedir(), '.zshrc'));
60
+ const cleanedRcs = [];
61
+ for (const rc of RC_FILES) {
62
+ const result = cleanRcFile(path.join(homeDir, rc));
63
+ if (result.cleaned) cleanedRcs.push(`${rc} (${result.lines} line${result.lines === 1 ? '' : 's'})`);
53
64
  }
54
65
 
55
- for (const rc of rcFiles) {
56
- try {
57
- let content = await fs.readFile(rc, 'utf8');
58
- const cleaned = content.split('\n').filter(
59
- line => !line.includes('adm completion') && !line.includes('source.*adm'),
60
- ).join('\n');
61
- if (content !== cleaned) {
62
- await fs.writeFile(rc, cleaned, 'utf8');
63
- console.log(chalk.green(` Cleaned ADM references from ${rc}`));
64
- }
65
- } catch { /* skip missing rc files */ }
66
+ if (dirExisted) {
67
+ log(`Removed ${dir}`);
68
+ } else {
69
+ log(`${dir} already removed`);
70
+ }
71
+ if (cleanedRcs.length > 0) {
72
+ for (const rc of cleanedRcs) log(`Cleaned PATH entries from ~/${rc}`);
66
73
  }
67
74
 
68
- console.log(chalk.green('\n ADM CLI fully uninstalled.'));
69
- return true;
75
+ log('ADM data removed. To uninstall the program itself, remove the npm package: npm rm -g @crystalplatforms/adm');
76
+
77
+ // Persist the uninstall report (owner decision: all reports are saved) —
78
+ // but the config dir is GONE by design, so the report lands in the OS
79
+ // temp dir instead of resurrecting ~/.adm.
80
+ const body = [
81
+ `ADM uninstall report — ${new Date().toISOString()}`,
82
+ `ADM ${getVersion()}`,
83
+ `config dir: ${dirExisted ? `removed (${dir})` : `already removed (${dir})`}`,
84
+ cleanedRcs.length ? `cleaned rc entries: ${cleanedRcs.join(', ')}` : 'no rc entries cleaned',
85
+ ].join('\n') + '\n';
86
+ const saved = saveReport('uninstall', body, { configDir: os.tmpdir() });
87
+ if (saved.file) log(`Uninstall report saved: ${saved.file}`);
88
+ return 0;
70
89
  }
71
90
 
72
- module.exports = { uninstall };
91
+ module.exports = { runUninstall, RC_FILES, RC_MARKERS };
@@ -0,0 +1,7 @@
1
+ const pkg = require('../../package.json');
2
+
3
+ function getVersion() {
4
+ return pkg.version;
5
+ }
6
+
7
+ module.exports = { getVersion };
@@ -0,0 +1,26 @@
1
+ // ── Single source of truth for the CLI surface (frozen contract) ──
2
+ // Exit codes: 0 = success (incl. "nothing to do")
3
+ // 1 = usage error (message enumerates the valid set)
4
+ // 2 = execution failure
5
+ const COMMANDS = [
6
+ { name: 'help', aliases: ['--help', '-h'], summary: 'Show available commands' },
7
+ { name: '-v', aliases: ['--version'], summary: 'Print ADM version' },
8
+ { name: 'feedback', summary: 'Show how to report bugs and ideas' },
9
+ { name: 'config', summary: 'Show ADM config and detected environment', flags: ['--json'] },
10
+ { name: 'uninstall', summary: 'Remove ADM data (~/.adm) and shell PATH entries' },
11
+ { name: 'upgrade', aliases: ['update'], summary: 'Self-update ADM (bundle: atomic swap · npm: prints the npm command)',
12
+ flags: ['--check', '--dry-run'] },
13
+ { name: 'dotfiles sync', summary: 'Sync dotfiles from a git repo into your home dir',
14
+ usage: 'adm dotfiles sync [--repo <url>] [--copy] [--only <name,name>]' },
15
+ ];
16
+
17
+ const AUTOMATION_HINTS = [
18
+ 'Automation:',
19
+ ' ADM_TOOLS=<id,id> adm install the given tools (no prompts)',
20
+ ' ADM_DRY_RUN=1 ADM_TOOLS=<ids> adm print the install plan only (no changes)',
21
+ ...(process.platform === 'win32' ? [
22
+ ' PowerShell: $env:ADM_TOOLS="typescript,node"; adm (then: $env:ADM_TOOLS=$null)',
23
+ ] : []),
24
+ ];
25
+
26
+ module.exports = { COMMANDS, AUTOMATION_HINTS };
@@ -1,32 +1,32 @@
1
- const fs = require('fs').promises;
2
- const path = require('path');
3
- const os = require('os');
4
-
5
- function configDir() {
6
- return process.env.ADM_CONFIG_DIR || path.join(os.homedir(), '.adm');
7
- }
8
-
9
- async function ensureConfigDir(dir) {
10
- await fs.mkdir(dir, { recursive: true });
11
- }
12
-
13
- async function readConfig() {
14
- const dir = configDir();
15
- const file = path.join(dir, 'config.json');
16
- try {
17
- const data = await fs.readFile(file, 'utf8');
18
- return JSON.parse(data);
19
- } catch (err) {
20
- if (err.code === 'ENOENT') return {};
21
- throw err;
22
- }
23
- }
24
-
25
- async function writeConfig(obj) {
26
- const dir = configDir();
27
- await ensureConfigDir(dir);
28
- const file = path.join(dir, 'config.json');
29
- await fs.writeFile(file, JSON.stringify(obj, null, 2), 'utf8');
30
- }
31
-
32
- module.exports = { configDir, readConfig, writeConfig, ensureConfigDir };
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ function configDir() {
6
+ return process.env.ADM_CONFIG_DIR || path.join(os.homedir(), '.adm');
7
+ }
8
+
9
+ async function ensureConfigDir(dir) {
10
+ await fs.mkdir(dir, { recursive: true });
11
+ }
12
+
13
+ async function readConfig(dirOverride) {
14
+ const dir = dirOverride || configDir();
15
+ const file = path.join(dir, 'config.json');
16
+ try {
17
+ const data = await fs.readFile(file, 'utf8');
18
+ return JSON.parse(data);
19
+ } catch (err) {
20
+ if (err.code === 'ENOENT') return {};
21
+ throw err;
22
+ }
23
+ }
24
+
25
+ async function writeConfig(obj, dirOverride) {
26
+ const dir = dirOverride || configDir();
27
+ await ensureConfigDir(dir);
28
+ const file = path.join(dir, 'config.json');
29
+ await fs.writeFile(file, JSON.stringify(obj, null, 2), 'utf8');
30
+ }
31
+
32
+ module.exports = { configDir, readConfig, writeConfig, ensureConfigDir };