@jondotsoy/envs 0.0.0-stage → 0.0.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/envs.js +204 -0
- package/package.json +7 -4
- package/README.md +0 -3
package/envs.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
|
|
4
|
+
// src/envs.ts
|
|
5
|
+
import { mkdir } from "fs/promises";
|
|
6
|
+
import { basename, dirname, join } from "path";
|
|
7
|
+
var {$ } = globalThis.Bun;
|
|
8
|
+
var HELP = `Usage: envs <command>
|
|
9
|
+
|
|
10
|
+
Commands:
|
|
11
|
+
init Create the .envs/ directory (.gitignore, values.yml)
|
|
12
|
+
pull Read each worktree's .env and write it into .envs/values.yml
|
|
13
|
+
push Write defaults and .envs/values.yml into the .env of each worktree
|
|
14
|
+
edit Pull, open .envs/values.yml in VS Code (code -w), then push
|
|
15
|
+
help Show this help message
|
|
16
|
+
`;
|
|
17
|
+
var defaultContext = () => ({
|
|
18
|
+
cwd: process.cwd(),
|
|
19
|
+
log: (message) => console.log(message),
|
|
20
|
+
error: (message) => console.error(message),
|
|
21
|
+
openEditor: async (path) => {
|
|
22
|
+
const proc = Bun.spawn(["code", "-w", path], {
|
|
23
|
+
env: { ...process.env },
|
|
24
|
+
stdio: ["inherit", "inherit", "inherit"]
|
|
25
|
+
});
|
|
26
|
+
return await proc.exited;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
async function findRoot(cwd) {
|
|
30
|
+
const result = await $`git rev-parse --path-format=absolute --git-common-dir`.cwd(cwd).nothrow().quiet();
|
|
31
|
+
if (result.exitCode !== 0)
|
|
32
|
+
return cwd;
|
|
33
|
+
return dirname(result.stdout.toString().trim());
|
|
34
|
+
}
|
|
35
|
+
async function listWorktrees(cwd) {
|
|
36
|
+
const out = await $`git worktree list --porcelain`.cwd(cwd).text();
|
|
37
|
+
return out.trim().split(`
|
|
38
|
+
|
|
39
|
+
`).map((block) => {
|
|
40
|
+
const lines = block.split(`
|
|
41
|
+
`);
|
|
42
|
+
const path = lines.find((l) => l.startsWith("worktree ")).slice(9);
|
|
43
|
+
const branch = lines.find((l) => l.startsWith("branch "));
|
|
44
|
+
return {
|
|
45
|
+
path,
|
|
46
|
+
name: branch ? branch.slice("branch refs/heads/".length) : basename(path)
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function parseDotenv(text) {
|
|
51
|
+
const result = {};
|
|
52
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
53
|
+
const line = raw.trim();
|
|
54
|
+
if (!line || line.startsWith("#"))
|
|
55
|
+
continue;
|
|
56
|
+
const match = line.match(/^(?:export\s+)?([\w.-]+)\s*=\s*(.*)$/);
|
|
57
|
+
if (!match)
|
|
58
|
+
continue;
|
|
59
|
+
let value = match[2].trim();
|
|
60
|
+
const quote = value[0];
|
|
61
|
+
if ((quote === '"' || quote === "'") && value.lastIndexOf(quote) > 0) {
|
|
62
|
+
value = value.slice(1, value.lastIndexOf(quote));
|
|
63
|
+
} else {
|
|
64
|
+
value = value.replace(/\s+#.*$/, "");
|
|
65
|
+
}
|
|
66
|
+
result[match[1]] = value;
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
function formatDotenvValue(value) {
|
|
71
|
+
if (value !== "" && !/[\s#"'\\$]/.test(value))
|
|
72
|
+
return value;
|
|
73
|
+
return value.includes('"') ? `'${value}'` : `"${value}"`;
|
|
74
|
+
}
|
|
75
|
+
function updateDotenv(text, entries) {
|
|
76
|
+
const pending = new Map(Object.entries(entries));
|
|
77
|
+
const lines = text === "" ? [] : text.replace(/\n$/, "").split(`
|
|
78
|
+
`);
|
|
79
|
+
const updated = lines.map((line) => {
|
|
80
|
+
const key = line.trim().match(/^(?:export\s+)?([\w.-]+)\s*=/)?.[1];
|
|
81
|
+
if (key === undefined || !pending.has(key))
|
|
82
|
+
return line;
|
|
83
|
+
const value = pending.get(key);
|
|
84
|
+
pending.delete(key);
|
|
85
|
+
return `${key}=${formatDotenvValue(value)}`;
|
|
86
|
+
});
|
|
87
|
+
for (const [key, value] of pending) {
|
|
88
|
+
updated.push(`${key}=${formatDotenvValue(value)}`);
|
|
89
|
+
}
|
|
90
|
+
return updated.join(`
|
|
91
|
+
`) + `
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
async function readValues(ctx) {
|
|
95
|
+
const path = join(await findRoot(ctx.cwd), ".envs/values.yml");
|
|
96
|
+
if (!await Bun.file(path).exists()) {
|
|
97
|
+
ctx.error(`${path} not found. Run \`envs init\` first.`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const values = Bun.YAML.parse(await Bun.file(path).text()) ?? {};
|
|
101
|
+
return { path, values };
|
|
102
|
+
}
|
|
103
|
+
async function init(ctx) {
|
|
104
|
+
const root = await findRoot(ctx.cwd);
|
|
105
|
+
const files = { ".envs/.gitignore": `*
|
|
106
|
+
`, ".envs/values.yml": "" };
|
|
107
|
+
await mkdir(join(root, ".envs"), { recursive: true });
|
|
108
|
+
for (const [name, content] of Object.entries(files)) {
|
|
109
|
+
const path = join(root, name);
|
|
110
|
+
if (await Bun.file(path).exists()) {
|
|
111
|
+
ctx.log(`${path} already exists`);
|
|
112
|
+
} else {
|
|
113
|
+
await Bun.write(path, content);
|
|
114
|
+
ctx.log(`Created ${path}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
async function push(ctx) {
|
|
120
|
+
const read = await readValues(ctx);
|
|
121
|
+
if (!read)
|
|
122
|
+
return 1;
|
|
123
|
+
const { values } = read;
|
|
124
|
+
for (const { name, path } of await listWorktrees(ctx.cwd)) {
|
|
125
|
+
const entries = {};
|
|
126
|
+
for (const [key, value] of Object.entries(values.defaults ?? {})) {
|
|
127
|
+
entries[key] = String(value);
|
|
128
|
+
}
|
|
129
|
+
for (const [key, byWorktree] of Object.entries(values.envs ?? {})) {
|
|
130
|
+
if (name in byWorktree)
|
|
131
|
+
entries[key] = String(byWorktree[name]);
|
|
132
|
+
}
|
|
133
|
+
if (Object.keys(entries).length === 0)
|
|
134
|
+
continue;
|
|
135
|
+
const dotenv = Bun.file(join(path, ".env"));
|
|
136
|
+
const current = await dotenv.exists() ? await dotenv.text() : "";
|
|
137
|
+
await Bun.write(dotenv, updateDotenv(current, entries));
|
|
138
|
+
ctx.log(`Pushed ${name}`);
|
|
139
|
+
}
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
async function pull(ctx) {
|
|
143
|
+
const read = await readValues(ctx);
|
|
144
|
+
if (!read)
|
|
145
|
+
return 1;
|
|
146
|
+
const { path: valuesPath, values } = read;
|
|
147
|
+
const envs = values.envs ??= {};
|
|
148
|
+
for (const { name, path } of await listWorktrees(ctx.cwd)) {
|
|
149
|
+
const dotenv = Bun.file(join(path, ".env"));
|
|
150
|
+
if (!await dotenv.exists())
|
|
151
|
+
continue;
|
|
152
|
+
const parsed = parseDotenv(await dotenv.text());
|
|
153
|
+
for (const [key, byWorktree] of Object.entries(envs)) {
|
|
154
|
+
if (!(key in parsed))
|
|
155
|
+
delete byWorktree[name];
|
|
156
|
+
}
|
|
157
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
158
|
+
(envs[key] ??= {})[name] = value;
|
|
159
|
+
}
|
|
160
|
+
ctx.log(`Pulled ${name}`);
|
|
161
|
+
}
|
|
162
|
+
for (const [key, byWorktree] of Object.entries(envs)) {
|
|
163
|
+
if (Object.keys(byWorktree).length === 0)
|
|
164
|
+
delete envs[key];
|
|
165
|
+
}
|
|
166
|
+
await Bun.write(valuesPath, Bun.YAML.stringify(values, null, 2));
|
|
167
|
+
return 0;
|
|
168
|
+
}
|
|
169
|
+
async function edit(ctx) {
|
|
170
|
+
const pulled = await pull(ctx);
|
|
171
|
+
if (pulled !== 0)
|
|
172
|
+
return pulled;
|
|
173
|
+
const valuesPath = join(await findRoot(ctx.cwd), ".envs/values.yml");
|
|
174
|
+
const code = await ctx.openEditor(valuesPath);
|
|
175
|
+
if (code !== 0)
|
|
176
|
+
return code;
|
|
177
|
+
return await push(ctx);
|
|
178
|
+
}
|
|
179
|
+
async function run(args, ctx = defaultContext()) {
|
|
180
|
+
const [command = "help"] = args;
|
|
181
|
+
switch (command) {
|
|
182
|
+
case "help":
|
|
183
|
+
case "--help":
|
|
184
|
+
case "-h":
|
|
185
|
+
ctx.log(HELP);
|
|
186
|
+
return 0;
|
|
187
|
+
case "init":
|
|
188
|
+
return await init(ctx);
|
|
189
|
+
case "push":
|
|
190
|
+
return await push(ctx);
|
|
191
|
+
case "pull":
|
|
192
|
+
return await pull(ctx);
|
|
193
|
+
case "edit":
|
|
194
|
+
return await edit(ctx);
|
|
195
|
+
default:
|
|
196
|
+
ctx.error(`Unknown command: ${command}
|
|
197
|
+
|
|
198
|
+
${HELP}`);
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/bin/envs.ts
|
|
204
|
+
process.exit(await run(Bun.argv.slice(2)));
|
package/package.json
CHANGED
package/README.md
DELETED