@gachlab/devup 0.5.0 → 0.7.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/CHANGELOG.md +30 -0
- package/README.md +74 -454
- package/dist/config/cli.d.ts +2 -1
- package/dist/config/cli.d.ts.map +1 -1
- package/dist/config/diff.d.ts +19 -0
- package/dist/config/diff.d.ts.map +1 -0
- package/dist/config/validator.d.ts +9 -0
- package/dist/config/validator.d.ts.map +1 -1
- package/dist/control-plane/socket-server.d.ts +31 -0
- package/dist/control-plane/socket-server.d.ts.map +1 -0
- package/dist/index.js +347 -19
- package/dist/index.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/LogsPanel.d.ts +9 -1
- package/dist/tui/LogsPanel.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/config/cli.d.ts
CHANGED
|
@@ -17,8 +17,9 @@ export interface CliArgs {
|
|
|
17
17
|
onceTimeout: number;
|
|
18
18
|
logFile: boolean;
|
|
19
19
|
logDir?: string;
|
|
20
|
+
watchConfig: boolean;
|
|
20
21
|
}
|
|
21
|
-
export declare const USAGE = "devup \u2014 terminal UI dev stack runner\n\nUsage: devup [options]\n\nService selection:\n --only apis | webs Start only APIs or only webs\n --services a,b,c Start only the named services\n --profile <name> Start the services in a named profile (see ROADMAP)\n --skip a,b,c Start everything except these\n --config <path> Use a custom config file\n\nLazy mode:\n --lazy Enable lazy mode (default)\n --no-lazy Start every service immediately\n --timeout <minutes> Idle timeout for lazy services. Default: 10\n\nReverse proxy:\n --proxy Enable proxy config generation\n --proxy-host <host> Override the target host (Docker/local)\n --proxy-conf <path> Override the generated config file path\n --proxy-tls Enable TLS in the generated config (default)\n --no-proxy-tls Disable TLS\n --proxy-entrypoint <n> Override entrypoint name (Traefik only)\n\nCI / scripting:\n --dry-run Print the resolved boot plan and exit\n --once Boot, wait for readiness, exit 0/1 (no TUI)\n --once-timeout <s> Max seconds to wait in --once mode. Default: 90\n\nLog files:\n --no-log-file Disable persistent log files\n --log-dir <path> Override log root (default: ~/.devup/logs)\n\nOther:\n -h, --help Show this help and exit\n -v, --version Show version and exit\n\nSee https://github.com/gachlab/devup for the full documentation.";
|
|
22
|
+
export declare const USAGE = "devup \u2014 terminal UI dev stack runner\n\nUsage: devup [options]\n\nService selection:\n --only apis | webs Start only APIs or only webs\n --services a,b,c Start only the named services\n --profile <name> Start the services in a named profile (see ROADMAP)\n --skip a,b,c Start everything except these\n --config <path> Use a custom config file\n\nLazy mode:\n --lazy Enable lazy mode (default)\n --no-lazy Start every service immediately\n --timeout <minutes> Idle timeout for lazy services. Default: 10\n\nReverse proxy:\n --proxy Enable proxy config generation\n --proxy-host <host> Override the target host (Docker/local)\n --proxy-conf <path> Override the generated config file path\n --proxy-tls Enable TLS in the generated config (default)\n --no-proxy-tls Disable TLS\n --proxy-entrypoint <n> Override entrypoint name (Traefik only)\n\nCI / scripting:\n --dry-run Print the resolved boot plan and exit\n --once Boot, wait for readiness, exit 0/1 (no TUI)\n --once-timeout <s> Max seconds to wait in --once mode. Default: 90\n\nLog files:\n --no-log-file Disable persistent log files\n --log-dir <path> Override log root (default: ~/.devup/logs)\n\nHot reload:\n --watch-config Watch devup.config.* and apply add/remove/restart\n service changes without exiting the TUI\n\nOther:\n -h, --help Show this help and exit\n -v, --version Show version and exit\n\nSee https://github.com/gachlab/devup for the full documentation.";
|
|
22
23
|
export declare function parseCliArgs(argv: string[]): CliArgs;
|
|
23
24
|
export declare function filterServices(services: ServiceConfig[], args: CliArgs, config?: Pick<DevStackConfig, 'profiles'>): ServiceConfig[];
|
|
24
25
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/config/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/config/cli.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhE,MAAM,WAAW,OAAO;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/config/cli.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhE,MAAM,WAAW,OAAO;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;CACtB;AAKD,eAAO,MAAM,KAAK,+qDAyC+C,CAAC;AAElE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CA4CpD;AAED,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,aAAa,EAAE,EACzB,IAAI,EAAE,OAAO,EACb,MAAM,CAAC,EAAE,IAAI,CAAC,cAAc,EAAE,UAAU,CAAC,GACxC,aAAa,EAAE,CA6BjB"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ServiceConfig } from './types.js';
|
|
2
|
+
export interface ServiceDiff {
|
|
3
|
+
added: ServiceConfig[];
|
|
4
|
+
removed: string[];
|
|
5
|
+
changed: Array<{
|
|
6
|
+
next: ServiceConfig;
|
|
7
|
+
prev: ServiceConfig;
|
|
8
|
+
}>;
|
|
9
|
+
unchanged: string[];
|
|
10
|
+
}
|
|
11
|
+
/** Computes the set-difference between two service lists by name.
|
|
12
|
+
* - added: in `next` but not in `prev`
|
|
13
|
+
* - removed: in `prev` but not in `next`
|
|
14
|
+
* - changed: in both but with a spawn-relevant field change
|
|
15
|
+
* - unchanged: in both with identical spawn-relevant fields */
|
|
16
|
+
export declare function diffServices(prev: ServiceConfig[], next: ServiceConfig[]): ServiceDiff;
|
|
17
|
+
/** Short human-readable summary for the TUI banner. */
|
|
18
|
+
export declare function summariseDiff(d: ServiceDiff): string;
|
|
19
|
+
//# sourceMappingURL=diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/config/diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,aAAa,CAAC;QAAC,IAAI,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC;IAC7D,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAeD;;;;gEAIgE;AAChE,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,WAAW,CAmBtF;AAED,uDAAuD;AACvD,wBAAgB,aAAa,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAOpD"}
|
|
@@ -3,6 +3,15 @@ export interface ValidationError {
|
|
|
3
3
|
field: string;
|
|
4
4
|
message: string;
|
|
5
5
|
}
|
|
6
|
+
export interface ValidationWarning {
|
|
7
|
+
field: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
/** Collects non-blocking warnings: things that look suspicious but don't justify
|
|
11
|
+
* refusing to start the stack. Run alongside `validateConfig` from the CLI entry
|
|
12
|
+
* point; print them and continue. */
|
|
13
|
+
export declare function collectWarnings(config: DevStackConfig): ValidationWarning[];
|
|
14
|
+
export declare function formatValidationWarnings(warnings: ValidationWarning[]): string;
|
|
6
15
|
export declare function validateConfig(config: DevStackConfig, cwd: string): ValidationError[];
|
|
7
16
|
export declare function formatValidationErrors(errors: ValidationError[]): string;
|
|
8
17
|
//# sourceMappingURL=validator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/config/validator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGjD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,GAAG,eAAe,EAAE,CA0LrF;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,CAExE"}
|
|
1
|
+
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/config/validator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAGjD,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;sCAEsC;AACtC,wBAAgB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,iBAAiB,EAAE,CAmB3E;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,iBAAiB,EAAE,GAAG,MAAM,CAE9E;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,MAAM,GAAG,eAAe,EAAE,CA0LrF;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,CAExE"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Server } from 'node:net';
|
|
2
|
+
import type { ProcessState } from '../process/types.js';
|
|
3
|
+
/** Minimal JSON-RPC-like protocol over a local Unix socket.
|
|
4
|
+
* Request ─► { id?, method, params? } newline-terminated JSON
|
|
5
|
+
* Response ─► { id?, result | error } newline-terminated JSON
|
|
6
|
+
*
|
|
7
|
+
* Auth model: unix socket created with `chmod 0600`. Anyone with read access
|
|
8
|
+
* to the socket file already has the same uid as the devup process — no
|
|
9
|
+
* additional auth needed. Strictly local; TCP exposure is intentionally
|
|
10
|
+
* out of scope. */
|
|
11
|
+
export interface RpcContext {
|
|
12
|
+
/** State of every service (read-only snapshot). */
|
|
13
|
+
states(): Map<string, ProcessState>;
|
|
14
|
+
/** Restart a service by name. */
|
|
15
|
+
restart(name: string): Promise<void>;
|
|
16
|
+
/** Stop a service by name. */
|
|
17
|
+
stop(name: string): void;
|
|
18
|
+
/** Tail N most recent log lines for the given service (from the persistent log file). */
|
|
19
|
+
tailLogs(svcName: string, lines: number): Promise<string[]>;
|
|
20
|
+
}
|
|
21
|
+
export interface SocketServerHandle {
|
|
22
|
+
server: Server;
|
|
23
|
+
path: string;
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export declare function defaultSocketPath(projectName: string): string;
|
|
27
|
+
export declare function startSocketServer(projectName: string, ctx: RpcContext, opts?: {
|
|
28
|
+
path?: string;
|
|
29
|
+
onLog?: (msg: string) => void;
|
|
30
|
+
}): Promise<SocketServerHandle>;
|
|
31
|
+
//# sourceMappingURL=socket-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket-server.d.ts","sourceRoot":"","sources":["../../src/control-plane/socket-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,MAAM,EAAe,MAAM,UAAU,CAAC;AAMlE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD;;;;;;;oBAOoB;AAEpB,MAAM,WAAW,UAAU;IACzB,mDAAmD;IACnD,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACpC,iCAAiC;IACjC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,8BAA8B;IAC9B,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CAC7D;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED,wBAAsB,iBAAiB,CACrC,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,UAAU,EACf,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;CAAO,GAC1D,OAAO,CAAC,kBAAkB,CAAC,CAiC7B"}
|
package/dist/index.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
import React7 from "react";
|
|
5
5
|
import { render } from "ink";
|
|
6
6
|
import { readFileSync as readFileSync2 } from "fs";
|
|
7
|
-
import { dirname as
|
|
7
|
+
import { dirname as dirname7, join as join8 } from "path";
|
|
8
8
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9
|
-
import { homedir as
|
|
9
|
+
import { homedir as homedir4 } from "os";
|
|
10
10
|
|
|
11
11
|
// src/config/loader.ts
|
|
12
12
|
import { existsSync } from "fs";
|
|
@@ -74,6 +74,26 @@ function rewriteServicePort(svc) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// src/config/validator.ts
|
|
77
|
+
function collectWarnings(config) {
|
|
78
|
+
const warnings = [];
|
|
79
|
+
if (!config.services?.length) return warnings;
|
|
80
|
+
for (const svc of config.services) {
|
|
81
|
+
const ep = svc.extraEnv?.["PORT"];
|
|
82
|
+
if (ep !== void 0) {
|
|
83
|
+
const expected = String(svc.port);
|
|
84
|
+
if (ep !== expected) {
|
|
85
|
+
warnings.push({
|
|
86
|
+
field: `services[${svc.name}].extraEnv.PORT`,
|
|
87
|
+
message: `extraEnv.PORT="${ep}" does not match port=${svc.port}. devup will health-check :${svc.port} but the service will probably bind to :${ep}.`
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return warnings;
|
|
93
|
+
}
|
|
94
|
+
function formatValidationWarnings(warnings) {
|
|
95
|
+
return warnings.map((w) => ` \u26A0 ${w.field}: ${w.message}`).join("\n");
|
|
96
|
+
}
|
|
77
97
|
function validateConfig(config, cwd) {
|
|
78
98
|
const errors = [];
|
|
79
99
|
if (!config.name?.trim()) {
|
|
@@ -272,6 +292,10 @@ Log files:
|
|
|
272
292
|
--no-log-file Disable persistent log files
|
|
273
293
|
--log-dir <path> Override log root (default: ~/.devup/logs)
|
|
274
294
|
|
|
295
|
+
Hot reload:
|
|
296
|
+
--watch-config Watch devup.config.* and apply add/remove/restart
|
|
297
|
+
service changes without exiting the TUI
|
|
298
|
+
|
|
275
299
|
Other:
|
|
276
300
|
-h, --help Show this help and exit
|
|
277
301
|
-v, --version Show version and exit
|
|
@@ -288,7 +312,8 @@ function parseCliArgs(argv) {
|
|
|
288
312
|
dryRun: false,
|
|
289
313
|
once: false,
|
|
290
314
|
onceTimeout: DEFAULT_ONCE_TIMEOUT,
|
|
291
|
-
logFile: true
|
|
315
|
+
logFile: true,
|
|
316
|
+
watchConfig: false
|
|
292
317
|
};
|
|
293
318
|
for (let i = 0; i < argv.length; i++) {
|
|
294
319
|
const arg = argv[i];
|
|
@@ -362,6 +387,9 @@ function parseCliArgs(argv) {
|
|
|
362
387
|
args.logDir = next;
|
|
363
388
|
i++;
|
|
364
389
|
break;
|
|
390
|
+
case "--watch-config":
|
|
391
|
+
args.watchConfig = true;
|
|
392
|
+
break;
|
|
365
393
|
}
|
|
366
394
|
}
|
|
367
395
|
return args;
|
|
@@ -1550,7 +1578,14 @@ function useProxySync(provider, opts, states, enabled) {
|
|
|
1550
1578
|
import { useEffect as useEffect3, useMemo } from "react";
|
|
1551
1579
|
import { Box, Text } from "ink";
|
|
1552
1580
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1553
|
-
function
|
|
1581
|
+
function resolveBorder(focused, filter, filteredColorIdx) {
|
|
1582
|
+
if (focused) return "cyan";
|
|
1583
|
+
if (filter && filteredColorIdx !== null && filteredColorIdx >= 0) {
|
|
1584
|
+
return tagColors[filteredColorIdx % tagColors.length];
|
|
1585
|
+
}
|
|
1586
|
+
return "gray";
|
|
1587
|
+
}
|
|
1588
|
+
function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLen, height, focused, scrollOffset, resetScroll, levelFilter = "all", filteredColorIdx = null }) {
|
|
1554
1589
|
const byService = filter ? logs.filter((l) => l.svcName === filter) : logs;
|
|
1555
1590
|
const filtered = levelFilter === "all" ? byService : levelFilter === "error" ? byService.filter((l) => l.level === "error") : byService.filter((l) => l.level === "error" || l.level === "warn");
|
|
1556
1591
|
const contentHeight = Math.max(1, height - 2);
|
|
@@ -1576,7 +1611,7 @@ function LogsPanel({ logs, filter, searchTerm, paused, showTimestamps, maxNameLe
|
|
|
1576
1611
|
`${filtered.length} lines`,
|
|
1577
1612
|
focused && totalLines > 0 ? `(${startIndex + 1}-${endIndex}/${totalLines})` : ""
|
|
1578
1613
|
].filter(Boolean).join(" ");
|
|
1579
|
-
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: focused
|
|
1614
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: resolveBorder(focused, filter, filteredColorIdx), height, children: [
|
|
1580
1615
|
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
|
|
1581
1616
|
" ",
|
|
1582
1617
|
label,
|
|
@@ -2148,6 +2183,186 @@ function pickTip(state) {
|
|
|
2148
2183
|
return null;
|
|
2149
2184
|
}
|
|
2150
2185
|
|
|
2186
|
+
// src/control-plane/socket-server.ts
|
|
2187
|
+
import { createServer } from "net";
|
|
2188
|
+
import { createInterface as createInterface2 } from "readline";
|
|
2189
|
+
import { existsSync as existsSync10, unlinkSync, chmodSync, mkdirSync as mkdirSync4, statSync as statSync2 } from "fs";
|
|
2190
|
+
import { dirname as dirname5 } from "path";
|
|
2191
|
+
import { join as join6 } from "path";
|
|
2192
|
+
import { homedir as homedir2 } from "os";
|
|
2193
|
+
function defaultSocketPath(projectName) {
|
|
2194
|
+
const safe = projectName.replace(/[^a-zA-Z0-9._-]+/g, "_") || "devup";
|
|
2195
|
+
return join6(homedir2(), ".devup", `sock-${safe}.sock`);
|
|
2196
|
+
}
|
|
2197
|
+
async function startSocketServer(projectName, ctx, opts = {}) {
|
|
2198
|
+
const path = opts.path ?? defaultSocketPath(projectName);
|
|
2199
|
+
mkdirSync4(dirname5(path), { recursive: true });
|
|
2200
|
+
if (existsSync10(path)) {
|
|
2201
|
+
try {
|
|
2202
|
+
const st = statSync2(path);
|
|
2203
|
+
if (st.isSocket()) unlinkSync(path);
|
|
2204
|
+
} catch {
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
const server = createServer((socket) => handleClient(socket, ctx));
|
|
2208
|
+
await new Promise((resolve4, reject) => {
|
|
2209
|
+
server.once("error", reject);
|
|
2210
|
+
server.listen(path, () => {
|
|
2211
|
+
server.off("error", reject);
|
|
2212
|
+
try {
|
|
2213
|
+
chmodSync(path, 384);
|
|
2214
|
+
} catch {
|
|
2215
|
+
}
|
|
2216
|
+
opts.onLog?.(`\u{1F50C} control plane at ${path}`);
|
|
2217
|
+
resolve4();
|
|
2218
|
+
});
|
|
2219
|
+
});
|
|
2220
|
+
return {
|
|
2221
|
+
server,
|
|
2222
|
+
path,
|
|
2223
|
+
async close() {
|
|
2224
|
+
await new Promise((resolve4) => server.close(() => resolve4()));
|
|
2225
|
+
if (existsSync10(path)) {
|
|
2226
|
+
try {
|
|
2227
|
+
unlinkSync(path);
|
|
2228
|
+
} catch {
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
};
|
|
2233
|
+
}
|
|
2234
|
+
function handleClient(socket, ctx) {
|
|
2235
|
+
const rl = createInterface2({ input: socket });
|
|
2236
|
+
rl.on("line", async (line) => {
|
|
2237
|
+
if (!line.trim()) return;
|
|
2238
|
+
let req;
|
|
2239
|
+
try {
|
|
2240
|
+
req = JSON.parse(line);
|
|
2241
|
+
} catch (e) {
|
|
2242
|
+
respond(socket, { error: { code: -32700, message: `parse error: ${e.message}` } });
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
if (typeof req.method !== "string") {
|
|
2246
|
+
respond(socket, { id: req.id, error: { code: -32600, message: "method required" } });
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
try {
|
|
2250
|
+
const result = await dispatch(req.method, req.params ?? {}, ctx);
|
|
2251
|
+
respond(socket, { id: req.id, result });
|
|
2252
|
+
} catch (e) {
|
|
2253
|
+
respond(socket, { id: req.id, error: { code: -32603, message: e.message ?? String(e) } });
|
|
2254
|
+
}
|
|
2255
|
+
});
|
|
2256
|
+
socket.on("error", () => {
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
function respond(socket, payload) {
|
|
2260
|
+
if (socket.writable) socket.write(JSON.stringify(payload) + "\n");
|
|
2261
|
+
}
|
|
2262
|
+
async function dispatch(method, params, ctx) {
|
|
2263
|
+
switch (method) {
|
|
2264
|
+
case "status": {
|
|
2265
|
+
const out = [];
|
|
2266
|
+
for (const [name, st] of ctx.states()) {
|
|
2267
|
+
out.push({
|
|
2268
|
+
name,
|
|
2269
|
+
status: st.status,
|
|
2270
|
+
health: st.health,
|
|
2271
|
+
port: st.svc.port,
|
|
2272
|
+
type: st.svc.type,
|
|
2273
|
+
errors: st.errors,
|
|
2274
|
+
restarts: st.restarts,
|
|
2275
|
+
pid: st.pid,
|
|
2276
|
+
startedAt: st.startedAt
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
return { services: out };
|
|
2280
|
+
}
|
|
2281
|
+
case "restart": {
|
|
2282
|
+
const svc = stringOrThrow(params["svc"] ?? params["service"], "svc");
|
|
2283
|
+
await ctx.restart(svc);
|
|
2284
|
+
return { ok: true };
|
|
2285
|
+
}
|
|
2286
|
+
case "stop": {
|
|
2287
|
+
const svc = stringOrThrow(params["svc"] ?? params["service"], "svc");
|
|
2288
|
+
ctx.stop(svc);
|
|
2289
|
+
return { ok: true };
|
|
2290
|
+
}
|
|
2291
|
+
case "logs.tail": {
|
|
2292
|
+
const svc = stringOrThrow(params["svc"] ?? params["service"], "svc");
|
|
2293
|
+
const lines = Math.max(1, Math.min(1e4, Number(params["lines"] ?? 100)));
|
|
2294
|
+
return { lines: await ctx.tailLogs(svc, lines) };
|
|
2295
|
+
}
|
|
2296
|
+
case "ping":
|
|
2297
|
+
return { ok: true, ts: Date.now() };
|
|
2298
|
+
default:
|
|
2299
|
+
throw new Error(`unknown method: ${method}`);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
function stringOrThrow(v, paramName) {
|
|
2303
|
+
if (typeof v !== "string" || !v.trim()) {
|
|
2304
|
+
throw new Error(`param "${paramName}" must be a non-empty string`);
|
|
2305
|
+
}
|
|
2306
|
+
return v;
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// src/tui/App.tsx
|
|
2310
|
+
import { createInterface as createInterface3 } from "readline";
|
|
2311
|
+
import { createReadStream as createReadStream2, existsSync as existsSync11, watch as fsWatch } from "fs";
|
|
2312
|
+
|
|
2313
|
+
// src/config/diff.ts
|
|
2314
|
+
var SPAWN_RELEVANT = [
|
|
2315
|
+
"cwd",
|
|
2316
|
+
"cmd",
|
|
2317
|
+
"args",
|
|
2318
|
+
"port",
|
|
2319
|
+
"phase",
|
|
2320
|
+
"maxMem",
|
|
2321
|
+
"preBuild",
|
|
2322
|
+
"watchBuild",
|
|
2323
|
+
"nodeArgs",
|
|
2324
|
+
"extraEnv",
|
|
2325
|
+
"healthCheck",
|
|
2326
|
+
"readyPattern",
|
|
2327
|
+
"errorPattern",
|
|
2328
|
+
"type"
|
|
2329
|
+
];
|
|
2330
|
+
function hasSpawnRelevantChange(prev, next) {
|
|
2331
|
+
for (const k of SPAWN_RELEVANT) {
|
|
2332
|
+
if (JSON.stringify(prev[k]) !== JSON.stringify(next[k])) return true;
|
|
2333
|
+
}
|
|
2334
|
+
return false;
|
|
2335
|
+
}
|
|
2336
|
+
function diffServices(prev, next) {
|
|
2337
|
+
const prevByName = new Map(prev.map((s) => [s.name, s]));
|
|
2338
|
+
const nextByName = new Map(next.map((s) => [s.name, s]));
|
|
2339
|
+
const added = [];
|
|
2340
|
+
const removed = [];
|
|
2341
|
+
const changed = [];
|
|
2342
|
+
const unchanged = [];
|
|
2343
|
+
for (const [name, p] of prevByName) {
|
|
2344
|
+
if (!nextByName.has(name)) {
|
|
2345
|
+
removed.push(name);
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
const n = nextByName.get(name);
|
|
2349
|
+
if (hasSpawnRelevantChange(p, n)) changed.push({ prev: p, next: n });
|
|
2350
|
+
else unchanged.push(name);
|
|
2351
|
+
}
|
|
2352
|
+
for (const [name, n] of nextByName) {
|
|
2353
|
+
if (!prevByName.has(name)) added.push(n);
|
|
2354
|
+
}
|
|
2355
|
+
return { added, removed, changed, unchanged };
|
|
2356
|
+
}
|
|
2357
|
+
function summariseDiff(d) {
|
|
2358
|
+
const parts = [];
|
|
2359
|
+
if (d.added.length) parts.push(`+${d.added.length} added`);
|
|
2360
|
+
if (d.removed.length) parts.push(`-${d.removed.length} removed`);
|
|
2361
|
+
if (d.changed.length) parts.push(`~${d.changed.length} changed`);
|
|
2362
|
+
if (!parts.length) parts.push("no changes");
|
|
2363
|
+
return parts.join(", ");
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2151
2366
|
// src/tui/App.tsx
|
|
2152
2367
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2153
2368
|
function buildServiceUrl(name, port, proxyActive, proxyOpts) {
|
|
@@ -2179,6 +2394,7 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2179
2394
|
const [booted, setBooted] = useState6(false);
|
|
2180
2395
|
const lazyProxies = useRef3(/* @__PURE__ */ new Map());
|
|
2181
2396
|
const externals = useRef3([]);
|
|
2397
|
+
const socketServer = useRef3(null);
|
|
2182
2398
|
const shownTips = useRef3(/* @__PURE__ */ new Set());
|
|
2183
2399
|
const [activeTip, setActiveTip] = useState6(null);
|
|
2184
2400
|
const kb = useKeyBindings({
|
|
@@ -2191,6 +2407,8 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2191
2407
|
});
|
|
2192
2408
|
const shutdown = useCallback3(async () => {
|
|
2193
2409
|
lazyProxies.current.forEach((p) => p.destroy());
|
|
2410
|
+
await socketServer.current?.close();
|
|
2411
|
+
socketServer.current = null;
|
|
2194
2412
|
await pm.cleanup();
|
|
2195
2413
|
if (externals.current.length) {
|
|
2196
2414
|
await stopExternals(externals.current, platform, {
|
|
@@ -2203,6 +2421,110 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2203
2421
|
await logSink?.close();
|
|
2204
2422
|
process.exit(0);
|
|
2205
2423
|
}, [pm, logSink, platform, baseCwd, env]);
|
|
2424
|
+
useEffect5(() => {
|
|
2425
|
+
if (!pm.manager) return;
|
|
2426
|
+
let handle = null;
|
|
2427
|
+
(async () => {
|
|
2428
|
+
try {
|
|
2429
|
+
handle = await startSocketServer(config.name, {
|
|
2430
|
+
states: () => pm.manager.state,
|
|
2431
|
+
restart: (name) => pm.manager.restart(name),
|
|
2432
|
+
stop: (name) => pm.manager.stop(name),
|
|
2433
|
+
tailLogs: async (svcName, lines) => {
|
|
2434
|
+
if (!logSink) return [];
|
|
2435
|
+
const file = logSink.pathFor(svcName);
|
|
2436
|
+
if (!existsSync11(file)) return [];
|
|
2437
|
+
return new Promise((resolve4, reject) => {
|
|
2438
|
+
const buf = [];
|
|
2439
|
+
const rl = createInterface3({ input: createReadStream2(file, { encoding: "utf8" }) });
|
|
2440
|
+
rl.on("line", (l) => {
|
|
2441
|
+
buf.push(l);
|
|
2442
|
+
if (buf.length > lines) buf.shift();
|
|
2443
|
+
});
|
|
2444
|
+
rl.on("close", () => resolve4(buf));
|
|
2445
|
+
rl.on("error", reject);
|
|
2446
|
+
});
|
|
2447
|
+
}
|
|
2448
|
+
}, { onLog: (msg) => pm.pushLog("devup", msg, 12) });
|
|
2449
|
+
socketServer.current = handle;
|
|
2450
|
+
} catch (e) {
|
|
2451
|
+
pm.pushLog("devup", `\u26A0 control plane disabled: ${e.message}`, 5);
|
|
2452
|
+
}
|
|
2453
|
+
})();
|
|
2454
|
+
return () => {
|
|
2455
|
+
void handle?.close();
|
|
2456
|
+
};
|
|
2457
|
+
}, [pm.manager, config.name, logSink]);
|
|
2458
|
+
useEffect5(() => {
|
|
2459
|
+
if (!cliArgs.watchConfig || !pm.manager) return;
|
|
2460
|
+
let watcher = null;
|
|
2461
|
+
let configPath;
|
|
2462
|
+
try {
|
|
2463
|
+
configPath = findConfigFile(baseCwd, cliArgs.configPath);
|
|
2464
|
+
} catch (e) {
|
|
2465
|
+
pm.pushLog("devup", `\u26A0 watch-config disabled: ${e.message}`, 5);
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
pm.pushLog("devup", `\u{1F440} watching ${configPath}`, 12);
|
|
2469
|
+
let reloadInFlight = false;
|
|
2470
|
+
let reloadAgain = false;
|
|
2471
|
+
const reload = async () => {
|
|
2472
|
+
if (reloadInFlight) {
|
|
2473
|
+
reloadAgain = true;
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2476
|
+
reloadInFlight = true;
|
|
2477
|
+
try {
|
|
2478
|
+
const nextCfg = await loadConfig(configPath);
|
|
2479
|
+
const errs = validateConfig(nextCfg, baseCwd);
|
|
2480
|
+
if (errs.length) {
|
|
2481
|
+
pm.pushLog("devup", `\u26A0 config reload failed:
|
|
2482
|
+
${formatValidationErrors(errs)}`, 5);
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
const mgr = pm.manager;
|
|
2486
|
+
const currentSvcs = [...mgr.state.values()].map((s) => s.svc);
|
|
2487
|
+
const diff = diffServices(currentSvcs, nextCfg.services);
|
|
2488
|
+
if (!diff.added.length && !diff.removed.length && !diff.changed.length) return;
|
|
2489
|
+
for (const name of diff.removed) {
|
|
2490
|
+
mgr.stop(name);
|
|
2491
|
+
mgr.state.delete(name);
|
|
2492
|
+
}
|
|
2493
|
+
let colorIdx = currentSvcs.length;
|
|
2494
|
+
for (const { next } of diff.changed) {
|
|
2495
|
+
const prev = mgr.state.get(next.name);
|
|
2496
|
+
const ci = prev?.colorIdx ?? colorIdx++;
|
|
2497
|
+
mgr.stop(next.name);
|
|
2498
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
2499
|
+
await mgr.install(next, ci);
|
|
2500
|
+
await mgr.start(next, ci, true);
|
|
2501
|
+
}
|
|
2502
|
+
for (const next of diff.added) {
|
|
2503
|
+
const ci = colorIdx++;
|
|
2504
|
+
await mgr.install(next, ci);
|
|
2505
|
+
await mgr.start(next, ci);
|
|
2506
|
+
}
|
|
2507
|
+
pm.pushLog("devup", `\u{1F501} config reloaded: ${summariseDiff(diff)}`, 12);
|
|
2508
|
+
} catch (e) {
|
|
2509
|
+
pm.pushLog("devup", `\u26A0 config reload error: ${e.message}`, 5);
|
|
2510
|
+
} finally {
|
|
2511
|
+
reloadInFlight = false;
|
|
2512
|
+
if (reloadAgain) {
|
|
2513
|
+
reloadAgain = false;
|
|
2514
|
+
void reload();
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2518
|
+
let debounceTimer = null;
|
|
2519
|
+
watcher = fsWatch(configPath, () => {
|
|
2520
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
2521
|
+
debounceTimer = setTimeout(() => void reload(), 250);
|
|
2522
|
+
});
|
|
2523
|
+
return () => {
|
|
2524
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
2525
|
+
watcher?.close();
|
|
2526
|
+
};
|
|
2527
|
+
}, [cliArgs.watchConfig, cliArgs.configPath, baseCwd, pm.manager, pm]);
|
|
2206
2528
|
useEffect5(() => {
|
|
2207
2529
|
pm.setPaused(kb.logsPaused || kb.logsScrollOffset > 0);
|
|
2208
2530
|
}, [kb.logsPaused, kb.logsScrollOffset, pm]);
|
|
@@ -2374,7 +2696,8 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2374
2696
|
focused: kb.panel === "logs",
|
|
2375
2697
|
scrollOffset: kb.logsScrollOffset,
|
|
2376
2698
|
resetScroll: kb.resetLogsScroll,
|
|
2377
|
-
levelFilter: kb.levelFilter
|
|
2699
|
+
levelFilter: kb.levelFilter,
|
|
2700
|
+
filteredColorIdx: kb.logFilter ? pm.states.get(kb.logFilter)?.colorIdx ?? null : null
|
|
2378
2701
|
}
|
|
2379
2702
|
),
|
|
2380
2703
|
/* @__PURE__ */ jsx6(
|
|
@@ -2400,23 +2723,23 @@ function App({ config, services, cliArgs, platform, env, baseCwd, proxyProvider,
|
|
|
2400
2723
|
}
|
|
2401
2724
|
|
|
2402
2725
|
// src/process/log-sink.ts
|
|
2403
|
-
import { existsSync as
|
|
2404
|
-
import { join as
|
|
2405
|
-
import { homedir as
|
|
2726
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync5, renameSync, createWriteStream } from "fs";
|
|
2727
|
+
import { join as join7, dirname as dirname6 } from "path";
|
|
2728
|
+
import { homedir as homedir3 } from "os";
|
|
2406
2729
|
var LogSink = class {
|
|
2407
2730
|
dir;
|
|
2408
2731
|
rotateOnStart;
|
|
2409
2732
|
streams = /* @__PURE__ */ new Map();
|
|
2410
2733
|
seen = /* @__PURE__ */ new Set();
|
|
2411
2734
|
constructor(opts) {
|
|
2412
|
-
const root = opts.rootDir ??
|
|
2413
|
-
this.dir =
|
|
2735
|
+
const root = opts.rootDir ?? join7(homedir3(), ".devup", "logs");
|
|
2736
|
+
this.dir = join7(root, sanitize2(opts.projectName));
|
|
2414
2737
|
this.rotateOnStart = opts.rotateOnStart ?? true;
|
|
2415
|
-
|
|
2738
|
+
mkdirSync5(this.dir, { recursive: true });
|
|
2416
2739
|
}
|
|
2417
2740
|
/** Returns the file path for a service log (useful for tests / UI). */
|
|
2418
2741
|
pathFor(svcName) {
|
|
2419
|
-
return
|
|
2742
|
+
return join7(this.dir, `${sanitize2(svcName)}.log`);
|
|
2420
2743
|
}
|
|
2421
2744
|
write(svcName, line) {
|
|
2422
2745
|
const stream = this.streamFor(svcName);
|
|
@@ -2435,9 +2758,9 @@ var LogSink = class {
|
|
|
2435
2758
|
let s = this.streams.get(svcName);
|
|
2436
2759
|
if (s) return s;
|
|
2437
2760
|
const file = this.pathFor(svcName);
|
|
2438
|
-
if (this.rotateOnStart && !this.seen.has(svcName) &&
|
|
2761
|
+
if (this.rotateOnStart && !this.seen.has(svcName) && existsSync12(file)) {
|
|
2439
2762
|
try {
|
|
2440
|
-
|
|
2763
|
+
mkdirSync5(dirname6(file), { recursive: true });
|
|
2441
2764
|
renameSync(file, file + ".prev");
|
|
2442
2765
|
} catch {
|
|
2443
2766
|
}
|
|
@@ -2623,8 +2946,8 @@ function defineConfig(config) {
|
|
|
2623
2946
|
// src/index.ts
|
|
2624
2947
|
function readVersion() {
|
|
2625
2948
|
try {
|
|
2626
|
-
const here =
|
|
2627
|
-
const pkgPath =
|
|
2949
|
+
const here = dirname7(fileURLToPath2(import.meta.url));
|
|
2950
|
+
const pkgPath = join8(here, "..", "package.json");
|
|
2628
2951
|
return JSON.parse(readFileSync2(pkgPath, "utf8")).version ?? "unknown";
|
|
2629
2952
|
} catch {
|
|
2630
2953
|
return "unknown";
|
|
@@ -2675,6 +2998,11 @@ async function main() {
|
|
|
2675
2998
|
${formatValidationErrors(errors)}`);
|
|
2676
2999
|
process.exit(1);
|
|
2677
3000
|
}
|
|
3001
|
+
const warnings = collectWarnings(config);
|
|
3002
|
+
if (warnings.length) {
|
|
3003
|
+
console.warn(`\u26A0 Config warnings:
|
|
3004
|
+
${formatValidationWarnings(warnings)}`);
|
|
3005
|
+
}
|
|
2678
3006
|
let services;
|
|
2679
3007
|
try {
|
|
2680
3008
|
services = filterServices(config.services, cliArgs, config);
|
|
@@ -2687,7 +3015,7 @@ ${formatValidationErrors(errors)}`);
|
|
|
2687
3015
|
process.exit(1);
|
|
2688
3016
|
}
|
|
2689
3017
|
const platform = await detectPlatform();
|
|
2690
|
-
const envFile = config.envFile ?
|
|
3018
|
+
const envFile = config.envFile ? join8(cwd, config.envFile) : join8(cwd, ".env");
|
|
2691
3019
|
const env = parseEnvFile(envFile, process.env);
|
|
2692
3020
|
if (config.env) {
|
|
2693
3021
|
for (const [k, v] of Object.entries(config.env)) {
|
|
@@ -2704,7 +3032,7 @@ ${formatValidationErrors(errors)}`);
|
|
|
2704
3032
|
routes: config.proxy.routes,
|
|
2705
3033
|
tls: cliArgs.proxyTls ?? config.proxy.tls ?? true,
|
|
2706
3034
|
entrypoint: cliArgs.proxyEntrypoint ?? config.proxy.entrypoint ?? "websecure",
|
|
2707
|
-
confPath: cliArgs.proxyConf ?? config.proxy.confPath ??
|
|
3035
|
+
confPath: cliArgs.proxyConf ?? config.proxy.confPath ?? join8(homedir4(), ".traefik", "traefik_conf.yaml")
|
|
2708
3036
|
};
|
|
2709
3037
|
}
|
|
2710
3038
|
if (cliArgs.dryRun) {
|