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/git.sh CHANGED
@@ -24,9 +24,13 @@ cmd_git_cmt() {
24
24
  die "Invalid choice. Operation aborted."
25
25
  fi
26
26
 
27
- log_info "Committing changes..."
28
- if ! git commit -m "$commit_msg"; then
29
- die "Commit failed. Aborting before pull/push."
27
+ if git diff --cached --quiet; then
28
+ log_warn "Nothing staged to commit checking whether there's anything already committed to sync."
29
+ else
30
+ log_info "Committing changes..."
31
+ if ! git commit -m "$commit_msg"; then
32
+ die "Commit failed. Aborting before pull/push."
33
+ fi
30
34
  fi
31
35
 
32
36
  local current_branch remote_name
@@ -38,14 +42,52 @@ cmd_git_cmt() {
38
42
  fi
39
43
 
40
44
  if [ -z "$remote_name" ]; then
41
- log_warn "Local commit dropped cleanly, but no remote is configured — sync was skipped."
45
+ log_warn "No remote configured — sync was skipped."
46
+ return 0
47
+ fi
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
+
72
+ local ahead_count
73
+ ahead_count=$(git rev-list --count "$remote_name/$current_branch..$current_branch" 2>/dev/null)
74
+ if [ "$ahead_count" = "0" ]; then
75
+ log_success "Already up to date with '$remote_name/$current_branch' — nothing to push."
42
76
  return 0
43
77
  fi
44
78
 
45
79
  log_info "Pulling updates from remote '$remote_name' on '$current_branch' via rebase..."
46
80
  if ! git pull --rebase "$remote_name" "$current_branch"; then
47
- log_error "MERGE CONFLICT DETECTED!"
48
- log_warn "Execution paused. Resolve conflicts to proceed."
81
+ local git_dir
82
+ git_dir=$(git rev-parse --git-dir 2>/dev/null)
83
+ if [ -d "$git_dir/rebase-merge" ] || [ -d "$git_dir/rebase-apply" ]; then
84
+ log_error "MERGE CONFLICT DETECTED!"
85
+ log_warn "Execution paused. Resolve conflicts to proceed."
86
+ else
87
+ log_error "Could not sync with '$remote_name' — this looks like a connection problem, not a merge conflict (see the git error above)."
88
+ 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"
89
+ exit 1
90
+ fi
49
91
  local open_editor
50
92
  read -r -p "Do you want to open VS Code to resolve this now? (y/n): " open_editor
51
93
  [[ "$open_editor" == "y" || "$open_editor" == "Y" ]] && code .
@@ -60,31 +102,67 @@ cmd_git_cmt() {
60
102
  fi
61
103
  }
62
104
 
63
- cmd_git_init() {
64
- log_info "Initializing local Git repository..."
65
- git init
105
+ _cmd_git_init_manual_remote() {
106
+ local remote_name=$1
107
+ local remote_url user_remote_name
108
+ read -r -p "Enter remote repository URL (or leave blank to skip): " remote_url
109
+ [ -n "$remote_url" ] || return 0
66
110
 
67
- local remote_name="origin" remote_url user_remote_name branch_name
68
- read -r -p "Enter remote repository URL: " remote_url
69
- if [ -n "$remote_url" ]; then
70
- read -r -p "Enter remote name (default: origin): " user_remote_name
71
- [ -n "$user_remote_name" ] && remote_name="$user_remote_name"
111
+ read -r -p "Enter remote name (default: $remote_name): " user_remote_name
112
+ [ -n "$user_remote_name" ] && remote_name="$user_remote_name"
72
113
 
73
- if git remote | grep -q "^$remote_name$"; then
74
- git remote set-url "$remote_name" "$remote_url"
75
- log_success "Remote '$remote_name' already existed. URL updated."
76
- else
77
- git remote add "$remote_name" "$remote_url"
78
- log_success "Remote '$remote_name' successfully added."
79
- fi
114
+ if git remote | grep -q "^$remote_name$"; then
115
+ git remote set-url "$remote_name" "$remote_url"
116
+ log_success "Remote '$remote_name' already existed. URL updated."
117
+ else
118
+ git remote add "$remote_name" "$remote_url"
119
+ log_success "Remote '$remote_name' successfully added."
80
120
  fi
121
+ }
81
122
 
123
+ cmd_git_init() {
124
+ log_info "Initializing local Git repository..."
125
+ git init
126
+
127
+ local branch_name
82
128
  read -r -p "Enter branch name (default: main): " branch_name
83
129
  branch_name=${branch_name:-main}
84
130
  git branch -M "$branch_name"
85
131
 
86
132
  git add .
87
133
  git commit -m "initial changes" 2>/dev/null
134
+
135
+ # Real GitHub repo creation, not just wiring a remote to a URL you
136
+ # already had to go create by hand — this is the whole point of
137
+ # `xgem git init` over plain `git init`. Falls back to the manual-URL
138
+ # flow if `gh` isn't installed/authenticated.
139
+ if has_cmd gh && gh auth status >/dev/null 2>&1; then
140
+ local create_choice
141
+ read -r -p "Create a new GitHub repository for this project right now? (Y/n): " create_choice
142
+ create_choice=${create_choice:-y}
143
+ if [[ "$create_choice" == "y" || "$create_choice" == "Y" ]]; then
144
+ local repo_name visibility vis_flag
145
+ read -r -p "Repository name (default: $(basename "$PWD")): " repo_name
146
+ repo_name=${repo_name:-$(basename "$PWD")}
147
+ read -r -p "Public or private? [public/private] (default: private): " visibility
148
+ visibility=${visibility:-private}
149
+ vis_flag="--private"
150
+ [[ "$visibility" == pub* || "$visibility" == Pub* ]] && vis_flag="--public"
151
+
152
+ if gh repo create "$repo_name" "$vis_flag" --source=. --remote=origin --push; then
153
+ log_success "Created GitHub repo '$repo_name' and pushed '$branch_name' to it."
154
+ else
155
+ log_error "gh repo create failed — falling back to manual remote setup."
156
+ _cmd_git_init_manual_remote origin
157
+ fi
158
+ log_success "Local baseline configuration setup completed."
159
+ return 0
160
+ fi
161
+ elif has_cmd gh; then
162
+ log_warn "GitHub CLI (gh) is installed but not authenticated — run 'gh auth login' to enable one-step repo creation next time."
163
+ fi
164
+
165
+ _cmd_git_init_manual_remote origin
88
166
  log_success "Local baseline configuration setup completed."
89
167
  }
90
168
 
@@ -144,13 +222,69 @@ cmd_git_rm_branch() {
144
222
  fi
145
223
  }
146
224
 
147
- # xgem git <cmt|init|rm-remote|rm-branch> ...
225
+ # cmd_git_branch: lists local branches, lets the user pick one (or create a
226
+ # new one), checks it out, and remembers it as this repo's default so
227
+ # there's a quick way back to it later. `xgem git cmt` always operates on
228
+ # whatever's actually checked out (that's the only thing that's correct for
229
+ # a commit), so this command's job is the checkout + remembering, not
230
+ # changing how cmt picks its branch.
231
+ cmd_git_branch() {
232
+ local -a branches=()
233
+ while IFS= read -r line; do
234
+ [ -n "$line" ] && branches+=("$line")
235
+ done < <(git branch --format='%(refname:short)' 2>/dev/null)
236
+
237
+ [ ${#branches[@]} -gt 0 ] || die "No local branches found."
238
+
239
+ local current_branch
240
+ current_branch=$(git branch --show-current 2>/dev/null)
241
+
242
+ echo "Available branches:"
243
+ local i=1 b
244
+ for b in "${branches[@]}"; do
245
+ if [ "$b" = "$current_branch" ]; then
246
+ echo "$i) $b (current)"
247
+ else
248
+ echo "$i) $b"
249
+ fi
250
+ i=$((i + 1))
251
+ done
252
+ local create_option=$i
253
+ echo "$create_option) Create new branch"
254
+
255
+ local choice
256
+ read -r -p "Select [1-$create_option]: " choice
257
+
258
+ if [ "$choice" = "$create_option" ]; then
259
+ local new_branch
260
+ read -r -p "Enter new branch name: " new_branch
261
+ [ -n "$new_branch" ] || die "Branch name cannot be empty."
262
+ git checkout -b "$new_branch" || die "Could not create branch '$new_branch'."
263
+ git config --local xgem.default-branch "$new_branch"
264
+ log_success "Created and switched to '$new_branch', set as default for this repo."
265
+ return 0
266
+ fi
267
+
268
+ if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#branches[@]} ]; then
269
+ die "Invalid selection."
270
+ fi
271
+
272
+ local selected="${branches[$((choice - 1))]}"
273
+ if [ "$selected" != "$current_branch" ]; then
274
+ git checkout "$selected" || die "Could not switch to branch '$selected'."
275
+ fi
276
+ git config --local xgem.default-branch "$selected"
277
+ log_success "Switched to '$selected' and set as default for this repo."
278
+ }
279
+
280
+ # xgem git <cmt|init|rm-remote|rm-branch|branch> ...
148
281
  cmd_git() {
149
282
  case "$1" in
150
283
  cmt) cmd_git_cmt "$2" ;;
151
284
  init) cmd_git_init ;;
152
285
  rm-remote) cmd_git_rm_remote ;;
153
286
  rm-branch) cmd_git_rm_branch ;;
154
- *) die "Unknown git subcommand '$1'. Usage: xgem git <cmt|init|rm-remote|rm-branch>" ;;
287
+ branch) cmd_git_branch ;;
288
+ *) die "Unknown git subcommand '$1'. Usage: xgem git <cmt|init|rm-remote|rm-branch|branch>" ;;
155
289
  esac
156
290
  }
package/lib/ios.sh CHANGED
@@ -60,8 +60,13 @@ ios_deployment_target_for_config() {
60
60
 
61
61
  if has_cmd xcodebuild; then
62
62
  local value
63
+ # Anchored: newer Xcode also emits DEPLOYMENT_TARGET_SETTING_NAME =
64
+ # IPHONEOS_DEPLOYMENT_TARGET (a meta-setting whose *value* is that
65
+ # string), which an unanchored grep matches before the real
66
+ # "IPHONEOS_DEPLOYMENT_TARGET = 15.6" line — anchoring to the key
67
+ # position avoids grabbing that decoy.
63
68
  value=$(xcodebuild -showBuildSettings "${xcb_args[@]}" -configuration "$config" 2>/dev/null \
64
- | grep -m1 'IPHONEOS_DEPLOYMENT_TARGET' | awk '{print $NF}')
69
+ | grep -m1 -E '^[[:space:]]*IPHONEOS_DEPLOYMENT_TARGET[[:space:]]*=' | awk '{print $NF}')
65
70
  [ -n "$value" ] && { echo "$value"; return 0; }
66
71
  fi
67
72
 
@@ -80,15 +85,36 @@ ios_pbxproj_current_max_target() {
80
85
  }
81
86
 
82
87
  # ios_required_spm_deployment_target [project_dir]
83
- # Reads .dart_tool/package_config.json (written by `flutter pub get`) to
84
- # find every resolved package's own ios/Package.swift, and returns the
85
- # highest .iOS(.vNN) platform floor declared across them. This replaces
86
- # hardcoding a guessed value (e.g. "15.0") with an actual measurement of
87
- # what the resolved dependency graph requires.
88
+ # Returns the highest .iOS(.vNN) platform floor declared across every
89
+ # resolved SwiftPM plugin, instead of hardcoding a guessed value.
90
+ #
91
+ # Primary source: ios/Flutter/ephemeral/Packages/.packages/<name>-<version>/,
92
+ # which `flutter pub get` itself populates with each SPM-enabled plugin as
93
+ # part of dependency resolution — this is the actual, already-resolved
94
+ # location Xcode/SPM uses, not a path we have to reconstruct. Scanning it
95
+ # directly sidesteps having to guess each plugin's on-disk manifest layout
96
+ # (which varies — not every plugin puts Package.swift at <root>/ios/).
97
+ #
98
+ # Falls back to parsing .dart_tool/package_config.json's rootUri entries
99
+ # (assuming the common <root>/ios/Package.swift convention) only if that
100
+ # ephemeral directory doesn't exist yet — e.g. before any pub get has run.
88
101
  ios_required_spm_deployment_target() {
89
102
  local project_dir=${1:-.}
90
- local pkg_config="$project_dir/.dart_tool/package_config.json"
103
+ local packages_dir="$project_dir/ios/Flutter/ephemeral/Packages/.packages"
104
+
105
+ if [ -d "$packages_dir" ]; then
106
+ # -L: per-plugin dirs here are frequently symlinks into pub-cache;
107
+ # without -L, find silently skips descending into them.
108
+ find -L "$packages_dir" -maxdepth 4 -name "Package.swift" -print0 2>/dev/null \
109
+ | xargs -0 grep -ohE '\.iOS\(\.v[0-9_]+\)|\.iOS\("[0-9.]+"\)' 2>/dev/null \
110
+ | grep -oE '[0-9]+(\.[0-9]+)?(_[0-9]+)?' | tr '_' '.' \
111
+ | sort -g | tail -1
112
+ return 0
113
+ fi
114
+
115
+ log_debug "No resolved SwiftPM packages directory at $packages_dir yet; falling back to package_config.json."
91
116
 
117
+ local pkg_config="$project_dir/.dart_tool/package_config.json"
92
118
  [ -f "$pkg_config" ] || { log_debug "No .dart_tool/package_config.json — run flutter pub get first."; return 1; }
93
119
  has_cmd python3 || { log_debug "python3 not found; cannot parse package_config.json."; return 1; }
94
120
 
@@ -118,9 +144,12 @@ for pkg in data.get("packages", []):
118
144
  except OSError:
119
145
  continue
120
146
 
121
- for m in re.finditer(r"\.iOS\(\.v(\d+(?:_\d+)?)\)", content):
147
+ # Both SPM syntaxes: enum .iOS(.v15) and string .iOS("15.0") (the form
148
+ # Flutter's own generator uses).
149
+ for m in re.finditer(r'\.iOS\(\.v(\d+(?:_\d+)?)\)|\.iOS\("(\d+(?:\.\d+)?)"\)', content):
150
+ raw = m.group(1) or m.group(2)
122
151
  try:
123
- fv = float(m.group(1).replace("_", "."))
152
+ fv = float(raw.replace("_", "."))
124
153
  except ValueError:
125
154
  continue
126
155
  max_target = max(max_target, fv)
@@ -164,21 +193,40 @@ ios_generated_package_swift_path() {
164
193
  echo "$guess"
165
194
  return 0
166
195
  fi
167
- find "$project_dir/ios" -maxdepth 6 -type f -name "Package.swift" \
196
+ # -L: plugin package directories under ephemeral/Packages/.packages are
197
+ # frequently symlinks into the pub-cache; a plain find silently skips
198
+ # symlinked directories instead of descending into them.
199
+ find -L "$project_dir/ios" -maxdepth 6 -type f -name "Package.swift" \
168
200
  -path "*FlutterGeneratedPluginSwiftPackage*" 2>/dev/null | head -1
169
201
  }
170
202
 
203
+ # SPM's SupportedPlatform.iOS accepts two syntaxes: the enum form .iOS(.v15)
204
+ # for the fixed known-version cases, and a string form .iOS("15.0") for
205
+ # arbitrary versions — Flutter's own code generator uses the *string* form
206
+ # for FlutterGeneratedPluginSwiftPackage, not the enum form. Matching only
207
+ # the enum form (as an earlier version of this function did) means the
208
+ # patch step silently never matches anything on a real generated package:
209
+ # sed finds no match, writes back an identical file, and "succeeds" while
210
+ # doing nothing.
211
+ _ios_extract_platform_version() {
212
+ grep -oE '\.iOS\(\.v[0-9_]+\)|\.iOS\("[0-9.]+"\)' | head -1 \
213
+ | grep -oE '[0-9]+(\.[0-9]+)?(_[0-9]+)?' | tr '_' '.'
214
+ }
215
+
171
216
  ios_generated_package_target() {
172
217
  local project_dir=${1:-.}
173
218
  local pkg
174
219
  pkg=$(ios_generated_package_swift_path "$project_dir")
175
220
  [ -n "$pkg" ] && [ -f "$pkg" ] || return 1
176
- grep -oE '\.iOS\(\.v[0-9_]+\)' "$pkg" | head -1 | grep -oE '[0-9_]+' | tr '_' '.'
221
+ _ios_extract_platform_version < "$pkg"
177
222
  }
178
223
 
179
224
  # Last-resort workaround for the still-open upstream desync bug: directly
180
225
  # patch the generated package's platform declaration. Always logged loudly
181
226
  # — this is a documented workaround for a known bug, never a silent hack.
227
+ # Writes the string form (.iOS("15.0")) since that's what Flutter's own
228
+ # generator uses in this file — matching its existing convention rather
229
+ # than introducing a different syntax.
182
230
  ios_patch_generated_package_target() {
183
231
  local new_target=$1
184
232
  local project_dir=${2:-.}
@@ -190,10 +238,9 @@ ios_patch_generated_package_target() {
190
238
  fi
191
239
 
192
240
  log_warn "Working around known Flutter SPM bug (flutter/flutter#186804): the regenerated package still doesn't match your deployment target, so patching it directly."
193
- local token=${new_target//./_}
194
241
  local tmp
195
242
  tmp=$(mktemp)
196
- sed -E "s/\.iOS\(\.v[0-9_]+\)/.iOS(.v${token})/g" "$pkg" > "$tmp"
243
+ sed -E "s/\.iOS\(\.v[0-9_]+\)/.iOS(\"${new_target}\")/g; s/\.iOS\(\"[0-9.]+\"\)/.iOS(\"${new_target}\")/g" "$pkg" > "$tmp"
197
244
  mv "$tmp" "$pkg"
198
245
  log_success "Patched generated package platform to iOS $new_target."
199
246
  }
@@ -205,14 +252,39 @@ ios_regenerate_generated_package() {
205
252
  ( cd "$project_dir" && flutter pub get )
206
253
  }
207
254
 
255
+ # ios_parse_required_from_build_log <log_file>
256
+ # Statically scanning plugin manifests to predict the required deployment
257
+ # target has proven unreliable in practice (plugin authors structure SPM
258
+ # manifests differently, package dirs can be symlinks, etc.). Xcode itself
259
+ # always computes this correctly and states it plainly in its own error:
260
+ # "requires minimum platform version X for the iOS platform". Parsing that
261
+ # directly is the authoritative fallback when the static pre-check misses
262
+ # something.
263
+ ios_parse_required_from_build_log() {
264
+ local log_file=$1
265
+ grep -oE "requires minimum platform version [0-9]+(\.[0-9]+)?" "$log_file" 2>/dev/null \
266
+ | grep -oE '[0-9]+(\.[0-9]+)?' \
267
+ | sort -g | tail -1
268
+ }
269
+
208
270
  ios_known_issue_check() {
209
- log_warn "Known upstream issue (flutter/flutter#186804, #189422, #162072): FlutterGeneratedPluginSwiftPackage's deployment target can desync from your app's IPHONEOS_DEPLOYMENT_TARGET. Currently open — xgem detects and works around it automatically below."
271
+ log_warn "Known upstream issue (flutter/flutter#186804, #189422, #162072): FlutterGeneratedPluginSwiftPackage's deployment target can desync from your app's IPHONEOS_DEPLOYMENT_TARGET — independently, even when the app's own target is already high enough. Currently open — xgem detects and works around it automatically below."
272
+ }
273
+
274
+ # _ios_ver_lt a b -> true if a < b, numeric comparison (not string equality,
275
+ # since "15" and "15.0" are the same version but different strings).
276
+ _ios_ver_lt() {
277
+ awk -v a="$1" -v b="$2" 'BEGIN{exit !(a<b)}'
210
278
  }
211
279
 
212
280
  # ios_reconcile_deployment_target [project_dir]
213
- # The core "self-healing" step: detect a required-vs-current mismatch, plan
214
- # the fix, confirm (unless --yes/--dry-run), apply, regenerate, verify, and
215
- # fall back to the documented workaround if the upstream bug is still biting.
281
+ # The core "self-healing" step. Checks TWO independent things against the
282
+ # required version the app's own pbxproj target, AND
283
+ # FlutterGeneratedPluginSwiftPackage's own declared platform because the
284
+ # upstream bug is exactly that these two can desync from each other. A
285
+ # project whose pbxproj is already at (say) 15.6 can still have a generated
286
+ # package stuck at 13.0; checking only the pbxproj target (as an earlier
287
+ # version of this function did) misses that case entirely.
216
288
  # Returns 0 if no action was needed or the fix succeeded; 1 if it couldn't
217
289
  # reconcile and the caller should stop before attempting a build.
218
290
  ios_reconcile_deployment_target() {
@@ -220,46 +292,53 @@ ios_reconcile_deployment_target() {
220
292
 
221
293
  ios_project_uses_spm "$project_dir" || { log_debug "Project does not use SwiftPM; skipping deployment-target reconciliation."; return 0; }
222
294
 
223
- local required current
295
+ local required
224
296
  required=$(ios_required_spm_deployment_target "$project_dir")
225
- current=$(ios_deployment_target_for_config "Release" "$project_dir")
226
-
227
297
  if [ -z "$required" ]; then
228
298
  log_debug "Could not determine a required deployment target from resolved SPM plugins; skipping reconciliation."
229
299
  return 0
230
300
  fi
231
- if [ -z "$current" ]; then
232
- log_warn "Could not determine the project's current deployment target; proceeding without reconciliation."
301
+
302
+ ios_known_issue_check
303
+
304
+ local current generated
305
+ current=$(ios_deployment_target_for_config "Release" "$project_dir")
306
+ generated=$(ios_generated_package_target "$project_dir")
307
+
308
+ local pbxproj_needs_bump=0 generated_needs_fix=0
309
+ [ -n "$current" ] && _ios_ver_lt "$current" "$required" && pbxproj_needs_bump=1
310
+ if [ -z "$generated" ] || _ios_ver_lt "$generated" "$required"; then
311
+ generated_needs_fix=1
312
+ fi
313
+
314
+ if [ "$pbxproj_needs_bump" = 0 ] && [ "$generated_needs_fix" = 0 ]; then
315
+ log_debug "Deployment target ($current) and generated package ($generated) already satisfy SwiftPM requirement ($required)."
233
316
  return 0
234
317
  fi
235
318
 
236
- ios_known_issue_check
319
+ log_warn "Resolved SwiftPM plugins require iOS $required."
320
+ [ "$pbxproj_needs_bump" = 1 ] && echo " - project.pbxproj currently targets iOS $current"
321
+ [ "$generated_needs_fix" = 1 ] && echo " - FlutterGeneratedPluginSwiftPackage currently declares iOS ${generated:-unknown}"
322
+ echo "Plan:"
323
+ [ "$pbxproj_needs_bump" = 1 ] && echo " 1. Set IPHONEOS_DEPLOYMENT_TARGET = $required across every build configuration in project.pbxproj"
324
+ echo " 2. Clear ios/Flutter/ephemeral and re-run 'flutter pub get' to regenerate FlutterGeneratedPluginSwiftPackage"
325
+ echo " 3. Verify the regenerated package declares iOS $required; if it still doesn't (known upstream bug), patch it directly"
326
+
327
+ if ! confirm "Apply this fix?"; then
328
+ log_warn "Skipped. The build will likely fail SwiftPM resolution until this is reconciled manually."
329
+ return 1
330
+ fi
331
+
332
+ [ "$pbxproj_needs_bump" = 1 ] && ios_pbxproj_patch_deployment_target "$required" "$project_dir"
333
+ ios_regenerate_generated_package "$project_dir"
237
334
 
238
- if awk -v a="$required" -v b="$current" 'BEGIN{exit !(a>b)}'; then
239
- log_warn "Resolved SwiftPM plugins require iOS $required, but the project targets iOS $current."
240
- echo "Plan:"
241
- echo " 1. Set IPHONEOS_DEPLOYMENT_TARGET = $required across every build configuration in project.pbxproj"
242
- echo " 2. Clear ios/Flutter/ephemeral and re-run 'flutter pub get' to regenerate FlutterGeneratedPluginSwiftPackage"
243
- echo " 3. Verify the regenerated package declares iOS $required; if it still doesn't (known upstream bug), patch it directly"
244
-
245
- if ! confirm "Apply this fix?"; then
246
- log_warn "Skipped. The build will likely fail SwiftPM resolution until the deployment target is reconciled manually."
247
- return 1
248
- fi
249
-
250
- ios_pbxproj_patch_deployment_target "$required" "$project_dir"
251
- ios_regenerate_generated_package "$project_dir"
252
-
253
- local regenerated
254
- regenerated=$(ios_generated_package_target "$project_dir")
255
- if [ "$regenerated" = "$required" ]; then
256
- log_success "Generated package now correctly declares iOS $regenerated."
257
- else
258
- log_warn "Generated package declares '${regenerated:-unknown}' after regeneration, not $required."
259
- ios_patch_generated_package_target "$required" "$project_dir"
260
- fi
335
+ local regenerated
336
+ regenerated=$(ios_generated_package_target "$project_dir")
337
+ if [ -n "$regenerated" ] && ! _ios_ver_lt "$regenerated" "$required"; then
338
+ log_success "Generated package now correctly declares iOS $regenerated."
261
339
  else
262
- log_debug "Deployment target ($current) already satisfies SwiftPM requirement ($required)."
340
+ log_warn "Generated package declares '${regenerated:-unknown}' after regeneration, still below $required."
341
+ ios_patch_generated_package_target "$required" "$project_dir"
263
342
  fi
264
343
 
265
344
  return 0
package/lib/scaffold.sh CHANGED
@@ -11,14 +11,33 @@
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
+
16
+ # _package_json_has_script <script> — checks the CURRENT directory's
17
+ # package.json, not a guess. Used to only offer lint/test automation when
18
+ # the project actually has those scripts, instead of always generating
19
+ # scripts that fail with "Missing script" on projects that don't.
20
+ _package_json_has_script() {
21
+ [ -f package.json ] || return 1
22
+ grep -qE "\"$1\"[[:space:]]*:" package.json 2>/dev/null
23
+ }
15
24
 
16
25
  framework_scripts() {
17
26
  case "$1" in
18
27
  flutter) echo "hard-clean build build-runner" ;;
19
- node) echo "hard-clean build start" ;;
28
+ node)
29
+ local scripts="hard-clean build start"
30
+ _package_json_has_script lint && scripts="$scripts lint"
31
+ _package_json_has_script test && scripts="$scripts test"
32
+ echo "$scripts"
33
+ ;;
20
34
  python) echo "hard-clean install" ;;
21
- react|vue|angular) echo "hard-clean build dev" ;;
35
+ react|vue|angular|next)
36
+ local scripts="hard-clean build dev"
37
+ _package_json_has_script lint && scripts="$scripts lint"
38
+ _package_json_has_script test && scripts="$scripts test"
39
+ echo "$scripts"
40
+ ;;
22
41
  go) echo "hard-clean build" ;;
23
42
  rust) echo "hard-clean build" ;;
24
43
  docker) echo "hard-clean build-up" ;;
@@ -30,8 +49,8 @@ framework_scripts() {
30
49
  _scaffold_template_dir() {
31
50
  local framework=$1
32
51
  case "$framework" in
33
- react|vue|angular) echo "$XGEM_HOME/templates/webframework" ;;
34
- *) echo "$XGEM_HOME/templates/$framework" ;;
52
+ react|vue|angular|next) echo "$XGEM_HOME/templates/webframework" ;;
53
+ *) echo "$XGEM_HOME/templates/$framework" ;;
35
54
  esac
36
55
  }
37
56
 
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"
4
+ XGEM_VERSION="2.0.0-alpha.12"
5
5
 
6
6
  print_version() {
7
7
  echo "xgem $XGEM_VERSION"
@@ -0,0 +1,44 @@
1
+ // xgem Windows engine doctor — mirrors lib/doctor.sh's general report.
2
+ // No iOS/SwiftPM engine exists here (see flutter.js) since Xcode has no
3
+ // Windows equivalent; `doctor ios` explains that instead of pretending.
4
+
5
+ const { getVersion, detectArch } = require('./utils');
6
+ const { paint } = require('./logger');
7
+
8
+ function printGeneral() {
9
+ console.log(paint('cyan', '=== xgem doctor (Windows) ==='));
10
+ console.log(`OS: win32`);
11
+ console.log(`Arch: ${detectArch()}`);
12
+ console.log(`git: ${getVersion('git') || 'not found'}`);
13
+ console.log(`flutter: ${getVersion('flutter') || 'not found'}`);
14
+ console.log(`dart: ${getVersion('dart') || 'not found'}`);
15
+ console.log(`node: ${getVersion('node') || 'not found'}`);
16
+ console.log(`python: ${getVersion('python') || getVersion('python3') || 'not found'}`);
17
+ console.log(`go: ${getVersion('go', ['version']) || 'not found'}`);
18
+ console.log(`cargo: ${getVersion('cargo') || 'not found'}`);
19
+ console.log(`docker: ${getVersion('docker') || 'not found'}`);
20
+ }
21
+
22
+ function printIos() {
23
+ console.log(paint('cyan', '=== xgem doctor ios (Windows) ==='));
24
+ console.log(paint('yellow', 'iOS/macOS builds are not available on Windows — Xcode has no Windows equivalent.'));
25
+ console.log('Use WSL2 with a Mac, or a real Mac, for Flutter iOS builds.');
26
+ const flutterVersion = getVersion('flutter');
27
+ if (flutterVersion) {
28
+ console.log(`\nflutter is installed here (${flutterVersion}) and can still build Android/Windows targets:`);
29
+ console.log(' xgem run flutter build (choose APK or Windows)');
30
+ }
31
+ }
32
+
33
+ function cmdDoctor(target) {
34
+ if (target === 'ios') {
35
+ printIos();
36
+ } else if (!target) {
37
+ printGeneral();
38
+ } else {
39
+ const { die } = require('./logger');
40
+ die(`Unknown doctor target '${target}'. Usage: xgem doctor [ios]`);
41
+ }
42
+ }
43
+
44
+ module.exports = { cmdDoctor, printGeneral, printIos };