xgem-cli 2.0.0-alpha.1 → 2.0.0-alpha.12

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 (49) hide show
  1. package/README.md +53 -31
  2. package/bin/xgem +21 -14
  3. package/bin/xgem.js +235 -0
  4. package/lib/create.sh +334 -0
  5. package/lib/doctor.sh +13 -2
  6. package/lib/flutter.sh +163 -18
  7. package/lib/git.sh +157 -23
  8. package/lib/ios.sh +126 -47
  9. package/lib/scaffold.sh +24 -5
  10. package/lib/version.sh +1 -1
  11. package/lib-win/doctor.js +44 -0
  12. package/lib-win/flutter.js +101 -0
  13. package/lib-win/git.js +279 -0
  14. package/lib-win/logger.js +35 -0
  15. package/lib-win/scaffold.js +102 -0
  16. package/lib-win/utils.js +67 -0
  17. package/package.json +8 -7
  18. package/templates/node/build.sh.tmpl +9 -1
  19. package/templates/node/hard-clean.sh.tmpl +34 -2
  20. package/templates/node/lint.sh.tmpl +10 -0
  21. package/templates/node/start.sh.tmpl +9 -1
  22. package/templates/node/test.sh.tmpl +10 -0
  23. package/templates/webframework/build.sh.tmpl +10 -1
  24. package/templates/webframework/dev.sh.tmpl +9 -1
  25. package/templates/webframework/hard-clean.sh.tmpl +35 -2
  26. package/templates/webframework/lint.sh.tmpl +10 -0
  27. package/templates/webframework/test.sh.tmpl +10 -0
  28. package/templates-win/docker/build-up.mjs.tmpl +3 -0
  29. package/templates-win/docker/hard-clean.mjs.tmpl +4 -0
  30. package/templates-win/flutter/build-runner.mjs.tmpl +4 -0
  31. package/templates-win/flutter/build.mjs.tmpl +4 -0
  32. package/templates-win/flutter/hard-clean.mjs.tmpl +6 -0
  33. package/templates-win/go/build.mjs.tmpl +3 -0
  34. package/templates-win/go/hard-clean.mjs.tmpl +4 -0
  35. package/templates-win/node/build.mjs.tmpl +10 -0
  36. package/templates-win/node/hard-clean.mjs.tmpl +28 -0
  37. package/templates-win/node/lint.mjs.tmpl +10 -0
  38. package/templates-win/node/start.mjs.tmpl +10 -0
  39. package/templates-win/node/test.mjs.tmpl +10 -0
  40. package/templates-win/python/hard-clean.mjs.tmpl +8 -0
  41. package/templates-win/python/install.mjs.tmpl +12 -0
  42. package/templates-win/rust/build.mjs.tmpl +3 -0
  43. package/templates-win/rust/hard-clean.mjs.tmpl +3 -0
  44. package/templates-win/webframework/build.mjs.tmpl +10 -0
  45. package/templates-win/webframework/dev.mjs.tmpl +10 -0
  46. package/templates-win/webframework/hard-clean.mjs.tmpl +32 -0
  47. package/templates-win/webframework/lint.mjs.tmpl +10 -0
  48. package/templates-win/webframework/test.mjs.tmpl +10 -0
  49. package/scripts/check-platform.js +0 -17
@@ -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,279 @@
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 path = require('node:path');
7
+ const { spawnSync } = require('node:child_process');
8
+ const { logInfo, logSuccess, logWarn, logError, die } = require('./logger');
9
+ const { prompt, hasCmd } = require('./utils');
10
+
11
+ function git(args, opts = {}) {
12
+ return spawnSync('git', args, { stdio: 'inherit', ...opts });
13
+ }
14
+
15
+ function gh(args, opts = {}) {
16
+ return spawnSync('gh', args, { stdio: 'inherit', ...opts });
17
+ }
18
+
19
+ function ghAuthenticated() {
20
+ return spawnSync('gh', ['auth', 'status'], { stdio: 'ignore' }).status === 0;
21
+ }
22
+
23
+ function gitCapture(args) {
24
+ const result = spawnSync('git', args, { encoding: 'utf8' });
25
+ return (result.stdout || '').trim();
26
+ }
27
+
28
+ function remoteExists(name) {
29
+ return gitCapture(['remote']).split(/\r?\n/).includes(name);
30
+ }
31
+
32
+ async function cmdCmt(commitMsg) {
33
+ if (!commitMsg) die('Missing commit message!');
34
+
35
+ logInfo('Checking repository status...');
36
+ git(['status', '-s']);
37
+ console.log('');
38
+
39
+ const addChoice = (await prompt('Do you want to add ALL files (a) or INDIVIDUAL files (i)? [a/i]')).toLowerCase();
40
+ if (addChoice === 'a') {
41
+ git(['add', '.']);
42
+ logSuccess('All files staged.');
43
+ } else if (addChoice === 'i') {
44
+ const files = await prompt('Enter specific file paths to add (space separated)');
45
+ const fileList = files.split(/\s+/).filter(Boolean);
46
+ if (fileList.length === 0) die('No files given.');
47
+ git(['add', ...fileList]);
48
+ logSuccess('Selected files staged.');
49
+ } else {
50
+ die('Invalid choice. Operation aborted.');
51
+ }
52
+
53
+ const hasStaged = spawnSync('git', ['diff', '--cached', '--quiet']).status !== 0;
54
+ if (!hasStaged) {
55
+ logWarn("Nothing staged to commit — checking whether there's anything already committed to sync.");
56
+ } else {
57
+ logInfo('Committing changes...');
58
+ const commitResult = git(['commit', '-m', commitMsg]);
59
+ if (commitResult.status !== 0) {
60
+ die('Commit failed. Aborting before pull/push.');
61
+ }
62
+ }
63
+
64
+ const currentBranch = gitCapture(['branch', '--show-current']);
65
+ const remoteName = remoteExists('origin') ? 'origin' : gitCapture(['remote']).split(/\r?\n/)[0];
66
+
67
+ if (!remoteName) {
68
+ logWarn('No remote configured — sync was skipped.');
69
+ return;
70
+ }
71
+
72
+ // A pull needs something to pull FROM. If this branch has never been
73
+ // pushed before, there's no remote ref, and `git pull --rebase` fails
74
+ // with "couldn't find remote ref <branch>" — not a connection problem
75
+ // or a conflict, just a brand-new branch. Check for that case first.
76
+ const lsRemoteStatus = spawnSync('git', ['ls-remote', '--exit-code', '--heads', remoteName, currentBranch]).status;
77
+
78
+ if (lsRemoteStatus === 2) {
79
+ logInfo(`'${currentBranch}' doesn't exist on '${remoteName}' yet — pushing to create it...`);
80
+ if (git(['push', '-u', remoteName, currentBranch]).status === 0) {
81
+ logSuccess('Git workflow complete! Branch created and pushed.');
82
+ } else {
83
+ die('Push operation failed.');
84
+ }
85
+ return;
86
+ } else if (lsRemoteStatus !== 0) {
87
+ logError(`Could not reach '${remoteName}' to check for '${currentBranch}' — this looks like a real connection problem.`);
88
+ logWarn(`Your commit is safe locally. Re-run 'xgem git cmt' once connectivity is restored, or push manually: git push ${remoteName} ${currentBranch}`);
89
+ process.exit(1);
90
+ }
91
+
92
+ const aheadCount = gitCapture(['rev-list', '--count', `${remoteName}/${currentBranch}..${currentBranch}`]);
93
+ if (aheadCount === '0') {
94
+ logSuccess(`Already up to date with '${remoteName}/${currentBranch}' — nothing to push.`);
95
+ return;
96
+ }
97
+
98
+ logInfo(`Pulling updates from remote '${remoteName}' on '${currentBranch}' via rebase...`);
99
+ const pullResult = git(['pull', '--rebase', remoteName, currentBranch]);
100
+ if (pullResult.status !== 0) {
101
+ const gitDir = gitCapture(['rev-parse', '--git-dir']);
102
+ const fs = require('node:fs');
103
+ const inConflict = fs.existsSync(path.join(gitDir, 'rebase-merge')) || fs.existsSync(path.join(gitDir, 'rebase-apply'));
104
+ if (inConflict) {
105
+ logError('MERGE CONFLICT DETECTED!');
106
+ logWarn('Execution paused. Resolve conflicts to proceed.');
107
+ } else {
108
+ logError(`Could not sync with '${remoteName}' — this looks like a connection problem, not a merge conflict (see the git error above).`);
109
+ logWarn(`Your commit is safe locally. Re-run 'xgem git cmt' once connectivity is restored, or push manually: git push ${remoteName} ${currentBranch}`);
110
+ }
111
+ process.exit(1);
112
+ }
113
+
114
+ logSuccess('Clean sync pull achieved. Pushing to upstream target...');
115
+ const pushResult = git(['push', remoteName, currentBranch]);
116
+ if (pushResult.status === 0) {
117
+ logSuccess('Git workflow complete! Code cleanly committed and synchronized.');
118
+ } else {
119
+ die('Push operation failed.');
120
+ }
121
+ }
122
+
123
+ async function manualRemoteSetup(defaultRemoteName) {
124
+ let remoteName = defaultRemoteName;
125
+ const remoteUrl = await prompt('Enter remote repository URL (or leave blank to skip)');
126
+ if (!remoteUrl) return;
127
+
128
+ const userRemoteName = await prompt(`Enter remote name (default: ${remoteName})`);
129
+ if (userRemoteName) remoteName = userRemoteName;
130
+
131
+ if (remoteExists(remoteName)) {
132
+ git(['remote', 'set-url', remoteName, remoteUrl]);
133
+ logSuccess(`Remote '${remoteName}' already existed. URL updated.`);
134
+ } else {
135
+ git(['remote', 'add', remoteName, remoteUrl]);
136
+ logSuccess(`Remote '${remoteName}' successfully added.`);
137
+ }
138
+ }
139
+
140
+ async function cmdInit() {
141
+ logInfo('Initializing local Git repository...');
142
+ git(['init']);
143
+
144
+ const branchName = (await prompt('Enter branch name (default: main)')) || 'main';
145
+ git(['branch', '-M', branchName]);
146
+
147
+ git(['add', '.']);
148
+ git(['commit', '-m', 'initial changes']);
149
+
150
+ // Real GitHub repo creation, not just wiring a remote to a URL you
151
+ // already had to go create by hand — the whole point of `xgem git
152
+ // init` over plain `git init`. Falls back to the manual-URL flow if
153
+ // `gh` isn't installed/authenticated.
154
+ if (hasCmd('gh') && ghAuthenticated()) {
155
+ const createChoice = (await prompt('Create a new GitHub repository for this project right now? (Y/n)')) || 'y';
156
+ if (createChoice.toLowerCase() === 'y') {
157
+ const cwdName = path.basename(process.cwd());
158
+ const repoName = (await prompt(`Repository name (default: ${cwdName})`)) || cwdName;
159
+ const visibility = ((await prompt('Public or private? [public/private] (default: private)')) || 'private').toLowerCase();
160
+ const visFlag = visibility.startsWith('pub') ? '--public' : '--private';
161
+
162
+ const result = gh(['repo', 'create', repoName, visFlag, '--source=.', '--remote=origin', '--push']);
163
+ if (result.status === 0) {
164
+ logSuccess(`Created GitHub repo '${repoName}' and pushed '${branchName}' to it.`);
165
+ } else {
166
+ logError('gh repo create failed — falling back to manual remote setup.');
167
+ await manualRemoteSetup('origin');
168
+ }
169
+ logSuccess('Local baseline configuration setup completed.');
170
+ return;
171
+ }
172
+ } else if (hasCmd('gh')) {
173
+ logWarn("GitHub CLI (gh) is installed but not authenticated — run 'gh auth login' to enable one-step repo creation next time.");
174
+ }
175
+
176
+ await manualRemoteSetup('origin');
177
+ logSuccess('Local baseline configuration setup completed.');
178
+ }
179
+
180
+ async function cmdRmRemote() {
181
+ logInfo('Current configured remotes:');
182
+ git(['remote', '-v']);
183
+ console.log('');
184
+
185
+ const remoteName = (await prompt('Enter the short remote name to remove (e.g. origin)')) || 'origin';
186
+ if (remoteExists(remoteName)) {
187
+ git(['remote', 'remove', remoteName]);
188
+ logSuccess(`Successfully removed remote reference configuration: ${remoteName}`);
189
+ } else {
190
+ die(`Remote tracking short-name '${remoteName}' does not exist.`);
191
+ }
192
+ }
193
+
194
+ async function cmdRmBranch() {
195
+ const targetBranch = await prompt('Enter the name of the branch you want to target');
196
+ if (!targetBranch) die('Branch name cannot be empty.');
197
+
198
+ const whereChoice = (await prompt('Where do you want to delete this branch? (l = local only, r = remote only, b = both) [l/r/b]')).toLowerCase();
199
+ console.log('');
200
+
201
+ if (whereChoice === 'l' || whereChoice === 'b') {
202
+ const currentBranch = gitCapture(['branch', '--show-current']);
203
+ if (currentBranch === targetBranch) {
204
+ logWarn(`You are currently sitting on '${targetBranch}'. Switching to safe branch...`);
205
+ for (const fallback of ['main', 'master', 'dev']) {
206
+ if (git(['checkout', fallback], { stdio: 'ignore' }).status === 0) break;
207
+ }
208
+ }
209
+
210
+ logInfo(`Force deleting local branch '${targetBranch}'...`);
211
+ if (git(['branch', '-D', targetBranch]).status === 0) {
212
+ logSuccess('Successfully deleted local copy of branch.');
213
+ } else {
214
+ logWarn('Local branch could not be dropped (it may already be gone).');
215
+ }
216
+ }
217
+
218
+ if (whereChoice === 'r' || whereChoice === 'b') {
219
+ const remoteTarget = (await prompt("Enter remote identifier (short-name like 'origin')")) || 'origin';
220
+ logInfo(`Sending deletion request for remote branch '${targetBranch}' to server...`);
221
+ if (git(['push', remoteTarget, '--delete', targetBranch]).status === 0) {
222
+ logSuccess(`Successfully wiped out remote branch '${targetBranch}' from server.`);
223
+ } else {
224
+ die('Server rejected the branch drop execution request.');
225
+ }
226
+ }
227
+ }
228
+
229
+ // Lists local branches, lets the user pick one (or create a new one),
230
+ // checks it out, and remembers it as this repo's default. cmt always
231
+ // operates on whatever's actually checked out — this command's job is the
232
+ // checkout + remembering, not changing how cmt picks its branch.
233
+ async function cmdBranch() {
234
+ const branches = gitCapture(['branch', '--format=%(refname:short)']).split(/\r?\n/).filter(Boolean);
235
+ if (branches.length === 0) die('No local branches found.');
236
+
237
+ const currentBranch = gitCapture(['branch', '--show-current']);
238
+
239
+ console.log('Available branches:');
240
+ branches.forEach((b, i) => {
241
+ console.log(`${i + 1}) ${b}${b === currentBranch ? ' (current)' : ''}`);
242
+ });
243
+ const createOption = branches.length + 1;
244
+ console.log(`${createOption}) Create new branch`);
245
+
246
+ const choice = await prompt(`Select [1-${createOption}]`);
247
+
248
+ if (choice === String(createOption)) {
249
+ const newBranch = await prompt('Enter new branch name');
250
+ if (!newBranch) die('Branch name cannot be empty.');
251
+ if (git(['checkout', '-b', newBranch]).status !== 0) die(`Could not create branch '${newBranch}'.`);
252
+ git(['config', '--local', 'xgem.default-branch', newBranch]);
253
+ logSuccess(`Created and switched to '${newBranch}', set as default for this repo.`);
254
+ return;
255
+ }
256
+
257
+ const idx = parseInt(choice, 10) - 1;
258
+ if (Number.isNaN(idx) || idx < 0 || idx >= branches.length) die('Invalid selection.');
259
+
260
+ const selected = branches[idx];
261
+ if (selected !== currentBranch) {
262
+ if (git(['checkout', selected]).status !== 0) die(`Could not switch to branch '${selected}'.`);
263
+ }
264
+ git(['config', '--local', 'xgem.default-branch', selected]);
265
+ logSuccess(`Switched to '${selected}' and set as default for this repo.`);
266
+ }
267
+
268
+ async function cmdGit(sub, arg) {
269
+ switch (sub) {
270
+ case 'cmt': return cmdCmt(arg);
271
+ case 'init': return cmdInit();
272
+ case 'branch': return cmdBranch();
273
+ case 'rm-remote': return cmdRmRemote();
274
+ case 'rm-branch': return cmdRmBranch();
275
+ default: die(`Unknown git subcommand '${sub}'. Usage: xgem git <cmt|init|branch|rm-remote|rm-branch>`);
276
+ }
277
+ }
278
+
279
+ 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,102 @@
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
+ const WEB_FRAMEWORKS = ['node', 'react', 'vue', 'angular', 'next'];
26
+
27
+ // Only offer lint/test automation when the current directory's
28
+ // package.json actually declares those scripts, instead of always
29
+ // generating scripts that fail with "Missing script" on projects that
30
+ // don't have them.
31
+ function packageJsonHasScript(scriptName) {
32
+ try {
33
+ const pkg = JSON.parse(require('node:fs').readFileSync('package.json', 'utf8'));
34
+ return Boolean(pkg.scripts && pkg.scripts[scriptName]);
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ function frameworkScripts(fw) {
41
+ const base = FRAMEWORK_SCRIPTS[fw];
42
+ if (!base) return null;
43
+ if (!WEB_FRAMEWORKS.includes(fw)) return base;
44
+
45
+ const scripts = [...base];
46
+ if (packageJsonHasScript('lint')) scripts.push('lint');
47
+ if (packageJsonHasScript('test')) scripts.push('test');
48
+ return scripts;
49
+ }
50
+
51
+ function templateDir(fw) {
52
+ const templatesRoot = path.join(__dirname, '..', 'templates-win');
53
+ if (fw === 'react' || fw === 'vue' || fw === 'angular' || fw === 'next') {
54
+ return path.join(templatesRoot, 'webframework');
55
+ }
56
+ return path.join(templatesRoot, fw);
57
+ }
58
+
59
+ function injectTemplates(fw, configDir) {
60
+ const scripts = frameworkScripts(fw);
61
+ if (!scripts) die(`Unknown framework '${fw}'.`);
62
+
63
+ const srcDir = templateDir(fw);
64
+ const destDir = path.join(configDir, fw);
65
+ fs.mkdirSync(destDir, { recursive: true });
66
+
67
+ for (const script of scripts) {
68
+ const src = path.join(srcDir, `${script}.mjs.tmpl`);
69
+ const dest = path.join(destDir, `${script}.mjs`);
70
+ if (!fs.existsSync(src)) {
71
+ logWarn(`Missing template ${src}, skipping.`);
72
+ continue;
73
+ }
74
+ const content = fs.readFileSync(src, 'utf8').replace(/__FRAMEWORK__/g, fw);
75
+ fs.writeFileSync(dest, content);
76
+ }
77
+ }
78
+
79
+ function runScript(fw, script, configDir) {
80
+ const target = path.join(configDir, fw, `${script}.mjs`);
81
+ if (fs.existsSync(target)) {
82
+ logInfo(`Running script '${script}' for ${fw}...`);
83
+ const result = spawnSync(process.execPath, [target], { stdio: 'inherit' });
84
+ process.exitCode = result.status ?? 0;
85
+ } else {
86
+ logError(`Script not found at ${target}`);
87
+ const fwDir = path.join(configDir, fw);
88
+ if (fs.existsSync(fwDir)) {
89
+ console.log(`Available scripts in '${fw}':`);
90
+ for (const f of fs.readdirSync(fwDir)) {
91
+ console.log(` - ${f.replace(/\.mjs$/, '')}`);
92
+ }
93
+ }
94
+ process.exitCode = 1;
95
+ }
96
+ }
97
+
98
+ function getRemainingFrameworks(configDir) {
99
+ return ALL_FRAMEWORKS.filter((fw) => !fs.existsSync(path.join(configDir, fw)));
100
+ }
101
+
102
+ module.exports = { ALL_FRAMEWORKS, frameworkScripts, injectTemplates, runScript, getRemainingFrameworks };
@@ -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.1",
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.12",
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
- "scripts/check-platform.js"
14
+ "templates-win"
13
15
  ],
14
16
  "engines": {
15
- "node": ">=14"
17
+ "node": ">=18"
16
18
  },
17
19
  "scripts": {
18
- "preinstall": "node scripts/check-platform.js",
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",
@@ -1,3 +1,11 @@
1
1
  #!/bin/bash
2
2
  echo -e "\033[1;34mBuilding Node project...\033[0m"
3
- npm run build
3
+ if [ -f pnpm-lock.yaml ]; then
4
+ pnpm run build
5
+ elif [ -f yarn.lock ]; then
6
+ yarn build
7
+ elif [ -f bun.lockb ] || [ -f bun.lock ]; then
8
+ bun run build
9
+ else
10
+ npm run build
11
+ fi
@@ -1,3 +1,35 @@
1
1
  #!/bin/bash
2
- echo -e "\033[1;33mNuking node_modules and resetting package locks...\033[0m"
3
- rm -rf node_modules package-lock.json yarn.lock && npm install
2
+ # Detect the package manager actually in use instead of assuming npm —
3
+ # blindly deleting yarn.lock/pnpm-lock.yaml on a project that uses them
4
+ # and running `npm install` instead corrupts the project by introducing a
5
+ # conflicting lockfile.
6
+ PKG_MANAGER=npm
7
+ if [ -f pnpm-lock.yaml ]; then
8
+ PKG_MANAGER=pnpm
9
+ elif [ -f yarn.lock ]; then
10
+ PKG_MANAGER=yarn
11
+ elif [ -f bun.lockb ] || [ -f bun.lock ]; then
12
+ PKG_MANAGER=bun
13
+ elif [ -f package.json ]; then
14
+ declared=$(grep -o '"packageManager"[[:space:]]*:[[:space:]]*"[a-z]*' package.json 2>/dev/null | grep -oE '[a-z]+$')
15
+ case "$declared" in npm|yarn|pnpm|bun) PKG_MANAGER=$declared ;; esac
16
+ fi
17
+
18
+ if [ -f .nvmrc ] && command -v node >/dev/null 2>&1; then
19
+ wanted=$(tr -d 'v[:space:]' < .nvmrc)
20
+ have=$(node -v | tr -d 'v')
21
+ case "$have" in
22
+ "$wanted"*) ;;
23
+ *) echo -e "\033[33mWarning: .nvmrc wants Node $wanted, but 'node -v' reports $have.\033[0m" ;;
24
+ esac
25
+ fi
26
+
27
+ echo -e "\033[1;33mNuking node_modules and resetting package locks (package manager: $PKG_MANAGER)...\033[0m"
28
+ rm -rf node_modules
29
+
30
+ case "$PKG_MANAGER" in
31
+ yarn) rm -f yarn.lock; yarn install ;;
32
+ pnpm) rm -f pnpm-lock.yaml; pnpm install ;;
33
+ bun) rm -f bun.lockb bun.lock; bun install ;;
34
+ *) rm -f package-lock.json; npm install ;;
35
+ esac