@vellumai/cli 0.8.8 → 0.8.9-staging.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/package.json +1 -1
- package/src/commands/login.ts +128 -9
- package/src/lib/__tests__/docker.test.ts +99 -0
- package/src/lib/docker.ts +92 -29
package/package.json
CHANGED
package/src/commands/login.ts
CHANGED
|
@@ -32,6 +32,131 @@ import { syncCloudAssistants } from "../lib/sync-cloud-assistants";
|
|
|
32
32
|
|
|
33
33
|
const LOGIN_TIMEOUT_MS = 120_000; // 2 minutes
|
|
34
34
|
|
|
35
|
+
function escapeHtml(s: string): string {
|
|
36
|
+
return s
|
|
37
|
+
.replace(/&/g, "&")
|
|
38
|
+
.replace(/</g, "<")
|
|
39
|
+
.replace(/>/g, ">")
|
|
40
|
+
.replace(/"/g, """)
|
|
41
|
+
.replace(/'/g, "'");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function renderLoginPage(title: string, subtitle: string, success: boolean): string {
|
|
45
|
+
const checkmarkSvg = `<svg class="icon" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
46
|
+
<circle cx="28" cy="28" r="28" fill="var(--positive-bg)"/>
|
|
47
|
+
<path class="check" d="M17 28.5L24.5 36L39 21" stroke="var(--positive-fg)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
48
|
+
</svg>`;
|
|
49
|
+
|
|
50
|
+
const errorSvg = `<svg class="icon" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
51
|
+
<circle cx="28" cy="28" r="28" fill="var(--negative-bg)"/>
|
|
52
|
+
<path class="cross cross-1" d="M20 20L36 36" stroke="var(--negative-fg)" stroke-width="3.5" stroke-linecap="round" fill="none"/>
|
|
53
|
+
<path class="cross cross-2" d="M36 20L20 36" stroke="var(--negative-fg)" stroke-width="3.5" stroke-linecap="round" fill="none"/>
|
|
54
|
+
</svg>`;
|
|
55
|
+
|
|
56
|
+
return `<!DOCTYPE html>
|
|
57
|
+
<html lang="en">
|
|
58
|
+
<head>
|
|
59
|
+
<meta charset="utf-8">
|
|
60
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
61
|
+
<title>${escapeHtml(title)}</title>
|
|
62
|
+
<style>
|
|
63
|
+
:root {
|
|
64
|
+
--surface: #F5F3EB;
|
|
65
|
+
--surface-card: #FFFFFF;
|
|
66
|
+
--card-border: #E8E6DA;
|
|
67
|
+
--text-primary: #2A2A28;
|
|
68
|
+
--text-secondary: #4A4A46;
|
|
69
|
+
--positive-bg: #D4DFD0;
|
|
70
|
+
--positive-fg: #516748;
|
|
71
|
+
--negative-bg: #F7DAC9;
|
|
72
|
+
--negative-fg: #DA491A;
|
|
73
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.06);
|
|
74
|
+
--font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
|
|
75
|
+
}
|
|
76
|
+
@media (prefers-color-scheme: dark) {
|
|
77
|
+
:root {
|
|
78
|
+
--surface: #1A1A18;
|
|
79
|
+
--surface-card: #2A2A28;
|
|
80
|
+
--card-border: #3A3A37;
|
|
81
|
+
--text-primary: #F5F3EB;
|
|
82
|
+
--text-secondary: #BDB9A9;
|
|
83
|
+
--positive-bg: #1A2316;
|
|
84
|
+
--positive-fg: #7A8B6F;
|
|
85
|
+
--negative-bg: #4E281D;
|
|
86
|
+
--negative-fg: #E86B40;
|
|
87
|
+
--shadow: 0 1px 3px rgba(0,0,0,0.2), 0 4px 12px rgba(0,0,0,0.3);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
91
|
+
body {
|
|
92
|
+
font-family: var(--font);
|
|
93
|
+
background: var(--surface);
|
|
94
|
+
color: var(--text-primary);
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
min-height: 100vh;
|
|
99
|
+
-webkit-font-smoothing: antialiased;
|
|
100
|
+
}
|
|
101
|
+
.card {
|
|
102
|
+
text-align: center;
|
|
103
|
+
padding: 48px 40px 40px;
|
|
104
|
+
background: var(--surface-card);
|
|
105
|
+
border: 1px solid var(--card-border);
|
|
106
|
+
border-radius: 16px;
|
|
107
|
+
box-shadow: var(--shadow);
|
|
108
|
+
max-width: 380px;
|
|
109
|
+
width: 100%;
|
|
110
|
+
opacity: 0;
|
|
111
|
+
transform: translateY(8px) scale(0.98);
|
|
112
|
+
animation: cardIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0.1s forwards;
|
|
113
|
+
}
|
|
114
|
+
@keyframes cardIn {
|
|
115
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
116
|
+
}
|
|
117
|
+
.icon {
|
|
118
|
+
width: 56px;
|
|
119
|
+
height: 56px;
|
|
120
|
+
margin-bottom: 20px;
|
|
121
|
+
}
|
|
122
|
+
.check {
|
|
123
|
+
stroke-dasharray: 32;
|
|
124
|
+
stroke-dashoffset: 32;
|
|
125
|
+
animation: draw 0.4s ease-out 0.45s forwards;
|
|
126
|
+
}
|
|
127
|
+
.cross {
|
|
128
|
+
stroke-dasharray: 22;
|
|
129
|
+
stroke-dashoffset: 22;
|
|
130
|
+
}
|
|
131
|
+
.cross-1 { animation: draw 0.3s ease-out 0.45s forwards; }
|
|
132
|
+
.cross-2 { animation: draw 0.3s ease-out 0.55s forwards; }
|
|
133
|
+
@keyframes draw {
|
|
134
|
+
to { stroke-dashoffset: 0; }
|
|
135
|
+
}
|
|
136
|
+
h1 {
|
|
137
|
+
font-size: 18px;
|
|
138
|
+
font-weight: 600;
|
|
139
|
+
letter-spacing: -0.2px;
|
|
140
|
+
color: var(--text-primary);
|
|
141
|
+
margin-bottom: 6px;
|
|
142
|
+
}
|
|
143
|
+
p {
|
|
144
|
+
font-size: 13px;
|
|
145
|
+
line-height: 1.5;
|
|
146
|
+
color: var(--text-secondary);
|
|
147
|
+
}
|
|
148
|
+
</style>
|
|
149
|
+
</head>
|
|
150
|
+
<body>
|
|
151
|
+
<div class="card">
|
|
152
|
+
${success ? checkmarkSvg : errorSvg}
|
|
153
|
+
<h1>${escapeHtml(title)}</h1>
|
|
154
|
+
<p>${escapeHtml(subtitle)}</p>
|
|
155
|
+
</div>
|
|
156
|
+
</body>
|
|
157
|
+
</html>`;
|
|
158
|
+
}
|
|
159
|
+
|
|
35
160
|
/**
|
|
36
161
|
* Open a URL in the user's default browser.
|
|
37
162
|
*/
|
|
@@ -72,26 +197,20 @@ function browserLogin(webUrl: string): Promise<string> {
|
|
|
72
197
|
|
|
73
198
|
if (receivedState !== state) {
|
|
74
199
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
75
|
-
res.end(
|
|
76
|
-
"<html><body><h2>Login failed</h2><p>State mismatch. Please try again.</p></body></html>",
|
|
77
|
-
);
|
|
200
|
+
res.end(renderLoginPage("Login Failed", "State mismatch. Please try again.", false));
|
|
78
201
|
cleanup("State mismatch — possible CSRF attack.");
|
|
79
202
|
return;
|
|
80
203
|
}
|
|
81
204
|
|
|
82
205
|
if (!sessionToken) {
|
|
83
206
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
84
|
-
res.end(
|
|
85
|
-
"<html><body><h2>Login failed</h2><p>No session token received. Please try again.</p></body></html>",
|
|
86
|
-
);
|
|
207
|
+
res.end(renderLoginPage("Login Failed", "No session token received. Please try again.", false));
|
|
87
208
|
cleanup("No session token received from platform.");
|
|
88
209
|
return;
|
|
89
210
|
}
|
|
90
211
|
|
|
91
212
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
92
|
-
res.end(
|
|
93
|
-
"<html><body><h2>Login successful!</h2><p>You can close this window and return to your terminal.</p></body></html>",
|
|
94
|
-
);
|
|
213
|
+
res.end(renderLoginPage("Login Successful", "You can close this window and return to your terminal.", true));
|
|
95
214
|
cleanup(null, sessionToken);
|
|
96
215
|
});
|
|
97
216
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, test, expect } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "fs";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
2
5
|
import {
|
|
3
6
|
ASSISTANT_INTERNAL_PORT,
|
|
4
7
|
AVATAR_DEVICE_ENV_VAR,
|
|
8
|
+
collectWatchTargets,
|
|
5
9
|
dockerResourceNames,
|
|
6
10
|
resolveAvatarDevicePath,
|
|
7
11
|
resolveDockerHatchMode,
|
|
@@ -277,3 +281,98 @@ describe("resolveDockerHatchMode", () => {
|
|
|
277
281
|
).toEqual({ build: false, watcher: false, fellBackToPull: true });
|
|
278
282
|
});
|
|
279
283
|
});
|
|
284
|
+
|
|
285
|
+
describe("collectWatchTargets", () => {
|
|
286
|
+
let repoRoot: string;
|
|
287
|
+
|
|
288
|
+
beforeEach(() => {
|
|
289
|
+
repoRoot = mkdtempSync(join(tmpdir(), "vellum-watch-"));
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
afterEach(() => {
|
|
293
|
+
rmSync(repoRoot, { recursive: true, force: true });
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
function scaffold(
|
|
297
|
+
relDir: string,
|
|
298
|
+
{ src = true, pkg = true, dockerfile = false } = {},
|
|
299
|
+
): void {
|
|
300
|
+
mkdirSync(join(repoRoot, relDir), { recursive: true });
|
|
301
|
+
if (src) mkdirSync(join(repoRoot, relDir, "src"), { recursive: true });
|
|
302
|
+
if (pkg) writeFileSync(join(repoRoot, relDir, "package.json"), "{}");
|
|
303
|
+
if (dockerfile) writeFileSync(join(repoRoot, relDir, "Dockerfile"), "");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
test("scopes watch targets to src/, package.json, and the Dockerfile", () => {
|
|
307
|
+
// GIVEN the three services (each with a Dockerfile) plus a couple of
|
|
308
|
+
// shared packages (libraries, no Dockerfile)
|
|
309
|
+
scaffold("assistant", { dockerfile: true });
|
|
310
|
+
scaffold("credential-executor", { dockerfile: true });
|
|
311
|
+
scaffold("gateway", { dockerfile: true });
|
|
312
|
+
scaffold("packages/service-contracts");
|
|
313
|
+
scaffold("packages/local-mode");
|
|
314
|
+
|
|
315
|
+
// WHEN we collect the watch targets
|
|
316
|
+
const { dirs, files } = collectWatchTargets(repoRoot);
|
|
317
|
+
|
|
318
|
+
// THEN only the src/ directories are watched recursively
|
|
319
|
+
expect(dirs.sort()).toEqual(
|
|
320
|
+
[
|
|
321
|
+
join(repoRoot, "assistant", "src"),
|
|
322
|
+
join(repoRoot, "credential-executor", "src"),
|
|
323
|
+
join(repoRoot, "gateway", "src"),
|
|
324
|
+
join(repoRoot, "packages", "local-mode", "src"),
|
|
325
|
+
join(repoRoot, "packages", "service-contracts", "src"),
|
|
326
|
+
].sort(),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// AND the package.json manifests and service Dockerfiles are watched as
|
|
330
|
+
// individual files (packages have no Dockerfile, so none is emitted)
|
|
331
|
+
expect(files.sort()).toEqual(
|
|
332
|
+
[
|
|
333
|
+
join(repoRoot, "assistant", "package.json"),
|
|
334
|
+
join(repoRoot, "assistant", "Dockerfile"),
|
|
335
|
+
join(repoRoot, "credential-executor", "package.json"),
|
|
336
|
+
join(repoRoot, "credential-executor", "Dockerfile"),
|
|
337
|
+
join(repoRoot, "gateway", "package.json"),
|
|
338
|
+
join(repoRoot, "gateway", "Dockerfile"),
|
|
339
|
+
join(repoRoot, "packages", "local-mode", "package.json"),
|
|
340
|
+
join(repoRoot, "packages", "service-contracts", "package.json"),
|
|
341
|
+
].sort(),
|
|
342
|
+
);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("never watches .claude/ command symlinks that crash the watcher", () => {
|
|
346
|
+
// GIVEN an assistant service whose .claude/commands holds a dangling
|
|
347
|
+
// symlink (as it does in a fresh checkout)
|
|
348
|
+
scaffold("assistant");
|
|
349
|
+
mkdirSync(join(repoRoot, "assistant", ".claude", "commands"), {
|
|
350
|
+
recursive: true,
|
|
351
|
+
});
|
|
352
|
+
symlinkSync(
|
|
353
|
+
join(repoRoot, "does-not-exist", "do.md"),
|
|
354
|
+
join(repoRoot, "assistant", ".claude", "commands", "do.md"),
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
// WHEN we collect the watch targets
|
|
358
|
+
const { dirs, files } = collectWatchTargets(repoRoot);
|
|
359
|
+
|
|
360
|
+
// THEN no watched path reaches into the .claude/ tree
|
|
361
|
+
const all = [...dirs, ...files];
|
|
362
|
+
expect(all.some((p) => p.includes(".claude"))).toBe(false);
|
|
363
|
+
expect(dirs).toContain(join(repoRoot, "assistant", "src"));
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("skips roots missing a src/ directory or package.json", () => {
|
|
367
|
+
// GIVEN a service with only a manifest and a package with only a src/ dir
|
|
368
|
+
scaffold("gateway", { src: false, pkg: true });
|
|
369
|
+
scaffold("packages/contracts-only", { src: true, pkg: false });
|
|
370
|
+
|
|
371
|
+
// WHEN we collect the watch targets
|
|
372
|
+
const { dirs, files } = collectWatchTargets(repoRoot);
|
|
373
|
+
|
|
374
|
+
// THEN absent paths are not emitted
|
|
375
|
+
expect(dirs).toEqual([join(repoRoot, "packages", "contracts-only", "src")]);
|
|
376
|
+
expect(files).toEqual([join(repoRoot, "gateway", "package.json")]);
|
|
377
|
+
});
|
|
378
|
+
});
|
package/src/lib/docker.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
existsSync,
|
|
5
5
|
mkdirSync,
|
|
6
6
|
readFileSync,
|
|
7
|
+
readdirSync,
|
|
7
8
|
watch as fsWatch,
|
|
8
9
|
} from "fs";
|
|
9
10
|
import { arch, platform } from "os";
|
|
@@ -788,6 +789,56 @@ export async function captureImageRefs(
|
|
|
788
789
|
return hasAll ? (refs as Record<ServiceName, string>) : null;
|
|
789
790
|
}
|
|
790
791
|
|
|
792
|
+
/**
|
|
793
|
+
* Build the set of paths the hot-reload watcher should observe, scoped to
|
|
794
|
+
* each service's `src/` tree, `package.json` manifest, and `Dockerfile`.
|
|
795
|
+
*
|
|
796
|
+
* We deliberately avoid recursively watching whole service directories.
|
|
797
|
+
* Those contain `.claude/` command symlinks — which dangle in a fresh
|
|
798
|
+
* checkout because they point at the separately-cloned `claude-skills`
|
|
799
|
+
* repo — as well as `node_modules`. `fs.watch(dir, { recursive: true })`
|
|
800
|
+
* traverses those entries and emits an unhandled `error` event on a broken
|
|
801
|
+
* symlink, which crashes the CLI process. Source code only ever lives under
|
|
802
|
+
* `src/`, so watching that tree plus the two manifests that drive the image
|
|
803
|
+
* build (`package.json` and `Dockerfile`) preserves hot-reload without
|
|
804
|
+
* walking into symlinked or generated trees. The `Dockerfile` is watched as
|
|
805
|
+
* an individual file for the same reason — editing build steps should
|
|
806
|
+
* trigger a rebuild, but the file sits next to the symlinked trees we avoid.
|
|
807
|
+
*
|
|
808
|
+
* Returning a plain record keeps this trivially unit-testable — see
|
|
809
|
+
* `__tests__/docker.test.ts`.
|
|
810
|
+
*/
|
|
811
|
+
export function collectWatchTargets(repoRoot: string): {
|
|
812
|
+
dirs: string[];
|
|
813
|
+
files: string[];
|
|
814
|
+
} {
|
|
815
|
+
const packagesDir = join(repoRoot, "packages");
|
|
816
|
+
const packageRoots = existsSync(packagesDir)
|
|
817
|
+
? readdirSync(packagesDir, { withFileTypes: true })
|
|
818
|
+
.filter((entry) => entry.isDirectory())
|
|
819
|
+
.map((entry) => join(packagesDir, entry.name))
|
|
820
|
+
: [];
|
|
821
|
+
|
|
822
|
+
const serviceRoots = [
|
|
823
|
+
join(repoRoot, "assistant"),
|
|
824
|
+
join(repoRoot, "credential-executor"),
|
|
825
|
+
join(repoRoot, "gateway"),
|
|
826
|
+
...packageRoots,
|
|
827
|
+
];
|
|
828
|
+
|
|
829
|
+
const dirs: string[] = [];
|
|
830
|
+
const files: string[] = [];
|
|
831
|
+
for (const root of serviceRoots) {
|
|
832
|
+
const srcDir = join(root, "src");
|
|
833
|
+
if (existsSync(srcDir)) dirs.push(srcDir);
|
|
834
|
+
for (const name of ["package.json", "Dockerfile"]) {
|
|
835
|
+
const file = join(root, name);
|
|
836
|
+
if (existsSync(file)) files.push(file);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return { dirs, files };
|
|
840
|
+
}
|
|
841
|
+
|
|
791
842
|
/**
|
|
792
843
|
* Determine which services are affected by a changed file path relative
|
|
793
844
|
* to the repository root.
|
|
@@ -821,9 +872,10 @@ function affectedServices(
|
|
|
821
872
|
}
|
|
822
873
|
|
|
823
874
|
/**
|
|
824
|
-
* Watch for
|
|
825
|
-
* and packages
|
|
826
|
-
*
|
|
875
|
+
* Watch for source changes across the assistant, gateway, credential-executor,
|
|
876
|
+
* and packages services — scoped to each service's `src/` tree, `package.json`,
|
|
877
|
+
* and `Dockerfile` (see `collectWatchTargets`). When changes are detected,
|
|
878
|
+
* rebuild the affected images and restart their containers.
|
|
827
879
|
*/
|
|
828
880
|
function startFileWatcher(opts: {
|
|
829
881
|
signingKey?: string;
|
|
@@ -837,12 +889,7 @@ function startFileWatcher(opts: {
|
|
|
837
889
|
}): () => void {
|
|
838
890
|
const { gatewayPort, imageTags, instanceName, repoRoot, res } = opts;
|
|
839
891
|
|
|
840
|
-
const watchDirs =
|
|
841
|
-
join(repoRoot, "assistant"),
|
|
842
|
-
join(repoRoot, "credential-executor"),
|
|
843
|
-
join(repoRoot, "gateway"),
|
|
844
|
-
join(repoRoot, "packages"),
|
|
845
|
-
];
|
|
892
|
+
const { dirs: watchDirs, files: watchFiles } = collectWatchTargets(repoRoot);
|
|
846
893
|
|
|
847
894
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
848
895
|
let pendingServices = new Set<ServiceName>();
|
|
@@ -919,37 +966,53 @@ function startFileWatcher(opts: {
|
|
|
919
966
|
|
|
920
967
|
const watchers: ReturnType<typeof fsWatch>[] = [];
|
|
921
968
|
|
|
969
|
+
function onChange(fullPath: string): void {
|
|
970
|
+
const services = affectedServices(fullPath, repoRoot);
|
|
971
|
+
if (services.size === 0) return;
|
|
972
|
+
|
|
973
|
+
for (const s of services) {
|
|
974
|
+
pendingServices.add(s);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
978
|
+
debounceTimer = setTimeout(() => {
|
|
979
|
+
debounceTimer = null;
|
|
980
|
+
rebuildAndRestart();
|
|
981
|
+
}, 500);
|
|
982
|
+
}
|
|
983
|
+
|
|
922
984
|
for (const dir of watchDirs) {
|
|
923
|
-
if (!existsSync(dir)) continue;
|
|
924
985
|
const watcher = fsWatch(dir, { recursive: true }, (_event, filename) => {
|
|
925
986
|
if (!filename) return;
|
|
926
|
-
if (
|
|
927
|
-
filename.includes("node_modules") ||
|
|
928
|
-
filename.includes(".env") ||
|
|
929
|
-
filename.startsWith(".")
|
|
930
|
-
) {
|
|
987
|
+
if (filename.includes("node_modules") || filename.includes(".env")) {
|
|
931
988
|
return;
|
|
932
989
|
}
|
|
990
|
+
onChange(join(dir, filename));
|
|
991
|
+
});
|
|
992
|
+
// fs.watch surfaces transient errors (e.g. an unreadable entry) as an
|
|
993
|
+
// `error` event, which would otherwise crash the process. Log and keep
|
|
994
|
+
// the remaining watchers running.
|
|
995
|
+
watcher.on("error", (err) => {
|
|
996
|
+
console.error(
|
|
997
|
+
`⚠️ File watcher error for ${dir}: ${err instanceof Error ? err.message : err}`,
|
|
998
|
+
);
|
|
999
|
+
});
|
|
1000
|
+
watchers.push(watcher);
|
|
1001
|
+
}
|
|
933
1002
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
943
|
-
debounceTimer = setTimeout(() => {
|
|
944
|
-
debounceTimer = null;
|
|
945
|
-
rebuildAndRestart();
|
|
946
|
-
}, 500);
|
|
1003
|
+
for (const file of watchFiles) {
|
|
1004
|
+
const watcher = fsWatch(file, () => onChange(file));
|
|
1005
|
+
watcher.on("error", (err) => {
|
|
1006
|
+
console.error(
|
|
1007
|
+
`⚠️ File watcher error for ${file}: ${err instanceof Error ? err.message : err}`,
|
|
1008
|
+
);
|
|
947
1009
|
});
|
|
948
1010
|
watchers.push(watcher);
|
|
949
1011
|
}
|
|
950
1012
|
|
|
951
1013
|
console.log("👀 Watching for file changes in:");
|
|
952
|
-
console.log("
|
|
1014
|
+
console.log(" <service>/src, <service>/package.json, <service>/Dockerfile");
|
|
1015
|
+
console.log(" for assistant/, gateway/, credential-executor/, packages/*");
|
|
953
1016
|
console.log("");
|
|
954
1017
|
|
|
955
1018
|
return () => {
|