convex-env-sync 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -6
- package/dist/index.d.ts +37 -1
- package/dist/index.js +136 -154
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,70 @@
|
|
|
1
1
|
# convex-env-sync
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A CLI tool that syncs environment variables with Convex using a locally defined `.env.convex` file. This saves you the hassle of having to go to the dashboard to configure environment variables. On startup, it performs an initial sync, then watches the file for changes and periodically polls the Convex deployment for remote updates.
|
|
4
|
+
|
|
5
|
+
It uses the official Convex CLI internally to read, set, and remove environment variables.
|
|
6
|
+
|
|
7
|
+
This tool intentionally does **not** attempt to provide type-safe access to environment variables inside your Convex functions. If you’re looking for that functionality, use [convex-env by @bentsignal](https://github.com/bentsignal/convex-env) (both tools can be used together).
|
|
8
|
+
|
|
9
|
+
## Guide
|
|
4
10
|
|
|
5
11
|
```bash
|
|
6
|
-
|
|
12
|
+
npx convex-env-sync
|
|
7
13
|
```
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
### Adding Variables
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
To add a variable to Convex, define it in your `.env.convex` file:
|
|
18
|
+
|
|
19
|
+
```env
|
|
20
|
+
API_KEY=sk-123
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The variable will be added to Convex when the file is saved.
|
|
24
|
+
|
|
25
|
+
### Deleting Variables
|
|
26
|
+
|
|
27
|
+
To delete a variable from Convex, set its value to `__DELETE__`:
|
|
28
|
+
|
|
29
|
+
```env
|
|
30
|
+
# This will delete API_KEY from Convex
|
|
31
|
+
API_KEY=__DELETE__
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The variable will be removed from Convex and deleted from the local file on save.
|
|
35
|
+
|
|
36
|
+
## Note
|
|
37
|
+
|
|
38
|
+
If the deployment and the local file have the same variables with different values, the local file takes precedence. This is because it is intended for use with dev deployments, in which you are usually the only user. To sync down the dev deployment version, simply delete the `.env.convex` file and run `npx convex-env-sync`.
|
|
39
|
+
|
|
40
|
+
### Setup in package.json
|
|
41
|
+
|
|
42
|
+
You will usually want to run this alongside your Convex dev server:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"scripts": {
|
|
47
|
+
"dev:convex": "npx convex-env-sync & convex dev"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
13
50
|
```
|
|
14
51
|
|
|
15
|
-
|
|
52
|
+
### Options
|
|
53
|
+
|
|
54
|
+
| Option | Description |
|
|
55
|
+
| ------------------ | --------------------------------------------------- |
|
|
56
|
+
| `--file <path>` | Path to env file (default: `.env.convex`) |
|
|
57
|
+
| `--once` | Run a single sync and exit |
|
|
58
|
+
| `--no-write-back` | Prevent updating the local file from remote changes |
|
|
59
|
+
| `--poll <seconds>` | Polling interval for remote changes (default: 300) |
|
|
60
|
+
| `-h, --help` | Show help message |
|
|
61
|
+
|
|
62
|
+
## Requirements
|
|
63
|
+
|
|
64
|
+
* Node.js 18+
|
|
65
|
+
* Convex CLI configured in the project (`convex dev` must work)
|
|
66
|
+
* Can be run via `bunx`, `npx`, or `pnpm dlx`
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export {
|
|
2
|
+
export interface EnvVars {
|
|
3
|
+
[key: string]: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CLIOptions {
|
|
6
|
+
file: string;
|
|
7
|
+
once: boolean;
|
|
8
|
+
writeBack: boolean;
|
|
9
|
+
poll: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SyncDeps {
|
|
12
|
+
getRemoteVars: () => Promise<EnvVars>;
|
|
13
|
+
setRemoteVar: (key: string, value: string) => Promise<void>;
|
|
14
|
+
deleteRemoteVar: (key: string) => Promise<void>;
|
|
15
|
+
readLocalVars: (filePath: string) => EnvVars;
|
|
16
|
+
writeLocalVars: (filePath: string, vars: EnvVars) => void;
|
|
17
|
+
fileExists: (filePath: string) => boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface SyncState {
|
|
20
|
+
lastKnownRemoteVars: EnvVars;
|
|
21
|
+
isSyncing: boolean;
|
|
22
|
+
}
|
|
23
|
+
export declare const DELETE_MARKER = "__DELETE__";
|
|
24
|
+
export declare function parseArgs(argv?: string[]): CLIOptions;
|
|
25
|
+
export declare function parseEnvFile(content: string): EnvVars;
|
|
26
|
+
export declare function serializeEnvFile(vars: EnvVars): string;
|
|
27
|
+
export interface SyncResult {
|
|
28
|
+
setRemoteCalls: Array<{
|
|
29
|
+
key: string;
|
|
30
|
+
value: string;
|
|
31
|
+
}>;
|
|
32
|
+
deleteRemoteCalls: string[];
|
|
33
|
+
localWritten: EnvVars | null;
|
|
34
|
+
newLastKnownRemoteVars: EnvVars;
|
|
35
|
+
pulledFromRemote: string[];
|
|
36
|
+
deletedFromLocal: string[];
|
|
37
|
+
}
|
|
38
|
+
export declare function syncCore(deps: SyncDeps, options: Pick<CLIOptions, "writeBack">, filePath: string, isInitialSync: boolean, state: SyncState): Promise<SyncResult | null>;
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync, readFileSync, watch, writeFileSync } from "fs";
|
|
4
4
|
import { resolve } from "path";
|
|
5
|
-
const DELETE_MARKER = "__DELETE__";
|
|
6
|
-
function parseArgs() {
|
|
7
|
-
const args = process.argv.slice(2);
|
|
5
|
+
export const DELETE_MARKER = "__DELETE__";
|
|
6
|
+
export function parseArgs(argv) {
|
|
7
|
+
const args = argv ?? process.argv.slice(2);
|
|
8
8
|
const options = {
|
|
9
9
|
file: ".env.convex",
|
|
10
10
|
once: false,
|
|
@@ -25,12 +25,17 @@ function parseArgs() {
|
|
|
25
25
|
options.writeBack = false;
|
|
26
26
|
}
|
|
27
27
|
else if (arg === "--poll" && nextArg) {
|
|
28
|
-
|
|
28
|
+
const parsed = parseInt(nextArg, 10);
|
|
29
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
30
|
+
console.error(`Invalid poll interval "${nextArg}", using default ${options.poll}`);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
options.poll = parsed;
|
|
34
|
+
}
|
|
29
35
|
i++;
|
|
30
36
|
}
|
|
31
37
|
else if (arg === "--help" || arg === "-h") {
|
|
32
|
-
console.log(`
|
|
33
|
-
convex-env-sync - Sync environment variables with Convex
|
|
38
|
+
console.log(`convex-env-sync - Sync environment variables with Convex
|
|
34
39
|
|
|
35
40
|
Usage: convex-env-sync [options]
|
|
36
41
|
|
|
@@ -42,17 +47,15 @@ Options:
|
|
|
42
47
|
-h, --help Show this help message
|
|
43
48
|
|
|
44
49
|
Special values:
|
|
45
|
-
__DELETE__ Set a variable to this value to delete it from Convex
|
|
46
|
-
`);
|
|
50
|
+
__DELETE__ Set a variable to this value to delete it from Convex`);
|
|
47
51
|
process.exit(0);
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
return options;
|
|
51
55
|
}
|
|
52
|
-
function parseEnvFile(content) {
|
|
56
|
+
export function parseEnvFile(content) {
|
|
53
57
|
const vars = {};
|
|
54
|
-
const
|
|
55
|
-
for (const line of lines) {
|
|
58
|
+
for (const line of content.split("\n")) {
|
|
56
59
|
const trimmed = line.trim();
|
|
57
60
|
if (!trimmed || trimmed.startsWith("#"))
|
|
58
61
|
continue;
|
|
@@ -61,22 +64,21 @@ function parseEnvFile(content) {
|
|
|
61
64
|
continue;
|
|
62
65
|
const key = trimmed.slice(0, eqIndex).trim();
|
|
63
66
|
let value = trimmed.slice(eqIndex + 1).trim();
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
68
|
+
value = value.slice(1, -1).replace(/\\"/g, '"');
|
|
69
|
+
}
|
|
70
|
+
else if (value.startsWith("'") && value.endsWith("'")) {
|
|
67
71
|
value = value.slice(1, -1);
|
|
68
72
|
}
|
|
69
|
-
if (key)
|
|
73
|
+
if (key)
|
|
70
74
|
vars[key] = value;
|
|
71
|
-
}
|
|
72
75
|
}
|
|
73
76
|
return vars;
|
|
74
77
|
}
|
|
75
|
-
function serializeEnvFile(vars) {
|
|
78
|
+
export function serializeEnvFile(vars) {
|
|
76
79
|
return (Object.entries(vars)
|
|
77
80
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
78
81
|
.map(([key, value]) => {
|
|
79
|
-
// Quote values that contain spaces or special characters
|
|
80
82
|
if (value.includes(" ") || value.includes('"') || value.includes("=")) {
|
|
81
83
|
value = `"${value.replace(/"/g, '\\"')}"`;
|
|
82
84
|
}
|
|
@@ -84,6 +86,74 @@ function serializeEnvFile(vars) {
|
|
|
84
86
|
})
|
|
85
87
|
.join("\n") + "\n");
|
|
86
88
|
}
|
|
89
|
+
export async function syncCore(deps, options, filePath, isInitialSync, state) {
|
|
90
|
+
if (state.isSyncing)
|
|
91
|
+
return null;
|
|
92
|
+
const result = {
|
|
93
|
+
setRemoteCalls: [],
|
|
94
|
+
deleteRemoteCalls: [],
|
|
95
|
+
localWritten: null,
|
|
96
|
+
newLastKnownRemoteVars: {},
|
|
97
|
+
pulledFromRemote: [],
|
|
98
|
+
deletedFromLocal: [],
|
|
99
|
+
};
|
|
100
|
+
const remoteVars = await deps.getRemoteVars();
|
|
101
|
+
const localVars = deps.readLocalVars(filePath);
|
|
102
|
+
const fileExists = deps.fileExists(filePath);
|
|
103
|
+
if (isInitialSync && !fileExists) {
|
|
104
|
+
deps.writeLocalVars(filePath, remoteVars);
|
|
105
|
+
result.localWritten = remoteVars;
|
|
106
|
+
result.newLastKnownRemoteVars = { ...remoteVars };
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
const toSetRemote = {};
|
|
110
|
+
const toDeleteRemote = [];
|
|
111
|
+
let updatedLocalVars = { ...localVars };
|
|
112
|
+
let localNeedsWrite = false;
|
|
113
|
+
for (const [key, value] of Object.entries(localVars)) {
|
|
114
|
+
if (value === DELETE_MARKER) {
|
|
115
|
+
if (key in remoteVars)
|
|
116
|
+
toDeleteRemote.push(key);
|
|
117
|
+
delete updatedLocalVars[key];
|
|
118
|
+
localNeedsWrite = true;
|
|
119
|
+
}
|
|
120
|
+
else if (remoteVars[key] !== value) {
|
|
121
|
+
toSetRemote[key] = value;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (options.writeBack) {
|
|
125
|
+
for (const [key, value] of Object.entries(remoteVars)) {
|
|
126
|
+
if (!(key in localVars) && !(key in toSetRemote)) {
|
|
127
|
+
updatedLocalVars[key] = value;
|
|
128
|
+
localNeedsWrite = true;
|
|
129
|
+
result.pulledFromRemote.push(key);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const key of Object.keys(state.lastKnownRemoteVars)) {
|
|
133
|
+
if (!(key in remoteVars) &&
|
|
134
|
+
key in localVars &&
|
|
135
|
+
localVars[key] !== DELETE_MARKER) {
|
|
136
|
+
delete updatedLocalVars[key];
|
|
137
|
+
localNeedsWrite = true;
|
|
138
|
+
result.deletedFromLocal.push(key);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const [key, value] of Object.entries(toSetRemote)) {
|
|
143
|
+
await deps.setRemoteVar(key, value);
|
|
144
|
+
result.setRemoteCalls.push({ key, value });
|
|
145
|
+
}
|
|
146
|
+
for (const key of toDeleteRemote) {
|
|
147
|
+
await deps.deleteRemoteVar(key);
|
|
148
|
+
result.deleteRemoteCalls.push(key);
|
|
149
|
+
}
|
|
150
|
+
if (localNeedsWrite && options.writeBack) {
|
|
151
|
+
deps.writeLocalVars(filePath, updatedLocalVars);
|
|
152
|
+
result.localWritten = updatedLocalVars;
|
|
153
|
+
}
|
|
154
|
+
result.newLastKnownRemoteVars = await deps.getRemoteVars();
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
87
157
|
function runConvexCommand(args) {
|
|
88
158
|
return new Promise((resolve, reject) => {
|
|
89
159
|
const proc = spawn("npx", ["convex", ...args], {
|
|
@@ -114,10 +184,7 @@ function runConvexCommand(args) {
|
|
|
114
184
|
async function getRemoteVars() {
|
|
115
185
|
const output = await runConvexCommand(["env", "list"]);
|
|
116
186
|
const vars = {};
|
|
117
|
-
|
|
118
|
-
// Format is typically: VAR_NAME=value or VAR_NAME="value"
|
|
119
|
-
const lines = output.split("\n");
|
|
120
|
-
for (const line of lines) {
|
|
187
|
+
for (const line of output.split("\n")) {
|
|
121
188
|
const trimmed = line.trim();
|
|
122
189
|
if (!trimmed)
|
|
123
190
|
continue;
|
|
@@ -126,24 +193,24 @@ async function getRemoteVars() {
|
|
|
126
193
|
continue;
|
|
127
194
|
const key = trimmed.slice(0, eqIndex).trim();
|
|
128
195
|
let value = trimmed.slice(eqIndex + 1).trim();
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
196
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
197
|
+
value = value.slice(1, -1).replace(/\\"/g, '"');
|
|
198
|
+
}
|
|
199
|
+
else if (value.startsWith("'") && value.endsWith("'")) {
|
|
132
200
|
value = value.slice(1, -1);
|
|
133
201
|
}
|
|
134
|
-
if (key)
|
|
202
|
+
if (key)
|
|
135
203
|
vars[key] = value;
|
|
136
|
-
}
|
|
137
204
|
}
|
|
138
205
|
return vars;
|
|
139
206
|
}
|
|
140
207
|
async function setRemoteVar(key, value) {
|
|
141
208
|
await runConvexCommand(["env", "set", key, value]);
|
|
142
|
-
console.log(`
|
|
209
|
+
console.log(`pushed ${key}`);
|
|
143
210
|
}
|
|
144
211
|
async function deleteRemoteVar(key) {
|
|
145
212
|
await runConvexCommand(["env", "remove", key]);
|
|
146
|
-
console.log(`
|
|
213
|
+
console.log(`deleted ${key}`);
|
|
147
214
|
}
|
|
148
215
|
function readLocalVars(filePath) {
|
|
149
216
|
if (!existsSync(filePath)) {
|
|
@@ -156,104 +223,47 @@ function writeLocalVars(filePath, vars) {
|
|
|
156
223
|
writeFileSync(filePath, serializeEnvFile(vars));
|
|
157
224
|
}
|
|
158
225
|
let lastKnownRemoteVars = {};
|
|
159
|
-
let lastKnownLocalVars = {};
|
|
160
226
|
let isSyncing = false;
|
|
161
227
|
async function sync(options, isInitialSync) {
|
|
162
|
-
if (isSyncing)
|
|
163
|
-
console.log("Sync already in progress, skipping...");
|
|
228
|
+
if (isSyncing)
|
|
164
229
|
return;
|
|
165
|
-
}
|
|
166
230
|
isSyncing = true;
|
|
167
231
|
const filePath = resolve(options.file);
|
|
232
|
+
const deps = {
|
|
233
|
+
getRemoteVars,
|
|
234
|
+
setRemoteVar,
|
|
235
|
+
deleteRemoteVar,
|
|
236
|
+
readLocalVars,
|
|
237
|
+
writeLocalVars,
|
|
238
|
+
fileExists: existsSync,
|
|
239
|
+
};
|
|
168
240
|
try {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
console.log(` ✓ Created ${options.file} with ${Object.keys(remoteVars).length} variables`);
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
writeLocalVars(filePath, {});
|
|
183
|
-
console.log(` ✓ Created empty ${options.file}`);
|
|
184
|
-
}
|
|
185
|
-
lastKnownRemoteVars = { ...remoteVars };
|
|
186
|
-
lastKnownLocalVars = { ...remoteVars };
|
|
187
|
-
console.log("Sync complete.");
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
// Track what needs to change
|
|
191
|
-
const toSetRemote = {};
|
|
192
|
-
const toDeleteRemote = [];
|
|
193
|
-
let updatedLocalVars = { ...localVars };
|
|
194
|
-
let localNeedsWrite = false;
|
|
195
|
-
// Process local variables -> push to remote
|
|
196
|
-
for (const [key, value] of Object.entries(localVars)) {
|
|
197
|
-
if (value === DELETE_MARKER) {
|
|
198
|
-
// User wants to delete this variable
|
|
199
|
-
if (key in remoteVars) {
|
|
200
|
-
toDeleteRemote.push(key);
|
|
241
|
+
const fileExistedBefore = existsSync(filePath);
|
|
242
|
+
const result = await syncCore(deps, options, filePath, isInitialSync, {
|
|
243
|
+
lastKnownRemoteVars,
|
|
244
|
+
isSyncing: false,
|
|
245
|
+
});
|
|
246
|
+
if (result) {
|
|
247
|
+
if (isInitialSync && !fileExistedBefore && result.localWritten) {
|
|
248
|
+
const count = Object.keys(result.localWritten).length;
|
|
249
|
+
if (count > 0) {
|
|
250
|
+
console.log(`created ${options.file} with ${count} var${count === 1 ? "" : "s"} from Convex`);
|
|
201
251
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
localNeedsWrite = true;
|
|
205
|
-
}
|
|
206
|
-
else if (remoteVars[key] !== value) {
|
|
207
|
-
// Variable is new or changed locally
|
|
208
|
-
toSetRemote[key] = value;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
// Process remote variables -> sync missing vars back to local
|
|
212
|
-
if (options.writeBack) {
|
|
213
|
-
// Add remote variables that are missing locally (re-sync from remote)
|
|
214
|
-
for (const [key, value] of Object.entries(remoteVars)) {
|
|
215
|
-
if (!(key in localVars) && !(key in toSetRemote)) {
|
|
216
|
-
// Variable exists in Convex but not locally - add it back
|
|
217
|
-
updatedLocalVars[key] = value;
|
|
218
|
-
localNeedsWrite = true;
|
|
219
|
-
console.log(` ← Synced from remote: ${key}`);
|
|
252
|
+
else {
|
|
253
|
+
console.log(`created ${options.file} (empty)`);
|
|
220
254
|
}
|
|
221
255
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (!(key in remoteVars) && key in localVars && localVars[key] !== DELETE_MARKER) {
|
|
225
|
-
// Variable was deleted remotely
|
|
226
|
-
delete updatedLocalVars[key];
|
|
227
|
-
localNeedsWrite = true;
|
|
228
|
-
console.log(` ← Remote deletion detected: ${key}`);
|
|
229
|
-
}
|
|
256
|
+
for (const key of result.pulledFromRemote) {
|
|
257
|
+
console.log(`pulled ${key}`);
|
|
230
258
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
for (const key of toDeleteRemote) {
|
|
237
|
-
await deleteRemoteVar(key);
|
|
238
|
-
}
|
|
239
|
-
// Write back to local file if needed
|
|
240
|
-
if (localNeedsWrite && options.writeBack) {
|
|
241
|
-
writeLocalVars(filePath, updatedLocalVars);
|
|
242
|
-
console.log(` ✓ Updated ${options.file}`);
|
|
243
|
-
}
|
|
244
|
-
// Update last known state
|
|
245
|
-
lastKnownRemoteVars = await getRemoteVars(); // Re-fetch after our changes
|
|
246
|
-
lastKnownLocalVars = readLocalVars(filePath);
|
|
247
|
-
const totalChanges = Object.keys(toSetRemote).length + toDeleteRemote.length + (localNeedsWrite ? 1 : 0);
|
|
248
|
-
if (totalChanges === 0) {
|
|
249
|
-
console.log("No changes detected.");
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
console.log("Sync complete.");
|
|
259
|
+
for (const key of result.deletedFromLocal) {
|
|
260
|
+
console.log(`removed ${key}`);
|
|
261
|
+
}
|
|
262
|
+
lastKnownRemoteVars = result.newLastKnownRemoteVars;
|
|
253
263
|
}
|
|
254
264
|
}
|
|
255
265
|
catch (error) {
|
|
256
|
-
console.error("
|
|
266
|
+
console.error("error:", error instanceof Error ? error.message : error);
|
|
257
267
|
}
|
|
258
268
|
finally {
|
|
259
269
|
isSyncing = false;
|
|
@@ -270,65 +280,37 @@ function debounce(fn, ms) {
|
|
|
270
280
|
async function main() {
|
|
271
281
|
const options = parseArgs();
|
|
272
282
|
const filePath = resolve(options.file);
|
|
273
|
-
console.log("Convex Environment Variable Sync");
|
|
274
|
-
console.log("================================");
|
|
275
|
-
console.log(`File: ${filePath}`);
|
|
276
|
-
console.log(`Write-back: ${options.writeBack}`);
|
|
277
|
-
console.log(`Poll interval: ${options.poll}s`);
|
|
278
|
-
console.log(`Mode: ${options.once ? "one-time sync" : "continuous watch"}`);
|
|
279
|
-
// Initial sync
|
|
280
283
|
await sync(options, true);
|
|
281
284
|
if (options.once) {
|
|
282
|
-
console.log("
|
|
285
|
+
console.log("synced");
|
|
283
286
|
process.exit(0);
|
|
284
287
|
}
|
|
285
|
-
|
|
288
|
+
console.log(`watching ${options.file}`);
|
|
286
289
|
const debouncedSync = debounce(() => sync(options, false), 500);
|
|
287
|
-
console.log(`\nWatching ${options.file} for changes...`);
|
|
288
|
-
console.log("Press Ctrl+C to exit.\n");
|
|
289
|
-
// Watch for file changes
|
|
290
290
|
let watcher = null;
|
|
291
291
|
const setupWatcher = () => {
|
|
292
|
-
if (watcher)
|
|
292
|
+
if (watcher)
|
|
293
293
|
watcher.close();
|
|
294
|
-
}
|
|
295
294
|
if (existsSync(filePath)) {
|
|
296
295
|
watcher = watch(filePath, (eventType) => {
|
|
297
|
-
if (eventType === "change")
|
|
298
|
-
console.log(`\nFile change detected...`);
|
|
296
|
+
if (eventType === "change")
|
|
299
297
|
debouncedSync();
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
watcher.on("error", (error) => {
|
|
303
|
-
console.error("Watcher error:", error);
|
|
304
|
-
// Try to re-establish watcher
|
|
305
|
-
setTimeout(setupWatcher, 1000);
|
|
306
298
|
});
|
|
299
|
+
watcher.on("error", () => setTimeout(setupWatcher, 1000));
|
|
307
300
|
}
|
|
308
301
|
};
|
|
309
302
|
setupWatcher();
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
console.log(`\nPolling remote...`);
|
|
313
|
-
sync(options, false);
|
|
314
|
-
}, options.poll * 1000);
|
|
315
|
-
// Handle graceful shutdown
|
|
316
|
-
process.on("SIGINT", () => {
|
|
317
|
-
console.log("\n\nShutting down...");
|
|
303
|
+
const pollInterval = setInterval(() => sync(options, false), options.poll * 1000);
|
|
304
|
+
const shutdown = () => {
|
|
318
305
|
if (watcher)
|
|
319
306
|
watcher.close();
|
|
320
307
|
clearInterval(pollInterval);
|
|
321
308
|
process.exit(0);
|
|
322
|
-
}
|
|
323
|
-
process.on("
|
|
324
|
-
|
|
325
|
-
if (watcher)
|
|
326
|
-
watcher.close();
|
|
327
|
-
clearInterval(pollInterval);
|
|
328
|
-
process.exit(0);
|
|
329
|
-
});
|
|
309
|
+
};
|
|
310
|
+
process.on("SIGINT", shutdown);
|
|
311
|
+
process.on("SIGTERM", shutdown);
|
|
330
312
|
}
|
|
331
313
|
main().catch((error) => {
|
|
332
|
-
console.error("
|
|
314
|
+
console.error("fatal:", error);
|
|
333
315
|
process.exit(1);
|
|
334
316
|
});
|