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
package/lib/create.sh ADDED
@@ -0,0 +1,334 @@
1
+ #!/bin/bash
2
+ # xgem project creation wizards — runs from `xgem init`, for frameworks
3
+ # with a real "ask sub-choice -> scaffold -> install -> launch" story
4
+ # worth building (react/vue/angular/next/flutter). The rest (node/python/
5
+ # go/rust/docker/swift) get a lighter "create with the ecosystem's own
6
+ # standard init command" step.
7
+ #
8
+ # Depends on lib/logger.sh, lib/utils.sh, lib/scaffold.sh. Uses `cd`
9
+ # directly (no subshells) so a newly created project directory persists as
10
+ # the cwd for the rest of `xgem init`.
11
+ #
12
+ # Ordering principle (this file's main correctness property): xgem's own
13
+ # bookkeeping (.xgem-automate + .gitignore, via _xgem_bookkeeping) always
14
+ # runs BEFORE the slow, interruptible step of each framework (an install,
15
+ # or an atomic scaffold+install like `ng new`) — never after. A real user
16
+ # hit this: Ctrl-C during `ng new`'s multi-minute install killed the whole
17
+ # xgem process before .xgem-automate was ever created, since it used to run
18
+ # only at the very end. Where a tool supports decoupling scaffold-from-
19
+ # install (--no-immediate, --skip-install, --no-pub), xgem scaffolds first
20
+ # (fast), does its own bookkeeping, then runs the install itself. Where a
21
+ # tool has no such flag (create-react-app), bookkeeping runs right after
22
+ # that one atomic call returns — a small residual risk, documented at the
23
+ # call site, not silently left as-was.
24
+ #
25
+ # Dev-server / device-run launch is deliberately deferred: creators only
26
+ # set XGEM_POST_INIT_LAUNCH_CMD (an array) rather than launching
27
+ # immediately, so `xgem init` finishes well before handing the terminal
28
+ # over to a long-running dev server.
29
+ XGEM_POST_INIT_LAUNCH_CMD=()
30
+
31
+ # Set by creators when a NEW folder was created (not "current directory").
32
+ # A child process can never change its parent shell's cwd, so once `xgem
33
+ # init` exits, the user's actual terminal is back wherever it started —
34
+ # xgem's own process is correctly cd'd into the new folder for the rest of
35
+ # ITS run, but that never propagates back. bin/xgem prints a "cd <name>"
36
+ # reminder using this at the very end.
37
+ XGEM_CREATED_NEW_FOLDER=""
38
+
39
+ # _xgem_bookkeeping <framework>
40
+ # Creates .xgem-automate + updates .gitignore. Called as early as the
41
+ # target directory allows, always before any slow/interruptible step.
42
+ _xgem_bookkeeping() {
43
+ local fw=$1
44
+ mkdir -p "$CONFIG_DIR"
45
+ scaffold_inject_templates "$fw" "$CONFIG_DIR"
46
+ log_success "Successfully appended standard scripts for: $CONFIG_DIR/$fw"
47
+
48
+ # Some teams want the generated scripts checked in so collaborators get
49
+ # the same automation; others want them private/local-only. Ask instead
50
+ # of always gitignoring.
51
+ local ignore_choice
52
+ read -r -p "Should $CONFIG_DIR/ be ignored by git (private to you), or tracked so collaborators get the same scripts? [ignore/track] (default: ignore): " ignore_choice
53
+ ignore_choice=${ignore_choice:-ignore}
54
+
55
+ if [[ "$ignore_choice" == t* || "$ignore_choice" == T* ]]; then
56
+ if [ -f .gitignore ] && grep -qx "$CONFIG_DIR/" .gitignore; then
57
+ local tmp
58
+ tmp=$(mktemp)
59
+ grep -vx "$CONFIG_DIR/" .gitignore > "$tmp" && mv "$tmp" .gitignore
60
+ log_info "Removed existing $CONFIG_DIR/ entry from .gitignore since you chose to track it."
61
+ fi
62
+ log_success "$CONFIG_DIR/ will be tracked in git."
63
+ elif [ -f .gitignore ]; then
64
+ if ! grep -qx "$CONFIG_DIR/" .gitignore; then
65
+ echo -e "\n$CONFIG_DIR/" >> .gitignore
66
+ log_success "Added automation tracking to .gitignore"
67
+ fi
68
+ else
69
+ echo "$CONFIG_DIR/" > .gitignore
70
+ log_success "Created .gitignore and hidden tracking layer folder references."
71
+ fi
72
+ }
73
+
74
+ # _prompt_project_location <human label> -> echoes "." or a new folder name.
75
+ # Pure prompt-and-echo, no other stdout output, so it's safe to capture via
76
+ # command substitution.
77
+ _prompt_project_location() {
78
+ local label=$1
79
+ local choice
80
+ read -r -p "Initialize $label in the current directory, or create a new folder? [current/new] (default: new): " choice
81
+ choice=${choice:-new}
82
+ if [[ "$choice" == c* || "$choice" == C* ]]; then
83
+ echo "."
84
+ return 0
85
+ fi
86
+ local name
87
+ read -r -p "Project name: " name
88
+ [ -n "$name" ] || die "Project name cannot be empty."
89
+ echo "$name"
90
+ }
91
+
92
+ # _enter_project_dir <project_dir> — mkdir+cd if a new folder was chosen,
93
+ # and records XGEM_CREATED_NEW_FOLDER. No-op for "." (current directory).
94
+ _enter_project_dir() {
95
+ local project_dir=$1
96
+ [ "$project_dir" = "." ] && return 0
97
+ XGEM_CREATED_NEW_FOLDER="$project_dir"
98
+ mkdir -p "$project_dir" && cd "$project_dir" || die "Could not enter $project_dir"
99
+ }
100
+
101
+ _open_url() {
102
+ local url=$1
103
+ case "$(detect_os)" in
104
+ darwin) open "$url" 2>/dev/null ;;
105
+ linux) xdg-open "$url" 2>/dev/null ;;
106
+ *) log_debug "Don't know how to open a browser on this OS — visit $url manually." ;;
107
+ esac
108
+ }
109
+
110
+ # _launch_dev_server_and_open_browser <cmd...>
111
+ # Starts the dev server in the background, tails its log for the first
112
+ # http://localhost URL it prints — this works across Vite/CRA/Next/Angular
113
+ # without hardcoding any one framework's default port (which can differ or
114
+ # be taken already) — opens it, then waits so Ctrl-C stops the server
115
+ # normally instead of leaving it orphaned in the background.
116
+ _launch_dev_server_and_open_browser() {
117
+ local log_file
118
+ log_file=$(mktemp)
119
+ log_info "Starting dev server: $*"
120
+ "$@" > "$log_file" 2>&1 &
121
+ local server_pid=$!
122
+
123
+ local url="" waited=0
124
+ while [ -z "$url" ] && [ "$waited" -lt 30 ] && kill -0 "$server_pid" 2>/dev/null; do
125
+ url=$(grep -oE 'https?://(localhost|127\.0\.0\.1)[:0-9]*[^[:space:]]*' "$log_file" 2>/dev/null | head -1)
126
+ if [ -z "$url" ]; then
127
+ sleep 1
128
+ waited=$((waited + 1))
129
+ fi
130
+ done
131
+
132
+ if [ -n "$url" ]; then
133
+ log_success "Dev server up at $url"
134
+ _open_url "$url"
135
+ elif kill -0 "$server_pid" 2>/dev/null; then
136
+ log_warn "Dev server is running (pid $server_pid) but its URL wasn't detected automatically after ${waited}s — check the output below."
137
+ else
138
+ log_error "Dev server exited early."
139
+ fi
140
+
141
+ cat "$log_file" &
142
+ wait "$server_pid" 2>/dev/null
143
+ }
144
+
145
+ _ask_launch_dev_server() {
146
+ [ ${#XGEM_POST_INIT_LAUNCH_CMD[@]} -gt 0 ] || return 0
147
+
148
+ # `flutter run` isn't a web dev server — no localhost URL to detect/
149
+ # open, and it depends on fully-inherited interactive stdin for its
150
+ # own hot-reload keybindings, which the background+log-tail approach
151
+ # would break. Run it directly. (Whether to run at all was already
152
+ # confirmed in _flutter_offer_run_on_device, so this doesn't ask again.)
153
+ if [ "${XGEM_POST_INIT_LAUNCH_CMD[0]}" = "flutter" ]; then
154
+ "${XGEM_POST_INIT_LAUNCH_CMD[@]}"
155
+ return 0
156
+ fi
157
+
158
+ local answer
159
+ read -r -p "Launch the dev server now and open it in your browser? (Y/n): " answer
160
+ answer=${answer:-y}
161
+ [[ "$answer" == "y" || "$answer" == "Y" ]] && _launch_dev_server_and_open_browser "${XGEM_POST_INIT_LAUNCH_CMD[@]}"
162
+ }
163
+
164
+ _create_react() {
165
+ local variant lang project_dir
166
+ read -r -p "Use Vite or Create React App (the 'normal' version)? [vite/cra] (default: vite): " variant
167
+ variant=${variant:-vite}
168
+ read -r -p "TypeScript or JavaScript? [ts/js] (default: ts): " lang
169
+ lang=${lang:-ts}
170
+ project_dir=$(_prompt_project_location "React")
171
+ _enter_project_dir "$project_dir"
172
+
173
+ if [[ "$variant" == cra* || "$variant" == CRA* ]]; then
174
+ require_cmd npx "Install Node.js (npm ships with it): https://nodejs.org"
175
+ # create-react-app has no flag to decouple scaffold-from-install
176
+ # (confirmed: nothing in --help), so this one atomic call does
177
+ # both — a Ctrl-C during its own install (it's known to be slow)
178
+ # would still lose xgem's bookkeeping below. Accepted residual
179
+ # risk for this specific, non-default, legacy-leaning option.
180
+ log_info "Create React App scaffolds and installs in one step and can take a few minutes — best to let it finish without interrupting."
181
+ local -a args=(create-react-app .)
182
+ [ "$lang" = "ts" ] && args+=(--template typescript)
183
+ npx "${args[@]}" || die "create-react-app failed."
184
+ _xgem_bookkeeping react
185
+ return 0
186
+ fi
187
+
188
+ require_cmd npm "Install Node.js (npm ships with it): https://nodejs.org"
189
+ local template="react"
190
+ [ "$lang" = "ts" ] && template="react-ts"
191
+ # --no-immediate: create-vite's own "install deps and start dev server"
192
+ # prompt would otherwise block here (and if Ctrl-C'd, kill this whole
193
+ # xgem process). --overwrite: harmless here since the only files that
194
+ # could exist at this point are ones xgem itself hasn't created yet
195
+ # (bookkeeping happens after this call, deliberately).
196
+ npm create vite@latest . -- --template "$template" --no-immediate --overwrite || die "npm create vite failed."
197
+ _xgem_bookkeeping react
198
+ log_info "Installing dependencies..."
199
+ npm install
200
+ XGEM_POST_INIT_LAUNCH_CMD=(npm run dev)
201
+ }
202
+
203
+ _create_vue() {
204
+ require_cmd npm "Install Node.js (npm ships with it): https://nodejs.org"
205
+ local project_dir
206
+ project_dir=$(_prompt_project_location "Vue")
207
+ _enter_project_dir "$project_dir"
208
+ # create-vue doesn't install automatically (confirmed: no node_modules
209
+ # after it runs), so there's already a natural gap here to bookkeep in
210
+ # before the slow step. --force since this dir may be genuinely empty
211
+ # or (for "current directory") may already contain unrelated files.
212
+ npm create vue@latest . --force || die "npm create vue failed."
213
+ _xgem_bookkeeping vue
214
+ log_info "Installing dependencies..."
215
+ npm install
216
+ XGEM_POST_INIT_LAUNCH_CMD=(npm run dev)
217
+ }
218
+
219
+ _create_angular() {
220
+ local project_dir
221
+ project_dir=$(_prompt_project_location "Angular")
222
+ _enter_project_dir "$project_dir"
223
+ local -a ng_cmd=(ng)
224
+ has_cmd ng || ng_cmd=(npx @angular/cli@latest)
225
+
226
+ # --skip-install: ng new's own install is a multi-minute step (Angular
227
+ # dependency trees are large) — decoupling it is exactly what let a
228
+ # real user's Ctrl-C lose xgem's bookkeeping before this fix.
229
+ # --skip-git: xgem has its own `xgem git init`; ng new's own auto-
230
+ # commit would happen before .xgem-automate even exists.
231
+ "${ng_cmd[@]}" new "$(basename "$PWD")" --directory=. --skip-install --skip-git \
232
+ || die "ng new failed."
233
+ _xgem_bookkeeping angular
234
+ log_info "Installing dependencies..."
235
+ npm install
236
+ XGEM_POST_INIT_LAUNCH_CMD=(npm run start)
237
+ }
238
+
239
+ _create_next() {
240
+ require_cmd npx "Install Node.js (npm ships with it): https://nodejs.org"
241
+ local project_dir
242
+ project_dir=$(_prompt_project_location "Next.js")
243
+ _enter_project_dir "$project_dir"
244
+ # --skip-install: decouple from create-next-app's own install, same
245
+ # reasoning as Angular above.
246
+ npx create-next-app@latest . --skip-install || die "create-next-app failed."
247
+ _xgem_bookkeeping next
248
+ log_info "Installing dependencies..."
249
+ npm install
250
+ XGEM_POST_INIT_LAUNCH_CMD=(npm run dev)
251
+ }
252
+
253
+ # Lighter path for frameworks without a dev-server/browser story — just the
254
+ # ecosystem's own standard init command, no sub-wizard. These are all fast
255
+ # (no multi-minute install step), so bookkeeping-then-init is low-risk
256
+ # regardless of ordering, but it's kept bookkeeping-first for consistency.
257
+ _create_simple() {
258
+ local fw=$1
259
+ local project_dir
260
+ project_dir=$(_prompt_project_location "$fw")
261
+ _enter_project_dir "$project_dir"
262
+ _xgem_bookkeeping "$fw"
263
+
264
+ case "$fw" in
265
+ node)
266
+ require_cmd npm "Install Node.js (npm ships with it): https://nodejs.org"
267
+ npm init -y
268
+ ;;
269
+ python)
270
+ require_cmd python3 "Install Python 3: https://www.python.org/downloads/"
271
+ python3 -m venv .venv
272
+ log_success "Created .venv — activate with 'source .venv/bin/activate'."
273
+ ;;
274
+ go)
275
+ require_cmd go "Install Go: https://go.dev/doc/install"
276
+ local module_name
277
+ read -r -p "Module name (e.g. github.com/you/project): " module_name
278
+ [ -n "$module_name" ] || module_name=$(basename "$PWD")
279
+ go mod init "$module_name"
280
+ ;;
281
+ rust)
282
+ require_cmd cargo "Install Rust: https://www.rust-lang.org/tools/install"
283
+ cargo init || die "cargo init failed."
284
+ ;;
285
+ swift)
286
+ require_cmd swift "Install Swift: https://www.swift.org/install/"
287
+ swift package init --type executable
288
+ ;;
289
+ docker)
290
+ if [ ! -f Dockerfile ]; then
291
+ cat > Dockerfile << 'EOF'
292
+ FROM alpine:latest
293
+ WORKDIR /app
294
+ COPY . .
295
+ CMD ["sh"]
296
+ EOF
297
+ log_success "Created a starter Dockerfile — edit it for your actual base image/entrypoint."
298
+ else
299
+ log_warn "Dockerfile already exists, leaving it as-is."
300
+ fi
301
+ ;;
302
+ esac
303
+ }
304
+
305
+ # create_project_wizard <framework>
306
+ # Asks whether to create a new project or use what's already here. Either
307
+ # way, xgem's own bookkeeping (.xgem-automate + .gitignore) is guaranteed
308
+ # to happen — for "existing", immediately (nothing to interrupt); for
309
+ # "new", each creator handles it internally at the earliest safe point,
310
+ # per the ordering principle documented at the top of this file.
311
+ create_project_wizard() {
312
+ local fw=$1
313
+ local create_choice
314
+ read -r -p "Create a brand-new $fw project, or use what's already in this directory? [new/existing] (default: existing): " create_choice
315
+ create_choice=${create_choice:-existing}
316
+
317
+ if [[ "$create_choice" != n* && "$create_choice" != N* ]]; then
318
+ _xgem_bookkeeping "$fw"
319
+ return 0
320
+ fi
321
+
322
+ case "$fw" in
323
+ react) _create_react ;;
324
+ vue) _create_vue ;;
325
+ angular) _create_angular ;;
326
+ next) _create_next ;;
327
+ flutter) create_flutter_project ;;
328
+ node|python|go|rust|docker|swift) _create_simple "$fw" ;;
329
+ *)
330
+ log_debug "No creation wizard for '$fw'; using current directory as-is."
331
+ _xgem_bookkeeping "$fw"
332
+ ;;
333
+ esac
334
+ }
package/lib/doctor.sh CHANGED
@@ -26,7 +26,10 @@ flutter_supports_flag() {
26
26
  flutter_config_spm_enabled() {
27
27
  has_cmd flutter || { echo "unknown (flutter not found)"; return; }
28
28
  local line
29
- line=$(flutter config 2>/dev/null | grep -i "swift-package-manager")
29
+ # Anchored to "enable-swift-package-manager:" (the current-value line)
30
+ # so this doesn't instead match the "--[no-]enable-swift-package-manager"
31
+ # flag-syntax help line that `flutter config` also prints.
32
+ line=$(flutter config 2>/dev/null | grep -im1 -E '^[[:space:]]*enable-swift-package-manager:')
30
33
  if [ -z "$line" ]; then
31
34
  echo "unknown (not reported by this Flutter version)"
32
35
  else
@@ -86,7 +89,7 @@ doctor_print_ios() {
86
89
  echo "Deployment target [$cfg]: ${t:-unknown}"
87
90
  done
88
91
 
89
- local required
92
+ local required generated
90
93
  required=$(ios_required_spm_deployment_target "$project_dir" 2>/dev/null)
91
94
  if [ -n "$required" ]; then
92
95
  echo "Required by resolved SwiftPM plugins: $required"
@@ -94,6 +97,14 @@ doctor_print_ios() {
94
97
  echo "Required by resolved SwiftPM plugins: unable to determine (run 'flutter pub get' first, or no SPM plugins declare an explicit floor)"
95
98
  fi
96
99
 
100
+ if [ "$uses_spm" = "yes" ]; then
101
+ generated=$(ios_generated_package_target "$project_dir" 2>/dev/null)
102
+ echo "FlutterGeneratedPluginSwiftPackage declares: ${generated:-unknown}"
103
+ if [ -n "$required" ] && [ -n "$generated" ] && _ios_ver_lt "$generated" "$required"; then
104
+ log_warn "This is stale — it declares iOS $generated but plugins need $required. This can happen even when your app's own deployment target is already high enough; 'xgem run flutter build' will detect and fix this."
105
+ fi
106
+ fi
107
+
97
108
  if [ "$uses_spm" = "yes" ]; then
98
109
  ios_known_issue_check
99
110
  fi
package/lib/flutter.sh CHANGED
@@ -66,14 +66,19 @@ _flutter_build_ios() {
66
66
 
67
67
  require_cmd xcodebuild "iOS builds require Xcode's command-line tools."
68
68
 
69
+ # Must run pub get BEFORE reconciling: the required-deployment-target
70
+ # check reads .dart_tool/package_config.json, and that file only
71
+ # reflects the plugin versions actually in use after a fresh pub get.
72
+ # Reconciling against a stale package_config.json can silently pass a
73
+ # check that the real, just-resolved dependency graph would fail.
74
+ log_info "Regenerating dependencies..."
75
+ flutter pub get
76
+
69
77
  log_info "Reconciling iOS deployment target against resolved SwiftPM plugins..."
70
78
  if ! ios_reconcile_deployment_target "."; then
71
79
  die "iOS deployment target could not be reconciled; aborting before build."
72
80
  fi
73
81
 
74
- log_info "Regenerating dependencies..."
75
- flutter pub get
76
-
77
82
  if ios_project_uses_pods "."; then
78
83
  log_info "CocoaPods detected - installing pods..."
79
84
  ( cd ios || exit 1; pod install )
@@ -100,7 +105,7 @@ _flutter_build_ios() {
100
105
  mkdir -p "$archive_dir"
101
106
  archive_path="$archive_dir/Runner $archive_time.xcarchive"
102
107
 
103
- if ! xcodebuild -workspace ios/Runner.xcworkspace \
108
+ local -a archive_cmd=(xcodebuild -workspace ios/Runner.xcworkspace \
104
109
  -scheme Runner \
105
110
  -sdk iphoneos \
106
111
  -configuration Release \
@@ -108,10 +113,35 @@ _flutter_build_ios() {
108
113
  -archivePath "$archive_path" \
109
114
  "BUILD_NUMBER=$build_number" \
110
115
  "MARKETING_VERSION=$build_name" \
111
- -allowProvisioningUpdates > "$log_file" 2>&1; then
112
- log_error "Archive FAILED"
113
- cat "$log_file"
114
- die "xcodebuild archive failed" "$?"
116
+ -allowProvisioningUpdates)
117
+
118
+ if ! "${archive_cmd[@]}" > "$log_file" 2>&1; then
119
+ # Static pre-checks can miss cases the real SwiftPM resolution
120
+ # catches (different plugin manifest layouts, symlinked package
121
+ # dirs, etc.) — Xcode's own error is authoritative, so parse it
122
+ # directly and retry once before giving up.
123
+ local retry_required
124
+ retry_required=$(ios_parse_required_from_build_log "$log_file")
125
+ if [ -n "$retry_required" ]; then
126
+ log_warn "Archive failed on a SwiftPM platform-version mismatch Xcode reports directly (requires iOS $retry_required). Retrying with that applied..."
127
+ ios_pbxproj_patch_deployment_target "$retry_required" "."
128
+ ios_regenerate_generated_package "."
129
+ local regenerated
130
+ regenerated=$(ios_generated_package_target ".")
131
+ if [ -z "$regenerated" ] || _ios_ver_lt "$regenerated" "$retry_required"; then
132
+ ios_patch_generated_package_target "$retry_required" "."
133
+ fi
134
+
135
+ if ! "${archive_cmd[@]}" > "$log_file" 2>&1; then
136
+ log_error "Archive FAILED again after retry"
137
+ cat "$log_file"
138
+ die "xcodebuild archive failed" "$?"
139
+ fi
140
+ else
141
+ log_error "Archive FAILED"
142
+ cat "$log_file"
143
+ die "xcodebuild archive failed" "$?"
144
+ fi
115
145
  fi
116
146
  log_success "Archive created at: $archive_path"
117
147
 
@@ -159,20 +189,21 @@ cmd_flutter_build() {
159
189
  echo ""
160
190
  echo "Select Target Platform:"
161
191
  echo "1) APK (Android)"
162
- echo "2) iOS"
163
- echo "3) macOS"
164
- echo "4) Windows"
165
- echo "5) Linux"
192
+ echo "2) App Bundle (Android, .aab — required for Play Store uploads)"
193
+ echo "3) iOS"
194
+ echo "4) macOS"
195
+ echo "5) Windows"
196
+ echo "6) Linux"
166
197
  local platform_choice
167
- read -r -p "Choose [1-5]: " platform_choice
168
- if ! [[ "$platform_choice" =~ ^[1-5]$ ]]; then
198
+ read -r -p "Choose [1-6]: " platform_choice
199
+ if ! [[ "$platform_choice" =~ ^[1-6]$ ]]; then
169
200
  die "Invalid platform choice '$platform_choice'."
170
201
  fi
171
202
 
172
203
  local mode="--release"
173
204
  [[ "$is_release" == "n" || "$is_release" == "N" ]] && mode="--debug"
174
205
 
175
- if [ "$platform_choice" -eq 2 ]; then
206
+ if [ "$platform_choice" -eq 3 ]; then
176
207
  _flutter_build_ios "$build_name" "$build_number" "$mode"
177
208
  return
178
209
  fi
@@ -180,9 +211,10 @@ cmd_flutter_build() {
180
211
  log_info "Initializing Flutter Build ($mode) for version $build_name+$build_number..."
181
212
  case $platform_choice in
182
213
  1) flutter build apk "$mode" "--build-name=$build_name" "--build-number=$build_number" ;;
183
- 3) flutter build macos "$mode" "--build-name=$build_name" "--build-number=$build_number" ;;
184
- 4) flutter build windows "$mode" "--build-name=$build_name" "--build-number=$build_number" ;;
185
- 5) flutter build linux "$mode" "--build-name=$build_name" "--build-number=$build_number" ;;
214
+ 2) flutter build appbundle "$mode" "--build-name=$build_name" "--build-number=$build_number" ;;
215
+ 4) flutter build macos "$mode" "--build-name=$build_name" "--build-number=$build_number" ;;
216
+ 5) flutter build windows "$mode" "--build-name=$build_name" "--build-number=$build_number" ;;
217
+ 6) flutter build linux "$mode" "--build-name=$build_name" "--build-number=$build_number" ;;
186
218
  esac
187
219
  }
188
220
 
@@ -205,3 +237,116 @@ flutter_run_native_command() {
205
237
  *) die "No native flutter command for '$script'." ;;
206
238
  esac
207
239
  }
240
+
241
+ # create_flutter_project — used by lib/create.sh's create_project_wizard.
242
+ # --no-pub decouples scaffolding from the (slower, network-dependent)
243
+ # `flutter pub get` step, so xgem's own bookkeeping (_xgem_bookkeeping) can
244
+ # happen in between — same ordering principle as the other creators in
245
+ # lib/create.sh: bookkeeping before any slow/interruptible step, never after.
246
+ create_flutter_project() {
247
+ require_cmd flutter "Install Flutter: https://docs.flutter.dev/get-started/install"
248
+ local project_dir
249
+ project_dir=$(_prompt_project_location "Flutter")
250
+ _enter_project_dir "$project_dir"
251
+
252
+ flutter create --no-pub . || die "flutter create failed."
253
+ _xgem_bookkeeping flutter
254
+ log_info "Resolving packages..."
255
+ flutter pub get
256
+ _flutter_offer_run_on_device
257
+ }
258
+
259
+ # _flutter_select_device -> echoes the chosen device id, or nothing if
260
+ # none available/selected. Uses `flutter devices --machine` (JSON) parsed
261
+ # with python3 — already a soft dependency elsewhere in lib/ios.sh for the
262
+ # same reason (no jq assumed present).
263
+ _flutter_select_device() {
264
+ require_cmd python3 "Needed to parse 'flutter devices --machine' output."
265
+ local devices_json_file
266
+ devices_json_file=$(mktemp)
267
+ flutter devices --machine > "$devices_json_file" 2>/dev/null
268
+
269
+ local -a ids=() labels=()
270
+ while IFS='|' read -r id label; do
271
+ [ -n "$id" ] || continue
272
+ ids+=("$id")
273
+ labels+=("$label")
274
+ done < <(python3 - "$devices_json_file" <<'PYEOF'
275
+ import json, sys
276
+
277
+ try:
278
+ with open(sys.argv[1]) as f:
279
+ devices = json.load(f)
280
+ except Exception:
281
+ devices = []
282
+
283
+ for d in devices:
284
+ platform = d.get("platform") or d.get("targetPlatform") or ""
285
+ name = d.get("name", "unknown")
286
+ device_id = d.get("id", "")
287
+ print(f"{device_id}|{name} ({platform})")
288
+ PYEOF
289
+ )
290
+ rm -f "$devices_json_file"
291
+
292
+ if [ ${#ids[@]} -eq 0 ]; then
293
+ log_warn "No running devices/simulators found."
294
+ _flutter_offer_launch_emulator
295
+ return 1
296
+ fi
297
+
298
+ echo "Available devices:" >&2
299
+ local i
300
+ for i in "${!ids[@]}"; do
301
+ echo "$((i + 1))) ${labels[$i]}" >&2
302
+ done
303
+
304
+ local choice
305
+ read -r -p "Select a device [1-${#ids[@]}]: " choice
306
+ if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt "${#ids[@]}" ]; then
307
+ log_warn "Invalid selection."
308
+ return 1
309
+ fi
310
+ echo "${ids[$((choice - 1))]}"
311
+ }
312
+
313
+ # No running devices — check for available-but-not-booted emulators and
314
+ # offer to launch one, matching the "check available simulators... then
315
+ # launch it" behavior the user asked for.
316
+ _flutter_offer_launch_emulator() {
317
+ local emulators_output
318
+ emulators_output=$(flutter emulators 2>/dev/null)
319
+ [ -n "$emulators_output" ] || return 1
320
+
321
+ echo "$emulators_output"
322
+ local launch_choice
323
+ read -r -p "Launch one of these emulators? Enter its id, or leave blank to skip: " launch_choice
324
+ [ -n "$launch_choice" ] || return 1
325
+
326
+ log_info "Launching emulator '$launch_choice'..."
327
+ flutter emulators --launch "$launch_choice"
328
+ log_info "Waiting for it to boot..."
329
+ sleep 5
330
+ }
331
+
332
+ # Only picks the device and records the launch command in
333
+ # XGEM_POST_INIT_LAUNCH_CMD — does NOT run `flutter run` itself. That's a
334
+ # long-running blocking process; running it here (before xgem's own
335
+ # template injection / .gitignore setup) means a Ctrl-C on it would kill
336
+ # this whole xgem process before that setup ever runs, same class of bug
337
+ # as create-vite's own --immediate flag caused for React. Device selection
338
+ # itself is just a quick menu prompt, so it's safe to do now.
339
+ _flutter_offer_run_on_device() {
340
+ local run_choice
341
+ read -r -p "Run the app now on a device/simulator? (Y/n): " run_choice
342
+ run_choice=${run_choice:-y}
343
+ [[ "$run_choice" == "y" || "$run_choice" == "Y" ]] || return 0
344
+
345
+ local device_id
346
+ device_id=$(_flutter_select_device)
347
+ if [ -n "$device_id" ]; then
348
+ XGEM_POST_INIT_LAUNCH_CMD=(flutter run -d "$device_id")
349
+ else
350
+ log_warn "No device selected — run 'flutter run' manually once one is available."
351
+ fi
352
+ }