dotswitch 1.0.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/LICENSE +21 -0
- package/README.md +242 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +586 -0
- package/dist/index.cjs +246 -0
- package/dist/index.d.cts +69 -0
- package/dist/index.d.mts +69 -0
- package/dist/index.mjs +202 -0
- package/package.json +79 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
14
|
+
__defProp(to, key, {
|
|
15
|
+
get: ((k) => from[k]).bind(null, key),
|
|
16
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
24
|
+
value: mod,
|
|
25
|
+
enumerable: true
|
|
26
|
+
}) : target, mod));
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
let node_fs = require("node:fs");
|
|
30
|
+
node_fs = __toESM(node_fs);
|
|
31
|
+
let node_path = require("node:path");
|
|
32
|
+
node_path = __toESM(node_path);
|
|
33
|
+
let picocolors = require("picocolors");
|
|
34
|
+
picocolors = __toESM(picocolors);
|
|
35
|
+
|
|
36
|
+
//#region src/lib/constants.ts
|
|
37
|
+
const TRACKER_PREFIX = "# dotswitch:";
|
|
38
|
+
const EXCLUDED_ENV_FILES = new Set([
|
|
39
|
+
".env",
|
|
40
|
+
".env.local",
|
|
41
|
+
".env.local.backup",
|
|
42
|
+
".env.example"
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/lib/tracker.ts
|
|
47
|
+
function createTrackerHeader(env) {
|
|
48
|
+
return `${TRACKER_PREFIX}${env}`;
|
|
49
|
+
}
|
|
50
|
+
function parseTrackerHeader(content) {
|
|
51
|
+
const firstLine = content.split("\n")[0];
|
|
52
|
+
if (firstLine?.startsWith(TRACKER_PREFIX)) return firstLine.slice(TRACKER_PREFIX.length).trim();
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
function addTrackerHeader(content, env) {
|
|
56
|
+
const header = createTrackerHeader(env);
|
|
57
|
+
if (parseTrackerHeader(content) !== null) {
|
|
58
|
+
const lines = content.split("\n");
|
|
59
|
+
lines[0] = header;
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
return `${header}\n${content}`;
|
|
63
|
+
}
|
|
64
|
+
function removeTrackerHeader(content) {
|
|
65
|
+
if (parseTrackerHeader(content) !== null) {
|
|
66
|
+
const lines = content.split("\n");
|
|
67
|
+
lines.shift();
|
|
68
|
+
return lines.join("\n");
|
|
69
|
+
}
|
|
70
|
+
return content;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/lib/logger.ts
|
|
75
|
+
const logger = {
|
|
76
|
+
success(message) {
|
|
77
|
+
console.log(picocolors.default.green(`✓ ${message}`));
|
|
78
|
+
},
|
|
79
|
+
info(message) {
|
|
80
|
+
console.log(picocolors.default.cyan(message));
|
|
81
|
+
},
|
|
82
|
+
warn(message) {
|
|
83
|
+
console.log(picocolors.default.yellow(`⚠ ${message}`));
|
|
84
|
+
},
|
|
85
|
+
error(message) {
|
|
86
|
+
console.error(picocolors.default.red(`✗ ${message}`));
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/lib/config.ts
|
|
92
|
+
const CONFIG_FILENAME = ".dotswitchrc.json";
|
|
93
|
+
const DEFAULT_CONFIG = {
|
|
94
|
+
target: ".env.local",
|
|
95
|
+
exclude: [],
|
|
96
|
+
hooks: {}
|
|
97
|
+
};
|
|
98
|
+
function loadConfig(dir, fsModule = node_fs.default) {
|
|
99
|
+
const configPath = node_path.default.join(dir, CONFIG_FILENAME);
|
|
100
|
+
try {
|
|
101
|
+
if (fsModule.existsSync(configPath)) {
|
|
102
|
+
const raw = JSON.parse(fsModule.readFileSync(configPath, "utf-8"));
|
|
103
|
+
return {
|
|
104
|
+
target: raw.target ?? DEFAULT_CONFIG.target,
|
|
105
|
+
exclude: raw.exclude ?? DEFAULT_CONFIG.exclude,
|
|
106
|
+
hooks: raw.hooks ?? DEFAULT_CONFIG.hooks
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
} catch {}
|
|
110
|
+
return { ...DEFAULT_CONFIG };
|
|
111
|
+
}
|
|
112
|
+
function getTargetFile(config) {
|
|
113
|
+
return config.target;
|
|
114
|
+
}
|
|
115
|
+
function getBackupFile(config) {
|
|
116
|
+
return `${config.target}.backup`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/lib/env.ts
|
|
121
|
+
function resolveConfig(dir, config, fsModule) {
|
|
122
|
+
return config ?? loadConfig(dir, fsModule);
|
|
123
|
+
}
|
|
124
|
+
function listEnvFiles(dir, fsModule = node_fs.default, config) {
|
|
125
|
+
const cfg = resolveConfig(dir, config, fsModule);
|
|
126
|
+
const entries = fsModule.readdirSync(dir);
|
|
127
|
+
const activeEnv = getActiveEnv(dir, fsModule, cfg);
|
|
128
|
+
const target = getTargetFile(cfg);
|
|
129
|
+
const backup = getBackupFile(cfg);
|
|
130
|
+
const excluded = new Set([
|
|
131
|
+
...EXCLUDED_ENV_FILES,
|
|
132
|
+
...cfg.exclude,
|
|
133
|
+
target,
|
|
134
|
+
backup
|
|
135
|
+
]);
|
|
136
|
+
return entries.filter((name) => name.startsWith(".env.") && !excluded.has(name)).sort().map((name) => {
|
|
137
|
+
const env = name.replace(/^\.env\./, "");
|
|
138
|
+
return {
|
|
139
|
+
name,
|
|
140
|
+
env,
|
|
141
|
+
path: node_path.default.join(dir, name),
|
|
142
|
+
active: env === activeEnv
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
function getActiveEnv(dir, fsModule = node_fs.default, config) {
|
|
147
|
+
const cfg = resolveConfig(dir, config, fsModule);
|
|
148
|
+
const targetPath = node_path.default.join(dir, getTargetFile(cfg));
|
|
149
|
+
try {
|
|
150
|
+
return parseTrackerHeader(fsModule.readFileSync(targetPath, "utf-8"));
|
|
151
|
+
} catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function backupEnvLocal(dir, fsModule = node_fs.default, config) {
|
|
156
|
+
const cfg = resolveConfig(dir, config, fsModule);
|
|
157
|
+
const target = getTargetFile(cfg);
|
|
158
|
+
const targetPath = node_path.default.join(dir, target);
|
|
159
|
+
const backupPath = node_path.default.join(dir, getBackupFile(cfg));
|
|
160
|
+
try {
|
|
161
|
+
if (fsModule.existsSync(targetPath)) {
|
|
162
|
+
fsModule.copyFileSync(targetPath, backupPath);
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
logger.warn(`Failed to back up ${target}: ${error instanceof Error ? error.message : String(error)}`);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function restoreEnvLocal(dir, fsModule = node_fs.default, config) {
|
|
172
|
+
const cfg = resolveConfig(dir, config, fsModule);
|
|
173
|
+
const target = getTargetFile(cfg);
|
|
174
|
+
const backup = getBackupFile(cfg);
|
|
175
|
+
const backupPath = node_path.default.join(dir, backup);
|
|
176
|
+
const targetPath = node_path.default.join(dir, target);
|
|
177
|
+
if (!fsModule.existsSync(backupPath)) throw new Error(`No backup file found (${backup})`);
|
|
178
|
+
fsModule.copyFileSync(backupPath, targetPath);
|
|
179
|
+
}
|
|
180
|
+
function switchEnv(dir, env, options = { backup: true }, fsModule = node_fs.default, config) {
|
|
181
|
+
const cfg = resolveConfig(dir, config, fsModule);
|
|
182
|
+
const sourcePath = node_path.default.join(dir, `.env.${env}`);
|
|
183
|
+
const targetPath = node_path.default.join(dir, getTargetFile(cfg));
|
|
184
|
+
if (!fsModule.existsSync(sourcePath)) throw new Error(`Environment file .env.${env} does not exist`);
|
|
185
|
+
if (options.backup) backupEnvLocal(dir, fsModule, cfg);
|
|
186
|
+
const tracked = addTrackerHeader(fsModule.readFileSync(sourcePath, "utf-8"), env);
|
|
187
|
+
fsModule.writeFileSync(targetPath, tracked, "utf-8");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/lib/parser.ts
|
|
192
|
+
/**
|
|
193
|
+
* Parse a .env file into a key-value map.
|
|
194
|
+
* Strips comments (lines starting with #) and empty lines.
|
|
195
|
+
*/
|
|
196
|
+
function parseEnvContent(content) {
|
|
197
|
+
const result = /* @__PURE__ */ new Map();
|
|
198
|
+
for (const line of content.split("\n")) {
|
|
199
|
+
const trimmed = line.trim();
|
|
200
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
201
|
+
const eqIndex = trimmed.indexOf("=");
|
|
202
|
+
if (eqIndex === -1) continue;
|
|
203
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
204
|
+
const value = trimmed.slice(eqIndex + 1).trim();
|
|
205
|
+
if (key) result.set(key, value);
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Compute the diff between two parsed env maps.
|
|
211
|
+
* "added" = keys in `to` but not in `from`.
|
|
212
|
+
* "removed" = keys in `from` but not in `to`.
|
|
213
|
+
* "changed" = keys in both with different values.
|
|
214
|
+
*/
|
|
215
|
+
function diffEnvMaps(from, to) {
|
|
216
|
+
const added = [];
|
|
217
|
+
const removed = [];
|
|
218
|
+
const changed = [];
|
|
219
|
+
const unchanged = [];
|
|
220
|
+
for (const key of from.keys()) if (!to.has(key)) removed.push(key);
|
|
221
|
+
else if (from.get(key) !== to.get(key)) changed.push(key);
|
|
222
|
+
else unchanged.push(key);
|
|
223
|
+
for (const key of to.keys()) if (!from.has(key)) added.push(key);
|
|
224
|
+
return {
|
|
225
|
+
added: added.sort(),
|
|
226
|
+
removed: removed.sort(),
|
|
227
|
+
changed: changed.sort(),
|
|
228
|
+
unchanged: unchanged.sort()
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
//#endregion
|
|
233
|
+
exports.addTrackerHeader = addTrackerHeader;
|
|
234
|
+
exports.backupEnvLocal = backupEnvLocal;
|
|
235
|
+
exports.createTrackerHeader = createTrackerHeader;
|
|
236
|
+
exports.diffEnvMaps = diffEnvMaps;
|
|
237
|
+
exports.getActiveEnv = getActiveEnv;
|
|
238
|
+
exports.getBackupFile = getBackupFile;
|
|
239
|
+
exports.getTargetFile = getTargetFile;
|
|
240
|
+
exports.listEnvFiles = listEnvFiles;
|
|
241
|
+
exports.loadConfig = loadConfig;
|
|
242
|
+
exports.parseEnvContent = parseEnvContent;
|
|
243
|
+
exports.parseTrackerHeader = parseTrackerHeader;
|
|
244
|
+
exports.removeTrackerHeader = removeTrackerHeader;
|
|
245
|
+
exports.restoreEnvLocal = restoreEnvLocal;
|
|
246
|
+
exports.switchEnv = switchEnv;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/config.d.ts
|
|
4
|
+
interface DotswitchConfig {
|
|
5
|
+
/** Target file to write to (default: ".env.local") */
|
|
6
|
+
target: string;
|
|
7
|
+
/** File patterns to exclude from env listing */
|
|
8
|
+
exclude: string[];
|
|
9
|
+
/** Branch-to-env mappings for git hook auto-switching */
|
|
10
|
+
hooks: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
declare function loadConfig(dir: string, fsModule?: typeof fs): DotswitchConfig;
|
|
13
|
+
declare function getTargetFile(config: DotswitchConfig): string;
|
|
14
|
+
declare function getBackupFile(config: DotswitchConfig): string;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/types.d.ts
|
|
17
|
+
interface EnvFile {
|
|
18
|
+
name: string;
|
|
19
|
+
env: string;
|
|
20
|
+
path: string;
|
|
21
|
+
active: boolean;
|
|
22
|
+
}
|
|
23
|
+
interface UseOptions {
|
|
24
|
+
force: boolean;
|
|
25
|
+
backup: boolean;
|
|
26
|
+
dryRun: boolean;
|
|
27
|
+
path: string;
|
|
28
|
+
}
|
|
29
|
+
interface CommonOptions {
|
|
30
|
+
path: string;
|
|
31
|
+
json: boolean;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/lib/env.d.ts
|
|
35
|
+
declare function listEnvFiles(dir: string, fsModule?: typeof fs, config?: DotswitchConfig): EnvFile[];
|
|
36
|
+
declare function getActiveEnv(dir: string, fsModule?: typeof fs, config?: DotswitchConfig): string | null;
|
|
37
|
+
declare function backupEnvLocal(dir: string, fsModule?: typeof fs, config?: DotswitchConfig): boolean;
|
|
38
|
+
declare function restoreEnvLocal(dir: string, fsModule?: typeof fs, config?: DotswitchConfig): void;
|
|
39
|
+
declare function switchEnv(dir: string, env: string, options?: {
|
|
40
|
+
backup: boolean;
|
|
41
|
+
}, fsModule?: typeof fs, config?: DotswitchConfig): void;
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/lib/tracker.d.ts
|
|
44
|
+
declare function createTrackerHeader(env: string): string;
|
|
45
|
+
declare function parseTrackerHeader(content: string): string | null;
|
|
46
|
+
declare function addTrackerHeader(content: string, env: string): string;
|
|
47
|
+
declare function removeTrackerHeader(content: string): string;
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/lib/parser.d.ts
|
|
50
|
+
/**
|
|
51
|
+
* Parse a .env file into a key-value map.
|
|
52
|
+
* Strips comments (lines starting with #) and empty lines.
|
|
53
|
+
*/
|
|
54
|
+
declare function parseEnvContent(content: string): Map<string, string>;
|
|
55
|
+
interface EnvDiff {
|
|
56
|
+
added: string[];
|
|
57
|
+
removed: string[];
|
|
58
|
+
changed: string[];
|
|
59
|
+
unchanged: string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Compute the diff between two parsed env maps.
|
|
63
|
+
* "added" = keys in `to` but not in `from`.
|
|
64
|
+
* "removed" = keys in `from` but not in `to`.
|
|
65
|
+
* "changed" = keys in both with different values.
|
|
66
|
+
*/
|
|
67
|
+
declare function diffEnvMaps(from: Map<string, string>, to: Map<string, string>): EnvDiff;
|
|
68
|
+
//#endregion
|
|
69
|
+
export { type CommonOptions, type DotswitchConfig, type EnvDiff, type EnvFile, type UseOptions, addTrackerHeader, backupEnvLocal, createTrackerHeader, diffEnvMaps, getActiveEnv, getBackupFile, getTargetFile, listEnvFiles, loadConfig, parseEnvContent, parseTrackerHeader, removeTrackerHeader, restoreEnvLocal, switchEnv };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/config.d.ts
|
|
4
|
+
interface DotswitchConfig {
|
|
5
|
+
/** Target file to write to (default: ".env.local") */
|
|
6
|
+
target: string;
|
|
7
|
+
/** File patterns to exclude from env listing */
|
|
8
|
+
exclude: string[];
|
|
9
|
+
/** Branch-to-env mappings for git hook auto-switching */
|
|
10
|
+
hooks: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
declare function loadConfig(dir: string, fsModule?: typeof fs): DotswitchConfig;
|
|
13
|
+
declare function getTargetFile(config: DotswitchConfig): string;
|
|
14
|
+
declare function getBackupFile(config: DotswitchConfig): string;
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/types.d.ts
|
|
17
|
+
interface EnvFile {
|
|
18
|
+
name: string;
|
|
19
|
+
env: string;
|
|
20
|
+
path: string;
|
|
21
|
+
active: boolean;
|
|
22
|
+
}
|
|
23
|
+
interface UseOptions {
|
|
24
|
+
force: boolean;
|
|
25
|
+
backup: boolean;
|
|
26
|
+
dryRun: boolean;
|
|
27
|
+
path: string;
|
|
28
|
+
}
|
|
29
|
+
interface CommonOptions {
|
|
30
|
+
path: string;
|
|
31
|
+
json: boolean;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/lib/env.d.ts
|
|
35
|
+
declare function listEnvFiles(dir: string, fsModule?: typeof fs, config?: DotswitchConfig): EnvFile[];
|
|
36
|
+
declare function getActiveEnv(dir: string, fsModule?: typeof fs, config?: DotswitchConfig): string | null;
|
|
37
|
+
declare function backupEnvLocal(dir: string, fsModule?: typeof fs, config?: DotswitchConfig): boolean;
|
|
38
|
+
declare function restoreEnvLocal(dir: string, fsModule?: typeof fs, config?: DotswitchConfig): void;
|
|
39
|
+
declare function switchEnv(dir: string, env: string, options?: {
|
|
40
|
+
backup: boolean;
|
|
41
|
+
}, fsModule?: typeof fs, config?: DotswitchConfig): void;
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/lib/tracker.d.ts
|
|
44
|
+
declare function createTrackerHeader(env: string): string;
|
|
45
|
+
declare function parseTrackerHeader(content: string): string | null;
|
|
46
|
+
declare function addTrackerHeader(content: string, env: string): string;
|
|
47
|
+
declare function removeTrackerHeader(content: string): string;
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/lib/parser.d.ts
|
|
50
|
+
/**
|
|
51
|
+
* Parse a .env file into a key-value map.
|
|
52
|
+
* Strips comments (lines starting with #) and empty lines.
|
|
53
|
+
*/
|
|
54
|
+
declare function parseEnvContent(content: string): Map<string, string>;
|
|
55
|
+
interface EnvDiff {
|
|
56
|
+
added: string[];
|
|
57
|
+
removed: string[];
|
|
58
|
+
changed: string[];
|
|
59
|
+
unchanged: string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Compute the diff between two parsed env maps.
|
|
63
|
+
* "added" = keys in `to` but not in `from`.
|
|
64
|
+
* "removed" = keys in `from` but not in `to`.
|
|
65
|
+
* "changed" = keys in both with different values.
|
|
66
|
+
*/
|
|
67
|
+
declare function diffEnvMaps(from: Map<string, string>, to: Map<string, string>): EnvDiff;
|
|
68
|
+
//#endregion
|
|
69
|
+
export { type CommonOptions, type DotswitchConfig, type EnvDiff, type EnvFile, type UseOptions, addTrackerHeader, backupEnvLocal, createTrackerHeader, diffEnvMaps, getActiveEnv, getBackupFile, getTargetFile, listEnvFiles, loadConfig, parseEnvContent, parseTrackerHeader, removeTrackerHeader, restoreEnvLocal, switchEnv };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
|
|
5
|
+
//#region src/lib/constants.ts
|
|
6
|
+
const TRACKER_PREFIX = "# dotswitch:";
|
|
7
|
+
const EXCLUDED_ENV_FILES = new Set([
|
|
8
|
+
".env",
|
|
9
|
+
".env.local",
|
|
10
|
+
".env.local.backup",
|
|
11
|
+
".env.example"
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/lib/tracker.ts
|
|
16
|
+
function createTrackerHeader(env) {
|
|
17
|
+
return `${TRACKER_PREFIX}${env}`;
|
|
18
|
+
}
|
|
19
|
+
function parseTrackerHeader(content) {
|
|
20
|
+
const firstLine = content.split("\n")[0];
|
|
21
|
+
if (firstLine?.startsWith(TRACKER_PREFIX)) return firstLine.slice(TRACKER_PREFIX.length).trim();
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
function addTrackerHeader(content, env) {
|
|
25
|
+
const header = createTrackerHeader(env);
|
|
26
|
+
if (parseTrackerHeader(content) !== null) {
|
|
27
|
+
const lines = content.split("\n");
|
|
28
|
+
lines[0] = header;
|
|
29
|
+
return lines.join("\n");
|
|
30
|
+
}
|
|
31
|
+
return `${header}\n${content}`;
|
|
32
|
+
}
|
|
33
|
+
function removeTrackerHeader(content) {
|
|
34
|
+
if (parseTrackerHeader(content) !== null) {
|
|
35
|
+
const lines = content.split("\n");
|
|
36
|
+
lines.shift();
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
return content;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/lib/logger.ts
|
|
44
|
+
const logger = {
|
|
45
|
+
success(message) {
|
|
46
|
+
console.log(pc.green(`✓ ${message}`));
|
|
47
|
+
},
|
|
48
|
+
info(message) {
|
|
49
|
+
console.log(pc.cyan(message));
|
|
50
|
+
},
|
|
51
|
+
warn(message) {
|
|
52
|
+
console.log(pc.yellow(`⚠ ${message}`));
|
|
53
|
+
},
|
|
54
|
+
error(message) {
|
|
55
|
+
console.error(pc.red(`✗ ${message}`));
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/lib/config.ts
|
|
61
|
+
const CONFIG_FILENAME = ".dotswitchrc.json";
|
|
62
|
+
const DEFAULT_CONFIG = {
|
|
63
|
+
target: ".env.local",
|
|
64
|
+
exclude: [],
|
|
65
|
+
hooks: {}
|
|
66
|
+
};
|
|
67
|
+
function loadConfig(dir, fsModule = fs) {
|
|
68
|
+
const configPath = path.join(dir, CONFIG_FILENAME);
|
|
69
|
+
try {
|
|
70
|
+
if (fsModule.existsSync(configPath)) {
|
|
71
|
+
const raw = JSON.parse(fsModule.readFileSync(configPath, "utf-8"));
|
|
72
|
+
return {
|
|
73
|
+
target: raw.target ?? DEFAULT_CONFIG.target,
|
|
74
|
+
exclude: raw.exclude ?? DEFAULT_CONFIG.exclude,
|
|
75
|
+
hooks: raw.hooks ?? DEFAULT_CONFIG.hooks
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
} catch {}
|
|
79
|
+
return { ...DEFAULT_CONFIG };
|
|
80
|
+
}
|
|
81
|
+
function getTargetFile(config) {
|
|
82
|
+
return config.target;
|
|
83
|
+
}
|
|
84
|
+
function getBackupFile(config) {
|
|
85
|
+
return `${config.target}.backup`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/lib/env.ts
|
|
90
|
+
function resolveConfig(dir, config, fsModule) {
|
|
91
|
+
return config ?? loadConfig(dir, fsModule);
|
|
92
|
+
}
|
|
93
|
+
function listEnvFiles(dir, fsModule = fs, config) {
|
|
94
|
+
const cfg = resolveConfig(dir, config, fsModule);
|
|
95
|
+
const entries = fsModule.readdirSync(dir);
|
|
96
|
+
const activeEnv = getActiveEnv(dir, fsModule, cfg);
|
|
97
|
+
const target = getTargetFile(cfg);
|
|
98
|
+
const backup = getBackupFile(cfg);
|
|
99
|
+
const excluded = new Set([
|
|
100
|
+
...EXCLUDED_ENV_FILES,
|
|
101
|
+
...cfg.exclude,
|
|
102
|
+
target,
|
|
103
|
+
backup
|
|
104
|
+
]);
|
|
105
|
+
return entries.filter((name) => name.startsWith(".env.") && !excluded.has(name)).sort().map((name) => {
|
|
106
|
+
const env = name.replace(/^\.env\./, "");
|
|
107
|
+
return {
|
|
108
|
+
name,
|
|
109
|
+
env,
|
|
110
|
+
path: path.join(dir, name),
|
|
111
|
+
active: env === activeEnv
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
function getActiveEnv(dir, fsModule = fs, config) {
|
|
116
|
+
const cfg = resolveConfig(dir, config, fsModule);
|
|
117
|
+
const targetPath = path.join(dir, getTargetFile(cfg));
|
|
118
|
+
try {
|
|
119
|
+
return parseTrackerHeader(fsModule.readFileSync(targetPath, "utf-8"));
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function backupEnvLocal(dir, fsModule = fs, config) {
|
|
125
|
+
const cfg = resolveConfig(dir, config, fsModule);
|
|
126
|
+
const target = getTargetFile(cfg);
|
|
127
|
+
const targetPath = path.join(dir, target);
|
|
128
|
+
const backupPath = path.join(dir, getBackupFile(cfg));
|
|
129
|
+
try {
|
|
130
|
+
if (fsModule.existsSync(targetPath)) {
|
|
131
|
+
fsModule.copyFileSync(targetPath, backupPath);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
logger.warn(`Failed to back up ${target}: ${error instanceof Error ? error.message : String(error)}`);
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function restoreEnvLocal(dir, fsModule = fs, config) {
|
|
141
|
+
const cfg = resolveConfig(dir, config, fsModule);
|
|
142
|
+
const target = getTargetFile(cfg);
|
|
143
|
+
const backup = getBackupFile(cfg);
|
|
144
|
+
const backupPath = path.join(dir, backup);
|
|
145
|
+
const targetPath = path.join(dir, target);
|
|
146
|
+
if (!fsModule.existsSync(backupPath)) throw new Error(`No backup file found (${backup})`);
|
|
147
|
+
fsModule.copyFileSync(backupPath, targetPath);
|
|
148
|
+
}
|
|
149
|
+
function switchEnv(dir, env, options = { backup: true }, fsModule = fs, config) {
|
|
150
|
+
const cfg = resolveConfig(dir, config, fsModule);
|
|
151
|
+
const sourcePath = path.join(dir, `.env.${env}`);
|
|
152
|
+
const targetPath = path.join(dir, getTargetFile(cfg));
|
|
153
|
+
if (!fsModule.existsSync(sourcePath)) throw new Error(`Environment file .env.${env} does not exist`);
|
|
154
|
+
if (options.backup) backupEnvLocal(dir, fsModule, cfg);
|
|
155
|
+
const tracked = addTrackerHeader(fsModule.readFileSync(sourcePath, "utf-8"), env);
|
|
156
|
+
fsModule.writeFileSync(targetPath, tracked, "utf-8");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/lib/parser.ts
|
|
161
|
+
/**
|
|
162
|
+
* Parse a .env file into a key-value map.
|
|
163
|
+
* Strips comments (lines starting with #) and empty lines.
|
|
164
|
+
*/
|
|
165
|
+
function parseEnvContent(content) {
|
|
166
|
+
const result = /* @__PURE__ */ new Map();
|
|
167
|
+
for (const line of content.split("\n")) {
|
|
168
|
+
const trimmed = line.trim();
|
|
169
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
170
|
+
const eqIndex = trimmed.indexOf("=");
|
|
171
|
+
if (eqIndex === -1) continue;
|
|
172
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
173
|
+
const value = trimmed.slice(eqIndex + 1).trim();
|
|
174
|
+
if (key) result.set(key, value);
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Compute the diff between two parsed env maps.
|
|
180
|
+
* "added" = keys in `to` but not in `from`.
|
|
181
|
+
* "removed" = keys in `from` but not in `to`.
|
|
182
|
+
* "changed" = keys in both with different values.
|
|
183
|
+
*/
|
|
184
|
+
function diffEnvMaps(from, to) {
|
|
185
|
+
const added = [];
|
|
186
|
+
const removed = [];
|
|
187
|
+
const changed = [];
|
|
188
|
+
const unchanged = [];
|
|
189
|
+
for (const key of from.keys()) if (!to.has(key)) removed.push(key);
|
|
190
|
+
else if (from.get(key) !== to.get(key)) changed.push(key);
|
|
191
|
+
else unchanged.push(key);
|
|
192
|
+
for (const key of to.keys()) if (!from.has(key)) added.push(key);
|
|
193
|
+
return {
|
|
194
|
+
added: added.sort(),
|
|
195
|
+
removed: removed.sort(),
|
|
196
|
+
changed: changed.sort(),
|
|
197
|
+
unchanged: unchanged.sort()
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
//#endregion
|
|
202
|
+
export { addTrackerHeader, backupEnvLocal, createTrackerHeader, diffEnvMaps, getActiveEnv, getBackupFile, getTargetFile, listEnvFiles, loadConfig, parseEnvContent, parseTrackerHeader, removeTrackerHeader, restoreEnvLocal, switchEnv };
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dotswitch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Quickly switch between .env files",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dotswitch": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.cjs",
|
|
10
|
+
"module": "./dist/index.mjs",
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": {
|
|
15
|
+
"types": "./dist/index.d.mts",
|
|
16
|
+
"default": "./dist/index.mjs"
|
|
17
|
+
},
|
|
18
|
+
"require": {
|
|
19
|
+
"types": "./dist/index.d.cts",
|
|
20
|
+
"default": "./dist/index.cjs"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsdown && mv dist/cli.mjs dist/cli.js && mv dist/cli.d.mts dist/cli.d.ts",
|
|
29
|
+
"dev": "tsx src/cli.ts",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest",
|
|
32
|
+
"typecheck": "tsc --noEmit"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"dotswitch",
|
|
36
|
+
"env",
|
|
37
|
+
"dotenv",
|
|
38
|
+
"env-switch",
|
|
39
|
+
"env-files",
|
|
40
|
+
"env-management",
|
|
41
|
+
"environment",
|
|
42
|
+
"environment-variables",
|
|
43
|
+
"configuration",
|
|
44
|
+
"cli",
|
|
45
|
+
"nextjs",
|
|
46
|
+
"vite",
|
|
47
|
+
"remix",
|
|
48
|
+
"monorepo",
|
|
49
|
+
"devtools",
|
|
50
|
+
"developer-tools",
|
|
51
|
+
"git-hooks"
|
|
52
|
+
],
|
|
53
|
+
"author": "Stefan Natter <https://natterstefan.me>",
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/natterstefan/dotswitch.git"
|
|
58
|
+
},
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/natterstefan/dotswitch/issues"
|
|
61
|
+
},
|
|
62
|
+
"homepage": "https://github.com/natterstefan/dotswitch#readme",
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=20"
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"@inquirer/select": "^4.1.0",
|
|
68
|
+
"commander": "^13.1.0",
|
|
69
|
+
"picocolors": "^1.1.1"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@types/node": "^22.13.4",
|
|
73
|
+
"memfs": "^4.17.0",
|
|
74
|
+
"tsdown": "^0.20.3",
|
|
75
|
+
"tsx": "^4.19.3",
|
|
76
|
+
"typescript": "^5.7.3",
|
|
77
|
+
"vitest": "^3.0.7"
|
|
78
|
+
}
|
|
79
|
+
}
|