xgem-cli 2.0.0-alpha.14 → 2.0.0-alpha.16
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/README.md +6 -3
- package/bin/xgem +13 -2
- package/bin/xgem.js +6 -2
- package/lib/bootstrap.sh +53 -22
- package/lib/ci.sh +46 -11
- package/lib/doctor.sh +83 -11
- package/lib/flutter.sh +3 -0
- package/lib/git.sh +31 -7
- package/lib/toolchain.sh +450 -0
- package/lib/update.sh +74 -0
- package/lib/utils.sh +16 -8
- package/lib/version.sh +1 -1
- package/lib-win/bootstrap.js +32 -9
- package/lib-win/ci.js +40 -10
- package/lib-win/doctor.js +48 -20
- package/lib-win/flutter.js +5 -4
- package/lib-win/git.js +20 -4
- package/lib-win/toolchain.js +284 -0
- package/lib-win/update.js +68 -0
- package/lib-win/utils.js +10 -2
- package/package.json +1 -1
package/lib-win/ci.js
CHANGED
|
@@ -1,40 +1,70 @@
|
|
|
1
|
-
// xgem Windows engine ci — port of lib/ci.sh: run
|
|
2
|
-
//
|
|
1
|
+
// xgem Windows engine ci — port of lib/ci.sh: run the checks for every
|
|
2
|
+
// configured framework before you push.
|
|
3
3
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const { spawnSync } = require('node:child_process');
|
|
7
7
|
const { logInfo, logSuccess, logWarn, logError, die } = require('./logger');
|
|
8
|
+
const toolchain = require('./toolchain');
|
|
8
9
|
|
|
9
10
|
const STEPS = ['lint', 'test', 'build'];
|
|
10
11
|
|
|
12
|
+
function runStep(label, cmd, args, results) {
|
|
13
|
+
logInfo(`Running ${label}...`);
|
|
14
|
+
const result = spawnSync(cmd, args, {
|
|
15
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
16
|
+
env: { ...process.env, CI: 'true' },
|
|
17
|
+
shell: cmd !== process.execPath,
|
|
18
|
+
});
|
|
19
|
+
const ok = (result.status ?? 1) === 0;
|
|
20
|
+
results.push({ line: `${ok ? 'PASS ' : 'FAIL '} ${label}`, ok });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function flutterSteps(results) {
|
|
24
|
+
if (!(await toolchain.ensure('flutter'))) {
|
|
25
|
+
results.push({ line: 'SKIP flutter (flutter not found)', ok: true });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
runStep('flutter analyze', 'flutter', ['analyze'], results);
|
|
29
|
+
if (fs.existsSync('test')) {
|
|
30
|
+
runStep('flutter test', 'flutter', ['test'], results);
|
|
31
|
+
} else {
|
|
32
|
+
results.push({ line: 'SKIP flutter test (no test/ directory)', ok: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
11
36
|
async function cmdCi(configDir) {
|
|
12
37
|
if (!fs.existsSync(configDir)) die(`No ${configDir} found — run 'xgem init' or 'xgem add' first.`);
|
|
13
38
|
|
|
14
39
|
const results = [];
|
|
15
|
-
let overallOk = true;
|
|
16
40
|
|
|
17
41
|
for (const fw of fs.readdirSync(configDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)) {
|
|
42
|
+
if (fw === 'flutter') {
|
|
43
|
+
await flutterSteps(results);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const needed = toolchain.fwTool(fw);
|
|
47
|
+
if (needed && !(await toolchain.ensure(needed))) {
|
|
48
|
+
results.push({ line: `SKIP ${fw} (${needed} not found)`, ok: true });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
18
51
|
for (const step of STEPS) {
|
|
19
52
|
const target = path.join(configDir, fw, `${step}.mjs`);
|
|
20
53
|
if (!fs.existsSync(target)) continue;
|
|
21
|
-
|
|
22
|
-
const result = spawnSync(process.execPath, [target], { stdio: 'inherit' });
|
|
23
|
-
const ok = (result.status ?? 1) === 0;
|
|
24
|
-
results.push(`${ok ? 'PASS ' : 'FAIL '} ${fw} ${step}`);
|
|
25
|
-
if (!ok) overallOk = false;
|
|
54
|
+
runStep(`${fw} ${step}`, process.execPath, [target], results);
|
|
26
55
|
}
|
|
27
56
|
}
|
|
28
57
|
|
|
29
58
|
if (results.length === 0) {
|
|
30
|
-
logWarn(`No
|
|
59
|
+
logWarn(`No checks found under ${configDir} to run.`);
|
|
31
60
|
return;
|
|
32
61
|
}
|
|
33
62
|
|
|
34
63
|
console.log('');
|
|
35
64
|
console.log('=== xgem ci summary ===');
|
|
36
|
-
results.forEach((r) => console.log(r));
|
|
65
|
+
results.forEach((r) => console.log(r.line));
|
|
37
66
|
|
|
67
|
+
const overallOk = results.every((r) => r.ok);
|
|
38
68
|
if (overallOk) logSuccess('All checks passed.');
|
|
39
69
|
else logError('Some checks failed.');
|
|
40
70
|
process.exitCode = overallOk ? 0 : 1;
|
package/lib-win/doctor.js
CHANGED
|
@@ -1,43 +1,71 @@
|
|
|
1
|
-
// xgem Windows engine doctor — mirrors lib/doctor.sh's
|
|
1
|
+
// xgem Windows engine doctor — mirrors lib/doctor.sh's toolchain report.
|
|
2
2
|
// No iOS/SwiftPM engine exists here (see flutter.js) since Xcode has no
|
|
3
3
|
// Windows equivalent; `doctor ios` explains that instead of pretending.
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const {
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const { detectArch } = require('./utils');
|
|
7
|
+
const { paint, logInfo } = require('./logger');
|
|
8
|
+
const toolchain = require('./toolchain');
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
const TOOLS = ['git', 'flutter', 'dart', 'node', 'python', 'go', 'cargo', 'docker', 'gh', 'fvm'];
|
|
11
|
+
|
|
12
|
+
const pretty = (p) => (p.startsWith(os.homedir()) ? `~${p.slice(os.homedir().length)}` : p);
|
|
13
|
+
|
|
14
|
+
async function printGeneral() {
|
|
9
15
|
console.log(paint('cyan', '=== xgem doctor (Windows) ==='));
|
|
10
|
-
console.log(`OS:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
console.log(
|
|
19
|
-
|
|
16
|
+
console.log(`OS: win32 Arch: ${detectArch()}\n`);
|
|
17
|
+
|
|
18
|
+
const noNetwork = process.env.XGEM_NO_NETWORK === '1';
|
|
19
|
+
if (!noNetwork) {
|
|
20
|
+
logInfo('Checking installed tools and the latest available versions...');
|
|
21
|
+
await Promise.all(TOOLS.filter((t) => toolchain.resolve(t)).map((t) => toolchain.latest(t)));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log(paint('cyan', 'Toolchains'));
|
|
25
|
+
for (const t of TOOLS) {
|
|
26
|
+
const found = toolchain.resolve(t);
|
|
27
|
+
if (!found) {
|
|
28
|
+
console.log(` ${t.padEnd(9)} ${'not found'.padEnd(11)} download: ${toolchain.url(t)}`);
|
|
29
|
+
const recipe = toolchain.installCmd(t);
|
|
30
|
+
if (recipe) console.log(` install: ${recipe} (or run: xgem update ${t})`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const version = toolchain.versionOf(t, found);
|
|
34
|
+
const shown = found.manager === 'other' ? '' : ` (${found.manager})`;
|
|
35
|
+
console.log(` ${t.padEnd(9)} ${(version || 'unknown').padEnd(11)} ${pretty(found.path)}${shown}`);
|
|
36
|
+
if (!found.onPath && t !== 'dart') {
|
|
37
|
+
console.log(` ! not on PATH: found via ${found.manager}, so plain scripts and CI won't see it (xgem resolves it for its own commands)`);
|
|
38
|
+
}
|
|
39
|
+
const newer = await toolchain.latest(t, true);
|
|
40
|
+
if (version && newer && toolchain.verLt(version, newer)) {
|
|
41
|
+
console.log(` ^ update available: ${version} -> ${newer} (run: xgem update ${t})`);
|
|
42
|
+
}
|
|
43
|
+
const pinned = t === 'dart' ? '' : toolchain.pin(t);
|
|
44
|
+
if (pinned && version && /^\d/.test(pinned) && !version.startsWith(pinned)) {
|
|
45
|
+
console.log(` ! this project pins ${t} ${pinned} but the active one is ${version}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
20
48
|
}
|
|
21
49
|
|
|
22
50
|
function printIos() {
|
|
23
51
|
console.log(paint('cyan', '=== xgem doctor ios (Windows) ==='));
|
|
24
52
|
console.log(paint('yellow', 'iOS/macOS builds are not available on Windows — Xcode has no Windows equivalent.'));
|
|
25
53
|
console.log('Use WSL2 with a Mac, or a real Mac, for Flutter iOS builds.');
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
28
|
-
console.log(`\nflutter is installed here (${
|
|
54
|
+
const found = toolchain.resolve('flutter');
|
|
55
|
+
if (found) {
|
|
56
|
+
console.log(`\nflutter is installed here (${toolchain.versionOf('flutter', found)}) and can still build Android/Windows targets:`);
|
|
29
57
|
console.log(' xgem run flutter build (choose APK or Windows)');
|
|
30
58
|
}
|
|
31
59
|
}
|
|
32
60
|
|
|
33
|
-
function cmdDoctor(target) {
|
|
61
|
+
async function cmdDoctor(target) {
|
|
34
62
|
if (target === 'ios') {
|
|
35
63
|
printIos();
|
|
36
64
|
} else if (!target) {
|
|
37
|
-
printGeneral();
|
|
65
|
+
await printGeneral();
|
|
38
66
|
} else {
|
|
39
67
|
const { die } = require('./logger');
|
|
40
|
-
die(`Unknown doctor target '${target}'. Usage: xgem doctor [ios]`);
|
|
68
|
+
die(`Unknown doctor target '${target}'. Usage: xgem doctor [ios] [--no-network]`);
|
|
41
69
|
}
|
|
42
70
|
}
|
|
43
71
|
|
package/lib-win/flutter.js
CHANGED
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
const fs = require('node:fs');
|
|
9
9
|
const { spawnSync } = require('node:child_process');
|
|
10
10
|
const { logInfo, logSuccess, logError, die } = require('./logger');
|
|
11
|
-
const { prompt
|
|
11
|
+
const { prompt } = require('./utils');
|
|
12
|
+
const toolchain = require('./toolchain');
|
|
12
13
|
|
|
13
14
|
function flutter(args) {
|
|
14
15
|
return spawnSync('flutter', args, { stdio: 'inherit' });
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
async function cmdHardClean() {
|
|
18
|
-
|
|
19
|
+
if (!(await toolchain.ensure('flutter'))) die('Flutter is required for this command.');
|
|
19
20
|
logInfo('Cleaning Flutter project...');
|
|
20
21
|
fs.rmSync('pubspec.lock', { force: true });
|
|
21
22
|
flutter(['clean']);
|
|
@@ -26,7 +27,7 @@ async function cmdHardClean() {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
async function cmdBuildRunner() {
|
|
29
|
-
|
|
30
|
+
if (!(await toolchain.ensure('flutter'))) die('Flutter is required for this command.');
|
|
30
31
|
logInfo('Running Flutter Build Runner...');
|
|
31
32
|
const verboseChoice = ((await prompt('Run in verbose mode? (Y/n)')) || 'Y').toLowerCase();
|
|
32
33
|
const args = ['pub', 'run', 'build_runner', 'build', '--delete-conflicting-outputs'];
|
|
@@ -43,7 +44,7 @@ function pubspecVersion() {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
async function cmdBuild() {
|
|
46
|
-
|
|
47
|
+
if (!(await toolchain.ensure('flutter'))) die('Flutter is required for this command.');
|
|
47
48
|
console.log('--- Flutter Build Orchestrator (Windows) ---');
|
|
48
49
|
|
|
49
50
|
let currentName = '';
|
package/lib-win/git.js
CHANGED
|
@@ -57,6 +57,24 @@ function baseBranch(remote) {
|
|
|
57
57
|
return '';
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
async function resolveConflictsHelp() {
|
|
61
|
+
const files = gitCapture(['diff', '--name-only', '--diff-filter=U']).split(/\r?\n/).filter(Boolean);
|
|
62
|
+
if (files.length) {
|
|
63
|
+
logWarn('Files with conflicts:');
|
|
64
|
+
files.forEach((f) => console.log(` - ${f}`));
|
|
65
|
+
}
|
|
66
|
+
logWarn('Resolve them, then run: git add <files> && git rebase --continue (or give up with: git rebase --abort)');
|
|
67
|
+
|
|
68
|
+
const openEditor = (await prompt('Do you want to open VS Code to resolve this now? (y/n)')).toLowerCase();
|
|
69
|
+
if (openEditor !== 'y') return;
|
|
70
|
+
|
|
71
|
+
if (hasCmd('code')) {
|
|
72
|
+
spawnSync('code', ['.'], { stdio: 'inherit', shell: true });
|
|
73
|
+
} else {
|
|
74
|
+
logWarn("VS Code's 'code' command isn't on your PATH. Reinstall VS Code with 'Add to PATH' enabled, or open the files above manually.");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
60
78
|
async function cmdCmt(commitMsg) {
|
|
61
79
|
if (!commitMsg) die('Missing commit message!');
|
|
62
80
|
|
|
@@ -131,7 +149,7 @@ async function cmdCmt(commitMsg) {
|
|
|
131
149
|
const inConflict = fs.existsSync(path.join(gitDir, 'rebase-merge')) || fs.existsSync(path.join(gitDir, 'rebase-apply'));
|
|
132
150
|
if (inConflict) {
|
|
133
151
|
logError('MERGE CONFLICT DETECTED!');
|
|
134
|
-
|
|
152
|
+
await resolveConflictsHelp();
|
|
135
153
|
} else {
|
|
136
154
|
logError(`Could not sync with '${remoteName}' — this looks like a connection problem, not a merge conflict (see the git error above).`);
|
|
137
155
|
logWarn(`Your commit is safe locally. Re-run 'xgem git cmt' once connectivity is restored, or push manually: git push ${remoteName} ${currentBranch}`);
|
|
@@ -369,9 +387,7 @@ async function cmdSync() {
|
|
|
369
387
|
const inConflict = fs.existsSync(path.join(gitDir, 'rebase-merge')) || fs.existsSync(path.join(gitDir, 'rebase-apply'));
|
|
370
388
|
if (inConflict) {
|
|
371
389
|
logError('MERGE CONFLICT DETECTED!');
|
|
372
|
-
|
|
373
|
-
const openEditor = (await prompt('Do you want to open VS Code to resolve this now? (y/n)')).toLowerCase();
|
|
374
|
-
if (openEditor === 'y') spawnSync('code', ['.'], { stdio: 'inherit' });
|
|
390
|
+
await resolveConflictsHelp();
|
|
375
391
|
} else {
|
|
376
392
|
logError('Rebase failed — this looks like a connection problem, not a merge conflict (see the git error above).');
|
|
377
393
|
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// xgem Windows engine toolchain layer — port of lib/toolchain.sh: finds
|
|
2
|
+
// tools where version managers keep them, offers to install missing ones,
|
|
3
|
+
// and reports/applies updates. Best-effort: not runtime-tested on Windows.
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
const https = require('node:https');
|
|
9
|
+
const { spawnSync } = require('node:child_process');
|
|
10
|
+
const { logInfo, logSuccess, logWarn, logError } = require('./logger');
|
|
11
|
+
|
|
12
|
+
const CACHE_DIR = process.env.XGEM_CACHE_DIR || path.join(os.homedir(), '.xgem', 'cache');
|
|
13
|
+
const HOME = os.homedir();
|
|
14
|
+
const EXTS = ['.exe', '.cmd', '.bat', ''];
|
|
15
|
+
|
|
16
|
+
const CANON = { npm: 'node', npx: 'node', corepack: 'node', pip: 'python', pip3: 'python', python3: 'python', rustc: 'cargo', rustup: 'cargo' };
|
|
17
|
+
const KNOWN = ['flutter', 'dart', 'node', 'python', 'go', 'cargo', 'docker', 'git', 'gh', 'fvm'];
|
|
18
|
+
const DISPLAY = { flutter: 'Flutter SDK', dart: 'Dart SDK', node: 'Node.js', python: 'Python 3', go: 'Go', cargo: 'Rust (cargo)', docker: 'Docker', git: 'Git', gh: 'GitHub CLI', fvm: 'FVM (Flutter Version Management)' };
|
|
19
|
+
const URLS = {
|
|
20
|
+
flutter: 'https://docs.flutter.dev/get-started/install/windows',
|
|
21
|
+
dart: 'https://docs.flutter.dev/get-started/install/windows',
|
|
22
|
+
node: 'https://nodejs.org/en/download',
|
|
23
|
+
python: 'https://www.python.org/downloads/windows/',
|
|
24
|
+
go: 'https://go.dev/dl/',
|
|
25
|
+
cargo: 'https://www.rust-lang.org/tools/install',
|
|
26
|
+
docker: 'https://www.docker.com/products/docker-desktop/',
|
|
27
|
+
git: 'https://git-scm.com/downloads/win',
|
|
28
|
+
gh: 'https://cli.github.com',
|
|
29
|
+
fvm: 'https://fvm.app/documentation/getting-started/installation',
|
|
30
|
+
};
|
|
31
|
+
const WINGET_IDS = { node: 'OpenJS.NodeJS.LTS', python: 'Python.Python.3.13', go: 'GoLang.Go', docker: 'Docker.DockerDesktop', git: 'Git.Git', gh: 'GitHub.cli', cargo: 'Rustlang.Rustup' };
|
|
32
|
+
const FW_TOOL = { flutter: 'flutter', node: 'node', react: 'node', vue: 'node', angular: 'node', next: 'node', python: 'python', go: 'go', rust: 'cargo' };
|
|
33
|
+
|
|
34
|
+
const canon = (t) => CANON[t] || t;
|
|
35
|
+
const isKnown = (t) => KNOWN.includes(canon(t));
|
|
36
|
+
const display = (t) => DISPLAY[t] || t;
|
|
37
|
+
const url = (t) => URLS[t] || '';
|
|
38
|
+
const fwTool = (fw) => FW_TOOL[fw] || '';
|
|
39
|
+
|
|
40
|
+
function findUp(rel) {
|
|
41
|
+
let dir = process.cwd();
|
|
42
|
+
for (;;) {
|
|
43
|
+
if (dir === HOME && process.cwd() !== HOME) return null;
|
|
44
|
+
const candidate = path.join(dir, rel);
|
|
45
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
46
|
+
const parent = path.dirname(dir);
|
|
47
|
+
if (parent === dir) return null;
|
|
48
|
+
dir = parent;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readJson(file) {
|
|
53
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function pin(tool) {
|
|
57
|
+
let v = '';
|
|
58
|
+
if (tool === 'flutter' || tool === 'dart') {
|
|
59
|
+
const rc = findUp('.fvmrc');
|
|
60
|
+
const cfg = findUp(path.join('.fvm', 'fvm_config.json'));
|
|
61
|
+
if (rc) v = (readJson(rc) || {}).flutter || '';
|
|
62
|
+
else if (cfg) v = (readJson(cfg) || {}).flutterSdkVersion || '';
|
|
63
|
+
} else if (tool === 'node') {
|
|
64
|
+
const f = findUp('.nvmrc');
|
|
65
|
+
if (f) v = fs.readFileSync(f, 'utf8').trim().replace(/^v/, '');
|
|
66
|
+
}
|
|
67
|
+
return v;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function listDirs(dir) {
|
|
71
|
+
try { return fs.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name); } catch { return []; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function newest(names) {
|
|
75
|
+
return [...names].sort((a, b) => a.localeCompare(b, undefined, { numeric: true })).pop();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function fvmRoots() {
|
|
79
|
+
return [process.env.FVM_CACHE_PATH, process.env.FVM_HOME, path.join(process.env.LOCALAPPDATA || '', 'fvm'), path.join(HOME, 'fvm'), path.join(HOME, '.fvm')].filter(Boolean);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function candidates(tool) {
|
|
83
|
+
const out = [];
|
|
84
|
+
const add = (dir, manager) => out.push({ dir, manager });
|
|
85
|
+
const pf = process.env.ProgramFiles || 'C:\\Program Files';
|
|
86
|
+
const local = process.env.LOCALAPPDATA || path.join(HOME, 'AppData', 'Local');
|
|
87
|
+
|
|
88
|
+
if (tool === 'flutter' || tool === 'dart') {
|
|
89
|
+
const proj = findUp(path.join('.fvm', 'flutter_sdk'));
|
|
90
|
+
if (proj) add(path.join(proj, 'bin'), 'fvm');
|
|
91
|
+
const p = pin('flutter');
|
|
92
|
+
for (const root of fvmRoots()) if (p) add(path.join(root, 'versions', p, 'bin'), 'fvm');
|
|
93
|
+
add(path.join(HOME, '.fvm', 'flutter_sdk', 'bin'), 'fvm');
|
|
94
|
+
for (const root of fvmRoots()) add(path.join(root, 'default', 'bin'), 'fvm');
|
|
95
|
+
for (const root of fvmRoots()) {
|
|
96
|
+
const v = newest(listDirs(path.join(root, 'versions')));
|
|
97
|
+
if (v) add(path.join(root, 'versions', v, 'bin'), 'fvm');
|
|
98
|
+
}
|
|
99
|
+
add('C:\\src\\flutter\\bin', 'other');
|
|
100
|
+
add(path.join(HOME, 'development', 'flutter', 'bin'), 'other');
|
|
101
|
+
} else if (tool === 'node') {
|
|
102
|
+
const nvmHome = process.env.NVM_HOME;
|
|
103
|
+
if (nvmHome) {
|
|
104
|
+
const p = pin('node');
|
|
105
|
+
const names = listDirs(nvmHome).filter((n) => /^v?\d/.test(n));
|
|
106
|
+
const match = p && /^\d/.test(p) ? names.filter((n) => n.replace(/^v/, '').startsWith(p)) : [];
|
|
107
|
+
const pick = newest(match.length ? match : names);
|
|
108
|
+
if (pick) add(path.join(nvmHome, pick), 'nvm');
|
|
109
|
+
}
|
|
110
|
+
if (process.env.NVM_SYMLINK) add(process.env.NVM_SYMLINK, 'nvm');
|
|
111
|
+
add(path.join(local, 'Volta', 'bin'), 'volta');
|
|
112
|
+
add(path.join(pf, 'nodejs'), 'other');
|
|
113
|
+
} else if (tool === 'python') {
|
|
114
|
+
const base = path.join(local, 'Programs', 'Python');
|
|
115
|
+
const v = newest(listDirs(base));
|
|
116
|
+
if (v) add(path.join(base, v), 'other');
|
|
117
|
+
} else if (tool === 'go') {
|
|
118
|
+
add(path.join(pf, 'Go', 'bin'), 'other');
|
|
119
|
+
} else if (tool === 'cargo') {
|
|
120
|
+
add(path.join(HOME, '.cargo', 'bin'), 'rustup');
|
|
121
|
+
} else if (tool === 'docker') {
|
|
122
|
+
add(path.join(pf, 'Docker', 'Docker', 'resources', 'bin'), 'other');
|
|
123
|
+
} else if (tool === 'git') {
|
|
124
|
+
add(path.join(pf, 'Git', 'cmd'), 'other');
|
|
125
|
+
} else if (tool === 'gh') {
|
|
126
|
+
add(path.join(pf, 'GitHub CLI'), 'other');
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function managerOf(p) {
|
|
132
|
+
const s = p.replace(/\\/g, '/').toLowerCase();
|
|
133
|
+
if (s.includes('/fvm/') || s.includes('/.fvm/')) return 'fvm';
|
|
134
|
+
if (s.includes('/nvm')) return 'nvm';
|
|
135
|
+
if (s.includes('/.cargo/')) return 'rustup';
|
|
136
|
+
if (s.includes('/volta/')) return 'volta';
|
|
137
|
+
if (s.includes('/winget/') || s.includes('/scoop/')) return 'package manager';
|
|
138
|
+
return 'other';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function whereFirst(cmd) {
|
|
142
|
+
const r = spawnSync('where', [cmd], { encoding: 'utf8', shell: false });
|
|
143
|
+
if (r.status !== 0) return '';
|
|
144
|
+
return (r.stdout || '').split(/\r?\n/).find(Boolean) || '';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// resolve(cmd) -> { path, dir, manager, onPath } or null
|
|
148
|
+
function resolve(cmd) {
|
|
149
|
+
const c = canon(cmd);
|
|
150
|
+
const onPath = whereFirst(cmd);
|
|
151
|
+
if (onPath) return { path: onPath, dir: path.dirname(onPath), manager: managerOf(onPath), onPath: true };
|
|
152
|
+
for (const { dir, manager } of candidates(c)) {
|
|
153
|
+
for (const ext of EXTS) {
|
|
154
|
+
const full = path.join(dir, cmd + ext);
|
|
155
|
+
if (fs.existsSync(full) && fs.statSync(full).isFile()) return { path: full, dir, manager, onPath: false };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function activate(found) {
|
|
162
|
+
if (!found || found.onPath) return;
|
|
163
|
+
process.env.PATH = `${found.dir}${path.delimiter}${process.env.PATH || ''}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function versionOf(t, found) {
|
|
167
|
+
if (t === 'flutter') {
|
|
168
|
+
const sdk = path.join(path.dirname(found.path), '..');
|
|
169
|
+
const j = readJson(path.join(sdk, 'bin', 'cache', 'flutter.version.json'));
|
|
170
|
+
if (j && j.frameworkVersion) return j.frameworkVersion;
|
|
171
|
+
}
|
|
172
|
+
const args = t === 'go' ? ['version'] : ['--version'];
|
|
173
|
+
const r = spawnSync(found.path, args, { encoding: 'utf8', shell: /\.(cmd|bat)$/i.test(found.path) });
|
|
174
|
+
const m = ((r.stdout || '') + (r.stderr || '')).split(/\r?\n/).slice(0, 3).join(' ').match(/\d+(\.\d+)+/);
|
|
175
|
+
return m ? m[0] : '';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function verLt(a, b) {
|
|
179
|
+
const pa = a.split('.').map(Number);
|
|
180
|
+
const pb = b.split('.').map(Number);
|
|
181
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
182
|
+
const x = pa[i] || 0;
|
|
183
|
+
const y = pb[i] || 0;
|
|
184
|
+
if (x !== y) return x < y;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function fetchText(u) {
|
|
190
|
+
return new Promise((resolveP) => {
|
|
191
|
+
const req = https.get(u, { timeout: 8000 }, (res) => {
|
|
192
|
+
if (res.statusCode !== 200) { res.resume(); resolveP(''); return; }
|
|
193
|
+
let data = '';
|
|
194
|
+
res.setEncoding('utf8');
|
|
195
|
+
res.on('data', (c) => { data += c; });
|
|
196
|
+
res.on('end', () => resolveP(data));
|
|
197
|
+
});
|
|
198
|
+
req.on('timeout', () => { req.destroy(); resolveP(''); });
|
|
199
|
+
req.on('error', () => resolveP(''));
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function fetchLatest(t) {
|
|
204
|
+
if (t === 'flutter') {
|
|
205
|
+
const j = JSON.parse((await fetchText('https://storage.googleapis.com/flutter_infra_release/releases/releases_windows.json')) || 'null');
|
|
206
|
+
if (!j) return '';
|
|
207
|
+
const rel = j.releases.find((r) => r.hash === j.current_release.stable && r.channel === 'stable');
|
|
208
|
+
return rel ? rel.version : '';
|
|
209
|
+
}
|
|
210
|
+
if (t === 'node') {
|
|
211
|
+
const list = JSON.parse((await fetchText('https://nodejs.org/dist/index.json')) || 'null');
|
|
212
|
+
const lts = list && list.find((r) => r.lts);
|
|
213
|
+
return lts ? lts.version.replace(/^v/, '') : '';
|
|
214
|
+
}
|
|
215
|
+
if (t === 'go') {
|
|
216
|
+
return ((await fetchText('https://go.dev/VERSION?m=text')).split(/\r?\n/)[0] || '').replace(/^go/, '');
|
|
217
|
+
}
|
|
218
|
+
return '';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// latest(t) -> newest known version, cached 12h; noNetwork uses only the cache.
|
|
222
|
+
async function latest(t, noNetwork = process.env.XGEM_NO_NETWORK === '1') {
|
|
223
|
+
const f = path.join(CACHE_DIR, `latest-${t}`);
|
|
224
|
+
const fresh = fs.existsSync(f) && Date.now() - fs.statSync(f).mtimeMs < 12 * 3600 * 1000;
|
|
225
|
+
if (noNetwork || fresh) return fs.existsSync(f) ? fs.readFileSync(f, 'utf8').trim() : '';
|
|
226
|
+
let v = '';
|
|
227
|
+
try { v = await fetchLatest(t); } catch { v = ''; }
|
|
228
|
+
if (v) {
|
|
229
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
230
|
+
fs.writeFileSync(f, v);
|
|
231
|
+
return v;
|
|
232
|
+
}
|
|
233
|
+
return fs.existsSync(f) ? fs.readFileSync(f, 'utf8').trim() : '';
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function installCmd(t) {
|
|
237
|
+
if (t === 'flutter' || t === 'dart') {
|
|
238
|
+
return resolve('fvm') ? 'fvm install stable && fvm global stable' : '';
|
|
239
|
+
}
|
|
240
|
+
if (t === 'fvm') return 'dart pub global activate fvm';
|
|
241
|
+
if (WINGET_IDS[t]) return `winget install --id ${WINGET_IDS[t]} -e`;
|
|
242
|
+
return '';
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function updateCmd(t, latestVersion, manager) {
|
|
246
|
+
if (t === 'flutter' && manager === 'fvm') return `fvm install ${latestVersion}`;
|
|
247
|
+
if (t === 'cargo' && manager === 'rustup') return 'rustup update';
|
|
248
|
+
if (WINGET_IDS[t] && t !== 'cargo') return `winget upgrade --id ${WINGET_IDS[t]} -e`;
|
|
249
|
+
return '';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ensure(t) -> true once the tool is usable, after explaining what's needed
|
|
253
|
+
// and (with confirmation) installing it.
|
|
254
|
+
async function ensure(tool) {
|
|
255
|
+
const { confirm } = require('./utils');
|
|
256
|
+
const t = canon(tool);
|
|
257
|
+
let found = resolve(tool);
|
|
258
|
+
if (found) { activate(found); return true; }
|
|
259
|
+
|
|
260
|
+
logWarn(`${display(t)} is required for this but wasn't found (checked PATH, version managers like fvm/nvm, and common install locations).`);
|
|
261
|
+
console.log(` Download: ${url(t)}`);
|
|
262
|
+
const recipe = installCmd(t);
|
|
263
|
+
if (!recipe) {
|
|
264
|
+
logInfo('Install it from the link above, then re-run this command.');
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
if (recipe.startsWith('winget') && !whereFirst('winget')) {
|
|
268
|
+
logInfo('That install route needs winget (App Installer from the Microsoft Store) — install it first, or use the download link above.');
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
console.log(` Install command: ${recipe}`);
|
|
272
|
+
if (!(await confirm(`Install ${display(t)} now?`))) {
|
|
273
|
+
logInfo("Skipped. Install it from the link above when you're ready.");
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
const r = spawnSync(recipe, { stdio: 'inherit', shell: true });
|
|
277
|
+
if (r.status !== 0) { logError('The install command failed (see its output above).'); return false; }
|
|
278
|
+
found = resolve(tool);
|
|
279
|
+
if (found) { activate(found); logSuccess(`${display(t)} is ready.`); return true; }
|
|
280
|
+
logWarn(`Install finished but '${tool}' still isn't reachable — you may need to open a new terminal.`);
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = { canon, isKnown, display, url, fwTool, pin, resolve, activate, versionOf, verLt, latest, installCmd, updateCmd, ensure, findUp, KNOWN };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// xgem update [tool] — port of lib/update.sh.
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('node:child_process');
|
|
4
|
+
const { logInfo, logSuccess, logWarn, logError, die } = require('./logger');
|
|
5
|
+
const { confirm } = require('./utils');
|
|
6
|
+
const toolchain = require('./toolchain');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TOOLS = ['flutter', 'node', 'cargo', 'go', 'fvm', 'gh', 'git', 'docker', 'python'];
|
|
9
|
+
|
|
10
|
+
async function updateOne(tool, explicit) {
|
|
11
|
+
const found = toolchain.resolve(tool);
|
|
12
|
+
if (!found) {
|
|
13
|
+
if (explicit) await toolchain.ensure(tool);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const version = toolchain.versionOf(tool, found);
|
|
18
|
+
const newest = await toolchain.latest(tool, true);
|
|
19
|
+
if (!newest) {
|
|
20
|
+
if (explicit) logWarn(`Couldn't determine the latest ${toolchain.display(tool)} version (offline, or no version source).`);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (!version || !toolchain.verLt(version, newest)) {
|
|
24
|
+
if (explicit) logSuccess(`${toolchain.display(tool)} ${version} is up to date.`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log('');
|
|
29
|
+
logInfo(`${toolchain.display(tool)}: ${version} -> ${newest}`);
|
|
30
|
+
const cmd = toolchain.updateCmd(tool, newest, found.manager);
|
|
31
|
+
if (!cmd) {
|
|
32
|
+
console.log(` No automated update route for this install (${found.manager}). Download: ${toolchain.url(tool)}`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
console.log(` Update command: ${cmd}`);
|
|
36
|
+
if (!(await confirm(`Update ${toolchain.display(tool)} to ${newest}?`))) return;
|
|
37
|
+
if (spawnSync(cmd, { stdio: 'inherit', shell: true }).status !== 0) {
|
|
38
|
+
logError('Update failed (see output above).');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
logSuccess(`${toolchain.display(tool)} update finished.`);
|
|
42
|
+
|
|
43
|
+
if (tool === 'flutter' && found.manager === 'fvm') {
|
|
44
|
+
if (await confirm(`Make Flutter ${newest} your global default (fvm global)?`)) {
|
|
45
|
+
spawnSync(`fvm global ${newest}`, { stdio: 'inherit', shell: true });
|
|
46
|
+
}
|
|
47
|
+
if ((toolchain.findUp('.fvmrc') || toolchain.findUp('.fvm')) && (await confirm(`Also pin ${newest} for the project in this directory (fvm use)?`))) {
|
|
48
|
+
spawnSync(`fvm use ${newest}`, { stdio: 'inherit', shell: true });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function cmdUpdate(target) {
|
|
54
|
+
if (target) {
|
|
55
|
+
if (!toolchain.isKnown(target)) die(`Unknown tool '${target}'. Known: ${toolchain.KNOWN.join(' ')}`);
|
|
56
|
+
await updateOne(toolchain.canon(target), true);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (process.env.XGEM_NO_NETWORK !== '1') {
|
|
60
|
+
logInfo('Checking installed tools for updates...');
|
|
61
|
+
await Promise.all(DEFAULT_TOOLS.filter((t) => toolchain.resolve(t)).map((t) => toolchain.latest(t)));
|
|
62
|
+
}
|
|
63
|
+
for (const t of DEFAULT_TOOLS) await updateOne(t, false);
|
|
64
|
+
console.log('');
|
|
65
|
+
logSuccess("Update check finished. Run 'xgem doctor' for the full toolchain report.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { cmdUpdate };
|
package/lib-win/utils.js
CHANGED
|
@@ -12,13 +12,21 @@ function detectArch() {
|
|
|
12
12
|
// works regardless of whether the tool itself supports --version.
|
|
13
13
|
function hasCmd(name) {
|
|
14
14
|
const result = spawnSync('where', [name], { stdio: 'ignore', shell: false });
|
|
15
|
-
|
|
15
|
+
if (result.status === 0) return true;
|
|
16
|
+
const toolchain = require('./toolchain');
|
|
17
|
+
if (!toolchain.isKnown(name)) return false;
|
|
18
|
+
const found = toolchain.resolve(name);
|
|
19
|
+
if (!found) return false;
|
|
20
|
+
toolchain.activate(found);
|
|
21
|
+
return true;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
function requireCmd(name, hint) {
|
|
19
25
|
if (!hasCmd(name)) {
|
|
20
26
|
const { die } = require('./logger');
|
|
21
|
-
|
|
27
|
+
const toolchain = require('./toolchain');
|
|
28
|
+
const link = toolchain.isKnown(name) ? ` Download: ${toolchain.url(toolchain.canon(name))}` : '';
|
|
29
|
+
die(hint ? `'${name}' is required but not found. ${hint}` : `'${name}' is required but not found in PATH.${link}`);
|
|
22
30
|
}
|
|
23
31
|
}
|
|
24
32
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xgem-cli",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.16",
|
|
4
4
|
"description": "Framework-aware automation CLI: scaffolds and runs clean/build/dev scripts per project type, an environment doctor, a SwiftPM-aware iOS build engine, and a git workflow helper. Runs natively on macOS, Linux, and Windows.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"xgem": "bin/xgem.js"
|