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.
- package/README.md +53 -31
- package/bin/xgem +21 -14
- package/bin/xgem.js +235 -0
- package/lib/create.sh +334 -0
- package/lib/doctor.sh +13 -2
- package/lib/flutter.sh +163 -18
- package/lib/git.sh +157 -23
- package/lib/ios.sh +126 -47
- package/lib/scaffold.sh +24 -5
- package/lib/version.sh +1 -1
- package/lib-win/doctor.js +44 -0
- package/lib-win/flutter.js +101 -0
- package/lib-win/git.js +279 -0
- package/lib-win/logger.js +35 -0
- package/lib-win/scaffold.js +102 -0
- package/lib-win/utils.js +67 -0
- package/package.json +8 -7
- package/templates/node/build.sh.tmpl +9 -1
- package/templates/node/hard-clean.sh.tmpl +34 -2
- package/templates/node/lint.sh.tmpl +10 -0
- package/templates/node/start.sh.tmpl +9 -1
- package/templates/node/test.sh.tmpl +10 -0
- package/templates/webframework/build.sh.tmpl +10 -1
- package/templates/webframework/dev.sh.tmpl +9 -1
- package/templates/webframework/hard-clean.sh.tmpl +35 -2
- package/templates/webframework/lint.sh.tmpl +10 -0
- package/templates/webframework/test.sh.tmpl +10 -0
- package/templates-win/docker/build-up.mjs.tmpl +3 -0
- package/templates-win/docker/hard-clean.mjs.tmpl +4 -0
- package/templates-win/flutter/build-runner.mjs.tmpl +4 -0
- package/templates-win/flutter/build.mjs.tmpl +4 -0
- package/templates-win/flutter/hard-clean.mjs.tmpl +6 -0
- package/templates-win/go/build.mjs.tmpl +3 -0
- package/templates-win/go/hard-clean.mjs.tmpl +4 -0
- package/templates-win/node/build.mjs.tmpl +10 -0
- package/templates-win/node/hard-clean.mjs.tmpl +28 -0
- package/templates-win/node/lint.mjs.tmpl +10 -0
- package/templates-win/node/start.mjs.tmpl +10 -0
- package/templates-win/node/test.mjs.tmpl +10 -0
- package/templates-win/python/hard-clean.mjs.tmpl +8 -0
- package/templates-win/python/install.mjs.tmpl +12 -0
- package/templates-win/rust/build.mjs.tmpl +3 -0
- package/templates-win/rust/hard-clean.mjs.tmpl +3 -0
- package/templates-win/webframework/build.mjs.tmpl +10 -0
- package/templates-win/webframework/dev.mjs.tmpl +10 -0
- package/templates-win/webframework/hard-clean.mjs.tmpl +32 -0
- package/templates-win/webframework/lint.mjs.tmpl +10 -0
- package/templates-win/webframework/test.mjs.tmpl +10 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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)
|
|
163
|
-
echo "3)
|
|
164
|
-
echo "4)
|
|
165
|
-
echo "5)
|
|
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-
|
|
168
|
-
if ! [[ "$platform_choice" =~ ^[1-
|
|
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
|
|
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
|
-
|
|
184
|
-
4) flutter build
|
|
185
|
-
5) flutter build
|
|
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
|
+
}
|