arashi 1.23.0 → 1.24.1

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
@@ -41,7 +41,17 @@ It also runs a quick `arashi --version` smoke test before declaring success.
41
41
 
42
42
  If curl installation fails, or if the smoke test reports a bad release artifact, use npm installation below or the manual release instructions in [`docs/INSTALLATION.md`](./docs/INSTALLATION.md).
43
43
 
44
- ### Option 2: Install with npm
44
+ ### Option 2: Install on Windows with PowerShell
45
+
46
+ PowerShell is the canonical Windows installer:
47
+
48
+ ```powershell
49
+ powershell -c "irm https://arashi.haphazard.dev/install.ps1 | iex"
50
+ ```
51
+
52
+ It verifies and installs `arashi.bin.exe`, the extensionless `arashi` wrapper for Git Bash, `arashi.ps1`, and `arashi.bat` from the same release. The default directory is `%USERPROFILE%\.arashi\bin`; the installer adds it to the persistent user PATH. It does not create or modify `.bashrc` or another shell profile. Open a new Git Bash window before running `arashi --version` so it inherits the PATH change.
53
+
54
+ ### Option 3: Install with npm
45
55
 
46
56
  ```bash
47
57
  npm install -g arashi
@@ -93,13 +103,17 @@ chmod +x arashi
93
103
  sudo mv arashi /usr/local/bin/arashi
94
104
  ```
95
105
 
96
- Windows (PowerShell):
106
+ Windows (PowerShell and Git Bash):
97
107
 
98
108
  ```powershell
99
- Invoke-WebRequest -Uri "https://github.com/corwinm/arashi/releases/latest/download/arashi-windows-x64.exe" -OutFile "arashi.exe"
100
- # Move arashi.exe to a folder on your PATH
109
+ # Download arashi-windows-x64.exe, arashi, arashi.ps1, arashi.bat,
110
+ # and arashi-checksums.txt from the same release.
111
+ # Verify all four payload files against arashi-checksums.txt.
112
+ # Rename arashi-windows-x64.exe to arashi.bin.exe and keep the four files together on PATH.
101
113
  ```
102
114
 
115
+ Windows manual installation requires `arashi-windows-x64.exe`, `arashi`, `arashi.ps1`, `arashi.bat`, and `arashi-checksums.txt` from the same release. Verify all four payload files, rename the executable to `arashi.bin.exe`, and keep the payload together on PATH.
116
+
103
117
  You can also build from source for local development:
104
118
 
105
119
  ```bash
@@ -117,12 +131,12 @@ Arashi currently provides these commands:
117
131
  - `arashi update [--check] [--dry-run] [--yes]`
118
132
  - `arashi add <git-url>`
119
133
  - `arashi clone [--all]`
120
- - `arashi create <branch> [--tmux|--sesh|--herdr]`
134
+ - `arashi create <branch> [--tab] [--tmux|--sesh|--herdr]`
121
135
  - `arashi list`
122
136
  - `arashi status`
123
137
  - `arashi remove <branch|path>`
124
138
  - `arashi prune [--dry-run]` - clean stale Git worktree metadata
125
- - `arashi switch [filter] [--repos|--all] [--cd|--no-cd] [--tmux|--sesh|--herdr] [--no-default-launch]`
139
+ - `arashi switch [filter] [--repos|--all] [--tab] [--cd|--no-cd] [--tmux|--sesh|--herdr] [--no-default-launch]`
126
140
  - `arashi shell init <bash|zsh|fish>`
127
141
  - `arashi shell install`
128
142
  - `arashi pull`
@@ -141,6 +155,7 @@ arashi create feature-auth-refresh
141
155
  arashi create feature-auth-refresh --launch
142
156
  arashi create feature-auth-refresh --tmux
143
157
  arashi create feature-auth-refresh --herdr
158
+ arashi create feature-auth-refresh --tab
144
159
  arashi create feature-auth-refresh --no-launch
145
160
  arashi shell install
146
161
  arashi status
@@ -151,11 +166,14 @@ arashi switch --repos docs # repo-name matching in child repos
151
166
  arashi switch --cd feature-auth-refresh # parent-shell cd when shell integration is active
152
167
  arashi switch --tmux feature-auth-refresh # force a new plain tmux window
153
168
  arashi switch --herdr feature-auth-refresh # open or focus a persistent Herdr workspace
169
+ arashi switch --tab feature-auth-refresh # request a true tab or managed equivalent
154
170
  arashi switch --no-default-launch # bypass configured sesh/Herdr mode once
155
171
  ```
156
172
 
157
173
  Explicit `--tmux` is a per-invocation launcher override for `create` and `switch`; it is not a persisted configuration mode. It requires an active tmux context whose `TMUX` value is non-empty after trimming, uses the selected worktree path as one argv-safe `tmux new-window -c` argument, and does not fall back to another launcher when the prerequisite or launch fails. On `create`, it implies both launch and switch, while validation failures occur before worktree mutation.
158
174
 
175
+ `--tab` is a CLI-only launch disposition for `create` and `switch`; it is never persisted. It requests a true terminal tab or documented managed-context equivalent, implies launch (and selection for `create`), overrides automatic parent-shell `cd`, and never degrades to a window or another launcher. Unsupported adapters fail with `TAB_DISPOSITION_UNSUPPORTED`; launch/preflight failures use `LAUNCH_FAILED`. Human-only tab launch is incompatible with `--json`.
176
+
159
177
  ### Managed Git ignore rules
160
178
 
161
179
  Configured workspaces keep `reposDir` and `worktreesDir` effectively ignored. Arashi asks Git
@@ -272,7 +290,7 @@ bindkey -s '^s' 'sesh connect $(arashi list | fzf)\n'
272
290
 
273
291
  You can also use `arashi switch --sesh` directly inside tmux to open the selected worktree in a new tmux window.
274
292
 
275
- `arashi switch` checks managed contexts in this order: tmux → Herdr → cmux → integrated IDE → Kitty. Managed Kitty selection requires both `KITTY_PID` and `KITTY_WINDOW_ID`; `TERM=xterm-kitty` alone remains only generic terminal evidence.
293
+ `arashi switch` checks managed contexts in this order: tmux → Herdr → cmux → integrated IDE → Kitty. Managed Kitty selection applies when any one of `KITTY_PID`, `KITTY_WINDOW_ID`, or `TERM=xterm-kitty` is present after normalization.
276
294
 
277
295
  Managed Kitty requires Kitty 0.43 or newer plus working `kitten @` remote control. Arashi reuses only its exact worktree marker and focuses that tab before launching a new session-backed tab. Once managed Kitty is selected, missing or unsupported tooling, denied remote control, duplicate markers, and validation failures are reported directly instead of falling back to another launcher.
278
296
 
package/bin/arashi CHANGED
@@ -14,23 +14,36 @@ done
14
14
  SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_SOURCE")" && pwd)"
15
15
  BINARY="$SCRIPT_DIR/arashi.bin"
16
16
 
17
- # Use platform-specific binary if main binary doesn't exist
17
+ # Use the Windows binary only with explicit Git-for-Windows/MSYS/Cygwin evidence.
18
+ # arashi.bin always retains precedence when both binaries are present.
18
19
  if [ ! -f "$BINARY" ]; then
19
20
  PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')
20
21
  ARCH=$(uname -m)
21
-
22
- case "$PLATFORM-$ARCH" in
23
- darwin-arm64)
24
- BINARY="$SCRIPT_DIR/arashi-macos-arm64"
25
- ;;
26
- linux-x86_64)
27
- BINARY="$SCRIPT_DIR/arashi-linux-x64"
28
- ;;
29
- *)
30
- echo "Error: Unsupported platform $PLATFORM-$ARCH" >&2
31
- exit 1
32
- ;;
22
+ WINDOWS_SHELL="false"
23
+
24
+ case "${MSYSTEM:-}" in
25
+ MINGW*|MSYS*) WINDOWS_SHELL="true" ;;
33
26
  esac
27
+ case "$PLATFORM" in
28
+ mingw*|msys*|cygwin*) WINDOWS_SHELL="true" ;;
29
+ esac
30
+
31
+ if [ "$WINDOWS_SHELL" = "true" ]; then
32
+ BINARY="$SCRIPT_DIR/arashi.bin.exe"
33
+ else
34
+ case "$PLATFORM-$ARCH" in
35
+ darwin-arm64)
36
+ BINARY="$SCRIPT_DIR/arashi-macos-arm64"
37
+ ;;
38
+ linux-x86_64)
39
+ BINARY="$SCRIPT_DIR/arashi-linux-x64"
40
+ ;;
41
+ *)
42
+ echo "Error: Unsupported platform $PLATFORM-$ARCH" >&2
43
+ exit 1
44
+ ;;
45
+ esac
46
+ fi
34
47
  fi
35
48
 
36
49
  if [ ! -f "$BINARY" ]; then
@@ -49,7 +62,11 @@ for arg in "$@"; do
49
62
  esac
50
63
  continue
51
64
  ;;
52
- *) command="$arg"; break ;;
65
+ *)
66
+ if [ -z "$command" ]; then
67
+ command="$arg"
68
+ fi
69
+ ;;
53
70
  esac
54
71
  done
55
72
 
@@ -0,0 +1,44 @@
1
+ const WINDOWS_BATCH_FILE = /\.(?:cmd|bat)$/i;
2
+ const CMD_ARGUMENT_VARIABLE_PREFIX = "ARASHI_CMD_ARGUMENT_";
3
+
4
+ // Quote according to CommandLineToArgvW's rules. The result is stored in an
5
+ // environment variable and introduced with ordinary expansion. Cmd expands each
6
+ // fixed %VARIABLE% token once, but does not rescan user-controlled contents as
7
+ // command syntax. Delayed expansion stays disabled so literal ! characters survive.
8
+ const quoteWindowsArgument = (argument) =>
9
+ `"${argument.replaceAll(/(\\*)"/g, String.raw`$1$1\"`).replace(/(\\*)$/, "$1$1")}"`;
10
+
11
+ export function prepareSpawnCommand(
12
+ command,
13
+ platform = process.platform,
14
+ env = process.env,
15
+ forceWindowsShell = false,
16
+ ) {
17
+ const executable = command[0];
18
+ if (
19
+ platform !== "win32" ||
20
+ (!forceWindowsShell && !WINDOWS_BATCH_FILE.test(executable))
21
+ ) {
22
+ return { args: command.slice(1), command: executable, windowsVerbatimArguments: false };
23
+ }
24
+
25
+ const values = command.map((argument) => quoteWindowsArgument(argument));
26
+ const variableNames = values.map((_value, index) => `${CMD_ARGUMENT_VARIABLE_PREFIX}${index}`);
27
+
28
+ const commandInterpreter =
29
+ Object.entries(env).find(([key]) => key.toLowerCase() === "comspec")?.[1] ?? "cmd.exe";
30
+
31
+ return {
32
+ args: ["/d", "/v:off", "/s", "/c", `"${variableNames.map((name) => `%${name}%`).join(" ")}"`],
33
+ command: commandInterpreter,
34
+ env: {
35
+ ...Object.fromEntries(
36
+ Object.entries(env).filter(
37
+ ([name]) => !name.toUpperCase().startsWith(CMD_ARGUMENT_VARIABLE_PREFIX),
38
+ ),
39
+ ),
40
+ ...Object.fromEntries(variableNames.map((name, index) => [name, values[index]])),
41
+ },
42
+ windowsVerbatimArguments: true,
43
+ };
44
+ }
package/bin/update.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { readFile } from "node:fs/promises";
3
- import { join } from "node:path";
3
+ import { dirname, join } from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { getPlatformInfo, installBinary, MANUAL_INSTALL_URL, PACKAGE_NAME } from "./install-binary.js";
7
+ import { prepareSpawnCommand } from "./prepare-spawn-command.js";
7
8
 
8
9
  export const UPDATE_COMMAND_DESCRIPTION = "Check for and apply Arashi updates";
9
10
 
@@ -97,15 +98,84 @@ export async function fetchLatestGitHubRelease({ fetchImpl = fetch, repo = "corw
97
98
  };
98
99
  }
99
100
 
101
+ function normalizeInstallPath(value) {
102
+ return String(value ?? "")
103
+ .trim()
104
+ .replaceAll("\\", "/")
105
+ .replace(/\/+$/, "")
106
+ .toLowerCase();
107
+ }
108
+
109
+ function readEnvironmentValue(env, name) {
110
+ return Object.entries(env).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1] ?? "";
111
+ }
112
+
113
+ function readEnvironmentPath(env, name) {
114
+ return normalizeInstallPath(readEnvironmentValue(env, name));
115
+ }
116
+
117
+ function detectPackageManagerFromInstallRoot(rootDir, env) {
118
+ const root = normalizeInstallPath(rootDir);
119
+ if (!root) return null;
120
+
121
+ const userProfile = readEnvironmentPath(env, "USERPROFILE");
122
+ const home = userProfile || readEnvironmentPath(env, "HOME");
123
+ const appData =
124
+ readEnvironmentPath(env, "APPDATA") ||
125
+ (userProfile ? `${userProfile}/appdata/roaming` : "");
126
+ const localAppData =
127
+ readEnvironmentPath(env, "LOCALAPPDATA") ||
128
+ (userProfile ? `${userProfile}/appdata/local` : "");
129
+
130
+ if (home && root === `${home}/.vite-plus/packages/${PACKAGE_NAME}/current/package`) {
131
+ return "vite-plus";
132
+ }
133
+ if (appData && root === `${appData}/npm/node_modules/${PACKAGE_NAME}`) return "npm";
134
+ if (localAppData && root === `${localAppData}/yarn/data/global/node_modules/${PACKAGE_NAME}`) {
135
+ return "yarn";
136
+ }
137
+ if (userProfile && root === `${userProfile}/.bun/install/global/node_modules/${PACKAGE_NAME}`) {
138
+ return "bun";
139
+ }
140
+
141
+ const pnpmHomes = new Set(
142
+ [localAppData ? `${localAppData}/pnpm` : "", readEnvironmentPath(env, "PNPM_HOME")].filter(Boolean),
143
+ );
144
+ for (const pnpmHome of pnpmHomes) {
145
+ const relativeRoot = root.startsWith(`${pnpmHome}/`) ? root.slice(pnpmHome.length + 1) : "";
146
+ if (
147
+ /^global\/[^/]+\/(?:\.pnpm\/[^/]+\/node_modules|node_modules)\/arashi$/.test(relativeRoot)
148
+ ) {
149
+ return "pnpm";
150
+ }
151
+ }
152
+
153
+ return null;
154
+ }
155
+
100
156
  export function selectPackageManagerCommand({ env = process.env, rootDir } = {}) {
101
- const userAgent = env.npm_config_user_agent ?? "";
102
- const execPath = env.npm_execpath ?? "";
157
+ const installRootManager = detectPackageManagerFromInstallRoot(rootDir, env);
158
+ if (installRootManager === "vite-plus") {
159
+ return { args: ["update", "-g", PACKAGE_NAME], command: "vp", label: "Vite+" };
160
+ }
161
+ if (installRootManager === "pnpm") {
162
+ return { args: ["add", "-g", `${PACKAGE_NAME}@latest`], command: "pnpm", label: "pnpm" };
163
+ }
164
+ if (installRootManager === "yarn") {
165
+ return { args: ["global", "add", `${PACKAGE_NAME}@latest`], command: "yarn", label: "yarn" };
166
+ }
167
+ if (installRootManager === "bun") {
168
+ return { args: ["add", "-g", `${PACKAGE_NAME}@latest`], command: "bun", label: "bun" };
169
+ }
170
+ if (installRootManager === "npm") {
171
+ return { args: ["install", "-g", `${PACKAGE_NAME}@latest`], command: "npm", label: "npm" };
172
+ }
173
+
174
+ const userAgent = readEnvironmentValue(env, "npm_config_user_agent");
175
+ const execPath = readEnvironmentValue(env, "npm_execpath");
103
176
  const combined = `${userAgent} ${execPath}`.toLowerCase();
104
- const normalizedRootDir = String(rootDir ?? "").toLowerCase();
105
177
  const looksLikeVitePlus =
106
- combined.includes("vite-plus") ||
107
- /(^|[\\/\s])vp(?:\.exe)?($|[\\/\s])/.test(combined) ||
108
- /(^|[\\/])\.vite-plus([\\/]|$)/.test(normalizedRootDir);
178
+ combined.includes("vite-plus") || /(^|[\\/\s])vp(?:\.exe)?($|[\\/\s])/.test(combined);
109
179
 
110
180
  if (looksLikeVitePlus) {
111
181
  return { args: ["update", "-g", PACKAGE_NAME], command: "vp", label: "Vite+" };
@@ -212,7 +282,8 @@ export async function runNpmManagedUpdate(argv = [], options = {}) {
212
282
  return 0;
213
283
  }
214
284
 
215
- const packageManager = options.packageManager ?? selectPackageManagerCommand(options);
285
+ const packageManager =
286
+ options.packageManager ?? selectPackageManagerCommand({ ...options, rootDir });
216
287
  log(`Update available: ${PACKAGE_NAME} v${metadata.version} -> v${latestVersion}`);
217
288
 
218
289
  if (flags.check) {
@@ -240,10 +311,21 @@ export async function runNpmManagedUpdate(argv = [], options = {}) {
240
311
  }
241
312
 
242
313
  const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync;
243
- const result = spawnSyncImpl(packageManager.command, packageManager.args, {
244
- cwd: rootDir,
314
+ const platform = options.platform ?? process.platform;
315
+ const env = options.env ?? process.env;
316
+ const updateCwd = options.updateCwd ?? dirname(process.execPath);
317
+ const invocation = prepareSpawnCommand(
318
+ [packageManager.command, ...packageManager.args],
319
+ platform,
320
+ env,
321
+ true,
322
+ );
323
+ const result = spawnSyncImpl(invocation.command, invocation.args, {
324
+ cwd: updateCwd,
245
325
  encoding: "utf8",
326
+ env: invocation.env ?? env,
246
327
  stdio: "inherit",
328
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments,
247
329
  });
248
330
 
249
331
  if (result.error) {
@@ -256,20 +338,39 @@ export async function runNpmManagedUpdate(argv = [], options = {}) {
256
338
  return 1;
257
339
  }
258
340
 
259
- let updatedMetadata = metadata;
260
341
  try {
261
- updatedMetadata = await readPackageMetadata(rootDir, options);
262
- } catch {
263
- updatedMetadata = { ...metadata, version: latestVersion };
264
- }
342
+ let activeRootDir = rootDir;
343
+ if (packageManager.command === "pnpm") {
344
+ const rootInvocation = prepareSpawnCommand([packageManager.command, "root", "-g"], platform, env, true);
345
+ const rootResult = spawnSyncImpl(rootInvocation.command, rootInvocation.args, {
346
+ cwd: updateCwd,
347
+ encoding: "utf8",
348
+ env: rootInvocation.env ?? env,
349
+ windowsVerbatimArguments: rootInvocation.windowsVerbatimArguments,
350
+ });
351
+ if (rootResult.error) throw rootResult.error;
352
+ if (rootResult.status !== 0) {
353
+ throw new Error(`pnpm root -g failed with exit code ${rootResult.status ?? "unknown"}`);
354
+ }
355
+ const globalRoot = String(rootResult.stdout ?? "").trim();
356
+ if (!globalRoot) throw new Error("pnpm root -g returned an empty path");
357
+ const separator = platform === "win32" ? "\\" : "/";
358
+ activeRootDir = `${globalRoot.replace(/[\\/]+$/, "")}${separator}${PACKAGE_NAME}`;
359
+ }
360
+
361
+ let updatedMetadata = metadata;
362
+ try {
363
+ updatedMetadata = await readPackageMetadata(activeRootDir, options);
364
+ } catch {
365
+ updatedMetadata = { ...metadata, version: latestVersion };
366
+ }
265
367
 
266
- try {
267
368
  const installBinaryImpl = options.installBinaryImpl ?? installBinary;
268
369
  const installResult = await installBinaryImpl({
269
370
  ...options,
270
371
  binDir: options.binDir,
271
372
  force: true,
272
- rootDir,
373
+ rootDir: activeRootDir,
273
374
  version: updatedMetadata.version,
274
375
  });
275
376
  log(`✓ Updated ${PACKAGE_NAME} from v${metadata.version} to v${updatedMetadata.version}.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arashi",
3
- "version": "1.23.0",
3
+ "version": "1.24.1",
4
4
  "description": "Git worktree manager for meta-repositories - The eye of the storm for your development workflow",
5
5
  "keywords": [
6
6
  "cli",
@@ -30,6 +30,7 @@
30
30
  "bin/arashi",
31
31
  "bin/arashi.js",
32
32
  "bin/install-binary.js",
33
+ "bin/prepare-spawn-command.js",
33
34
  "bin/update.js",
34
35
  "bin/arashi.bat",
35
36
  "bin/arashi.ps1",