xgem-cli 2.0.0-alpha.10 → 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 CHANGED
@@ -13,7 +13,13 @@ your environment, and `xgem git` wraps a common stage → commit → rebase-pull
13
13
  Flutter's iOS builds go through a dedicated engine that detects whether your
14
14
  project uses CocoaPods or Swift Package Manager (SPM), figures out the actual
15
15
  deployment target your resolved SPM plugins require (instead of guessing),
16
- and reconciles it automatically — see [Why the iOS build engine exists](#why-the-ios-build-engine-exists).
16
+ and reconciles it automatically.
17
+
18
+ **Full documentation: [docs/](https://github.com/Code4Hacker/x-gem-cli/blob/xgem/docs/README.md)** — getting started, every command in
19
+ depth, exactly what each framework's project-creation wizard does, the iOS build
20
+ engine explained, Windows support, and troubleshooting. This README is a summary.
21
+ (Absolute link on purpose — `docs/` isn't shipped inside the npm package itself,
22
+ so a relative link would 404 on npmjs.com's rendered README.)
17
23
 
18
24
  ## Install
19
25
 
@@ -68,31 +74,13 @@ flutter, node, python, react, vue, angular, next, go, rust, docker, swift
68
74
  ## Creating a new project
69
75
 
70
76
  `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.
77
+ what's already in the directory — real `create-vite`/`create-vue`/`ng new`/
78
+ `create-next-app`/`flutter create` scaffolding, with framework-specific
79
+ questions and a working dev-server/device launch at the end. Package manager
80
+ (npm/yarn/pnpm/bun) is auto-detected everywhere, never assumed. Answering
81
+ "existing" (the default) skips all of this and just scaffolds
82
+ `.xgem-automate/` into the current directory. Full detail, including exactly
83
+ what runs for each framework: **[docs/frameworks.md](https://github.com/Code4Hacker/x-gem-cli/blob/xgem/docs/frameworks.md)**.
96
84
 
97
85
  ## Architecture
98
86
 
@@ -118,47 +106,25 @@ of logic that can drift out of sync with two copies.
118
106
  ## Why the iOS build engine exists
119
107
 
120
108
  Flutter auto-generates a wrapper Swift package, `FlutterGeneratedPluginSwiftPackage`,
121
- that aggregates your plugins' SPM dependencies. Its declared minimum iOS platform can
122
- desync from your app's own `IPHONEOS_DEPLOYMENT_TARGET` — a confirmed, currently-open
123
- upstream Flutter bug ([flutter/flutter#186804](https://github.com/flutter/flutter/issues/186804),
109
+ whose declared minimum iOS platform can desync from your app's own
110
+ `IPHONEOS_DEPLOYMENT_TARGET` independently, even when your app's own target is
111
+ already high enough. This is a confirmed, currently-open upstream Flutter bug
112
+ ([flutter/flutter#186804](https://github.com/flutter/flutter/issues/186804),
124
113
  [#189422](https://github.com/flutter/flutter/issues/189422),
125
- [#162072](https://github.com/flutter/flutter/issues/162072)). A single `sed` patch to
126
- `project.pbxproj` doesn't reliably fix this: there are multiple deployment-target entries
127
- across Debug/Release/Profile × Runner/RunnerTests, and even a fully correct patch doesn't
128
- force Flutter to regenerate the generated package.
129
-
130
- `xgem run flutter build` (for the iOS target) instead:
131
-
132
- 1. Detects whether the project uses CocoaPods, SPM, or both.
133
- 2. Computes the deployment target actually required by your *resolved* SPM plugins
134
- (by reading their `ios/Package.swift` manifests via `.dart_tool/package_config.json`),
135
- instead of assuming a fixed value.
136
- 3. Shows a plan and asks for confirmation (unless `--yes`/`--dry-run`) before patching
137
- *every* `IPHONEOS_DEPLOYMENT_TARGET` occurrence in `project.pbxproj`.
138
- 4. Forces a clean regeneration (`flutter pub get` after clearing `ios/Flutter/ephemeral`)
139
- and verifies the regenerated package matches.
140
- 5. If it still doesn't (the upstream bug above), patches the generated package directly
141
- as a documented, loudly-logged last resort — never silently.
142
-
143
- Run `xgem doctor ios` any time for a standalone readiness report without doing a build.
114
+ [#162072](https://github.com/flutter/flutter/issues/162072)). `xgem run flutter build`
115
+ (iOS target) detects the actual requirement from your resolved SPM plugins, patches
116
+ every deployment-target occurrence, forces a clean regeneration, verifies it, and
117
+ falls back to a documented direct patch if the upstream bug is still biting — full
118
+ detail: **[docs/ios-build-engine.md](https://github.com/Code4Hacker/x-gem-cli/blob/xgem/docs/ios-build-engine.md)**. Run `xgem doctor ios`
119
+ any time for a standalone readiness report without doing a build.
144
120
 
145
121
  ## Windows support
146
122
 
147
- Homebrew and the curl installer are macOS/Linux only, same as any bash tool that's
148
- not going to change. npm is different: it's published with a small native Windows
149
- engine (`lib-win/`, driven by `bin/xgem.js`) so `npm install -g xgem-cli` actually works
150
- in `cmd.exe`/PowerShell, no WSL required.
151
-
152
- What works on Windows: `init`/`add`/`run`/`terminate`, `doctor`, the git workflow
153
- commands, and Flutter builds for **APK, App Bundle, and Windows desktop** targets. What doesn't:
154
- Flutter **iOS/macOS** builds — Xcode has no Windows equivalent, so `xgem doctor ios`
155
- explains that plainly instead of pretending. `swift` isn't offered as a framework
156
- choice on Windows for the same reason. If you're inside WSL, none of this applies —
157
- WSL reports itself as Linux, so you get the full bash engine automatically.
158
-
159
- On macOS/Linux, npm installs run the exact same bash engine as Homebrew/curl (`bin/xgem.js`
160
- is a thin passthrough that execs the real `bin/xgem` script) — there's only one
161
- implementation to trust on POSIX; Windows is the only platform with a second one.
123
+ Homebrew and the curl installer are macOS/Linux only, same as any bash tool. npm is
124
+ different: it ships a small native Windows engine (`lib-win/`) so `npm install -g
125
+ xgem-cli` actually works in `cmd.exe`/PowerShell, no WSL required everything except
126
+ Flutter iOS/macOS builds (Xcode has no Windows equivalent) and the project-creation
127
+ wizards (not ported yet). Full detail: **[docs/windows.md](https://github.com/Code4Hacker/x-gem-cli/blob/xgem/docs/windows.md)**.
162
128
 
163
129
  ## Roadmap
164
130
 
package/bin/xgem CHANGED
@@ -91,26 +91,18 @@ case "${1:-}" in
91
91
  PS3="Enter target selection number: "
92
92
  select fw in "${ALL_FRAMEWORKS[@]}"; do
93
93
  if [ -n "$fw" ]; then
94
+ # create_project_wizard handles ALL of xgem's own
95
+ # bookkeeping (.xgem-automate + .gitignore) internally now,
96
+ # at the earliest safe point for each framework — never
97
+ # after a slow/interruptible step, so a Ctrl-C during (say)
98
+ # `ng new`'s install can no longer lose it. See lib/create.sh.
94
99
  create_project_wizard "$fw"
95
- mkdir -p "$CONFIG_DIR"
96
- scaffold_inject_templates "$fw" "$CONFIG_DIR"
97
- log_success "Successfully appended standard scripts for: $CONFIG_DIR/$fw"
98
100
  break
99
101
  else
100
102
  echo "Invalid selection."
101
103
  fi
102
104
  done
103
105
 
104
- if [ -f .gitignore ]; then
105
- if ! grep -q "$CONFIG_DIR/" .gitignore; then
106
- echo -e "\n$CONFIG_DIR/" >> .gitignore
107
- log_success "Added automation tracking to .gitignore"
108
- fi
109
- else
110
- echo "$CONFIG_DIR/" > .gitignore
111
- log_success "Created .gitignore and hidden tracking layer folder references."
112
- fi
113
-
114
106
  _ask_launch_dev_server
115
107
 
116
108
  # A child process (this script) can never change the parent
package/bin/xgem.js CHANGED
@@ -61,8 +61,23 @@ async function selectFramework(options) {
61
61
  return options[idx];
62
62
  }
63
63
 
64
- function updateGitignore() {
64
+ // Some teams want the generated scripts checked in so collaborators get the
65
+ // same automation; others want them private/local-only. Ask instead of
66
+ // always gitignoring.
67
+ async function updateGitignore() {
65
68
  const gitignorePath = '.gitignore';
69
+ const ignoreChoice = ((await prompt(`Should ${CONFIG_DIR}/ be ignored by git (private to you), or tracked so collaborators get the same scripts? [ignore/track]`, 'ignore'))).toLowerCase();
70
+
71
+ if (ignoreChoice.startsWith('t')) {
72
+ if (fs.existsSync(gitignorePath)) {
73
+ const lines = fs.readFileSync(gitignorePath, 'utf8').split(/\r?\n/).filter((l) => l !== `${CONFIG_DIR}/`);
74
+ fs.writeFileSync(gitignorePath, lines.join('\n'));
75
+ logInfo(`Removed existing ${CONFIG_DIR}/ entry from .gitignore since you chose to track it.`);
76
+ }
77
+ logSuccess(`${CONFIG_DIR}/ will be tracked in git.`);
78
+ return;
79
+ }
80
+
66
81
  if (fs.existsSync(gitignorePath)) {
67
82
  const content = fs.readFileSync(gitignorePath, 'utf8');
68
83
  if (!content.includes(`${CONFIG_DIR}/`)) {
@@ -89,7 +104,7 @@ async function cmdInit() {
89
104
  scaffold.injectTemplates(fw, CONFIG_DIR);
90
105
  logSuccess(`Successfully appended standard scripts for: ${CONFIG_DIR}/${fw}`);
91
106
 
92
- updateGitignore();
107
+ await updateGitignore();
93
108
  }
94
109
 
95
110
  async function cmdAdd() {
package/lib/create.sh CHANGED
@@ -1,29 +1,76 @@
1
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.
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
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.
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`.
12
11
  #
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.
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.
17
29
  XGEM_POST_INIT_LAUNCH_CMD=()
18
30
 
19
31
  # Set by creators when a NEW folder was created (not "current directory").
20
32
  # A child process can never change its parent shell's cwd, so once `xgem
21
33
  # init` exits, the user's actual terminal is back wherever it started —
22
34
  # xgem's own process is correctly cd'd into the new folder for the rest of
23
- # ITS run (template injection, dev server), but that never propagates back.
24
- # bin/xgem prints a "cd <name>" reminder using this at the very end.
35
+ # ITS run, but that never propagates back. bin/xgem prints a "cd <name>"
36
+ # reminder using this at the very end.
25
37
  XGEM_CREATED_NEW_FOLDER=""
26
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
+
27
74
  # _prompt_project_location <human label> -> echoes "." or a new folder name.
28
75
  # Pure prompt-and-echo, no other stdout output, so it's safe to capture via
29
76
  # command substitution.
@@ -42,6 +89,15 @@ _prompt_project_location() {
42
89
  echo "$name"
43
90
  }
44
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
+
45
101
  _open_url() {
46
102
  local url=$1
47
103
  case "$(detect_os)" in
@@ -89,13 +145,11 @@ _launch_dev_server_and_open_browser() {
89
145
  _ask_launch_dev_server() {
90
146
  [ ${#XGEM_POST_INIT_LAUNCH_CMD[@]} -gt 0 ] || return 0
91
147
 
92
- # `flutter run` isn't a web dev server — it has no localhost URL to
93
- # detect/open, and it depends on a fully-inherited interactive stdin
94
- # for its own hot-reload keybindings (r/R/q/etc.), which the
95
- # background+log-tail approach below would break. Run it directly.
96
- # (Whether to run at all was already confirmed in
97
- # _flutter_offer_run_on_device before this command was ever set, so
98
- # this doesn't ask again.)
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.)
99
153
  if [ "${XGEM_POST_INIT_LAUNCH_CMD[0]}" = "flutter" ]; then
100
154
  "${XGEM_POST_INIT_LAUNCH_CMD[@]}"
101
155
  return 0
@@ -114,27 +168,33 @@ _create_react() {
114
168
  read -r -p "TypeScript or JavaScript? [ts/js] (default: ts): " lang
115
169
  lang=${lang:-ts}
116
170
  project_dir=$(_prompt_project_location "React")
117
- [ "$project_dir" != "." ] && XGEM_CREATED_NEW_FOLDER="$project_dir"
171
+ _enter_project_dir "$project_dir"
118
172
 
119
173
  if [[ "$variant" == cra* || "$variant" == CRA* ]]; then
120
174
  require_cmd npx "Install Node.js (npm ships with it): https://nodejs.org"
121
- local -a args=(create-react-app)
122
- [ "$project_dir" != "." ] && args+=("$project_dir") || args+=(.)
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 .)
123
182
  [ "$lang" = "ts" ] && args+=(--template typescript)
124
183
  npx "${args[@]}" || die "create-react-app failed."
125
- else
126
- require_cmd npm "Install Node.js (npm ships with it): https://nodejs.org"
127
- local template="react"
128
- [ "$lang" = "ts" ] && template="react-ts"
129
- # --no-immediate: create-vite's own "install deps and start dev
130
- # server" prompt would otherwise block here and, if Ctrl-C'd, kill
131
- # this whole xgem process before it ever reaches template
132
- # injection / .gitignore setup below. xgem handles install/launch
133
- # itself instead, after everything else is done.
134
- npm create vite@latest "$project_dir" -- --template "$template" --no-immediate || die "npm create vite failed."
184
+ _xgem_bookkeeping react
185
+ return 0
135
186
  fi
136
187
 
137
- [ "$project_dir" != "." ] && { cd "$project_dir" || die "Could not enter $project_dir"; }
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
138
198
  log_info "Installing dependencies..."
139
199
  npm install
140
200
  XGEM_POST_INIT_LAUNCH_CMD=(npm run dev)
@@ -144,9 +204,13 @@ _create_vue() {
144
204
  require_cmd npm "Install Node.js (npm ships with it): https://nodejs.org"
145
205
  local project_dir
146
206
  project_dir=$(_prompt_project_location "Vue")
147
- [ "$project_dir" != "." ] && XGEM_CREATED_NEW_FOLDER="$project_dir"
148
- npm create vue@latest "$project_dir" || die "npm create vue failed."
149
- [ "$project_dir" != "." ] && { cd "$project_dir" || die "Could not enter $project_dir"; }
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
150
214
  log_info "Installing dependencies..."
151
215
  npm install
152
216
  XGEM_POST_INIT_LAUNCH_CMD=(npm run dev)
@@ -155,16 +219,20 @@ _create_vue() {
155
219
  _create_angular() {
156
220
  local project_dir
157
221
  project_dir=$(_prompt_project_location "Angular")
158
- [ "$project_dir" != "." ] && XGEM_CREATED_NEW_FOLDER="$project_dir"
222
+ _enter_project_dir "$project_dir"
159
223
  local -a ng_cmd=(ng)
160
224
  has_cmd ng || ng_cmd=(npx @angular/cli@latest)
161
225
 
162
- if [ "$project_dir" = "." ]; then
163
- "${ng_cmd[@]}" new "$(basename "$PWD")" --directory=. || die "ng new failed."
164
- else
165
- "${ng_cmd[@]}" new "$project_dir" || die "ng new failed."
166
- cd "$project_dir" || die "Could not enter $project_dir"
167
- fi
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
168
236
  XGEM_POST_INIT_LAUNCH_CMD=(npm run start)
169
237
  }
170
238
 
@@ -172,20 +240,26 @@ _create_next() {
172
240
  require_cmd npx "Install Node.js (npm ships with it): https://nodejs.org"
173
241
  local project_dir
174
242
  project_dir=$(_prompt_project_location "Next.js")
175
- [ "$project_dir" != "." ] && XGEM_CREATED_NEW_FOLDER="$project_dir"
176
- npx create-next-app@latest "$project_dir" || die "create-next-app failed."
177
- [ "$project_dir" != "." ] && { cd "$project_dir" || die "Could not enter $project_dir"; }
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
178
250
  XGEM_POST_INIT_LAUNCH_CMD=(npm run dev)
179
251
  }
180
252
 
181
253
  # Lighter path for frameworks without a dev-server/browser story — just the
182
- # ecosystem's own standard init command, no sub-wizard.
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.
183
257
  _create_simple() {
184
258
  local fw=$1
185
259
  local project_dir
186
260
  project_dir=$(_prompt_project_location "$fw")
187
- [ "$project_dir" != "." ] && XGEM_CREATED_NEW_FOLDER="$project_dir"
188
- [ "$project_dir" != "." ] && { mkdir -p "$project_dir" && cd "$project_dir" || die "Could not enter $project_dir"; }
261
+ _enter_project_dir "$project_dir"
262
+ _xgem_bookkeeping "$fw"
189
263
 
190
264
  case "$fw" in
191
265
  node)
@@ -206,13 +280,7 @@ _create_simple() {
206
280
  ;;
207
281
  rust)
208
282
  require_cmd cargo "Install Rust: https://www.rust-lang.org/tools/install"
209
- if [ "$project_dir" != "." ]; then
210
- cd .. || return 1
211
- cargo new "$project_dir" || die "cargo new failed."
212
- cd "$project_dir" || die "Could not enter $project_dir"
213
- else
214
- cargo init || die "cargo init failed."
215
- fi
283
+ cargo init || die "cargo init failed."
216
284
  ;;
217
285
  swift)
218
286
  require_cmd swift "Install Swift: https://www.swift.org/install/"
@@ -235,15 +303,21 @@ EOF
235
303
  }
236
304
 
237
305
  # create_project_wizard <framework>
238
- # Asks whether to create a new project or use what's already here; if
239
- # creating, dispatches to the right per-framework creator. Leaves the
240
- # process cwd inside the (possibly new) project directory either way.
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.
241
311
  create_project_wizard() {
242
312
  local fw=$1
243
313
  local create_choice
244
314
  read -r -p "Create a brand-new $fw project, or use what's already in this directory? [new/existing] (default: existing): " create_choice
245
315
  create_choice=${create_choice:-existing}
246
- [[ "$create_choice" == n* || "$create_choice" == N* ]] || return 0
316
+
317
+ if [[ "$create_choice" != n* && "$create_choice" != N* ]]; then
318
+ _xgem_bookkeeping "$fw"
319
+ return 0
320
+ fi
247
321
 
248
322
  case "$fw" in
249
323
  react) _create_react ;;
@@ -252,6 +326,9 @@ create_project_wizard() {
252
326
  next) _create_next ;;
253
327
  flutter) create_flutter_project ;;
254
328
  node|python|go|rust|docker|swift) _create_simple "$fw" ;;
255
- *) log_debug "No creation wizard for '$fw'; using current directory as-is." ;;
329
+ *)
330
+ log_debug "No creation wizard for '$fw'; using current directory as-is."
331
+ _xgem_bookkeeping "$fw"
332
+ ;;
256
333
  esac
257
334
  }
package/lib/flutter.sh CHANGED
@@ -239,18 +239,20 @@ flutter_run_native_command() {
239
239
  }
240
240
 
241
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.
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.
243
246
  create_flutter_project() {
244
247
  require_cmd flutter "Install Flutter: https://docs.flutter.dev/get-started/install"
245
248
  local project_dir
246
249
  project_dir=$(_prompt_project_location "Flutter")
247
- [ "$project_dir" != "." ] && XGEM_CREATED_NEW_FOLDER="$project_dir"
248
- if [ "$project_dir" = "." ]; then
249
- flutter create . || die "flutter create failed."
250
- else
251
- flutter create "$project_dir" || die "flutter create failed."
252
- cd "$project_dir" || die "Could not enter $project_dir"
253
- fi
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
254
256
  _flutter_offer_run_on_device
255
257
  }
256
258
 
package/lib/git.sh CHANGED
@@ -102,31 +102,67 @@ cmd_git_cmt() {
102
102
  fi
103
103
  }
104
104
 
105
- cmd_git_init() {
106
- log_info "Initializing local Git repository..."
107
- git init
105
+ _cmd_git_init_manual_remote() {
106
+ local remote_name=$1
107
+ local remote_url user_remote_name
108
+ read -r -p "Enter remote repository URL (or leave blank to skip): " remote_url
109
+ [ -n "$remote_url" ] || return 0
108
110
 
109
- local remote_name="origin" remote_url user_remote_name branch_name
110
- read -r -p "Enter remote repository URL: " remote_url
111
- if [ -n "$remote_url" ]; then
112
- read -r -p "Enter remote name (default: origin): " user_remote_name
113
- [ -n "$user_remote_name" ] && remote_name="$user_remote_name"
111
+ read -r -p "Enter remote name (default: $remote_name): " user_remote_name
112
+ [ -n "$user_remote_name" ] && remote_name="$user_remote_name"
114
113
 
115
- if git remote | grep -q "^$remote_name$"; then
116
- git remote set-url "$remote_name" "$remote_url"
117
- log_success "Remote '$remote_name' already existed. URL updated."
118
- else
119
- git remote add "$remote_name" "$remote_url"
120
- log_success "Remote '$remote_name' successfully added."
121
- fi
114
+ if git remote | grep -q "^$remote_name$"; then
115
+ git remote set-url "$remote_name" "$remote_url"
116
+ log_success "Remote '$remote_name' already existed. URL updated."
117
+ else
118
+ git remote add "$remote_name" "$remote_url"
119
+ log_success "Remote '$remote_name' successfully added."
122
120
  fi
121
+ }
123
122
 
123
+ cmd_git_init() {
124
+ log_info "Initializing local Git repository..."
125
+ git init
126
+
127
+ local branch_name
124
128
  read -r -p "Enter branch name (default: main): " branch_name
125
129
  branch_name=${branch_name:-main}
126
130
  git branch -M "$branch_name"
127
131
 
128
132
  git add .
129
133
  git commit -m "initial changes" 2>/dev/null
134
+
135
+ # Real GitHub repo creation, not just wiring a remote to a URL you
136
+ # already had to go create by hand — this is the whole point of
137
+ # `xgem git init` over plain `git init`. Falls back to the manual-URL
138
+ # flow if `gh` isn't installed/authenticated.
139
+ if has_cmd gh && gh auth status >/dev/null 2>&1; then
140
+ local create_choice
141
+ read -r -p "Create a new GitHub repository for this project right now? (Y/n): " create_choice
142
+ create_choice=${create_choice:-y}
143
+ if [[ "$create_choice" == "y" || "$create_choice" == "Y" ]]; then
144
+ local repo_name visibility vis_flag
145
+ read -r -p "Repository name (default: $(basename "$PWD")): " repo_name
146
+ repo_name=${repo_name:-$(basename "$PWD")}
147
+ read -r -p "Public or private? [public/private] (default: private): " visibility
148
+ visibility=${visibility:-private}
149
+ vis_flag="--private"
150
+ [[ "$visibility" == pub* || "$visibility" == Pub* ]] && vis_flag="--public"
151
+
152
+ if gh repo create "$repo_name" "$vis_flag" --source=. --remote=origin --push; then
153
+ log_success "Created GitHub repo '$repo_name' and pushed '$branch_name' to it."
154
+ else
155
+ log_error "gh repo create failed — falling back to manual remote setup."
156
+ _cmd_git_init_manual_remote origin
157
+ fi
158
+ log_success "Local baseline configuration setup completed."
159
+ return 0
160
+ fi
161
+ elif has_cmd gh; then
162
+ log_warn "GitHub CLI (gh) is installed but not authenticated — run 'gh auth login' to enable one-step repo creation next time."
163
+ fi
164
+
165
+ _cmd_git_init_manual_remote origin
130
166
  log_success "Local baseline configuration setup completed."
131
167
  }
132
168
 
package/lib/scaffold.sh CHANGED
@@ -13,12 +13,31 @@
13
13
 
14
14
  ALL_FRAMEWORKS=(flutter node python react vue angular next go rust docker swift)
15
15
 
16
+ # _package_json_has_script <script> — checks the CURRENT directory's
17
+ # package.json, not a guess. Used to only offer lint/test automation when
18
+ # the project actually has those scripts, instead of always generating
19
+ # scripts that fail with "Missing script" on projects that don't.
20
+ _package_json_has_script() {
21
+ [ -f package.json ] || return 1
22
+ grep -qE "\"$1\"[[:space:]]*:" package.json 2>/dev/null
23
+ }
24
+
16
25
  framework_scripts() {
17
26
  case "$1" in
18
27
  flutter) echo "hard-clean build build-runner" ;;
19
- node) echo "hard-clean build start" ;;
28
+ node)
29
+ local scripts="hard-clean build start"
30
+ _package_json_has_script lint && scripts="$scripts lint"
31
+ _package_json_has_script test && scripts="$scripts test"
32
+ echo "$scripts"
33
+ ;;
20
34
  python) echo "hard-clean install" ;;
21
- react|vue|angular|next) echo "hard-clean build dev" ;;
35
+ react|vue|angular|next)
36
+ local scripts="hard-clean build dev"
37
+ _package_json_has_script lint && scripts="$scripts lint"
38
+ _package_json_has_script test && scripts="$scripts test"
39
+ echo "$scripts"
40
+ ;;
22
41
  go) echo "hard-clean build" ;;
23
42
  rust) echo "hard-clean build" ;;
24
43
  docker) echo "hard-clean build-up" ;;
package/lib/version.sh CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
  # xgem version info.
3
3
 
4
- XGEM_VERSION="2.0.0-alpha.10"
4
+ XGEM_VERSION="2.0.0-alpha.12"
5
5
 
6
6
  print_version() {
7
7
  echo "xgem $XGEM_VERSION"
package/lib-win/git.js CHANGED
@@ -3,14 +3,23 @@
3
3
  // commit-exit-code check and origin-preference fix already in lib/git.sh
4
4
  // carry over here too.
5
5
 
6
+ const path = require('node:path');
6
7
  const { spawnSync } = require('node:child_process');
7
8
  const { logInfo, logSuccess, logWarn, logError, die } = require('./logger');
8
- const { prompt } = require('./utils');
9
+ const { prompt, hasCmd } = require('./utils');
9
10
 
10
11
  function git(args, opts = {}) {
11
12
  return spawnSync('git', args, { stdio: 'inherit', ...opts });
12
13
  }
13
14
 
15
+ function gh(args, opts = {}) {
16
+ return spawnSync('gh', args, { stdio: 'inherit', ...opts });
17
+ }
18
+
19
+ function ghAuthenticated() {
20
+ return spawnSync('gh', ['auth', 'status'], { stdio: 'ignore' }).status === 0;
21
+ }
22
+
14
23
  function gitCapture(args) {
15
24
  const result = spawnSync('git', args, { encoding: 'utf8' });
16
25
  return (result.stdout || '').trim();
@@ -90,7 +99,6 @@ async function cmdCmt(commitMsg) {
90
99
  const pullResult = git(['pull', '--rebase', remoteName, currentBranch]);
91
100
  if (pullResult.status !== 0) {
92
101
  const gitDir = gitCapture(['rev-parse', '--git-dir']);
93
- const path = require('node:path');
94
102
  const fs = require('node:fs');
95
103
  const inConflict = fs.existsSync(path.join(gitDir, 'rebase-merge')) || fs.existsSync(path.join(gitDir, 'rebase-apply'));
96
104
  if (inConflict) {
@@ -112,30 +120,60 @@ async function cmdCmt(commitMsg) {
112
120
  }
113
121
  }
114
122
 
115
- async function cmdInit() {
116
- logInfo('Initializing local Git repository...');
117
- git(['init']);
123
+ async function manualRemoteSetup(defaultRemoteName) {
124
+ let remoteName = defaultRemoteName;
125
+ const remoteUrl = await prompt('Enter remote repository URL (or leave blank to skip)');
126
+ if (!remoteUrl) return;
118
127
 
119
- let remoteName = 'origin';
120
- const remoteUrl = await prompt('Enter remote repository URL');
121
- if (remoteUrl) {
122
- const userRemoteName = await prompt('Enter remote name (default: origin)');
123
- if (userRemoteName) remoteName = userRemoteName;
128
+ const userRemoteName = await prompt(`Enter remote name (default: ${remoteName})`);
129
+ if (userRemoteName) remoteName = userRemoteName;
124
130
 
125
- if (remoteExists(remoteName)) {
126
- git(['remote', 'set-url', remoteName, remoteUrl]);
127
- logSuccess(`Remote '${remoteName}' already existed. URL updated.`);
128
- } else {
129
- git(['remote', 'add', remoteName, remoteUrl]);
130
- logSuccess(`Remote '${remoteName}' successfully added.`);
131
- }
131
+ if (remoteExists(remoteName)) {
132
+ git(['remote', 'set-url', remoteName, remoteUrl]);
133
+ logSuccess(`Remote '${remoteName}' already existed. URL updated.`);
134
+ } else {
135
+ git(['remote', 'add', remoteName, remoteUrl]);
136
+ logSuccess(`Remote '${remoteName}' successfully added.`);
132
137
  }
138
+ }
139
+
140
+ async function cmdInit() {
141
+ logInfo('Initializing local Git repository...');
142
+ git(['init']);
133
143
 
134
144
  const branchName = (await prompt('Enter branch name (default: main)')) || 'main';
135
145
  git(['branch', '-M', branchName]);
136
146
 
137
147
  git(['add', '.']);
138
148
  git(['commit', '-m', 'initial changes']);
149
+
150
+ // Real GitHub repo creation, not just wiring a remote to a URL you
151
+ // already had to go create by hand — the whole point of `xgem git
152
+ // init` over plain `git init`. Falls back to the manual-URL flow if
153
+ // `gh` isn't installed/authenticated.
154
+ if (hasCmd('gh') && ghAuthenticated()) {
155
+ const createChoice = (await prompt('Create a new GitHub repository for this project right now? (Y/n)')) || 'y';
156
+ if (createChoice.toLowerCase() === 'y') {
157
+ const cwdName = path.basename(process.cwd());
158
+ const repoName = (await prompt(`Repository name (default: ${cwdName})`)) || cwdName;
159
+ const visibility = ((await prompt('Public or private? [public/private] (default: private)')) || 'private').toLowerCase();
160
+ const visFlag = visibility.startsWith('pub') ? '--public' : '--private';
161
+
162
+ const result = gh(['repo', 'create', repoName, visFlag, '--source=.', '--remote=origin', '--push']);
163
+ if (result.status === 0) {
164
+ logSuccess(`Created GitHub repo '${repoName}' and pushed '${branchName}' to it.`);
165
+ } else {
166
+ logError('gh repo create failed — falling back to manual remote setup.');
167
+ await manualRemoteSetup('origin');
168
+ }
169
+ logSuccess('Local baseline configuration setup completed.');
170
+ return;
171
+ }
172
+ } else if (hasCmd('gh')) {
173
+ logWarn("GitHub CLI (gh) is installed but not authenticated — run 'gh auth login' to enable one-step repo creation next time.");
174
+ }
175
+
176
+ await manualRemoteSetup('origin');
139
177
  logSuccess('Local baseline configuration setup completed.');
140
178
  }
141
179
 
@@ -22,8 +22,30 @@ const FRAMEWORK_SCRIPTS = {
22
22
  docker: ['hard-clean', 'build-up'],
23
23
  };
24
24
 
25
+ const WEB_FRAMEWORKS = ['node', 'react', 'vue', 'angular', 'next'];
26
+
27
+ // Only offer lint/test automation when the current directory's
28
+ // package.json actually declares those scripts, instead of always
29
+ // generating scripts that fail with "Missing script" on projects that
30
+ // don't have them.
31
+ function packageJsonHasScript(scriptName) {
32
+ try {
33
+ const pkg = JSON.parse(require('node:fs').readFileSync('package.json', 'utf8'));
34
+ return Boolean(pkg.scripts && pkg.scripts[scriptName]);
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
25
40
  function frameworkScripts(fw) {
26
- return FRAMEWORK_SCRIPTS[fw] || null;
41
+ const base = FRAMEWORK_SCRIPTS[fw];
42
+ if (!base) return null;
43
+ if (!WEB_FRAMEWORKS.includes(fw)) return base;
44
+
45
+ const scripts = [...base];
46
+ if (packageJsonHasScript('lint')) scripts.push('lint');
47
+ if (packageJsonHasScript('test')) scripts.push('test');
48
+ return scripts;
27
49
  }
28
50
 
29
51
  function templateDir(fw) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xgem-cli",
3
- "version": "2.0.0-alpha.10",
3
+ "version": "2.0.0-alpha.12",
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"
@@ -1,3 +1,11 @@
1
1
  #!/bin/bash
2
2
  echo -e "\033[1;34mBuilding Node project...\033[0m"
3
- npm run build
3
+ if [ -f pnpm-lock.yaml ]; then
4
+ pnpm run build
5
+ elif [ -f yarn.lock ]; then
6
+ yarn build
7
+ elif [ -f bun.lockb ] || [ -f bun.lock ]; then
8
+ bun run build
9
+ else
10
+ npm run build
11
+ fi
@@ -1,3 +1,35 @@
1
1
  #!/bin/bash
2
- echo -e "\033[1;33mNuking node_modules and resetting package locks...\033[0m"
3
- rm -rf node_modules package-lock.json yarn.lock && npm install
2
+ # Detect the package manager actually in use instead of assuming npm —
3
+ # blindly deleting yarn.lock/pnpm-lock.yaml on a project that uses them
4
+ # and running `npm install` instead corrupts the project by introducing a
5
+ # conflicting lockfile.
6
+ PKG_MANAGER=npm
7
+ if [ -f pnpm-lock.yaml ]; then
8
+ PKG_MANAGER=pnpm
9
+ elif [ -f yarn.lock ]; then
10
+ PKG_MANAGER=yarn
11
+ elif [ -f bun.lockb ] || [ -f bun.lock ]; then
12
+ PKG_MANAGER=bun
13
+ elif [ -f package.json ]; then
14
+ declared=$(grep -o '"packageManager"[[:space:]]*:[[:space:]]*"[a-z]*' package.json 2>/dev/null | grep -oE '[a-z]+$')
15
+ case "$declared" in npm|yarn|pnpm|bun) PKG_MANAGER=$declared ;; esac
16
+ fi
17
+
18
+ if [ -f .nvmrc ] && command -v node >/dev/null 2>&1; then
19
+ wanted=$(tr -d 'v[:space:]' < .nvmrc)
20
+ have=$(node -v | tr -d 'v')
21
+ case "$have" in
22
+ "$wanted"*) ;;
23
+ *) echo -e "\033[33mWarning: .nvmrc wants Node $wanted, but 'node -v' reports $have.\033[0m" ;;
24
+ esac
25
+ fi
26
+
27
+ echo -e "\033[1;33mNuking node_modules and resetting package locks (package manager: $PKG_MANAGER)...\033[0m"
28
+ rm -rf node_modules
29
+
30
+ case "$PKG_MANAGER" in
31
+ yarn) rm -f yarn.lock; yarn install ;;
32
+ pnpm) rm -f pnpm-lock.yaml; pnpm install ;;
33
+ bun) rm -f bun.lockb bun.lock; bun install ;;
34
+ *) rm -f package-lock.json; npm install ;;
35
+ esac
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+ if [ -f pnpm-lock.yaml ]; then
3
+ pnpm run lint
4
+ elif [ -f yarn.lock ]; then
5
+ yarn lint
6
+ elif [ -f bun.lockb ] || [ -f bun.lock ]; then
7
+ bun run lint
8
+ else
9
+ npm run lint
10
+ fi
@@ -1,2 +1,10 @@
1
1
  #!/bin/bash
2
- npm run start
2
+ if [ -f pnpm-lock.yaml ]; then
3
+ pnpm run start
4
+ elif [ -f yarn.lock ]; then
5
+ yarn start
6
+ elif [ -f bun.lockb ] || [ -f bun.lock ]; then
7
+ bun run start
8
+ else
9
+ npm run start
10
+ fi
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+ if [ -f pnpm-lock.yaml ]; then
3
+ pnpm run test
4
+ elif [ -f yarn.lock ]; then
5
+ yarn test
6
+ elif [ -f bun.lockb ] || [ -f bun.lock ]; then
7
+ bun run test
8
+ else
9
+ npm run test
10
+ fi
@@ -1,2 +1,11 @@
1
1
  #!/bin/bash
2
- npm run build
2
+ # Use whatever package manager this project actually uses.
3
+ if [ -f pnpm-lock.yaml ]; then
4
+ pnpm run build
5
+ elif [ -f yarn.lock ]; then
6
+ yarn build
7
+ elif [ -f bun.lockb ] || [ -f bun.lock ]; then
8
+ bun run build
9
+ else
10
+ npm run build
11
+ fi
@@ -1,2 +1,10 @@
1
1
  #!/bin/bash
2
- npm run dev
2
+ if [ -f pnpm-lock.yaml ]; then
3
+ pnpm run dev
4
+ elif [ -f yarn.lock ]; then
5
+ yarn dev
6
+ elif [ -f bun.lockb ] || [ -f bun.lock ]; then
7
+ bun run dev
8
+ else
9
+ npm run dev
10
+ fi
@@ -1,3 +1,36 @@
1
1
  #!/bin/bash
2
- echo -e "\033[1;33mResetting __FRAMEWORK__ dependencies...\033[0m"
3
- rm -rf node_modules package-lock.json yarn.lock && npm install
2
+ # Detect the package manager actually in use instead of assuming npm —
3
+ # blindly deleting yarn.lock/pnpm-lock.yaml on a project that uses them
4
+ # and running `npm install` instead corrupts the project by introducing a
5
+ # conflicting lockfile.
6
+ PKG_MANAGER=npm
7
+ if [ -f pnpm-lock.yaml ]; then
8
+ PKG_MANAGER=pnpm
9
+ elif [ -f yarn.lock ]; then
10
+ PKG_MANAGER=yarn
11
+ elif [ -f bun.lockb ] || [ -f bun.lock ]; then
12
+ PKG_MANAGER=bun
13
+ elif [ -f package.json ]; then
14
+ declared=$(grep -o '"packageManager"[[:space:]]*:[[:space:]]*"[a-z]*' package.json 2>/dev/null | grep -oE '[a-z]+$')
15
+ case "$declared" in npm|yarn|pnpm|bun) PKG_MANAGER=$declared ;; esac
16
+ fi
17
+
18
+ # Warn (don't block) on a Node version mismatch against .nvmrc, if present.
19
+ if [ -f .nvmrc ] && command -v node >/dev/null 2>&1; then
20
+ wanted=$(tr -d 'v[:space:]' < .nvmrc)
21
+ have=$(node -v | tr -d 'v')
22
+ case "$have" in
23
+ "$wanted"*) ;;
24
+ *) echo -e "\033[33mWarning: .nvmrc wants Node $wanted, but 'node -v' reports $have.\033[0m" ;;
25
+ esac
26
+ fi
27
+
28
+ echo -e "\033[1;33mResetting __FRAMEWORK__ dependencies (package manager: $PKG_MANAGER)...\033[0m"
29
+ rm -rf node_modules
30
+
31
+ case "$PKG_MANAGER" in
32
+ yarn) rm -f yarn.lock; yarn install ;;
33
+ pnpm) rm -f pnpm-lock.yaml; pnpm install ;;
34
+ bun) rm -f bun.lockb bun.lock; bun install ;;
35
+ *) rm -f package-lock.json; npm install ;;
36
+ esac
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+ if [ -f pnpm-lock.yaml ]; then
3
+ pnpm run lint
4
+ elif [ -f yarn.lock ]; then
5
+ yarn lint
6
+ elif [ -f bun.lockb ] || [ -f bun.lock ]; then
7
+ bun run lint
8
+ else
9
+ npm run lint
10
+ fi
@@ -0,0 +1,10 @@
1
+ #!/bin/bash
2
+ if [ -f pnpm-lock.yaml ]; then
3
+ pnpm run test
4
+ elif [ -f yarn.lock ]; then
5
+ yarn test
6
+ elif [ -f bun.lockb ] || [ -f bun.lock ]; then
7
+ bun run test
8
+ else
9
+ npm run test
10
+ fi
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync } from 'node:child_process';
3
- console.log('Building Node project...');
4
- execSync('npm run build', { stdio: 'inherit' });
3
+ import { existsSync } from 'node:fs';
4
+
5
+ let cmd = 'npm run build';
6
+ if (existsSync('pnpm-lock.yaml')) cmd = 'pnpm run build';
7
+ else if (existsSync('yarn.lock')) cmd = 'yarn build';
8
+ else if (existsSync('bun.lockb') || existsSync('bun.lock')) cmd = 'bun run build';
9
+
10
+ execSync(cmd, { stdio: 'inherit' });
@@ -1,9 +1,28 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync } from 'node:child_process';
3
- import { rmSync } from 'node:fs';
3
+ import { existsSync, rmSync, readFileSync } from 'node:fs';
4
4
 
5
- console.log('Nuking node_modules and resetting package locks...');
5
+ function detectPkgManager() {
6
+ if (existsSync('pnpm-lock.yaml')) return 'pnpm';
7
+ if (existsSync('yarn.lock')) return 'yarn';
8
+ if (existsSync('bun.lockb') || existsSync('bun.lock')) return 'bun';
9
+ if (existsSync('package.json')) {
10
+ try {
11
+ const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
12
+ if (['npm', 'yarn', 'pnpm', 'bun'].includes(pkg.packageManager?.split('@')[0])) {
13
+ return pkg.packageManager.split('@')[0];
14
+ }
15
+ } catch { /* ignore malformed package.json */ }
16
+ }
17
+ return 'npm';
18
+ }
19
+
20
+ const pm = detectPkgManager();
21
+ console.log(`Nuking node_modules and resetting package locks (package manager: ${pm})...`);
6
22
  rmSync('node_modules', { recursive: true, force: true });
7
- rmSync('package-lock.json', { force: true });
8
- rmSync('yarn.lock', { force: true });
9
- execSync('npm install', { stdio: 'inherit' });
23
+
24
+ const lockfiles = { npm: ['package-lock.json'], yarn: ['yarn.lock'], pnpm: ['pnpm-lock.yaml'], bun: ['bun.lockb', 'bun.lock'] };
25
+ for (const f of lockfiles[pm]) rmSync(f, { force: true });
26
+
27
+ const installCmd = { npm: 'npm install', yarn: 'yarn', pnpm: 'pnpm install', bun: 'bun install' };
28
+ execSync(installCmd[pm], { stdio: 'inherit' });
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
4
+
5
+ let cmd = 'npm run lint';
6
+ if (existsSync('pnpm-lock.yaml')) cmd = 'pnpm run lint';
7
+ else if (existsSync('yarn.lock')) cmd = 'yarn lint';
8
+ else if (existsSync('bun.lockb') || existsSync('bun.lock')) cmd = 'bun run lint';
9
+
10
+ execSync(cmd, { stdio: 'inherit' });
@@ -1,3 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync } from 'node:child_process';
3
- execSync('npm run start', { stdio: 'inherit' });
3
+ import { existsSync } from 'node:fs';
4
+
5
+ let cmd = 'npm run start';
6
+ if (existsSync('pnpm-lock.yaml')) cmd = 'pnpm run start';
7
+ else if (existsSync('yarn.lock')) cmd = 'yarn start';
8
+ else if (existsSync('bun.lockb') || existsSync('bun.lock')) cmd = 'bun run start';
9
+
10
+ execSync(cmd, { stdio: 'inherit' });
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
4
+
5
+ let cmd = 'npm run test';
6
+ if (existsSync('pnpm-lock.yaml')) cmd = 'pnpm run test';
7
+ else if (existsSync('yarn.lock')) cmd = 'yarn test';
8
+ else if (existsSync('bun.lockb') || existsSync('bun.lock')) cmd = 'bun run test';
9
+
10
+ execSync(cmd, { stdio: 'inherit' });
@@ -1,3 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync } from 'node:child_process';
3
- execSync('npm run build', { stdio: 'inherit' });
3
+ import { existsSync } from 'node:fs';
4
+
5
+ let cmd = 'npm run build';
6
+ if (existsSync('pnpm-lock.yaml')) cmd = 'pnpm run build';
7
+ else if (existsSync('yarn.lock')) cmd = 'yarn build';
8
+ else if (existsSync('bun.lockb') || existsSync('bun.lock')) cmd = 'bun run build';
9
+
10
+ execSync(cmd, { stdio: 'inherit' });
@@ -1,3 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync } from 'node:child_process';
3
- execSync('npm run dev', { stdio: 'inherit' });
3
+ import { existsSync } from 'node:fs';
4
+
5
+ let cmd = 'npm run dev';
6
+ if (existsSync('pnpm-lock.yaml')) cmd = 'pnpm run dev';
7
+ else if (existsSync('yarn.lock')) cmd = 'yarn dev';
8
+ else if (existsSync('bun.lockb') || existsSync('bun.lock')) cmd = 'bun run dev';
9
+
10
+ execSync(cmd, { stdio: 'inherit' });
@@ -1,9 +1,32 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync } from 'node:child_process';
3
- import { rmSync } from 'node:fs';
3
+ import { existsSync, rmSync, readFileSync } from 'node:fs';
4
4
 
5
- console.log('Resetting __FRAMEWORK__ dependencies...');
5
+ // Detect the package manager actually in use instead of assuming npm —
6
+ // blindly deleting yarn.lock/pnpm-lock.yaml on a project that uses them
7
+ // and running `npm install` instead corrupts the project by introducing a
8
+ // conflicting lockfile.
9
+ function detectPkgManager() {
10
+ if (existsSync('pnpm-lock.yaml')) return 'pnpm';
11
+ if (existsSync('yarn.lock')) return 'yarn';
12
+ if (existsSync('bun.lockb') || existsSync('bun.lock')) return 'bun';
13
+ if (existsSync('package.json')) {
14
+ try {
15
+ const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
16
+ if (['npm', 'yarn', 'pnpm', 'bun'].includes(pkg.packageManager?.split('@')[0])) {
17
+ return pkg.packageManager.split('@')[0];
18
+ }
19
+ } catch { /* ignore malformed package.json */ }
20
+ }
21
+ return 'npm';
22
+ }
23
+
24
+ const pm = detectPkgManager();
25
+ console.log(`Resetting __FRAMEWORK__ dependencies (package manager: ${pm})...`);
6
26
  rmSync('node_modules', { recursive: true, force: true });
7
- rmSync('package-lock.json', { force: true });
8
- rmSync('yarn.lock', { force: true });
9
- execSync('npm install', { stdio: 'inherit' });
27
+
28
+ const lockfiles = { npm: ['package-lock.json'], yarn: ['yarn.lock'], pnpm: ['pnpm-lock.yaml'], bun: ['bun.lockb', 'bun.lock'] };
29
+ for (const f of lockfiles[pm]) rmSync(f, { force: true });
30
+
31
+ const installCmd = { npm: 'npm install', yarn: 'yarn', pnpm: 'pnpm install', bun: 'bun install' };
32
+ execSync(installCmd[pm], { stdio: 'inherit' });
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
4
+
5
+ let cmd = 'npm run lint';
6
+ if (existsSync('pnpm-lock.yaml')) cmd = 'pnpm run lint';
7
+ else if (existsSync('yarn.lock')) cmd = 'yarn lint';
8
+ else if (existsSync('bun.lockb') || existsSync('bun.lock')) cmd = 'bun run lint';
9
+
10
+ execSync(cmd, { stdio: 'inherit' });
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import { execSync } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
4
+
5
+ let cmd = 'npm run test';
6
+ if (existsSync('pnpm-lock.yaml')) cmd = 'pnpm run test';
7
+ else if (existsSync('yarn.lock')) cmd = 'yarn test';
8
+ else if (existsSync('bun.lockb') || existsSync('bun.lock')) cmd = 'bun run test';
9
+
10
+ execSync(cmd, { stdio: 'inherit' });