dotswitch 1.0.0 → 1.1.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 +46 -5
- package/dist/cli.js +111 -19
- package/dist/index.cjs +64 -1
- package/dist/index.d.cts +16 -1
- package/dist/index.d.mts +16 -1
- package/dist/index.mjs +63 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://github.com/natterstefan/dotswitch/actions/workflows/ci.yml)
|
|
6
6
|
|
|
7
|
-
Quickly switch between `.env` files. Copies
|
|
7
|
+
Quickly switch between `.env` files. Copies
|
|
8
|
+
`.env.<environment>` to `.env.local` (or a custom target) and
|
|
9
|
+
tracks the active environment via a header comment. Works with
|
|
10
|
+
Next.js, Vite, Remix, and any project that uses `.env` files.
|
|
8
11
|
|
|
9
12
|
## Install
|
|
10
13
|
|
|
@@ -121,7 +124,9 @@ All commands support:
|
|
|
121
124
|
|
|
122
125
|
## Configuration
|
|
123
126
|
|
|
124
|
-
Create a `.dotswitchrc.json` in your project root to customize
|
|
127
|
+
Create a `.dotswitchrc.json` in your project root to customize
|
|
128
|
+
behavior. Everything is optional — dotswitch works out of the
|
|
129
|
+
box without a config file.
|
|
125
130
|
|
|
126
131
|
```json
|
|
127
132
|
{
|
|
@@ -143,7 +148,9 @@ Create a `.dotswitchrc.json` in your project root to customize behavior. Everyth
|
|
|
143
148
|
|
|
144
149
|
### Custom target file
|
|
145
150
|
|
|
146
|
-
By default dotswitch writes to `.env.local`, but some
|
|
151
|
+
By default dotswitch writes to `.env.local`, but some
|
|
152
|
+
frameworks use `.env` directly. Set the `target` field to
|
|
153
|
+
change this:
|
|
147
154
|
|
|
148
155
|
```json
|
|
149
156
|
{
|
|
@@ -175,7 +182,8 @@ Automatically switch environments when you check out a branch.
|
|
|
175
182
|
dotswitch hook install
|
|
176
183
|
```
|
|
177
184
|
|
|
178
|
-
Now `git checkout staging/feat-login` will automatically run
|
|
185
|
+
Now `git checkout staging/feat-login` will automatically run
|
|
186
|
+
`dotswitch use staging`.
|
|
179
187
|
|
|
180
188
|
### Patterns
|
|
181
189
|
|
|
@@ -203,6 +211,29 @@ dotswitch ls --path "./packages/*"
|
|
|
203
211
|
|
|
204
212
|
Each directory is processed independently with labeled output.
|
|
205
213
|
|
|
214
|
+
## Git worktree support
|
|
215
|
+
|
|
216
|
+
dotswitch works transparently in
|
|
217
|
+
[git worktrees](https://git-scm.com/docs/git-worktree). When
|
|
218
|
+
you run any command from a worktree, it automatically resolves
|
|
219
|
+
back to the main repo where your `.env.*` files and
|
|
220
|
+
`.dotswitchrc.json` live.
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# From a worktree, all commands operate on the main repo
|
|
224
|
+
cd /path/to/my-worktree
|
|
225
|
+
dotswitch ls # lists envs from the main repo
|
|
226
|
+
dotswitch use staging # switches in the main repo
|
|
227
|
+
dotswitch hook install # installs hook in the shared .git/hooks
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Explicit `--path` arguments are rebased automatically, so
|
|
231
|
+
monorepo globs also work from worktrees:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
dotswitch use staging --path "./apps/*"
|
|
235
|
+
```
|
|
236
|
+
|
|
206
237
|
## How it works
|
|
207
238
|
|
|
208
239
|
When you run `dotswitch use staging`, it:
|
|
@@ -211,7 +242,8 @@ When you run `dotswitch use staging`, it:
|
|
|
211
242
|
2. Copies `.env.staging` to `.env.local`
|
|
212
243
|
3. Prepends a `# dotswitch:staging` header to track the active environment
|
|
213
244
|
|
|
214
|
-
The header comment is how `dotswitch ls` and
|
|
245
|
+
The header comment is how `dotswitch ls` and
|
|
246
|
+
`dotswitch current` know which environment is active.
|
|
215
247
|
|
|
216
248
|
## Programmatic API
|
|
217
249
|
|
|
@@ -226,6 +258,7 @@ import {
|
|
|
226
258
|
loadConfig,
|
|
227
259
|
parseEnvContent,
|
|
228
260
|
diffEnvMaps,
|
|
261
|
+
resolveProjectRoot,
|
|
229
262
|
} from "dotswitch";
|
|
230
263
|
|
|
231
264
|
const files = listEnvFiles(process.cwd());
|
|
@@ -233,6 +266,14 @@ const active = getActiveEnv(process.cwd());
|
|
|
233
266
|
switchEnv(process.cwd(), "staging", { backup: true });
|
|
234
267
|
```
|
|
235
268
|
|
|
269
|
+
In worktree-aware scripts, resolve the project root first:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
const projectRoot = resolveProjectRoot(process.cwd());
|
|
273
|
+
const files = listEnvFiles(projectRoot);
|
|
274
|
+
switchEnv(projectRoot, "staging", { backup: true });
|
|
275
|
+
```
|
|
276
|
+
|
|
236
277
|
## Requirements
|
|
237
278
|
|
|
238
279
|
- Node.js >= 20
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { Command } from "commander";
|
|
4
3
|
import fs from "node:fs";
|
|
5
4
|
import path from "node:path";
|
|
5
|
+
import { Command } from "commander";
|
|
6
6
|
import pc from "picocolors";
|
|
7
7
|
import select from "@inquirer/select";
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
8
9
|
|
|
9
10
|
//#region src/lib/constants.ts
|
|
10
11
|
const ENV_LOCAL = ".env.local";
|
|
@@ -54,7 +55,7 @@ const logger = {
|
|
|
54
55
|
console.log(pc.cyan(message));
|
|
55
56
|
},
|
|
56
57
|
warn(message) {
|
|
57
|
-
console.
|
|
58
|
+
console.error(pc.yellow(`⚠ ${message}`));
|
|
58
59
|
},
|
|
59
60
|
error(message) {
|
|
60
61
|
console.error(pc.red(`✗ ${message}`));
|
|
@@ -421,6 +422,66 @@ function diffCommand(env1, env2, options) {
|
|
|
421
422
|
}
|
|
422
423
|
}
|
|
423
424
|
|
|
425
|
+
//#endregion
|
|
426
|
+
//#region src/lib/git.ts
|
|
427
|
+
function git(dir, ...args) {
|
|
428
|
+
try {
|
|
429
|
+
return execFileSync("git", [
|
|
430
|
+
"-C",
|
|
431
|
+
dir,
|
|
432
|
+
...args
|
|
433
|
+
], {
|
|
434
|
+
encoding: "utf-8",
|
|
435
|
+
stdio: [
|
|
436
|
+
"pipe",
|
|
437
|
+
"pipe",
|
|
438
|
+
"pipe"
|
|
439
|
+
]
|
|
440
|
+
}).trim();
|
|
441
|
+
} catch (error) {
|
|
442
|
+
const stderr = error instanceof Error && "stderr" in error ? String(error.stderr).trim() : "";
|
|
443
|
+
if (stderr) logger.warn(`git ${args.join(" ")}: ${stderr}`);
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Resolve the common (shared) git directory for a project.
|
|
449
|
+
* - Regular repos: returns <dir>/.git
|
|
450
|
+
* - Worktrees: returns the main repo's .git directory
|
|
451
|
+
* - Non-git directories: returns null
|
|
452
|
+
*/
|
|
453
|
+
function resolveCommonGitDir(dir) {
|
|
454
|
+
const gitPath = path.join(dir, ".git");
|
|
455
|
+
try {
|
|
456
|
+
if (fs.statSync(gitPath).isDirectory()) return gitPath;
|
|
457
|
+
} catch {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
const result = git(dir, "rev-parse", "--git-common-dir");
|
|
461
|
+
if (!result) return null;
|
|
462
|
+
return path.isAbsolute(result) ? result : path.resolve(dir, result);
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Resolve the project root directory.
|
|
466
|
+
* In a worktree, this returns the main repo's root (parent of its .git dir).
|
|
467
|
+
* Otherwise returns the given directory as-is.
|
|
468
|
+
*/
|
|
469
|
+
function resolveProjectRoot(dir) {
|
|
470
|
+
const gitPath = path.join(dir, ".git");
|
|
471
|
+
let stats;
|
|
472
|
+
try {
|
|
473
|
+
stats = fs.statSync(gitPath);
|
|
474
|
+
} catch {
|
|
475
|
+
return dir;
|
|
476
|
+
}
|
|
477
|
+
if (stats.isDirectory()) return dir;
|
|
478
|
+
if (stats.isFile()) {
|
|
479
|
+
const commonGitDir = resolveCommonGitDir(dir);
|
|
480
|
+
if (commonGitDir) return path.dirname(commonGitDir);
|
|
481
|
+
}
|
|
482
|
+
return dir;
|
|
483
|
+
}
|
|
484
|
+
|
|
424
485
|
//#endregion
|
|
425
486
|
//#region src/lib/hooks.ts
|
|
426
487
|
const HOOK_FILENAME = "post-checkout";
|
|
@@ -436,8 +497,8 @@ fi
|
|
|
436
497
|
${HOOK_MARKER_END}`;
|
|
437
498
|
}
|
|
438
499
|
function getHooksDir(dir) {
|
|
439
|
-
const gitDir =
|
|
440
|
-
if (!
|
|
500
|
+
const gitDir = resolveCommonGitDir(dir);
|
|
501
|
+
if (!gitDir) return null;
|
|
441
502
|
return path.join(gitDir, "hooks");
|
|
442
503
|
}
|
|
443
504
|
function installHook(dir) {
|
|
@@ -537,48 +598,79 @@ function hookBranchCommand(branch, options) {
|
|
|
537
598
|
//#endregion
|
|
538
599
|
//#region src/cli.ts
|
|
539
600
|
const pkg = createRequire(import.meta.url)("../package.json");
|
|
601
|
+
/**
|
|
602
|
+
* Check whether a directory contains any `.env.*` source files
|
|
603
|
+
* (i.e. files that dotswitch would operate on, excluding standard non-source files).
|
|
604
|
+
*/
|
|
605
|
+
function hasEnvFiles(dir) {
|
|
606
|
+
try {
|
|
607
|
+
return fs.readdirSync(dir).some((name) => name.startsWith(".env.") && !EXCLUDED_ENV_FILES.has(name));
|
|
608
|
+
} catch {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Resolve the effective path for a command.
|
|
614
|
+
* - No --path given: if in a worktree with local env files, operate locally;
|
|
615
|
+
* otherwise resolve to the main repo root.
|
|
616
|
+
* - Explicit --path: rebase it relative to the main repo when in a worktree,
|
|
617
|
+
* so glob patterns like "./apps/*" expand against the main repo.
|
|
618
|
+
*/
|
|
619
|
+
function resolveCommandPath(explicitPath) {
|
|
620
|
+
const cwd = process.cwd();
|
|
621
|
+
const projectRoot = resolveProjectRoot(cwd);
|
|
622
|
+
if (!explicitPath) {
|
|
623
|
+
if (projectRoot !== cwd && hasEnvFiles(cwd)) return cwd;
|
|
624
|
+
return projectRoot;
|
|
625
|
+
}
|
|
626
|
+
if (projectRoot === cwd) return explicitPath;
|
|
627
|
+
const absolute = path.resolve(explicitPath);
|
|
628
|
+
const relative = path.relative(cwd, absolute);
|
|
629
|
+
return path.resolve(projectRoot, relative);
|
|
630
|
+
}
|
|
540
631
|
const program = new Command();
|
|
541
632
|
program.name("dotswitch").description("Quickly switch between .env files").version(pkg.version);
|
|
542
|
-
program.command("use [env]").description("Switch to a .env.<env> file (interactive if no env given)").option("-f, --force", "skip confirmation if already active", false).option("--no-backup", "skip .env.local backup").option("-n, --dry-run", "show what would happen without making changes", false).option("-p, --path <dir>", "project directory"
|
|
633
|
+
program.command("use [env]").description("Switch to a .env.<env> file (interactive if no env given)").option("-f, --force", "skip confirmation if already active", false).option("--no-backup", "skip .env.local backup").option("-n, --dry-run", "show what would happen without making changes", false).option("-p, --path <dir>", "project directory").option("--hook-branch <branch>", "internal: auto-switch by branch name").action(async (env, opts) => {
|
|
634
|
+
const projectPath = resolveCommandPath(opts.path);
|
|
543
635
|
if (opts.hookBranch) {
|
|
544
|
-
hookBranchCommand(opts.hookBranch, { path:
|
|
636
|
+
hookBranchCommand(opts.hookBranch, { path: projectPath });
|
|
545
637
|
return;
|
|
546
638
|
}
|
|
547
639
|
await useCommand(env, {
|
|
548
640
|
force: opts.force,
|
|
549
641
|
backup: opts.backup,
|
|
550
642
|
dryRun: opts.dryRun,
|
|
551
|
-
path:
|
|
643
|
+
path: projectPath
|
|
552
644
|
});
|
|
553
645
|
});
|
|
554
|
-
program.command("ls").description("List available .env.* files").option("-p, --path <dir>", "project directory"
|
|
646
|
+
program.command("ls").description("List available .env.* files").option("-p, --path <dir>", "project directory").option("--json", "output as JSON", false).action((opts) => {
|
|
555
647
|
lsCommand({
|
|
556
|
-
path: opts.path,
|
|
648
|
+
path: resolveCommandPath(opts.path),
|
|
557
649
|
json: opts.json
|
|
558
650
|
});
|
|
559
651
|
});
|
|
560
|
-
program.command("current").description("Show the currently active environment").option("-p, --path <dir>", "project directory"
|
|
652
|
+
program.command("current").description("Show the currently active environment").option("-p, --path <dir>", "project directory").option("--json", "output as JSON", false).action((opts) => {
|
|
561
653
|
currentCommand({
|
|
562
|
-
path: opts.path,
|
|
654
|
+
path: resolveCommandPath(opts.path),
|
|
563
655
|
json: opts.json
|
|
564
656
|
});
|
|
565
657
|
});
|
|
566
|
-
program.command("restore").description("Restore .env.local from the backup file").option("-p, --path <dir>", "project directory"
|
|
567
|
-
restoreCommand({ path: opts.path });
|
|
658
|
+
program.command("restore").description("Restore .env.local from the backup file").option("-p, --path <dir>", "project directory").action((opts) => {
|
|
659
|
+
restoreCommand({ path: resolveCommandPath(opts.path) });
|
|
568
660
|
});
|
|
569
|
-
program.command("diff <env1> [env2]").description("Compare keys between two env files (defaults: .env.local vs env1)").option("-p, --path <dir>", "project directory"
|
|
661
|
+
program.command("diff <env1> [env2]").description("Compare keys between two env files (defaults: .env.local vs env1)").option("-p, --path <dir>", "project directory").option("--show-values", "show actual values in the diff", false).option("--json", "output as JSON", false).action((env1, env2, opts) => {
|
|
570
662
|
diffCommand(env1, env2, {
|
|
571
|
-
path: opts.path,
|
|
663
|
+
path: resolveCommandPath(opts.path),
|
|
572
664
|
showValues: opts.showValues,
|
|
573
665
|
json: opts.json
|
|
574
666
|
});
|
|
575
667
|
});
|
|
576
668
|
const hookCmd = program.command("hook").description("Manage git post-checkout hook for auto-switching");
|
|
577
|
-
hookCmd.command("install").description("Install the post-checkout git hook").option("-p, --path <dir>", "project directory"
|
|
578
|
-
hookInstallCommand({ path: opts.path });
|
|
669
|
+
hookCmd.command("install").description("Install the post-checkout git hook").option("-p, --path <dir>", "project directory").action((opts) => {
|
|
670
|
+
hookInstallCommand({ path: resolveCommandPath(opts.path) });
|
|
579
671
|
});
|
|
580
|
-
hookCmd.command("remove").description("Remove the post-checkout git hook").option("-p, --path <dir>", "project directory"
|
|
581
|
-
hookRemoveCommand({ path: opts.path });
|
|
672
|
+
hookCmd.command("remove").description("Remove the post-checkout git hook").option("-p, --path <dir>", "project directory").action((opts) => {
|
|
673
|
+
hookRemoveCommand({ path: resolveCommandPath(opts.path) });
|
|
582
674
|
});
|
|
583
675
|
program.parse();
|
|
584
676
|
|
package/dist/index.cjs
CHANGED
|
@@ -32,6 +32,7 @@ let node_path = require("node:path");
|
|
|
32
32
|
node_path = __toESM(node_path);
|
|
33
33
|
let picocolors = require("picocolors");
|
|
34
34
|
picocolors = __toESM(picocolors);
|
|
35
|
+
let node_child_process = require("node:child_process");
|
|
35
36
|
|
|
36
37
|
//#region src/lib/constants.ts
|
|
37
38
|
const TRACKER_PREFIX = "# dotswitch:";
|
|
@@ -80,7 +81,7 @@ const logger = {
|
|
|
80
81
|
console.log(picocolors.default.cyan(message));
|
|
81
82
|
},
|
|
82
83
|
warn(message) {
|
|
83
|
-
console.
|
|
84
|
+
console.error(picocolors.default.yellow(`⚠ ${message}`));
|
|
84
85
|
},
|
|
85
86
|
error(message) {
|
|
86
87
|
console.error(picocolors.default.red(`✗ ${message}`));
|
|
@@ -229,6 +230,66 @@ function diffEnvMaps(from, to) {
|
|
|
229
230
|
};
|
|
230
231
|
}
|
|
231
232
|
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/lib/git.ts
|
|
235
|
+
function git(dir, ...args) {
|
|
236
|
+
try {
|
|
237
|
+
return (0, node_child_process.execFileSync)("git", [
|
|
238
|
+
"-C",
|
|
239
|
+
dir,
|
|
240
|
+
...args
|
|
241
|
+
], {
|
|
242
|
+
encoding: "utf-8",
|
|
243
|
+
stdio: [
|
|
244
|
+
"pipe",
|
|
245
|
+
"pipe",
|
|
246
|
+
"pipe"
|
|
247
|
+
]
|
|
248
|
+
}).trim();
|
|
249
|
+
} catch (error) {
|
|
250
|
+
const stderr = error instanceof Error && "stderr" in error ? String(error.stderr).trim() : "";
|
|
251
|
+
if (stderr) logger.warn(`git ${args.join(" ")}: ${stderr}`);
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Resolve the common (shared) git directory for a project.
|
|
257
|
+
* - Regular repos: returns <dir>/.git
|
|
258
|
+
* - Worktrees: returns the main repo's .git directory
|
|
259
|
+
* - Non-git directories: returns null
|
|
260
|
+
*/
|
|
261
|
+
function resolveCommonGitDir(dir) {
|
|
262
|
+
const gitPath = node_path.default.join(dir, ".git");
|
|
263
|
+
try {
|
|
264
|
+
if (node_fs.default.statSync(gitPath).isDirectory()) return gitPath;
|
|
265
|
+
} catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
const result = git(dir, "rev-parse", "--git-common-dir");
|
|
269
|
+
if (!result) return null;
|
|
270
|
+
return node_path.default.isAbsolute(result) ? result : node_path.default.resolve(dir, result);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Resolve the project root directory.
|
|
274
|
+
* In a worktree, this returns the main repo's root (parent of its .git dir).
|
|
275
|
+
* Otherwise returns the given directory as-is.
|
|
276
|
+
*/
|
|
277
|
+
function resolveProjectRoot(dir) {
|
|
278
|
+
const gitPath = node_path.default.join(dir, ".git");
|
|
279
|
+
let stats;
|
|
280
|
+
try {
|
|
281
|
+
stats = node_fs.default.statSync(gitPath);
|
|
282
|
+
} catch {
|
|
283
|
+
return dir;
|
|
284
|
+
}
|
|
285
|
+
if (stats.isDirectory()) return dir;
|
|
286
|
+
if (stats.isFile()) {
|
|
287
|
+
const commonGitDir = resolveCommonGitDir(dir);
|
|
288
|
+
if (commonGitDir) return node_path.default.dirname(commonGitDir);
|
|
289
|
+
}
|
|
290
|
+
return dir;
|
|
291
|
+
}
|
|
292
|
+
|
|
232
293
|
//#endregion
|
|
233
294
|
exports.addTrackerHeader = addTrackerHeader;
|
|
234
295
|
exports.backupEnvLocal = backupEnvLocal;
|
|
@@ -242,5 +303,7 @@ exports.loadConfig = loadConfig;
|
|
|
242
303
|
exports.parseEnvContent = parseEnvContent;
|
|
243
304
|
exports.parseTrackerHeader = parseTrackerHeader;
|
|
244
305
|
exports.removeTrackerHeader = removeTrackerHeader;
|
|
306
|
+
exports.resolveCommonGitDir = resolveCommonGitDir;
|
|
307
|
+
exports.resolveProjectRoot = resolveProjectRoot;
|
|
245
308
|
exports.restoreEnvLocal = restoreEnvLocal;
|
|
246
309
|
exports.switchEnv = switchEnv;
|
package/dist/index.d.cts
CHANGED
|
@@ -66,4 +66,19 @@ interface EnvDiff {
|
|
|
66
66
|
*/
|
|
67
67
|
declare function diffEnvMaps(from: Map<string, string>, to: Map<string, string>): EnvDiff;
|
|
68
68
|
//#endregion
|
|
69
|
-
|
|
69
|
+
//#region src/lib/git.d.ts
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the common (shared) git directory for a project.
|
|
72
|
+
* - Regular repos: returns <dir>/.git
|
|
73
|
+
* - Worktrees: returns the main repo's .git directory
|
|
74
|
+
* - Non-git directories: returns null
|
|
75
|
+
*/
|
|
76
|
+
declare function resolveCommonGitDir(dir: string): string | null;
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the project root directory.
|
|
79
|
+
* In a worktree, this returns the main repo's root (parent of its .git dir).
|
|
80
|
+
* Otherwise returns the given directory as-is.
|
|
81
|
+
*/
|
|
82
|
+
declare function resolveProjectRoot(dir: string): string;
|
|
83
|
+
//#endregion
|
|
84
|
+
export { type CommonOptions, type DotswitchConfig, type EnvDiff, type EnvFile, type UseOptions, addTrackerHeader, backupEnvLocal, createTrackerHeader, diffEnvMaps, getActiveEnv, getBackupFile, getTargetFile, listEnvFiles, loadConfig, parseEnvContent, parseTrackerHeader, removeTrackerHeader, resolveCommonGitDir, resolveProjectRoot, restoreEnvLocal, switchEnv };
|
package/dist/index.d.mts
CHANGED
|
@@ -66,4 +66,19 @@ interface EnvDiff {
|
|
|
66
66
|
*/
|
|
67
67
|
declare function diffEnvMaps(from: Map<string, string>, to: Map<string, string>): EnvDiff;
|
|
68
68
|
//#endregion
|
|
69
|
-
|
|
69
|
+
//#region src/lib/git.d.ts
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the common (shared) git directory for a project.
|
|
72
|
+
* - Regular repos: returns <dir>/.git
|
|
73
|
+
* - Worktrees: returns the main repo's .git directory
|
|
74
|
+
* - Non-git directories: returns null
|
|
75
|
+
*/
|
|
76
|
+
declare function resolveCommonGitDir(dir: string): string | null;
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the project root directory.
|
|
79
|
+
* In a worktree, this returns the main repo's root (parent of its .git dir).
|
|
80
|
+
* Otherwise returns the given directory as-is.
|
|
81
|
+
*/
|
|
82
|
+
declare function resolveProjectRoot(dir: string): string;
|
|
83
|
+
//#endregion
|
|
84
|
+
export { type CommonOptions, type DotswitchConfig, type EnvDiff, type EnvFile, type UseOptions, addTrackerHeader, backupEnvLocal, createTrackerHeader, diffEnvMaps, getActiveEnv, getBackupFile, getTargetFile, listEnvFiles, loadConfig, parseEnvContent, parseTrackerHeader, removeTrackerHeader, resolveCommonGitDir, resolveProjectRoot, restoreEnvLocal, switchEnv };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import pc from "picocolors";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
4
5
|
|
|
5
6
|
//#region src/lib/constants.ts
|
|
6
7
|
const TRACKER_PREFIX = "# dotswitch:";
|
|
@@ -49,7 +50,7 @@ const logger = {
|
|
|
49
50
|
console.log(pc.cyan(message));
|
|
50
51
|
},
|
|
51
52
|
warn(message) {
|
|
52
|
-
console.
|
|
53
|
+
console.error(pc.yellow(`⚠ ${message}`));
|
|
53
54
|
},
|
|
54
55
|
error(message) {
|
|
55
56
|
console.error(pc.red(`✗ ${message}`));
|
|
@@ -199,4 +200,64 @@ function diffEnvMaps(from, to) {
|
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
//#endregion
|
|
202
|
-
|
|
203
|
+
//#region src/lib/git.ts
|
|
204
|
+
function git(dir, ...args) {
|
|
205
|
+
try {
|
|
206
|
+
return execFileSync("git", [
|
|
207
|
+
"-C",
|
|
208
|
+
dir,
|
|
209
|
+
...args
|
|
210
|
+
], {
|
|
211
|
+
encoding: "utf-8",
|
|
212
|
+
stdio: [
|
|
213
|
+
"pipe",
|
|
214
|
+
"pipe",
|
|
215
|
+
"pipe"
|
|
216
|
+
]
|
|
217
|
+
}).trim();
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const stderr = error instanceof Error && "stderr" in error ? String(error.stderr).trim() : "";
|
|
220
|
+
if (stderr) logger.warn(`git ${args.join(" ")}: ${stderr}`);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Resolve the common (shared) git directory for a project.
|
|
226
|
+
* - Regular repos: returns <dir>/.git
|
|
227
|
+
* - Worktrees: returns the main repo's .git directory
|
|
228
|
+
* - Non-git directories: returns null
|
|
229
|
+
*/
|
|
230
|
+
function resolveCommonGitDir(dir) {
|
|
231
|
+
const gitPath = path.join(dir, ".git");
|
|
232
|
+
try {
|
|
233
|
+
if (fs.statSync(gitPath).isDirectory()) return gitPath;
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const result = git(dir, "rev-parse", "--git-common-dir");
|
|
238
|
+
if (!result) return null;
|
|
239
|
+
return path.isAbsolute(result) ? result : path.resolve(dir, result);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Resolve the project root directory.
|
|
243
|
+
* In a worktree, this returns the main repo's root (parent of its .git dir).
|
|
244
|
+
* Otherwise returns the given directory as-is.
|
|
245
|
+
*/
|
|
246
|
+
function resolveProjectRoot(dir) {
|
|
247
|
+
const gitPath = path.join(dir, ".git");
|
|
248
|
+
let stats;
|
|
249
|
+
try {
|
|
250
|
+
stats = fs.statSync(gitPath);
|
|
251
|
+
} catch {
|
|
252
|
+
return dir;
|
|
253
|
+
}
|
|
254
|
+
if (stats.isDirectory()) return dir;
|
|
255
|
+
if (stats.isFile()) {
|
|
256
|
+
const commonGitDir = resolveCommonGitDir(dir);
|
|
257
|
+
if (commonGitDir) return path.dirname(commonGitDir);
|
|
258
|
+
}
|
|
259
|
+
return dir;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
//#endregion
|
|
263
|
+
export { addTrackerHeader, backupEnvLocal, createTrackerHeader, diffEnvMaps, getActiveEnv, getBackupFile, getTargetFile, listEnvFiles, loadConfig, parseEnvContent, parseTrackerHeader, removeTrackerHeader, resolveCommonGitDir, resolveProjectRoot, restoreEnvLocal, switchEnv };
|