@gw-tools/gw 0.20.14 → 0.21.0-beta.2

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
@@ -234,7 +234,7 @@ gw sync feat-existing-branch .env
234
234
  - **Auto-copy files**: Configure once, automatically copy `.env`, secrets, and config files to every new worktree
235
235
  - **Hooks support**: Run commands before/after worktree creation (install dependencies, validate setup, etc.)
236
236
  - **Copy files between worktrees**: Easily copy secrets, environment files, and configurations from one worktree to another
237
- - **Automatic shell integration**: Shell function installs automatically on npm install for seamless `gw cd` navigation
237
+ - **Automatic shell integration**: Eval-based shell integration keeps `gw cd` navigation always up-to-date
238
238
  - **Auto-configured per repository**: Each repository gets its own local config file, automatically created on first use
239
239
  - **Dry-run mode**: Preview what would be copied without making changes
240
240
  - **Standalone binary**: Compiles to a single executable with no runtime dependencies
@@ -556,13 +556,13 @@ gw cd api
556
556
 
557
557
  #### How It Works
558
558
 
559
- The `cd` command integrates with your shell through an automatically installed function (see [install-shell](#install-shell)). When you run `gw cd <worktree>`:
559
+ The `cd` command integrates with your shell through an eval-based function (see [install-shell](#install-shell)). When you run `gw cd <worktree>`:
560
560
 
561
561
  1. The command finds the matching worktree path
562
562
  2. The shell function intercepts the call and navigates you there
563
563
  3. All other `gw` commands pass through normally
564
564
 
565
- **Note**: Shell integration is automatically installed when you install via npm. If needed, you can manually install or remove it using `gw install-shell`.
565
+ **Note**: Shell integration is set up automatically when you install via npm. You can also add it manually by adding `eval "$(gw install-shell)"` to your shell config.
566
566
 
567
567
  ### pr
568
568
 
@@ -722,9 +722,7 @@ If not configured, defaults to "main" branch and "merge" strategy.
722
722
 
723
723
  ### install-shell
724
724
 
725
- Install or remove shell integration for the `gw cd` command and enable real-time streaming output. This is automatically run during `npm install`, but can be run manually if needed.
726
-
727
- The installation creates an integration script in `~/.gw/shell/` and adds a single line to your shell configuration to source it, keeping your shell config clean and minimal.
725
+ Output shell integration code for the `gw cd` command and enable real-time streaming output. The shell code is always generated from the current binary, so updates happen automatically.
728
726
 
729
727
  Shell integration provides:
730
728
 
@@ -734,42 +732,44 @@ Shell integration provides:
734
732
  - **Multi-alias support**: Install for different command names (e.g., `gw-dev` for development)
735
733
 
736
734
  ```bash
737
- gw install-shell [options]
735
+ # Add to ~/.zshrc or ~/.bashrc
736
+ eval "$(gw install-shell)"
737
+
738
+ # Add to ~/.config/fish/config.fish
739
+ gw install-shell | source
738
740
  ```
739
741
 
740
742
  #### Options
741
743
 
742
- - `--name, -n NAME`: Install under a different command name (default: `gw`)
744
+ - `--name, -n NAME`: Output for a different command name (default: `gw`)
743
745
  - `--command, -c CMD`: Actual command to run (use with `--name` for aliases/dev)
744
- - `--remove`: Remove shell integration
745
- - `--quiet, -q`: Suppress output messages
746
+ - `--remove`: Remove shell integration from config files
747
+ - `--quiet, -q`: Suppress output messages (for `--remove`)
746
748
  - `-h, --help`: Show help message
747
749
 
748
750
  #### Examples
749
751
 
750
752
  ```bash
751
- # Install shell integration (usually not needed - auto-installed)
753
+ # Preview the shell function output
752
754
  gw install-shell
753
755
 
754
- # Install for development (with Deno)
755
- # Note: Remove any 'alias gw-dev=...' from .zshrc first!
756
- gw install-shell --name gw-dev \
757
- --command "deno run --allow-all ~/path/to/gw-tools/packages/gw-tool/src/main.ts"
756
+ # Add to your shell config (zsh/bash)
757
+ echo 'eval "$(gw install-shell)"' >> ~/.zshrc
758
758
 
759
- # Remove shell integration for 'gw-dev'
760
- gw install-shell --name gw-dev --remove
759
+ # Add for development (with Deno)
760
+ echo 'eval "$(gw install-shell --name gw-dev --command \"deno run --allow-all ~/path/to/main.ts\")"' >> ~/.zshrc
761
761
 
762
- # Install quietly (for automation)
763
- gw install-shell --quiet
762
+ # Remove shell integration (legacy files + eval lines)
763
+ gw install-shell --remove
764
764
  ```
765
765
 
766
766
  **Supported Shells:**
767
767
 
768
- - **Zsh** (~/.zshrc sources ~/.gw/shell/integration[-NAME].zsh)
769
- - **Bash** (~/.bashrc sources ~/.gw/shell/integration[-NAME].bash)
770
- - **Fish** (~/.config/fish/functions/[NAME].fish)
768
+ - **Zsh**: `eval "$(gw install-shell)"` in `~/.zshrc`
769
+ - **Bash**: `eval "$(gw install-shell)"` in `~/.bashrc`
770
+ - **Fish**: `gw install-shell | source` in `~/.config/fish/config.fish`
771
771
 
772
- The command is idempotent - running it multiple times won't create duplicate entries. It will also automatically migrate old inline installations to the new format.
772
+ The `--remove` flag cleans up both the new eval-based format and any legacy file-based installations.
773
773
 
774
774
  ### root
775
775
 
package/install.js CHANGED
@@ -134,10 +134,10 @@ async function install() {
134
134
  // Try to install shell integration, but don't fail if it errors
135
135
  console.log('\n⚙️ Setting up shell integration...');
136
136
  try {
137
- await installShellIntegration(binaryPath);
137
+ installShellIntegration();
138
138
  } catch (error) {
139
139
  console.log(' Shell integration setup encountered an issue.');
140
- console.log(' You can install it manually later with: gw install-shell');
140
+ console.log(' You can add it manually: eval \'"$(gw install-shell)"\' in your shell config');
141
141
  }
142
142
 
143
143
  console.log('\nRun "gw --help" to get started.');
@@ -154,79 +154,62 @@ async function install() {
154
154
  }
155
155
 
156
156
  /**
157
- * Install shell integration
157
+ * Install shell integration by appending eval line to shell config
158
158
  */
159
- async function installShellIntegration(binaryPath, retries = 3) {
160
- const { spawn } = require('child_process');
159
+ function installShellIntegration() {
160
+ const { existsSync: fsExists, readFileSync: fsRead, writeFileSync: fsWrite, mkdirSync: fsMkdir } = require('fs');
161
161
 
162
- // Check environment before attempting shell integration
163
- const hasRequiredEnv = process.env.HOME || process.env.USERPROFILE;
164
- const hasShell = process.env.SHELL;
162
+ const home = process.env.HOME || process.env.USERPROFILE;
163
+ const shell = process.env.SHELL || '';
164
+ const shellName = shell.split('/').pop() || '';
165
165
 
166
- if (!hasRequiredEnv) {
166
+ if (!home) {
167
167
  console.log(' Skipping shell integration: HOME environment variable not set');
168
- console.log(' Run "gw install-shell" manually after installation');
168
+ console.log(' Add eval \'"$(gw install-shell)"\' to your shell config manually');
169
169
  return;
170
170
  }
171
171
 
172
- if (!hasShell) {
172
+ if (!shellName) {
173
173
  console.log(' Skipping shell integration: SHELL environment variable not set');
174
- console.log(' Run "gw install-shell" manually after installation');
174
+ console.log(' Add eval \'"$(gw install-shell)"\' to your shell config manually');
175
175
  return;
176
176
  }
177
177
 
178
- return new Promise((resolve) => {
179
- let child;
178
+ let configFile;
179
+ let evalLine;
180
+
181
+ if (shellName === 'zsh') {
182
+ configFile = join(home, '.zshrc');
183
+ evalLine = 'eval "$(gw install-shell)"';
184
+ } else if (shellName === 'bash') {
185
+ configFile = join(home, '.bashrc');
186
+ evalLine = 'eval "$(gw install-shell)"';
187
+ } else if (shellName === 'fish') {
188
+ const configDir = join(home, '.config', 'fish');
189
+ if (!fsExists(configDir)) {
190
+ fsMkdir(configDir, { recursive: true });
191
+ }
192
+ configFile = join(configDir, 'config.fish');
193
+ evalLine = 'gw install-shell | source';
194
+ } else {
195
+ console.log(` Unsupported shell: ${shellName}`);
196
+ console.log(' Add eval \'"$(gw install-shell)"\' to your shell config manually');
197
+ return;
198
+ }
180
199
 
181
- try {
182
- child = spawn(binaryPath, ['install-shell'], {
183
- stdio: ['inherit', 'inherit', 'pipe'], // stdin, stdout, stderr
184
- });
185
- } catch (err) {
186
- // Catch synchronous spawn errors (e.g., ETXTBSY thrown immediately)
187
- if (err.code === 'ETXTBSY' && retries > 0) {
188
- setTimeout(() => {
189
- installShellIntegration(binaryPath, retries - 1).then(resolve);
190
- }, 200);
191
- return;
192
- }
193
- console.log(' Shell integration setup encountered an issue.');
194
- console.log(' You can install it manually later with: gw install-shell');
195
- resolve();
200
+ // Check if already present
201
+ if (fsExists(configFile)) {
202
+ const content = fsRead(configFile, 'utf8');
203
+ if (content.includes('gw install-shell')) {
204
+ console.log('✓ Shell integration already configured!');
196
205
  return;
197
206
  }
207
+ }
198
208
 
199
- let stderrOutput = '';
200
- child.stderr.on('data', (data) => {
201
- stderrOutput += data.toString();
202
- // Also display it immediately
203
- process.stderr.write(data);
204
- });
205
-
206
- child.on('close', (code) => {
207
- if (code === 0) {
208
- console.log('✓ Shell integration installed!');
209
- } else {
210
- console.log(' Shell integration failed with exit code:', code);
211
- if (stderrOutput) {
212
- console.log(' Error:', stderrOutput.trim());
213
- }
214
- console.log(' You can install it manually later with: gw install-shell');
215
- }
216
- resolve();
217
- });
218
-
219
- child.on('error', async (err) => {
220
- // Retry on ETXTBSY (text file busy) error
221
- if (err.code === 'ETXTBSY' && retries > 0) {
222
- await new Promise((r) => setTimeout(r, 200));
223
- return installShellIntegration(binaryPath, retries - 1).then(resolve);
224
- }
225
- console.log(' Shell integration setup encountered an issue.');
226
- console.log(' You can install it manually later with: gw install-shell');
227
- resolve();
228
- });
229
- });
209
+ // Append eval line
210
+ fsWrite(configFile, '\n' + evalLine + '\n', { flag: 'a' });
211
+ console.log(`✓ Shell integration added to ${configFile}`);
212
+ console.log(' Restart your terminal or source your shell config to activate.');
230
213
  }
231
214
 
232
215
  // Run installation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gw-tools/gw",
3
- "version": "0.20.14",
3
+ "version": "0.21.0-beta.2",
4
4
  "description": "A command-line tool for managing git worktrees - copy files between worktrees with ease",
5
5
  "keywords": [
6
6
  "git",
package/uninstall.js CHANGED
@@ -18,47 +18,57 @@ function manualRemoval() {
18
18
  const shell = process.env.SHELL || '';
19
19
  const shellName = shell.split('/').pop() || '';
20
20
 
21
- let configFile;
22
- let scriptFile;
23
21
  let removed = false;
24
22
 
23
+ // Remove legacy integration script files
24
+ const legacyFiles = [
25
+ join(home, '.gw', 'shell', 'integration.zsh'),
26
+ join(home, '.gw', 'shell', 'integration.bash'),
27
+ join(home, '.config', 'fish', 'functions', 'gw.fish'),
28
+ ];
29
+
30
+ for (const scriptFile of legacyFiles) {
31
+ if (existsSync(scriptFile)) {
32
+ try {
33
+ unlinkSync(scriptFile);
34
+ console.log(` Removed: ${scriptFile}`);
35
+ removed = true;
36
+ } catch (error) {
37
+ console.log(` Could not remove: ${scriptFile}`);
38
+ }
39
+ }
40
+ }
41
+
42
+ // Determine config files to clean up
43
+ const configFiles = [];
25
44
  if (shellName === 'zsh') {
26
- configFile = join(home, '.zshrc');
27
- scriptFile = join(home, '.gw', 'shell', 'integration.zsh');
45
+ configFiles.push(join(home, '.zshrc'));
28
46
  } else if (shellName === 'bash') {
29
- configFile = join(home, '.bashrc');
30
- scriptFile = join(home, '.gw', 'shell', 'integration.bash');
47
+ configFiles.push(join(home, '.bashrc'));
31
48
  } else if (shellName === 'fish') {
32
- scriptFile = join(home, '.config', 'fish', 'functions', 'gw.fish');
33
- configFile = null; // Fish doesn't need config file cleanup
49
+ configFiles.push(join(home, '.config', 'fish', 'config.fish'));
34
50
  } else {
35
- console.log(' Could not detect shell for manual cleanup');
36
- return false;
51
+ // Try all common config files
52
+ configFiles.push(join(home, '.zshrc'));
53
+ configFiles.push(join(home, '.bashrc'));
54
+ configFiles.push(join(home, '.config', 'fish', 'config.fish'));
37
55
  }
38
56
 
39
- // Remove the integration script file
40
- if (existsSync(scriptFile)) {
41
- try {
42
- unlinkSync(scriptFile);
43
- console.log(` Removed: ${scriptFile}`);
44
- removed = true;
45
- } catch (error) {
46
- console.log(` Could not remove: ${scriptFile}`);
47
- }
48
- }
57
+ for (const configFile of configFiles) {
58
+ if (!existsSync(configFile)) continue;
49
59
 
50
- // Remove source line from config file (for bash/zsh)
51
- if (configFile && existsSync(configFile)) {
52
60
  try {
53
61
  const content = readFileSync(configFile, 'utf8');
54
62
  const lines = content.split('\n');
55
63
  const filtered = [];
56
64
  let skipNext = false;
65
+ let fileModified = false;
57
66
 
58
67
  for (const line of lines) {
68
+ // Remove old format: comment + source line
59
69
  if (line.includes('# gw-tools shell integration')) {
60
70
  skipNext = true;
61
- removed = true;
71
+ fileModified = true;
62
72
  continue;
63
73
  }
64
74
  if (skipNext && line.includes('source ~/.gw/shell/integration')) {
@@ -66,12 +76,20 @@ function manualRemoval() {
66
76
  continue;
67
77
  }
68
78
  skipNext = false;
79
+
80
+ // Remove new eval-based format and fish source format
81
+ if (line.includes('gw install-shell')) {
82
+ fileModified = true;
83
+ continue;
84
+ }
85
+
69
86
  filtered.push(line);
70
87
  }
71
88
 
72
- if (removed) {
89
+ if (fileModified) {
73
90
  writeFileSync(configFile, filtered.join('\n'));
74
91
  console.log(` Updated: ${configFile}`);
92
+ removed = true;
75
93
  }
76
94
  } catch (error) {
77
95
  console.log(` Could not update: ${configFile}`);