arashi 1.24.0 → 1.25.0

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
@@ -172,7 +172,7 @@ arashi switch --no-default-launch # bypass configured sesh/Herdr mode
172
172
 
173
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.
174
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`.
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. For switch, it bypasses configured `sesh` or `herdr` launch defaults; for create, it bypasses configured generic or editor-scoped launch defaults. An explicit launcher selector remains authoritative. Unsupported adapters fail with `TAB_DISPOSITION_UNSUPPORTED`; launch/preflight failures use `LAUNCH_FAILED`. Human-only tab launch is incompatible with `--json`.
176
176
 
177
177
  ### Managed Git ignore rules
178
178
 
@@ -229,24 +229,16 @@ For automated installs, set `ARASHI_SHELL_INTEGRATION=yes` to enable it without
229
229
 
230
230
  ## Hooks
231
231
 
232
- Arashi can run lifecycle hooks during `arashi create` and `arashi remove`.
232
+ Arashi can run lifecycle hooks during `arashi create` and `arashi remove`. Configured create uses
233
+ workspace `pre-create`/`post-create` plus repository-specific `pre-create.<repo>` and
234
+ `post-create.<repo>` files. Configured remove evaluates repository, workspace, user-global targeted,
235
+ and user-global shared scopes once per target repository. Standalone mode activates only targeted
236
+ and shared user-global hooks.
233
237
 
234
- - Global hooks in `.arashi/hooks/`:
235
- - `pre-create.sh`
236
- - `post-create.sh`
237
- - `pre-remove.sh`
238
- - `post-remove.sh`
239
- - Repository-specific hooks:
240
- - `pre-create.<repo>.sh`
241
- - `post-create.<repo>.sh`
242
- - Scoped remove hooks:
243
- - repository scope: `repos/<repo>/.arashi/hooks/pre-remove.sh` and `post-remove.sh`
244
- - global shared: `~/.arashi/hooks/pre-remove.sh` and `post-remove.sh`
245
- - global targeted: `~/.arashi/hooks/<repo>/pre-remove.sh` and `post-remove.sh`
246
-
247
- For `arashi remove`, hook execution order is: repository scope -> workspace-root scope -> global targeted scope -> global shared scope.
248
-
249
- `pre-remove.sh` is useful for teardown before deletion (for example, stopping tmux sessions), and `post-remove.sh` can run final cleanup after remove operations complete.
238
+ POSIX uses executable `.sh` files. Windows uses one case-insensitive `.ps1`, `.cmd`, or `.bat`
239
+ candidate per location and never runs `.sh` implicitly. Hook failures participate in create rollback
240
+ or remove finalization, all hooks default to a 300000 ms timeout, and JSON results expose the ordered
241
+ ledger at `data.hookOutcomes` on success or `error.details.hookOutcomes` on failure.
250
242
 
251
243
  See [`docs/hooks.md`](./docs/hooks.md) for hook behavior, environment variables, and examples.
252
244
 
@@ -351,7 +343,7 @@ Legacy switch-only `launchMode` and `launch_mode` fields remain readable for a b
351
343
 
352
344
  Use `defaults.create` for terminal `arashi create` behavior. Use `defaults.editors.<host>.create` for editor-specific overrides such as VS Code extension create flows. Supported hosts are `vscode`, `cursor`, and `kiro`. Each scope has one canonical `launch` choice: `none` | `auto` | `sesh` | `herdr`. `switch` stays independent, while any enabled launch implies switch handling for the newly created primary worktree.
353
345
 
354
- Create precedence is: reject `--sesh` plus `--herdr`; then explicit `--sesh` / `--herdr`; `--launch`; `--no-launch`; the matching configured scope; and built-in `none`. An editor-hosted invocation does not fall back to terminal or another editor scope.
346
+ Create precedence is: reject `--sesh` plus `--herdr`; then explicit `--sesh` / `--herdr`; `--tab` or `--launch`; `--no-launch`; the matching configured scope; and built-in `none`. `--tab` bypasses the matching configured scope unless an explicit launcher selector is present. An editor-hosted invocation does not fall back to terminal or another editor scope.
355
347
 
356
348
  Legacy create booleans plus `launchMode` / `launch_mode` remain readable for a bounded compatibility window. Accepted combinations warn on stderr with the exact canonical replacement and do not rewrite the file. Disabled launch plus a launcher, conflicting aliases, and conflicting canonical/legacy choices are rejected before workspace mutation.
357
349
 
@@ -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.24.0",
3
+ "version": "1.25.0",
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",
@@ -34,7 +34,10 @@
34
34
  "description": "Optional workspace-level hooks settings",
35
35
  "properties": {
36
36
  "timeout": {
37
- "description": "Timeout in milliseconds for long-running operations",
37
+ "description": "Lifecycle-hook timeout in milliseconds (default: 300000)",
38
+ "maximum": 2147483647,
39
+ "minimum": 1,
40
+ "multipleOf": 1,
38
41
  "type": "number"
39
42
  }
40
43
  },