xgem-cli 2.0.0-alpha.1 → 2.0.0-alpha.10
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 +64 -8
- package/bin/xgem +19 -4
- package/bin/xgem.js +220 -0
- package/lib/create.sh +257 -0
- package/lib/doctor.sh +13 -2
- package/lib/flutter.sh +161 -18
- package/lib/git.sh +106 -8
- package/lib/ios.sh +126 -47
- package/lib/scaffold.sh +4 -4
- package/lib/version.sh +1 -1
- package/lib-win/doctor.js +44 -0
- package/lib-win/flutter.js +101 -0
- package/lib-win/git.js +241 -0
- package/lib-win/logger.js +35 -0
- package/lib-win/scaffold.js +80 -0
- package/lib-win/utils.js +67 -0
- package/package.json +8 -7
- package/templates-win/docker/build-up.mjs.tmpl +3 -0
- package/templates-win/docker/hard-clean.mjs.tmpl +4 -0
- package/templates-win/flutter/build-runner.mjs.tmpl +4 -0
- package/templates-win/flutter/build.mjs.tmpl +4 -0
- package/templates-win/flutter/hard-clean.mjs.tmpl +6 -0
- package/templates-win/go/build.mjs.tmpl +3 -0
- package/templates-win/go/hard-clean.mjs.tmpl +4 -0
- package/templates-win/node/build.mjs.tmpl +4 -0
- package/templates-win/node/hard-clean.mjs.tmpl +9 -0
- package/templates-win/node/start.mjs.tmpl +3 -0
- package/templates-win/python/hard-clean.mjs.tmpl +8 -0
- package/templates-win/python/install.mjs.tmpl +12 -0
- package/templates-win/rust/build.mjs.tmpl +3 -0
- package/templates-win/rust/hard-clean.mjs.tmpl +3 -0
- package/templates-win/webframework/build.mjs.tmpl +3 -0
- package/templates-win/webframework/dev.mjs.tmpl +3 -0
- package/templates-win/webframework/hard-clean.mjs.tmpl +9 -0
- package/scripts/check-platform.js +0 -17
package/lib/scaffold.sh
CHANGED
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
#
|
|
12
12
|
# Depends on lib/logger.sh and XGEM_HOME (set by bin/xgem).
|
|
13
13
|
|
|
14
|
-
ALL_FRAMEWORKS=(flutter node python react vue angular go rust docker swift)
|
|
14
|
+
ALL_FRAMEWORKS=(flutter node python react vue angular next go rust docker swift)
|
|
15
15
|
|
|
16
16
|
framework_scripts() {
|
|
17
17
|
case "$1" in
|
|
18
18
|
flutter) echo "hard-clean build build-runner" ;;
|
|
19
19
|
node) echo "hard-clean build start" ;;
|
|
20
20
|
python) echo "hard-clean install" ;;
|
|
21
|
-
react|vue|angular) echo "hard-clean build dev" ;;
|
|
21
|
+
react|vue|angular|next) echo "hard-clean build dev" ;;
|
|
22
22
|
go) echo "hard-clean build" ;;
|
|
23
23
|
rust) echo "hard-clean build" ;;
|
|
24
24
|
docker) echo "hard-clean build-up" ;;
|
|
@@ -30,8 +30,8 @@ framework_scripts() {
|
|
|
30
30
|
_scaffold_template_dir() {
|
|
31
31
|
local framework=$1
|
|
32
32
|
case "$framework" in
|
|
33
|
-
react|vue|angular) echo "$XGEM_HOME/templates/webframework" ;;
|
|
34
|
-
*)
|
|
33
|
+
react|vue|angular|next) echo "$XGEM_HOME/templates/webframework" ;;
|
|
34
|
+
*) echo "$XGEM_HOME/templates/$framework" ;;
|
|
35
35
|
esac
|
|
36
36
|
}
|
|
37
37
|
|
package/lib/version.sh
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// xgem Windows engine doctor — mirrors lib/doctor.sh's general report.
|
|
2
|
+
// No iOS/SwiftPM engine exists here (see flutter.js) since Xcode has no
|
|
3
|
+
// Windows equivalent; `doctor ios` explains that instead of pretending.
|
|
4
|
+
|
|
5
|
+
const { getVersion, detectArch } = require('./utils');
|
|
6
|
+
const { paint } = require('./logger');
|
|
7
|
+
|
|
8
|
+
function printGeneral() {
|
|
9
|
+
console.log(paint('cyan', '=== xgem doctor (Windows) ==='));
|
|
10
|
+
console.log(`OS: win32`);
|
|
11
|
+
console.log(`Arch: ${detectArch()}`);
|
|
12
|
+
console.log(`git: ${getVersion('git') || 'not found'}`);
|
|
13
|
+
console.log(`flutter: ${getVersion('flutter') || 'not found'}`);
|
|
14
|
+
console.log(`dart: ${getVersion('dart') || 'not found'}`);
|
|
15
|
+
console.log(`node: ${getVersion('node') || 'not found'}`);
|
|
16
|
+
console.log(`python: ${getVersion('python') || getVersion('python3') || 'not found'}`);
|
|
17
|
+
console.log(`go: ${getVersion('go', ['version']) || 'not found'}`);
|
|
18
|
+
console.log(`cargo: ${getVersion('cargo') || 'not found'}`);
|
|
19
|
+
console.log(`docker: ${getVersion('docker') || 'not found'}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function printIos() {
|
|
23
|
+
console.log(paint('cyan', '=== xgem doctor ios (Windows) ==='));
|
|
24
|
+
console.log(paint('yellow', 'iOS/macOS builds are not available on Windows — Xcode has no Windows equivalent.'));
|
|
25
|
+
console.log('Use WSL2 with a Mac, or a real Mac, for Flutter iOS builds.');
|
|
26
|
+
const flutterVersion = getVersion('flutter');
|
|
27
|
+
if (flutterVersion) {
|
|
28
|
+
console.log(`\nflutter is installed here (${flutterVersion}) and can still build Android/Windows targets:`);
|
|
29
|
+
console.log(' xgem run flutter build (choose APK or Windows)');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function cmdDoctor(target) {
|
|
34
|
+
if (target === 'ios') {
|
|
35
|
+
printIos();
|
|
36
|
+
} else if (!target) {
|
|
37
|
+
printGeneral();
|
|
38
|
+
} else {
|
|
39
|
+
const { die } = require('./logger');
|
|
40
|
+
die(`Unknown doctor target '${target}'. Usage: xgem doctor [ios]`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { cmdDoctor, printGeneral, printIos };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// xgem Windows engine flutter commands. hard-clean/build-runner have no
|
|
2
|
+
// platform-specific dependency and work as-is. build only offers targets
|
|
3
|
+
// that are actually buildable from a Windows host (APK, Windows desktop) —
|
|
4
|
+
// iOS/macOS need Xcode (a Mac), and Flutter can't cross-compile Linux
|
|
5
|
+
// desktop from Windows either, so those fail fast with a clear message
|
|
6
|
+
// instead of attempting anything.
|
|
7
|
+
|
|
8
|
+
const fs = require('node:fs');
|
|
9
|
+
const { spawnSync } = require('node:child_process');
|
|
10
|
+
const { logInfo, logSuccess, logError, die } = require('./logger');
|
|
11
|
+
const { prompt, requireCmd } = require('./utils');
|
|
12
|
+
|
|
13
|
+
function flutter(args) {
|
|
14
|
+
return spawnSync('flutter', args, { stdio: 'inherit' });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function cmdHardClean() {
|
|
18
|
+
requireCmd('flutter', 'Install Flutter: https://docs.flutter.dev/get-started/install/windows');
|
|
19
|
+
logInfo('Cleaning Flutter project...');
|
|
20
|
+
fs.rmSync('pubspec.lock', { force: true });
|
|
21
|
+
flutter(['clean']);
|
|
22
|
+
logInfo('Getting Flutter packages...');
|
|
23
|
+
flutter(['pub', 'get']);
|
|
24
|
+
logInfo('Running Flutter...');
|
|
25
|
+
flutter(['run', '-v']);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function cmdBuildRunner() {
|
|
29
|
+
requireCmd('flutter', 'Install Flutter: https://docs.flutter.dev/get-started/install/windows');
|
|
30
|
+
logInfo('Running Flutter Build Runner...');
|
|
31
|
+
const verboseChoice = ((await prompt('Run in verbose mode? (Y/n)')) || 'Y').toLowerCase();
|
|
32
|
+
const args = ['pub', 'run', 'build_runner', 'build', '--delete-conflicting-outputs'];
|
|
33
|
+
if (verboseChoice === 'y') args.push('-v');
|
|
34
|
+
logInfo(`Executing: flutter ${args.join(' ')}`);
|
|
35
|
+
flutter(args);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function pubspecVersion() {
|
|
39
|
+
if (!fs.existsSync('pubspec.yaml')) return null;
|
|
40
|
+
const content = fs.readFileSync('pubspec.yaml', 'utf8');
|
|
41
|
+
const match = content.match(/^version:\s*(\S+)/m);
|
|
42
|
+
return match ? match[1] : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function cmdBuild() {
|
|
46
|
+
requireCmd('flutter', 'Install Flutter: https://docs.flutter.dev/get-started/install/windows');
|
|
47
|
+
console.log('--- Flutter Build Orchestrator (Windows) ---');
|
|
48
|
+
|
|
49
|
+
let currentName = '';
|
|
50
|
+
let currentNum = '';
|
|
51
|
+
const currentFull = pubspecVersion();
|
|
52
|
+
if (currentFull) {
|
|
53
|
+
[currentName, currentNum = '1'] = currentFull.split('+');
|
|
54
|
+
logInfo(`Current pubspec.yaml version: ${currentFull}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const buildName = (await prompt('Enter build version name', currentName || '1.0.0'));
|
|
58
|
+
const buildNumber = (await prompt('Enter build number', currentNum || '1'));
|
|
59
|
+
const isRelease = ((await prompt('Is this a release build? (Y/n)', 'y'))).toLowerCase();
|
|
60
|
+
|
|
61
|
+
if (fs.existsSync('pubspec.yaml')) {
|
|
62
|
+
const newVersion = `${buildName}+${buildNumber}`;
|
|
63
|
+
logSuccess(`Updating pubspec.yaml to version: ${newVersion}`);
|
|
64
|
+
const content = fs.readFileSync('pubspec.yaml', 'utf8');
|
|
65
|
+
fs.writeFileSync('pubspec.yaml', content.replace(/^version:.*/m, `version: ${newVersion}`));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
console.log('\nSelect Target Platform:');
|
|
69
|
+
console.log('1) APK (Android)');
|
|
70
|
+
console.log('2) App Bundle (Android, .aab — required for Play Store uploads)');
|
|
71
|
+
console.log('3) Windows');
|
|
72
|
+
console.log('(iOS/macOS require a Mac; Linux desktop can\'t be cross-built from Windows.)');
|
|
73
|
+
const platformChoice = await prompt('Choose [1-3]');
|
|
74
|
+
|
|
75
|
+
const mode = (isRelease === 'n') ? '--debug' : '--release';
|
|
76
|
+
const buildArgs = [`--build-name=${buildName}`, `--build-number=${buildNumber}`];
|
|
77
|
+
|
|
78
|
+
if (platformChoice === '1') {
|
|
79
|
+
logInfo(`Building APK (${mode})...`);
|
|
80
|
+
flutter(['build', 'apk', mode, ...buildArgs]);
|
|
81
|
+
} else if (platformChoice === '2') {
|
|
82
|
+
logInfo(`Building App Bundle (${mode})...`);
|
|
83
|
+
flutter(['build', 'appbundle', mode, ...buildArgs]);
|
|
84
|
+
} else if (platformChoice === '3') {
|
|
85
|
+
logInfo(`Building Windows desktop app (${mode})...`);
|
|
86
|
+
flutter(['build', 'windows', mode, ...buildArgs]);
|
|
87
|
+
} else {
|
|
88
|
+
die(`'${platformChoice}' isn't buildable from Windows. iOS/macOS need a Mac (xgem doctor ios explains more); Linux desktop needs a Linux host.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function cmdFlutter(script) {
|
|
93
|
+
switch (script) {
|
|
94
|
+
case 'hard-clean': return cmdHardClean();
|
|
95
|
+
case 'build': return cmdBuild();
|
|
96
|
+
case 'build-runner': return cmdBuildRunner();
|
|
97
|
+
default: die(`No native flutter command for '${script}'.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = { cmdFlutter };
|
package/lib-win/git.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// xgem Windows engine git workflow — port of lib/git.sh (cmt/init/rm-remote/rm-branch).
|
|
2
|
+
// git.exe behaves the same on Windows, so this is a straight port; the
|
|
3
|
+
// commit-exit-code check and origin-preference fix already in lib/git.sh
|
|
4
|
+
// carry over here too.
|
|
5
|
+
|
|
6
|
+
const { spawnSync } = require('node:child_process');
|
|
7
|
+
const { logInfo, logSuccess, logWarn, logError, die } = require('./logger');
|
|
8
|
+
const { prompt } = require('./utils');
|
|
9
|
+
|
|
10
|
+
function git(args, opts = {}) {
|
|
11
|
+
return spawnSync('git', args, { stdio: 'inherit', ...opts });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function gitCapture(args) {
|
|
15
|
+
const result = spawnSync('git', args, { encoding: 'utf8' });
|
|
16
|
+
return (result.stdout || '').trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function remoteExists(name) {
|
|
20
|
+
return gitCapture(['remote']).split(/\r?\n/).includes(name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function cmdCmt(commitMsg) {
|
|
24
|
+
if (!commitMsg) die('Missing commit message!');
|
|
25
|
+
|
|
26
|
+
logInfo('Checking repository status...');
|
|
27
|
+
git(['status', '-s']);
|
|
28
|
+
console.log('');
|
|
29
|
+
|
|
30
|
+
const addChoice = (await prompt('Do you want to add ALL files (a) or INDIVIDUAL files (i)? [a/i]')).toLowerCase();
|
|
31
|
+
if (addChoice === 'a') {
|
|
32
|
+
git(['add', '.']);
|
|
33
|
+
logSuccess('All files staged.');
|
|
34
|
+
} else if (addChoice === 'i') {
|
|
35
|
+
const files = await prompt('Enter specific file paths to add (space separated)');
|
|
36
|
+
const fileList = files.split(/\s+/).filter(Boolean);
|
|
37
|
+
if (fileList.length === 0) die('No files given.');
|
|
38
|
+
git(['add', ...fileList]);
|
|
39
|
+
logSuccess('Selected files staged.');
|
|
40
|
+
} else {
|
|
41
|
+
die('Invalid choice. Operation aborted.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const hasStaged = spawnSync('git', ['diff', '--cached', '--quiet']).status !== 0;
|
|
45
|
+
if (!hasStaged) {
|
|
46
|
+
logWarn("Nothing staged to commit — checking whether there's anything already committed to sync.");
|
|
47
|
+
} else {
|
|
48
|
+
logInfo('Committing changes...');
|
|
49
|
+
const commitResult = git(['commit', '-m', commitMsg]);
|
|
50
|
+
if (commitResult.status !== 0) {
|
|
51
|
+
die('Commit failed. Aborting before pull/push.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const currentBranch = gitCapture(['branch', '--show-current']);
|
|
56
|
+
const remoteName = remoteExists('origin') ? 'origin' : gitCapture(['remote']).split(/\r?\n/)[0];
|
|
57
|
+
|
|
58
|
+
if (!remoteName) {
|
|
59
|
+
logWarn('No remote configured — sync was skipped.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A pull needs something to pull FROM. If this branch has never been
|
|
64
|
+
// pushed before, there's no remote ref, and `git pull --rebase` fails
|
|
65
|
+
// with "couldn't find remote ref <branch>" — not a connection problem
|
|
66
|
+
// or a conflict, just a brand-new branch. Check for that case first.
|
|
67
|
+
const lsRemoteStatus = spawnSync('git', ['ls-remote', '--exit-code', '--heads', remoteName, currentBranch]).status;
|
|
68
|
+
|
|
69
|
+
if (lsRemoteStatus === 2) {
|
|
70
|
+
logInfo(`'${currentBranch}' doesn't exist on '${remoteName}' yet — pushing to create it...`);
|
|
71
|
+
if (git(['push', '-u', remoteName, currentBranch]).status === 0) {
|
|
72
|
+
logSuccess('Git workflow complete! Branch created and pushed.');
|
|
73
|
+
} else {
|
|
74
|
+
die('Push operation failed.');
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
} else if (lsRemoteStatus !== 0) {
|
|
78
|
+
logError(`Could not reach '${remoteName}' to check for '${currentBranch}' — this looks like a real connection problem.`);
|
|
79
|
+
logWarn(`Your commit is safe locally. Re-run 'xgem git cmt' once connectivity is restored, or push manually: git push ${remoteName} ${currentBranch}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const aheadCount = gitCapture(['rev-list', '--count', `${remoteName}/${currentBranch}..${currentBranch}`]);
|
|
84
|
+
if (aheadCount === '0') {
|
|
85
|
+
logSuccess(`Already up to date with '${remoteName}/${currentBranch}' — nothing to push.`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
logInfo(`Pulling updates from remote '${remoteName}' on '${currentBranch}' via rebase...`);
|
|
90
|
+
const pullResult = git(['pull', '--rebase', remoteName, currentBranch]);
|
|
91
|
+
if (pullResult.status !== 0) {
|
|
92
|
+
const gitDir = gitCapture(['rev-parse', '--git-dir']);
|
|
93
|
+
const path = require('node:path');
|
|
94
|
+
const fs = require('node:fs');
|
|
95
|
+
const inConflict = fs.existsSync(path.join(gitDir, 'rebase-merge')) || fs.existsSync(path.join(gitDir, 'rebase-apply'));
|
|
96
|
+
if (inConflict) {
|
|
97
|
+
logError('MERGE CONFLICT DETECTED!');
|
|
98
|
+
logWarn('Execution paused. Resolve conflicts to proceed.');
|
|
99
|
+
} else {
|
|
100
|
+
logError(`Could not sync with '${remoteName}' — this looks like a connection problem, not a merge conflict (see the git error above).`);
|
|
101
|
+
logWarn(`Your commit is safe locally. Re-run 'xgem git cmt' once connectivity is restored, or push manually: git push ${remoteName} ${currentBranch}`);
|
|
102
|
+
}
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
logSuccess('Clean sync pull achieved. Pushing to upstream target...');
|
|
107
|
+
const pushResult = git(['push', remoteName, currentBranch]);
|
|
108
|
+
if (pushResult.status === 0) {
|
|
109
|
+
logSuccess('Git workflow complete! Code cleanly committed and synchronized.');
|
|
110
|
+
} else {
|
|
111
|
+
die('Push operation failed.');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function cmdInit() {
|
|
116
|
+
logInfo('Initializing local Git repository...');
|
|
117
|
+
git(['init']);
|
|
118
|
+
|
|
119
|
+
let remoteName = 'origin';
|
|
120
|
+
const remoteUrl = await prompt('Enter remote repository URL');
|
|
121
|
+
if (remoteUrl) {
|
|
122
|
+
const userRemoteName = await prompt('Enter remote name (default: origin)');
|
|
123
|
+
if (userRemoteName) remoteName = userRemoteName;
|
|
124
|
+
|
|
125
|
+
if (remoteExists(remoteName)) {
|
|
126
|
+
git(['remote', 'set-url', remoteName, remoteUrl]);
|
|
127
|
+
logSuccess(`Remote '${remoteName}' already existed. URL updated.`);
|
|
128
|
+
} else {
|
|
129
|
+
git(['remote', 'add', remoteName, remoteUrl]);
|
|
130
|
+
logSuccess(`Remote '${remoteName}' successfully added.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const branchName = (await prompt('Enter branch name (default: main)')) || 'main';
|
|
135
|
+
git(['branch', '-M', branchName]);
|
|
136
|
+
|
|
137
|
+
git(['add', '.']);
|
|
138
|
+
git(['commit', '-m', 'initial changes']);
|
|
139
|
+
logSuccess('Local baseline configuration setup completed.');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function cmdRmRemote() {
|
|
143
|
+
logInfo('Current configured remotes:');
|
|
144
|
+
git(['remote', '-v']);
|
|
145
|
+
console.log('');
|
|
146
|
+
|
|
147
|
+
const remoteName = (await prompt('Enter the short remote name to remove (e.g. origin)')) || 'origin';
|
|
148
|
+
if (remoteExists(remoteName)) {
|
|
149
|
+
git(['remote', 'remove', remoteName]);
|
|
150
|
+
logSuccess(`Successfully removed remote reference configuration: ${remoteName}`);
|
|
151
|
+
} else {
|
|
152
|
+
die(`Remote tracking short-name '${remoteName}' does not exist.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function cmdRmBranch() {
|
|
157
|
+
const targetBranch = await prompt('Enter the name of the branch you want to target');
|
|
158
|
+
if (!targetBranch) die('Branch name cannot be empty.');
|
|
159
|
+
|
|
160
|
+
const whereChoice = (await prompt('Where do you want to delete this branch? (l = local only, r = remote only, b = both) [l/r/b]')).toLowerCase();
|
|
161
|
+
console.log('');
|
|
162
|
+
|
|
163
|
+
if (whereChoice === 'l' || whereChoice === 'b') {
|
|
164
|
+
const currentBranch = gitCapture(['branch', '--show-current']);
|
|
165
|
+
if (currentBranch === targetBranch) {
|
|
166
|
+
logWarn(`You are currently sitting on '${targetBranch}'. Switching to safe branch...`);
|
|
167
|
+
for (const fallback of ['main', 'master', 'dev']) {
|
|
168
|
+
if (git(['checkout', fallback], { stdio: 'ignore' }).status === 0) break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
logInfo(`Force deleting local branch '${targetBranch}'...`);
|
|
173
|
+
if (git(['branch', '-D', targetBranch]).status === 0) {
|
|
174
|
+
logSuccess('Successfully deleted local copy of branch.');
|
|
175
|
+
} else {
|
|
176
|
+
logWarn('Local branch could not be dropped (it may already be gone).');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (whereChoice === 'r' || whereChoice === 'b') {
|
|
181
|
+
const remoteTarget = (await prompt("Enter remote identifier (short-name like 'origin')")) || 'origin';
|
|
182
|
+
logInfo(`Sending deletion request for remote branch '${targetBranch}' to server...`);
|
|
183
|
+
if (git(['push', remoteTarget, '--delete', targetBranch]).status === 0) {
|
|
184
|
+
logSuccess(`Successfully wiped out remote branch '${targetBranch}' from server.`);
|
|
185
|
+
} else {
|
|
186
|
+
die('Server rejected the branch drop execution request.');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Lists local branches, lets the user pick one (or create a new one),
|
|
192
|
+
// checks it out, and remembers it as this repo's default. cmt always
|
|
193
|
+
// operates on whatever's actually checked out — this command's job is the
|
|
194
|
+
// checkout + remembering, not changing how cmt picks its branch.
|
|
195
|
+
async function cmdBranch() {
|
|
196
|
+
const branches = gitCapture(['branch', '--format=%(refname:short)']).split(/\r?\n/).filter(Boolean);
|
|
197
|
+
if (branches.length === 0) die('No local branches found.');
|
|
198
|
+
|
|
199
|
+
const currentBranch = gitCapture(['branch', '--show-current']);
|
|
200
|
+
|
|
201
|
+
console.log('Available branches:');
|
|
202
|
+
branches.forEach((b, i) => {
|
|
203
|
+
console.log(`${i + 1}) ${b}${b === currentBranch ? ' (current)' : ''}`);
|
|
204
|
+
});
|
|
205
|
+
const createOption = branches.length + 1;
|
|
206
|
+
console.log(`${createOption}) Create new branch`);
|
|
207
|
+
|
|
208
|
+
const choice = await prompt(`Select [1-${createOption}]`);
|
|
209
|
+
|
|
210
|
+
if (choice === String(createOption)) {
|
|
211
|
+
const newBranch = await prompt('Enter new branch name');
|
|
212
|
+
if (!newBranch) die('Branch name cannot be empty.');
|
|
213
|
+
if (git(['checkout', '-b', newBranch]).status !== 0) die(`Could not create branch '${newBranch}'.`);
|
|
214
|
+
git(['config', '--local', 'xgem.default-branch', newBranch]);
|
|
215
|
+
logSuccess(`Created and switched to '${newBranch}', set as default for this repo.`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const idx = parseInt(choice, 10) - 1;
|
|
220
|
+
if (Number.isNaN(idx) || idx < 0 || idx >= branches.length) die('Invalid selection.');
|
|
221
|
+
|
|
222
|
+
const selected = branches[idx];
|
|
223
|
+
if (selected !== currentBranch) {
|
|
224
|
+
if (git(['checkout', selected]).status !== 0) die(`Could not switch to branch '${selected}'.`);
|
|
225
|
+
}
|
|
226
|
+
git(['config', '--local', 'xgem.default-branch', selected]);
|
|
227
|
+
logSuccess(`Switched to '${selected}' and set as default for this repo.`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function cmdGit(sub, arg) {
|
|
231
|
+
switch (sub) {
|
|
232
|
+
case 'cmt': return cmdCmt(arg);
|
|
233
|
+
case 'init': return cmdInit();
|
|
234
|
+
case 'branch': return cmdBranch();
|
|
235
|
+
case 'rm-remote': return cmdRmRemote();
|
|
236
|
+
case 'rm-branch': return cmdRmBranch();
|
|
237
|
+
default: die(`Unknown git subcommand '${sub}'. Usage: xgem git <cmt|init|branch|rm-remote|rm-branch>`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = { cmdGit };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// xgem Windows engine logger — mirrors lib/logger.sh's levels/colors.
|
|
2
|
+
// Manual ANSI codes (no dependency): Windows 10+ cmd.exe and PowerShell
|
|
3
|
+
// both support ANSI escapes natively, and we skip color entirely when
|
|
4
|
+
// stdout isn't a TTY (e.g. piped output).
|
|
5
|
+
|
|
6
|
+
const isTTY = process.stdout.isTTY === true;
|
|
7
|
+
|
|
8
|
+
const colors = {
|
|
9
|
+
red: '\x1b[31m',
|
|
10
|
+
green: '\x1b[32m',
|
|
11
|
+
yellow: '\x1b[33m',
|
|
12
|
+
blue: '\x1b[1;34m',
|
|
13
|
+
cyan: '\x1b[1;36m',
|
|
14
|
+
gray: '\x1b[90m',
|
|
15
|
+
reset: '\x1b[0m',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function paint(color, text) {
|
|
19
|
+
return isTTY ? `${colors[color]}${text}${colors.reset}` : text;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const VERBOSE = process.env.XGEM_VERBOSE === '1';
|
|
23
|
+
|
|
24
|
+
function logInfo(msg) { console.log(`${paint('blue', '[INFO]')} ${msg}`); }
|
|
25
|
+
function logSuccess(msg) { console.log(`${paint('green', '[ OK ]')} ${msg}`); }
|
|
26
|
+
function logWarn(msg) { console.error(`${paint('yellow', '[WARN]')} ${msg}`); }
|
|
27
|
+
function logError(msg) { console.error(`${paint('red', '[FAIL]')} ${msg}`); }
|
|
28
|
+
function logDebug(msg) { if (VERBOSE) console.error(`${paint('gray', `[DBG ] ${msg}`)}`); }
|
|
29
|
+
|
|
30
|
+
function die(msg, code = 1) {
|
|
31
|
+
logError(msg);
|
|
32
|
+
process.exit(code);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { paint, logInfo, logSuccess, logWarn, logError, logDebug, die, isTTY };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// xgem Windows engine generic scaffold — mirrors lib/scaffold.sh, but
|
|
2
|
+
// writes/runs .mjs templates instead of .sh. `swift` is intentionally
|
|
3
|
+
// omitted: there's no meaningful Windows Swift toolchain story.
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { spawnSync } = require('node:child_process');
|
|
8
|
+
const { logInfo, logSuccess, logWarn, logError, die } = require('./logger');
|
|
9
|
+
|
|
10
|
+
const ALL_FRAMEWORKS = ['flutter', 'node', 'python', 'react', 'vue', 'angular', 'next', 'go', 'rust', 'docker'];
|
|
11
|
+
|
|
12
|
+
const FRAMEWORK_SCRIPTS = {
|
|
13
|
+
flutter: ['hard-clean', 'build', 'build-runner'],
|
|
14
|
+
node: ['hard-clean', 'build', 'start'],
|
|
15
|
+
python: ['hard-clean', 'install'],
|
|
16
|
+
react: ['hard-clean', 'build', 'dev'],
|
|
17
|
+
vue: ['hard-clean', 'build', 'dev'],
|
|
18
|
+
angular: ['hard-clean', 'build', 'dev'],
|
|
19
|
+
next: ['hard-clean', 'build', 'dev'],
|
|
20
|
+
go: ['hard-clean', 'build'],
|
|
21
|
+
rust: ['hard-clean', 'build'],
|
|
22
|
+
docker: ['hard-clean', 'build-up'],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function frameworkScripts(fw) {
|
|
26
|
+
return FRAMEWORK_SCRIPTS[fw] || null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function templateDir(fw) {
|
|
30
|
+
const templatesRoot = path.join(__dirname, '..', 'templates-win');
|
|
31
|
+
if (fw === 'react' || fw === 'vue' || fw === 'angular' || fw === 'next') {
|
|
32
|
+
return path.join(templatesRoot, 'webframework');
|
|
33
|
+
}
|
|
34
|
+
return path.join(templatesRoot, fw);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function injectTemplates(fw, configDir) {
|
|
38
|
+
const scripts = frameworkScripts(fw);
|
|
39
|
+
if (!scripts) die(`Unknown framework '${fw}'.`);
|
|
40
|
+
|
|
41
|
+
const srcDir = templateDir(fw);
|
|
42
|
+
const destDir = path.join(configDir, fw);
|
|
43
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
for (const script of scripts) {
|
|
46
|
+
const src = path.join(srcDir, `${script}.mjs.tmpl`);
|
|
47
|
+
const dest = path.join(destDir, `${script}.mjs`);
|
|
48
|
+
if (!fs.existsSync(src)) {
|
|
49
|
+
logWarn(`Missing template ${src}, skipping.`);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const content = fs.readFileSync(src, 'utf8').replace(/__FRAMEWORK__/g, fw);
|
|
53
|
+
fs.writeFileSync(dest, content);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function runScript(fw, script, configDir) {
|
|
58
|
+
const target = path.join(configDir, fw, `${script}.mjs`);
|
|
59
|
+
if (fs.existsSync(target)) {
|
|
60
|
+
logInfo(`Running script '${script}' for ${fw}...`);
|
|
61
|
+
const result = spawnSync(process.execPath, [target], { stdio: 'inherit' });
|
|
62
|
+
process.exitCode = result.status ?? 0;
|
|
63
|
+
} else {
|
|
64
|
+
logError(`Script not found at ${target}`);
|
|
65
|
+
const fwDir = path.join(configDir, fw);
|
|
66
|
+
if (fs.existsSync(fwDir)) {
|
|
67
|
+
console.log(`Available scripts in '${fw}':`);
|
|
68
|
+
for (const f of fs.readdirSync(fwDir)) {
|
|
69
|
+
console.log(` - ${f.replace(/\.mjs$/, '')}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getRemainingFrameworks(configDir) {
|
|
77
|
+
return ALL_FRAMEWORKS.filter((fw) => !fs.existsSync(path.join(configDir, fw)));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { ALL_FRAMEWORKS, frameworkScripts, injectTemplates, runScript, getRemainingFrameworks };
|
package/lib-win/utils.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// xgem Windows engine utilities — mirrors lib/utils.sh.
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('node:child_process');
|
|
4
|
+
const readline = require('node:readline');
|
|
5
|
+
const { logInfo, logDebug } = require('./logger');
|
|
6
|
+
|
|
7
|
+
function detectArch() {
|
|
8
|
+
return process.arch === 'x64' ? 'x64' : process.arch;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// hasCmd(name) -> boolean, checked via `where` (a Windows builtin) so it
|
|
12
|
+
// works regardless of whether the tool itself supports --version.
|
|
13
|
+
function hasCmd(name) {
|
|
14
|
+
const result = spawnSync('where', [name], { stdio: 'ignore', shell: false });
|
|
15
|
+
return result.status === 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function requireCmd(name, hint) {
|
|
19
|
+
if (!hasCmd(name)) {
|
|
20
|
+
const { die } = require('./logger');
|
|
21
|
+
die(hint ? `'${name}' is required but not found. ${hint}` : `'${name}' is required but not found in PATH.`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// getVersion(name, args) -> first line of stdout, or null if the command
|
|
26
|
+
// isn't present / errors out.
|
|
27
|
+
function getVersion(name, args = ['--version']) {
|
|
28
|
+
if (!hasCmd(name)) return null;
|
|
29
|
+
const result = spawnSync(name, args, { encoding: 'utf8' });
|
|
30
|
+
if (result.error || result.status !== 0) return null;
|
|
31
|
+
const out = (result.stdout || result.stderr || '').split(/\r?\n/)[0];
|
|
32
|
+
return out ? out.trim() : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// confirm(prompt) -> Promise<boolean>. Honors XGEM_YES (auto-approve) and
|
|
36
|
+
// XGEM_DRY_RUN (always decline, just like lib/utils.sh's confirm()).
|
|
37
|
+
function confirm(prompt) {
|
|
38
|
+
if (process.env.XGEM_DRY_RUN === '1') {
|
|
39
|
+
logInfo(`(dry-run) would prompt: ${prompt}`);
|
|
40
|
+
return Promise.resolve(false);
|
|
41
|
+
}
|
|
42
|
+
if (process.env.XGEM_YES === '1') {
|
|
43
|
+
logDebug(`auto-confirmed (--yes): ${prompt}`);
|
|
44
|
+
return Promise.resolve(true);
|
|
45
|
+
}
|
|
46
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
rl.question(`${prompt} [y/N]: `, (answer) => {
|
|
49
|
+
rl.close();
|
|
50
|
+
resolve(answer.trim().toLowerCase() === 'y');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// prompt(question, defaultValue) -> Promise<string>
|
|
56
|
+
function prompt(question, defaultValue = '') {
|
|
57
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
58
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
61
|
+
rl.close();
|
|
62
|
+
resolve(answer.trim() || defaultValue);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { detectArch, hasCmd, requireCmd, getVersion, confirm, prompt };
|
package/package.json
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xgem-cli",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
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.",
|
|
3
|
+
"version": "2.0.0-alpha.10",
|
|
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
|
-
"xgem": "bin/xgem"
|
|
6
|
+
"xgem": "bin/xgem.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
9
|
"bin/xgem",
|
|
10
|
+
"bin/xgem.js",
|
|
10
11
|
"lib",
|
|
12
|
+
"lib-win",
|
|
11
13
|
"templates",
|
|
12
|
-
"
|
|
14
|
+
"templates-win"
|
|
13
15
|
],
|
|
14
16
|
"engines": {
|
|
15
|
-
"node": ">=
|
|
17
|
+
"node": ">=18"
|
|
16
18
|
},
|
|
17
19
|
"scripts": {
|
|
18
|
-
"
|
|
19
|
-
"test": "bash -n bin/xgem && for f in lib/*.sh; do bash -n \"$f\"; done"
|
|
20
|
+
"test": "bash -n bin/xgem && for f in lib/*.sh; do bash -n \"$f\"; done && for f in bin/xgem.js lib-win/*.js; do node --check \"$f\"; done"
|
|
20
21
|
},
|
|
21
22
|
"keywords": [
|
|
22
23
|
"cli",
|