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

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.
@@ -0,0 +1,143 @@
1
+ // xgem Windows engine release command — port of lib/release.sh.
2
+
3
+ const fs = require('node:fs');
4
+ const { spawnSync } = require('node:child_process');
5
+ const { logInfo, logSuccess, logWarn, die } = require('./logger');
6
+ const { prompt, confirm } = require('./utils');
7
+ const { defaultRemote } = require('./git');
8
+
9
+ const VERSION_FILES = ['package.json', 'pubspec.yaml', 'Cargo.toml', 'pyproject.toml'];
10
+
11
+ function detectVersionFiles() {
12
+ return VERSION_FILES.filter((f) => fs.existsSync(f));
13
+ }
14
+
15
+ function readVersion(file) {
16
+ const content = fs.readFileSync(file, 'utf8');
17
+ if (file === 'package.json') {
18
+ const m = content.match(/"version"\s*:\s*"([^"]+)"/);
19
+ return m ? m[1] : '';
20
+ }
21
+ if (file === 'pubspec.yaml') {
22
+ const m = content.match(/^version:\s*(\S+)/m);
23
+ return m ? m[1] : '';
24
+ }
25
+ const m = content.match(/^version\s*=\s*"([^"]+)"/m);
26
+ return m ? m[1] : '';
27
+ }
28
+
29
+ function writeVersion(file, newVersion) {
30
+ const content = fs.readFileSync(file, 'utf8');
31
+ let updated;
32
+ if (file === 'package.json') {
33
+ updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
34
+ } else if (file === 'pubspec.yaml') {
35
+ updated = content.replace(/^version:.*/m, `version: ${newVersion}`);
36
+ } else {
37
+ updated = content.replace(/^version\s*=\s*"[^"]+"/m, `version = "${newVersion}"`);
38
+ }
39
+ fs.writeFileSync(file, updated);
40
+ }
41
+
42
+ function bumpVersion(version, kind) {
43
+ const plusIdx = version.indexOf('+');
44
+ const core = plusIdx === -1 ? version : version.slice(0, plusIdx);
45
+ const suffix = plusIdx === -1 ? '' : version.slice(plusIdx);
46
+
47
+ const m = core.match(/^(\d+)\.(\d+)\.(\d+)$/);
48
+ if (!m) return '';
49
+ let [major, minor, patch] = m.slice(1).map(Number);
50
+ if (kind === 'major') { major += 1; minor = 0; patch = 0; }
51
+ else if (kind === 'minor') { minor += 1; patch = 0; }
52
+ else { patch += 1; }
53
+ return `${major}.${minor}.${patch}${suffix}`;
54
+ }
55
+
56
+ function git(args, opts = {}) {
57
+ return spawnSync('git', args, { stdio: 'inherit', ...opts });
58
+ }
59
+
60
+ function gitCapture(args) {
61
+ const result = spawnSync('git', args, { encoding: 'utf8' });
62
+ return (result.stdout || '').trim();
63
+ }
64
+
65
+ function writeChangelog(version, lastTag) {
66
+ const range = lastTag ? `${lastTag}..HEAD` : 'HEAD';
67
+ const commits = gitCapture(['log', '--format=%s', range]).split(/\r?\n/).filter(Boolean);
68
+ const features = commits.filter((c) => c.startsWith('feat'));
69
+ const fixes = commits.filter((c) => c.startsWith('fix'));
70
+ const other = commits.filter((c) => !c.startsWith('feat') && !c.startsWith('fix'));
71
+
72
+ let section = `## v${version} - ${new Date().toISOString().slice(0, 10)}\n`;
73
+ if (features.length) section += `\n### Features\n${features.map((c) => `- ${c}`).join('\n')}\n`;
74
+ if (fixes.length) section += `\n### Fixes\n${fixes.map((c) => `- ${c}`).join('\n')}\n`;
75
+ if (other.length) section += `\n### Other\n${other.map((c) => `- ${c}`).join('\n')}\n`;
76
+
77
+ const existing = fs.existsSync('CHANGELOG.md')
78
+ ? fs.readFileSync('CHANGELOG.md', 'utf8').split('\n').slice(1).join('\n')
79
+ : '';
80
+ fs.writeFileSync('CHANGELOG.md', `# Changelog\n\n${section}\n${existing}`);
81
+ }
82
+
83
+ async function cmdRelease(bumpKind) {
84
+ const versionFiles = detectVersionFiles();
85
+ if (versionFiles.length === 0) {
86
+ die('No recognized version file (package.json/pubspec.yaml/Cargo.toml/pyproject.toml) found here.');
87
+ }
88
+
89
+ let file = versionFiles[0];
90
+ if (versionFiles.length > 1) {
91
+ console.log('Multiple version files found:');
92
+ versionFiles.forEach((f, i) => console.log(`${i + 1}) ${f}`));
93
+ const choice = await prompt(`Which is the canonical one to bump? [1-${versionFiles.length}]`);
94
+ const idx = parseInt(choice, 10) - 1;
95
+ if (Number.isNaN(idx) || idx < 0 || idx >= versionFiles.length) die('Invalid selection.');
96
+ file = versionFiles[idx];
97
+ }
98
+
99
+ const current = readVersion(file);
100
+ if (!current) die(`Could not read a version from ${file}.`);
101
+ logInfo(`Current version in ${file}: ${current}`);
102
+
103
+ if (bumpKind && !['major', 'minor', 'patch'].includes(bumpKind)) {
104
+ die(`Unknown bump kind '${bumpKind}'. Usage: xgem release [major|minor|patch]`);
105
+ }
106
+ if (!bumpKind) {
107
+ bumpKind = (await prompt('Bump [major/minor/patch]', 'patch')) || 'patch';
108
+ }
109
+
110
+ let newVersion = bumpVersion(current, bumpKind);
111
+ if (!newVersion) {
112
+ logWarn(`'${current}' isn't a plain X.Y.Z version — can't bump it automatically.`);
113
+ newVersion = await prompt('Enter the new version directly');
114
+ if (!newVersion) die('A new version is required.');
115
+ }
116
+
117
+ logInfo(`${current} -> ${newVersion}`);
118
+ if (!(await confirm('Write this version, update CHANGELOG.md, commit, and tag?'))) {
119
+ logInfo('Cancelled.');
120
+ return;
121
+ }
122
+
123
+ writeVersion(file, newVersion);
124
+
125
+ const lastTag = gitCapture(['describe', '--tags', '--abbrev=0']);
126
+ writeChangelog(newVersion, lastTag);
127
+
128
+ git(['add', file, 'CHANGELOG.md']);
129
+ if (git(['commit', '-m', `chore(release): v${newVersion}`]).status !== 0) die('Commit failed.');
130
+ if (git(['tag', '-a', `v${newVersion}`, '-m', `v${newVersion}`]).status !== 0) die('Tag failed.');
131
+ logSuccess(`Committed and tagged v${newVersion}.`);
132
+
133
+ const remote = defaultRemote();
134
+ if (remote && (await confirm(`Push commit and tag to '${remote}'?`))) {
135
+ const currentBranch = gitCapture(['branch', '--show-current']);
136
+ const pushed = git(['push', remote, currentBranch]).status === 0
137
+ && git(['push', remote, `v${newVersion}`]).status === 0;
138
+ if (pushed) logSuccess(`Pushed v${newVersion} to '${remote}'.`);
139
+ else die('Push failed — the commit and tag are safe locally.');
140
+ }
141
+ }
142
+
143
+ module.exports = { cmdRelease };
@@ -6,6 +6,7 @@ const fs = require('node:fs');
6
6
  const path = require('node:path');
7
7
  const { spawnSync } = require('node:child_process');
8
8
  const { logInfo, logSuccess, logWarn, logError, die } = require('./logger');
9
+ const { prompt } = require('./utils');
9
10
 
10
11
  const ALL_FRAMEWORKS = ['flutter', 'node', 'python', 'react', 'vue', 'angular', 'next', 'go', 'rust', 'docker'];
11
12
 
@@ -99,4 +100,43 @@ function getRemainingFrameworks(configDir) {
99
100
  return ALL_FRAMEWORKS.filter((fw) => !fs.existsSync(path.join(configDir, fw)));
100
101
  }
101
102
 
102
- module.exports = { ALL_FRAMEWORKS, frameworkScripts, injectTemplates, runScript, getRemainingFrameworks };
103
+ // Some teams want the generated scripts checked in so collaborators get the
104
+ // same automation; others want them private/local-only. Ask instead of
105
+ // always gitignoring.
106
+ async function updateGitignore(configDir) {
107
+ const gitignorePath = '.gitignore';
108
+ const ignoreChoice = (await prompt(`Should ${configDir}/ be ignored by git (private to you), or tracked so collaborators get the same scripts? [ignore/track]`, 'ignore')).toLowerCase();
109
+
110
+ if (ignoreChoice.startsWith('t')) {
111
+ if (fs.existsSync(gitignorePath)) {
112
+ const lines = fs.readFileSync(gitignorePath, 'utf8').split(/\r?\n/).filter((l) => l !== `${configDir}/`);
113
+ fs.writeFileSync(gitignorePath, lines.join('\n'));
114
+ logInfo(`Removed existing ${configDir}/ entry from .gitignore since you chose to track it.`);
115
+ }
116
+ logSuccess(`${configDir}/ will be tracked in git.`);
117
+ return;
118
+ }
119
+
120
+ if (fs.existsSync(gitignorePath)) {
121
+ const content = fs.readFileSync(gitignorePath, 'utf8');
122
+ if (!content.includes(`${configDir}/`)) {
123
+ fs.appendFileSync(gitignorePath, `\n${configDir}/\n`);
124
+ logSuccess('Added automation tracking to .gitignore');
125
+ }
126
+ } else {
127
+ fs.writeFileSync(gitignorePath, `${configDir}/\n`);
128
+ logSuccess('Created .gitignore and hidden tracking layer folder references.');
129
+ }
130
+ }
131
+
132
+ // bookkeeping(fw, configDir) — creates configDir/fw's scripts and updates
133
+ // .gitignore. Called from both `xgem init` and `xgem bootstrap` so a
134
+ // project scaffolded either way ends up in the same state.
135
+ async function bookkeeping(fw, configDir) {
136
+ fs.mkdirSync(configDir, { recursive: true });
137
+ injectTemplates(fw, configDir);
138
+ logSuccess(`Successfully appended standard scripts for: ${configDir}/${fw}`);
139
+ await updateGitignore(configDir);
140
+ }
141
+
142
+ module.exports = { ALL_FRAMEWORKS, frameworkScripts, injectTemplates, runScript, getRemainingFrameworks, updateGitignore, bookkeeping };
@@ -0,0 +1,53 @@
1
+ // xgem status — port of lib/status.sh: a one-screen dashboard across every
2
+ // project xgem has ever scaffolded.
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+ const { logInfo } = require('./logger');
8
+ const { registryList } = require('./registry');
9
+
10
+ function gitCaptureIn(cwd, args) {
11
+ const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
12
+ return result.status === 0 ? (result.stdout || '').trim() : '';
13
+ }
14
+
15
+ function statusRow(projectPath, configDir) {
16
+ const branch = gitCaptureIn(projectPath, ['branch', '--show-current']) || '-';
17
+ const porcelain = gitCaptureIn(projectPath, ['status', '--porcelain']);
18
+ const dirty = porcelain ? porcelain.split(/\r?\n/).filter(Boolean).length : 0;
19
+
20
+ const counts = gitCaptureIn(projectPath, ['rev-list', '--left-right', '--count', '@{u}...HEAD']);
21
+ let aheadBehind = '-';
22
+ if (counts) {
23
+ const [behind, ahead] = counts.split(/\s+/);
24
+ aheadBehind = `-${behind} +${ahead}`;
25
+ }
26
+
27
+ const configPath = path.join(projectPath, configDir);
28
+ const frameworks = fs.existsSync(configPath)
29
+ ? fs.readdirSync(configPath, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).join(',') || '-'
30
+ : '-';
31
+
32
+ return { projectPath, branch, dirty, aheadBehind, frameworks };
33
+ }
34
+
35
+ function pad(str, width) {
36
+ return str.length >= width ? str : str + ' '.repeat(width - str.length);
37
+ }
38
+
39
+ async function cmdStatus(configDir) {
40
+ const projects = registryList(configDir);
41
+ if (projects.length === 0) {
42
+ logInfo("No xgem-tracked projects found yet — run 'xgem init' in a project to start tracking it.");
43
+ return;
44
+ }
45
+
46
+ console.log(`${pad('PROJECT', 45)} ${pad('BRANCH', 20)} ${pad('DIRTY', 6)} ${pad('AHEAD/BEHIND', 12)} FRAMEWORKS`);
47
+ for (const p of projects) {
48
+ const row = statusRow(p, configDir);
49
+ console.log(`${pad(row.projectPath, 45)} ${pad(row.branch, 20)} ${pad(String(row.dirty), 6)} ${pad(row.aheadBehind, 12)} ${row.frameworks}`);
50
+ }
51
+ }
52
+
53
+ module.exports = { cmdStatus };
package/lib-win/utils.js CHANGED
@@ -32,6 +32,11 @@ function getVersion(name, args = ['--version']) {
32
32
  return out ? out.trim() : null;
33
33
  }
34
34
 
35
+ // openUrl(url) — best-effort browser open on Windows.
36
+ function openUrl(url) {
37
+ spawnSync('cmd', ['/c', 'start', '""', url], { stdio: 'ignore' });
38
+ }
39
+
35
40
  // confirm(prompt) -> Promise<boolean>. Honors XGEM_YES (auto-approve) and
36
41
  // XGEM_DRY_RUN (always decline, just like lib/utils.sh's confirm()).
37
42
  function confirm(prompt) {
@@ -64,4 +69,4 @@ function prompt(question, defaultValue = '') {
64
69
  });
65
70
  }
66
71
 
67
- module.exports = { detectArch, hasCmd, requireCmd, getVersion, confirm, prompt };
72
+ module.exports = { detectArch, hasCmd, requireCmd, getVersion, confirm, prompt, openUrl };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xgem-cli",
3
- "version": "2.0.0-alpha.12",
3
+ "version": "2.0.0-alpha.14",
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"