vyft 0.3.0-alpha → 0.4.0-alpha
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/dist/build.d.ts +1 -0
- package/dist/build.js +9 -4
- package/dist/cli.js +491 -47
- package/dist/docker.d.ts +10 -3
- package/dist/docker.js +173 -91
- package/dist/exec.d.ts +1 -1
- package/dist/exec.js +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/local/dev.d.ts +31 -0
- package/dist/local/dev.js +109 -0
- package/dist/local/index.d.ts +2 -0
- package/dist/local/index.js +2 -0
- package/dist/local/runtime.d.ts +61 -0
- package/dist/local/runtime.js +391 -0
- package/dist/resource.d.ts +7 -0
- package/dist/runtime.d.ts +7 -1
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.js +23 -8
- package/dist/symbols.d.ts +2 -0
- package/dist/symbols.js +2 -0
- package/package.json +3 -1
- package/templates/fullstack/package.json +2 -6
- package/templates/fullstack/compose.yaml +0 -14
package/dist/docker.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Logger } from "pino";
|
|
2
2
|
import type { ReverseProxy } from "./proxy.js";
|
|
3
|
-
import type { Resource, ResourceType } from "./resource.js";
|
|
4
|
-
import type { Runtime } from "./runtime.js";
|
|
3
|
+
import type { Resource, ResourceType, Service } from "./resource.js";
|
|
4
|
+
import type { CreateResult, Runtime } from "./runtime.js";
|
|
5
5
|
export declare function parseRoute(route: string): {
|
|
6
6
|
host: string;
|
|
7
7
|
path?: string;
|
|
@@ -17,8 +17,10 @@ export declare class DockerClient implements Runtime {
|
|
|
17
17
|
private docker;
|
|
18
18
|
private localDocker;
|
|
19
19
|
private isRemote;
|
|
20
|
+
private sshClient;
|
|
20
21
|
private project;
|
|
21
22
|
private secretValues;
|
|
23
|
+
private builtImages;
|
|
22
24
|
private log;
|
|
23
25
|
proxy: ReverseProxy;
|
|
24
26
|
verbose: boolean;
|
|
@@ -26,10 +28,14 @@ export declare class DockerClient implements Runtime {
|
|
|
26
28
|
host?: string;
|
|
27
29
|
parentLogger?: Logger;
|
|
28
30
|
});
|
|
31
|
+
/** Close the SSH connection (if any), allowing the process to exit. */
|
|
32
|
+
destroy(): void;
|
|
29
33
|
private ensureNetwork;
|
|
30
34
|
ensureInfrastructure(): Promise<void>;
|
|
31
35
|
listManagedResources(): Promise<ManagedResource[]>;
|
|
32
|
-
create(resource: Resource
|
|
36
|
+
create(resource: Resource, options?: {
|
|
37
|
+
silent?: boolean;
|
|
38
|
+
}): Promise<CreateResult>;
|
|
33
39
|
exists(resource: Resource): Promise<boolean>;
|
|
34
40
|
remove(resource: Resource): Promise<void>;
|
|
35
41
|
private createVolume;
|
|
@@ -42,6 +48,7 @@ export declare class DockerClient implements Runtime {
|
|
|
42
48
|
private lookupSecretId;
|
|
43
49
|
private createDerivedSecret;
|
|
44
50
|
private resolveEnv;
|
|
51
|
+
buildImage(service: Service): Promise<string | null>;
|
|
45
52
|
private createService;
|
|
46
53
|
private serviceExists;
|
|
47
54
|
waitForHealthy(id: string, timeoutMs?: number): Promise<void>;
|
package/dist/docker.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import http from "node:http";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { PassThrough } from "node:stream";
|
|
4
|
-
import { log } from "@clack/prompts";
|
|
5
5
|
import Docker from "dockerode";
|
|
6
|
+
import { Client as SshClient } from "ssh2";
|
|
6
7
|
import tar from "tar-fs";
|
|
7
8
|
import { buildStatic } from "./build.js";
|
|
8
9
|
import { logger as defaultLogger } from "./logger.js";
|
|
@@ -55,21 +56,74 @@ export function parseRoute(route) {
|
|
|
55
56
|
path: route.slice(slashIndex),
|
|
56
57
|
};
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Create a Docker client that reuses a single SSH connection instead of
|
|
61
|
+
* opening a new one for every API call (the docker-modem default).
|
|
62
|
+
*/
|
|
63
|
+
function createSshDocker(sshUrl) {
|
|
64
|
+
const parsed = new URL(sshUrl);
|
|
65
|
+
const conn = new SshClient();
|
|
66
|
+
let connected = false;
|
|
67
|
+
const ready = new Promise((resolve, reject) => {
|
|
68
|
+
conn.once("ready", () => {
|
|
69
|
+
connected = true;
|
|
70
|
+
resolve();
|
|
71
|
+
});
|
|
72
|
+
conn.once("error", reject);
|
|
73
|
+
conn.connect({
|
|
74
|
+
host: parsed.hostname,
|
|
75
|
+
port: parsed.port ? parseInt(parsed.port, 10) : 22,
|
|
76
|
+
username: parsed.username || undefined,
|
|
77
|
+
agent: process.env.SSH_AUTH_SOCK,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
const agent = new http.Agent({ keepAlive: false });
|
|
81
|
+
// biome-ignore lint/suspicious/noExplicitAny: monkey-patching internal Agent method for SSH tunneling
|
|
82
|
+
agent.createConnection = (_opts, fn) => {
|
|
83
|
+
const exec = () => conn.exec("docker system dial-stdio", (err, stream) => {
|
|
84
|
+
if (err)
|
|
85
|
+
return fn(err);
|
|
86
|
+
fn(null, stream);
|
|
87
|
+
});
|
|
88
|
+
if (connected)
|
|
89
|
+
exec();
|
|
90
|
+
else
|
|
91
|
+
ready.then(exec).catch((err) => fn(err));
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
docker: new Docker({
|
|
95
|
+
protocol: "http",
|
|
96
|
+
host: parsed.hostname,
|
|
97
|
+
agent,
|
|
98
|
+
// biome-ignore lint/suspicious/noExplicitAny: dockerode typing doesn't expose agent option
|
|
99
|
+
}),
|
|
100
|
+
sshClient: conn,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
58
103
|
export class DockerClient {
|
|
59
104
|
docker;
|
|
60
105
|
localDocker;
|
|
61
106
|
isRemote;
|
|
107
|
+
sshClient = null;
|
|
62
108
|
project;
|
|
63
109
|
secretValues = new Map();
|
|
110
|
+
builtImages = new Map();
|
|
64
111
|
log;
|
|
65
112
|
proxy;
|
|
66
113
|
verbose = false;
|
|
67
114
|
constructor(project, opts) {
|
|
68
115
|
this.localDocker = new Docker({ socketPath: "/var/run/docker.sock" });
|
|
69
116
|
const host = opts?.host;
|
|
70
|
-
if (host)
|
|
71
|
-
|
|
72
|
-
|
|
117
|
+
if (host?.startsWith("ssh://")) {
|
|
118
|
+
const { docker, sshClient } = createSshDocker(host);
|
|
119
|
+
this.docker = docker;
|
|
120
|
+
this.sshClient = sshClient;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
if (host)
|
|
124
|
+
process.env.DOCKER_HOST = host;
|
|
125
|
+
this.docker = new Docker();
|
|
126
|
+
}
|
|
73
127
|
this.isRemote = !!host;
|
|
74
128
|
this.project = project;
|
|
75
129
|
this.log = (opts?.parentLogger ?? defaultLogger).child({
|
|
@@ -80,6 +134,11 @@ export class DockerClient {
|
|
|
80
134
|
if (host)
|
|
81
135
|
this.log.debug({ host }, "using remote endpoint");
|
|
82
136
|
}
|
|
137
|
+
/** Close the SSH connection (if any), allowing the process to exit. */
|
|
138
|
+
destroy() {
|
|
139
|
+
this.sshClient?.end();
|
|
140
|
+
this.sshClient = null;
|
|
141
|
+
}
|
|
83
142
|
async ensureNetwork(name, labels = {}) {
|
|
84
143
|
const networks = await this.docker.listNetworks({
|
|
85
144
|
filters: JSON.stringify({ name: [name] }),
|
|
@@ -153,7 +212,7 @@ export class DockerClient {
|
|
|
153
212
|
}
|
|
154
213
|
return resources;
|
|
155
214
|
}
|
|
156
|
-
async create(resource) {
|
|
215
|
+
async create(resource, options) {
|
|
157
216
|
switch (resource.type) {
|
|
158
217
|
case "volume":
|
|
159
218
|
return this.createVolume(resource);
|
|
@@ -162,7 +221,7 @@ export class DockerClient {
|
|
|
162
221
|
case "service":
|
|
163
222
|
return this.createService(resource);
|
|
164
223
|
case "site":
|
|
165
|
-
return this.createStatic(resource);
|
|
224
|
+
return this.createStatic(resource, options?.silent);
|
|
166
225
|
}
|
|
167
226
|
}
|
|
168
227
|
async exists(resource) {
|
|
@@ -205,9 +264,9 @@ export class DockerClient {
|
|
|
205
264
|
Name: volume.id,
|
|
206
265
|
Labels: labels,
|
|
207
266
|
});
|
|
208
|
-
const
|
|
209
|
-
this.log.debug({ volumeName: volume.id, durationMs }, "volume created");
|
|
210
|
-
|
|
267
|
+
const duration = Math.round(performance.now() - start);
|
|
268
|
+
this.log.debug({ volumeName: volume.id, durationMs: duration }, "volume created");
|
|
269
|
+
return { action: "created", duration };
|
|
211
270
|
}
|
|
212
271
|
async volumeExists(id) {
|
|
213
272
|
const start = performance.now();
|
|
@@ -229,7 +288,6 @@ export class DockerClient {
|
|
|
229
288
|
await this.docker.getVolume(id).remove();
|
|
230
289
|
const durationMs = Math.round(performance.now() - start);
|
|
231
290
|
this.log.debug({ volumeName: id, durationMs }, "volume removed");
|
|
232
|
-
log.step(`Removed ${id}`);
|
|
233
291
|
}
|
|
234
292
|
async createSecret(secret) {
|
|
235
293
|
const start = performance.now();
|
|
@@ -240,9 +298,9 @@ export class DockerClient {
|
|
|
240
298
|
await this.storeSecretData(secret.id, value, {
|
|
241
299
|
"vyft.length": length.toString(),
|
|
242
300
|
});
|
|
243
|
-
const
|
|
244
|
-
this.log.debug({ secretName: secret.id, length, durationMs }, "secret created");
|
|
245
|
-
|
|
301
|
+
const duration = Math.round(performance.now() - start);
|
|
302
|
+
this.log.debug({ secretName: secret.id, length, durationMs: duration }, "secret created");
|
|
303
|
+
return { action: "created", duration };
|
|
246
304
|
}
|
|
247
305
|
async secretExists(id) {
|
|
248
306
|
const start = performance.now();
|
|
@@ -264,7 +322,6 @@ export class DockerClient {
|
|
|
264
322
|
await this.docker.getSecret(id).remove();
|
|
265
323
|
const durationMs = Math.round(performance.now() - start);
|
|
266
324
|
this.log.debug({ secretName: id, durationMs }, "secret removed");
|
|
267
|
-
log.step(`Removed ${id}`);
|
|
268
325
|
}
|
|
269
326
|
async storeSecretData(name, value, labels = {}) {
|
|
270
327
|
this.log.debug({ secretName: name }, "storing secret data");
|
|
@@ -291,7 +348,6 @@ export class DockerClient {
|
|
|
291
348
|
"vyft.derived": "true",
|
|
292
349
|
"vyft.parent-service": parentService,
|
|
293
350
|
});
|
|
294
|
-
log.step(`Created ${name}`);
|
|
295
351
|
}
|
|
296
352
|
async resolveEnv(serviceId, env) {
|
|
297
353
|
const envList = [];
|
|
@@ -349,60 +405,70 @@ export class DockerClient {
|
|
|
349
405
|
this.log.debug({ serviceId, envCount: envList.length, secretMountCount: secrets.length }, "env resolved");
|
|
350
406
|
return { envList, secrets };
|
|
351
407
|
}
|
|
408
|
+
async buildImage(service) {
|
|
409
|
+
const config = service.config;
|
|
410
|
+
if (typeof config.image !== "object")
|
|
411
|
+
return null;
|
|
412
|
+
const { context = ".", dockerfile = "Dockerfile" } = config.image;
|
|
413
|
+
const buildTag = `${service.id}:latest`;
|
|
414
|
+
this.log.debug({ resourceId: service.id, context, dockerfile }, "image build started");
|
|
415
|
+
const buildStart = performance.now();
|
|
416
|
+
const tarStream = tar.pack(path.resolve(context));
|
|
417
|
+
const stream = await this.localDocker.buildImage(tarStream, {
|
|
418
|
+
t: buildTag,
|
|
419
|
+
dockerfile,
|
|
420
|
+
});
|
|
421
|
+
await new Promise((resolve, reject) => {
|
|
422
|
+
this.localDocker.modem.followProgress(stream, (err) => (err ? reject(err) : resolve()), (event) => {
|
|
423
|
+
if (event.error)
|
|
424
|
+
this.log.error({ error: event.error }, "build error");
|
|
425
|
+
else if (this.verbose && event.stream)
|
|
426
|
+
process.stdout.write(event.stream);
|
|
427
|
+
this.log.trace({ event }, "build progress");
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
const buildDurationMs = Math.round(performance.now() - buildStart);
|
|
431
|
+
this.log.debug({
|
|
432
|
+
resourceId: service.id,
|
|
433
|
+
image: buildTag,
|
|
434
|
+
durationMs: buildDurationMs,
|
|
435
|
+
}, "image build completed");
|
|
436
|
+
const images = await this.localDocker.listImages({
|
|
437
|
+
filters: JSON.stringify({ reference: [buildTag] }),
|
|
438
|
+
});
|
|
439
|
+
if (images.length === 0) {
|
|
440
|
+
throw new Error(`Image ${buildTag} was not built successfully`);
|
|
441
|
+
}
|
|
442
|
+
// Tag with content hash so the orchestrator detects the new image
|
|
443
|
+
const shortHash = images[0].Id.replace("sha256:", "").slice(0, 12);
|
|
444
|
+
const imageName = `${service.id}:${shortHash}`;
|
|
445
|
+
await this.localDocker.getImage(buildTag).tag({
|
|
446
|
+
repo: service.id,
|
|
447
|
+
tag: shortHash,
|
|
448
|
+
});
|
|
449
|
+
if (this.isRemote) {
|
|
450
|
+
this.log.debug({ image: imageName }, "transferring image to remote");
|
|
451
|
+
const transferStart = performance.now();
|
|
452
|
+
const imageStream = await this.localDocker.getImage(imageName).get();
|
|
453
|
+
await this.docker.loadImage(imageStream);
|
|
454
|
+
const transferDurationMs = Math.round(performance.now() - transferStart);
|
|
455
|
+
this.log.debug({ image: imageName, durationMs: transferDurationMs }, "image transferred to remote");
|
|
456
|
+
}
|
|
457
|
+
this.builtImages.set(service.id, imageName);
|
|
458
|
+
return imageName;
|
|
459
|
+
}
|
|
352
460
|
async createService(service) {
|
|
353
461
|
const start = performance.now();
|
|
354
|
-
await this.ensureNetwork(`${this.project}-internal`, {
|
|
355
|
-
"com.docker.stack.namespace": this.project,
|
|
356
|
-
"vyft.managed": "true",
|
|
357
|
-
"vyft.project": this.project,
|
|
358
|
-
});
|
|
359
462
|
const config = service.config;
|
|
360
463
|
let imageName;
|
|
361
464
|
if (typeof config.image === "object") {
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const buildStart = performance.now();
|
|
366
|
-
const tarStream = tar.pack(path.resolve(context));
|
|
367
|
-
const stream = await this.localDocker.buildImage(tarStream, {
|
|
368
|
-
t: buildTag,
|
|
369
|
-
dockerfile,
|
|
370
|
-
});
|
|
371
|
-
await new Promise((resolve, reject) => {
|
|
372
|
-
this.localDocker.modem.followProgress(stream, (err) => (err ? reject(err) : resolve()), (event) => {
|
|
373
|
-
if (event.error)
|
|
374
|
-
log.error(event.error);
|
|
375
|
-
else if (this.verbose && event.stream)
|
|
376
|
-
process.stdout.write(event.stream);
|
|
377
|
-
this.log.trace({ event }, "build progress");
|
|
378
|
-
});
|
|
379
|
-
});
|
|
380
|
-
const buildDurationMs = Math.round(performance.now() - buildStart);
|
|
381
|
-
this.log.debug({
|
|
382
|
-
resourceId: service.id,
|
|
383
|
-
image: buildTag,
|
|
384
|
-
durationMs: buildDurationMs,
|
|
385
|
-
}, "image build completed");
|
|
386
|
-
const images = await this.localDocker.listImages({
|
|
387
|
-
filters: JSON.stringify({ reference: [buildTag] }),
|
|
388
|
-
});
|
|
389
|
-
if (images.length === 0) {
|
|
390
|
-
throw new Error(`Image ${buildTag} was not built successfully`);
|
|
465
|
+
const prebuilt = this.builtImages.get(service.id);
|
|
466
|
+
if (prebuilt) {
|
|
467
|
+
imageName = prebuilt;
|
|
391
468
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
await this.localDocker.getImage(buildTag).tag({
|
|
396
|
-
repo: service.id,
|
|
397
|
-
tag: shortHash,
|
|
398
|
-
});
|
|
399
|
-
if (this.isRemote) {
|
|
400
|
-
this.log.debug({ image: imageName }, "transferring image to remote");
|
|
401
|
-
const transferStart = performance.now();
|
|
402
|
-
const imageStream = await this.localDocker.getImage(imageName).get();
|
|
403
|
-
await this.docker.loadImage(imageStream);
|
|
404
|
-
const transferDurationMs = Math.round(performance.now() - transferStart);
|
|
405
|
-
this.log.debug({ image: imageName, durationMs: transferDurationMs }, "image transferred to remote");
|
|
469
|
+
else {
|
|
470
|
+
await this.buildImage(service);
|
|
471
|
+
imageName = this.builtImages.get(service.id);
|
|
406
472
|
}
|
|
407
473
|
}
|
|
408
474
|
else {
|
|
@@ -526,14 +592,18 @@ export class DockerClient {
|
|
|
526
592
|
port,
|
|
527
593
|
});
|
|
528
594
|
}
|
|
529
|
-
const
|
|
595
|
+
const duration = Math.round(performance.now() - start);
|
|
530
596
|
this.log.info({
|
|
531
597
|
resourceId: service.id,
|
|
532
598
|
image: imageName,
|
|
533
599
|
hasRoute: !!config.route,
|
|
534
|
-
durationMs,
|
|
600
|
+
durationMs: duration,
|
|
535
601
|
}, existing ? "service updated" : "service created");
|
|
536
|
-
|
|
602
|
+
return {
|
|
603
|
+
action: existing ? "updated" : "created",
|
|
604
|
+
route: config.route,
|
|
605
|
+
duration,
|
|
606
|
+
};
|
|
537
607
|
}
|
|
538
608
|
async serviceExists(id) {
|
|
539
609
|
try {
|
|
@@ -588,7 +658,6 @@ export class DockerClient {
|
|
|
588
658
|
}
|
|
589
659
|
const durationMs = Math.round(performance.now() - start);
|
|
590
660
|
this.log.info({ resourceId: id, durationMs }, "service removed");
|
|
591
|
-
log.step(`Removed ${id}`);
|
|
592
661
|
}
|
|
593
662
|
async pullImage(image) {
|
|
594
663
|
this.log.debug({ image }, "pulling image");
|
|
@@ -598,19 +667,15 @@ export class DockerClient {
|
|
|
598
667
|
});
|
|
599
668
|
this.log.debug({ image }, "image pulled");
|
|
600
669
|
}
|
|
601
|
-
async createStatic(static_) {
|
|
670
|
+
async createStatic(static_, silent) {
|
|
602
671
|
const start = performance.now();
|
|
603
|
-
await this.ensureNetwork(`${this.project}-internal`, {
|
|
604
|
-
"com.docker.stack.namespace": this.project,
|
|
605
|
-
"vyft.managed": "true",
|
|
606
|
-
"vyft.project": this.project,
|
|
607
|
-
});
|
|
608
672
|
this.log.debug({ resourceId: static_.id, route: static_.config.route }, "creating static site");
|
|
609
673
|
this.log.debug({ resourceId: static_.id }, "static build starting");
|
|
610
674
|
const { outputPath } = await buildStatic(static_.config.build.cwd, {
|
|
611
675
|
output: static_.config.build.output,
|
|
612
676
|
command: static_.config.build.command,
|
|
613
677
|
env: static_.config.build.env,
|
|
678
|
+
silent,
|
|
614
679
|
}, this.log);
|
|
615
680
|
const volumeName = `${static_.id}-files`;
|
|
616
681
|
if (!(await this.volumeExists(volumeName))) {
|
|
@@ -706,9 +771,17 @@ export class DockerClient {
|
|
|
706
771
|
host: static_.id,
|
|
707
772
|
port: 80,
|
|
708
773
|
});
|
|
709
|
-
const
|
|
710
|
-
this.log.info({
|
|
711
|
-
|
|
774
|
+
const duration = Math.round(performance.now() - start);
|
|
775
|
+
this.log.info({
|
|
776
|
+
resourceId: static_.id,
|
|
777
|
+
route: static_.config.route,
|
|
778
|
+
durationMs: duration,
|
|
779
|
+
}, existing ? "site updated" : "site created");
|
|
780
|
+
return {
|
|
781
|
+
action: existing ? "updated" : "created",
|
|
782
|
+
route: static_.config.route,
|
|
783
|
+
duration,
|
|
784
|
+
};
|
|
712
785
|
}
|
|
713
786
|
async removeStatic(id) {
|
|
714
787
|
const start = performance.now();
|
|
@@ -744,7 +817,6 @@ export class DockerClient {
|
|
|
744
817
|
}
|
|
745
818
|
const durationMs = Math.round(performance.now() - start);
|
|
746
819
|
this.log.info({ resourceId: id, durationMs }, "site removed");
|
|
747
|
-
log.step(`Removed ${id}`);
|
|
748
820
|
}
|
|
749
821
|
async *serviceLogs(options) {
|
|
750
822
|
const allServices = await this.docker.listServices({
|
|
@@ -765,26 +837,29 @@ export class DockerClient {
|
|
|
765
837
|
});
|
|
766
838
|
}
|
|
767
839
|
if (matched.length === 0) {
|
|
768
|
-
log.warn("
|
|
840
|
+
this.log.warn("no matching services found");
|
|
769
841
|
return;
|
|
770
842
|
}
|
|
771
843
|
const tail = options.tail ?? 100;
|
|
772
|
-
// Collect all containers with their short service name
|
|
773
|
-
const targets =
|
|
774
|
-
for (const svc of matched) {
|
|
844
|
+
// Collect all containers with their short service name (parallel)
|
|
845
|
+
const targets = (await Promise.all(matched.map(async (svc) => {
|
|
775
846
|
const name = svc.Spec?.Name || "";
|
|
776
|
-
const short = name.startsWith(prefix)
|
|
847
|
+
const short = name.startsWith(prefix)
|
|
848
|
+
? name.slice(prefix.length)
|
|
849
|
+
: name;
|
|
777
850
|
const containers = await this.docker.listContainers({
|
|
778
851
|
filters: JSON.stringify({
|
|
779
852
|
label: [`com.docker.swarm.service.name=${name}`],
|
|
780
853
|
}),
|
|
781
854
|
});
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
855
|
+
return containers.map((cInfo) => ({
|
|
856
|
+
container: this.docker.getContainer(cInfo.Id),
|
|
857
|
+
short,
|
|
858
|
+
}));
|
|
859
|
+
}))).flat();
|
|
786
860
|
if (!options.follow) {
|
|
787
|
-
|
|
861
|
+
// Fetch all logs in parallel
|
|
862
|
+
const results = await Promise.all(targets.map(async ({ container, short }) => {
|
|
788
863
|
const buf = await container.logs({
|
|
789
864
|
stdout: true,
|
|
790
865
|
stderr: true,
|
|
@@ -792,19 +867,26 @@ export class DockerClient {
|
|
|
792
867
|
tail,
|
|
793
868
|
timestamps: true,
|
|
794
869
|
});
|
|
870
|
+
const entries = [];
|
|
795
871
|
let offset = 0;
|
|
796
872
|
while (offset + 8 <= buf.length) {
|
|
797
873
|
const type = buf[offset];
|
|
798
874
|
const size = buf.readUInt32BE(offset + 4);
|
|
799
875
|
offset += 8;
|
|
800
876
|
const text = buf.subarray(offset, offset + size).toString();
|
|
801
|
-
|
|
877
|
+
entries.push({
|
|
802
878
|
service: short,
|
|
803
879
|
stream: type === 2 ? "stderr" : "stdout",
|
|
804
880
|
text,
|
|
805
|
-
};
|
|
881
|
+
});
|
|
806
882
|
offset += size;
|
|
807
883
|
}
|
|
884
|
+
return entries;
|
|
885
|
+
}));
|
|
886
|
+
for (const entries of results) {
|
|
887
|
+
for (const entry of entries) {
|
|
888
|
+
yield entry;
|
|
889
|
+
}
|
|
808
890
|
}
|
|
809
891
|
return;
|
|
810
892
|
}
|
|
@@ -820,7 +902,7 @@ export class DockerClient {
|
|
|
820
902
|
n();
|
|
821
903
|
}
|
|
822
904
|
};
|
|
823
|
-
|
|
905
|
+
await Promise.all(targets.map(async ({ container, short }) => {
|
|
824
906
|
const stream = await container.logs({
|
|
825
907
|
stdout: true,
|
|
826
908
|
stderr: true,
|
|
@@ -851,7 +933,7 @@ export class DockerClient {
|
|
|
851
933
|
n();
|
|
852
934
|
}
|
|
853
935
|
});
|
|
854
|
-
}
|
|
936
|
+
}));
|
|
855
937
|
while (active > 0 || buffer.length > 0) {
|
|
856
938
|
if (buffer.length === 0) {
|
|
857
939
|
await new Promise((r) => {
|
package/dist/exec.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { Logger } from "pino";
|
|
2
|
-
export declare function exec(command: string, cwd: string, env?: Record<string, string>, log?: Logger): Promise<void>;
|
|
2
|
+
export declare function exec(command: string, cwd: string, env?: Record<string, string>, log?: Logger, silent?: boolean): Promise<void>;
|
package/dist/exec.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
export async function exec(command, cwd, env, log) {
|
|
2
|
+
export async function exec(command, cwd, env, log, silent) {
|
|
3
3
|
const start = performance.now();
|
|
4
4
|
log?.debug({ command, cwd }, "process spawning");
|
|
5
5
|
return new Promise((resolve, reject) => {
|
|
6
6
|
const proc = spawn(command, {
|
|
7
7
|
cwd,
|
|
8
|
-
stdio: "inherit",
|
|
8
|
+
stdio: silent ? "pipe" : "inherit",
|
|
9
9
|
shell: true,
|
|
10
10
|
env: env ? { ...process.env, ...env } : undefined,
|
|
11
11
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type { EnvValue, HealthCheckConfig, Interpolation, Reference, Resource, R
|
|
|
3
3
|
export { isInterpolation, isReference, isSecret, validateId, validateRoute, } from "./resource.js";
|
|
4
4
|
export type { Runtime } from "./runtime.js";
|
|
5
5
|
export type { BackupConfig, Bucket, Minio, MinioConfig, Mongo, MongoConfig, Mysql, MysqlConfig, Nats, NatsConfig, Postgres, PostgresConfig, Rabbitmq, RabbitmqConfig, Redis, RedisConfig, Storage, StorageConfig, } from "./services/index.js";
|
|
6
|
+
export { isManaged } from "./services/index.js";
|
|
6
7
|
export type { RuntimeMeta, RuntimeRef } from "./symbols.js";
|
|
7
|
-
export { VYFT_RUNTIME } from "./symbols.js";
|
|
8
|
+
export { VYFT_MANAGED, VYFT_RUNTIME } from "./symbols.js";
|
|
8
9
|
export declare const service: (id: string, config: import("./swarm/types.js").SwarmServiceConfig) => import("./resource.js").Service, secret: (id: string, config?: import("./resource.js").SecretConfig) => import("./resource.js").Secret, volume: (id: string, config?: import("./resource.js").VolumeConfig) => import("./resource.js").Volume, site: (id: string, config: import("./resource.js").SiteConfig) => import("./resource.js").Site, postgres: (id: string, config?: import("./index.js").PostgresConfig) => import("./index.js").Postgres, mysql: (id: string, config?: import("./index.js").MysqlConfig) => import("./index.js").Mysql, redis: (id: string, config?: import("./index.js").RedisConfig) => import("./index.js").Redis, rabbitmq: (id: string, config?: import("./index.js").RabbitmqConfig) => import("./index.js").Rabbitmq, nats: (id: string, config?: import("./index.js").NatsConfig) => import("./index.js").Nats, mongo: (id: string, config?: import("./index.js").MongoConfig) => import("./index.js").Mongo, minio: (id: string, config?: import("./index.js").MinioConfig) => import("./index.js").Minio, storage: (id: string, config?: import("./index.js").StorageConfig) => import("./index.js").Storage;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { interpolate } from "./interpolate.js";
|
|
2
2
|
export { isInterpolation, isReference, isSecret, validateId, validateRoute, } from "./resource.js";
|
|
3
|
-
export {
|
|
3
|
+
export { isManaged } from "./services/index.js";
|
|
4
|
+
export { VYFT_MANAGED, VYFT_RUNTIME } from "./symbols.js";
|
|
4
5
|
import { createFactories } from "./swarm/factories.js";
|
|
5
6
|
const factories = createFactories({});
|
|
6
7
|
export const { service, secret, volume, site, postgres, mysql, redis, rabbitmq, nats, mongo, minio, storage, } = factories;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { EnvValue, Service } from "../resource.js";
|
|
2
|
+
export interface DevProcess {
|
|
3
|
+
service: Service;
|
|
4
|
+
command: string;
|
|
5
|
+
cwd: string;
|
|
6
|
+
env: Record<string, string>;
|
|
7
|
+
}
|
|
8
|
+
type LogHandler = (service: string, stream: "stdout" | "stderr", text: string) => void;
|
|
9
|
+
/**
|
|
10
|
+
* Manages user service child processes for local development.
|
|
11
|
+
*/
|
|
12
|
+
export declare class DevRunner {
|
|
13
|
+
private processes;
|
|
14
|
+
private onLog;
|
|
15
|
+
constructor(opts?: {
|
|
16
|
+
onLog?: LogHandler;
|
|
17
|
+
});
|
|
18
|
+
start(devProcesses: DevProcess[]): void;
|
|
19
|
+
stop(): void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve env vars for a dev process.
|
|
23
|
+
*
|
|
24
|
+
* - Secrets → generated plaintext value
|
|
25
|
+
* - Interpolations → fully resolved, with known service hosts remapped to localhost
|
|
26
|
+
* - Plain strings → as-is, with known service hosts remapped to localhost
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveDevEnv(env: Record<string, EnvValue>, secretValues: Map<string, string>, hostRemap: Map<string, string>): Record<string, string>;
|
|
29
|
+
/** Detect the project's package manager from lock files. */
|
|
30
|
+
export declare function detectPackageManager(projectRoot: string): string;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isInterpolation, isReference } from "../resource.js";
|
|
5
|
+
/**
|
|
6
|
+
* Manages user service child processes for local development.
|
|
7
|
+
*/
|
|
8
|
+
export class DevRunner {
|
|
9
|
+
processes = [];
|
|
10
|
+
onLog;
|
|
11
|
+
constructor(opts) {
|
|
12
|
+
this.onLog = opts?.onLog;
|
|
13
|
+
}
|
|
14
|
+
start(devProcesses) {
|
|
15
|
+
for (const dp of devProcesses) {
|
|
16
|
+
const child = spawn(dp.command, {
|
|
17
|
+
cwd: dp.cwd,
|
|
18
|
+
env: { ...process.env, ...dp.env },
|
|
19
|
+
shell: true,
|
|
20
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
21
|
+
});
|
|
22
|
+
const name = dp.service.id;
|
|
23
|
+
child.stdout?.on("data", (chunk) => {
|
|
24
|
+
for (const line of chunk.toString().split("\n")) {
|
|
25
|
+
if (line)
|
|
26
|
+
this.onLog?.(name, "stdout", line);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
child.stderr?.on("data", (chunk) => {
|
|
30
|
+
for (const line of chunk.toString().split("\n")) {
|
|
31
|
+
if (line)
|
|
32
|
+
this.onLog?.(name, "stderr", line);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
child.on("exit", (code) => {
|
|
36
|
+
this.onLog?.(name, "stderr", `process exited with code ${code}`);
|
|
37
|
+
});
|
|
38
|
+
this.processes.push(child);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
stop() {
|
|
42
|
+
for (const child of this.processes) {
|
|
43
|
+
child.kill("SIGTERM");
|
|
44
|
+
}
|
|
45
|
+
this.processes = [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve env vars for a dev process.
|
|
50
|
+
*
|
|
51
|
+
* - Secrets → generated plaintext value
|
|
52
|
+
* - Interpolations → fully resolved, with known service hosts remapped to localhost
|
|
53
|
+
* - Plain strings → as-is, with known service hosts remapped to localhost
|
|
54
|
+
*/
|
|
55
|
+
export function resolveDevEnv(env, secretValues, hostRemap) {
|
|
56
|
+
const result = {};
|
|
57
|
+
for (const [key, value] of Object.entries(env)) {
|
|
58
|
+
if (typeof value === "string") {
|
|
59
|
+
result[key] = remapHosts(value, hostRemap);
|
|
60
|
+
}
|
|
61
|
+
else if (isReference(value)) {
|
|
62
|
+
const secretValue = secretValues.get(value.id);
|
|
63
|
+
if (!secretValue) {
|
|
64
|
+
throw new Error(`Secret ${value.id} not yet generated`);
|
|
65
|
+
}
|
|
66
|
+
result[key] = secretValue;
|
|
67
|
+
}
|
|
68
|
+
else if (isInterpolation(value)) {
|
|
69
|
+
const parts = [];
|
|
70
|
+
for (let i = 0; i < value.strings.length; i++) {
|
|
71
|
+
parts.push(value.strings[i]);
|
|
72
|
+
if (i < value.values.length) {
|
|
73
|
+
const v = value.values[i];
|
|
74
|
+
if (isReference(v)) {
|
|
75
|
+
const secretValue = secretValues.get(v.id);
|
|
76
|
+
if (!secretValue) {
|
|
77
|
+
throw new Error(`Secret ${v.id} not yet generated`);
|
|
78
|
+
}
|
|
79
|
+
parts.push(secretValue);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
parts.push(remapHosts(v, hostRemap));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
result[key] = parts.join("");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
function remapHosts(value, hostRemap) {
|
|
92
|
+
let result = value;
|
|
93
|
+
for (const [from, to] of hostRemap) {
|
|
94
|
+
result = result.replaceAll(from, to);
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
/** Detect the project's package manager from lock files. */
|
|
99
|
+
export function detectPackageManager(projectRoot) {
|
|
100
|
+
if (existsSync(path.join(projectRoot, "bun.lock")))
|
|
101
|
+
return "bun";
|
|
102
|
+
if (existsSync(path.join(projectRoot, "bun.lockb")))
|
|
103
|
+
return "bun";
|
|
104
|
+
if (existsSync(path.join(projectRoot, "pnpm-lock.yaml")))
|
|
105
|
+
return "pnpm";
|
|
106
|
+
if (existsSync(path.join(projectRoot, "yarn.lock")))
|
|
107
|
+
return "yarn";
|
|
108
|
+
return "npm";
|
|
109
|
+
}
|