@muthuishere/vsync 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -37
- package/bin/sync.ts +6 -3
- package/bin/vsync.ts +0 -0
- package/package.json +7 -3
- package/src/envfile.ts +71 -25
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# vsync
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**One encrypted vault for your environment secrets, shared across your team, mirrored to GitHub & GCP, audited every time someone touches it.**
|
|
4
4
|
|
|
5
|
-

|
|
5
|
+

|
|
6
6
|
|
|
7
7
|
A `.env` file is the friendliest thing in your repo: one line per secret, edited by hand, loaded by every framework. It's also the worst thing in your repo — passed around on Slack, copy-pasted into the wrong window, never the same on any two laptops, **never encrypted, never versioned, never auditable**. The moment one teammate's secrets drift from another's, you stop trusting `.env` and start emailing JSON files.
|
|
8
8
|
|
|
@@ -16,10 +16,11 @@ vsync keeps the `.env` you already write, and turns it into a real vault:
|
|
|
16
16
|
- **Per-machine key in the OS keychain.** `Bun.secrets` — macOS Keychain, Linux libsecret, Windows Credential Manager. The S3 bucket alone is useless; the key alone is useless. Both halves required to decrypt.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
|
|
19
|
+
bun install -g @muthuishere/vsync # or: npm install -g @muthuishere/vsync
|
|
20
|
+
vsync --help
|
|
20
21
|
```
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
One global install, then `vsync` is on PATH. No shell-rc edits, no giant base64 blob in `~/.zshrc`. (Allergic to global installs? `bunx @muthuishere/vsync <subcommand>` works too — same code path, slower invocation.)
|
|
23
24
|
|
|
24
25
|
---
|
|
25
26
|
|
|
@@ -103,13 +104,16 @@ A `.share` file bundles **both halves** under one passphrase. Sent on a differen
|
|
|
103
104
|
|
|
104
105
|
## Install
|
|
105
106
|
|
|
106
|
-
You don't. Run via `bunx`:
|
|
107
|
-
|
|
108
107
|
```bash
|
|
109
|
-
|
|
108
|
+
bun install -g @muthuishere/vsync # or: npm install -g @muthuishere/vsync
|
|
109
|
+
vsync --help
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
Requires Bun ≥ 1.2.21 (for `Bun.secrets`).
|
|
112
|
+
Requires Bun ≥ 1.2.21 on PATH (for `Bun.secrets`) — the shebang is `#!/usr/bin/env bun`, so `bun` must be installed even if you used `npm install -g` for the package itself. Most users have Bun anyway; if not, see [bun.sh](https://bun.sh).
|
|
113
|
+
|
|
114
|
+
Don't want to install? `bunx @muthuishere/vsync <subcommand>` runs the same code from npm cache each time — fine for trying it out, slower for daily use.
|
|
115
|
+
|
|
116
|
+
For local development of vsync itself:
|
|
113
117
|
|
|
114
118
|
```bash
|
|
115
119
|
git clone git@github.com:muthuishere/vsync.git
|
|
@@ -125,14 +129,14 @@ bun test
|
|
|
125
129
|
```bash
|
|
126
130
|
# 1. Generate the per-(repo, env) key + config. First-ever invocation prompts
|
|
127
131
|
# for S3 creds; subsequent inits pre-fill from ~/.config/vsync/defaults.
|
|
128
|
-
|
|
132
|
+
vsync init dev
|
|
129
133
|
|
|
130
134
|
# 2. Put your secrets under infra/vault/dev/ and push.
|
|
131
135
|
echo "DATABASE_URL=postgres://..." > infra/vault/dev/.env.dev
|
|
132
|
-
|
|
136
|
+
vsync push dev
|
|
133
137
|
|
|
134
138
|
# 3. Hand the team a share file + passphrase (different channels).
|
|
135
|
-
|
|
139
|
+
vsync export dev
|
|
136
140
|
```
|
|
137
141
|
|
|
138
142
|
For an onboarding cheat sheet to drop into your repo (so teammates and AI agents know vsync exists), run `vsync docs > infra/AGENTS.md`. Plain stdout — pipe it wherever you want.
|
|
@@ -144,11 +148,11 @@ cd <cloned-repo>
|
|
|
144
148
|
|
|
145
149
|
# 1. Import the share file your teammate sent (carries S3 creds + key).
|
|
146
150
|
# No prior `init` required on this machine.
|
|
147
|
-
|
|
151
|
+
vsync import dev ./reqsume-dev.share
|
|
148
152
|
# Passphrase: <paste>
|
|
149
153
|
|
|
150
154
|
# 2. Pull the encrypted bundle.
|
|
151
|
-
|
|
155
|
+
vsync pull dev
|
|
152
156
|
```
|
|
153
157
|
|
|
154
158
|
After step 2, `infra/vault/dev/` is populated and the encryption key is in your keychain.
|
|
@@ -157,18 +161,18 @@ After step 2, `infra/vault/dev/` is populated and the encryption key is in your
|
|
|
157
161
|
|
|
158
162
|
```bash
|
|
159
163
|
# I edited infra/vault/dev/.env.dev locally:
|
|
160
|
-
|
|
164
|
+
vsync push dev
|
|
161
165
|
|
|
162
166
|
# Get the latest from S3:
|
|
163
|
-
|
|
167
|
+
vsync pull dev
|
|
164
168
|
|
|
165
169
|
# See what versions exist on S3:
|
|
166
|
-
|
|
170
|
+
vsync versions dev
|
|
167
171
|
|
|
168
172
|
# Push secrets out to GitHub / GCP:
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
173
|
+
vsync sync dev gh
|
|
174
|
+
vsync sync dev gcp
|
|
175
|
+
vsync sync dev all
|
|
172
176
|
```
|
|
173
177
|
|
|
174
178
|
`pull` makes a local backup at `~/.config/vsync/backups/<env>-<ts>.zip.enc` before overwriting (two-deep rolling buffer). See "Recovering a local backup" below if you ever need one.
|
|
@@ -196,17 +200,24 @@ Every command works fully via flags or fully via prompts.
|
|
|
196
200
|
|
|
197
201
|
### `sync` env-file parsing
|
|
198
202
|
|
|
199
|
-
|
|
203
|
+
**File references — `*_PATH` / `*_FILE`.** Any key in `.env.<env>` whose name ends in `_PATH` or `_FILE` is read from disk; vsync pushes the file's contents under the key with the suffix stripped. Examples:
|
|
204
|
+
|
|
205
|
+
- `SSH_PRIVATE_KEY_PATH=keys/reqsume_dev` → pushes `<vault>/keys/reqsume_dev` as `SSH_PRIVATE_KEY`.
|
|
206
|
+
- `GCP_SA_KEY_FILE=keys/sa.json` → pushes `<vault>/keys/sa.json` as `GCP_SA_KEY`.
|
|
200
207
|
|
|
201
|
-
|
|
202
|
-
- `SSH_KEY_PATH=<path>` → reads the file, pushes as `SSH_PRIVATE_KEY`.
|
|
208
|
+
Relative paths anchor to `VAULT_ROOT` (the directory of the env file being parsed). Placeholders `${VAULT_ROOT}`, `${HOME}`, and leading `~/` are expanded in every value. Any missing or unreadable referenced file aborts the whole sync before any push (all-or-none).
|
|
203
209
|
|
|
204
210
|
Two local-only keys (skipped — used by `gh` / `gcloud` on the local machine, not pushed):
|
|
205
211
|
|
|
206
212
|
- `GITHUB_TOKEN`
|
|
207
213
|
- `GOOGLE_APPLICATION_CREDENTIALS`
|
|
208
214
|
|
|
209
|
-
|
|
215
|
+
Two routing keys (consumed by `vsync sync` itself, never pushed):
|
|
216
|
+
|
|
217
|
+
- `GITHUB_REPO`
|
|
218
|
+
- `GCP_PROJECT_ID`
|
|
219
|
+
|
|
220
|
+
Everything else is pushed verbatim. Full convention in [`docs/guide/sync.md`](docs/guide/sync.md) and design context in [`docs/specs/v0.6-vault-relative-file-refs.md`](docs/specs/v0.6-vault-relative-file-refs.md).
|
|
210
221
|
|
|
211
222
|
### Audit log
|
|
212
223
|
|
|
@@ -238,7 +249,7 @@ Auth is **outside vsync's scope** — the lib trusts whatever `gh` and `gcloud`
|
|
|
238
249
|
|
|
239
250
|
**`vsync sync <env> gh`:**
|
|
240
251
|
1. Resolves `sync.gh.repo` from per-repo config (or `--gh-repo` flag, or first-run prompt).
|
|
241
|
-
2. Parses `<vaultFolder>/.env.<env>` into push-ready KVs (
|
|
252
|
+
2. Parses `<vaultFolder>/.env.<env>` into push-ready KVs (file-ref + skip rules below).
|
|
242
253
|
3. For each KV in a 6-worker pool: `gh secret set <KEY> --env <env> --repo <sync.gh.repo>` with the value on stdin.
|
|
243
254
|
4. Requires `gh` CLI installed and `gh auth login` already done.
|
|
244
255
|
|
|
@@ -248,7 +259,33 @@ Auth is **outside vsync's scope** — the lib trusts whatever `gh` and `gcloud`
|
|
|
248
259
|
3. For each KV: `gcloud secrets describe <KEY> --project=<proj>` to check existence; either `gcloud secrets versions add <KEY>` (exists) or `gcloud secrets create <KEY> --replication-policy=automatic` (new). Value on stdin via `--data-file=-`.
|
|
249
260
|
4. Requires `gcloud` CLI installed and `gcloud auth login` done. Per-env isolation comes from per-env GCP projects (dev project ≠ prod project) — secret names are flat within a project.
|
|
250
261
|
|
|
251
|
-
**`vsync sync <env> all`** runs both in sequence.
|
|
262
|
+
**`vsync sync <env> all`** runs both in sequence. Per-secret push failures don't abort siblings; final summary lists what failed. Parse-time failures (see "all-or-none" below) abort everything before any push.
|
|
263
|
+
|
|
264
|
+
### File-reference convention (`.env.<env>`)
|
|
265
|
+
|
|
266
|
+
Vsync reads each line of `.env.<env>` and pushes a KV to gh/gcp. Some keys carry **file paths** instead of literal values — vsync reads the file and pushes its bytes:
|
|
267
|
+
|
|
268
|
+
| In env file | Pushed as | Notes |
|
|
269
|
+
|---|---|---|
|
|
270
|
+
| `FOO_PATH=keys/foo` | `FOO` = file contents | suffix `_PATH` stripped; file resolved vault-relative |
|
|
271
|
+
| `FOO_FILE=keys/foo` | `FOO` = file contents | suffix `_FILE` stripped; same resolution |
|
|
272
|
+
| `SSH_PRIVATE_KEY_PATH=keys/dev` | `SSH_PRIVATE_KEY` = file contents | name the env-file key after the secret you want |
|
|
273
|
+
| `GITHUB_TOKEN=…` | *skipped* | local-only |
|
|
274
|
+
| `GOOGLE_APPLICATION_CREDENTIALS=…` | *skipped* | local-only |
|
|
275
|
+
| `GITHUB_REPO=…`, `GCP_PROJECT_ID=…` | *routing meta* — never pushed | consumed by sync itself |
|
|
276
|
+
|
|
277
|
+
**Path resolution.** Relative paths anchor to `VAULT_ROOT` (the directory of the env file being parsed). Three forms of placeholder expansion are recognised in **every** value — file-ref or plain:
|
|
278
|
+
|
|
279
|
+
| Form | Means |
|
|
280
|
+
|---|---|
|
|
281
|
+
| `${VAULT_ROOT}/keys/foo` | `<vault>/keys/foo` (explicit) |
|
|
282
|
+
| `keys/foo` or `./keys/foo` | `<vault>/keys/foo` (implicit — no placeholder needed) |
|
|
283
|
+
| `~/.ssh/id_rsa` or `${HOME}/.ssh/id_rsa` | `$HOME/.ssh/id_rsa` |
|
|
284
|
+
| `/abs/path` | absolute, pass-through |
|
|
285
|
+
|
|
286
|
+
**All-or-none on file refs.** If any `*_PATH` / `*_FILE` references a missing or unreadable file, vsync collects every such error and aborts before pushing anything. No partial syncs.
|
|
287
|
+
|
|
288
|
+
The canonical short-form reference lives in the header comment of [`src/envfile.ts`](src/envfile.ts); design context is in [`docs/specs/v0.6-vault-relative-file-refs.md`](docs/specs/v0.6-vault-relative-file-refs.md).
|
|
252
289
|
|
|
253
290
|
---
|
|
254
291
|
|
|
@@ -299,19 +336,13 @@ In practice, just don't lose the keychain entry. `pull` itself is the recovery p
|
|
|
299
336
|
|
|
300
337
|
## Versioning
|
|
301
338
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
0.
|
|
305
|
-
|
|
306
|
-
0.3.
|
|
307
|
-
|
|
308
|
-
```bash
|
|
309
|
-
vsync init dev # auto-relocates root .env.dev if it exists
|
|
310
|
-
vsync push dev
|
|
311
|
-
vsync export dev # re-share with team
|
|
312
|
-
```
|
|
339
|
+
| Release | What's in it |
|
|
340
|
+
|---|---|
|
|
341
|
+
| **0.5.0** | `vsync use <env>` — symlinks `./.env` (or `--link=<path>`) at the vault's env file so `dotenv.config()` just works; switch envs with one command. README rewrite + flow diagram. |
|
|
342
|
+
| 0.4.0 | Append-only audit log at `s3://<bucket>/<repo>/<env>/audit.csv` + `vsync audit` viewer. Expandable `meta` JSON cell via `--note` / `--meta` + matching env vars. |
|
|
343
|
+
| 0.3.0 | Opinionated layout: vault folder at `infra/vault/<env>/` with `--vault-folder` override; self-contained per-(repo, env) config; `vsync sync` for GitHub / GCP fanout. |
|
|
313
344
|
|
|
314
|
-
|
|
345
|
+
All 0.x releases are wire-compatible with each other on the S3 bundle envelope (`RQE1`) and manifest seal (`RQEM0001`). New clients tolerate the absence of features added in later versions; old clients ignore new objects (like `audit.csv`) on the bucket.
|
|
315
346
|
|
|
316
347
|
---
|
|
317
348
|
|
package/bin/sync.ts
CHANGED
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
// cfg.sync.gcp.project), NOT in the .env file. First run prompts for
|
|
9
9
|
// missing routing and saves it; subsequent runs are zero-prompt.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// -
|
|
11
|
+
// File-reference + skip rules in src/envfile.ts (canonical short-form ref in
|
|
12
|
+
// that file's header comment; design spec in docs/specs/v0.6-vault-relative-file-refs.md):
|
|
13
|
+
// - *_PATH / *_FILE → strip suffix, push file contents under stripped name
|
|
14
14
|
// - GITHUB_TOKEN, GOOGLE_APPLICATION_CREDENTIALS skipped (local-only)
|
|
15
|
+
// - Path resolution anchors to VAULT_ROOT (dir of the .env.<env> being parsed);
|
|
16
|
+
// ${VAULT_ROOT} / ${HOME} / leading ~/ are expanded in every value.
|
|
17
|
+
// - Any missing referenced file aborts the whole sync before any push.
|
|
15
18
|
|
|
16
19
|
import { join } from "node:path";
|
|
17
20
|
import { parseArgs } from "../src/argv";
|
package/bin/vsync.ts
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muthuishere/vsync",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Encrypted secret-sync CLI for small teams. Self-contained per-(repo, env) config + OS keychain key + AES-GCM-on-S3 + share-file onboarding + fanout to GitHub/GCP. Bun-native, run via bunx.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
"README.md"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"test": "bun test"
|
|
15
|
+
"test": "bun test",
|
|
16
|
+
"docs:dev": "vitepress dev docs",
|
|
17
|
+
"docs:build": "vitepress build docs",
|
|
18
|
+
"docs:preview": "vitepress preview docs"
|
|
16
19
|
},
|
|
17
20
|
"keywords": [
|
|
18
21
|
"secrets",
|
|
@@ -41,6 +44,7 @@
|
|
|
41
44
|
},
|
|
42
45
|
"license": "MIT",
|
|
43
46
|
"devDependencies": {
|
|
44
|
-
"@types/bun": "latest"
|
|
47
|
+
"@types/bun": "latest",
|
|
48
|
+
"vitepress": "^1.6.4"
|
|
45
49
|
}
|
|
46
50
|
}
|
package/src/envfile.ts
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
// Parse a `.env.<ENV>` file into push-ready secret tasks.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
3
|
+
// Behavior overview:
|
|
4
4
|
// - skip blank lines + `#` comments
|
|
5
5
|
// - first `=` splits key/value, both trimmed
|
|
6
6
|
// - strip a single pair of surrounding `"` or `'` from the value
|
|
7
7
|
// - skip GITHUB_TOKEN / GOOGLE_APPLICATION_CREDENTIALS (local-only)
|
|
8
|
-
// -
|
|
9
|
-
//
|
|
8
|
+
// - placeholder expansion in every value: `${VAULT_ROOT}`, `${HOME}`,
|
|
9
|
+
// leading `~/`. `VAULT_ROOT` = the directory the env file lives in.
|
|
10
|
+
//
|
|
11
|
+
// File-reference convention (vsync reads the file, pushes its contents
|
|
12
|
+
// under the stripped key name):
|
|
13
|
+
//
|
|
14
|
+
// FOO_PATH=keys/foo -> push as FOO with contents of <vault>/keys/foo
|
|
15
|
+
// FOO_FILE=./keys/foo -> push as FOO with contents of <vault>/keys/foo
|
|
16
|
+
//
|
|
17
|
+
// Relative paths are always resolved against `VAULT_ROOT` (i.e. the env
|
|
18
|
+
// file's own directory). Absolute paths and `~/` are honored as-is.
|
|
19
|
+
//
|
|
20
|
+
// All-or-none: if any file referenced by a `*_PATH`/`*_FILE` key is missing
|
|
21
|
+
// or unreadable, parseEnvFile throws a single aggregated error and emits no
|
|
22
|
+
// tasks. Sync must not run partially.
|
|
10
23
|
//
|
|
11
24
|
// GITHUB_REPO and GCP_PROJECT_ID are pulled out into `meta` and never pushed
|
|
12
25
|
// as secrets — they're routing config consumed by sync-secrets itself.
|
|
13
26
|
|
|
14
27
|
import { existsSync, readFileSync } from "node:fs";
|
|
15
28
|
import { homedir } from "node:os";
|
|
16
|
-
import { join } from "node:path";
|
|
29
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
17
30
|
|
|
18
31
|
export type SecretTask = { key: string; value: string };
|
|
19
32
|
|
|
@@ -25,13 +38,17 @@ export type ParsedEnv = {
|
|
|
25
38
|
const LOCAL_ONLY = new Set(["GITHUB_TOKEN", "GOOGLE_APPLICATION_CREDENTIALS"]);
|
|
26
39
|
const ROUTING = new Set(["GITHUB_REPO", "GCP_PROJECT_ID"]);
|
|
27
40
|
|
|
41
|
+
const PATH_SUFFIXES = ["_PATH", "_FILE"] as const;
|
|
42
|
+
|
|
28
43
|
export function parseEnvFile(path: string): ParsedEnv {
|
|
29
44
|
if (!existsSync(path)) {
|
|
30
45
|
throw new Error(`.env file not found: ${path}`);
|
|
31
46
|
}
|
|
32
47
|
const raw = readFileSync(path, "utf8");
|
|
48
|
+
const vaultRoot = dirname(path);
|
|
33
49
|
const tasks: SecretTask[] = [];
|
|
34
50
|
const meta: ParsedEnv["meta"] = {};
|
|
51
|
+
const errors: string[] = [];
|
|
35
52
|
|
|
36
53
|
for (const rawLine of raw.split(/\r?\n/)) {
|
|
37
54
|
const line = rawLine.trim();
|
|
@@ -41,41 +58,39 @@ export function parseEnvFile(path: string): ParsedEnv {
|
|
|
41
58
|
if (eq === -1) continue;
|
|
42
59
|
|
|
43
60
|
const key = line.slice(0, eq).trim();
|
|
44
|
-
|
|
61
|
+
const rawValue = stripQuotes(line.slice(eq + 1).trim());
|
|
45
62
|
|
|
46
63
|
if (LOCAL_ONLY.has(key)) {
|
|
47
64
|
console.log(`Skipping ${key} (local use only)`);
|
|
48
65
|
continue;
|
|
49
66
|
}
|
|
50
67
|
|
|
68
|
+
const value = expandPlaceholders(rawValue, vaultRoot);
|
|
69
|
+
|
|
51
70
|
if (ROUTING.has(key)) {
|
|
52
71
|
meta[key as keyof ParsedEnv["meta"]] = value;
|
|
53
72
|
continue;
|
|
54
73
|
}
|
|
55
74
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (key === "SSH_KEY_PATH") {
|
|
66
|
-
try {
|
|
67
|
-
tasks.push({ key: "SSH_PRIVATE_KEY", value: readFileExpandTilde(value) });
|
|
68
|
-
} catch (e) {
|
|
69
|
-
console.warn(
|
|
70
|
-
`Warning: error reading SSH private key from ${value}: ${(e as Error).message}`,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
75
|
+
// Generic suffix rule: *_PATH or *_FILE → strip suffix, push file body.
|
|
76
|
+
const stripped = stripPathSuffix(key);
|
|
77
|
+
if (stripped) {
|
|
78
|
+
readFileRef(value, vaultRoot, key, errors, (content) => {
|
|
79
|
+
tasks.push({ key: stripped, value: content });
|
|
80
|
+
});
|
|
73
81
|
continue;
|
|
74
82
|
}
|
|
75
83
|
|
|
84
|
+
// Plain value.
|
|
76
85
|
tasks.push({ key, value });
|
|
77
86
|
}
|
|
78
87
|
|
|
88
|
+
if (errors.length > 0) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`parseEnvFile: aborting sync — ${errors.length} file reference(s) could not be resolved:\n - ${errors.join("\n - ")}`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
return { tasks, meta };
|
|
80
95
|
}
|
|
81
96
|
|
|
@@ -89,7 +104,38 @@ function stripQuotes(value: string): string {
|
|
|
89
104
|
return value;
|
|
90
105
|
}
|
|
91
106
|
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
107
|
+
function expandPlaceholders(value: string, vaultRoot: string): string {
|
|
108
|
+
let out = value;
|
|
109
|
+
if (out.startsWith("~/")) out = join(homedir(), out.slice(2));
|
|
110
|
+
out = out.replaceAll("${VAULT_ROOT}", vaultRoot);
|
|
111
|
+
out = out.replaceAll("${HOME}", homedir());
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function stripPathSuffix(key: string): string | null {
|
|
116
|
+
for (const suf of PATH_SUFFIXES) {
|
|
117
|
+
if (key.endsWith(suf) && key.length > suf.length) {
|
|
118
|
+
return key.slice(0, -suf.length);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function readFileRef(
|
|
125
|
+
value: string,
|
|
126
|
+
vaultRoot: string,
|
|
127
|
+
key: string,
|
|
128
|
+
errors: string[],
|
|
129
|
+
onSuccess: (content: string) => void,
|
|
130
|
+
): void {
|
|
131
|
+
const resolved = isAbsolute(value) ? value : join(vaultRoot, value);
|
|
132
|
+
if (!existsSync(resolved)) {
|
|
133
|
+
errors.push(`${key}: file not found at ${resolved}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
onSuccess(readFileSync(resolved, "utf8"));
|
|
138
|
+
} catch (e) {
|
|
139
|
+
errors.push(`${key}: error reading ${resolved}: ${(e as Error).message}`);
|
|
140
|
+
}
|
|
95
141
|
}
|