@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,72 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const ora = require('ora');
|
|
5
|
+
const child_process = require('child_process');
|
|
6
|
+
const { readConfig, ensureConfigDir } = require('../config');
|
|
7
|
+
const { syncDotfiles } = require('../utils/dotfiles-sync');
|
|
8
|
+
|
|
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 = {}) {
|
|
18
|
+
const configDir = process.env.ADM_CONFIG_DIR || path.join(require('os').homedir(), '.adm');
|
|
19
|
+
const dotfilesDir = path.join(configDir, 'dotfiles');
|
|
20
|
+
|
|
21
|
+
// Get repo URL
|
|
22
|
+
let repoUrl = options.repo;
|
|
23
|
+
if (!repoUrl) {
|
|
24
|
+
const config = await readConfig();
|
|
25
|
+
repoUrl = config?.dotfiles?.repo;
|
|
26
|
+
}
|
|
27
|
+
if (!repoUrl) {
|
|
28
|
+
throw new Error('No dotfiles repo URL. Use --repo or set in config (dotfiles.repo).');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Clone or pull repo
|
|
32
|
+
const s = ora('Syncing dotfiles...').start();
|
|
33
|
+
try {
|
|
34
|
+
if (fs.existsSync(path.join(dotfilesDir, '.git'))) {
|
|
35
|
+
s.text = 'Pulling updates...';
|
|
36
|
+
child_process.execSync(`git pull`, { cwd: dotfilesDir, stdio: 'pipe' });
|
|
37
|
+
} else {
|
|
38
|
+
// Remove empty directory if it exists
|
|
39
|
+
if (fs.existsSync(dotfilesDir)) {
|
|
40
|
+
fs.rmSync(dotfilesDir, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
s.text = 'Cloning dotfiles repo...';
|
|
43
|
+
child_process.execSync(`git clone ${repoUrl} ${dotfilesDir}`, { stdio: 'pipe' });
|
|
44
|
+
}
|
|
45
|
+
s.succeed('Dotfiles repo ready');
|
|
46
|
+
} catch (err) {
|
|
47
|
+
s.fail(`Git error: ${err.message}`);
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Sync files
|
|
52
|
+
console.log(chalk.bold('\n Syncing files:'));
|
|
53
|
+
const result = await syncDotfiles({
|
|
54
|
+
repo: repoUrl,
|
|
55
|
+
copy: options.copy,
|
|
56
|
+
only: options.only,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Summary
|
|
60
|
+
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
|
+
}
|
|
68
|
+
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { syncDotfilesCommand };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const gitlab = require('../integrations/gitlab');
|
|
5
|
+
const { listStoredServices } = require('../utils/keychain');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Lists issues from connected platforms (GitHub + GitLab)
|
|
9
|
+
* When multiple platforms — interactive selection
|
|
10
|
+
* When --platform given — skips selection
|
|
11
|
+
*/
|
|
12
|
+
async function listIssues(options = {}) {
|
|
13
|
+
const services = await listStoredServices();
|
|
14
|
+
|
|
15
|
+
if (services.length === 0) {
|
|
16
|
+
throw new Error('No connected platforms. Run /connect to connect GitHub or GitLab.');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Filter to platforms supporting issues
|
|
20
|
+
const platforms = services.filter(s => ['github', 'gitlab'].includes(s));
|
|
21
|
+
if (platforms.length === 0) {
|
|
22
|
+
throw new Error('No connected platforms. Run /connect to connect GitHub or GitLab.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let platform = options.platform;
|
|
26
|
+
|
|
27
|
+
// When --platform given, use it
|
|
28
|
+
if (!platform) {
|
|
29
|
+
if (platforms.length === 1) {
|
|
30
|
+
platform = platforms[0];
|
|
31
|
+
} else {
|
|
32
|
+
// Interactive selection
|
|
33
|
+
const answer = await inquirer.prompt([{
|
|
34
|
+
type: 'list',
|
|
35
|
+
name: 'platform',
|
|
36
|
+
message: 'Which platform to fetch issues from?',
|
|
37
|
+
choices: platforms.map(p => ({ name: p.charAt(0).toUpperCase() + p.slice(1), value: p })),
|
|
38
|
+
}]);
|
|
39
|
+
platform = answer.platform;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const s = ora(`Fetching issues from ${platform}...`).start();
|
|
44
|
+
try {
|
|
45
|
+
let issues;
|
|
46
|
+
if (platform === 'gitlab') {
|
|
47
|
+
issues = await gitlab.listIssues(options);
|
|
48
|
+
} else {
|
|
49
|
+
// GitHub uses search API for issues
|
|
50
|
+
const github = require('../integrations/github');
|
|
51
|
+
const octokit = await github.getClient();
|
|
52
|
+
const { data: user } = await octokit.users.getAuthenticated();
|
|
53
|
+
const { data } = await octokit.search.issuesAndPullRequests({
|
|
54
|
+
q: `author:${user.login} is:issue is:open`,
|
|
55
|
+
sort: 'updated',
|
|
56
|
+
order: 'desc',
|
|
57
|
+
per_page: options.limit || 20,
|
|
58
|
+
});
|
|
59
|
+
issues = data.items.map(issue => ({
|
|
60
|
+
iid: issue.number,
|
|
61
|
+
title: issue.title,
|
|
62
|
+
state: issue.state,
|
|
63
|
+
url: issue.html_url,
|
|
64
|
+
updatedAt: issue.updated_at,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
s.succeed(`Found ${issues.length} issues on ${platform}`);
|
|
69
|
+
for (const issue of issues) {
|
|
70
|
+
console.log(` ${chalk.bold(`#${issue.iid}`)} ${issue.title}`);
|
|
71
|
+
console.log(` ${chalk.gray(issue.url)}`);
|
|
72
|
+
}
|
|
73
|
+
return issues;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
s.fail(err.message);
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { listIssues };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
const github = require('../integrations/github');
|
|
4
|
+
|
|
5
|
+
async function listPRs(options = {}) {
|
|
6
|
+
const s = ora('Fetching pull requests...').start();
|
|
7
|
+
try {
|
|
8
|
+
const prs = await github.listPRs(options);
|
|
9
|
+
s.stop();
|
|
10
|
+
if (prs.length === 0) {
|
|
11
|
+
console.log(chalk.yellow(' No open pull requests found.'));
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
for (const pr of prs) {
|
|
15
|
+
console.log(
|
|
16
|
+
` ${chalk.bold(`#${pr.number}`)} ${pr.title}`,
|
|
17
|
+
chalk.gray(`(${pr.repo})`),
|
|
18
|
+
chalk.green(pr.state),
|
|
19
|
+
);
|
|
20
|
+
console.log(chalk.gray(` ${pr.url}`));
|
|
21
|
+
}
|
|
22
|
+
return prs;
|
|
23
|
+
} catch (err) {
|
|
24
|
+
s.fail(err.message);
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function draftPR(title, options = {}) {
|
|
30
|
+
const s = ora(`Creating draft PR "${title}"...`).start();
|
|
31
|
+
try {
|
|
32
|
+
const result = await github.createDraftPR(title, options);
|
|
33
|
+
s.succeed(`Draft PR #${result.number} created`);
|
|
34
|
+
console.log(chalk.gray(` ${result.url}`));
|
|
35
|
+
return result;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
s.fail(err.message);
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function commentPR(prNumber, message, options = {}) {
|
|
43
|
+
const s = ora(`Commenting on PR #${prNumber}...`).start();
|
|
44
|
+
try {
|
|
45
|
+
const result = await github.commentOnPR(prNumber, message, options);
|
|
46
|
+
s.succeed(`Comment added to PR #${prNumber}`);
|
|
47
|
+
console.log(chalk.gray(` ${result.url}`));
|
|
48
|
+
return result;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
s.fail(err.message);
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { listPRs, draftPR, commentPR };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const inquirer = require('inquirer');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { resolveTheme, listThemes } = require('../ui/theme');
|
|
4
|
+
const { readConfig, writeConfig } = require('../config');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* /theme — interactive theme selector with live preview.
|
|
8
|
+
* Arrow keys to browse, Enter to apply, theme saved to ~/.adm/config.json.
|
|
9
|
+
*/
|
|
10
|
+
async function themeCommand() {
|
|
11
|
+
const config = await readConfig();
|
|
12
|
+
const currentTheme = config.theme || 'dark';
|
|
13
|
+
const themes = listThemes();
|
|
14
|
+
|
|
15
|
+
// ── Show current theme ────────────────────────────────
|
|
16
|
+
console.log('');
|
|
17
|
+
const cur = resolveTheme(config);
|
|
18
|
+
const cc = cur.colors;
|
|
19
|
+
console.log(
|
|
20
|
+
chalk.hex(cc.primary)(' Current: ') +
|
|
21
|
+
chalk.hex(cc.accent)(cur.name) +
|
|
22
|
+
chalk.hex(cc.muted)(' (' + currentTheme + ')')
|
|
23
|
+
);
|
|
24
|
+
console.log('');
|
|
25
|
+
|
|
26
|
+
// ── Build preview for each theme ──────────────────────
|
|
27
|
+
const choices = themes.map(t => {
|
|
28
|
+
const c = t.colors;
|
|
29
|
+
const block = (hex) => chalk.hex(hex)('██');
|
|
30
|
+
const label =
|
|
31
|
+
' ' +
|
|
32
|
+
block(c.primary) + block(c.accent) + block(c.secondary) +
|
|
33
|
+
' ' +
|
|
34
|
+
chalk.hex(c.primary).bold(t.name.padEnd(10)) +
|
|
35
|
+
chalk.hex(c.muted)('adm> ') +
|
|
36
|
+
chalk.hex(c.text)('hello') + ' ' +
|
|
37
|
+
chalk.hex(c.success)('✓') + ' ' +
|
|
38
|
+
chalk.hex(c.error)('✗') + ' ' +
|
|
39
|
+
chalk.hex(c.warning)('⚠');
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
name: label,
|
|
43
|
+
value: t.name,
|
|
44
|
+
short: t.name,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// ── Interactive selection ──────────────────────────────
|
|
49
|
+
const { selected } = await inquirer.prompt([
|
|
50
|
+
{
|
|
51
|
+
type: 'list',
|
|
52
|
+
name: 'selected',
|
|
53
|
+
message: 'Choose a theme:',
|
|
54
|
+
choices,
|
|
55
|
+
default: currentTheme,
|
|
56
|
+
pageSize: themes.length,
|
|
57
|
+
loop: false,
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
// ── Save to config ────────────────────────────────────
|
|
62
|
+
if (selected === currentTheme) {
|
|
63
|
+
const t = resolveTheme({ theme: selected });
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log(chalk.hex(t.colors.muted)(' Already using ' + selected + ' theme.'));
|
|
66
|
+
console.log('');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const newConfig = { ...config, theme: selected };
|
|
71
|
+
await writeConfig(newConfig);
|
|
72
|
+
|
|
73
|
+
const t = resolveTheme({ theme: selected });
|
|
74
|
+
const nc = t.colors;
|
|
75
|
+
console.log('');
|
|
76
|
+
console.log(
|
|
77
|
+
chalk.hex(nc.success)(' ✓ Theme saved: ') +
|
|
78
|
+
chalk.hex(nc.primary).bold(selected)
|
|
79
|
+
);
|
|
80
|
+
console.log(
|
|
81
|
+
chalk.hex(nc.muted)(' ~/.adm/config.json updated') +
|
|
82
|
+
chalk.hex(nc.text)(' — restart to see changes everywhere.')
|
|
83
|
+
);
|
|
84
|
+
console.log('');
|
|
85
|
+
|
|
86
|
+
// ── Show preview of new theme ─────────────────────────
|
|
87
|
+
showPreview(nc, selected);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function showPreview(c, name) {
|
|
91
|
+
const border = chalk.hex(c.primary);
|
|
92
|
+
const pad = ' ';
|
|
93
|
+
console.log('');
|
|
94
|
+
console.log(border(' ┌──────────────────────────────────┐'));
|
|
95
|
+
console.log(border(' │ ') + chalk.hex(c.text).bold(' ADM') + chalk.hex(c.muted)(' · ' + name) + pad.slice(0, Math.max(0, 18 - name.length)) + border('│'));
|
|
96
|
+
console.log(border(' │ ') + chalk.hex(c.muted)(' adm> ') + chalk.hex(c.text)('status') + pad.slice(0, 12) + border('│'));
|
|
97
|
+
console.log(border(' │ ') + chalk.hex(c.success)(' ✓ All systems go') + pad.slice(0, 10) + border('│'));
|
|
98
|
+
console.log(border(' │ ') + chalk.hex(c.accent)(' ██ ██') + chalk.hex(c.text)(' 12:34') + pad.slice(0, 10) + border('│'));
|
|
99
|
+
console.log(border(' └──────────────────────────────────┘'));
|
|
100
|
+
console.log('');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { themeCommand };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
async function uninstall() {
|
|
8
|
+
const configDir = process.env.ADM_CONFIG_DIR || path.join(os.homedir(), '.adm');
|
|
9
|
+
|
|
10
|
+
console.log(chalk.yellow(' Uninstalling ADM CLI...\n'));
|
|
11
|
+
|
|
12
|
+
// Remove config directory
|
|
13
|
+
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}`));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
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
|
+
});
|
|
34
|
+
|
|
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}`));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
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'));
|
|
53
|
+
}
|
|
54
|
+
|
|
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
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log(chalk.green('\n ADM CLI fully uninstalled.'));
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { uninstall };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const { spawn } = require('child_process');
|
|
2
|
+
const chalk = require('chalk');
|
|
3
|
+
const { version: currentVersion } = require('../../package.json');
|
|
4
|
+
|
|
5
|
+
const FRAMES = ['◢', '◣', '◤', '◥'];
|
|
6
|
+
|
|
7
|
+
function createLoader(text) {
|
|
8
|
+
let i = 0;
|
|
9
|
+
let active = true;
|
|
10
|
+
const interval = setInterval(() => {
|
|
11
|
+
i = (i + 1) % FRAMES.length;
|
|
12
|
+
process.stdout.write(`\r${chalk.cyan(FRAMES[i])} ${text}`);
|
|
13
|
+
}, 80);
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
update(t) { text = t; },
|
|
17
|
+
succeed(msg) {
|
|
18
|
+
active = false;
|
|
19
|
+
clearInterval(interval);
|
|
20
|
+
process.stdout.write(`\r${chalk.green('✔')} ${msg || text}\n`);
|
|
21
|
+
},
|
|
22
|
+
fail(msg) {
|
|
23
|
+
active = false;
|
|
24
|
+
clearInterval(interval);
|
|
25
|
+
process.stdout.write(`\r${chalk.red('✖')} ${msg || text}\n`);
|
|
26
|
+
},
|
|
27
|
+
info(msg) {
|
|
28
|
+
active = false;
|
|
29
|
+
clearInterval(interval);
|
|
30
|
+
process.stdout.write(`\r${chalk.yellow('ℹ')} ${msg || text}\n`);
|
|
31
|
+
},
|
|
32
|
+
stop() {
|
|
33
|
+
if (!active) return;
|
|
34
|
+
active = false;
|
|
35
|
+
clearInterval(interval);
|
|
36
|
+
process.stdout.write('\r' + ' '.repeat((text || '').length + 4) + '\r');
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function runSpawn(cmd, args) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
let stdout = '';
|
|
44
|
+
let stderr = '';
|
|
45
|
+
const child = spawn(cmd, args, { stdio: 'pipe' });
|
|
46
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
47
|
+
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
48
|
+
child.on('close', (code) => {
|
|
49
|
+
if (code === 0) resolve(stdout.trim());
|
|
50
|
+
else reject(new Error(stderr.trim() || `Command failed with code ${code}`));
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function upgrade() {
|
|
56
|
+
const spinner = createLoader('Checking for updates...');
|
|
57
|
+
|
|
58
|
+
let latestVersion;
|
|
59
|
+
try {
|
|
60
|
+
const output = await runSpawn('npm', ['view', '@crystalgames/adm', 'version']);
|
|
61
|
+
latestVersion = output.split('\n').pop().trim();
|
|
62
|
+
} catch (err) {
|
|
63
|
+
spinner.fail('Update check failed');
|
|
64
|
+
return {
|
|
65
|
+
output: chalk.red(`Unable to check for updates: ${err.message}`),
|
|
66
|
+
shouldExit: false,
|
|
67
|
+
shouldClear: false,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (latestVersion === currentVersion) {
|
|
72
|
+
spinner.succeed(chalk.green(`Already up to date (v${currentVersion})`));
|
|
73
|
+
return {
|
|
74
|
+
output: '',
|
|
75
|
+
shouldExit: false,
|
|
76
|
+
shouldClear: false,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
spinner.info(chalk.yellow(`Update available: v${currentVersion} → v${latestVersion}`));
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
output: `Update to v${latestVersion}? [y/N]`,
|
|
84
|
+
needsConfirm: true,
|
|
85
|
+
confirmMessage: `Update available: v${currentVersion} → v${latestVersion}`,
|
|
86
|
+
async onConfirm() {
|
|
87
|
+
const installSpinner = createLoader(`Updating to v${latestVersion}...`);
|
|
88
|
+
try {
|
|
89
|
+
await runSpawn('npm', ['install', '-g', '@crystalgames/adm@latest']);
|
|
90
|
+
installSpinner.succeed(chalk.green(`Updated to v${latestVersion}`));
|
|
91
|
+
return {
|
|
92
|
+
output: chalk.green(`Restarting...`),
|
|
93
|
+
shouldRestart: true,
|
|
94
|
+
shouldExit: false,
|
|
95
|
+
shouldClear: false,
|
|
96
|
+
};
|
|
97
|
+
} catch (err) {
|
|
98
|
+
installSpinner.fail(chalk.red('Update failed'));
|
|
99
|
+
return {
|
|
100
|
+
output: chalk.red(`Update failed: ${err.message}`),
|
|
101
|
+
shouldExit: false,
|
|
102
|
+
shouldClear: false,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
onCancel() {
|
|
107
|
+
return { output: chalk.gray('Cancelled.'), shouldExit: false, shouldClear: false };
|
|
108
|
+
},
|
|
109
|
+
shouldExit: false,
|
|
110
|
+
shouldClear: false,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { upgrade };
|
|
@@ -0,0 +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 };
|