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 CHANGED
@@ -1,10 +1,14 @@
1
1
  # xgem
2
2
 
3
- A framework-aware automation CLI. `xgem init` scaffolds clean/build/dev scripts
4
- tailored to your project type (Flutter, Node, Python, React/Vue/Angular, Go,
5
- Rust, Docker, Swift), `xgem run <framework> <script>` runs them, `xgem doctor`
6
- reports on your environment, and `xgem git` wraps a common stage → commit →
7
- rebase-pull push workflow.
3
+ A framework-aware automation CLI. `xgem init` can create a brand-new project
4
+ (real `npm create vite`/`create-next-app`/`create-vue`/`ng new`/`flutter create`
5
+ scaffolding, framework-specific questions, dependency install, and launching
6
+ the dev server in your browser or the app on a picked device/simulator) or
7
+ just scaffold clean/build/dev scripts into a project you already have
8
+ (Flutter, Node, Python, React/Vue/Angular/Next, Go, Rust, Docker, Swift).
9
+ `xgem run <framework> <script>` runs those scripts, `xgem doctor` reports on
10
+ your environment, and `xgem git` wraps a common stage → commit → rebase-pull
11
+ → push workflow.
8
12
 
9
13
  Flutter's iOS builds go through a dedicated engine that detects whether your
10
14
  project uses CocoaPods or Swift Package Manager (SPM), figures out the actual
@@ -27,7 +31,8 @@ xgem isn't in Homebrew's central `homebrew-core` (that requires a formal submiss
27
31
  npm install -g xgem-cli
28
32
  ```
29
33
 
30
- *(not yet published to the npm registry)*
34
+ Works natively on Windows too (no WSL needed) npm's install runs a small Node.js
35
+ engine there instead of the bash one; see [Windows support](#windows-support).
31
36
 
32
37
  ### curl
33
38
 
@@ -48,6 +53,7 @@ xgem doctor ios [path] Report on iOS build readiness: CocoaPods vs SPM, d
48
53
  targets per config, and whether they're reconciled
49
54
  xgem git cmt "message" Auto-stage, commit, rebase-pull, and push
50
55
  xgem git init Setup local repo, attach remote tracker shortcuts
56
+ xgem git branch Pick, create, or switch branches; remembers your choice
51
57
  xgem git rm-remote Drop a configured remote
52
58
  xgem git rm-branch Safely drop local and/or remote branch
53
59
  xgem --version Print xgem's version
@@ -57,7 +63,36 @@ Flags (any command): --yes (skip confirmations), --dry-run (show, don't apply),
57
63
 
58
64
  ## Supported frameworks
59
65
 
60
- flutter, node, python, react, vue, angular, go, rust, docker, swift
66
+ flutter, node, python, react, vue, angular, next, go, rust, docker, swift
67
+
68
+ ## Creating a new project
69
+
70
+ `xgem init` asks, per framework, whether to create a brand-new project or use
71
+ what's already in the directory:
72
+
73
+ - **react**: Vite or Create React App ("the normal version"), then TypeScript
74
+ or JavaScript, then current directory or a new folder.
75
+ - **vue** / **next**: runs the official `create-vue`/`create-next-app` wizard
76
+ directly, so you get their own real prompts (router, Tailwind, App Router,
77
+ etc.) rather than a re-implementation of them.
78
+ - **angular**: `ng new` (via a local Angular CLI if you have one, `npx
79
+ @angular/cli` otherwise).
80
+ - **flutter**: `flutter create`, then lists available devices/simulators
81
+ (falling back to available-but-not-booted emulators you can launch) and
82
+ runs on whichever you pick.
83
+ - **node/python/go/rust/docker/swift**: the ecosystem's own minimal init
84
+ command (`npm init`, a `.venv`, `cargo new`, `go mod init`, a starter
85
+ Dockerfile, `swift package init`) — no sub-wizard, since these don't have
86
+ an equivalent "dev server" to launch.
87
+
88
+ For the frameworks with a dev server (react/vue/angular/next), after
89
+ dependencies install xgem offers to start it and open your default browser —
90
+ it tails the server's own log for the first `http://localhost:PORT` it
91
+ prints rather than guessing a framework's default port, so it works even if
92
+ that port's already taken and a different one gets picked.
93
+
94
+ Answering "existing" (the default) skips all of this and behaves exactly
95
+ like before — just scaffolds `.xgem-automate/` into the current directory.
61
96
 
62
97
  ## Architecture
63
98
 
@@ -69,8 +104,9 @@ lib/
69
104
  doctor.sh environment + feature detection ("xgem doctor")
70
105
  ios.sh the SwiftPM-aware iOS build engine
71
106
  flutter.sh flutter command group (build/hard-clean/build-runner), delegates iOS builds to ios.sh
72
- git.sh git cmt/init/rm-remote/rm-branch
107
+ git.sh git cmt/init/branch/rm-remote/rm-branch
73
108
  scaffold.sh generic clean/build/dev handling for the other, simpler frameworks
109
+ create.sh project creation wizards (react/vue/angular/next/simple frameworks)
74
110
  templates/ the actual clean/build/dev script content xgem scaffolds into your project's .xgem-automate/
75
111
  ```
76
112
 
@@ -106,6 +142,24 @@ force Flutter to regenerate the generated package.
106
142
 
107
143
  Run `xgem doctor ios` any time for a standalone readiness report without doing a build.
108
144
 
145
+ ## Windows support
146
+
147
+ Homebrew and the curl installer are macOS/Linux only, same as any bash tool — that's
148
+ not going to change. npm is different: it's published with a small native Windows
149
+ engine (`lib-win/`, driven by `bin/xgem.js`) so `npm install -g xgem-cli` actually works
150
+ in `cmd.exe`/PowerShell, no WSL required.
151
+
152
+ What works on Windows: `init`/`add`/`run`/`terminate`, `doctor`, the git workflow
153
+ commands, and Flutter builds for **APK, App Bundle, and Windows desktop** targets. What doesn't:
154
+ Flutter **iOS/macOS** builds — Xcode has no Windows equivalent, so `xgem doctor ios`
155
+ explains that plainly instead of pretending. `swift` isn't offered as a framework
156
+ choice on Windows for the same reason. If you're inside WSL, none of this applies —
157
+ WSL reports itself as Linux, so you get the full bash engine automatically.
158
+
159
+ On macOS/Linux, npm installs run the exact same bash engine as Homebrew/curl (`bin/xgem.js`
160
+ is a thin passthrough that execs the real `bin/xgem` script) — there's only one
161
+ implementation to trust on POSIX; Windows is the only platform with a second one.
162
+
109
163
  ## Roadmap
110
164
 
111
165
  Deferred to a follow-up pass, not yet in this release:
@@ -114,6 +168,8 @@ Deferred to a follow-up pass, not yet in this release:
114
168
  - Shell completions, `--json` output, per-project config caching (stop re-prompting
115
169
  build name/number/platform every run)
116
170
  - Deeper Android SDK/Java detection
171
+ - Project creation wizards on the Windows engine (`lib-win/`) — react/vue/angular/next
172
+ scaffolding is pure Node.js tooling and would work fine there too, just not built yet
117
173
 
118
174
  ## License
119
175
 
package/bin/xgem CHANGED
@@ -33,6 +33,8 @@ source "$XGEM_HOME/lib/ios.sh"
33
33
  source "$XGEM_HOME/lib/flutter.sh"
34
34
  # shellcheck source=lib/scaffold.sh
35
35
  source "$XGEM_HOME/lib/scaffold.sh"
36
+ # shellcheck source=lib/create.sh
37
+ source "$XGEM_HOME/lib/create.sh"
36
38
 
37
39
  CONFIG_DIR=".xgem-automate"
38
40
 
@@ -70,6 +72,7 @@ print_usage() {
70
72
  echo " xgem doctor [ios] - Report on your environment / iOS SwiftPM build readiness"
71
73
  echo " xgem git cmt \"message\" - Auto-stage, commit, rebase-pull, and push"
72
74
  echo " xgem git init - Setup local repo, attach remote tracker shortcuts"
75
+ echo " xgem git branch - Pick, create, or switch branches; remembers your choice"
73
76
  echo " xgem git rm-remote - Drop specified target remote tracing rules"
74
77
  echo " xgem git rm-branch - Safely drop local and remote workspace branch states"
75
78
  echo " xgem --version - Print xgem's version"
@@ -84,12 +87,12 @@ case "${1:-}" in
84
87
  log_warn "Workspace configuration layer already exists! Use 'xgem add' to append frameworks."
85
88
  exit 0
86
89
  fi
87
- mkdir -p "$CONFIG_DIR"
88
-
89
90
  echo -e "\033[1;34mSelect your starting framework architecture:\033[0m"
90
91
  PS3="Enter target selection number: "
91
92
  select fw in "${ALL_FRAMEWORKS[@]}"; do
92
93
  if [ -n "$fw" ]; then
94
+ create_project_wizard "$fw"
95
+ mkdir -p "$CONFIG_DIR"
93
96
  scaffold_inject_templates "$fw" "$CONFIG_DIR"
94
97
  log_success "Successfully appended standard scripts for: $CONFIG_DIR/$fw"
95
98
  break
@@ -107,6 +110,18 @@ case "${1:-}" in
107
110
  echo "$CONFIG_DIR/" > .gitignore
108
111
  log_success "Created .gitignore and hidden tracking layer folder references."
109
112
  fi
113
+
114
+ _ask_launch_dev_server
115
+
116
+ # A child process (this script) can never change the parent
117
+ # shell's cwd — xgem's own process correctly cd'd into the new
118
+ # project folder for everything above, but your actual terminal
119
+ # is still wherever it started. Say so explicitly instead of
120
+ # leaving you to discover it the confusing way.
121
+ if [ -n "$XGEM_CREATED_NEW_FOLDER" ]; then
122
+ echo ""
123
+ log_info "Your project is in ./$XGEM_CREATED_NEW_FOLDER — your terminal is still in the original directory. Run: cd $XGEM_CREATED_NEW_FOLDER"
124
+ fi
110
125
  exit 0
111
126
  ;;
112
127
 
@@ -182,7 +197,7 @@ case "${1:-}" in
182
197
  ;;
183
198
 
184
199
  git)
185
- [ -n "${2:-}" ] || die "Usage: xgem git <cmt|init|rm-remote|rm-branch>"
200
+ [ -n "${2:-}" ] || die "Usage: xgem git <cmt|init|branch|rm-remote|rm-branch>"
186
201
  cmd_git "$2" "${3:-}"
187
202
  exit 0
188
203
  ;;
@@ -197,7 +212,7 @@ case "${1:-}" in
197
212
  exit 0
198
213
  ;;
199
214
 
200
- flutter|node|python|react|vue|angular|go|rust|docker|swift)
215
+ flutter|node|python|react|vue|angular|next|go|rust|docker|swift)
201
216
  TARGET_FW=$1
202
217
  TARGET_FW_UPPER=$(echo "$TARGET_FW" | tr '[:lower:]' '[:upper:]')
203
218
 
package/bin/xgem.js ADDED
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env node
2
+ // xgem npm entrypoint. On macOS/Linux this is a thin passthrough to the
3
+ // real, already-tested bash bin/xgem sitting next to it — zero logic
4
+ // duplication, zero behavioral change for POSIX users. On Windows it runs
5
+ // the native lib-win/*.js engine, since bash doesn't exist there.
6
+
7
+ const path = require('node:path');
8
+ const { spawnSync } = require('node:child_process');
9
+
10
+ if (process.platform !== 'win32') {
11
+ const bashScript = path.join(__dirname, 'xgem');
12
+ const result = spawnSync(bashScript, process.argv.slice(2), { stdio: 'inherit' });
13
+ process.exit(result.status ?? 0);
14
+ }
15
+
16
+ // ---- Windows-native engine ----
17
+ const fs = require('node:fs');
18
+ const { logInfo, logSuccess, logWarn, logError, die, paint } = require('../lib-win/logger');
19
+ const { prompt } = require('../lib-win/utils');
20
+ const { cmdDoctor } = require('../lib-win/doctor');
21
+ const { cmdGit } = require('../lib-win/git');
22
+ const { cmdFlutter } = require('../lib-win/flutter');
23
+ const scaffold = require('../lib-win/scaffold');
24
+
25
+ const CONFIG_DIR = '.xgem-automate';
26
+
27
+ function printBanner() {
28
+ console.log(paint('yellow', ''));
29
+ console.log('██╗ ██╗ ██████╗ ███████╗███╗ ███╗██╗███╗ ██╗██╗');
30
+ console.log('╚██╗██╔╝ ██╔════╝ ██╔════╝████╗ ████║██║████╗ ██║██║');
31
+ console.log(' ╚███╔╝ █████╗██║ ███╗█████╗ ██╔████╔██║██║██╔██╗ ██║██║');
32
+ console.log(' ██╔██╗ ╚════╝██║ ██║██╔══╝ ██║╚██╔╝██║██║██║╚██╗██║██║');
33
+ console.log('██╔╝ ██╗ ╚██████╔╝███████╗██║ ╚═╝ ██║██║██║ ╚████║██║');
34
+ console.log('╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═╝');
35
+ console.log(paint('cyan', ' ....Windows engine....\n'));
36
+ }
37
+
38
+ function printUsage() {
39
+ console.log('Usage:');
40
+ console.log(' xgem init - Initialize tracking system and add first framework');
41
+ console.log(' xgem add - Interactive state-aware addition of remaining frameworks');
42
+ console.log(' xgem run <framework> <script> - Run a workspace automation command script');
43
+ console.log(' xgem terminate - Purge all generated automated layouts completely');
44
+ console.log(' xgem <framework> [help] - View tailored instructions for a specific script layout');
45
+ console.log(' xgem doctor [ios] - Report on your environment / iOS build availability');
46
+ console.log(' xgem git cmt "message" - Auto-stage, commit, rebase-pull, and push');
47
+ console.log(' xgem git init - Setup local repo, attach remote tracker shortcuts');
48
+ console.log(' xgem git branch - Pick, create, or switch branches; remembers your choice');
49
+ console.log(' xgem git rm-remote - Drop specified target remote tracing rules');
50
+ console.log(' xgem git rm-branch - Safely drop local and remote workspace branch states');
51
+ console.log(' xgem --version - Print xgem\'s version');
52
+ console.log('');
53
+ console.log('Flags (any command): --yes (skip confirmations), --dry-run (show, don\'t apply), --verbose');
54
+ }
55
+
56
+ async function selectFramework(options) {
57
+ options.forEach((fw, i) => console.log(`${i + 1}) ${fw}`));
58
+ const choice = await prompt('Enter target selection number');
59
+ const idx = parseInt(choice, 10) - 1;
60
+ if (Number.isNaN(idx) || idx < 0 || idx >= options.length) return null;
61
+ return options[idx];
62
+ }
63
+
64
+ function updateGitignore() {
65
+ const gitignorePath = '.gitignore';
66
+ if (fs.existsSync(gitignorePath)) {
67
+ const content = fs.readFileSync(gitignorePath, 'utf8');
68
+ if (!content.includes(`${CONFIG_DIR}/`)) {
69
+ fs.appendFileSync(gitignorePath, `\n${CONFIG_DIR}/\n`);
70
+ logSuccess('Added automation tracking to .gitignore');
71
+ }
72
+ } else {
73
+ fs.writeFileSync(gitignorePath, `${CONFIG_DIR}/\n`);
74
+ logSuccess('Created .gitignore and hidden tracking layer folder references.');
75
+ }
76
+ }
77
+
78
+ async function cmdInit() {
79
+ printBanner();
80
+ if (fs.existsSync(CONFIG_DIR)) {
81
+ logWarn("Workspace configuration layer already exists! Use 'xgem add' to append frameworks.");
82
+ return;
83
+ }
84
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
85
+
86
+ console.log('Select your starting framework architecture:');
87
+ const fw = await selectFramework(scaffold.ALL_FRAMEWORKS);
88
+ if (!fw) die('Invalid selection.');
89
+ scaffold.injectTemplates(fw, CONFIG_DIR);
90
+ logSuccess(`Successfully appended standard scripts for: ${CONFIG_DIR}/${fw}`);
91
+
92
+ updateGitignore();
93
+ }
94
+
95
+ async function cmdAdd() {
96
+ if (!fs.existsSync(CONFIG_DIR)) die("System tracking layer not found. Run 'xgem init' first.");
97
+
98
+ const remaining = scaffold.getRemainingFrameworks(CONFIG_DIR);
99
+ if (remaining.length === 0) {
100
+ logWarn('All available framework blocks are already generated inside the workspace!');
101
+ const restore = (await prompt('Do you want to restore/regenerate all framework templates to defaults? (y/n)')).toLowerCase();
102
+ if (restore === 'y') {
103
+ for (const fw of scaffold.ALL_FRAMEWORKS) scaffold.injectTemplates(fw, CONFIG_DIR);
104
+ logSuccess('All framework automation layouts cleanly re-generated!');
105
+ }
106
+ return;
107
+ }
108
+
109
+ console.log('Select a framework script block to add:');
110
+ const fw = await selectFramework(remaining);
111
+ if (!fw) { console.log('Operation canceled.'); return; }
112
+ scaffold.injectTemplates(fw, CONFIG_DIR);
113
+ logSuccess(`Successfully loaded and updated files for: ${CONFIG_DIR}/${fw}`);
114
+ }
115
+
116
+ async function cmdRun(fw, script) {
117
+ if (!fw || !script) die('Usage: xgem run <framework> <script>');
118
+ if (fw === 'flutter' && ['hard-clean', 'build', 'build-runner'].includes(script)) {
119
+ await cmdFlutter(script);
120
+ } else {
121
+ scaffold.runScript(fw, script, CONFIG_DIR);
122
+ }
123
+ }
124
+
125
+ async function cmdTerminate() {
126
+ if (!fs.existsSync(CONFIG_DIR)) die(`Tracking layer ${CONFIG_DIR} not found. Nothing to terminate.`);
127
+
128
+ logWarn(`Initiating core termination sequence for ${CONFIG_DIR}...`);
129
+ const folders = fs.readdirSync(CONFIG_DIR, { withFileTypes: true })
130
+ .filter((d) => d.isDirectory())
131
+ .map((d) => d.name);
132
+
133
+ const { confirm } = require('../lib-win/utils');
134
+ const ok = await confirm(`This deletes ${CONFIG_DIR} and everything in it. Continue?`);
135
+ if (!ok) { logInfo('Cancelled.'); return; }
136
+
137
+ for (const fw of folders) {
138
+ console.log(` [ x ] Removing ${fw}...`);
139
+ fs.rmSync(path.join(CONFIG_DIR, fw), { recursive: true, force: true });
140
+ }
141
+ fs.rmSync(CONFIG_DIR, { recursive: true, force: true });
142
+
143
+ if (fs.existsSync('.gitignore')) {
144
+ const lines = fs.readFileSync('.gitignore', 'utf8').split(/\r?\n/).filter((l) => l !== `${CONFIG_DIR}/`);
145
+ fs.writeFileSync('.gitignore', lines.join('\n'));
146
+ logSuccess('Cleaned up .gitignore tracking records.');
147
+ }
148
+
149
+ logSuccess(`Termination complete. Total blocks destroyed: ${folders.length} (${folders.join(', ') || 'none'})`);
150
+ }
151
+
152
+ function printFrameworkHelp(fw) {
153
+ console.log(paint('cyan', `=== ${fw.toUpperCase()} Automation Blueprint (Windows) ===`));
154
+ if (!fs.existsSync(path.join(CONFIG_DIR, fw))) {
155
+ logWarn('Status: Not currently injected in this workspace.');
156
+ console.log("Run 'xgem add' to instantly append its structural scripts.\n");
157
+ } else {
158
+ logSuccess(`Status: Active & Mounted inside ${CONFIG_DIR}/${fw}/`);
159
+ console.log('');
160
+ }
161
+ console.log(`To execute scripts, use: xgem run ${fw} <script>\n`);
162
+ console.log('Configured Commands:');
163
+ for (const script of scaffold.frameworkScripts(fw) || []) {
164
+ console.log(` ${script}`);
165
+ }
166
+ if (fw === 'flutter') {
167
+ console.log('');
168
+ logInfo("Flutter builds on Windows support APK, App Bundle, and Windows desktop targets. Run 'xgem doctor ios' for why iOS/macOS aren't available here.");
169
+ }
170
+ console.log('');
171
+ }
172
+
173
+ async function main() {
174
+ const rawArgs = process.argv.slice(2);
175
+ const args = [];
176
+ for (const arg of rawArgs) {
177
+ if (arg === '--yes') process.env.XGEM_YES = '1';
178
+ else if (arg === '--dry-run') process.env.XGEM_DRY_RUN = '1';
179
+ else if (arg === '--verbose') process.env.XGEM_VERBOSE = '1';
180
+ else args.push(arg);
181
+ }
182
+
183
+ const [cmd, a2, a3] = args;
184
+
185
+ switch (cmd) {
186
+ case 'init': return cmdInit();
187
+ case 'add': return cmdAdd();
188
+ case 'run': return cmdRun(a2, a3);
189
+ case 'terminate': return cmdTerminate();
190
+ case 'git':
191
+ if (!a2) die('Usage: xgem git <cmt|init|branch|rm-remote|rm-branch>');
192
+ return cmdGit(a2, a3);
193
+ case 'doctor': return cmdDoctor(a2);
194
+ case '--version':
195
+ case '-V':
196
+ case 'version': {
197
+ const pkg = require('../package.json');
198
+ console.log(`xgem ${pkg.version} (Windows engine)`);
199
+ return;
200
+ }
201
+ case 'flutter':
202
+ case 'node':
203
+ case 'python':
204
+ case 'react':
205
+ case 'vue':
206
+ case 'angular':
207
+ case 'next':
208
+ case 'go':
209
+ case 'rust':
210
+ case 'docker':
211
+ return printFrameworkHelp(cmd);
212
+ default:
213
+ return printUsage();
214
+ }
215
+ }
216
+
217
+ main().catch((err) => {
218
+ logError(err.message || String(err));
219
+ process.exit(1);
220
+ });
package/lib/create.sh ADDED
@@ -0,0 +1,257 @@
1
+ #!/bin/bash
2
+ # xgem project creation wizards — runs from `xgem init`, before
3
+ # scaffold_inject_templates, for frameworks with a real "ask sub-choice ->
4
+ # scaffold -> install -> launch" story worth building (react/vue/angular/
5
+ # next/flutter). The rest (node/python/go/rust/docker/swift) get a lighter
6
+ # "create with the ecosystem's own standard init command" step.
7
+ #
8
+ # Depends on lib/logger.sh, lib/utils.sh. Uses `cd` directly (no
9
+ # subshells) so a newly created project directory persists as the cwd for
10
+ # the rest of `xgem init` (template injection, .gitignore) without the
11
+ # caller needing to track/re-cd into a returned path.
12
+ #
13
+ # Dev-server launch is deliberately deferred: creators only set
14
+ # XGEM_POST_INIT_LAUNCH_CMD (an array) rather than launching immediately,
15
+ # so `xgem init` finishes template injection and .gitignore setup *before*
16
+ # handing the terminal over to a long-running dev server.
17
+ XGEM_POST_INIT_LAUNCH_CMD=()
18
+
19
+ # Set by creators when a NEW folder was created (not "current directory").
20
+ # A child process can never change its parent shell's cwd, so once `xgem
21
+ # init` exits, the user's actual terminal is back wherever it started —
22
+ # xgem's own process is correctly cd'd into the new folder for the rest of
23
+ # ITS run (template injection, dev server), but that never propagates back.
24
+ # bin/xgem prints a "cd <name>" reminder using this at the very end.
25
+ XGEM_CREATED_NEW_FOLDER=""
26
+
27
+ # _prompt_project_location <human label> -> echoes "." or a new folder name.
28
+ # Pure prompt-and-echo, no other stdout output, so it's safe to capture via
29
+ # command substitution.
30
+ _prompt_project_location() {
31
+ local label=$1
32
+ local choice
33
+ read -r -p "Initialize $label in the current directory, or create a new folder? [current/new] (default: new): " choice
34
+ choice=${choice:-new}
35
+ if [[ "$choice" == c* || "$choice" == C* ]]; then
36
+ echo "."
37
+ return 0
38
+ fi
39
+ local name
40
+ read -r -p "Project name: " name
41
+ [ -n "$name" ] || die "Project name cannot be empty."
42
+ echo "$name"
43
+ }
44
+
45
+ _open_url() {
46
+ local url=$1
47
+ case "$(detect_os)" in
48
+ darwin) open "$url" 2>/dev/null ;;
49
+ linux) xdg-open "$url" 2>/dev/null ;;
50
+ *) log_debug "Don't know how to open a browser on this OS — visit $url manually." ;;
51
+ esac
52
+ }
53
+
54
+ # _launch_dev_server_and_open_browser <cmd...>
55
+ # Starts the dev server in the background, tails its log for the first
56
+ # http://localhost URL it prints — this works across Vite/CRA/Next/Angular
57
+ # without hardcoding any one framework's default port (which can differ or
58
+ # be taken already) — opens it, then waits so Ctrl-C stops the server
59
+ # normally instead of leaving it orphaned in the background.
60
+ _launch_dev_server_and_open_browser() {
61
+ local log_file
62
+ log_file=$(mktemp)
63
+ log_info "Starting dev server: $*"
64
+ "$@" > "$log_file" 2>&1 &
65
+ local server_pid=$!
66
+
67
+ local url="" waited=0
68
+ while [ -z "$url" ] && [ "$waited" -lt 30 ] && kill -0 "$server_pid" 2>/dev/null; do
69
+ url=$(grep -oE 'https?://(localhost|127\.0\.0\.1)[:0-9]*[^[:space:]]*' "$log_file" 2>/dev/null | head -1)
70
+ if [ -z "$url" ]; then
71
+ sleep 1
72
+ waited=$((waited + 1))
73
+ fi
74
+ done
75
+
76
+ if [ -n "$url" ]; then
77
+ log_success "Dev server up at $url"
78
+ _open_url "$url"
79
+ elif kill -0 "$server_pid" 2>/dev/null; then
80
+ log_warn "Dev server is running (pid $server_pid) but its URL wasn't detected automatically after ${waited}s — check the output below."
81
+ else
82
+ log_error "Dev server exited early."
83
+ fi
84
+
85
+ cat "$log_file" &
86
+ wait "$server_pid" 2>/dev/null
87
+ }
88
+
89
+ _ask_launch_dev_server() {
90
+ [ ${#XGEM_POST_INIT_LAUNCH_CMD[@]} -gt 0 ] || return 0
91
+
92
+ # `flutter run` isn't a web dev server — it has no localhost URL to
93
+ # detect/open, and it depends on a fully-inherited interactive stdin
94
+ # for its own hot-reload keybindings (r/R/q/etc.), which the
95
+ # background+log-tail approach below would break. Run it directly.
96
+ # (Whether to run at all was already confirmed in
97
+ # _flutter_offer_run_on_device before this command was ever set, so
98
+ # this doesn't ask again.)
99
+ if [ "${XGEM_POST_INIT_LAUNCH_CMD[0]}" = "flutter" ]; then
100
+ "${XGEM_POST_INIT_LAUNCH_CMD[@]}"
101
+ return 0
102
+ fi
103
+
104
+ local answer
105
+ read -r -p "Launch the dev server now and open it in your browser? (Y/n): " answer
106
+ answer=${answer:-y}
107
+ [[ "$answer" == "y" || "$answer" == "Y" ]] && _launch_dev_server_and_open_browser "${XGEM_POST_INIT_LAUNCH_CMD[@]}"
108
+ }
109
+
110
+ _create_react() {
111
+ local variant lang project_dir
112
+ read -r -p "Use Vite or Create React App (the 'normal' version)? [vite/cra] (default: vite): " variant
113
+ variant=${variant:-vite}
114
+ read -r -p "TypeScript or JavaScript? [ts/js] (default: ts): " lang
115
+ lang=${lang:-ts}
116
+ project_dir=$(_prompt_project_location "React")
117
+ [ "$project_dir" != "." ] && XGEM_CREATED_NEW_FOLDER="$project_dir"
118
+
119
+ if [[ "$variant" == cra* || "$variant" == CRA* ]]; then
120
+ require_cmd npx "Install Node.js (npm ships with it): https://nodejs.org"
121
+ local -a args=(create-react-app)
122
+ [ "$project_dir" != "." ] && args+=("$project_dir") || args+=(.)
123
+ [ "$lang" = "ts" ] && args+=(--template typescript)
124
+ npx "${args[@]}" || die "create-react-app failed."
125
+ else
126
+ require_cmd npm "Install Node.js (npm ships with it): https://nodejs.org"
127
+ local template="react"
128
+ [ "$lang" = "ts" ] && template="react-ts"
129
+ # --no-immediate: create-vite's own "install deps and start dev
130
+ # server" prompt would otherwise block here and, if Ctrl-C'd, kill
131
+ # this whole xgem process before it ever reaches template
132
+ # injection / .gitignore setup below. xgem handles install/launch
133
+ # itself instead, after everything else is done.
134
+ npm create vite@latest "$project_dir" -- --template "$template" --no-immediate || die "npm create vite failed."
135
+ fi
136
+
137
+ [ "$project_dir" != "." ] && { cd "$project_dir" || die "Could not enter $project_dir"; }
138
+ log_info "Installing dependencies..."
139
+ npm install
140
+ XGEM_POST_INIT_LAUNCH_CMD=(npm run dev)
141
+ }
142
+
143
+ _create_vue() {
144
+ require_cmd npm "Install Node.js (npm ships with it): https://nodejs.org"
145
+ local project_dir
146
+ project_dir=$(_prompt_project_location "Vue")
147
+ [ "$project_dir" != "." ] && XGEM_CREATED_NEW_FOLDER="$project_dir"
148
+ npm create vue@latest "$project_dir" || die "npm create vue failed."
149
+ [ "$project_dir" != "." ] && { cd "$project_dir" || die "Could not enter $project_dir"; }
150
+ log_info "Installing dependencies..."
151
+ npm install
152
+ XGEM_POST_INIT_LAUNCH_CMD=(npm run dev)
153
+ }
154
+
155
+ _create_angular() {
156
+ local project_dir
157
+ project_dir=$(_prompt_project_location "Angular")
158
+ [ "$project_dir" != "." ] && XGEM_CREATED_NEW_FOLDER="$project_dir"
159
+ local -a ng_cmd=(ng)
160
+ has_cmd ng || ng_cmd=(npx @angular/cli@latest)
161
+
162
+ if [ "$project_dir" = "." ]; then
163
+ "${ng_cmd[@]}" new "$(basename "$PWD")" --directory=. || die "ng new failed."
164
+ else
165
+ "${ng_cmd[@]}" new "$project_dir" || die "ng new failed."
166
+ cd "$project_dir" || die "Could not enter $project_dir"
167
+ fi
168
+ XGEM_POST_INIT_LAUNCH_CMD=(npm run start)
169
+ }
170
+
171
+ _create_next() {
172
+ require_cmd npx "Install Node.js (npm ships with it): https://nodejs.org"
173
+ local project_dir
174
+ project_dir=$(_prompt_project_location "Next.js")
175
+ [ "$project_dir" != "." ] && XGEM_CREATED_NEW_FOLDER="$project_dir"
176
+ npx create-next-app@latest "$project_dir" || die "create-next-app failed."
177
+ [ "$project_dir" != "." ] && { cd "$project_dir" || die "Could not enter $project_dir"; }
178
+ XGEM_POST_INIT_LAUNCH_CMD=(npm run dev)
179
+ }
180
+
181
+ # Lighter path for frameworks without a dev-server/browser story — just the
182
+ # ecosystem's own standard init command, no sub-wizard.
183
+ _create_simple() {
184
+ local fw=$1
185
+ local project_dir
186
+ project_dir=$(_prompt_project_location "$fw")
187
+ [ "$project_dir" != "." ] && XGEM_CREATED_NEW_FOLDER="$project_dir"
188
+ [ "$project_dir" != "." ] && { mkdir -p "$project_dir" && cd "$project_dir" || die "Could not enter $project_dir"; }
189
+
190
+ case "$fw" in
191
+ node)
192
+ require_cmd npm "Install Node.js (npm ships with it): https://nodejs.org"
193
+ npm init -y
194
+ ;;
195
+ python)
196
+ require_cmd python3 "Install Python 3: https://www.python.org/downloads/"
197
+ python3 -m venv .venv
198
+ log_success "Created .venv — activate with 'source .venv/bin/activate'."
199
+ ;;
200
+ go)
201
+ require_cmd go "Install Go: https://go.dev/doc/install"
202
+ local module_name
203
+ read -r -p "Module name (e.g. github.com/you/project): " module_name
204
+ [ -n "$module_name" ] || module_name=$(basename "$PWD")
205
+ go mod init "$module_name"
206
+ ;;
207
+ rust)
208
+ require_cmd cargo "Install Rust: https://www.rust-lang.org/tools/install"
209
+ if [ "$project_dir" != "." ]; then
210
+ cd .. || return 1
211
+ cargo new "$project_dir" || die "cargo new failed."
212
+ cd "$project_dir" || die "Could not enter $project_dir"
213
+ else
214
+ cargo init || die "cargo init failed."
215
+ fi
216
+ ;;
217
+ swift)
218
+ require_cmd swift "Install Swift: https://www.swift.org/install/"
219
+ swift package init --type executable
220
+ ;;
221
+ docker)
222
+ if [ ! -f Dockerfile ]; then
223
+ cat > Dockerfile << 'EOF'
224
+ FROM alpine:latest
225
+ WORKDIR /app
226
+ COPY . .
227
+ CMD ["sh"]
228
+ EOF
229
+ log_success "Created a starter Dockerfile — edit it for your actual base image/entrypoint."
230
+ else
231
+ log_warn "Dockerfile already exists, leaving it as-is."
232
+ fi
233
+ ;;
234
+ esac
235
+ }
236
+
237
+ # create_project_wizard <framework>
238
+ # Asks whether to create a new project or use what's already here; if
239
+ # creating, dispatches to the right per-framework creator. Leaves the
240
+ # process cwd inside the (possibly new) project directory either way.
241
+ create_project_wizard() {
242
+ local fw=$1
243
+ local create_choice
244
+ read -r -p "Create a brand-new $fw project, or use what's already in this directory? [new/existing] (default: existing): " create_choice
245
+ create_choice=${create_choice:-existing}
246
+ [[ "$create_choice" == n* || "$create_choice" == N* ]] || return 0
247
+
248
+ case "$fw" in
249
+ react) _create_react ;;
250
+ vue) _create_vue ;;
251
+ angular) _create_angular ;;
252
+ next) _create_next ;;
253
+ flutter) create_flutter_project ;;
254
+ node|python|go|rust|docker|swift) _create_simple "$fw" ;;
255
+ *) log_debug "No creation wizard for '$fw'; using current directory as-is." ;;
256
+ esac
257
+ }