@krislavten/env-sync 0.1.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 +233 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +143 -0
- package/dist/cli.js.map +1 -0
- package/dist/core.d.ts +50 -0
- package/dist/core.js +199 -0
- package/dist/core.js.map +1 -0
- package/dist/hook.d.ts +11 -0
- package/dist/hook.js +84 -0
- package/dist/hook.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/render.d.ts +2 -0
- package/dist/render.js +21 -0
- package/dist/render.js.map +1 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 krislavten
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# env-sync
|
|
2
|
+
|
|
3
|
+
> Local-first env file synchronization for people who live across worktrees, repos, and project variants.
|
|
4
|
+
|
|
5
|
+
`env-sync` is a small CLI for keeping local `.env` files aligned without turning secrets into a shared service, a git artifact, or a Slack message.
|
|
6
|
+
|
|
7
|
+
It is built around a simple idea: env files are local operational state, but modern development is rarely done in one checkout. You may have multiple worktrees for the same repo, a web app and worker split across directories, test-only env files, and project-specific variants that should never bleed into each other. `env-sync` gives that workflow a boring, explicit, auditable local mechanism.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
env-sync push pilot
|
|
11
|
+
env-sync pull pilot
|
|
12
|
+
env-sync status pilot
|
|
13
|
+
env-sync diff pilot
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
By default, it syncs `.env.local` in the current directory to:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
~/.env-sync/<project>/files/.env.local
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
No cloud account. No secret values in status output. No implicit overwrite.
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
Most env-file workflows fail in one of three ways:
|
|
27
|
+
|
|
28
|
+
- **Copy-paste drift**: one worktree gets the new key, another keeps the stale value.
|
|
29
|
+
- **Unsafe visibility**: debugging commands print secret values into logs, terminals, or shared tickets.
|
|
30
|
+
- **Namespace accidents**: two projects both have `.env.local`, but the contents are not interchangeable.
|
|
31
|
+
|
|
32
|
+
`env-sync` treats those as product constraints, not documentation warnings.
|
|
33
|
+
|
|
34
|
+
## Design Principles
|
|
35
|
+
|
|
36
|
+
- **Local-first**: the source of truth is on your machine, under `~/.env-sync`.
|
|
37
|
+
- **Project-explicit**: every sync operation belongs to a namespace such as `pilot`, `rush-app`, or `side-project`.
|
|
38
|
+
- **No silent clobbering**: `pull` refuses to overwrite different local content unless you pass `--force`.
|
|
39
|
+
- **Secret-aware output**: `status` and `diff` show key names and short digests, never raw values.
|
|
40
|
+
- **Repo-agnostic**: the CLI does not know or care whether the current directory is Pilot, a worktree, a monorepo package, or a standalone app.
|
|
41
|
+
- **Small surface area**: first make local synchronization dependable; add remote or team workflows only when the local contract is solid.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
Requirements:
|
|
46
|
+
|
|
47
|
+
- Node.js 20 or newer
|
|
48
|
+
- pnpm 10 or newer
|
|
49
|
+
|
|
50
|
+
From npm:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
npm install -g @krislavten/env-sync
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
From source:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
git clone https://github.com/krislavten/env-sync.git
|
|
60
|
+
cd env-sync
|
|
61
|
+
pnpm install
|
|
62
|
+
pnpm build
|
|
63
|
+
pnpm link --global
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
env-sync --help
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
Save the current directory's `.env.local` under the `pilot` namespace:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
env-sync push pilot
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Restore it in another worktree:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
env-sync pull pilot
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
If the destination already has different content, the pull is blocked:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
.env.local: blocked-different
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Overwrite intentionally:
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
env-sync pull pilot --force
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`--force` updates the local file in place. It does not create a backup or audit trail, so use it only when the stored snapshot is the version you intend to keep.
|
|
99
|
+
|
|
100
|
+
Check whether local and stored files match:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
env-sync status pilot
|
|
104
|
+
env-sync diff pilot
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`diff` output is intentionally redacted:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
.env.local: different
|
|
111
|
+
~ API_TOKEN local:f44e64e75f39 stored:8ed3f6ad685b
|
|
112
|
+
+ NEW_FEATURE_FLAG stored:7f021a1415b8
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The key names are visible. The values are not.
|
|
116
|
+
|
|
117
|
+
## Multiple Env Files
|
|
118
|
+
|
|
119
|
+
Pass extra files explicitly:
|
|
120
|
+
|
|
121
|
+
```sh
|
|
122
|
+
env-sync push pilot \
|
|
123
|
+
--file .env.local \
|
|
124
|
+
--file .env.test.local \
|
|
125
|
+
--file apps/agent-hub/.env.local
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Or create a project config:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
env-sync init pilot \
|
|
132
|
+
--file .env.local \
|
|
133
|
+
--file .env.test.local \
|
|
134
|
+
--file apps/agent-hub/.env.local
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This writes:
|
|
138
|
+
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"project": "pilot",
|
|
142
|
+
"files": [
|
|
143
|
+
".env.local",
|
|
144
|
+
".env.test.local",
|
|
145
|
+
"apps/agent-hub/.env.local"
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`.env-sync.json` is safe to commit because it contains only the namespace and file paths, not secret values.
|
|
151
|
+
|
|
152
|
+
After `init`, the project argument becomes optional for read-only checks:
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
env-sync status
|
|
156
|
+
env-sync diff
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Command Reference
|
|
160
|
+
|
|
161
|
+
```text
|
|
162
|
+
env-sync init <project> [--file <path>...]
|
|
163
|
+
env-sync push <project> [--file <path>...]
|
|
164
|
+
env-sync pull <project> [--file <path>...] [--force]
|
|
165
|
+
env-sync status [project] [--file <path>...]
|
|
166
|
+
env-sync diff [project] [--file <path>...]
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
If no `--file` is provided, `env-sync` uses files from `.env-sync.json`. If no config exists, it falls back to `.env.local`.
|
|
170
|
+
|
|
171
|
+
## Storage Model
|
|
172
|
+
|
|
173
|
+
Each project namespace gets its own directory:
|
|
174
|
+
|
|
175
|
+
```text
|
|
176
|
+
~/.env-sync/
|
|
177
|
+
pilot/
|
|
178
|
+
files/
|
|
179
|
+
.env.local
|
|
180
|
+
.env.test.local
|
|
181
|
+
apps/agent-hub/.env.local
|
|
182
|
+
rush-app/
|
|
183
|
+
files/
|
|
184
|
+
.env.local
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
This is deliberately plain. You can inspect it, back it up, remove it, or copy it between machines using tools you already trust.
|
|
188
|
+
|
|
189
|
+
The CLI creates project store directories with owner-only directory permissions and writes synced files with owner-only file permissions where the platform supports POSIX modes. The contents are still plaintext on disk.
|
|
190
|
+
|
|
191
|
+
## Safety Model
|
|
192
|
+
|
|
193
|
+
`env-sync` is not a secret manager. It is a local file synchronization tool with conservative defaults.
|
|
194
|
+
|
|
195
|
+
What it does:
|
|
196
|
+
|
|
197
|
+
- Keeps project namespaces separated.
|
|
198
|
+
- Refuses accidental overwrites.
|
|
199
|
+
- Avoids printing secret values in `status` and `diff`.
|
|
200
|
+
- Stores synced files with owner-only file permissions where the platform supports it.
|
|
201
|
+
- Rejects env file paths outside the current project directory.
|
|
202
|
+
|
|
203
|
+
What it does not do:
|
|
204
|
+
|
|
205
|
+
- Encrypt stored env files.
|
|
206
|
+
- Manage cloud secret rotation.
|
|
207
|
+
- Share secrets with teammates.
|
|
208
|
+
- Decide which values are production-safe.
|
|
209
|
+
|
|
210
|
+
If you need centralized access control, audit logs, or encryption at rest, use a real secret manager. `env-sync` is for the local development gap before that system should be involved.
|
|
211
|
+
|
|
212
|
+
## Development
|
|
213
|
+
|
|
214
|
+
```sh
|
|
215
|
+
pnpm install
|
|
216
|
+
pnpm check
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`pnpm check` runs TypeScript build and tests.
|
|
220
|
+
|
|
221
|
+
## Roadmap
|
|
222
|
+
|
|
223
|
+
- Safer `init` flows for existing repos and worktrees.
|
|
224
|
+
- Optional named profiles per project, for example `pilot/testing` and `pilot/dogfood`.
|
|
225
|
+
- Better human-readable summaries for added, removed, and changed keys.
|
|
226
|
+
- Optional encrypted local store.
|
|
227
|
+
- Optional import/export bundles for machine migration.
|
|
228
|
+
|
|
229
|
+
The project will stay local-first unless there is a clear reason to expand the trust boundary.
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { CONFIG_FILE, diffText, initConfig, installHook, pull, push, resolveScope, status, uninstallHook } from "./index.js";
|
|
3
|
+
async function main(argv) {
|
|
4
|
+
const args = parseArgs(argv);
|
|
5
|
+
if (args.help || !args.command) {
|
|
6
|
+
printHelp();
|
|
7
|
+
return args.command ? 0 : 1;
|
|
8
|
+
}
|
|
9
|
+
if (args.command === "init") {
|
|
10
|
+
const result = await initConfig({ cwd: process.cwd(), project: args.project, files: args.files });
|
|
11
|
+
console.log(`created ${CONFIG_FILE}`);
|
|
12
|
+
console.log(`project: ${result.config.project}`);
|
|
13
|
+
console.log(`files: ${result.config.files.join(", ")}`);
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
if (args.command === "hook") {
|
|
17
|
+
if (args.subcommand === "install") {
|
|
18
|
+
const project = requireArg(args.project, "hook install requires a project");
|
|
19
|
+
const result = await installHook({ cwd: process.cwd(), project, bin: args.bin });
|
|
20
|
+
console.log(`installed post-checkout hook: ${result.path}`);
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
if (args.subcommand === "uninstall") {
|
|
24
|
+
const result = await uninstallHook(process.cwd());
|
|
25
|
+
console.log(`uninstalled post-checkout hook: ${result.path}`);
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
throw new Error("usage: env-sync hook install <project> | env-sync hook uninstall");
|
|
29
|
+
}
|
|
30
|
+
const scope = await resolveScope({ cwd: process.cwd(), project: args.project, files: args.files });
|
|
31
|
+
if (args.command === "push") {
|
|
32
|
+
const results = await push(scope);
|
|
33
|
+
for (const result of results) {
|
|
34
|
+
console.log(`${result.file}: ${result.action}`);
|
|
35
|
+
}
|
|
36
|
+
return results.some((result) => result.action === "missing-local") ? 2 : 0;
|
|
37
|
+
}
|
|
38
|
+
if (args.command === "pull") {
|
|
39
|
+
const results = await pull(scope, args.force);
|
|
40
|
+
for (const result of results) {
|
|
41
|
+
console.log(`${result.file}: ${result.action}`);
|
|
42
|
+
}
|
|
43
|
+
return results.some((result) => result.action === "blocked-different" || result.action === "missing-store") ? 2 : 0;
|
|
44
|
+
}
|
|
45
|
+
if (args.command === "status" || args.command === "diff") {
|
|
46
|
+
const entries = await status(scope);
|
|
47
|
+
for (const entry of entries) {
|
|
48
|
+
console.log(`${entry.file}: ${entry.state}`);
|
|
49
|
+
if (args.command === "diff") {
|
|
50
|
+
const text = diffText(entry.localKeys, entry.storedKeys);
|
|
51
|
+
console.log(text ? indent(text) : " no key changes");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return entries.some((entry) => entry.state === "different" || entry.state.startsWith("missing")) ? 1 : 0;
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`unknown command: ${args.command}`);
|
|
57
|
+
}
|
|
58
|
+
function parseArgs(argv) {
|
|
59
|
+
const parsed = { files: [], force: false, help: false };
|
|
60
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
61
|
+
const arg = argv[index];
|
|
62
|
+
if (arg === "--help" || arg === "-h") {
|
|
63
|
+
parsed.help = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (arg === "--force") {
|
|
67
|
+
parsed.force = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (arg === "--file" || arg === "-f") {
|
|
71
|
+
const file = argv[index + 1];
|
|
72
|
+
if (!file) {
|
|
73
|
+
throw new Error(`${arg} requires a path`);
|
|
74
|
+
}
|
|
75
|
+
parsed.files.push(file);
|
|
76
|
+
index += 1;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (arg === "--bin") {
|
|
80
|
+
if (parsed.command !== "hook") {
|
|
81
|
+
throw new Error("--bin is only supported by env-sync hook install");
|
|
82
|
+
}
|
|
83
|
+
const bin = argv[index + 1];
|
|
84
|
+
if (!bin) {
|
|
85
|
+
throw new Error("--bin requires a path");
|
|
86
|
+
}
|
|
87
|
+
parsed.bin = bin;
|
|
88
|
+
index += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (arg.startsWith("-")) {
|
|
92
|
+
throw new Error(`unknown option: ${arg}`);
|
|
93
|
+
}
|
|
94
|
+
if (!parsed.command) {
|
|
95
|
+
parsed.command = arg;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (parsed.command === "hook" && !parsed.subcommand) {
|
|
99
|
+
parsed.subcommand = arg;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!parsed.project) {
|
|
103
|
+
parsed.project = arg;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
throw new Error(`unexpected argument: ${arg}`);
|
|
107
|
+
}
|
|
108
|
+
return parsed;
|
|
109
|
+
}
|
|
110
|
+
function printHelp() {
|
|
111
|
+
console.log(`env-sync
|
|
112
|
+
|
|
113
|
+
Usage:
|
|
114
|
+
env-sync init <project> [--file <path>...]
|
|
115
|
+
env-sync push <project> [--file <path>...]
|
|
116
|
+
env-sync pull <project> [--file <path>...] [--force]
|
|
117
|
+
env-sync status [project] [--file <path>...]
|
|
118
|
+
env-sync diff [project] [--file <path>...]
|
|
119
|
+
env-sync hook install <project>
|
|
120
|
+
env-sync hook uninstall
|
|
121
|
+
`);
|
|
122
|
+
}
|
|
123
|
+
function requireArg(value, message) {
|
|
124
|
+
if (!value) {
|
|
125
|
+
throw new Error(message);
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
function indent(text) {
|
|
130
|
+
return text
|
|
131
|
+
.split("\n")
|
|
132
|
+
.map((line) => ` ${line}`)
|
|
133
|
+
.join("\n");
|
|
134
|
+
}
|
|
135
|
+
main(process.argv.slice(2))
|
|
136
|
+
.then((code) => {
|
|
137
|
+
process.exitCode = code;
|
|
138
|
+
})
|
|
139
|
+
.catch((error) => {
|
|
140
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
141
|
+
process.exitCode = 1;
|
|
142
|
+
});
|
|
143
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAY7H,KAAK,UAAU,IAAI,CAAC,IAAc;IAChC,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QAC/B,SAAS,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9B,CAAC;IAED,IAAI,IAAI,CAAC,OAAO,KAAK,MAAM,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;QAClG,OAAO,CAAC,GAAG,CAAC,WAAW,WAAW,EAAE,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,YAAY,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC;QACjD,OAAO,CAAC,GAAG,CAAC,UAAU,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACxD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,IAAI,IAAI,CAAC,OAAO,KAAK,MAAM,EAAE,CAAC;QAC5B,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;YAClC,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,iCAAiC,CAAC,CAAC;YAC5E,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;YACjF,OAAO,CAAC,GAAG,CAAC,iCAAiC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;YAC5D,OAAO,CAAC,CAAC;QACX,CAAC;QACD,IAAI,IAAI,CAAC,UAAU,KAAK,WAAW,EAAE,CAAC;YACpC,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;YAClD,OAAO,CAAC,GAAG,CAAC,mCAAmC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;YAC9D,OAAO,CAAC,CAAC;QACX,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;IACtF,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IACnG,IAAI,IAAI,CAAC,OAAO,KAAK,MAAM,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,CAAC;QAClC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;QAClD,CAAC;QACD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7E,CAAC;IAED,IAAI,IAAI,CAAC,OAAO,KAAK,MAAM,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;QAClD,CAAC;QACD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,mBAAmB,IAAI,MAAM,CAAC,MAAM,KAAK,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACtH,CAAC;IAED,IAAI,IAAI,CAAC,OAAO,KAAK,QAAQ,IAAI,IAAI,CAAC,OAAO,KAAK,MAAM,EAAE,CAAC;QACzD,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC;QACpC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;YAC5B,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;YAC7C,IAAI,IAAI,CAAC,OAAO,KAAK,MAAM,EAAE,CAAC;gBAC5B,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;gBACzD,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC;YACxD,CAAC;QACH,CAAC;QACD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,WAAW,IAAI,KAAK,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3G,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,oBAAoB,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;AACtD,CAAC;AAED,SAAS,SAAS,CAAC,IAAc;IAC/B,MAAM,MAAM,GAAe,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IACpE,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACpD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;QACxB,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACrC,MAAM,CAAC,IAAI,GAAG,IAAI,CAAC;YACnB,SAAS;QACX,CAAC;QACD,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;YACtB,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC;YACpB,SAAS;QACX,CAAC;QACD,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACrC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;YAC7B,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,kBAAkB,CAAC,CAAC;YAC5C,CAAC;YACD,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACxB,KAAK,IAAI,CAAC,CAAC;YACX,SAAS;QACX,CAAC;QACD,IAAI,GAAG,KAAK,OAAO,EAAE,CAAC;YACpB,IAAI,MAAM,CAAC,OAAO,KAAK,MAAM,EAAE,CAAC;gBAC9B,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;YACtE,CAAC;YACD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;YAC5B,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,MAAM,IAAI,KAAK,CAAC,uBAAuB,CAAC,CAAC;YAC3C,CAAC;YACD,MAAM,CAAC,GAAG,GAAG,GAAG,CAAC;YACjB,KAAK,IAAI,CAAC,CAAC;YACX,SAAS;QACX,CAAC;QACD,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,mBAAmB,GAAG,EAAE,CAAC,CAAC;QAC5C,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,CAAC,OAAO,GAAG,GAAG,CAAC;YACrB,SAAS;QACX,CAAC;QACD,IAAI,MAAM,CAAC,OAAO,KAAK,MAAM,IAAI,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC;YACpD,MAAM,CAAC,UAAU,GAAG,GAAG,CAAC;YACxB,SAAS;QACX,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,CAAC,OAAO,GAAG,GAAG,CAAC;YACrB,SAAS;QACX,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,EAAE,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,SAAS;IAChB,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;CAUb,CAAC,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,KAAyB,EAAE,OAAe;IAC5D,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;IAC3B,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,MAAM,CAAC,IAAY;IAC1B,OAAO,IAAI;SACR,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;SAC1B,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;KACxB,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;IACb,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;AAC1B,CAAC,CAAC;KACD,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IACxB,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACtE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare const CONFIG_FILE = ".env-sync.json";
|
|
2
|
+
export declare const DEFAULT_ENV_FILE = ".env.local";
|
|
3
|
+
export type EnvSyncConfig = {
|
|
4
|
+
project?: string;
|
|
5
|
+
files?: string[];
|
|
6
|
+
};
|
|
7
|
+
export type ResolveOptions = {
|
|
8
|
+
cwd: string;
|
|
9
|
+
project?: string;
|
|
10
|
+
files?: string[];
|
|
11
|
+
};
|
|
12
|
+
export type ResolvedScope = {
|
|
13
|
+
cwd: string;
|
|
14
|
+
project: string;
|
|
15
|
+
files: string[];
|
|
16
|
+
storeRoot: string;
|
|
17
|
+
};
|
|
18
|
+
export type FileState = "missing-both" | "missing-local" | "missing-store" | "same" | "different";
|
|
19
|
+
export type StatusEntry = {
|
|
20
|
+
file: string;
|
|
21
|
+
state: FileState;
|
|
22
|
+
localKeys: RedactedEnvEntry[];
|
|
23
|
+
storedKeys: RedactedEnvEntry[];
|
|
24
|
+
};
|
|
25
|
+
export type RedactedEnvEntry = {
|
|
26
|
+
key: string;
|
|
27
|
+
digest: string;
|
|
28
|
+
};
|
|
29
|
+
export type PullResult = {
|
|
30
|
+
file: string;
|
|
31
|
+
action: "created" | "updated" | "skipped-same" | "blocked-different" | "missing-store";
|
|
32
|
+
};
|
|
33
|
+
export type PushResult = {
|
|
34
|
+
file: string;
|
|
35
|
+
action: "stored" | "missing-local";
|
|
36
|
+
};
|
|
37
|
+
export type InitResult = {
|
|
38
|
+
path: string;
|
|
39
|
+
config: Required<EnvSyncConfig>;
|
|
40
|
+
};
|
|
41
|
+
export declare function initConfig(options: ResolveOptions): Promise<InitResult>;
|
|
42
|
+
export declare function push(scope: ResolvedScope): Promise<PushResult[]>;
|
|
43
|
+
export declare function pull(scope: ResolvedScope, force?: boolean): Promise<PullResult[]>;
|
|
44
|
+
export declare function status(scope: ResolvedScope): Promise<StatusEntry[]>;
|
|
45
|
+
export declare function resolveScope(options: ResolveOptions): Promise<ResolvedScope>;
|
|
46
|
+
export declare function readConfig(cwd: string): Promise<EnvSyncConfig>;
|
|
47
|
+
export declare function projectStoreRoot(project: string): string;
|
|
48
|
+
export declare function storedFilePath(scope: ResolvedScope, file: string): string;
|
|
49
|
+
export declare function localFilePath(cwd: string, file: string): string;
|
|
50
|
+
export declare function redactEnv(content: string | undefined): RedactedEnvEntry[];
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
export const CONFIG_FILE = ".env-sync.json";
|
|
6
|
+
export const DEFAULT_ENV_FILE = ".env.local";
|
|
7
|
+
export async function initConfig(options) {
|
|
8
|
+
const project = requireProject(options.project);
|
|
9
|
+
const files = uniqueFiles(options.files?.length ? options.files : [DEFAULT_ENV_FILE]);
|
|
10
|
+
const config = { project, files };
|
|
11
|
+
const configPath = path.join(options.cwd, CONFIG_FILE);
|
|
12
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
13
|
+
return { path: configPath, config };
|
|
14
|
+
}
|
|
15
|
+
export async function push(scope) {
|
|
16
|
+
const results = [];
|
|
17
|
+
for (const file of scope.files) {
|
|
18
|
+
const localPath = localFilePath(scope.cwd, file);
|
|
19
|
+
const local = await readTextIfExists(localPath);
|
|
20
|
+
if (local === undefined) {
|
|
21
|
+
results.push({ file, action: "missing-local" });
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const storedPath = storedFilePath(scope, file);
|
|
25
|
+
await mkdir(path.dirname(storedPath), { recursive: true, mode: 0o700 });
|
|
26
|
+
await writeFile(storedPath, local, { encoding: "utf8", mode: 0o600 });
|
|
27
|
+
results.push({ file, action: "stored" });
|
|
28
|
+
}
|
|
29
|
+
return results;
|
|
30
|
+
}
|
|
31
|
+
export async function pull(scope, force = false) {
|
|
32
|
+
const results = [];
|
|
33
|
+
for (const file of scope.files) {
|
|
34
|
+
const stored = await readTextIfExists(storedFilePath(scope, file));
|
|
35
|
+
if (stored === undefined) {
|
|
36
|
+
results.push({ file, action: "missing-store" });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const localPath = localFilePath(scope.cwd, file);
|
|
40
|
+
const local = await readTextIfExists(localPath);
|
|
41
|
+
if (local === stored) {
|
|
42
|
+
results.push({ file, action: "skipped-same" });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (local !== undefined && !force) {
|
|
46
|
+
results.push({ file, action: "blocked-different" });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
await mkdir(path.dirname(localPath), { recursive: true });
|
|
50
|
+
await writeFile(localPath, stored, { encoding: "utf8", mode: 0o600 });
|
|
51
|
+
results.push({ file, action: local === undefined ? "created" : "updated" });
|
|
52
|
+
}
|
|
53
|
+
return results;
|
|
54
|
+
}
|
|
55
|
+
export async function status(scope) {
|
|
56
|
+
const results = [];
|
|
57
|
+
for (const file of scope.files) {
|
|
58
|
+
const local = await readTextIfExists(localFilePath(scope.cwd, file));
|
|
59
|
+
const stored = await readTextIfExists(storedFilePath(scope, file));
|
|
60
|
+
results.push({
|
|
61
|
+
file,
|
|
62
|
+
state: fileState(local, stored),
|
|
63
|
+
localKeys: redactEnv(local),
|
|
64
|
+
storedKeys: redactEnv(stored),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return results;
|
|
68
|
+
}
|
|
69
|
+
export async function resolveScope(options) {
|
|
70
|
+
const config = await readConfig(options.cwd);
|
|
71
|
+
const project = options.project ?? config.project;
|
|
72
|
+
const files = options.files?.length ? options.files : config.files?.length ? config.files : [DEFAULT_ENV_FILE];
|
|
73
|
+
return {
|
|
74
|
+
cwd: options.cwd,
|
|
75
|
+
project: requireProject(project),
|
|
76
|
+
files: uniqueFiles(files),
|
|
77
|
+
storeRoot: projectStoreRoot(requireProject(project)),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export async function readConfig(cwd) {
|
|
81
|
+
const configPath = path.join(cwd, CONFIG_FILE);
|
|
82
|
+
const raw = await readTextIfExists(configPath);
|
|
83
|
+
if (raw === undefined) {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
const parsed = JSON.parse(raw);
|
|
87
|
+
if (parsed.project !== undefined && typeof parsed.project !== "string") {
|
|
88
|
+
throw new Error(`${CONFIG_FILE}: project must be a string`);
|
|
89
|
+
}
|
|
90
|
+
if (parsed.files !== undefined && !Array.isArray(parsed.files)) {
|
|
91
|
+
throw new Error(`${CONFIG_FILE}: files must be an array`);
|
|
92
|
+
}
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
export function projectStoreRoot(project) {
|
|
96
|
+
return path.join(homedir(), ".env-sync", safeProjectName(project));
|
|
97
|
+
}
|
|
98
|
+
export function storedFilePath(scope, file) {
|
|
99
|
+
return path.join(scope.storeRoot, "files", safeRelativePath(file));
|
|
100
|
+
}
|
|
101
|
+
export function localFilePath(cwd, file) {
|
|
102
|
+
const resolved = path.resolve(cwd, file);
|
|
103
|
+
const relative = path.relative(cwd, resolved);
|
|
104
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
105
|
+
throw new Error(`file path must stay inside the current directory: ${file}`);
|
|
106
|
+
}
|
|
107
|
+
return resolved;
|
|
108
|
+
}
|
|
109
|
+
export function redactEnv(content) {
|
|
110
|
+
if (content === undefined) {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
const entries = new Map();
|
|
114
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
115
|
+
const line = rawLine.trim();
|
|
116
|
+
if (!line || line.startsWith("#")) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const normalized = line.startsWith("export ") ? line.slice("export ".length).trimStart() : line;
|
|
120
|
+
const equalsIndex = normalized.indexOf("=");
|
|
121
|
+
if (equalsIndex <= 0) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const key = normalized.slice(0, equalsIndex).trim();
|
|
125
|
+
const value = normalized.slice(equalsIndex + 1);
|
|
126
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
entries.set(key, shortDigest(value));
|
|
130
|
+
}
|
|
131
|
+
return [...entries.entries()]
|
|
132
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
133
|
+
.map(([key, digest]) => ({ key, digest }));
|
|
134
|
+
}
|
|
135
|
+
function fileState(local, stored) {
|
|
136
|
+
if (local === undefined && stored === undefined) {
|
|
137
|
+
return "missing-both";
|
|
138
|
+
}
|
|
139
|
+
if (local === undefined) {
|
|
140
|
+
return "missing-local";
|
|
141
|
+
}
|
|
142
|
+
if (stored === undefined) {
|
|
143
|
+
return "missing-store";
|
|
144
|
+
}
|
|
145
|
+
return local === stored ? "same" : "different";
|
|
146
|
+
}
|
|
147
|
+
function safeProjectName(project) {
|
|
148
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(project)) {
|
|
149
|
+
throw new Error("project must contain only letters, numbers, dot, underscore, or dash");
|
|
150
|
+
}
|
|
151
|
+
return project;
|
|
152
|
+
}
|
|
153
|
+
function safeRelativePath(file) {
|
|
154
|
+
if (!file || path.isAbsolute(file)) {
|
|
155
|
+
throw new Error(`file path must be relative: ${file}`);
|
|
156
|
+
}
|
|
157
|
+
const normalized = path.normalize(file);
|
|
158
|
+
if (normalized.startsWith("..") || path.isAbsolute(normalized)) {
|
|
159
|
+
throw new Error(`file path must stay inside the current directory: ${file}`);
|
|
160
|
+
}
|
|
161
|
+
return normalized;
|
|
162
|
+
}
|
|
163
|
+
function requireProject(project) {
|
|
164
|
+
if (!project) {
|
|
165
|
+
throw new Error(`project is required. Pass it explicitly or run env-sync init <project>`);
|
|
166
|
+
}
|
|
167
|
+
return safeProjectName(project);
|
|
168
|
+
}
|
|
169
|
+
function uniqueFiles(files) {
|
|
170
|
+
const seen = new Set();
|
|
171
|
+
const unique = [];
|
|
172
|
+
for (const file of files) {
|
|
173
|
+
const safe = safeRelativePath(file);
|
|
174
|
+
if (!seen.has(safe)) {
|
|
175
|
+
seen.add(safe);
|
|
176
|
+
unique.push(safe);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return unique;
|
|
180
|
+
}
|
|
181
|
+
async function readTextIfExists(file) {
|
|
182
|
+
try {
|
|
183
|
+
await stat(file);
|
|
184
|
+
return await readFile(file, "utf8");
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function shortDigest(value) {
|
|
194
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
195
|
+
}
|
|
196
|
+
function isNodeError(error) {
|
|
197
|
+
return error instanceof Error && "code" in error;
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=core.js.map
|
package/dist/core.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core.js","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"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"}
|
package/dist/hook.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type HookInstallOptions = {
|
|
2
|
+
cwd: string;
|
|
3
|
+
project: string;
|
|
4
|
+
bin?: string;
|
|
5
|
+
};
|
|
6
|
+
export type HookInstallResult = {
|
|
7
|
+
path: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function installHook(options: HookInstallOptions): Promise<HookInstallResult>;
|
|
10
|
+
export declare function uninstallHook(cwd: string): Promise<HookInstallResult>;
|
|
11
|
+
export declare function renderHookBlock(project: string, bin: string): string;
|
package/dist/hook.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { chmod, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const START_MARKER = "# >>> env-sync";
|
|
7
|
+
const END_MARKER = "# <<< env-sync";
|
|
8
|
+
const ZERO_HEAD = "0000000000000000000000000000000000000000";
|
|
9
|
+
export async function installHook(options) {
|
|
10
|
+
validateProject(options.project);
|
|
11
|
+
const hookPath = await gitPath(options.cwd, "hooks/post-checkout");
|
|
12
|
+
const existing = await readTextIfExists(hookPath);
|
|
13
|
+
const block = renderHookBlock(options.project, options.bin ?? "env-sync");
|
|
14
|
+
const next = upsertManagedBlock(existing ?? "#!/bin/sh\n", block);
|
|
15
|
+
await writeFile(hookPath, next, "utf8");
|
|
16
|
+
await chmod(hookPath, 0o755);
|
|
17
|
+
return { path: hookPath };
|
|
18
|
+
}
|
|
19
|
+
export async function uninstallHook(cwd) {
|
|
20
|
+
const hookPath = await gitPath(cwd, "hooks/post-checkout");
|
|
21
|
+
const existing = await readTextIfExists(hookPath);
|
|
22
|
+
if (existing === undefined) {
|
|
23
|
+
return { path: hookPath };
|
|
24
|
+
}
|
|
25
|
+
await writeFile(hookPath, removeManagedBlock(existing), "utf8");
|
|
26
|
+
return { path: hookPath };
|
|
27
|
+
}
|
|
28
|
+
export function renderHookBlock(project, bin) {
|
|
29
|
+
return `${START_MARKER}
|
|
30
|
+
# Managed by env-sync. Runs only for the initial checkout of a new worktree.
|
|
31
|
+
old_head="$1"
|
|
32
|
+
is_branch_checkout="$3"
|
|
33
|
+
zero="${ZERO_HEAD}"
|
|
34
|
+
|
|
35
|
+
if [ "$old_head" = "$zero" ] && [ "$is_branch_checkout" = "1" ]; then
|
|
36
|
+
ENV_SYNC_BIN=${quoteShell(bin)}
|
|
37
|
+
if [ -x "$ENV_SYNC_BIN" ] || command -v "$ENV_SYNC_BIN" >/dev/null 2>&1; then
|
|
38
|
+
"$ENV_SYNC_BIN" pull ${quoteShell(project)} || {
|
|
39
|
+
echo "env-sync: pull skipped or blocked; run env-sync status ${project}" >&2
|
|
40
|
+
}
|
|
41
|
+
else
|
|
42
|
+
echo "env-sync: command not found, skip env pull" >&2
|
|
43
|
+
fi
|
|
44
|
+
fi
|
|
45
|
+
${END_MARKER}
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
function upsertManagedBlock(existing, block) {
|
|
49
|
+
const without = removeManagedBlock(existing).trimEnd();
|
|
50
|
+
return `${without}\n\n${block}`;
|
|
51
|
+
}
|
|
52
|
+
function removeManagedBlock(existing) {
|
|
53
|
+
const pattern = new RegExp(`\\n?${escapeRegExp(START_MARKER)}[\\s\\S]*?${escapeRegExp(END_MARKER)}\\n?`, "g");
|
|
54
|
+
const next = existing.replace(pattern, "\n").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
55
|
+
return next ? `${next}\n` : "";
|
|
56
|
+
}
|
|
57
|
+
async function gitPath(cwd, relativePath) {
|
|
58
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--git-path", relativePath], { cwd });
|
|
59
|
+
const gitPathOutput = stdout.trim();
|
|
60
|
+
return path.isAbsolute(gitPathOutput) ? gitPathOutput : path.resolve(cwd, gitPathOutput);
|
|
61
|
+
}
|
|
62
|
+
async function readTextIfExists(file) {
|
|
63
|
+
try {
|
|
64
|
+
return await readFile(file, "utf8");
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function quoteShell(value) {
|
|
74
|
+
return `'${value.replaceAll("'", "'\"'\"'")}'`;
|
|
75
|
+
}
|
|
76
|
+
function validateProject(project) {
|
|
77
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(project)) {
|
|
78
|
+
throw new Error("project must contain only letters, numbers, dot, underscore, or dash");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function escapeRegExp(value) {
|
|
82
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=hook.js.map
|
package/dist/hook.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook.js","sourceRoot":"","sources":["../src/hook.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
|
package/dist/render.d.ts
ADDED
package/dist/render.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function diffText(localKeys, storedKeys) {
|
|
2
|
+
const local = new Map(localKeys.map((entry) => [entry.key, entry.digest]));
|
|
3
|
+
const stored = new Map(storedKeys.map((entry) => [entry.key, entry.digest]));
|
|
4
|
+
const keys = [...new Set([...local.keys(), ...stored.keys()])].sort();
|
|
5
|
+
const lines = [];
|
|
6
|
+
for (const key of keys) {
|
|
7
|
+
const localDigest = local.get(key);
|
|
8
|
+
const storedDigest = stored.get(key);
|
|
9
|
+
if (localDigest === undefined) {
|
|
10
|
+
lines.push(`+ ${key} stored:${storedDigest}`);
|
|
11
|
+
}
|
|
12
|
+
else if (storedDigest === undefined) {
|
|
13
|
+
lines.push(`- ${key} local:${localDigest}`);
|
|
14
|
+
}
|
|
15
|
+
else if (localDigest !== storedDigest) {
|
|
16
|
+
lines.push(`~ ${key} local:${localDigest} stored:${storedDigest}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return lines.join("\n");
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,QAAQ,CAAC,SAA6B,EAAE,UAA8B;IACpF,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAC3E,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IAC7E,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,EAAE,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACtE,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,WAAW,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACnC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC9B,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,WAAW,YAAY,EAAE,CAAC,CAAC;QAChD,CAAC;aAAM,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;YACtC,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,UAAU,WAAW,EAAE,CAAC,CAAC;QAC9C,CAAC;aAAM,IAAI,WAAW,KAAK,YAAY,EAAE,CAAC;YACxC,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,UAAU,WAAW,WAAW,YAAY,EAAE,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@krislavten/env-sync",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A small local CLI for syncing env files across worktrees and projects.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/krislavten/env-sync.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/krislavten/env-sync#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/krislavten/env-sync/issues"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"env-sync": "dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"check": "pnpm build && pnpm test"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"env",
|
|
29
|
+
"dotenv",
|
|
30
|
+
"cli",
|
|
31
|
+
"worktree"
|
|
32
|
+
],
|
|
33
|
+
"author": "krislavten",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.10.0",
|
|
37
|
+
"typescript": "^5.7.2",
|
|
38
|
+
"vitest": "^2.1.8"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20"
|
|
42
|
+
}
|
|
43
|
+
}
|