xgem-cli 2.0.0-alpha

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/lib/git.sh ADDED
@@ -0,0 +1,156 @@
1
+ #!/bin/bash
2
+ # xgem git workflow helper: cmt / init / rm-remote / rm-branch.
3
+ # Depends on lib/logger.sh.
4
+
5
+ cmd_git_cmt() {
6
+ local commit_msg=$1
7
+ [ -n "$commit_msg" ] || die "Missing commit message!"
8
+
9
+ log_info "Checking repository status..."
10
+ git status -s
11
+ echo ""
12
+
13
+ local add_choice
14
+ read -r -p "Do you want to add ALL files (a) or INDIVIDUAL files (i)? [a/i]: " add_choice
15
+ if [[ "$add_choice" == "a" || "$add_choice" == "A" ]]; then
16
+ git add .
17
+ log_success "All files staged."
18
+ elif [[ "$add_choice" == "i" || "$add_choice" == "I" ]]; then
19
+ local -a specific_files
20
+ read -r -p "Enter specific file paths to add (space separated): " -a specific_files
21
+ git add "${specific_files[@]}"
22
+ log_success "Selected files staged."
23
+ else
24
+ die "Invalid choice. Operation aborted."
25
+ fi
26
+
27
+ log_info "Committing changes..."
28
+ if ! git commit -m "$commit_msg"; then
29
+ die "Commit failed. Aborting before pull/push."
30
+ fi
31
+
32
+ local current_branch remote_name
33
+ current_branch=$(git branch --show-current 2>/dev/null)
34
+ if git remote | grep -q "^origin$"; then
35
+ remote_name="origin"
36
+ else
37
+ remote_name=$(git remote | head -n 1)
38
+ fi
39
+
40
+ if [ -z "$remote_name" ]; then
41
+ log_warn "Local commit dropped cleanly, but no remote is configured — sync was skipped."
42
+ return 0
43
+ fi
44
+
45
+ log_info "Pulling updates from remote '$remote_name' on '$current_branch' via rebase..."
46
+ if ! git pull --rebase "$remote_name" "$current_branch"; then
47
+ log_error "MERGE CONFLICT DETECTED!"
48
+ log_warn "Execution paused. Resolve conflicts to proceed."
49
+ local open_editor
50
+ read -r -p "Do you want to open VS Code to resolve this now? (y/n): " open_editor
51
+ [[ "$open_editor" == "y" || "$open_editor" == "Y" ]] && code .
52
+ exit 1
53
+ fi
54
+
55
+ log_success "Clean sync pull achieved. Pushing to upstream target..."
56
+ if git push "$remote_name" "$current_branch"; then
57
+ log_success "Git workflow complete! Code cleanly committed and synchronized."
58
+ else
59
+ die "Push operation failed."
60
+ fi
61
+ }
62
+
63
+ cmd_git_init() {
64
+ log_info "Initializing local Git repository..."
65
+ git init
66
+
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"
72
+
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
80
+ fi
81
+
82
+ read -r -p "Enter branch name (default: main): " branch_name
83
+ branch_name=${branch_name:-main}
84
+ git branch -M "$branch_name"
85
+
86
+ git add .
87
+ git commit -m "initial changes" 2>/dev/null
88
+ log_success "Local baseline configuration setup completed."
89
+ }
90
+
91
+ cmd_git_rm_remote() {
92
+ log_info "Current configured remotes:"
93
+ git remote -v
94
+ echo ""
95
+
96
+ local remote_name
97
+ read -r -p "Enter the short remote name to remove (e.g. origin): " remote_name
98
+ remote_name=${remote_name:-origin}
99
+
100
+ if git remote | grep -q "^$remote_name$"; then
101
+ git remote remove "$remote_name"
102
+ log_success "Successfully removed remote reference configuration: $remote_name"
103
+ else
104
+ die "Remote tracking short-name '$remote_name' does not exist."
105
+ fi
106
+ }
107
+
108
+ cmd_git_rm_branch() {
109
+ local target_branch
110
+ read -r -p "Enter the name of the branch you want to target: " target_branch
111
+ [ -n "$target_branch" ] || die "Branch name cannot be empty."
112
+
113
+ local where_choice
114
+ read -r -p "Where do you want to delete this branch? (l = local only, r = remote only, b = both) [l/r/b]: " where_choice
115
+ echo ""
116
+
117
+ if [[ "$where_choice" =~ ^[lLbB]$ ]]; then
118
+ local current_branch
119
+ current_branch=$(git branch --show-current 2>/dev/null)
120
+ if [ "$current_branch" = "$target_branch" ]; then
121
+ log_warn "You are currently sitting on '$target_branch'. Switching to safe branch..."
122
+ git checkout main 2>/dev/null || git checkout master 2>/dev/null || git checkout dev 2>/dev/null
123
+ fi
124
+
125
+ log_info "Force deleting local branch '$target_branch'..."
126
+ if git branch -D "$target_branch"; then
127
+ log_success "Successfully deleted local copy of branch."
128
+ else
129
+ log_warn "Local branch could not be dropped (it may already be gone)."
130
+ fi
131
+ fi
132
+
133
+ if [[ "$where_choice" =~ ^[rRbB]$ ]]; then
134
+ local remote_target
135
+ read -r -p "Enter remote identifier (short-name like 'origin'): " remote_target
136
+ remote_target=${remote_target:-origin}
137
+
138
+ log_info "Sending deletion request for remote branch '$target_branch' to server..."
139
+ if git push "$remote_target" --delete "$target_branch"; then
140
+ log_success "Successfully wiped out remote branch '$target_branch' from server."
141
+ else
142
+ die "Server rejected the branch drop execution request."
143
+ fi
144
+ fi
145
+ }
146
+
147
+ # xgem git <cmt|init|rm-remote|rm-branch> ...
148
+ cmd_git() {
149
+ case "$1" in
150
+ cmt) cmd_git_cmt "$2" ;;
151
+ init) cmd_git_init ;;
152
+ rm-remote) cmd_git_rm_remote ;;
153
+ rm-branch) cmd_git_rm_branch ;;
154
+ *) die "Unknown git subcommand '$1'. Usage: xgem git <cmt|init|rm-remote|rm-branch>" ;;
155
+ esac
156
+ }
package/lib/ios.sh ADDED
@@ -0,0 +1,266 @@
1
+ #!/bin/bash
2
+ # xgem iOS build engine — redesigned to handle Swift Package Manager (SPM)
3
+ # reliably, not just CocoaPods.
4
+ #
5
+ # Background (why this exists): Flutter auto-generates a wrapper Swift
6
+ # package, FlutterGeneratedPluginSwiftPackage, that aggregates plugin SPM
7
+ # dependencies. Its declared minimum iOS platform does not always stay in
8
+ # sync with the app's IPHONEOS_DEPLOYMENT_TARGET in project.pbxproj — a
9
+ # confirmed, currently-open upstream Flutter bug (flutter/flutter#186804,
10
+ # #189422, #162072). A single sed patch to pbxproj alone does not fix this
11
+ # reliably: (a) pbxproj has multiple IPHONEOS_DEPLOYMENT_TARGET entries
12
+ # across Debug/Release/Profile x Runner/RunnerTests, and (b) even a fully
13
+ # correct pbxproj patch doesn't force-regenerate the generated package.
14
+ #
15
+ # This engine: detects the actual required deployment target from resolved
16
+ # SPM plugin manifests (instead of hardcoding a guess), patches every
17
+ # pbxproj occurrence, forces regeneration, verifies the regenerated package
18
+ # matches, and falls back to directly patching the generated package as a
19
+ # documented last resort if the upstream desync bug is still present.
20
+ #
21
+ # Depends on lib/logger.sh, lib/utils.sh.
22
+
23
+ ios_pbxproj_path() {
24
+ local project_dir=${1:-.}
25
+ echo "$project_dir/ios/Runner.xcodeproj/project.pbxproj"
26
+ }
27
+
28
+ ios_project_uses_pods() {
29
+ local project_dir=${1:-.}
30
+ [ -f "$project_dir/ios/Podfile" ]
31
+ }
32
+
33
+ # ios_project_uses_spm: authoritative signal is a reference to the generated
34
+ # aggregate package inside the Xcode project itself, not just Flutter's
35
+ # global config toggle (which reflects intent, not what's actually wired
36
+ # into *this* project).
37
+ ios_project_uses_spm() {
38
+ local project_dir=${1:-.}
39
+ local pbxproj
40
+ pbxproj=$(ios_pbxproj_path "$project_dir")
41
+ [ -f "$pbxproj" ] && grep -q "FlutterGeneratedPluginSwiftPackage" "$pbxproj"
42
+ }
43
+
44
+ # ios_deployment_target_for_config <Debug|Release|Profile> [project_dir]
45
+ # Authoritative: asks Xcode itself via -showBuildSettings rather than
46
+ # grepping pbxproj, so it reflects any xcconfig overrides too. Falls back
47
+ # to the pbxproj's own declared value if xcodebuild isn't available.
48
+ ios_deployment_target_for_config() {
49
+ local config=$1
50
+ local project_dir=${2:-.}
51
+ local -a xcb_args=()
52
+
53
+ if [ -f "$project_dir/ios/Runner.xcworkspace/contents.xcworkspacedata" ]; then
54
+ xcb_args=(-workspace "$project_dir/ios/Runner.xcworkspace" -scheme Runner)
55
+ elif [ -d "$project_dir/ios/Runner.xcodeproj" ]; then
56
+ xcb_args=(-project "$project_dir/ios/Runner.xcodeproj" -target Runner)
57
+ else
58
+ return 1
59
+ fi
60
+
61
+ if has_cmd xcodebuild; then
62
+ local value
63
+ value=$(xcodebuild -showBuildSettings "${xcb_args[@]}" -configuration "$config" 2>/dev/null \
64
+ | grep -m1 'IPHONEOS_DEPLOYMENT_TARGET' | awk '{print $NF}')
65
+ [ -n "$value" ] && { echo "$value"; return 0; }
66
+ fi
67
+
68
+ log_debug "xcodebuild unavailable or gave no result; falling back to pbxproj grep for $config."
69
+ ios_pbxproj_current_max_target "$project_dir"
70
+ }
71
+
72
+ ios_pbxproj_current_max_target() {
73
+ local project_dir=${1:-.}
74
+ local pbxproj
75
+ pbxproj=$(ios_pbxproj_path "$project_dir")
76
+ [ -f "$pbxproj" ] || return 1
77
+ grep -oE 'IPHONEOS_DEPLOYMENT_TARGET = [0-9]+(\.[0-9]+)?' "$pbxproj" \
78
+ | grep -oE '[0-9]+(\.[0-9]+)?' \
79
+ | sort -g | tail -1
80
+ }
81
+
82
+ # 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
+ ios_required_spm_deployment_target() {
89
+ local project_dir=${1:-.}
90
+ local pkg_config="$project_dir/.dart_tool/package_config.json"
91
+
92
+ [ -f "$pkg_config" ] || { log_debug "No .dart_tool/package_config.json — run flutter pub get first."; return 1; }
93
+ has_cmd python3 || { log_debug "python3 not found; cannot parse package_config.json."; return 1; }
94
+
95
+ python3 - "$pkg_config" <<'PYEOF'
96
+ import json, os, re, sys
97
+
98
+ pkg_config_path = sys.argv[1]
99
+ with open(pkg_config_path) as f:
100
+ data = json.load(f)
101
+
102
+ base_dir = os.path.dirname(os.path.abspath(pkg_config_path))
103
+ max_target = 0.0
104
+
105
+ for pkg in data.get("packages", []):
106
+ root_uri = pkg.get("rootUri", "")
107
+ if root_uri.startswith("file://"):
108
+ root = root_uri[len("file://"):]
109
+ else:
110
+ root = os.path.normpath(os.path.join(base_dir, root_uri))
111
+
112
+ manifest = os.path.join(root, "ios", "Package.swift")
113
+ if not os.path.isfile(manifest):
114
+ continue
115
+ try:
116
+ with open(manifest, "r", errors="ignore") as mf:
117
+ content = mf.read()
118
+ except OSError:
119
+ continue
120
+
121
+ for m in re.finditer(r"\.iOS\(\.v(\d+(?:_\d+)?)\)", content):
122
+ try:
123
+ fv = float(m.group(1).replace("_", "."))
124
+ except ValueError:
125
+ continue
126
+ max_target = max(max_target, fv)
127
+
128
+ if max_target > 0:
129
+ print(max_target)
130
+ PYEOF
131
+ }
132
+
133
+ # ios_pbxproj_patch_deployment_target <new_target> [project_dir]
134
+ # Patches EVERY IPHONEOS_DEPLOYMENT_TARGET occurrence (all configs, all
135
+ # targets), unlike a single-match sed. Keeps a timestamped backup.
136
+ ios_pbxproj_patch_deployment_target() {
137
+ local new_target=$1
138
+ local project_dir=${2:-.}
139
+ local pbxproj
140
+ pbxproj=$(ios_pbxproj_path "$project_dir")
141
+ [ -f "$pbxproj" ] || die "project.pbxproj not found at $pbxproj"
142
+
143
+ local backup="${pbxproj}.xgem-bak-$(date +%Y%m%d%H%M%S)"
144
+ cp "$pbxproj" "$backup"
145
+ log_debug "Backed up pbxproj to $backup"
146
+
147
+ local tmp
148
+ tmp=$(mktemp)
149
+ sed -E "s/(IPHONEOS_DEPLOYMENT_TARGET = )[0-9]+(\.[0-9]+)?;/\1${new_target};/g" "$pbxproj" > "$tmp"
150
+ mv "$tmp" "$pbxproj"
151
+
152
+ local count
153
+ count=$(grep -c "IPHONEOS_DEPLOYMENT_TARGET = ${new_target};" "$pbxproj")
154
+ log_success "Patched $count IPHONEOS_DEPLOYMENT_TARGET occurrence(s) in project.pbxproj to $new_target."
155
+ }
156
+
157
+ # Locates the generated aggregate package. The exact path has moved across
158
+ # Flutter releases, so this feature-detects it via `find` rather than
159
+ # assuming a single fixed location.
160
+ ios_generated_package_swift_path() {
161
+ local project_dir=${1:-.}
162
+ local guess="$project_dir/ios/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage/Package.swift"
163
+ if [ -f "$guess" ]; then
164
+ echo "$guess"
165
+ return 0
166
+ fi
167
+ find "$project_dir/ios" -maxdepth 6 -type f -name "Package.swift" \
168
+ -path "*FlutterGeneratedPluginSwiftPackage*" 2>/dev/null | head -1
169
+ }
170
+
171
+ ios_generated_package_target() {
172
+ local project_dir=${1:-.}
173
+ local pkg
174
+ pkg=$(ios_generated_package_swift_path "$project_dir")
175
+ [ -n "$pkg" ] && [ -f "$pkg" ] || return 1
176
+ grep -oE '\.iOS\(\.v[0-9_]+\)' "$pkg" | head -1 | grep -oE '[0-9_]+' | tr '_' '.'
177
+ }
178
+
179
+ # Last-resort workaround for the still-open upstream desync bug: directly
180
+ # patch the generated package's platform declaration. Always logged loudly
181
+ # — this is a documented workaround for a known bug, never a silent hack.
182
+ ios_patch_generated_package_target() {
183
+ local new_target=$1
184
+ local project_dir=${2:-.}
185
+ local pkg
186
+ pkg=$(ios_generated_package_swift_path "$project_dir")
187
+ if [ -z "$pkg" ] || [ ! -f "$pkg" ]; then
188
+ log_warn "Could not locate FlutterGeneratedPluginSwiftPackage/Package.swift to patch."
189
+ return 1
190
+ fi
191
+
192
+ 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
+ local tmp
195
+ tmp=$(mktemp)
196
+ sed -E "s/\.iOS\(\.v[0-9_]+\)/.iOS(.v${token})/g" "$pkg" > "$tmp"
197
+ mv "$tmp" "$pkg"
198
+ log_success "Patched generated package platform to iOS $new_target."
199
+ }
200
+
201
+ ios_regenerate_generated_package() {
202
+ local project_dir=${1:-.}
203
+ log_info "Clearing ephemeral iOS config to force a clean regeneration..."
204
+ rm -rf "$project_dir/ios/Flutter/ephemeral"
205
+ ( cd "$project_dir" && flutter pub get )
206
+ }
207
+
208
+ 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."
210
+ }
211
+
212
+ # 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.
216
+ # Returns 0 if no action was needed or the fix succeeded; 1 if it couldn't
217
+ # reconcile and the caller should stop before attempting a build.
218
+ ios_reconcile_deployment_target() {
219
+ local project_dir=${1:-.}
220
+
221
+ ios_project_uses_spm "$project_dir" || { log_debug "Project does not use SwiftPM; skipping deployment-target reconciliation."; return 0; }
222
+
223
+ local required current
224
+ required=$(ios_required_spm_deployment_target "$project_dir")
225
+ current=$(ios_deployment_target_for_config "Release" "$project_dir")
226
+
227
+ if [ -z "$required" ]; then
228
+ log_debug "Could not determine a required deployment target from resolved SPM plugins; skipping reconciliation."
229
+ return 0
230
+ fi
231
+ if [ -z "$current" ]; then
232
+ log_warn "Could not determine the project's current deployment target; proceeding without reconciliation."
233
+ return 0
234
+ fi
235
+
236
+ ios_known_issue_check
237
+
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
261
+ else
262
+ log_debug "Deployment target ($current) already satisfies SwiftPM requirement ($required)."
263
+ fi
264
+
265
+ return 0
266
+ }
package/lib/logger.sh ADDED
@@ -0,0 +1,28 @@
1
+ #!/bin/bash
2
+ # xgem logger — consistent log levels + verbose/debug mode.
3
+ # Sourced by bin/xgem before any other lib module.
4
+
5
+ : "${XGEM_VERBOSE:=0}"
6
+
7
+ _c_red=$'\033[31m'
8
+ _c_green=$'\033[32m'
9
+ _c_yellow=$'\033[33m'
10
+ _c_blue=$'\033[1;34m'
11
+ _c_gray=$'\033[90m'
12
+ _c_reset=$'\033[0m'
13
+
14
+ log_info() { echo -e "${_c_blue}[INFO]${_c_reset} $*"; }
15
+ log_success() { echo -e "${_c_green}[ OK ]${_c_reset} $*"; }
16
+ log_warn() { echo -e "${_c_yellow}[WARN]${_c_reset} $*" >&2; }
17
+ log_error() { echo -e "${_c_red}[FAIL]${_c_reset} $*" >&2; }
18
+
19
+ log_debug() {
20
+ [ "$XGEM_VERBOSE" = "1" ] || return 0
21
+ echo -e "${_c_gray}[DBG ] $*${_c_reset}" >&2
22
+ }
23
+
24
+ # die <message> [exit_code]
25
+ die() {
26
+ log_error "$1"
27
+ exit "${2:-1}"
28
+ }
@@ -0,0 +1,88 @@
1
+ #!/bin/bash
2
+ # xgem generic scaffold engine for frameworks whose automation is just a
3
+ # canned script (node, python, react/vue/angular, go, rust, docker, swift,
4
+ # and flutter's on-disk copies) — one module driven by template files
5
+ # instead of near-duplicate per-framework code.
6
+ #
7
+ # Flutter's `xgem run flutter <script>` is intercepted earlier by
8
+ # lib/flutter.sh's native commands (see flutter_native_command_exists in
9
+ # bin/xgem's dispatch); the flutter templates here still get scaffolded to
10
+ # `.xgem-automate/flutter/` for manual use and documentation parity.
11
+ #
12
+ # Depends on lib/logger.sh and XGEM_HOME (set by bin/xgem).
13
+
14
+ ALL_FRAMEWORKS=(flutter node python react vue angular go rust docker swift)
15
+
16
+ framework_scripts() {
17
+ case "$1" in
18
+ flutter) echo "hard-clean build build-runner" ;;
19
+ node) echo "hard-clean build start" ;;
20
+ python) echo "hard-clean install" ;;
21
+ react|vue|angular) echo "hard-clean build dev" ;;
22
+ go) echo "hard-clean build" ;;
23
+ rust) echo "hard-clean build" ;;
24
+ docker) echo "hard-clean build-up" ;;
25
+ swift) echo "hard-clean build" ;;
26
+ *) return 1 ;;
27
+ esac
28
+ }
29
+
30
+ _scaffold_template_dir() {
31
+ local framework=$1
32
+ case "$framework" in
33
+ react|vue|angular) echo "$XGEM_HOME/templates/webframework" ;;
34
+ *) echo "$XGEM_HOME/templates/$framework" ;;
35
+ esac
36
+ }
37
+
38
+ # scaffold_inject_templates <framework> <config_dir>
39
+ scaffold_inject_templates() {
40
+ local framework=$1
41
+ local config_dir=$2
42
+ local scripts src_dir dest_dir script
43
+
44
+ scripts=$(framework_scripts "$framework") || die "Unknown framework '$framework'."
45
+ src_dir=$(_scaffold_template_dir "$framework")
46
+ dest_dir="$config_dir/$framework"
47
+ mkdir -p "$dest_dir"
48
+
49
+ for script in $scripts; do
50
+ local src="$src_dir/${script}.sh.tmpl"
51
+ local dest="$dest_dir/${script}.sh"
52
+ if [ ! -f "$src" ]; then
53
+ log_warn "Missing template $src, skipping."
54
+ continue
55
+ fi
56
+ sed "s/__FRAMEWORK__/$framework/g" "$src" > "$dest"
57
+ chmod +x "$dest"
58
+ done
59
+ }
60
+
61
+ # scaffold_run_script <framework> <script> <config_dir>
62
+ scaffold_run_script() {
63
+ local framework=$1
64
+ local script=$2
65
+ local config_dir=$3
66
+ local target="$config_dir/$framework/${script}.sh"
67
+
68
+ if [ -f "$target" ]; then
69
+ log_info "Running script '$script' for $framework..."
70
+ bash "$target"
71
+ else
72
+ log_error "Script not found at $target"
73
+ if [ -d "$config_dir/$framework" ]; then
74
+ echo "Available scripts in '$framework':"
75
+ ls "$config_dir/$framework" | sed 's/\.sh$//' | sed 's/^/ - /'
76
+ fi
77
+ exit 1
78
+ fi
79
+ }
80
+
81
+ scaffold_get_remaining_frameworks() {
82
+ local config_dir=$1
83
+ REMAINING_FWS=()
84
+ local fw
85
+ for fw in "${ALL_FRAMEWORKS[@]}"; do
86
+ [ -d "$config_dir/$fw" ] || REMAINING_FWS+=("$fw")
87
+ done
88
+ }
package/lib/utils.sh ADDED
@@ -0,0 +1,61 @@
1
+ #!/bin/bash
2
+ # xgem shared utilities — os/arch detection, confirmation prompts, command checks.
3
+ # Depends on lib/logger.sh being sourced first.
4
+
5
+ : "${XGEM_YES:=0}"
6
+ : "${XGEM_DRY_RUN:=0}"
7
+
8
+ # detect_os -> darwin | linux | unknown
9
+ detect_os() {
10
+ case "$(uname -s)" in
11
+ Darwin) echo "darwin" ;;
12
+ Linux) echo "linux" ;;
13
+ *) echo "unknown" ;;
14
+ esac
15
+ }
16
+
17
+ # detect_arch -> arm64 | x86_64 | <raw uname -m>
18
+ detect_arch() {
19
+ uname -m
20
+ }
21
+
22
+ # is_apple_silicon -> 0 (true) if darwin + arm64, 1 otherwise
23
+ is_apple_silicon() {
24
+ [ "$(detect_os)" = "darwin" ] && [ "$(detect_arch)" = "arm64" ]
25
+ }
26
+
27
+ # has_cmd <name> -> 0/1, no output
28
+ has_cmd() {
29
+ command -v "$1" >/dev/null 2>&1
30
+ }
31
+
32
+ # require_cmd <name> [install-hint]
33
+ require_cmd() {
34
+ local name=$1
35
+ local hint=${2:-}
36
+ if ! has_cmd "$name"; then
37
+ if [ -n "$hint" ]; then
38
+ die "'$name' is required but not found. $hint"
39
+ else
40
+ die "'$name' is required but not found in PATH."
41
+ fi
42
+ fi
43
+ }
44
+
45
+ # confirm "prompt text" -> 0 if approved, 1 otherwise
46
+ # Honors XGEM_YES=1 (from --yes) to auto-approve, and always returns 1
47
+ # (declines) under XGEM_DRY_RUN so callers never apply changes in dry-run mode.
48
+ confirm() {
49
+ local prompt=$1
50
+ if [ "$XGEM_DRY_RUN" = "1" ]; then
51
+ log_info "(dry-run) would prompt: $prompt"
52
+ return 1
53
+ fi
54
+ if [ "$XGEM_YES" = "1" ]; then
55
+ log_debug "auto-confirmed (--yes): $prompt"
56
+ return 0
57
+ fi
58
+ local reply
59
+ read -r -p "$prompt [y/N]: " reply
60
+ [[ "$reply" == "y" || "$reply" == "Y" ]]
61
+ }
package/lib/version.sh ADDED
@@ -0,0 +1,8 @@
1
+ #!/bin/bash
2
+ # xgem version info.
3
+
4
+ XGEM_VERSION="2.0.0-alpha"
5
+
6
+ print_version() {
7
+ echo "xgem $XGEM_VERSION"
8
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "xgem-cli",
3
+ "version": "2.0.0-alpha",
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.",
5
+ "bin": {
6
+ "xgem": "bin/xgem"
7
+ },
8
+ "files": [
9
+ "bin/xgem",
10
+ "lib",
11
+ "templates"
12
+ ],
13
+ "os": [
14
+ "darwin",
15
+ "linux"
16
+ ],
17
+ "engines": {
18
+ "node": ">=14"
19
+ },
20
+ "scripts": {
21
+ "test": "bash -n bin/xgem && for f in lib/*.sh; do bash -n \"$f\"; done"
22
+ },
23
+ "keywords": [
24
+ "cli",
25
+ "automation",
26
+ "flutter",
27
+ "node",
28
+ "build",
29
+ "git"
30
+ ],
31
+ "author": "Paulo Michael",
32
+ "license": "MIT"
33
+ }
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ docker-compose up --build -d
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+ echo -e "\033[1;33mPruning unused Docker assets and volumes...\033[0m"
3
+ docker system prune -a --volumes -f
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+ # This file just delegates to xgem's native flutter engine (lib/flutter.sh)
3
+ # so there's a single source of truth instead of two copies that can drift.
4
+ exec xgem run flutter build-runner
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ # This file just delegates to xgem's native flutter engine (lib/flutter.sh),
3
+ # which includes the SwiftPM-aware iOS deployment-target reconciliation.
4
+ # It exists on disk for documentation/manual-run parity with other frameworks.
5
+ exec xgem run flutter build
@@ -0,0 +1,5 @@
1
+ #!/bin/bash
2
+ # This file just delegates to xgem's native flutter engine (lib/flutter.sh)
3
+ # so there's a single source of truth instead of two copies that can drift.
4
+ # It exists on disk for documentation/manual-run parity with other frameworks.
5
+ exec xgem run flutter hard-clean
@@ -0,0 +1,2 @@
1
+ #!/bin/bash
2
+ go build -o bin/main main.go
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+ echo -e "\033[1;33mCleaning Go module cache...\033[0m"
3
+ go clean -modcache