xgem-cli 2.0.0-alpha.7 → 2.0.0-alpha.9

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
@@ -49,6 +53,7 @@ xgem doctor ios [path] Report on iOS build readiness: CocoaPods vs SPM, d
49
53
  targets per config, and whether they're reconciled
50
54
  xgem git cmt "message" Auto-stage, commit, rebase-pull, and push
51
55
  xgem git init Setup local repo, attach remote tracker shortcuts
56
+ xgem git branch Pick, create, or switch branches; remembers your choice
52
57
  xgem git rm-remote Drop a configured remote
53
58
  xgem git rm-branch Safely drop local and/or remote branch
54
59
  xgem --version Print xgem's version
@@ -58,7 +63,36 @@ Flags (any command): --yes (skip confirmations), --dry-run (show, don't apply),
58
63
 
59
64
  ## Supported frameworks
60
65
 
61
- 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.
62
96
 
63
97
  ## Architecture
64
98
 
@@ -70,8 +104,9 @@ lib/
70
104
  doctor.sh environment + feature detection ("xgem doctor")
71
105
  ios.sh the SwiftPM-aware iOS build engine
72
106
  flutter.sh flutter command group (build/hard-clean/build-runner), delegates iOS builds to ios.sh
73
- git.sh git cmt/init/rm-remote/rm-branch
107
+ git.sh git cmt/init/branch/rm-remote/rm-branch
74
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)
75
110
  templates/ the actual clean/build/dev script content xgem scaffolds into your project's .xgem-automate/
76
111
  ```
77
112
 
@@ -133,6 +168,8 @@ Deferred to a follow-up pass, not yet in this release:
133
168
  - Shell completions, `--json` output, per-project config caching (stop re-prompting
134
169
  build name/number/platform every run)
135
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
136
173
 
137
174
  ## License
138
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,8 @@ 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
110
115
  exit 0
111
116
  ;;
112
117
 
@@ -182,7 +187,7 @@ case "${1:-}" in
182
187
  ;;
183
188
 
184
189
  git)
185
- [ -n "${2:-}" ] || die "Usage: xgem git <cmt|init|rm-remote|rm-branch>"
190
+ [ -n "${2:-}" ] || die "Usage: xgem git <cmt|init|branch|rm-remote|rm-branch>"
186
191
  cmd_git "$2" "${3:-}"
187
192
  exit 0
188
193
  ;;
@@ -197,7 +202,7 @@ case "${1:-}" in
197
202
  exit 0
198
203
  ;;
199
204
 
200
- flutter|node|python|react|vue|angular|go|rust|docker|swift)
205
+ flutter|node|python|react|vue|angular|next|go|rust|docker|swift)
201
206
  TARGET_FW=$1
202
207
  TARGET_FW_UPPER=$(echo "$TARGET_FW" | tr '[:lower:]' '[:upper:]')
203
208
 
package/bin/xgem.js CHANGED
@@ -45,6 +45,7 @@ function printUsage() {
45
45
  console.log(' xgem doctor [ios] - Report on your environment / iOS build availability');
46
46
  console.log(' xgem git cmt "message" - Auto-stage, commit, rebase-pull, and push');
47
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');
48
49
  console.log(' xgem git rm-remote - Drop specified target remote tracing rules');
49
50
  console.log(' xgem git rm-branch - Safely drop local and remote workspace branch states');
50
51
  console.log(' xgem --version - Print xgem\'s version');
@@ -187,7 +188,7 @@ async function main() {
187
188
  case 'run': return cmdRun(a2, a3);
188
189
  case 'terminate': return cmdTerminate();
189
190
  case 'git':
190
- if (!a2) die('Usage: xgem git <cmt|init|rm-remote|rm-branch>');
191
+ if (!a2) die('Usage: xgem git <cmt|init|branch|rm-remote|rm-branch>');
191
192
  return cmdGit(a2, a3);
192
193
  case 'doctor': return cmdDoctor(a2);
193
194
  case '--version':
@@ -203,6 +204,7 @@ async function main() {
203
204
  case 'react':
204
205
  case 'vue':
205
206
  case 'angular':
207
+ case 'next':
206
208
  case 'go':
207
209
  case 'rust':
208
210
  case 'docker':
package/lib/create.sh ADDED
@@ -0,0 +1,226 @@
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
+ # _prompt_project_location <human label> -> echoes "." or a new folder name.
20
+ # Pure prompt-and-echo, no other stdout output, so it's safe to capture via
21
+ # command substitution.
22
+ _prompt_project_location() {
23
+ local label=$1
24
+ local choice
25
+ read -r -p "Initialize $label in the current directory, or create a new folder? [current/new] (default: new): " choice
26
+ choice=${choice:-new}
27
+ if [[ "$choice" == c* || "$choice" == C* ]]; then
28
+ echo "."
29
+ return 0
30
+ fi
31
+ local name
32
+ read -r -p "Project name: " name
33
+ [ -n "$name" ] || die "Project name cannot be empty."
34
+ echo "$name"
35
+ }
36
+
37
+ _open_url() {
38
+ local url=$1
39
+ case "$(detect_os)" in
40
+ darwin) open "$url" 2>/dev/null ;;
41
+ linux) xdg-open "$url" 2>/dev/null ;;
42
+ *) log_debug "Don't know how to open a browser on this OS — visit $url manually." ;;
43
+ esac
44
+ }
45
+
46
+ # _launch_dev_server_and_open_browser <cmd...>
47
+ # Starts the dev server in the background, tails its log for the first
48
+ # http://localhost URL it prints — this works across Vite/CRA/Next/Angular
49
+ # without hardcoding any one framework's default port (which can differ or
50
+ # be taken already) — opens it, then waits so Ctrl-C stops the server
51
+ # normally instead of leaving it orphaned in the background.
52
+ _launch_dev_server_and_open_browser() {
53
+ local log_file
54
+ log_file=$(mktemp)
55
+ log_info "Starting dev server: $*"
56
+ "$@" > "$log_file" 2>&1 &
57
+ local server_pid=$!
58
+
59
+ local url="" waited=0
60
+ while [ -z "$url" ] && [ "$waited" -lt 30 ] && kill -0 "$server_pid" 2>/dev/null; do
61
+ url=$(grep -oE 'https?://(localhost|127\.0\.0\.1)[:0-9]*[^[:space:]]*' "$log_file" 2>/dev/null | head -1)
62
+ if [ -z "$url" ]; then
63
+ sleep 1
64
+ waited=$((waited + 1))
65
+ fi
66
+ done
67
+
68
+ if [ -n "$url" ]; then
69
+ log_success "Dev server up at $url"
70
+ _open_url "$url"
71
+ elif kill -0 "$server_pid" 2>/dev/null; then
72
+ log_warn "Dev server is running (pid $server_pid) but its URL wasn't detected automatically after ${waited}s — check the output below."
73
+ else
74
+ log_error "Dev server exited early."
75
+ fi
76
+
77
+ cat "$log_file" &
78
+ wait "$server_pid" 2>/dev/null
79
+ }
80
+
81
+ _ask_launch_dev_server() {
82
+ [ ${#XGEM_POST_INIT_LAUNCH_CMD[@]} -gt 0 ] || return 0
83
+ local answer
84
+ read -r -p "Launch the dev server now and open it in your browser? (Y/n): " answer
85
+ answer=${answer:-y}
86
+ [[ "$answer" == "y" || "$answer" == "Y" ]] && _launch_dev_server_and_open_browser "${XGEM_POST_INIT_LAUNCH_CMD[@]}"
87
+ }
88
+
89
+ _create_react() {
90
+ local variant lang project_dir
91
+ read -r -p "Use Vite or Create React App (the 'normal' version)? [vite/cra] (default: vite): " variant
92
+ variant=${variant:-vite}
93
+ read -r -p "TypeScript or JavaScript? [ts/js] (default: ts): " lang
94
+ lang=${lang:-ts}
95
+ project_dir=$(_prompt_project_location "React")
96
+
97
+ if [[ "$variant" == cra* || "$variant" == CRA* ]]; then
98
+ require_cmd npx "Install Node.js (npm ships with it): https://nodejs.org"
99
+ local -a args=(create-react-app)
100
+ [ "$project_dir" != "." ] && args+=("$project_dir") || args+=(.)
101
+ [ "$lang" = "ts" ] && args+=(--template typescript)
102
+ npx "${args[@]}" || die "create-react-app failed."
103
+ else
104
+ require_cmd npm "Install Node.js (npm ships with it): https://nodejs.org"
105
+ local template="react"
106
+ [ "$lang" = "ts" ] && template="react-ts"
107
+ npm create vite@latest "$project_dir" -- --template "$template" || die "npm create vite failed."
108
+ fi
109
+
110
+ [ "$project_dir" != "." ] && { cd "$project_dir" || die "Could not enter $project_dir"; }
111
+ log_info "Installing dependencies..."
112
+ npm install
113
+ XGEM_POST_INIT_LAUNCH_CMD=(npm run dev)
114
+ }
115
+
116
+ _create_vue() {
117
+ require_cmd npm "Install Node.js (npm ships with it): https://nodejs.org"
118
+ local project_dir
119
+ project_dir=$(_prompt_project_location "Vue")
120
+ npm create vue@latest "$project_dir" || die "npm create vue failed."
121
+ [ "$project_dir" != "." ] && { cd "$project_dir" || die "Could not enter $project_dir"; }
122
+ log_info "Installing dependencies..."
123
+ npm install
124
+ XGEM_POST_INIT_LAUNCH_CMD=(npm run dev)
125
+ }
126
+
127
+ _create_angular() {
128
+ local project_dir
129
+ project_dir=$(_prompt_project_location "Angular")
130
+ local -a ng_cmd=(ng)
131
+ has_cmd ng || ng_cmd=(npx @angular/cli@latest)
132
+
133
+ if [ "$project_dir" = "." ]; then
134
+ "${ng_cmd[@]}" new "$(basename "$PWD")" --directory=. || die "ng new failed."
135
+ else
136
+ "${ng_cmd[@]}" new "$project_dir" || die "ng new failed."
137
+ cd "$project_dir" || die "Could not enter $project_dir"
138
+ fi
139
+ XGEM_POST_INIT_LAUNCH_CMD=(npm run start)
140
+ }
141
+
142
+ _create_next() {
143
+ require_cmd npx "Install Node.js (npm ships with it): https://nodejs.org"
144
+ local project_dir
145
+ project_dir=$(_prompt_project_location "Next.js")
146
+ npx create-next-app@latest "$project_dir" || die "create-next-app failed."
147
+ [ "$project_dir" != "." ] && { cd "$project_dir" || die "Could not enter $project_dir"; }
148
+ XGEM_POST_INIT_LAUNCH_CMD=(npm run dev)
149
+ }
150
+
151
+ # Lighter path for frameworks without a dev-server/browser story — just the
152
+ # ecosystem's own standard init command, no sub-wizard.
153
+ _create_simple() {
154
+ local fw=$1
155
+ local project_dir
156
+ project_dir=$(_prompt_project_location "$fw")
157
+ [ "$project_dir" != "." ] && { mkdir -p "$project_dir" && cd "$project_dir" || die "Could not enter $project_dir"; }
158
+
159
+ case "$fw" in
160
+ node)
161
+ require_cmd npm "Install Node.js (npm ships with it): https://nodejs.org"
162
+ npm init -y
163
+ ;;
164
+ python)
165
+ require_cmd python3 "Install Python 3: https://www.python.org/downloads/"
166
+ python3 -m venv .venv
167
+ log_success "Created .venv — activate with 'source .venv/bin/activate'."
168
+ ;;
169
+ go)
170
+ require_cmd go "Install Go: https://go.dev/doc/install"
171
+ local module_name
172
+ read -r -p "Module name (e.g. github.com/you/project): " module_name
173
+ [ -n "$module_name" ] || module_name=$(basename "$PWD")
174
+ go mod init "$module_name"
175
+ ;;
176
+ rust)
177
+ require_cmd cargo "Install Rust: https://www.rust-lang.org/tools/install"
178
+ if [ "$project_dir" != "." ]; then
179
+ cd .. || return 1
180
+ cargo new "$project_dir" || die "cargo new failed."
181
+ cd "$project_dir" || die "Could not enter $project_dir"
182
+ else
183
+ cargo init || die "cargo init failed."
184
+ fi
185
+ ;;
186
+ swift)
187
+ require_cmd swift "Install Swift: https://www.swift.org/install/"
188
+ swift package init --type executable
189
+ ;;
190
+ docker)
191
+ if [ ! -f Dockerfile ]; then
192
+ cat > Dockerfile << 'EOF'
193
+ FROM alpine:latest
194
+ WORKDIR /app
195
+ COPY . .
196
+ CMD ["sh"]
197
+ EOF
198
+ log_success "Created a starter Dockerfile — edit it for your actual base image/entrypoint."
199
+ else
200
+ log_warn "Dockerfile already exists, leaving it as-is."
201
+ fi
202
+ ;;
203
+ esac
204
+ }
205
+
206
+ # create_project_wizard <framework>
207
+ # Asks whether to create a new project or use what's already here; if
208
+ # creating, dispatches to the right per-framework creator. Leaves the
209
+ # process cwd inside the (possibly new) project directory either way.
210
+ create_project_wizard() {
211
+ local fw=$1
212
+ local create_choice
213
+ read -r -p "Create a brand-new $fw project, or use what's already in this directory? [new/existing] (default: existing): " create_choice
214
+ create_choice=${create_choice:-existing}
215
+ [[ "$create_choice" == n* || "$create_choice" == N* ]] || return 0
216
+
217
+ case "$fw" in
218
+ react) _create_react ;;
219
+ vue) _create_vue ;;
220
+ angular) _create_angular ;;
221
+ next) _create_next ;;
222
+ flutter) create_flutter_project ;;
223
+ node|python|go|rust|docker|swift) _create_simple "$fw" ;;
224
+ *) log_debug "No creation wizard for '$fw'; using current directory as-is." ;;
225
+ esac
226
+ }
package/lib/flutter.sh CHANGED
@@ -237,3 +237,106 @@ flutter_run_native_command() {
237
237
  *) die "No native flutter command for '$script'." ;;
238
238
  esac
239
239
  }
240
+
241
+ # create_flutter_project — used by lib/create.sh's create_project_wizard.
242
+ # `flutter create` needs a target dir; "." scaffolds into the current one.
243
+ create_flutter_project() {
244
+ require_cmd flutter "Install Flutter: https://docs.flutter.dev/get-started/install"
245
+ local project_dir
246
+ project_dir=$(_prompt_project_location "Flutter")
247
+ if [ "$project_dir" = "." ]; then
248
+ flutter create . || die "flutter create failed."
249
+ else
250
+ flutter create "$project_dir" || die "flutter create failed."
251
+ cd "$project_dir" || die "Could not enter $project_dir"
252
+ fi
253
+ _flutter_offer_run_on_device
254
+ }
255
+
256
+ # _flutter_select_device -> echoes the chosen device id, or nothing if
257
+ # none available/selected. Uses `flutter devices --machine` (JSON) parsed
258
+ # with python3 — already a soft dependency elsewhere in lib/ios.sh for the
259
+ # same reason (no jq assumed present).
260
+ _flutter_select_device() {
261
+ require_cmd python3 "Needed to parse 'flutter devices --machine' output."
262
+ local devices_json_file
263
+ devices_json_file=$(mktemp)
264
+ flutter devices --machine > "$devices_json_file" 2>/dev/null
265
+
266
+ local -a ids=() labels=()
267
+ while IFS='|' read -r id label; do
268
+ [ -n "$id" ] || continue
269
+ ids+=("$id")
270
+ labels+=("$label")
271
+ done < <(python3 - "$devices_json_file" <<'PYEOF'
272
+ import json, sys
273
+
274
+ try:
275
+ with open(sys.argv[1]) as f:
276
+ devices = json.load(f)
277
+ except Exception:
278
+ devices = []
279
+
280
+ for d in devices:
281
+ platform = d.get("platform") or d.get("targetPlatform") or ""
282
+ name = d.get("name", "unknown")
283
+ device_id = d.get("id", "")
284
+ print(f"{device_id}|{name} ({platform})")
285
+ PYEOF
286
+ )
287
+ rm -f "$devices_json_file"
288
+
289
+ if [ ${#ids[@]} -eq 0 ]; then
290
+ log_warn "No running devices/simulators found."
291
+ _flutter_offer_launch_emulator
292
+ return 1
293
+ fi
294
+
295
+ echo "Available devices:" >&2
296
+ local i
297
+ for i in "${!ids[@]}"; do
298
+ echo "$((i + 1))) ${labels[$i]}" >&2
299
+ done
300
+
301
+ local choice
302
+ read -r -p "Select a device [1-${#ids[@]}]: " choice
303
+ if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt "${#ids[@]}" ]; then
304
+ log_warn "Invalid selection."
305
+ return 1
306
+ fi
307
+ echo "${ids[$((choice - 1))]}"
308
+ }
309
+
310
+ # No running devices — check for available-but-not-booted emulators and
311
+ # offer to launch one, matching the "check available simulators... then
312
+ # launch it" behavior the user asked for.
313
+ _flutter_offer_launch_emulator() {
314
+ local emulators_output
315
+ emulators_output=$(flutter emulators 2>/dev/null)
316
+ [ -n "$emulators_output" ] || return 1
317
+
318
+ echo "$emulators_output"
319
+ local launch_choice
320
+ read -r -p "Launch one of these emulators? Enter its id, or leave blank to skip: " launch_choice
321
+ [ -n "$launch_choice" ] || return 1
322
+
323
+ log_info "Launching emulator '$launch_choice'..."
324
+ flutter emulators --launch "$launch_choice"
325
+ log_info "Waiting for it to boot..."
326
+ sleep 5
327
+ }
328
+
329
+ _flutter_offer_run_on_device() {
330
+ local run_choice
331
+ read -r -p "Run the app now on a device/simulator? (Y/n): " run_choice
332
+ run_choice=${run_choice:-y}
333
+ [[ "$run_choice" == "y" || "$run_choice" == "Y" ]] || return 0
334
+
335
+ local device_id
336
+ device_id=$(_flutter_select_device)
337
+ if [ -n "$device_id" ]; then
338
+ flutter run -d "$device_id"
339
+ else
340
+ log_warn "No device selected — run 'flutter run' manually once one is available."
341
+ fi
342
+ }
package/lib/git.sh CHANGED
@@ -46,6 +46,29 @@ cmd_git_cmt() {
46
46
  return 0
47
47
  fi
48
48
 
49
+ # A pull needs something to pull FROM. If this branch has never been
50
+ # pushed before, there's no remote ref to pull, and `git pull --rebase`
51
+ # fails with "couldn't find remote ref <branch>" — which is not a
52
+ # connection problem or a conflict, just a brand-new branch. Check for
53
+ # that specific case first so it gets its own accurate message instead
54
+ # of being lumped in with real connectivity failures.
55
+ git ls-remote --exit-code --heads "$remote_name" "$current_branch" >/dev/null 2>&1
56
+ local ls_remote_status=$?
57
+
58
+ if [ "$ls_remote_status" -eq 2 ]; then
59
+ log_info "'$current_branch' doesn't exist on '$remote_name' yet — pushing to create it..."
60
+ if git push -u "$remote_name" "$current_branch"; then
61
+ log_success "Git workflow complete! Branch created and pushed."
62
+ else
63
+ die "Push operation failed."
64
+ fi
65
+ return 0
66
+ elif [ "$ls_remote_status" -ne 0 ]; then
67
+ log_error "Could not reach '$remote_name' to check for '$current_branch' — this looks like a real connection problem."
68
+ log_warn "Your commit is safe locally. Re-run 'xgem git cmt' once connectivity is restored, or push manually: git push $remote_name $current_branch"
69
+ exit 1
70
+ fi
71
+
49
72
  local ahead_count
50
73
  ahead_count=$(git rev-list --count "$remote_name/$current_branch..$current_branch" 2>/dev/null)
51
74
  if [ "$ahead_count" = "0" ]; then
@@ -163,13 +186,69 @@ cmd_git_rm_branch() {
163
186
  fi
164
187
  }
165
188
 
166
- # xgem git <cmt|init|rm-remote|rm-branch> ...
189
+ # cmd_git_branch: lists local branches, lets the user pick one (or create a
190
+ # new one), checks it out, and remembers it as this repo's default so
191
+ # there's a quick way back to it later. `xgem git cmt` always operates on
192
+ # whatever's actually checked out (that's the only thing that's correct for
193
+ # a commit), so this command's job is the checkout + remembering, not
194
+ # changing how cmt picks its branch.
195
+ cmd_git_branch() {
196
+ local -a branches=()
197
+ while IFS= read -r line; do
198
+ [ -n "$line" ] && branches+=("$line")
199
+ done < <(git branch --format='%(refname:short)' 2>/dev/null)
200
+
201
+ [ ${#branches[@]} -gt 0 ] || die "No local branches found."
202
+
203
+ local current_branch
204
+ current_branch=$(git branch --show-current 2>/dev/null)
205
+
206
+ echo "Available branches:"
207
+ local i=1 b
208
+ for b in "${branches[@]}"; do
209
+ if [ "$b" = "$current_branch" ]; then
210
+ echo "$i) $b (current)"
211
+ else
212
+ echo "$i) $b"
213
+ fi
214
+ i=$((i + 1))
215
+ done
216
+ local create_option=$i
217
+ echo "$create_option) Create new branch"
218
+
219
+ local choice
220
+ read -r -p "Select [1-$create_option]: " choice
221
+
222
+ if [ "$choice" = "$create_option" ]; then
223
+ local new_branch
224
+ read -r -p "Enter new branch name: " new_branch
225
+ [ -n "$new_branch" ] || die "Branch name cannot be empty."
226
+ git checkout -b "$new_branch" || die "Could not create branch '$new_branch'."
227
+ git config --local xgem.default-branch "$new_branch"
228
+ log_success "Created and switched to '$new_branch', set as default for this repo."
229
+ return 0
230
+ fi
231
+
232
+ if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#branches[@]} ]; then
233
+ die "Invalid selection."
234
+ fi
235
+
236
+ local selected="${branches[$((choice - 1))]}"
237
+ if [ "$selected" != "$current_branch" ]; then
238
+ git checkout "$selected" || die "Could not switch to branch '$selected'."
239
+ fi
240
+ git config --local xgem.default-branch "$selected"
241
+ log_success "Switched to '$selected' and set as default for this repo."
242
+ }
243
+
244
+ # xgem git <cmt|init|rm-remote|rm-branch|branch> ...
167
245
  cmd_git() {
168
246
  case "$1" in
169
247
  cmt) cmd_git_cmt "$2" ;;
170
248
  init) cmd_git_init ;;
171
249
  rm-remote) cmd_git_rm_remote ;;
172
250
  rm-branch) cmd_git_rm_branch ;;
173
- *) die "Unknown git subcommand '$1'. Usage: xgem git <cmt|init|rm-remote|rm-branch>" ;;
251
+ branch) cmd_git_branch ;;
252
+ *) die "Unknown git subcommand '$1'. Usage: xgem git <cmt|init|rm-remote|rm-branch|branch>" ;;
174
253
  esac
175
254
  }
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
- *) echo "$XGEM_HOME/templates/$framework" ;;
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
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # xgem version info.
3
3
 
4
- XGEM_VERSION="2.0.0-alpha.7"
4
+ XGEM_VERSION="2.0.0-alpha.9"
5
5
 
6
6
  print_version() {
7
7
  echo "xgem $XGEM_VERSION"
package/lib-win/git.js CHANGED
@@ -60,6 +60,26 @@ async function cmdCmt(commitMsg) {
60
60
  return;
61
61
  }
62
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
+
63
83
  const aheadCount = gitCapture(['rev-list', '--count', `${remoteName}/${currentBranch}..${currentBranch}`]);
64
84
  if (aheadCount === '0') {
65
85
  logSuccess(`Already up to date with '${remoteName}/${currentBranch}' — nothing to push.`);
@@ -168,13 +188,53 @@ async function cmdRmBranch() {
168
188
  }
169
189
  }
170
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
+
171
230
  async function cmdGit(sub, arg) {
172
231
  switch (sub) {
173
232
  case 'cmt': return cmdCmt(arg);
174
233
  case 'init': return cmdInit();
234
+ case 'branch': return cmdBranch();
175
235
  case 'rm-remote': return cmdRmRemote();
176
236
  case 'rm-branch': return cmdRmBranch();
177
- default: die(`Unknown git subcommand '${sub}'. Usage: xgem git <cmt|init|rm-remote|rm-branch>`);
237
+ default: die(`Unknown git subcommand '${sub}'. Usage: xgem git <cmt|init|branch|rm-remote|rm-branch>`);
178
238
  }
179
239
  }
180
240
 
@@ -7,7 +7,7 @@ const path = require('node:path');
7
7
  const { spawnSync } = require('node:child_process');
8
8
  const { logInfo, logSuccess, logWarn, logError, die } = require('./logger');
9
9
 
10
- const ALL_FRAMEWORKS = ['flutter', 'node', 'python', 'react', 'vue', 'angular', 'go', 'rust', 'docker'];
10
+ const ALL_FRAMEWORKS = ['flutter', 'node', 'python', 'react', 'vue', 'angular', 'next', 'go', 'rust', 'docker'];
11
11
 
12
12
  const FRAMEWORK_SCRIPTS = {
13
13
  flutter: ['hard-clean', 'build', 'build-runner'],
@@ -16,6 +16,7 @@ const FRAMEWORK_SCRIPTS = {
16
16
  react: ['hard-clean', 'build', 'dev'],
17
17
  vue: ['hard-clean', 'build', 'dev'],
18
18
  angular: ['hard-clean', 'build', 'dev'],
19
+ next: ['hard-clean', 'build', 'dev'],
19
20
  go: ['hard-clean', 'build'],
20
21
  rust: ['hard-clean', 'build'],
21
22
  docker: ['hard-clean', 'build-up'],
@@ -27,7 +28,7 @@ function frameworkScripts(fw) {
27
28
 
28
29
  function templateDir(fw) {
29
30
  const templatesRoot = path.join(__dirname, '..', 'templates-win');
30
- if (fw === 'react' || fw === 'vue' || fw === 'angular') {
31
+ if (fw === 'react' || fw === 'vue' || fw === 'angular' || fw === 'next') {
31
32
  return path.join(templatesRoot, 'webframework');
32
33
  }
33
34
  return path.join(templatesRoot, fw);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xgem-cli",
3
- "version": "2.0.0-alpha.7",
3
+ "version": "2.0.0-alpha.9",
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"