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 +44 -7
- package/bin/xgem +9 -4
- package/bin/xgem.js +3 -1
- package/lib/create.sh +226 -0
- package/lib/flutter.sh +103 -0
- package/lib/git.sh +81 -2
- package/lib/scaffold.sh +4 -4
- package/lib/version.sh +1 -1
- package/lib-win/git.js +61 -1
- package/lib-win/scaffold.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# xgem
|
|
2
2
|
|
|
3
|
-
A framework-aware automation CLI. `xgem init`
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
*)
|
|
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
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
|
|
package/lib-win/scaffold.js
CHANGED
|
@@ -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.
|
|
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"
|