@devrouter/cli 0.0.22 → 0.0.23
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 +69 -34
- package/dist/dev.js +2879 -808
- package/package.json +3 -2
- package/upgrade-prompts/0.0.23.md +44 -0
package/dist/dev.js
CHANGED
|
@@ -161,6 +161,7 @@ This folder is managed by the devrouter CLI.
|
|
|
161
161
|
- dev init [--write-agents] [--write-skill] [--with-linear]
|
|
162
162
|
- dev -V [--repo <path>] (installed/local version + next upgrade)
|
|
163
163
|
- dev upgrade [version] [--repo <path>]
|
|
164
|
+
- dev setup --yes [--repo <path>] [--json]
|
|
164
165
|
- dev up
|
|
165
166
|
- dev down
|
|
166
167
|
- dev status
|
|
@@ -169,10 +170,13 @@ This folder is managed by the devrouter CLI.
|
|
|
169
170
|
- dev open <name>
|
|
170
171
|
- dev logs [-f] [--tail N]
|
|
171
172
|
- dev repo init
|
|
173
|
+
- dev repo inspect [--repo <path>] [--json]
|
|
174
|
+
- dev repo devcontainer write [--repo <path>] [--dry-run] [--yes] [--json]
|
|
175
|
+
- dev repo devcontainer verify [--repo <path>] [--live] [--yes] [--json]
|
|
172
176
|
- dev repo agents [--with-linear]
|
|
173
177
|
- dev app add --name <name> --host <host.localhost> --protocol <http|tcp> --runtime <host|docker>
|
|
174
178
|
- dev app run <name>
|
|
175
|
-
- dev app exec <name> [--shell] [--env
|
|
179
|
+
- dev app exec <name> [--shell] [--env <env>] -- <command>
|
|
176
180
|
- dev app ls
|
|
177
181
|
- dev app rm <name>
|
|
178
182
|
- dev tls install
|
|
@@ -340,6 +344,45 @@ var init_router = __esm({
|
|
|
340
344
|
}
|
|
341
345
|
});
|
|
342
346
|
|
|
347
|
+
// src/core/capabilities.ts
|
|
348
|
+
function formatSupportedTcpProtocols() {
|
|
349
|
+
return SUPPORTED_TCP_PROTOCOLS.join(", ");
|
|
350
|
+
}
|
|
351
|
+
function formatSupportedProtocolsForRuntime(runtime) {
|
|
352
|
+
return RUNTIME_PROTOCOL_COMPATIBILITY[runtime].join(", ");
|
|
353
|
+
}
|
|
354
|
+
function buildPostgresDependencyUrl(port) {
|
|
355
|
+
return POSTGRES_DEPENDENCY_URL_TEMPLATE.replace("<port>", String(port));
|
|
356
|
+
}
|
|
357
|
+
function buildPostgresDependencyShadowUrl(port) {
|
|
358
|
+
return POSTGRES_DEPENDENCY_SHADOW_URL_TEMPLATE.replace("<port>", String(port));
|
|
359
|
+
}
|
|
360
|
+
var SUPPORTED_RUNTIMES, SUPPORTED_PROTOCOLS, SUPPORTED_TCP_PROTOCOLS, DEPENDENCY_ONLY_RUNTIME, RUNTIME_PROTOCOL_COMPATIBILITY, WORKSPACE_PLACEHOLDER, SECRET_MANAGER_ENV_PLACEHOLDER, DEP_ENV_SUFFIXES, POSTGRES_DEPENDENCY_USER, POSTGRES_DEPENDENCY_PASSWORD, POSTGRES_DEPENDENCY_DATABASE, POSTGRES_DEPENDENCY_SHADOW_DATABASE, POSTGRES_DEPENDENCY_URL_TEMPLATE, POSTGRES_DEPENDENCY_SHADOW_URL_TEMPLATE;
|
|
361
|
+
var init_capabilities = __esm({
|
|
362
|
+
"src/core/capabilities.ts"() {
|
|
363
|
+
"use strict";
|
|
364
|
+
init_router();
|
|
365
|
+
SUPPORTED_RUNTIMES = ["host", "docker", "proxy"];
|
|
366
|
+
SUPPORTED_PROTOCOLS = ["http", "tcp"];
|
|
367
|
+
SUPPORTED_TCP_PROTOCOLS = Object.freeze(Object.keys(TCP_PROTOCOL_REGISTRY));
|
|
368
|
+
DEPENDENCY_ONLY_RUNTIME = "docker";
|
|
369
|
+
RUNTIME_PROTOCOL_COMPATIBILITY = {
|
|
370
|
+
host: ["http"],
|
|
371
|
+
docker: ["http", "tcp"],
|
|
372
|
+
proxy: ["http", "tcp"]
|
|
373
|
+
};
|
|
374
|
+
WORKSPACE_PLACEHOLDER = "${WORKSPACE}";
|
|
375
|
+
SECRET_MANAGER_ENV_PLACEHOLDER = "{env}";
|
|
376
|
+
DEP_ENV_SUFFIXES = ["HOST", "PORT", "URL", "SHADOW_URL"];
|
|
377
|
+
POSTGRES_DEPENDENCY_USER = "prisma";
|
|
378
|
+
POSTGRES_DEPENDENCY_PASSWORD = "prisma";
|
|
379
|
+
POSTGRES_DEPENDENCY_DATABASE = "prisma";
|
|
380
|
+
POSTGRES_DEPENDENCY_SHADOW_DATABASE = "shadow";
|
|
381
|
+
POSTGRES_DEPENDENCY_URL_TEMPLATE = `postgres://${POSTGRES_DEPENDENCY_USER}:${POSTGRES_DEPENDENCY_PASSWORD}@localhost:<port>/${POSTGRES_DEPENDENCY_DATABASE}`;
|
|
382
|
+
POSTGRES_DEPENDENCY_SHADOW_URL_TEMPLATE = `postgres://${POSTGRES_DEPENDENCY_USER}:${POSTGRES_DEPENDENCY_PASSWORD}@localhost:<port>/${POSTGRES_DEPENDENCY_SHADOW_DATABASE}`;
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
343
386
|
// src/core/host-routes.ts
|
|
344
387
|
function parseUpstream(upstream) {
|
|
345
388
|
const match = UPSTREAM_RE.exec(upstream.trim());
|
|
@@ -351,8 +394,8 @@ function parseUpstream(upstream) {
|
|
|
351
394
|
if (port < 1 || port > 65535) {
|
|
352
395
|
throw new Error(`upstream port must be between 1 and 65535 (got ${port}).`);
|
|
353
396
|
}
|
|
354
|
-
const
|
|
355
|
-
return { host, port, upstreamHost };
|
|
397
|
+
const upstreamHost2 = LOOPBACK_HOSTS.has(host) ? "host.docker.internal" : host;
|
|
398
|
+
return { host, port, upstreamHost: upstreamHost2 };
|
|
356
399
|
}
|
|
357
400
|
function isPidRunning(pid) {
|
|
358
401
|
if (!pid || pid <= 0) {
|
|
@@ -553,31 +596,16 @@ function removeHostRouteById(id) {
|
|
|
553
596
|
return true;
|
|
554
597
|
});
|
|
555
598
|
}
|
|
556
|
-
function
|
|
599
|
+
function removeHostRoutesWhere(predicate) {
|
|
557
600
|
return withStateLock(() => {
|
|
558
601
|
const routes = listHostRouteState();
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
}
|
|
563
|
-
if (repoPath && route.repoPath !== repoPath) {
|
|
564
|
-
return false;
|
|
565
|
-
}
|
|
566
|
-
return true;
|
|
567
|
-
});
|
|
568
|
-
if (matches.length === 0) {
|
|
569
|
-
throw new Error(`No host route named '${name}' found.`);
|
|
570
|
-
}
|
|
571
|
-
if (matches.length > 1 && !repoPath) {
|
|
572
|
-
const refs = matches.map((route) => `${route.name} (${route.repoPath})`).join(", ");
|
|
573
|
-
throw new Error(
|
|
574
|
-
`Multiple host routes named '${name}' exist. Re-run with --repo to disambiguate: ${refs}`
|
|
575
|
-
);
|
|
602
|
+
const removed = routes.filter(predicate);
|
|
603
|
+
if (removed.length === 0) {
|
|
604
|
+
return [];
|
|
576
605
|
}
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
return match;
|
|
606
|
+
const removedIds = new Set(removed.map((route) => route.id));
|
|
607
|
+
writeState(routes.filter((route) => !removedIds.has(route.id)));
|
|
608
|
+
return removed;
|
|
581
609
|
});
|
|
582
610
|
}
|
|
583
611
|
function listHostRoutes(tlsEnabled) {
|
|
@@ -759,6 +787,19 @@ function parseEnvMap(value, pathLabel) {
|
|
|
759
787
|
}
|
|
760
788
|
return result;
|
|
761
789
|
}
|
|
790
|
+
function isSupportedProtocol(value) {
|
|
791
|
+
return SUPPORTED_PROTOCOLS.includes(value);
|
|
792
|
+
}
|
|
793
|
+
function isSupportedRuntime(value) {
|
|
794
|
+
return SUPPORTED_RUNTIMES.includes(value);
|
|
795
|
+
}
|
|
796
|
+
function isSupportedTcpProtocol(value) {
|
|
797
|
+
return SUPPORTED_TCP_PROTOCOLS.includes(value);
|
|
798
|
+
}
|
|
799
|
+
function runtimeSupportsProtocol(runtime, protocol) {
|
|
800
|
+
const supportedProtocols = RUNTIME_PROTOCOL_COMPATIBILITY[runtime];
|
|
801
|
+
return supportedProtocols.includes(protocol);
|
|
802
|
+
}
|
|
762
803
|
function parseDependencies(value, pathLabel) {
|
|
763
804
|
if (value === void 0) {
|
|
764
805
|
return [];
|
|
@@ -807,21 +848,21 @@ function parseHostStrategy(value, pathLabel) {
|
|
|
807
848
|
function parseDockerConfig(value, pathLabel) {
|
|
808
849
|
const objectValue = ensureObject(value, pathLabel);
|
|
809
850
|
ensureAllowedKeys(objectValue, ["service", "internalPort", "composeFiles", "router"], pathLabel);
|
|
810
|
-
const
|
|
851
|
+
const composeFiles2 = toStringArray(objectValue.composeFiles, `${pathLabel}.composeFiles`);
|
|
811
852
|
return {
|
|
812
853
|
service: toStringOrThrow(objectValue.service, `${pathLabel}.service`),
|
|
813
854
|
internalPort: toIntegerOrThrow(objectValue.internalPort, `${pathLabel}.internalPort`),
|
|
814
|
-
composeFiles:
|
|
855
|
+
composeFiles: composeFiles2.length > 0 ? composeFiles2 : ["docker-compose.yml"],
|
|
815
856
|
router: objectValue.router === void 0 ? void 0 : toStringOrThrow(objectValue.router, `${pathLabel}.router`)
|
|
816
857
|
};
|
|
817
858
|
}
|
|
818
859
|
function parseDependencyDockerConfig(value, pathLabel) {
|
|
819
860
|
const objectValue = ensureObject(value, pathLabel);
|
|
820
861
|
ensureAllowedKeys(objectValue, ["service", "composeFiles"], pathLabel);
|
|
821
|
-
const
|
|
862
|
+
const composeFiles2 = toStringArray(objectValue.composeFiles, `${pathLabel}.composeFiles`);
|
|
822
863
|
return {
|
|
823
864
|
service: toStringOrThrow(objectValue.service, `${pathLabel}.service`),
|
|
824
|
-
composeFiles:
|
|
865
|
+
composeFiles: composeFiles2.length > 0 ? composeFiles2 : ["docker-compose.yml"]
|
|
825
866
|
};
|
|
826
867
|
}
|
|
827
868
|
function parseHostOrThrow(value, pathLabel) {
|
|
@@ -863,13 +904,13 @@ function parseApp(value, index) {
|
|
|
863
904
|
throw new Error(`${pathLabel}.hostRun is not supported when kind=dependency.`);
|
|
864
905
|
}
|
|
865
906
|
const runtime2 = toStringOrThrow(objectValue.runtime, `${pathLabel}.runtime`);
|
|
866
|
-
if (runtime2 !==
|
|
867
|
-
throw new Error(`${pathLabel}.runtime must be '
|
|
907
|
+
if (runtime2 !== DEPENDENCY_ONLY_RUNTIME) {
|
|
908
|
+
throw new Error(`${pathLabel}.runtime must be '${DEPENDENCY_ONLY_RUNTIME}' when kind=dependency.`);
|
|
868
909
|
}
|
|
869
910
|
return {
|
|
870
911
|
kind: "dependency",
|
|
871
912
|
name,
|
|
872
|
-
runtime:
|
|
913
|
+
runtime: DEPENDENCY_ONLY_RUNTIME,
|
|
873
914
|
dependencies,
|
|
874
915
|
docker: parseDependencyDockerConfig(objectValue.docker, `${pathLabel}.docker`)
|
|
875
916
|
};
|
|
@@ -877,8 +918,14 @@ function parseApp(value, index) {
|
|
|
877
918
|
const host = parseHostOrThrow(objectValue.host, `${pathLabel}.host`);
|
|
878
919
|
const protocol = toStringOrThrow(objectValue.protocol, `${pathLabel}.protocol`);
|
|
879
920
|
const runtime = toStringOrThrow(objectValue.runtime, `${pathLabel}.runtime`);
|
|
921
|
+
if (!isSupportedProtocol(protocol)) {
|
|
922
|
+
throw new Error(`${pathLabel}.protocol must be one of: ${SUPPORTED_PROTOCOLS.join(", ")}.`);
|
|
923
|
+
}
|
|
924
|
+
if (!isSupportedRuntime(runtime)) {
|
|
925
|
+
throw new Error(`${pathLabel}.runtime must be one of: ${SUPPORTED_RUNTIMES.join(", ")}.`);
|
|
926
|
+
}
|
|
880
927
|
if (runtime === "host") {
|
|
881
|
-
if (
|
|
928
|
+
if (!runtimeSupportsProtocol("host", protocol)) {
|
|
882
929
|
throw new Error(`${pathLabel}: host runtime currently supports only protocol=http.`);
|
|
883
930
|
}
|
|
884
931
|
const hostRun = ensureObject(objectValue.hostRun, `${pathLabel}.hostRun`);
|
|
@@ -916,10 +963,9 @@ function parseApp(value, index) {
|
|
|
916
963
|
}
|
|
917
964
|
if (protocol === "tcp") {
|
|
918
965
|
const tcpProtocol = toStringOrThrow(objectValue.tcpProtocol, `${pathLabel}.tcpProtocol`);
|
|
919
|
-
|
|
920
|
-
if (!supportedProtocols.includes(tcpProtocol)) {
|
|
966
|
+
if (!isSupportedTcpProtocol(tcpProtocol)) {
|
|
921
967
|
throw new Error(
|
|
922
|
-
`${pathLabel}.tcpProtocol must be one of: ${
|
|
968
|
+
`${pathLabel}.tcpProtocol must be one of: ${formatSupportedTcpProtocols()}.`
|
|
923
969
|
);
|
|
924
970
|
}
|
|
925
971
|
return {
|
|
@@ -934,7 +980,7 @@ function parseApp(value, index) {
|
|
|
934
980
|
}
|
|
935
981
|
}
|
|
936
982
|
if (runtime === "proxy") {
|
|
937
|
-
if (
|
|
983
|
+
if (!runtimeSupportsProtocol("proxy", protocol)) {
|
|
938
984
|
throw new Error(`${pathLabel}: proxy runtime supports protocol=http or protocol=tcp.`);
|
|
939
985
|
}
|
|
940
986
|
if (objectValue.hostRun !== void 0) {
|
|
@@ -950,9 +996,8 @@ function parseApp(value, index) {
|
|
|
950
996
|
assertUpstreamSpec(upstream, `${pathLabel}.upstream`);
|
|
951
997
|
if (protocol === "tcp") {
|
|
952
998
|
const tcpProtocol = toStringOrThrow(objectValue.tcpProtocol, `${pathLabel}.tcpProtocol`);
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
throw new Error(`${pathLabel}.tcpProtocol must be one of: ${supportedProtocols.join(", ")}.`);
|
|
999
|
+
if (!isSupportedTcpProtocol(tcpProtocol)) {
|
|
1000
|
+
throw new Error(`${pathLabel}.tcpProtocol must be one of: ${formatSupportedTcpProtocols()}.`);
|
|
956
1001
|
}
|
|
957
1002
|
return {
|
|
958
1003
|
name,
|
|
@@ -987,11 +1032,11 @@ function parseConfig(raw, configPath) {
|
|
|
987
1032
|
const metadata = ensureObject(root.devrouter, `${configPath}.devrouter`);
|
|
988
1033
|
ensureAllowedKeys(metadata, ["version"], `${configPath}.devrouter`);
|
|
989
1034
|
if (metadata.version !== void 0) {
|
|
990
|
-
const
|
|
991
|
-
if (!DEVROUTER_VERSION_RE.test(
|
|
1035
|
+
const devrouterVersion2 = toStringOrThrow(metadata.version, `${configPath}.devrouter.version`);
|
|
1036
|
+
if (!DEVROUTER_VERSION_RE.test(devrouterVersion2)) {
|
|
992
1037
|
throw new Error(`${configPath}.devrouter.version must be a semantic version like 0.0.14.`);
|
|
993
1038
|
}
|
|
994
|
-
devrouter = { version:
|
|
1039
|
+
devrouter = { version: devrouterVersion2 };
|
|
995
1040
|
} else {
|
|
996
1041
|
devrouter = {};
|
|
997
1042
|
}
|
|
@@ -1021,8 +1066,10 @@ function parseConfig(raw, configPath) {
|
|
|
1021
1066
|
throw new Error(`${configPath}.secretManager.defaultEnv must be alphanumeric with hyphens.`);
|
|
1022
1067
|
}
|
|
1023
1068
|
}
|
|
1024
|
-
if (command.includes(
|
|
1025
|
-
throw new Error(
|
|
1069
|
+
if (command.includes(SECRET_MANAGER_ENV_PLACEHOLDER) && !defaultEnv) {
|
|
1070
|
+
throw new Error(
|
|
1071
|
+
`${configPath}.secretManager.defaultEnv is required when command contains ${SECRET_MANAGER_ENV_PLACEHOLDER}.`
|
|
1072
|
+
);
|
|
1026
1073
|
}
|
|
1027
1074
|
secretManager = { command, ...defaultEnv ? { defaultEnv } : {} };
|
|
1028
1075
|
}
|
|
@@ -1122,8 +1169,8 @@ function buildAppFromOptions(options) {
|
|
|
1122
1169
|
}
|
|
1123
1170
|
const dependencies = options.dependsOn.map((app) => ({ app }));
|
|
1124
1171
|
if (kind === "dependency") {
|
|
1125
|
-
if (options.runtime !== void 0 && options.runtime !==
|
|
1126
|
-
throw new Error(
|
|
1172
|
+
if (options.runtime !== void 0 && options.runtime !== DEPENDENCY_ONLY_RUNTIME) {
|
|
1173
|
+
throw new Error(`--runtime must be ${DEPENDENCY_ONLY_RUNTIME} when --kind dependency`);
|
|
1127
1174
|
}
|
|
1128
1175
|
if (options.host !== void 0) {
|
|
1129
1176
|
throw new Error("--host is not supported when --kind dependency");
|
|
@@ -1152,7 +1199,7 @@ function buildAppFromOptions(options) {
|
|
|
1152
1199
|
return {
|
|
1153
1200
|
kind: "dependency",
|
|
1154
1201
|
name: options.name,
|
|
1155
|
-
runtime:
|
|
1202
|
+
runtime: DEPENDENCY_ONLY_RUNTIME,
|
|
1156
1203
|
dependencies,
|
|
1157
1204
|
docker: {
|
|
1158
1205
|
service: options.service,
|
|
@@ -1219,10 +1266,9 @@ function buildAppFromOptions(options) {
|
|
|
1219
1266
|
assertUpstreamSpec(options.upstream, "--upstream");
|
|
1220
1267
|
if (options.protocol === "tcp") {
|
|
1221
1268
|
const tcpProtocol2 = options.tcpProtocol;
|
|
1222
|
-
|
|
1223
|
-
if (!tcpProtocol2 || !supportedProtocols2.includes(tcpProtocol2)) {
|
|
1269
|
+
if (!tcpProtocol2 || !isSupportedTcpProtocol(tcpProtocol2)) {
|
|
1224
1270
|
throw new Error(
|
|
1225
|
-
`--tcp-protocol must be one of: ${
|
|
1271
|
+
`--tcp-protocol must be one of: ${formatSupportedTcpProtocols()} when --runtime proxy --protocol tcp`
|
|
1226
1272
|
);
|
|
1227
1273
|
}
|
|
1228
1274
|
return {
|
|
@@ -1266,10 +1312,9 @@ function buildAppFromOptions(options) {
|
|
|
1266
1312
|
docker
|
|
1267
1313
|
};
|
|
1268
1314
|
}
|
|
1269
|
-
const tcpProtocol = options.tcpProtocol ??
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
throw new Error(`--tcp-protocol must be one of: ${supportedProtocols.join(", ")}`);
|
|
1315
|
+
const tcpProtocol = options.tcpProtocol ?? DEFAULT_TCP_PROTOCOL;
|
|
1316
|
+
if (!isSupportedTcpProtocol(tcpProtocol)) {
|
|
1317
|
+
throw new Error(`--tcp-protocol must be one of: ${formatSupportedTcpProtocols()}`);
|
|
1273
1318
|
}
|
|
1274
1319
|
return {
|
|
1275
1320
|
name: options.name,
|
|
@@ -1410,7 +1455,7 @@ function resolveAppDependencies(config, app) {
|
|
|
1410
1455
|
}
|
|
1411
1456
|
return results;
|
|
1412
1457
|
}
|
|
1413
|
-
var import_node_fs4, import_node_path4, import_yaml2, CONFIG_FILE_NAME, VALID_HOSTNAME_RE, DEVROUTER_VERSION_RE, VALID_ENV_NAME_RE, VALID_ENV_VAR_RE,
|
|
1458
|
+
var import_node_fs4, import_node_path4, import_yaml2, CONFIG_FILE_NAME, DEFAULT_TCP_PROTOCOL, VALID_HOSTNAME_RE, DEVROUTER_VERSION_RE, VALID_ENV_NAME_RE, VALID_ENV_VAR_RE, UPSTREAM_TEMPLATE_RE, MAX_COMMAND_LENGTH, DEFAULT_HOST_STRATEGY;
|
|
1414
1459
|
var init_repo_config = __esm({
|
|
1415
1460
|
"src/core/repo-config.ts"() {
|
|
1416
1461
|
"use strict";
|
|
@@ -1418,14 +1463,14 @@ var init_repo_config = __esm({
|
|
|
1418
1463
|
import_node_path4 = __toESM(require("path"));
|
|
1419
1464
|
import_yaml2 = __toESM(require("yaml"));
|
|
1420
1465
|
init_host_routes();
|
|
1421
|
-
init_router();
|
|
1422
1466
|
init_workspace();
|
|
1467
|
+
init_capabilities();
|
|
1423
1468
|
CONFIG_FILE_NAME = ".devrouter.yml";
|
|
1469
|
+
DEFAULT_TCP_PROTOCOL = "postgres";
|
|
1424
1470
|
VALID_HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\.localhost$/;
|
|
1425
1471
|
DEVROUTER_VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
1426
1472
|
VALID_ENV_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/i;
|
|
1427
1473
|
VALID_ENV_VAR_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1428
|
-
WORKSPACE_PLACEHOLDER = "${WORKSPACE}";
|
|
1429
1474
|
UPSTREAM_TEMPLATE_RE = /^(?:\$\{WORKSPACE\}|[a-zA-Z0-9._-])+:\d{1,5}$/;
|
|
1430
1475
|
MAX_COMMAND_LENGTH = 4096;
|
|
1431
1476
|
DEFAULT_HOST_STRATEGY = {
|
|
@@ -1458,6 +1503,15 @@ function renderCommandIntentSection() {
|
|
|
1458
1503
|
);
|
|
1459
1504
|
return ["Command intent reference:", ...lines].join("\n");
|
|
1460
1505
|
}
|
|
1506
|
+
function quotedUnion(values) {
|
|
1507
|
+
return values.map((value) => `"${value}"`).join(" | ");
|
|
1508
|
+
}
|
|
1509
|
+
function formatDepEnvNames() {
|
|
1510
|
+
return DEP_ENV_SUFFIXES.map((suffix) => `\`{PREFIX}_${suffix}\``).join(", ");
|
|
1511
|
+
}
|
|
1512
|
+
function formatProtocolRule(runtime) {
|
|
1513
|
+
return formatSupportedProtocolsForRuntime(runtime).replace(", ", " or ");
|
|
1514
|
+
}
|
|
1461
1515
|
function buildOnboardingPrompt(options = {}) {
|
|
1462
1516
|
const repoPath = resolveRepoPath(options.repo);
|
|
1463
1517
|
const entriesJson = normalizeEntriesJson(options.entriesJson);
|
|
@@ -1484,8 +1538,8 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
1484
1538
|
"- version: 1 (required)",
|
|
1485
1539
|
"- devrouter.version: semantic version string (recommended; required for `dev -V`/`dev upgrade`)",
|
|
1486
1540
|
"- project.name: string (optional)",
|
|
1487
|
-
|
|
1488
|
-
|
|
1541
|
+
`- secretManager.command: string (optional; SM command including trailing \`--\` boundary; supports \`${SECRET_MANAGER_ENV_PLACEHOLDER}\` template placeholder)`,
|
|
1542
|
+
`- secretManager.defaultEnv: string (optional; fallback env for \`${SECRET_MANAGER_ENV_PLACEHOLDER}\` template; required when command contains \`${SECRET_MANAGER_ENV_PLACEHOLDER}\`)`,
|
|
1489
1543
|
"- apps: array (required)",
|
|
1490
1544
|
"",
|
|
1491
1545
|
"Canonical valid skeleton:",
|
|
@@ -1502,8 +1556,8 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
1502
1556
|
'- dependencies: [{ app: "<name>", envMap: { TARGET: "SOURCE" } }] (optional; envMap aliases per-dep vars to app-expected names)',
|
|
1503
1557
|
"- if kind=app:",
|
|
1504
1558
|
" - host: <name>.localhost (single-label or multi-segment, for example `api.v2.app.localhost`)",
|
|
1505
|
-
|
|
1506
|
-
|
|
1559
|
+
` - protocol: ${quotedUnion(SUPPORTED_PROTOCOLS)}`,
|
|
1560
|
+
` - runtime: ${quotedUnion(SUPPORTED_RUNTIMES)}`,
|
|
1507
1561
|
"- if kind=app and runtime=host:",
|
|
1508
1562
|
" - hostRun.command: string",
|
|
1509
1563
|
" - hostRun.cwd: string",
|
|
@@ -1517,32 +1571,32 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
1517
1571
|
" - optional docker.router: string",
|
|
1518
1572
|
"- if kind=app and runtime=proxy:",
|
|
1519
1573
|
' - upstream: "host:port" (an already-running port, e.g. a devcontainer published on 127.0.0.1:3000, or a container reachable by name on a shared Docker network such as `derivatives-db:5432`)',
|
|
1520
|
-
|
|
1574
|
+
` - upstream may use the \`${WORKSPACE_PLACEHOLDER}\` placeholder (e.g. \`${WORKSPACE_PLACEHOLDER}-app:3000\`) to target a per-workspace devcontainer alias; it is substituted with the resolved workspace token at runtime and re-validated. Do NOT put \`${WORKSPACE_PLACEHOLDER}\` in \`host\` (rejected) \u2014 the host is auto-namespaced.`,
|
|
1521
1575
|
" - protocol=http registers an HTTP route; protocol=tcp registers a TLS-SNI TCP route and additionally requires tcpProtocol",
|
|
1522
1576
|
" - do not set hostRun/docker/dependencies (proxy only registers a route to the upstream)",
|
|
1523
1577
|
" - loopback hosts (localhost/127.0.0.1/0.0.0.0) are rewritten to host.docker.internal for Traefik",
|
|
1524
1578
|
"- if kind=app and protocol=tcp:",
|
|
1525
|
-
|
|
1579
|
+
` - tcpProtocol: ${quotedUnion(SUPPORTED_TCP_PROTOCOLS)}`,
|
|
1526
1580
|
"- if kind=dependency:",
|
|
1527
|
-
|
|
1581
|
+
` - runtime: "${DEPENDENCY_ONLY_RUNTIME}"`,
|
|
1528
1582
|
" - docker.service: string",
|
|
1529
1583
|
" - docker.composeFiles: string[]",
|
|
1530
1584
|
" - do not set host/protocol/tcpProtocol/hostRun/docker.internalPort/docker.router",
|
|
1531
1585
|
"",
|
|
1532
1586
|
"Validation rules to enforce:",
|
|
1533
1587
|
"- kind=app host must end with .localhost",
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1588
|
+
`- kind=app runtime=host supports protocol=${formatProtocolRule("host")} only`,
|
|
1589
|
+
`- kind=app runtime=proxy supports protocol=${formatProtocolRule("proxy")}, requires upstream (host:port), and has no dependencies; protocol=tcp also requires tcpProtocol`,
|
|
1590
|
+
`- kind=app protocol=tcp requires runtime=docker or proxy, and tcpProtocol (${formatSupportedTcpProtocols()})`,
|
|
1591
|
+
`- kind=dependency requires runtime=${DEPENDENCY_ONLY_RUNTIME} and non-routed docker config only`,
|
|
1538
1592
|
"- unknown keys are not allowed (strict schema)",
|
|
1539
1593
|
"",
|
|
1540
1594
|
"Docker compose file requirements:",
|
|
1541
1595
|
"- Every service that acts as a dependency MUST define a healthcheck \u2014 devrouter uses `docker compose up --wait` which blocks until healthy; without a healthcheck the wait returns immediately and the dependent app may start before the service is ready.",
|
|
1542
1596
|
"- Services MUST NOT publish host ports (`ports:` mapping) for any port owned by devrouter (80, 443, 5432) \u2014 Traefik owns these; conflicts cause bind failures.",
|
|
1543
1597
|
"- Services SHOULD NOT publish host ports at all \u2014 devrouter handles external routing via Traefik labels; publishing ports creates conflicts when running multiple repos.",
|
|
1544
|
-
|
|
1545
|
-
|
|
1598
|
+
`- For TCP dependencies of host apps, devrouter automatically publishes a random host port and injects per-dep deterministic env vars: ${formatDepEnvNames()}. \`{PREFIX}_URL\` is protocol-specific, \`{PREFIX}_SHADOW_URL\` is postgres only. \`{PREFIX} = dep.name.toUpperCase().replace(/-/g, "_")\`.`,
|
|
1599
|
+
`- For postgres deps, \`{PREFIX}_URL=${POSTGRES_DEPENDENCY_URL_TEMPLATE}\` and \`{PREFIX}_SHADOW_URL=${POSTGRES_DEPENDENCY_SHADOW_URL_TEMPLATE}\`. Use config-level \`envMap\` on the dependency reference to alias these to app-expected names (e.g. \`DATABASE_URL: DB_URL\`).`,
|
|
1546
1600
|
"- The Postgres service in docker-compose must use matching credentials (`POSTGRES_USER=prisma`, `POSTGRES_PASSWORD=prisma`, `POSTGRES_DB=prisma`) and create the shadow database (e.g. via an init script or the app's migration tool). If existing credentials differ, either update them to match or override via `envMap` aliasing.",
|
|
1547
1601
|
"- If you changed postgres credentials/database defaults on an existing persistent volume, startup may still fail due to stale data. Recommend reconciling credentials or recreating volumes (for example `docker compose down -v`) when safe.",
|
|
1548
1602
|
"- The TLS/SNI route on :5432 remains available for tools supporting `sslnegotiation=direct` (psql 17+, pgAdmin).",
|
|
@@ -1556,7 +1610,7 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
1556
1610
|
"- Host-runtime dependencies are NOT auto-started in v1 (must be started manually).",
|
|
1557
1611
|
"- kind=dependency entries are dependency-only: they do not create routes and cannot be direct targets for `dev app run`, `dev app exec`, or `dev open`.",
|
|
1558
1612
|
"- kind=dependency services are started/stopped as declared in compose (no Traefik labels added, no env/port injection).",
|
|
1559
|
-
|
|
1613
|
+
`- For TCP dependencies of host apps, devrouter publishes a random host port and injects per-dep deterministic vars: \`{PREFIX}_HOST=localhost\`, \`{PREFIX}_PORT=<port>\`, \`{PREFIX}_URL\` (protocol-specific), \`{PREFIX}_SHADOW_URL\` (postgres only). \`{PREFIX} = dep.name.toUpperCase().replace(/-/g, "_")\`.`,
|
|
1560
1614
|
"- Config-level `envMap` on dependency references aliases per-dep vars to app-expected names. Example: `envMap: { DATABASE_URL: DB_URL }` maps per-dep `DB_URL` to `DATABASE_URL` in the app process.",
|
|
1561
1615
|
"- If the repo's Postgres docker-compose service uses different credentials than the injected defaults (`prisma:prisma`), flag this to the user and recommend aligning the compose env vars or using `envMap` aliasing.",
|
|
1562
1616
|
"- Postgres multiplexing on shared :5432 requires TLS/SNI (useful for psql 17+, pgAdmin, not standard app clients).",
|
|
@@ -1566,17 +1620,17 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
1566
1620
|
"- `envMap` on dependency references (config-level) aliases per-dep vars after dependency env resolution. `envMap` fails fast when source var is missing.",
|
|
1567
1621
|
"",
|
|
1568
1622
|
"Workspace isolation (parallel git worktrees / agents):",
|
|
1569
|
-
|
|
1623
|
+
`- A "workspace token" lets several worktrees of one repo run in parallel without host/route collisions. It is a single identity spanning three layers: the devpod workspace id (\`devpod up --id <ws>\`), the routes devrouter registers, and the \`${WORKSPACE_PLACEHOLDER}\` placeholder in \`.devrouter.yml\` upstreams + the devcontainer compose network alias.`,
|
|
1570
1624
|
"- Token resolution precedence: `--workspace <slug>` flag > `DEVROUTER_WORKSPACE` env var > auto-derived from a linked git worktree branch (sanitized: lowercase, non-alphanumeric \u2192 `-`, capped at 32 chars) > none. The primary checkout resolves to no token and routes exactly as before (fully back-compatible).",
|
|
1571
|
-
|
|
1625
|
+
`- When a workspace is active: hosts auto-namespace (\`web.localhost\` \u2192 \`web.<ws>.localhost\`), \`${WORKSPACE_PLACEHOLDER}\` in \`upstream\` is substituted with the token, and the docker \`router\` key is suffixed per workspace. The runtime config is computed in memory only \u2014 the committed \`.devrouter.yml\` is never rewritten.`,
|
|
1572
1626
|
"- TLS: namespaced hosts (`web.<ws>.localhost`) are not covered by the `*.localhost` wildcard; devrouter auto-extends the mkcert cert SANs for active hosts when TLS is enabled.",
|
|
1573
|
-
"- Lifecycle: `dev workspace up <branch>` (create worktree + devpod + routes), `dev workspace ls` (list worktrees/tokens/route counts), `dev workspace down <workspace|branch>` (free routes + stop devpod + remove worktree). `dev doctor`
|
|
1574
|
-
|
|
1627
|
+
"- Lifecycle: `dev workspace up <branch>` (create worktree + devpod + routes), `dev workspace ls` (list worktrees/tokens/route counts), `dev workspace down <workspace|branch>` (free routes + stop devpod + remove worktree). `dev doctor` reports orphaned workspace proxy routes whose worktree dir was removed without `dev workspace down`.",
|
|
1628
|
+
`- devcontainer integration: the devcontainer compose service exposes a devnet alias \`${WORKSPACE_PLACEHOLDER}-app\` (default \`WORKSPACE=<project>\` in \`devcontainer.env\`), and the proxy app uses \`upstream: ${WORKSPACE_PLACEHOLDER}-app:<port>\`. Spinning up workspace \`feat-a\` \u2192 alias \`feat-a-app\`, host \`app.feat-a.localhost\`.`,
|
|
1575
1629
|
"",
|
|
1576
1630
|
"Secret Manager Integration (config-based):",
|
|
1577
1631
|
"- Optional top-level `secretManager.command` in `.devrouter.yml` wraps `dev app run` and `dev app exec` commands with the SM command and re-applies devrouter-injected dep env vars after the SM boundary via `env KEY=VAL` prefix.",
|
|
1578
|
-
|
|
1579
|
-
|
|
1632
|
+
`- Example config: \`secretManager: { command: "infisical run --env ${SECRET_MANAGER_ENV_PLACEHOLDER} --", defaultEnv: "dev" }\`.`,
|
|
1633
|
+
`- \`${SECRET_MANAGER_ENV_PLACEHOLDER}\` template placeholder in \`secretManager.command\` is resolved at runtime. \`defaultEnv\` provides the fallback; \`--env\` CLI flag overrides it.`,
|
|
1580
1634
|
"- When configured, the effective command becomes: `<secretManager.command> env {PREFIX}_URL=<val> ... <user-command>`.",
|
|
1581
1635
|
"- This ensures devrouter-injected vars take precedence over SM-defined values without manual forwarding.",
|
|
1582
1636
|
"- Config-level `envMap` targets are also included in the re-injection set.",
|
|
@@ -1596,21 +1650,30 @@ function buildOnboardingPrompt(options = {}) {
|
|
|
1596
1650
|
"- Warning: Do not run migration/seed until env probe confirms expected DB variables and values.",
|
|
1597
1651
|
"",
|
|
1598
1652
|
"Required workflow:",
|
|
1599
|
-
"1)
|
|
1600
|
-
"2)
|
|
1601
|
-
"3) For
|
|
1602
|
-
"4)
|
|
1603
|
-
"5)
|
|
1604
|
-
"6)
|
|
1653
|
+
"1) Run `dev setup --yes --json` for devrouter-owned machine state; use `dev doctor --repo <REPO_PATH> --json` to diagnose missing prerequisites without mutation.",
|
|
1654
|
+
"2) Run `dev repo inspect --repo <REPO_PATH> --json` before editing files.",
|
|
1655
|
+
"3) For the supported Node/pnpm/Postgres devcontainer shape, run `dev repo devcontainer write --repo <REPO_PATH> --dry-run --json`, review the plan, then run `dev repo devcontainer write --repo <REPO_PATH> --yes`.",
|
|
1656
|
+
"4) For unsupported shapes or custom existing files, make minimal manual edits and explain the assumptions.",
|
|
1657
|
+
"5) Verify static evidence with `dev repo devcontainer verify --repo <REPO_PATH> --json`; after the devcontainer is running, use `dev repo devcontainer verify --repo <REPO_PATH> --live --yes --json` for route probes.",
|
|
1658
|
+
"6) Keep edits minimal, explicit, and idempotent. Do not modify unrelated services.",
|
|
1659
|
+
"7) If required info is missing or ambiguous, stop and ask targeted questions.",
|
|
1660
|
+
"",
|
|
1661
|
+
"Validation commands to run/report for the devcontainer path:",
|
|
1662
|
+
"- dev setup --yes --json",
|
|
1663
|
+
"- dev doctor --repo <REPO_PATH> --json",
|
|
1664
|
+
"- dev repo inspect --repo <REPO_PATH> --json",
|
|
1665
|
+
"- dev repo devcontainer write --repo <REPO_PATH> --dry-run --json",
|
|
1666
|
+
"- dev repo devcontainer write --repo <REPO_PATH> --yes",
|
|
1667
|
+
"- dev repo devcontainer verify --repo <REPO_PATH> --json",
|
|
1668
|
+
"- After `devpod up <REPO_PATH>`: dev repo devcontainer verify --repo <REPO_PATH> --live --yes --json",
|
|
1605
1669
|
"",
|
|
1606
|
-
"Validation commands to run/report:",
|
|
1607
|
-
"- dev
|
|
1608
|
-
"-
|
|
1670
|
+
"Validation commands to run/report for host/docker runtime apps:",
|
|
1671
|
+
"- dev setup --yes --json",
|
|
1672
|
+
"- dev doctor --repo <REPO_PATH> --json",
|
|
1609
1673
|
"- dev app ls --repo <REPO_PATH>",
|
|
1610
1674
|
"- For each entry (when safe): dev app run <name> --repo <REPO_PATH> --yes",
|
|
1611
1675
|
"- Run one-shot commands with dep env: dev app exec <name> --repo <REPO_PATH> --yes -- <command>",
|
|
1612
1676
|
"- Probe effective env before migration/seed: dev app exec <name> --repo <REPO_PATH> --yes -- printenv DB_URL DB_HOST DB_PORT DB_SHADOW_URL",
|
|
1613
|
-
"- dev doctor --repo <REPO_PATH> --json",
|
|
1614
1677
|
"- dev ls",
|
|
1615
1678
|
"- For HTTP entries: curl -I http://<host>",
|
|
1616
1679
|
'- For TCP postgres entries: provide connection hint (example: psql "... sslmode=require")',
|
|
@@ -1654,6 +1717,7 @@ var COMMAND_INTENTS;
|
|
|
1654
1717
|
var init_ai_prompt = __esm({
|
|
1655
1718
|
"src/core/ai-prompt.ts"() {
|
|
1656
1719
|
"use strict";
|
|
1720
|
+
init_capabilities();
|
|
1657
1721
|
init_repo_config();
|
|
1658
1722
|
COMMAND_INTENTS = [
|
|
1659
1723
|
{
|
|
@@ -1668,6 +1732,10 @@ var init_ai_prompt = __esm({
|
|
|
1668
1732
|
command: "dev upgrade [version]",
|
|
1669
1733
|
purpose: "Show upgrade targets from .devrouter.yml devrouter.version and print target adaptation prompt."
|
|
1670
1734
|
},
|
|
1735
|
+
{
|
|
1736
|
+
command: "dev setup",
|
|
1737
|
+
purpose: "Run first-time devrouter machine setup and report structured diagnostics."
|
|
1738
|
+
},
|
|
1671
1739
|
{
|
|
1672
1740
|
command: "dev up",
|
|
1673
1741
|
purpose: "Start shared Traefik and ensure the shared devnet network."
|
|
@@ -1697,6 +1765,18 @@ var init_ai_prompt = __esm({
|
|
|
1697
1765
|
command: "dev repo init",
|
|
1698
1766
|
purpose: "Create `.devrouter.yml` in a target repository."
|
|
1699
1767
|
},
|
|
1768
|
+
{
|
|
1769
|
+
command: "dev repo inspect",
|
|
1770
|
+
purpose: "Inspect package, scripts, compose services, env names, devcontainer, devrouter config, and agent guidance for onboarding."
|
|
1771
|
+
},
|
|
1772
|
+
{
|
|
1773
|
+
command: "dev repo devcontainer write",
|
|
1774
|
+
purpose: "Dry-run or write conservative managed Node/pnpm/Postgres devcontainer/devrouter scaffold files."
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
command: "dev repo devcontainer verify",
|
|
1778
|
+
purpose: "Emit static onboarding evidence, or live route probes with --live --yes."
|
|
1779
|
+
},
|
|
1700
1780
|
{
|
|
1701
1781
|
command: "dev app add",
|
|
1702
1782
|
purpose: "Add or update one app entry in `.devrouter.yml`."
|
|
@@ -1954,51 +2034,76 @@ Local dev routing via a shared Traefik reverse proxy. Provides stable \`*.localh
|
|
|
1954
2034
|
\`\`\`yaml
|
|
1955
2035
|
version: 1
|
|
1956
2036
|
devrouter:
|
|
1957
|
-
version: <semver>
|
|
2037
|
+
version: <semver> # required for dev -V / dev upgrade
|
|
1958
2038
|
project:
|
|
1959
|
-
name: <string>
|
|
2039
|
+
name: <string> # optional
|
|
1960
2040
|
apps:
|
|
1961
|
-
- name: <string>
|
|
1962
|
-
kind: app | dependency
|
|
1963
|
-
dependencies:
|
|
2041
|
+
- name: <string> # unique within repo
|
|
2042
|
+
kind: app | dependency # optional, default: app
|
|
2043
|
+
dependencies: # optional
|
|
1964
2044
|
- app: <other-name>
|
|
2045
|
+
envMap: # optional; maps target env var name -> per-dep source var name
|
|
2046
|
+
DATABASE_URL: <UPPER_DEP_NAME>_URL
|
|
1965
2047
|
|
|
1966
2048
|
# if kind=app:
|
|
1967
2049
|
host: <name>.localhost
|
|
1968
2050
|
protocol: http | tcp
|
|
1969
|
-
runtime: host | docker
|
|
2051
|
+
runtime: host | docker | proxy
|
|
2052
|
+
|
|
2053
|
+
# if kind=app and runtime=proxy (protocol http or tcp):
|
|
2054
|
+
upstream: 127.0.0.1:3000 # already-running port to route to; no lifecycle/deps
|
|
2055
|
+
# Loopback (127.0.0.1/localhost) -> host.docker.internal (a published host
|
|
2056
|
+
# port). A non-loopback name is passed verbatim and resolved over devnet \u2014
|
|
2057
|
+
# so a devcontainer container ON devnet (with a network alias) can be fronted
|
|
2058
|
+
# by NAME with NO published host port: upstream: <alias>:3000. This is the
|
|
2059
|
+
# collision-free way to run many apps at once (each its own *.localhost).
|
|
2060
|
+
# upstream may use the \${WORKSPACE} placeholder (e.g. \${WORKSPACE}-app:3000)
|
|
2061
|
+
# to target a per-workspace devcontainer alias \u2014 substituted with the resolved
|
|
2062
|
+
# workspace token at runtime. See "Workspace isolation" below. Do NOT put
|
|
2063
|
+
# \${WORKSPACE} in \`host\` (rejected); the host is auto-namespaced.
|
|
2064
|
+
#
|
|
2065
|
+
# proxy + tcp (front a DB in an externally-managed container, e.g. a
|
|
2066
|
+
# devcontainer's Postgres on devnet) \u2014 no per-DB host port:
|
|
2067
|
+
# protocol: tcp
|
|
2068
|
+
# tcpProtocol: postgres # selects shared entrypoint :5432
|
|
2069
|
+
# upstream: <db-alias>:5432 # devnet alias of the DB container
|
|
2070
|
+
# Requires \`dev tls install\` (SNI is read from the TLS ClientHello). Connect
|
|
2071
|
+
# with direct-SSL so the ClientHello carries SNI, e.g.:
|
|
2072
|
+
# psql "host=db.<app>.localhost port=5432 sslmode=require sslnegotiation=direct ..."
|
|
1970
2073
|
|
|
1971
2074
|
# if kind=app and runtime=host (protocol must be http):
|
|
1972
2075
|
hostRun:
|
|
1973
2076
|
command: <string>
|
|
1974
|
-
cwd: <string>
|
|
1975
|
-
portTimeout: 120
|
|
2077
|
+
cwd: <string> # relative to repo root, must not escape it
|
|
2078
|
+
portTimeout: 120 # seconds, optional
|
|
1976
2079
|
strategy:
|
|
1977
2080
|
type: auto
|
|
1978
2081
|
denyPorts: [80, 443, 5432]
|
|
1979
|
-
allowPortRange:
|
|
2082
|
+
allowPortRange: '1024-65535'
|
|
1980
2083
|
|
|
1981
2084
|
# if kind=app and runtime=docker:
|
|
1982
2085
|
docker:
|
|
1983
2086
|
service: <string>
|
|
1984
2087
|
internalPort: <number>
|
|
1985
|
-
composeFiles: [<string>]
|
|
1986
|
-
router: <string>
|
|
2088
|
+
composeFiles: [<string>] # relative to repo root
|
|
2089
|
+
router: <string> # optional
|
|
1987
2090
|
|
|
1988
2091
|
# if kind=app and protocol=tcp:
|
|
1989
|
-
tcpProtocol: postgres
|
|
2092
|
+
tcpProtocol: postgres # required; runtime must be docker OR proxy
|
|
1990
2093
|
|
|
1991
2094
|
# if kind=dependency:
|
|
1992
2095
|
runtime: docker
|
|
1993
2096
|
docker:
|
|
1994
2097
|
service: <string>
|
|
1995
|
-
composeFiles: [<string>]
|
|
2098
|
+
composeFiles: [<string>] # relative to repo root
|
|
1996
2099
|
\`\`\`
|
|
1997
2100
|
|
|
1998
2101
|
Validation rules:
|
|
2102
|
+
|
|
1999
2103
|
- \`kind=app\`: \`host\` must end with \`.localhost\`
|
|
2000
2104
|
- \`kind=app\`: \`runtime=host\` supports \`protocol=http\` only
|
|
2001
|
-
- \`kind=app\`: \`protocol=
|
|
2105
|
+
- \`kind=app\`: \`runtime=proxy\` supports \`protocol=http\` or \`protocol=tcp\`, requires \`upstream\` (\`host:port\`), and forbids \`hostRun\`/\`docker\`/\`dependencies\` (it only registers a route to an externally-managed upstream). \`protocol=tcp\` additionally requires \`tcpProtocol\` and TLS (\`dev tls install\`)
|
|
2106
|
+
- \`kind=app\`: \`protocol=tcp\` requires \`runtime=docker\` (devrouter-managed container) or \`runtime=proxy\` (externally-managed upstream), plus a supported \`tcpProtocol\` (postgres/redis/mariadb/mysql)
|
|
2002
2107
|
- \`kind=dependency\`: must use \`runtime=docker\` and does not allow routed fields (\`host\`/\`protocol\`/\`tcpProtocol\`/\`hostRun\`/\`docker.internalPort\`/\`docker.router\`)
|
|
2003
2108
|
- Unknown keys rejected (strict schema)
|
|
2004
2109
|
|
|
@@ -2006,13 +2111,14 @@ Validation rules:
|
|
|
2006
2111
|
|
|
2007
2112
|
- **Healthcheck required**: every dependency service must define a \`healthcheck\`. \`docker compose up --wait\` blocks until healthy; without one, wait returns immediately.
|
|
2008
2113
|
- **No published ports**: services must not publish host ports for devrouter-owned ports (80, 443, 5432). Avoid publishing ports at all -- devrouter handles routing via Traefik.
|
|
2009
|
-
- **Postgres credentials**: use \`POSTGRES_USER=prisma\`, \`POSTGRES_PASSWORD=prisma\`, \`POSTGRES_DB=prisma\` and create a \`shadow\` database. devrouter injects \`
|
|
2114
|
+
- **Postgres credentials**: use \`POSTGRES_USER=prisma\`, \`POSTGRES_PASSWORD=prisma\`, \`POSTGRES_DB=prisma\` and create a \`shadow\` database. devrouter injects per-dep \`{PREFIX}_URL\` / \`{PREFIX}_SHADOW_URL\` with these credentials.
|
|
2010
2115
|
- **Persistent volume warning**: if postgres defaults changed on an existing volume, reconcile credentials/data or recreate volumes when safe (for example \`docker compose down -v\`).
|
|
2011
2116
|
|
|
2012
2117
|
Example healthcheck:
|
|
2118
|
+
|
|
2013
2119
|
\`\`\`yaml
|
|
2014
2120
|
healthcheck:
|
|
2015
|
-
test: [
|
|
2121
|
+
test: ['CMD-SHELL', 'pg_isready -U prisma -d prisma']
|
|
2016
2122
|
interval: 5s
|
|
2017
2123
|
timeout: 3s
|
|
2018
2124
|
retries: 20
|
|
@@ -2020,40 +2126,61 @@ healthcheck:
|
|
|
2020
2126
|
|
|
2021
2127
|
## Env var injection
|
|
2022
2128
|
|
|
2023
|
-
When a host app depends on a TCP
|
|
2129
|
+
When a host app depends on a TCP Docker service, \`dev app run\` and \`dev app exec\` inject per-dep deterministic vars (where \`{PREFIX} = dep.name.toUpperCase().replace(/-/g, "_")\`):
|
|
2024
2130
|
|
|
2025
|
-
| Variable
|
|
2026
|
-
|
|
2027
|
-
|
|
|
2028
|
-
|
|
|
2029
|
-
| \`
|
|
2030
|
-
| \`
|
|
2131
|
+
| Variable | Value |
|
|
2132
|
+
| ----------------------- | ----------------------------------------------------------- |
|
|
2133
|
+
| \`{PREFIX}_HOST\` | \`localhost\` |
|
|
2134
|
+
| \`{PREFIX}_PORT\` | random mapped port |
|
|
2135
|
+
| \`{PREFIX}_URL\` | protocol-specific URL (postgres, redis, mysql/mariadb) |
|
|
2136
|
+
| \`{PREFIX}_SHADOW_URL\` | \`postgres://prisma:prisma@localhost:<port>/shadow\` (postgres only) |
|
|
2031
2137
|
|
|
2032
2138
|
Host apps also receive \`PORT\` (random free port), \`HOSTNAME=0.0.0.0\`, \`HOST=0.0.0.0\`.
|
|
2033
2139
|
|
|
2034
|
-
|
|
2140
|
+
Config-level \`envMap\` on dependency references aliases per-dep vars to app-expected names (for example \`DATABASE_URL: DB_URL\` maps the per-dep \`DB_URL\` to \`DATABASE_URL\`).
|
|
2141
|
+
|
|
2142
|
+
## Workspace isolation (parallel git worktrees / agents)
|
|
2143
|
+
|
|
2144
|
+
Run several worktrees of one repo in parallel without host/route collisions. A **workspace token** is a single identity spanning three layers: the devpod workspace id (\`devpod up --id <ws>\`), the routes devrouter registers, and the \`\${WORKSPACE}\` placeholder in \`.devrouter.yml\` upstreams + the devcontainer compose network alias.
|
|
2145
|
+
|
|
2146
|
+
- **Token resolution** (precedence): \`--workspace <slug>\` flag > \`DEVROUTER_WORKSPACE\` env var > auto-derived from a linked git worktree branch (sanitized: lowercase, non-alphanumeric \u2192 \`-\`, capped at 32 chars) > none. The primary checkout resolves to no token and routes exactly as before (back-compatible).
|
|
2147
|
+
- **When active**: hosts auto-namespace (\`web.localhost\` \u2192 \`web.<ws>.localhost\`), \`\${WORKSPACE}\` in \`upstream\` is substituted with the token, and the docker \`router\` key is suffixed per workspace. The runtime config is computed in memory only \u2014 the committed \`.devrouter.yml\` is never rewritten.
|
|
2148
|
+
- **TLS**: namespaced hosts (\`web.<ws>.localhost\`) are not covered by the \`*.localhost\` wildcard; devrouter auto-extends the mkcert cert SANs for active hosts when TLS is enabled.
|
|
2149
|
+
- **devcontainer integration**: the devcontainer compose service exposes a devnet alias \`\${WORKSPACE}-app\` (default \`WORKSPACE=<project>\` in \`devcontainer.env\`); the proxy app uses \`upstream: \${WORKSPACE}-app:<port>\`. Workspace \`feat-a\` \u2192 alias \`feat-a-app\`, host \`app.feat-a.localhost\`.
|
|
2150
|
+
- **Lifecycle**: \`dev workspace up <branch>\` (create worktree + devpod + routes), \`dev workspace ls\` (list worktrees/tokens/route counts), \`dev workspace down <workspace|branch>\` (free routes by state-file workspace tag + stop devpod + remove worktree). \`dev doctor\` reports orphaned workspace proxy routes whose worktree dir was removed without \`dev workspace down\`.
|
|
2035
2151
|
|
|
2036
2152
|
## Secret manager interop (Infisical/Doppler)
|
|
2037
2153
|
|
|
2154
|
+
- Config-based SM integration: set \`secretManager.command\` in \`.devrouter.yml\` (include trailing \`--\`). devrouter wraps commands and re-injects dep env vars after the SM boundary.
|
|
2155
|
+
- \`secretManager.defaultEnv\`: optional fallback environment for \`{env}\` template in command string.
|
|
2156
|
+
- \`{env}\` template placeholder: \`secretManager.command: "infisical run --env {env} --"\` resolved at runtime. \`--env <env>\` CLI flag overrides \`defaultEnv\`.
|
|
2157
|
+
- Example config:
|
|
2158
|
+
\`\`\`yaml
|
|
2159
|
+
secretManager:
|
|
2160
|
+
command: infisical run --env {env} --
|
|
2161
|
+
defaultEnv: dev
|
|
2162
|
+
\`\`\`
|
|
2163
|
+
- Use \`envMap\` on dependency references to alias per-dep vars to app-expected names:
|
|
2164
|
+
\`\`\`yaml
|
|
2165
|
+
dependencies:
|
|
2166
|
+
- app: db
|
|
2167
|
+
envMap:
|
|
2168
|
+
DATABASE_URL: DB_URL
|
|
2169
|
+
DIRECT_URL: DB_URL
|
|
2170
|
+
SHADOW_DATABASE_URL: DB_SHADOW_URL
|
|
2171
|
+
\`\`\`
|
|
2038
2172
|
- Prefer argv-safe command forms. Do not wrap \`infisical run\` or \`doppler run\` in \`sh -lc\` unless shell expansion is strictly required.
|
|
2039
2173
|
- Canonical Infisical migrate command:
|
|
2040
|
-
\`dev app exec <app> --yes --
|
|
2041
|
-
- Canonical Infisical seed command:
|
|
2042
|
-
\`dev app exec <app> --yes --env-map DATABASE_URI=DATABASE_URL -- infisical run --projectId <id> --env=<env> -- pnpm payload seed\`
|
|
2174
|
+
\`dev app exec <app> --yes -- infisical run --projectId <id> --env=<env> -- pnpm payload migrate\`
|
|
2043
2175
|
- Canonical env probe command (run before migrate/seed):
|
|
2044
|
-
\`dev app exec <app> --yes --
|
|
2176
|
+
\`dev app exec <app> --yes -- printenv DB_URL DB_HOST DB_PORT DB_SHADOW_URL\`
|
|
2045
2177
|
- Canonical Doppler migrate command:
|
|
2046
|
-
\`dev app exec <app> --yes --
|
|
2047
|
-
- Precedence best practice: avoid defining
|
|
2048
|
-
- Precedence best practice: store remote/prod URLs under non-conflicting names (for example \`PROD_DATABASE_URL\`) and map intentionally
|
|
2049
|
-
- Precedence best practice: if secret manager must define
|
|
2050
|
-
- Avoid pre-wrapper DB assignments such as \`DATABASE_URI=... <wrapper> run -- ...\`; wrapper-managed env may override those values.
|
|
2051
|
-
- Safe host-run override pattern when wrapper also defines \`DATABASE_URI\`:
|
|
2052
|
-
\`infisical run --projectId <id> --env=<env> -- env DATABASE_URI=\${DATABASE_URL:?missing DATABASE_URL} pnpm dev\`
|
|
2053
|
-
- \`dev app run\` does not currently expose \`--env-map\`; if an app only accepts \`DATABASE_URI\`, prefer app-level fallback (\`DATABASE_URI\` then \`DATABASE_URL\`) or a small repo-local wrapper script.
|
|
2054
|
-
- \`dev doctor --repo .\` warns on risky pre-wrapper DB assignments before \`run --\` for host apps that depend on postgres.
|
|
2178
|
+
\`dev app exec <app> --yes -- doppler run -- pnpm payload migrate\`
|
|
2179
|
+
- Precedence best practice: avoid defining per-dep var names in Infisical/Doppler when you expect devrouter local DB injection.
|
|
2180
|
+
- Precedence best practice: store remote/prod URLs under non-conflicting names (for example \`PROD_DATABASE_URL\`) and map intentionally via \`envMap\`.
|
|
2181
|
+
- Precedence best practice: if secret manager must define DB vars, run the env probe and verify values before any migration/seed.
|
|
2055
2182
|
- Use \`dev app exec --shell -- "<single command string>"\` only when shell expansion is required.
|
|
2056
|
-
-
|
|
2183
|
+
- \`envMap\` fails fast when source var is missing so migrations do not run with partial mapping.
|
|
2057
2184
|
|
|
2058
2185
|
## Upgrade handling (required)
|
|
2059
2186
|
|
|
@@ -2080,6 +2207,7 @@ Host apps also receive \`PORT\` (random free port), \`HOSTNAME=0.0.0.0\`, \`HOST
|
|
|
2080
2207
|
- \`dev init [--write-agents] [--write-skill] [--with-linear]\`: print AI onboarding prompt (non-mutating by default)
|
|
2081
2208
|
- \`dev -V [--repo .]\`: show installed CLI version, local repo version, and next upgrade target
|
|
2082
2209
|
- \`dev upgrade [version] [--repo .]\`: list upgrade targets or print target Agent Adaptation Prompt
|
|
2210
|
+
- \`dev setup --yes [--repo .] [--json]\`: first-run machine setup plus structured diagnostics
|
|
2083
2211
|
- \`dev up\` / \`dev down\`: start/stop shared Traefik router
|
|
2084
2212
|
- \`dev status\`: router/container/network/TLS health
|
|
2085
2213
|
- \`dev doctor [--repo .]\`: deep diagnostics (global + repo)
|
|
@@ -2088,36 +2216,54 @@ Host apps also receive \`PORT\` (random free port), \`HOSTNAME=0.0.0.0\`, \`HOST
|
|
|
2088
2216
|
- \`dev logs [-f]\`: Traefik access logs
|
|
2089
2217
|
- \`dev tls install\`: install mkcert certs, enable HTTPS + TCP/SNI
|
|
2090
2218
|
- \`dev repo init\`: create \`.devrouter.yml\`
|
|
2219
|
+
- \`dev repo inspect [--json]\`: inspect package, scripts, compose services, env names, devcontainer, devrouter config, and agent guidance for onboarding
|
|
2220
|
+
- \`dev repo devcontainer write --dry-run --json\`: plan conservative Node/pnpm/Postgres devcontainer/devrouter scaffold files without writing
|
|
2221
|
+
- \`dev repo devcontainer write --yes\`: write managed Node/pnpm/Postgres devcontainer/devrouter scaffold files when no custom-file conflicts exist
|
|
2222
|
+
- \`dev repo devcontainer verify --json\`: emit read-only onboarding evidence for PRs
|
|
2223
|
+
- \`dev repo devcontainer verify --live --yes --json\`: register proxy routes and probe HTTP routes after the devcontainer is running
|
|
2091
2224
|
- \`dev repo agents [--with-linear]\`: write devrouter section in AGENTS.md + install this skill (and optional Linear workflow assets)
|
|
2092
2225
|
- \`dev app add\`: add/update app entry in \`.devrouter.yml\`
|
|
2093
2226
|
- \`dev app ls\`: list app entries
|
|
2094
|
-
- \`dev app run <name
|
|
2095
|
-
- \`dev app exec <name> [--shell] [--env
|
|
2096
|
-
- \`dev app rm <name
|
|
2227
|
+
- \`dev app run <name> [--env <env>] [--workspace <slug>]\`: run app with dependency lifecycle (--env overrides SM defaultEnv; --workspace overrides the per-workspace token)
|
|
2228
|
+
- \`dev app exec <name> [--shell] [--env <env>] [--workspace <slug>] -- <cmd>\`: one-shot command with resolved dep env
|
|
2229
|
+
- \`dev app rm <name> [--keep-config]\`: remove app entry (\`--keep-config\` frees only the live route/hostname, leaves \`.devrouter.yml\` untouched)
|
|
2230
|
+
- \`dev workspace up <branch> [--path <dir>] [--no-devpod] [--open]\`: create a worktree + devpod + namespaced routes
|
|
2231
|
+
- \`dev workspace ls [--json]\`: list git worktrees with workspace token + route count
|
|
2232
|
+
- \`dev workspace down <workspace|branch> [--keep-worktree] [--keep-devpod]\`: free routes + stop devpod + remove worktree
|
|
2097
2233
|
|
|
2098
2234
|
## Validation workflow
|
|
2099
2235
|
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2236
|
+
For devcontainer onboarding:
|
|
2237
|
+
|
|
2238
|
+
1. \`dev setup --repo . --yes --json\`
|
|
2239
|
+
2. \`dev doctor --repo . --json\`
|
|
2240
|
+
3. \`dev repo inspect --repo . --json\`
|
|
2241
|
+
4. \`dev repo devcontainer write --repo . --dry-run --json\`
|
|
2242
|
+
5. \`dev repo devcontainer write --repo . --yes\`
|
|
2243
|
+
6. \`dev repo devcontainer verify --repo . --json\`
|
|
2244
|
+
7. Start the devcontainer, for example \`devpod up .\`
|
|
2245
|
+
8. \`dev repo devcontainer verify --repo . --live --yes --json\`
|
|
2246
|
+
|
|
2247
|
+
For existing host/docker runtime apps:
|
|
2248
|
+
|
|
2249
|
+
1. \`dev setup --repo . --yes\`
|
|
2250
|
+
2. \`dev doctor --repo .\`
|
|
2251
|
+
3. \`dev app ls --repo .\`
|
|
2252
|
+
4. \`dev app run <host-app> --repo . --yes\`
|
|
2253
|
+
5. \`dev ls\`
|
|
2254
|
+
6. \`curl -I https://<host>.localhost\`
|
|
2255
|
+
7. For TCP/Postgres, use \`dev open <name>\` for the connection hint.
|
|
2108
2256
|
|
|
2109
2257
|
## Runtime behavior notes
|
|
2110
2258
|
|
|
2111
|
-
- \`dev app run\` auto-starts Docker dependencies
|
|
2259
|
+
- \`dev app run\` auto-starts Docker dependencies and waits for health. Host app runs stop auto-started docker deps on exit; docker app runs leave target services running until explicit cleanup.
|
|
2112
2260
|
- Host-runtime dependencies are NOT auto-started (v1).
|
|
2113
2261
|
- \`kind=dependency\` entries do not create routes and cannot be direct targets for \`dev app run\`, \`dev app exec\`, or \`dev open\`.
|
|
2114
2262
|
- \`kind=dependency\` services start as declared in compose (no Traefik label wiring, no random port publishing, no injected env vars).
|
|
2115
2263
|
- Postgres on shared \`:5432\` requires TLS/SNI (\`dev tls install\`). Standard app clients should use the injected random port instead.
|
|
2116
|
-
- \`dev app exec\`
|
|
2117
|
-
- \`dev app exec\` stops only deps started by that exec call; already-running deps stay running.
|
|
2118
|
-
- If exec cannot determine pre-existing running services, it leaves selected deps running to avoid non-owned teardown.
|
|
2264
|
+
- \`dev app exec\` follows the same dep lifecycle for one-shot commands and preserves argv semantics by default (\`shell: false\`).
|
|
2119
2265
|
- \`dev app exec --shell\` is explicit and requires exactly one command string after \`--\`.
|
|
2120
|
-
- Secret-manager overlap caveat: if Infisical/Doppler defines DB vars too, probe effective env (\`printenv
|
|
2266
|
+
- Secret-manager overlap caveat: if Infisical/Doppler defines DB vars too, probe effective env (\`printenv DB_URL DB_HOST DB_PORT\`) before migrate/seed.
|
|
2121
2267
|
`;
|
|
2122
2268
|
LINEAR_WORKFLOW_SKILL_CONTENT = `---
|
|
2123
2269
|
name: linear-workflow
|
|
@@ -2235,8 +2381,8 @@ If the repository uses devrouter, use \`dev upgrade\` to resolve the required Ag
|
|
|
2235
2381
|
function isoTimestamp(now) {
|
|
2236
2382
|
return (now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
2237
2383
|
}
|
|
2238
|
-
function requiredPlaceholder(
|
|
2239
|
-
return `<REQUIRED: ${
|
|
2384
|
+
function requiredPlaceholder(path16) {
|
|
2385
|
+
return `<REQUIRED: ${path16}>`;
|
|
2240
2386
|
}
|
|
2241
2387
|
function normalizeOptionalValue(value) {
|
|
2242
2388
|
const trimmed = value.trim();
|
|
@@ -2554,6 +2700,52 @@ function printDoctorReport(report) {
|
|
|
2554
2700
|
}
|
|
2555
2701
|
}
|
|
2556
2702
|
}
|
|
2703
|
+
function printSetupReport(report) {
|
|
2704
|
+
const summaryRows = [
|
|
2705
|
+
["Generated", report.generatedAt],
|
|
2706
|
+
["Repo path", report.repoPath ?? "-"],
|
|
2707
|
+
["Actions performed", String(report.summary.actions.performed)],
|
|
2708
|
+
["Actions skipped", String(report.summary.actions.skipped)],
|
|
2709
|
+
["Actions failed", String(report.summary.actions.failed)],
|
|
2710
|
+
["Checks OK", String(report.summary.checks.ok)],
|
|
2711
|
+
["Checks WARN", String(report.summary.checks.warn)],
|
|
2712
|
+
["Checks ERROR", String(report.summary.checks.error)]
|
|
2713
|
+
];
|
|
2714
|
+
process.stdout.write(`${renderTable(["FIELD", "VALUE"], summaryRows)}
|
|
2715
|
+
|
|
2716
|
+
`);
|
|
2717
|
+
const actionRows = report.actions.map((entry) => [
|
|
2718
|
+
entry.id,
|
|
2719
|
+
entry.status.toUpperCase(),
|
|
2720
|
+
entry.summary,
|
|
2721
|
+
entry.suggestion ?? "-"
|
|
2722
|
+
]);
|
|
2723
|
+
process.stdout.write(`${renderTable(["ACTION", "STATUS", "SUMMARY", "SUGGESTION"], actionRows)}
|
|
2724
|
+
`);
|
|
2725
|
+
const detailedActions = report.actions.filter((entry) => entry.details);
|
|
2726
|
+
if (detailedActions.length > 0) {
|
|
2727
|
+
process.stdout.write("\nAction details:\n");
|
|
2728
|
+
for (const entry of detailedActions) {
|
|
2729
|
+
process.stdout.write(`- ${entry.id}: ${entry.details}
|
|
2730
|
+
`);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
const failingChecks = report.checks.filter((check) => check.level !== "ok");
|
|
2734
|
+
if (failingChecks.length > 0) {
|
|
2735
|
+
process.stdout.write("\nDiagnostic findings:\n");
|
|
2736
|
+
for (const check of failingChecks) {
|
|
2737
|
+
process.stdout.write(`- ${check.id} [${check.level.toUpperCase()}]: ${check.summary}
|
|
2738
|
+
`);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
if (report.nextSteps.length > 0) {
|
|
2742
|
+
process.stdout.write("\nRecommended next steps:\n");
|
|
2743
|
+
for (const step of report.nextSteps) {
|
|
2744
|
+
process.stdout.write(`- ${step}
|
|
2745
|
+
`);
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2557
2749
|
var init_output = __esm({
|
|
2558
2750
|
"src/core/output.ts"() {
|
|
2559
2751
|
"use strict";
|
|
@@ -2946,138 +3138,217 @@ var init_docker = __esm({
|
|
|
2946
3138
|
}
|
|
2947
3139
|
});
|
|
2948
3140
|
|
|
2949
|
-
// src/
|
|
2950
|
-
function
|
|
2951
|
-
const result = (0, import_node_child_process4.spawnSync)(
|
|
3141
|
+
// src/core/tls.ts
|
|
3142
|
+
function runOrThrow(command, args) {
|
|
3143
|
+
const result = (0, import_node_child_process4.spawnSync)(command, args, {
|
|
2952
3144
|
encoding: "utf-8"
|
|
2953
3145
|
});
|
|
2954
|
-
if (result.status !== 0
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
const lines = result.stdout.trim().split(/\r?\n/);
|
|
2958
|
-
if (lines.length <= 1) {
|
|
2959
|
-
return [];
|
|
3146
|
+
if (result.status !== 0) {
|
|
3147
|
+
const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
3148
|
+
throw new Error(`${command} ${args.join(" ")} failed: ${details || "unknown error"}`);
|
|
2960
3149
|
}
|
|
2961
|
-
return lines.slice(1).map((line) => {
|
|
2962
|
-
const parts = line.trim().split(/\s+/);
|
|
2963
|
-
const command = parts[0] ?? "?";
|
|
2964
|
-
const pid = parts[1] ?? "?";
|
|
2965
|
-
const user = parts[2] ?? "?";
|
|
2966
|
-
const address = parts.slice(-2).join(" ") || "?";
|
|
2967
|
-
return {
|
|
2968
|
-
port,
|
|
2969
|
-
command,
|
|
2970
|
-
pid,
|
|
2971
|
-
user,
|
|
2972
|
-
address
|
|
2973
|
-
};
|
|
2974
|
-
});
|
|
2975
3150
|
}
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3151
|
+
function commandExists(command) {
|
|
3152
|
+
const result = (0, import_node_child_process4.spawnSync)("sh", ["-c", `command -v ${command}`], { encoding: "utf-8" });
|
|
3153
|
+
return result.status === 0;
|
|
3154
|
+
}
|
|
3155
|
+
function ensureMkcert() {
|
|
3156
|
+
if (commandExists("mkcert")) {
|
|
3157
|
+
return;
|
|
2981
3158
|
}
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
// src/commands/up.ts
|
|
2985
|
-
var up_exports = {};
|
|
2986
|
-
__export(up_exports, {
|
|
2987
|
-
runUpCommand: () => runUpCommand
|
|
2988
|
-
});
|
|
2989
|
-
function buildPortConflictMessage() {
|
|
2990
|
-
const listeners = [...findPortListeners(80), ...findPortListeners(443), ...findPortListeners(5432)];
|
|
2991
|
-
if (listeners.length === 0) {
|
|
2992
|
-
return "";
|
|
3159
|
+
if (!commandExists("brew")) {
|
|
3160
|
+
throw new Error("mkcert is missing and Homebrew is not available.");
|
|
2993
3161
|
}
|
|
2994
|
-
|
|
2995
|
-
return `- port ${listener.port}: ${listener.command} (pid ${listener.pid}, user ${listener.user}, ${listener.address})`;
|
|
2996
|
-
}).join("\n");
|
|
2997
|
-
return `Cannot start devrouter because host ports 80/443/5432 are already in use:
|
|
2998
|
-
${details}
|
|
2999
|
-
|
|
3000
|
-
Mitigation:
|
|
3001
|
-
1) Stop the conflicting process/container
|
|
3002
|
-
2) Re-run: dev up
|
|
3003
|
-
|
|
3004
|
-
Debug commands:
|
|
3005
|
-
- lsof -nP -iTCP:80 -sTCP:LISTEN
|
|
3006
|
-
- lsof -nP -iTCP:443 -sTCP:LISTEN
|
|
3007
|
-
- lsof -nP -iTCP:5432 -sTCP:LISTEN`;
|
|
3162
|
+
runOrThrow("brew", ["install", "mkcert"]);
|
|
3008
3163
|
}
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3164
|
+
function normalizeHost(host) {
|
|
3165
|
+
return host.trim().toLowerCase();
|
|
3166
|
+
}
|
|
3167
|
+
function normalizeUniqueHosts(hosts) {
|
|
3168
|
+
const set = /* @__PURE__ */ new Set();
|
|
3169
|
+
for (const host of hosts) {
|
|
3170
|
+
const normalized = normalizeHost(host);
|
|
3171
|
+
if (normalized.length === 0) {
|
|
3172
|
+
continue;
|
|
3017
3173
|
}
|
|
3174
|
+
set.add(normalized);
|
|
3018
3175
|
}
|
|
3019
|
-
|
|
3020
|
-
process.stdout.write("devrouter is up.\n");
|
|
3176
|
+
return Array.from(set.values()).sort();
|
|
3021
3177
|
}
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3178
|
+
function parseDnsHostsFromSubjectAltName(subjectAltName) {
|
|
3179
|
+
const names = [];
|
|
3180
|
+
const matches = subjectAltName.matchAll(/DNS:([^,\n]+)/g);
|
|
3181
|
+
for (const match of matches) {
|
|
3182
|
+
const host = normalizeHost(match[1] ?? "");
|
|
3183
|
+
if (host.length > 0) {
|
|
3184
|
+
names.push(host);
|
|
3185
|
+
}
|
|
3028
3186
|
}
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
// src/commands/down.ts
|
|
3032
|
-
var down_exports = {};
|
|
3033
|
-
__export(down_exports, {
|
|
3034
|
-
runDownCommand: () => runDownCommand
|
|
3035
|
-
});
|
|
3036
|
-
async function runDownCommand() {
|
|
3037
|
-
stopRouterStack();
|
|
3038
|
-
clearActiveTcpProtocols();
|
|
3039
|
-
process.stdout.write("devrouter is down.\n");
|
|
3187
|
+
return normalizeUniqueHosts(names);
|
|
3040
3188
|
}
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
});
|
|
3047
|
-
|
|
3048
|
-
// src/core/status.ts
|
|
3049
|
-
function hasPortBinding(ports, privatePort, publicPort) {
|
|
3050
|
-
if (!ports) {
|
|
3051
|
-
return false;
|
|
3189
|
+
function parseCertificateDnsHosts(pem) {
|
|
3190
|
+
const certificate = new import_node_crypto.X509Certificate(pem);
|
|
3191
|
+
const subjectAltName = certificate.subjectAltName ?? "";
|
|
3192
|
+
if (subjectAltName.length === 0) {
|
|
3193
|
+
return [];
|
|
3052
3194
|
}
|
|
3053
|
-
return
|
|
3195
|
+
return parseDnsHostsFromSubjectAltName(subjectAltName);
|
|
3054
3196
|
}
|
|
3055
|
-
function
|
|
3056
|
-
const
|
|
3057
|
-
const
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3197
|
+
function isHostCoveredByCertificateHost(host, certificateHost) {
|
|
3198
|
+
const normalizedHost = normalizeHost(host);
|
|
3199
|
+
const normalizedCertificateHost = normalizeHost(certificateHost);
|
|
3200
|
+
if (normalizedCertificateHost.startsWith("*.")) {
|
|
3201
|
+
const suffix = normalizedCertificateHost.slice(1);
|
|
3202
|
+
if (!normalizedHost.endsWith(suffix)) {
|
|
3203
|
+
return false;
|
|
3204
|
+
}
|
|
3205
|
+
const wildcardPart = normalizedHost.slice(0, normalizedHost.length - suffix.length);
|
|
3206
|
+
return wildcardPart.length > 0 && !wildcardPart.includes(".");
|
|
3062
3207
|
}
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3208
|
+
return normalizedHost === normalizedCertificateHost;
|
|
3209
|
+
}
|
|
3210
|
+
function findUncoveredCertificateHosts(requiredHosts, certificateHosts) {
|
|
3211
|
+
const normalizedRequired = normalizeUniqueHosts(requiredHosts);
|
|
3212
|
+
const normalizedCertificateHosts = normalizeUniqueHosts(certificateHosts);
|
|
3213
|
+
return normalizedRequired.filter(
|
|
3214
|
+
(requiredHost) => !normalizedCertificateHosts.some(
|
|
3215
|
+
(certificateHost) => isHostCoveredByCertificateHost(requiredHost, certificateHost)
|
|
3216
|
+
)
|
|
3217
|
+
);
|
|
3218
|
+
}
|
|
3219
|
+
function readCurrentCertificateHosts() {
|
|
3220
|
+
if (!import_node_fs7.default.existsSync(CERT_FILE)) {
|
|
3221
|
+
return [];
|
|
3073
3222
|
}
|
|
3223
|
+
const pem = import_node_fs7.default.readFileSync(CERT_FILE, "utf-8");
|
|
3224
|
+
return parseCertificateDnsHosts(pem);
|
|
3225
|
+
}
|
|
3226
|
+
function currentCertificateHostsOrEmpty() {
|
|
3074
3227
|
try {
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3228
|
+
return readCurrentCertificateHosts();
|
|
3229
|
+
} catch {
|
|
3230
|
+
return [];
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
function buildDesiredTLSCertificateHosts(requestedHosts, existingCertificateHosts) {
|
|
3234
|
+
return normalizeUniqueHosts([
|
|
3235
|
+
...DEFAULT_TLS_CERT_HOSTS,
|
|
3236
|
+
...existingCertificateHosts,
|
|
3237
|
+
...requestedHosts
|
|
3238
|
+
]);
|
|
3239
|
+
}
|
|
3240
|
+
function getTLSHostCoverage(hosts) {
|
|
3241
|
+
const requiredHosts = normalizeUniqueHosts([...DEFAULT_TLS_CERT_HOSTS, ...hosts]);
|
|
3242
|
+
const certificateHosts = readCurrentCertificateHosts();
|
|
3243
|
+
const uncoveredHosts = findUncoveredCertificateHosts(requiredHosts, certificateHosts);
|
|
3244
|
+
return {
|
|
3245
|
+
requiredHosts,
|
|
3246
|
+
certificateHosts,
|
|
3247
|
+
uncoveredHosts
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
async function installTLS(options = {}) {
|
|
3251
|
+
ensureRouterFiles();
|
|
3252
|
+
const alreadyEnabled = isTLSEnabled();
|
|
3253
|
+
const desiredHosts = buildDesiredTLSCertificateHosts(
|
|
3254
|
+
options.hosts ?? [],
|
|
3255
|
+
currentCertificateHostsOrEmpty()
|
|
3256
|
+
);
|
|
3257
|
+
ensureMkcert();
|
|
3258
|
+
runOrThrow("mkcert", ["-install"]);
|
|
3259
|
+
runOrThrow("mkcert", [
|
|
3260
|
+
"-cert-file",
|
|
3261
|
+
CERT_FILE,
|
|
3262
|
+
"-key-file",
|
|
3263
|
+
CERT_KEY_FILE,
|
|
3264
|
+
...desiredHosts
|
|
3265
|
+
]);
|
|
3266
|
+
setTLSEnabled(true);
|
|
3267
|
+
refreshHostRoutesDynamicFile();
|
|
3268
|
+
const routerContainer = await findContainerByName("devrouter-traefik");
|
|
3269
|
+
if (routerContainer && await isContainerRunning("devrouter-traefik")) {
|
|
3270
|
+
startRouterStack();
|
|
3271
|
+
}
|
|
3272
|
+
return { alreadyEnabled, hosts: desiredHosts };
|
|
3273
|
+
}
|
|
3274
|
+
async function ensureTLSHostsCovered(hosts) {
|
|
3275
|
+
if (!isTLSEnabled()) {
|
|
3276
|
+
return {
|
|
3277
|
+
refreshed: false,
|
|
3278
|
+
uncoveredHosts: [],
|
|
3279
|
+
certificateHosts: []
|
|
3280
|
+
};
|
|
3281
|
+
}
|
|
3282
|
+
const coverage = getTLSHostCoverage(hosts);
|
|
3283
|
+
if (coverage.uncoveredHosts.length === 0) {
|
|
3284
|
+
return {
|
|
3285
|
+
refreshed: false,
|
|
3286
|
+
uncoveredHosts: [],
|
|
3287
|
+
certificateHosts: coverage.certificateHosts
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
try {
|
|
3291
|
+
const refreshed = await installTLS({ hosts: coverage.requiredHosts });
|
|
3292
|
+
return {
|
|
3293
|
+
refreshed: true,
|
|
3294
|
+
uncoveredHosts: coverage.uncoveredHosts,
|
|
3295
|
+
certificateHosts: refreshed.hosts
|
|
3296
|
+
};
|
|
3297
|
+
} catch (error) {
|
|
3298
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3299
|
+
throw new Error(
|
|
3300
|
+
`TLS cert does not currently cover host(s): ${coverage.uncoveredHosts.join(", ")}. Automatic refresh failed: ${message}
|
|
3301
|
+
Run: dev tls install`
|
|
3302
|
+
);
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
var import_node_fs7, import_node_child_process4, import_node_crypto, DEFAULT_TLS_CERT_HOSTS;
|
|
3306
|
+
var init_tls = __esm({
|
|
3307
|
+
"src/core/tls.ts"() {
|
|
3308
|
+
"use strict";
|
|
3309
|
+
import_node_fs7 = __toESM(require("fs"));
|
|
3310
|
+
import_node_child_process4 = require("child_process");
|
|
3311
|
+
import_node_crypto = require("crypto");
|
|
3312
|
+
init_router();
|
|
3313
|
+
init_docker();
|
|
3314
|
+
init_host_routes();
|
|
3315
|
+
DEFAULT_TLS_CERT_HOSTS = ["localhost", "*.localhost"];
|
|
3316
|
+
}
|
|
3317
|
+
});
|
|
3318
|
+
|
|
3319
|
+
// src/core/status.ts
|
|
3320
|
+
function hasPortBinding(ports, privatePort, publicPort) {
|
|
3321
|
+
if (!ports) {
|
|
3322
|
+
return false;
|
|
3323
|
+
}
|
|
3324
|
+
return ports.some((port) => port.PrivatePort === privatePort && port.PublicPort === publicPort);
|
|
3325
|
+
}
|
|
3326
|
+
function toRepoStatus(repoPath) {
|
|
3327
|
+
const resolvedRepoPath = resolveRepoPath(repoPath);
|
|
3328
|
+
const configPath = getRepoConfigPath(resolvedRepoPath);
|
|
3329
|
+
const explicitRepo = typeof repoPath === "string" && repoPath.trim().length > 0;
|
|
3330
|
+
const configExists = import_node_fs8.default.existsSync(configPath);
|
|
3331
|
+
if (!explicitRepo && !configExists) {
|
|
3332
|
+
return void 0;
|
|
3333
|
+
}
|
|
3334
|
+
if (!configExists) {
|
|
3335
|
+
return {
|
|
3336
|
+
path: resolvedRepoPath,
|
|
3337
|
+
configPath,
|
|
3338
|
+
exists: false,
|
|
3339
|
+
valid: false,
|
|
3340
|
+
appCount: 0,
|
|
3341
|
+
tcpAppCount: 0,
|
|
3342
|
+
error: `Missing .devrouter.yml in ${resolvedRepoPath}`
|
|
3343
|
+
};
|
|
3344
|
+
}
|
|
3345
|
+
try {
|
|
3346
|
+
const config = loadRuntimeConfig(resolvedRepoPath).config;
|
|
3347
|
+
const tcpAppCount = config.apps.filter(
|
|
3348
|
+
(app) => app.kind !== "dependency" && app.protocol === "tcp"
|
|
3349
|
+
).length;
|
|
3350
|
+
return {
|
|
3351
|
+
path: resolvedRepoPath,
|
|
3081
3352
|
configPath,
|
|
3082
3353
|
exists: true,
|
|
3083
3354
|
valid: true,
|
|
@@ -3181,141 +3452,17 @@ async function collectRouterStatus(repoPath) {
|
|
|
3181
3452
|
}
|
|
3182
3453
|
};
|
|
3183
3454
|
}
|
|
3184
|
-
var
|
|
3455
|
+
var import_node_fs8;
|
|
3185
3456
|
var init_status = __esm({
|
|
3186
3457
|
"src/core/status.ts"() {
|
|
3187
3458
|
"use strict";
|
|
3188
|
-
|
|
3459
|
+
import_node_fs8 = __toESM(require("fs"));
|
|
3189
3460
|
init_docker();
|
|
3190
3461
|
init_router();
|
|
3191
3462
|
init_repo_config();
|
|
3192
3463
|
}
|
|
3193
3464
|
});
|
|
3194
3465
|
|
|
3195
|
-
// src/commands/status.ts
|
|
3196
|
-
var status_exports = {};
|
|
3197
|
-
__export(status_exports, {
|
|
3198
|
-
runStatusCommand: () => runStatusCommand
|
|
3199
|
-
});
|
|
3200
|
-
async function runStatusCommand(options) {
|
|
3201
|
-
const status = await collectRouterStatus(options.repo);
|
|
3202
|
-
if (options.json) {
|
|
3203
|
-
printJSON(status);
|
|
3204
|
-
return;
|
|
3205
|
-
}
|
|
3206
|
-
printStatus(status);
|
|
3207
|
-
}
|
|
3208
|
-
var init_status2 = __esm({
|
|
3209
|
-
"src/commands/status.ts"() {
|
|
3210
|
-
"use strict";
|
|
3211
|
-
init_output();
|
|
3212
|
-
init_status();
|
|
3213
|
-
}
|
|
3214
|
-
});
|
|
3215
|
-
|
|
3216
|
-
// src/core/concurrency.ts
|
|
3217
|
-
function routeUrl(host) {
|
|
3218
|
-
const scheme = isTLSEnabled() ? "https" : "http";
|
|
3219
|
-
return `${scheme}://${host}`;
|
|
3220
|
-
}
|
|
3221
|
-
function evictIfStale(route) {
|
|
3222
|
-
if (route.mode === "proxy") {
|
|
3223
|
-
return false;
|
|
3224
|
-
}
|
|
3225
|
-
if (isPidRunning(route.pid)) {
|
|
3226
|
-
return false;
|
|
3227
|
-
}
|
|
3228
|
-
removeHostRouteById(route.id);
|
|
3229
|
-
return true;
|
|
3230
|
-
}
|
|
3231
|
-
function assertAppNotRunning(repoPath, app) {
|
|
3232
|
-
const routes = listHostRouteState();
|
|
3233
|
-
const targetId = buildHostRouteId(repoPath, app.name);
|
|
3234
|
-
for (const route of routes) {
|
|
3235
|
-
if (route.id === targetId) {
|
|
3236
|
-
if (evictIfStale(route)) {
|
|
3237
|
-
continue;
|
|
3238
|
-
}
|
|
3239
|
-
throw new AppAlreadyRunningError(
|
|
3240
|
-
app.name,
|
|
3241
|
-
routeUrl(route.host),
|
|
3242
|
-
route.pid,
|
|
3243
|
-
route.repoPath
|
|
3244
|
-
);
|
|
3245
|
-
}
|
|
3246
|
-
if (route.host === app.host) {
|
|
3247
|
-
if (evictIfStale(route)) {
|
|
3248
|
-
continue;
|
|
3249
|
-
}
|
|
3250
|
-
throw new HostnameConflictError(
|
|
3251
|
-
app.host,
|
|
3252
|
-
route.name,
|
|
3253
|
-
route.repoPath,
|
|
3254
|
-
route.pid
|
|
3255
|
-
);
|
|
3256
|
-
}
|
|
3257
|
-
}
|
|
3258
|
-
}
|
|
3259
|
-
function evictStaleHostRoutes() {
|
|
3260
|
-
const routes = listHostRouteState();
|
|
3261
|
-
let evicted = 0;
|
|
3262
|
-
for (const route of routes) {
|
|
3263
|
-
if (evictIfStale(route)) {
|
|
3264
|
-
evicted += 1;
|
|
3265
|
-
}
|
|
3266
|
-
}
|
|
3267
|
-
return evicted;
|
|
3268
|
-
}
|
|
3269
|
-
function evictOrphanedWorkspaceRoutes() {
|
|
3270
|
-
const orphans = listHostRouteState().filter(
|
|
3271
|
-
(route) => route.mode === "proxy" && route.workspace !== void 0 && !import_node_fs8.default.existsSync(route.repoPath)
|
|
3272
|
-
);
|
|
3273
|
-
for (const route of orphans) {
|
|
3274
|
-
removeHostRouteById(route.id);
|
|
3275
|
-
}
|
|
3276
|
-
return orphans.length;
|
|
3277
|
-
}
|
|
3278
|
-
var import_node_fs8, AppAlreadyRunningError, HostnameConflictError;
|
|
3279
|
-
var init_concurrency = __esm({
|
|
3280
|
-
"src/core/concurrency.ts"() {
|
|
3281
|
-
"use strict";
|
|
3282
|
-
import_node_fs8 = __toESM(require("fs"));
|
|
3283
|
-
init_router();
|
|
3284
|
-
init_host_routes();
|
|
3285
|
-
AppAlreadyRunningError = class extends Error {
|
|
3286
|
-
constructor(appName, url, pid, repoPath) {
|
|
3287
|
-
const lines = [
|
|
3288
|
-
`App "${appName}" is already running.`,
|
|
3289
|
-
` URL: ${url}`,
|
|
3290
|
-
pid ? ` PID: ${pid}` : null,
|
|
3291
|
-
` Repo: ${repoPath}`
|
|
3292
|
-
].filter(Boolean);
|
|
3293
|
-
super(lines.join("\n"));
|
|
3294
|
-
this.appName = appName;
|
|
3295
|
-
this.url = url;
|
|
3296
|
-
this.pid = pid;
|
|
3297
|
-
this.repoPath = repoPath;
|
|
3298
|
-
this.name = "AppAlreadyRunningError";
|
|
3299
|
-
}
|
|
3300
|
-
};
|
|
3301
|
-
HostnameConflictError = class extends Error {
|
|
3302
|
-
constructor(hostname, existingApp, existingRepoPath, existingPid) {
|
|
3303
|
-
const lines = [
|
|
3304
|
-
`Hostname "${hostname}" is already claimed by app "${existingApp}".`,
|
|
3305
|
-
existingPid ? ` PID: ${existingPid}` : null,
|
|
3306
|
-
` Repo: ${existingRepoPath}`
|
|
3307
|
-
].filter(Boolean);
|
|
3308
|
-
super(lines.join("\n"));
|
|
3309
|
-
this.hostname = hostname;
|
|
3310
|
-
this.existingApp = existingApp;
|
|
3311
|
-
this.existingRepoPath = existingRepoPath;
|
|
3312
|
-
this.existingPid = existingPid;
|
|
3313
|
-
this.name = "HostnameConflictError";
|
|
3314
|
-
}
|
|
3315
|
-
};
|
|
3316
|
-
}
|
|
3317
|
-
});
|
|
3318
|
-
|
|
3319
3466
|
// src/core/routes.ts
|
|
3320
3467
|
function resolveEntrypointToProtocol(entrypoint) {
|
|
3321
3468
|
for (const [protocol, entry] of Object.entries(TCP_PROTOCOL_REGISTRY)) {
|
|
@@ -3491,186 +3638,475 @@ var init_paths = __esm({
|
|
|
3491
3638
|
}
|
|
3492
3639
|
});
|
|
3493
3640
|
|
|
3494
|
-
// src/core/
|
|
3495
|
-
function
|
|
3496
|
-
const
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
throw new Error(`${command} ${args.join(" ")} failed: ${details || "unknown error"}`);
|
|
3641
|
+
// src/core/route-state.ts
|
|
3642
|
+
function comparablePath(filePath) {
|
|
3643
|
+
const resolved = import_node_path8.default.resolve(filePath);
|
|
3644
|
+
try {
|
|
3645
|
+
return import_node_fs9.default.realpathSync.native(resolved);
|
|
3646
|
+
} catch {
|
|
3647
|
+
return resolved;
|
|
3502
3648
|
}
|
|
3503
3649
|
}
|
|
3504
|
-
function
|
|
3505
|
-
|
|
3506
|
-
return result.status === 0;
|
|
3650
|
+
function sameRoutePath(left, right) {
|
|
3651
|
+
return comparablePath(left) === comparablePath(right);
|
|
3507
3652
|
}
|
|
3508
|
-
function
|
|
3509
|
-
|
|
3510
|
-
|
|
3653
|
+
function listRoutesForWorktreePaths(worktreePaths) {
|
|
3654
|
+
const routes = listHostRouteState();
|
|
3655
|
+
const byWorktreePath = /* @__PURE__ */ new Map();
|
|
3656
|
+
for (const worktreePath of worktreePaths) {
|
|
3657
|
+
byWorktreePath.set(worktreePath, []);
|
|
3511
3658
|
}
|
|
3512
|
-
|
|
3513
|
-
|
|
3659
|
+
for (const route of routes) {
|
|
3660
|
+
for (const worktreePath of worktreePaths) {
|
|
3661
|
+
if (sameRoutePath(route.repoPath, worktreePath)) {
|
|
3662
|
+
byWorktreePath.get(worktreePath)?.push(route);
|
|
3663
|
+
break;
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3514
3666
|
}
|
|
3515
|
-
|
|
3667
|
+
return byWorktreePath;
|
|
3516
3668
|
}
|
|
3517
|
-
function
|
|
3518
|
-
return
|
|
3669
|
+
function removeWorkspaceRoutesForWorktree(workspace, worktreePath) {
|
|
3670
|
+
return removeHostRoutesWhere(
|
|
3671
|
+
(route) => route.workspace === workspace && sameRoutePath(route.repoPath, worktreePath)
|
|
3672
|
+
);
|
|
3519
3673
|
}
|
|
3520
|
-
function
|
|
3521
|
-
|
|
3522
|
-
for (const host of hosts) {
|
|
3523
|
-
const normalized = normalizeHost(host);
|
|
3524
|
-
if (normalized.length === 0) {
|
|
3525
|
-
continue;
|
|
3526
|
-
}
|
|
3527
|
-
set.add(normalized);
|
|
3528
|
-
}
|
|
3529
|
-
return Array.from(set.values()).sort();
|
|
3674
|
+
function removeRouteForApp(repoPath, appName) {
|
|
3675
|
+
return removeHostRoutesWhere((route) => route.name === appName && sameRoutePath(route.repoPath, repoPath));
|
|
3530
3676
|
}
|
|
3531
|
-
function
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
for (const match of matches) {
|
|
3535
|
-
const host = normalizeHost(match[1] ?? "");
|
|
3536
|
-
if (host.length > 0) {
|
|
3537
|
-
names.push(host);
|
|
3538
|
-
}
|
|
3677
|
+
function isStaleProcessRoute(route) {
|
|
3678
|
+
if (route.mode === "proxy") {
|
|
3679
|
+
return false;
|
|
3539
3680
|
}
|
|
3540
|
-
|
|
3681
|
+
if (!route.pid) {
|
|
3682
|
+
return true;
|
|
3683
|
+
}
|
|
3684
|
+
return !isPidRunning(route.pid);
|
|
3541
3685
|
}
|
|
3542
|
-
function
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3686
|
+
function findStaleProcessRoutes(routes = listHostRouteState()) {
|
|
3687
|
+
return routes.filter((route) => isStaleProcessRoute(route));
|
|
3688
|
+
}
|
|
3689
|
+
function evictStaleRouteIfNeeded(route) {
|
|
3690
|
+
if (!isStaleProcessRoute(route)) {
|
|
3691
|
+
return "not-stale";
|
|
3547
3692
|
}
|
|
3548
|
-
|
|
3693
|
+
const removed = removeHostRoutesWhere(
|
|
3694
|
+
(candidate) => candidate.id === route.id && isStaleProcessRoute(candidate)
|
|
3695
|
+
);
|
|
3696
|
+
return removed.length > 0 ? "evicted" : "changed";
|
|
3549
3697
|
}
|
|
3550
|
-
function
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
const
|
|
3555
|
-
|
|
3556
|
-
|
|
3698
|
+
function reconcileRouteRunConflict(repoPath, app) {
|
|
3699
|
+
for (; ; ) {
|
|
3700
|
+
const routes = listHostRouteState();
|
|
3701
|
+
let shouldRetry = false;
|
|
3702
|
+
for (const route of routes) {
|
|
3703
|
+
if (route.name === app.name && sameRoutePath(route.repoPath, repoPath)) {
|
|
3704
|
+
const staleEviction = evictStaleRouteIfNeeded(route);
|
|
3705
|
+
if (staleEviction === "evicted") {
|
|
3706
|
+
continue;
|
|
3707
|
+
}
|
|
3708
|
+
if (staleEviction === "changed") {
|
|
3709
|
+
shouldRetry = true;
|
|
3710
|
+
break;
|
|
3711
|
+
}
|
|
3712
|
+
return { kind: "same-app", route };
|
|
3713
|
+
}
|
|
3714
|
+
if (route.host === app.host) {
|
|
3715
|
+
const staleEviction = evictStaleRouteIfNeeded(route);
|
|
3716
|
+
if (staleEviction === "evicted") {
|
|
3717
|
+
continue;
|
|
3718
|
+
}
|
|
3719
|
+
if (staleEviction === "changed") {
|
|
3720
|
+
shouldRetry = true;
|
|
3721
|
+
break;
|
|
3722
|
+
}
|
|
3723
|
+
return { kind: "hostname", route };
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
if (!shouldRetry) {
|
|
3727
|
+
return void 0;
|
|
3557
3728
|
}
|
|
3558
|
-
const wildcardPart = normalizedHost.slice(0, normalizedHost.length - suffix.length);
|
|
3559
|
-
return wildcardPart.length > 0 && !wildcardPart.includes(".");
|
|
3560
3729
|
}
|
|
3561
|
-
return normalizedHost === normalizedCertificateHost;
|
|
3562
3730
|
}
|
|
3563
|
-
function
|
|
3564
|
-
|
|
3565
|
-
const normalizedCertificateHosts = normalizeUniqueHosts(certificateHosts);
|
|
3566
|
-
return normalizedRequired.filter(
|
|
3567
|
-
(requiredHost) => !normalizedCertificateHosts.some(
|
|
3568
|
-
(certificateHost) => isHostCoveredByCertificateHost(requiredHost, certificateHost)
|
|
3569
|
-
)
|
|
3570
|
-
);
|
|
3731
|
+
function isOrphanedWorkspaceProxyRoute(route) {
|
|
3732
|
+
return route.mode === "proxy" && route.workspace !== void 0 && !import_node_fs9.default.existsSync(route.repoPath);
|
|
3571
3733
|
}
|
|
3572
|
-
function
|
|
3573
|
-
|
|
3574
|
-
|
|
3734
|
+
function findOrphanedWorkspaceProxyRoutes(routes = listHostRouteState()) {
|
|
3735
|
+
return routes.filter((route) => isOrphanedWorkspaceProxyRoute(route));
|
|
3736
|
+
}
|
|
3737
|
+
var import_node_fs9, import_node_path8;
|
|
3738
|
+
var init_route_state = __esm({
|
|
3739
|
+
"src/core/route-state.ts"() {
|
|
3740
|
+
"use strict";
|
|
3741
|
+
import_node_fs9 = __toESM(require("fs"));
|
|
3742
|
+
import_node_path8 = __toESM(require("path"));
|
|
3743
|
+
init_host_routes();
|
|
3575
3744
|
}
|
|
3576
|
-
|
|
3577
|
-
|
|
3745
|
+
});
|
|
3746
|
+
|
|
3747
|
+
// src/core/tool-diagnostics.ts
|
|
3748
|
+
function outputFromResult(result) {
|
|
3749
|
+
const stdout = typeof result.stdout === "string" ? result.stdout.trim() : "";
|
|
3750
|
+
const stderr = typeof result.stderr === "string" ? result.stderr.trim() : "";
|
|
3751
|
+
const output3 = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
3752
|
+
return output3.length > 0 ? output3 : void 0;
|
|
3578
3753
|
}
|
|
3579
|
-
function
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
}
|
|
3583
|
-
|
|
3754
|
+
function runTool(command, args = []) {
|
|
3755
|
+
const result = (0, import_node_child_process5.spawnSync)(command, args, {
|
|
3756
|
+
encoding: "utf-8"
|
|
3757
|
+
});
|
|
3758
|
+
if (result.error) {
|
|
3759
|
+
return {
|
|
3760
|
+
ok: false,
|
|
3761
|
+
error: result.error.message
|
|
3762
|
+
};
|
|
3763
|
+
}
|
|
3764
|
+
const output3 = outputFromResult(result);
|
|
3765
|
+
if (result.status === 0) {
|
|
3766
|
+
return { ok: true, output: output3 };
|
|
3584
3767
|
}
|
|
3768
|
+
return {
|
|
3769
|
+
ok: false,
|
|
3770
|
+
output: output3,
|
|
3771
|
+
error: output3 ?? `${command} ${args.join(" ")} exited with status ${result.status ?? "unknown"}`
|
|
3772
|
+
};
|
|
3585
3773
|
}
|
|
3586
|
-
function
|
|
3587
|
-
return
|
|
3588
|
-
...DEFAULT_TLS_CERT_HOSTS,
|
|
3589
|
-
...existingCertificateHosts,
|
|
3590
|
-
...requestedHosts
|
|
3591
|
-
]);
|
|
3774
|
+
function firstLine(value) {
|
|
3775
|
+
return value?.split(/\r?\n/).find((line) => line.trim().length > 0)?.trim();
|
|
3592
3776
|
}
|
|
3593
|
-
function
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3777
|
+
function parsePackageManager(value) {
|
|
3778
|
+
if (typeof value !== "string") {
|
|
3779
|
+
return void 0;
|
|
3780
|
+
}
|
|
3781
|
+
const trimmed = value.trim();
|
|
3782
|
+
if (trimmed.length === 0) {
|
|
3783
|
+
return void 0;
|
|
3784
|
+
}
|
|
3785
|
+
const separator = trimmed.lastIndexOf("@");
|
|
3786
|
+
if (separator <= 0) {
|
|
3787
|
+
return { name: trimmed };
|
|
3788
|
+
}
|
|
3597
3789
|
return {
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
uncoveredHosts
|
|
3790
|
+
name: trimmed.slice(0, separator),
|
|
3791
|
+
version: trimmed.slice(separator + 1)
|
|
3601
3792
|
};
|
|
3602
3793
|
}
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
const desiredHosts = buildDesiredTLSCertificateHosts(
|
|
3607
|
-
options.hosts ?? [],
|
|
3608
|
-
currentCertificateHostsOrEmpty()
|
|
3609
|
-
);
|
|
3610
|
-
ensureMkcert();
|
|
3611
|
-
runOrThrow("mkcert", ["-install"]);
|
|
3612
|
-
runOrThrow("mkcert", [
|
|
3613
|
-
"-cert-file",
|
|
3614
|
-
CERT_FILE,
|
|
3615
|
-
"-key-file",
|
|
3616
|
-
CERT_KEY_FILE,
|
|
3617
|
-
...desiredHosts
|
|
3618
|
-
]);
|
|
3619
|
-
setTLSEnabled(true);
|
|
3620
|
-
refreshHostRoutesDynamicFile();
|
|
3621
|
-
const routerContainer = await findContainerByName("devrouter-traefik");
|
|
3622
|
-
if (routerContainer && await isContainerRunning("devrouter-traefik")) {
|
|
3623
|
-
startRouterStack();
|
|
3794
|
+
function parseMajor(value) {
|
|
3795
|
+
if (!value) {
|
|
3796
|
+
return void 0;
|
|
3624
3797
|
}
|
|
3625
|
-
|
|
3798
|
+
const match = value.match(/(\d+)/);
|
|
3799
|
+
return match ? Number(match[1]) : void 0;
|
|
3626
3800
|
}
|
|
3627
|
-
|
|
3628
|
-
if (
|
|
3801
|
+
function parseMinimumNodeMajor(value) {
|
|
3802
|
+
if (typeof value !== "string") {
|
|
3803
|
+
return void 0;
|
|
3804
|
+
}
|
|
3805
|
+
const match = value.match(/>=\s*(\d+)/);
|
|
3806
|
+
if (!match) {
|
|
3807
|
+
return void 0;
|
|
3808
|
+
}
|
|
3809
|
+
return Number(match[1]);
|
|
3810
|
+
}
|
|
3811
|
+
function readPackageJson(repoPath) {
|
|
3812
|
+
const packagePath = import_node_path9.default.join(repoPath, "package.json");
|
|
3813
|
+
if (!import_node_fs10.default.existsSync(packagePath)) {
|
|
3814
|
+
return void 0;
|
|
3815
|
+
}
|
|
3816
|
+
try {
|
|
3817
|
+
return JSON.parse(import_node_fs10.default.readFileSync(packagePath, "utf-8"));
|
|
3818
|
+
} catch {
|
|
3819
|
+
return void 0;
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
function nodeToolchainCheck(repoPath) {
|
|
3823
|
+
const pkg = readPackageJson(repoPath);
|
|
3824
|
+
if (!pkg) {
|
|
3629
3825
|
return {
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3826
|
+
id: "global.node-toolchain",
|
|
3827
|
+
level: "ok",
|
|
3828
|
+
summary: "No package.json found; Node toolchain check is not applicable."
|
|
3633
3829
|
};
|
|
3634
3830
|
}
|
|
3635
|
-
const
|
|
3636
|
-
|
|
3831
|
+
const engines = typeof pkg.engines === "object" && pkg.engines ? pkg.engines : {};
|
|
3832
|
+
const volta = typeof pkg.volta === "object" && pkg.volta ? pkg.volta : {};
|
|
3833
|
+
const nodeRequirement = typeof volta.node === "string" ? volta.node : engines.node;
|
|
3834
|
+
const minimumNodeMajor = parseMinimumNodeMajor(nodeRequirement) ?? parseMajor(String(nodeRequirement ?? ""));
|
|
3835
|
+
const currentNodeMajor = parseMajor(process.versions.node);
|
|
3836
|
+
const packageManager = parsePackageManager(pkg.packageManager);
|
|
3837
|
+
const details = [`node=${process.versions.node}`];
|
|
3838
|
+
const problems = [];
|
|
3839
|
+
if (minimumNodeMajor !== void 0) {
|
|
3840
|
+
details.push(`expectedNode=${String(nodeRequirement)}`);
|
|
3841
|
+
if (currentNodeMajor !== void 0 && currentNodeMajor < minimumNodeMajor) {
|
|
3842
|
+
problems.push(`Node ${process.versions.node} is older than ${String(nodeRequirement)}`);
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
if (packageManager?.name === "pnpm") {
|
|
3846
|
+
const pnpm = runTool("pnpm", ["--version"]);
|
|
3847
|
+
if (!pnpm.ok) {
|
|
3848
|
+
problems.push(`pnpm is missing (${pnpm.error ?? "not found"})`);
|
|
3849
|
+
} else {
|
|
3850
|
+
const actualPnpm = firstLine(pnpm.output) ?? "unknown";
|
|
3851
|
+
details.push(`pnpm=${actualPnpm}`);
|
|
3852
|
+
if (packageManager.version) {
|
|
3853
|
+
details.push(`expectedPnpm=${packageManager.version}`);
|
|
3854
|
+
const expectedMajor = parseMajor(packageManager.version);
|
|
3855
|
+
const actualMajor = parseMajor(actualPnpm);
|
|
3856
|
+
if (expectedMajor !== void 0 && actualMajor !== void 0 && expectedMajor !== actualMajor) {
|
|
3857
|
+
problems.push(`pnpm major ${actualMajor} does not match expected ${expectedMajor}`);
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3861
|
+
} else if (packageManager) {
|
|
3862
|
+
details.push(`packageManager=${packageManager.name}${packageManager.version ? `@${packageManager.version}` : ""}`);
|
|
3863
|
+
}
|
|
3864
|
+
if (problems.length > 0) {
|
|
3637
3865
|
return {
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3866
|
+
id: "global.node-toolchain",
|
|
3867
|
+
level: "warn",
|
|
3868
|
+
summary: "Node package toolchain may not match this repo.",
|
|
3869
|
+
details: [...details, ...problems].join(", "),
|
|
3870
|
+
suggestion: packageManager?.name === "pnpm" && packageManager.version ? `Install pnpm ${packageManager.version}: npm install -g pnpm@${packageManager.version}` : "Install the Node/package-manager versions declared by this repo."
|
|
3871
|
+
};
|
|
3872
|
+
}
|
|
3873
|
+
return {
|
|
3874
|
+
id: "global.node-toolchain",
|
|
3875
|
+
level: "ok",
|
|
3876
|
+
summary: "Node package toolchain is available for this repo.",
|
|
3877
|
+
details: details.join(", ")
|
|
3878
|
+
};
|
|
3879
|
+
}
|
|
3880
|
+
function buildGlobalToolChecks(repoPath) {
|
|
3881
|
+
const checks = [];
|
|
3882
|
+
const compose = runTool("docker", ["compose", "version"]);
|
|
3883
|
+
checks.push({
|
|
3884
|
+
id: "global.docker-compose",
|
|
3885
|
+
level: compose.ok ? "ok" : "error",
|
|
3886
|
+
summary: compose.ok ? "Docker Compose v2 is reachable." : "Docker Compose v2 is not reachable.",
|
|
3887
|
+
details: firstLine(compose.output) ?? compose.error,
|
|
3888
|
+
suggestion: compose.ok ? void 0 : "Install/start Docker with Compose v2, then run: dev setup --yes"
|
|
3889
|
+
});
|
|
3890
|
+
const mkcert = runTool("mkcert", ["-version"]);
|
|
3891
|
+
const brew = runTool("brew", ["--version"]);
|
|
3892
|
+
checks.push({
|
|
3893
|
+
id: "global.mkcert",
|
|
3894
|
+
level: mkcert.ok ? "ok" : "warn",
|
|
3895
|
+
summary: mkcert.ok ? "mkcert is installed." : "mkcert is not installed.",
|
|
3896
|
+
details: mkcert.ok ? firstLine(mkcert.output) : mkcert.error,
|
|
3897
|
+
suggestion: mkcert.ok ? void 0 : brew.ok ? "Install mkcert: brew install mkcert" : "Install mkcert for local HTTPS, then run: dev setup --yes"
|
|
3898
|
+
});
|
|
3899
|
+
const devpod = runTool("devpod", ["version"]);
|
|
3900
|
+
checks.push({
|
|
3901
|
+
id: "global.devpod",
|
|
3902
|
+
level: devpod.ok ? "ok" : "warn",
|
|
3903
|
+
summary: devpod.ok ? "DevPod is installed." : "DevPod is not installed.",
|
|
3904
|
+
details: firstLine(devpod.output) ?? devpod.error,
|
|
3905
|
+
suggestion: devpod.ok ? void 0 : "Install DevPod for devcontainer workspace flows: brew install devpod"
|
|
3906
|
+
});
|
|
3907
|
+
checks.push(nodeToolchainCheck(repoPath));
|
|
3908
|
+
return checks;
|
|
3909
|
+
}
|
|
3910
|
+
var import_node_fs10, import_node_path9, import_node_child_process5;
|
|
3911
|
+
var init_tool_diagnostics = __esm({
|
|
3912
|
+
"src/core/tool-diagnostics.ts"() {
|
|
3913
|
+
"use strict";
|
|
3914
|
+
import_node_fs10 = __toESM(require("fs"));
|
|
3915
|
+
import_node_path9 = __toESM(require("path"));
|
|
3916
|
+
import_node_child_process5 = require("child_process");
|
|
3917
|
+
}
|
|
3918
|
+
});
|
|
3919
|
+
|
|
3920
|
+
// src/core/devcontainer-diagnostics.ts
|
|
3921
|
+
function asRecord(value) {
|
|
3922
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3923
|
+
return void 0;
|
|
3924
|
+
}
|
|
3925
|
+
return value;
|
|
3926
|
+
}
|
|
3927
|
+
function asStringArray(value) {
|
|
3928
|
+
if (!Array.isArray(value)) {
|
|
3929
|
+
return [];
|
|
3930
|
+
}
|
|
3931
|
+
return value.filter((entry) => typeof entry === "string");
|
|
3932
|
+
}
|
|
3933
|
+
function normalizeWorkspaceToken(value) {
|
|
3934
|
+
return value.replace(/\$\{WORKSPACE(?::-[^}]+)?\}/g, "${WORKSPACE}");
|
|
3935
|
+
}
|
|
3936
|
+
function expandAliasCandidates(value, workspace) {
|
|
3937
|
+
const candidates = /* @__PURE__ */ new Set([normalizeWorkspaceToken(value)]);
|
|
3938
|
+
const defaulted = value.replace(/\$\{WORKSPACE:-([^}]+)\}/g, "$1");
|
|
3939
|
+
if (defaulted !== value) {
|
|
3940
|
+
candidates.add(defaulted);
|
|
3941
|
+
}
|
|
3942
|
+
if (workspace) {
|
|
3943
|
+
const workspaceValue = value.replace(/\$\{WORKSPACE:-[^}]+\}/g, workspace).replace(/\$\{WORKSPACE\}/g, workspace);
|
|
3944
|
+
candidates.add(workspaceValue);
|
|
3945
|
+
}
|
|
3946
|
+
return Array.from(candidates.values());
|
|
3947
|
+
}
|
|
3948
|
+
function formatPublishedPort(serviceName, value) {
|
|
3949
|
+
if (typeof value === "string") {
|
|
3950
|
+
return `${serviceName}: ${value}`;
|
|
3951
|
+
}
|
|
3952
|
+
const record = asRecord(value);
|
|
3953
|
+
if (!record) {
|
|
3954
|
+
return void 0;
|
|
3955
|
+
}
|
|
3956
|
+
const target = record.target;
|
|
3957
|
+
const published = record.published;
|
|
3958
|
+
if (published !== void 0 && target !== void 0) {
|
|
3959
|
+
return `${serviceName}: ${String(published)}:${String(target)}`;
|
|
3960
|
+
}
|
|
3961
|
+
if (target !== void 0) {
|
|
3962
|
+
return `${serviceName}: target ${String(target)}`;
|
|
3963
|
+
}
|
|
3964
|
+
return `${serviceName}: ${JSON.stringify(record)}`;
|
|
3965
|
+
}
|
|
3966
|
+
function upstreamHost(upstream) {
|
|
3967
|
+
const separator = upstream.lastIndexOf(":");
|
|
3968
|
+
return separator > 0 ? upstream.slice(0, separator) : upstream;
|
|
3969
|
+
}
|
|
3970
|
+
function isLoopbackHost(host) {
|
|
3971
|
+
return host === "127.0.0.1" || host === "localhost" || host === "host.docker.internal";
|
|
3972
|
+
}
|
|
3973
|
+
function inspectCompose(composeFile, workspace) {
|
|
3974
|
+
if (!import_node_fs11.default.existsSync(composeFile)) {
|
|
3975
|
+
return {
|
|
3976
|
+
aliases: [],
|
|
3977
|
+
publishedPorts: [],
|
|
3978
|
+
devnetExternal: false,
|
|
3979
|
+
parseError: `.devcontainer/docker-compose.yml is missing.`
|
|
3641
3980
|
};
|
|
3642
3981
|
}
|
|
3643
3982
|
try {
|
|
3644
|
-
const
|
|
3983
|
+
const raw = import_node_fs11.default.readFileSync(composeFile, "utf-8");
|
|
3984
|
+
const parsed = import_yaml3.default.parse(raw);
|
|
3985
|
+
const root = asRecord(parsed);
|
|
3986
|
+
const services = asRecord(root?.services);
|
|
3987
|
+
const networks = asRecord(root?.networks);
|
|
3988
|
+
const devnet = asRecord(networks?.devnet);
|
|
3989
|
+
const external = devnet?.external;
|
|
3990
|
+
const devnetExternal = external === true || asRecord(external) !== void 0;
|
|
3991
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
3992
|
+
const publishedPorts = [];
|
|
3993
|
+
for (const [serviceName, serviceValue] of Object.entries(services ?? {})) {
|
|
3994
|
+
const service = asRecord(serviceValue);
|
|
3995
|
+
if (!service) {
|
|
3996
|
+
continue;
|
|
3997
|
+
}
|
|
3998
|
+
if (Array.isArray(service.ports)) {
|
|
3999
|
+
for (const port of service.ports) {
|
|
4000
|
+
const formatted = formatPublishedPort(serviceName, port);
|
|
4001
|
+
if (formatted) {
|
|
4002
|
+
publishedPorts.push(formatted);
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
const networks2 = service.networks;
|
|
4007
|
+
if (Array.isArray(networks2)) {
|
|
4008
|
+
continue;
|
|
4009
|
+
}
|
|
4010
|
+
const networkMap = asRecord(networks2);
|
|
4011
|
+
const devnet2 = asRecord(networkMap?.devnet);
|
|
4012
|
+
for (const alias of asStringArray(devnet2?.aliases)) {
|
|
4013
|
+
aliases.add(alias);
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
3645
4016
|
return {
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
4017
|
+
aliases: Array.from(aliases.values()).sort(),
|
|
4018
|
+
publishedPorts,
|
|
4019
|
+
devnetExternal
|
|
3649
4020
|
};
|
|
3650
4021
|
} catch (error) {
|
|
3651
4022
|
const message = error instanceof Error ? error.message : String(error);
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
4023
|
+
return {
|
|
4024
|
+
aliases: [],
|
|
4025
|
+
publishedPorts: [],
|
|
4026
|
+
devnetExternal: false,
|
|
4027
|
+
parseError: message
|
|
4028
|
+
};
|
|
3656
4029
|
}
|
|
3657
4030
|
}
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
4031
|
+
function routedProxyApps(config) {
|
|
4032
|
+
if (!config) {
|
|
4033
|
+
return [];
|
|
4034
|
+
}
|
|
4035
|
+
return config.apps.filter(
|
|
4036
|
+
(app) => app.kind !== "dependency" && app.runtime === "proxy"
|
|
4037
|
+
);
|
|
4038
|
+
}
|
|
4039
|
+
function buildDevcontainerChecks(repoPath, config, workspace) {
|
|
4040
|
+
const devcontainerDir = import_node_path10.default.join(repoPath, ".devcontainer");
|
|
4041
|
+
if (!import_node_fs11.default.existsSync(devcontainerDir)) {
|
|
4042
|
+
return [];
|
|
4043
|
+
}
|
|
4044
|
+
const compose = inspectCompose(import_node_path10.default.join(devcontainerDir, "docker-compose.yml"), workspace);
|
|
4045
|
+
const checks = [];
|
|
4046
|
+
if (compose.parseError) {
|
|
4047
|
+
checks.push({
|
|
4048
|
+
id: "repo.devcontainer.aliases",
|
|
4049
|
+
level: "warn",
|
|
4050
|
+
summary: "Could not inspect devcontainer devnet aliases.",
|
|
4051
|
+
details: compose.parseError,
|
|
4052
|
+
suggestion: "Add .devcontainer/docker-compose.yml services on the external devnet network with aliases."
|
|
4053
|
+
});
|
|
4054
|
+
} else {
|
|
4055
|
+
const aliasesReady = compose.aliases.length > 0 && compose.devnetExternal;
|
|
4056
|
+
checks.push({
|
|
4057
|
+
id: "repo.devcontainer.aliases",
|
|
4058
|
+
level: aliasesReady ? "ok" : "warn",
|
|
4059
|
+
summary: aliasesReady ? `Found ${compose.aliases.length} devnet alias(es) on the external devnet network.` : compose.aliases.length > 0 ? "Devcontainer aliases exist, but top-level devnet is not marked external." : "No devnet aliases found in the devcontainer compose file.",
|
|
4060
|
+
details: compose.aliases.length > 0 ? `aliases=${compose.aliases.join(", ")}, devnetExternal=${String(compose.devnetExternal)}` : void 0,
|
|
4061
|
+
suggestion: aliasesReady ? void 0 : "Attach routable devcontainer services to an external devnet network and add stable aliases."
|
|
4062
|
+
});
|
|
4063
|
+
}
|
|
4064
|
+
checks.push({
|
|
4065
|
+
id: "repo.devcontainer.no-published-ports",
|
|
4066
|
+
level: compose.parseError ? "warn" : compose.publishedPorts.length === 0 ? "ok" : "error",
|
|
4067
|
+
summary: compose.parseError ? "Could not inspect devcontainer compose file for published host ports." : compose.publishedPorts.length === 0 ? "Devcontainer compose file does not publish host ports." : `Devcontainer compose file publishes ${compose.publishedPorts.length} host port(s).`,
|
|
4068
|
+
details: compose.publishedPorts.length > 0 ? compose.publishedPorts.join(", ") : void 0,
|
|
4069
|
+
suggestion: compose.parseError || compose.publishedPorts.length > 0 ? "Remove published ports and route services through devnet aliases with devrouter proxy apps." : void 0
|
|
4070
|
+
});
|
|
4071
|
+
if (!config) {
|
|
4072
|
+
checks.push({
|
|
4073
|
+
id: "repo.devcontainer.upstream-alias-match",
|
|
4074
|
+
level: "warn",
|
|
4075
|
+
summary: "Cannot compare devcontainer aliases to .devrouter.yml upstreams.",
|
|
4076
|
+
suggestion: "Add or fix .devrouter.yml proxy entries for the devcontainer services."
|
|
4077
|
+
});
|
|
4078
|
+
return checks;
|
|
4079
|
+
}
|
|
4080
|
+
const aliasSet = new Set(
|
|
4081
|
+
compose.aliases.flatMap((alias) => expandAliasCandidates(alias, workspace)).map(normalizeWorkspaceToken)
|
|
4082
|
+
);
|
|
4083
|
+
const proxyApps2 = routedProxyApps(config);
|
|
4084
|
+
const nonLoopbackUpstreams = proxyApps2.map((app) => ({
|
|
4085
|
+
app: app.name,
|
|
4086
|
+
host: normalizeWorkspaceToken(upstreamHost(app.upstream))
|
|
4087
|
+
})).filter((entry) => !isLoopbackHost(entry.host));
|
|
4088
|
+
const missing = nonLoopbackUpstreams.filter((entry) => !aliasSet.has(entry.host));
|
|
4089
|
+
checks.push({
|
|
4090
|
+
id: "repo.devcontainer.upstream-alias-match",
|
|
4091
|
+
level: proxyApps2.length > 0 && missing.length === 0 ? "ok" : "warn",
|
|
4092
|
+
summary: proxyApps2.length === 0 ? "No devrouter proxy apps found for the devcontainer." : missing.length === 0 ? "Devrouter proxy upstreams match devcontainer devnet aliases." : `${missing.length} devrouter proxy upstream(s) do not match devcontainer aliases.`,
|
|
4093
|
+
details: missing.length > 0 ? missing.map((entry) => `${entry.app}: ${entry.host}`).join(", ") : void 0,
|
|
4094
|
+
suggestion: proxyApps2.length === 0 || missing.length > 0 ? "Align .devrouter.yml proxy upstream hosts with .devcontainer/docker-compose.yml devnet aliases." : void 0
|
|
4095
|
+
});
|
|
4096
|
+
return checks;
|
|
4097
|
+
}
|
|
4098
|
+
var import_node_fs11, import_node_path10, import_yaml3;
|
|
4099
|
+
var init_devcontainer_diagnostics = __esm({
|
|
4100
|
+
"src/core/devcontainer-diagnostics.ts"() {
|
|
3661
4101
|
"use strict";
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
init_router();
|
|
3666
|
-
init_docker();
|
|
3667
|
-
init_host_routes();
|
|
3668
|
-
DEFAULT_TLS_CERT_HOSTS = ["localhost", "*.localhost"];
|
|
4102
|
+
import_node_fs11 = __toESM(require("fs"));
|
|
4103
|
+
import_node_path10 = __toESM(require("path"));
|
|
4104
|
+
import_yaml3 = __toESM(require("yaml"));
|
|
3669
4105
|
}
|
|
3670
4106
|
});
|
|
3671
4107
|
|
|
3672
4108
|
// src/core/doctor.ts
|
|
3673
|
-
function
|
|
4109
|
+
function asRecord2(value) {
|
|
3674
4110
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3675
4111
|
return void 0;
|
|
3676
4112
|
}
|
|
@@ -3695,7 +4131,7 @@ function parseComposeEnvironment(value) {
|
|
|
3695
4131
|
}
|
|
3696
4132
|
return env;
|
|
3697
4133
|
}
|
|
3698
|
-
const objectValue =
|
|
4134
|
+
const objectValue = asRecord2(value);
|
|
3699
4135
|
if (!objectValue) {
|
|
3700
4136
|
return env;
|
|
3701
4137
|
}
|
|
@@ -3728,15 +4164,15 @@ function inspectPostgresCredentials(repoPath, config) {
|
|
|
3728
4164
|
parseErrors.push(`${app.name}: ${composeFile} (${message})`);
|
|
3729
4165
|
continue;
|
|
3730
4166
|
}
|
|
3731
|
-
if (!
|
|
4167
|
+
if (!import_node_fs12.default.existsSync(absolutePath)) {
|
|
3732
4168
|
continue;
|
|
3733
4169
|
}
|
|
3734
4170
|
try {
|
|
3735
|
-
const raw =
|
|
3736
|
-
const parsed =
|
|
3737
|
-
const root =
|
|
3738
|
-
const services =
|
|
3739
|
-
const service =
|
|
4171
|
+
const raw = import_node_fs12.default.readFileSync(absolutePath, "utf-8");
|
|
4172
|
+
const parsed = import_yaml4.default.parse(raw);
|
|
4173
|
+
const root = asRecord2(parsed);
|
|
4174
|
+
const services = asRecord2(root?.services);
|
|
4175
|
+
const service = asRecord2(services?.[app.docker.service]);
|
|
3740
4176
|
if (!service) {
|
|
3741
4177
|
continue;
|
|
3742
4178
|
}
|
|
@@ -3835,17 +4271,6 @@ function inspectHostCommandPrecedence(config) {
|
|
|
3835
4271
|
}
|
|
3836
4272
|
return { inspectedCount, riskyApps };
|
|
3837
4273
|
}
|
|
3838
|
-
function isPidRunning2(pid) {
|
|
3839
|
-
if (!pid || pid <= 0) {
|
|
3840
|
-
return false;
|
|
3841
|
-
}
|
|
3842
|
-
try {
|
|
3843
|
-
process.kill(pid, 0);
|
|
3844
|
-
return true;
|
|
3845
|
-
} catch {
|
|
3846
|
-
return false;
|
|
3847
|
-
}
|
|
3848
|
-
}
|
|
3849
4274
|
function addCheck(checks, check) {
|
|
3850
4275
|
checks.push(check);
|
|
3851
4276
|
}
|
|
@@ -3878,6 +4303,8 @@ async function buildDoctorReport(options = {}) {
|
|
|
3878
4303
|
const fileLayout = getRouterFileLayout();
|
|
3879
4304
|
const resolvedRepoPath = resolveRepoPath(options.repo);
|
|
3880
4305
|
const explicitRepo = typeof options.repo === "string" && options.repo.trim().length > 0;
|
|
4306
|
+
let loadedConfig;
|
|
4307
|
+
let loadedWorkspace;
|
|
3881
4308
|
if (fileLayout.missing.length === 0) {
|
|
3882
4309
|
addCheck(checks, {
|
|
3883
4310
|
id: "global.router-files",
|
|
@@ -3893,6 +4320,9 @@ async function buildDoctorReport(options = {}) {
|
|
|
3893
4320
|
suggestion: "Run: dev up"
|
|
3894
4321
|
});
|
|
3895
4322
|
}
|
|
4323
|
+
for (const check of buildGlobalToolChecks(resolvedRepoPath)) {
|
|
4324
|
+
addCheck(checks, check);
|
|
4325
|
+
}
|
|
3896
4326
|
let statusNextSteps = [];
|
|
3897
4327
|
try {
|
|
3898
4328
|
const status = await collectRouterStatus(options.repo);
|
|
@@ -3995,7 +4425,10 @@ async function buildDoctorReport(options = {}) {
|
|
|
3995
4425
|
suggestion: "Run: dev app add --name <name> --host <name>.localhost --protocol http --runtime host"
|
|
3996
4426
|
});
|
|
3997
4427
|
}
|
|
3998
|
-
const
|
|
4428
|
+
const runtimeConfig = loadRuntimeConfig(repo.path);
|
|
4429
|
+
const config = runtimeConfig.config;
|
|
4430
|
+
loadedConfig = config;
|
|
4431
|
+
loadedWorkspace = runtimeConfig.workspace;
|
|
3999
4432
|
const appNames = new Set(config.apps.map((app) => app.name));
|
|
4000
4433
|
const missingDependencies = config.apps.flatMap(
|
|
4001
4434
|
(app) => app.dependencies.filter((dependency) => !appNames.has(dependency.app)).map((dependency) => `${app.name}->${dependency.app}`)
|
|
@@ -4009,8 +4442,8 @@ async function buildDoctorReport(options = {}) {
|
|
|
4009
4442
|
const missingComposeFiles = config.apps.filter((app) => app.runtime === "docker").flatMap((app) => app.docker.composeFiles.map((filePath) => ({
|
|
4010
4443
|
app: app.name,
|
|
4011
4444
|
filePath,
|
|
4012
|
-
absolutePath:
|
|
4013
|
-
}))).filter((entry) => !
|
|
4445
|
+
absolutePath: import_node_path11.default.resolve(repo.path, filePath)
|
|
4446
|
+
}))).filter((entry) => !import_node_fs12.default.existsSync(entry.absolutePath));
|
|
4014
4447
|
addCheck(checks, {
|
|
4015
4448
|
id: "repo.compose-files",
|
|
4016
4449
|
level: missingComposeFiles.length === 0 ? "ok" : "error",
|
|
@@ -4053,8 +4486,8 @@ async function buildDoctorReport(options = {}) {
|
|
|
4053
4486
|
const missingHostCwds = config.apps.filter((app) => app.runtime === "host").map((app) => ({
|
|
4054
4487
|
app: app.name,
|
|
4055
4488
|
cwd: app.hostRun.cwd,
|
|
4056
|
-
absolutePath:
|
|
4057
|
-
})).filter((entry) => !
|
|
4489
|
+
absolutePath: import_node_path11.default.resolve(repo.path, app.hostRun.cwd)
|
|
4490
|
+
})).filter((entry) => !import_node_fs12.default.existsSync(entry.absolutePath));
|
|
4058
4491
|
addCheck(checks, {
|
|
4059
4492
|
id: "repo.host-cwd",
|
|
4060
4493
|
level: missingHostCwds.length === 0 ? "ok" : "error",
|
|
@@ -4123,6 +4556,9 @@ async function buildDoctorReport(options = {}) {
|
|
|
4123
4556
|
details: message
|
|
4124
4557
|
});
|
|
4125
4558
|
}
|
|
4559
|
+
for (const check of buildDevcontainerChecks(resolvedRepoPath, loadedConfig, loadedWorkspace)) {
|
|
4560
|
+
addCheck(checks, check);
|
|
4561
|
+
}
|
|
4126
4562
|
try {
|
|
4127
4563
|
const tlsEnabled = isTLSEnabled();
|
|
4128
4564
|
const containers = await listContainers(true);
|
|
@@ -4144,29 +4580,20 @@ async function buildDoctorReport(options = {}) {
|
|
|
4144
4580
|
details: message
|
|
4145
4581
|
});
|
|
4146
4582
|
}
|
|
4147
|
-
const staleHostRoutes =
|
|
4148
|
-
|
|
4149
|
-
// Proxy routes have no backing process; they are never "stale".
|
|
4150
|
-
route.mode === "proxy" ? false : route.pid ? !isPidRunning2(route.pid) : true
|
|
4151
|
-
)
|
|
4152
|
-
);
|
|
4153
|
-
addCheck(checks, {
|
|
4154
|
-
id: "routes.host-state",
|
|
4155
|
-
level: staleHostRoutes.length === 0 ? "ok" : "warn",
|
|
4156
|
-
summary: staleHostRoutes.length === 0 ? "Host route state contains only running process entries." : `${staleHostRoutes.length} host route entr${staleHostRoutes.length === 1 ? "y is" : "ies are"} stale (process not running).`,
|
|
4157
|
-
suggestion: staleHostRoutes.length === 0 ? void 0 : "Re-run the affected host app(s): dev app run <name> --repo <path>"
|
|
4158
|
-
});
|
|
4159
|
-
const evictedCount = evictStaleHostRoutes();
|
|
4583
|
+
const staleHostRoutes = findStaleProcessRoutes();
|
|
4584
|
+
const staleCount = staleHostRoutes.length;
|
|
4160
4585
|
addCheck(checks, {
|
|
4161
4586
|
id: "routes.stale-host-routes",
|
|
4162
|
-
level:
|
|
4163
|
-
summary:
|
|
4587
|
+
level: staleCount === 0 ? "ok" : "warn",
|
|
4588
|
+
summary: staleCount === 0 ? "No stale host route entries detected." : `${staleCount} stale host route entr${staleCount === 1 ? "y" : "ies"} detected (dead PID).`,
|
|
4589
|
+
suggestion: staleCount === 0 ? void 0 : "Re-run the affected host app(s) or remove routes with: dev app rm <name> --repo <path> --keep-config"
|
|
4164
4590
|
});
|
|
4165
|
-
const
|
|
4591
|
+
const orphanedWorkspaceRoutes = findOrphanedWorkspaceProxyRoutes();
|
|
4166
4592
|
addCheck(checks, {
|
|
4167
4593
|
id: "routes.orphaned-workspace-routes",
|
|
4168
|
-
level:
|
|
4169
|
-
summary:
|
|
4594
|
+
level: orphanedWorkspaceRoutes.length === 0 ? "ok" : "warn",
|
|
4595
|
+
summary: orphanedWorkspaceRoutes.length === 0 ? "No orphaned workspace proxy routes detected." : `${orphanedWorkspaceRoutes.length} orphaned workspace proxy route entr${orphanedWorkspaceRoutes.length === 1 ? "y" : "ies"} detected (worktree removed without 'dev workspace down').`,
|
|
4596
|
+
suggestion: orphanedWorkspaceRoutes.length === 0 ? void 0 : "Run: dev workspace down <workspace>"
|
|
4170
4597
|
});
|
|
4171
4598
|
const summary = collectSummary(checks);
|
|
4172
4599
|
const nextSteps = collectNextSteps(checks, statusNextSteps);
|
|
@@ -4178,22 +4605,24 @@ async function buildDoctorReport(options = {}) {
|
|
|
4178
4605
|
nextSteps
|
|
4179
4606
|
};
|
|
4180
4607
|
}
|
|
4181
|
-
var
|
|
4608
|
+
var import_node_fs12, import_node_path11, import_yaml4, POSTGRES_DEFAULTS;
|
|
4182
4609
|
var init_doctor = __esm({
|
|
4183
4610
|
"src/core/doctor.ts"() {
|
|
4184
4611
|
"use strict";
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4612
|
+
import_node_fs12 = __toESM(require("fs"));
|
|
4613
|
+
import_node_path11 = __toESM(require("path"));
|
|
4614
|
+
import_yaml4 = __toESM(require("yaml"));
|
|
4188
4615
|
init_docker();
|
|
4189
4616
|
init_host_routes();
|
|
4190
|
-
init_concurrency();
|
|
4191
4617
|
init_repo_config();
|
|
4192
4618
|
init_router();
|
|
4193
4619
|
init_status();
|
|
4194
4620
|
init_routes();
|
|
4195
4621
|
init_paths();
|
|
4196
4622
|
init_tls();
|
|
4623
|
+
init_route_state();
|
|
4624
|
+
init_tool_diagnostics();
|
|
4625
|
+
init_devcontainer_diagnostics();
|
|
4197
4626
|
POSTGRES_DEFAULTS = {
|
|
4198
4627
|
POSTGRES_USER: "prisma",
|
|
4199
4628
|
POSTGRES_PASSWORD: "prisma",
|
|
@@ -4202,6 +4631,305 @@ var init_doctor = __esm({
|
|
|
4202
4631
|
}
|
|
4203
4632
|
});
|
|
4204
4633
|
|
|
4634
|
+
// src/core/setup.ts
|
|
4635
|
+
function action(status, entry) {
|
|
4636
|
+
return { status, ...entry };
|
|
4637
|
+
}
|
|
4638
|
+
function actionStatusCounts(actions) {
|
|
4639
|
+
return actions.reduce(
|
|
4640
|
+
(acc, entry) => {
|
|
4641
|
+
acc[entry.status] += 1;
|
|
4642
|
+
return acc;
|
|
4643
|
+
},
|
|
4644
|
+
{ performed: 0, skipped: 0, failed: 0 }
|
|
4645
|
+
);
|
|
4646
|
+
}
|
|
4647
|
+
function collectNextSteps2(report, doctorNextSteps) {
|
|
4648
|
+
const steps = /* @__PURE__ */ new Set();
|
|
4649
|
+
for (const entry of report.actions) {
|
|
4650
|
+
if (entry.status === "failed" || entry.status === "skipped") {
|
|
4651
|
+
if (entry.suggestion) {
|
|
4652
|
+
steps.add(entry.suggestion);
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
for (const check of report.checks) {
|
|
4657
|
+
if (check.level !== "ok" && check.suggestion) {
|
|
4658
|
+
steps.add(check.suggestion);
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
4661
|
+
for (const step of doctorNextSteps) {
|
|
4662
|
+
steps.add(step);
|
|
4663
|
+
}
|
|
4664
|
+
return Array.from(steps.values());
|
|
4665
|
+
}
|
|
4666
|
+
async function runSetup(options = {}) {
|
|
4667
|
+
const repoPath = resolveRepoPath(options.repo);
|
|
4668
|
+
const actions = [];
|
|
4669
|
+
if (!options.yes) {
|
|
4670
|
+
actions.push(action("failed", {
|
|
4671
|
+
id: "setup.confirmation",
|
|
4672
|
+
summary: "Setup requires --yes before mutating devrouter-owned machine state.",
|
|
4673
|
+
suggestion: "Run: dev setup --yes"
|
|
4674
|
+
}));
|
|
4675
|
+
const doctor2 = await buildDoctorReport({ repo: options.repo });
|
|
4676
|
+
const partialReport2 = { actions, checks: doctor2.checks };
|
|
4677
|
+
return {
|
|
4678
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4679
|
+
repoPath,
|
|
4680
|
+
actions,
|
|
4681
|
+
checks: doctor2.checks,
|
|
4682
|
+
summary: {
|
|
4683
|
+
actions: actionStatusCounts(actions),
|
|
4684
|
+
checks: doctor2.summary
|
|
4685
|
+
},
|
|
4686
|
+
nextSteps: collectNextSteps2(partialReport2, doctor2.nextSteps)
|
|
4687
|
+
};
|
|
4688
|
+
}
|
|
4689
|
+
try {
|
|
4690
|
+
const missingBefore = getRouterFileLayout().missing;
|
|
4691
|
+
ensureRouterFiles();
|
|
4692
|
+
actions.push(action(missingBefore.length === 0 ? "skipped" : "performed", {
|
|
4693
|
+
id: "global.router-files",
|
|
4694
|
+
summary: missingBefore.length === 0 ? "Global router files were already present." : `Created missing global router file(s): ${missingBefore.join(", ")}`
|
|
4695
|
+
}));
|
|
4696
|
+
} catch (error) {
|
|
4697
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4698
|
+
actions.push(action("failed", {
|
|
4699
|
+
id: "global.router-files",
|
|
4700
|
+
summary: "Failed to ensure global router files.",
|
|
4701
|
+
details: message,
|
|
4702
|
+
suggestion: "Check write access to ~/.config/devrouter, then run: dev setup --yes"
|
|
4703
|
+
}));
|
|
4704
|
+
}
|
|
4705
|
+
try {
|
|
4706
|
+
const existed = await networkExists(DEVNET_NAME);
|
|
4707
|
+
await ensureNetwork(DEVNET_NAME);
|
|
4708
|
+
actions.push(action(existed ? "skipped" : "performed", {
|
|
4709
|
+
id: "global.devnet",
|
|
4710
|
+
summary: existed ? "Shared Docker network devnet already exists." : "Created shared Docker network devnet."
|
|
4711
|
+
}));
|
|
4712
|
+
} catch (error) {
|
|
4713
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4714
|
+
actions.push(action("failed", {
|
|
4715
|
+
id: "global.devnet",
|
|
4716
|
+
summary: "Failed to ensure shared Docker network devnet.",
|
|
4717
|
+
details: message,
|
|
4718
|
+
suggestion: "Start Docker and verify Docker context, then run: dev setup --yes"
|
|
4719
|
+
}));
|
|
4720
|
+
}
|
|
4721
|
+
try {
|
|
4722
|
+
const wasRunning = await isContainerRunning(ROUTER_CONTAINER_NAME);
|
|
4723
|
+
startRouterStack();
|
|
4724
|
+
actions.push(action(wasRunning ? "skipped" : "performed", {
|
|
4725
|
+
id: "global.router-stack",
|
|
4726
|
+
summary: wasRunning ? "Shared Traefik router was already running." : "Started shared Traefik router."
|
|
4727
|
+
}));
|
|
4728
|
+
} catch (error) {
|
|
4729
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4730
|
+
actions.push(action("failed", {
|
|
4731
|
+
id: "global.router-stack",
|
|
4732
|
+
summary: "Failed to start shared Traefik router.",
|
|
4733
|
+
details: message,
|
|
4734
|
+
suggestion: "Resolve Docker/port conflicts on 80, 443, or 5432, then run: dev setup --yes"
|
|
4735
|
+
}));
|
|
4736
|
+
}
|
|
4737
|
+
const mkcert = runTool("mkcert", ["-version"]);
|
|
4738
|
+
if (!mkcert.ok) {
|
|
4739
|
+
actions.push(action("skipped", {
|
|
4740
|
+
id: "global.tls",
|
|
4741
|
+
summary: "Skipped TLS setup because mkcert is not installed.",
|
|
4742
|
+
details: mkcert.error,
|
|
4743
|
+
suggestion: "Install mkcert, then run: dev setup --yes"
|
|
4744
|
+
}));
|
|
4745
|
+
} else {
|
|
4746
|
+
try {
|
|
4747
|
+
const tls = await installTLS();
|
|
4748
|
+
actions.push(action(tls.alreadyEnabled ? "skipped" : "performed", {
|
|
4749
|
+
id: "global.tls",
|
|
4750
|
+
summary: tls.alreadyEnabled ? "TLS was already enabled; certificates were refreshed." : "Installed local TLS certificates and enabled TLS routing.",
|
|
4751
|
+
details: `hosts=${tls.hosts.join(", ")}`
|
|
4752
|
+
}));
|
|
4753
|
+
} catch (error) {
|
|
4754
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4755
|
+
actions.push(action("failed", {
|
|
4756
|
+
id: "global.tls",
|
|
4757
|
+
summary: "Failed to install local TLS certificates.",
|
|
4758
|
+
details: message,
|
|
4759
|
+
suggestion: "Run: dev tls install"
|
|
4760
|
+
}));
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
const doctor = await buildDoctorReport({ repo: options.repo });
|
|
4764
|
+
const partialReport = { actions, checks: doctor.checks };
|
|
4765
|
+
return {
|
|
4766
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4767
|
+
repoPath,
|
|
4768
|
+
actions,
|
|
4769
|
+
checks: doctor.checks,
|
|
4770
|
+
summary: {
|
|
4771
|
+
actions: actionStatusCounts(actions),
|
|
4772
|
+
checks: doctor.summary
|
|
4773
|
+
},
|
|
4774
|
+
nextSteps: collectNextSteps2(partialReport, doctor.nextSteps)
|
|
4775
|
+
};
|
|
4776
|
+
}
|
|
4777
|
+
var init_setup = __esm({
|
|
4778
|
+
"src/core/setup.ts"() {
|
|
4779
|
+
"use strict";
|
|
4780
|
+
init_docker();
|
|
4781
|
+
init_router();
|
|
4782
|
+
init_tls();
|
|
4783
|
+
init_doctor();
|
|
4784
|
+
init_tool_diagnostics();
|
|
4785
|
+
init_repo_config();
|
|
4786
|
+
}
|
|
4787
|
+
});
|
|
4788
|
+
|
|
4789
|
+
// src/commands/setup.ts
|
|
4790
|
+
var setup_exports = {};
|
|
4791
|
+
__export(setup_exports, {
|
|
4792
|
+
runSetupCommand: () => runSetupCommand
|
|
4793
|
+
});
|
|
4794
|
+
async function runSetupCommand(options) {
|
|
4795
|
+
const report = await runSetup({ repo: options.repo, yes: Boolean(options.yes) });
|
|
4796
|
+
if (options.json) {
|
|
4797
|
+
printJSON(report);
|
|
4798
|
+
} else {
|
|
4799
|
+
printSetupReport(report);
|
|
4800
|
+
}
|
|
4801
|
+
if (report.summary.actions.failed > 0 || report.summary.checks.error > 0) {
|
|
4802
|
+
process.exitCode = 1;
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4805
|
+
var init_setup2 = __esm({
|
|
4806
|
+
"src/commands/setup.ts"() {
|
|
4807
|
+
"use strict";
|
|
4808
|
+
init_setup();
|
|
4809
|
+
init_output();
|
|
4810
|
+
}
|
|
4811
|
+
});
|
|
4812
|
+
|
|
4813
|
+
// src/util/ports.ts
|
|
4814
|
+
function findPortListeners(port) {
|
|
4815
|
+
const result = (0, import_node_child_process6.spawnSync)("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
|
|
4816
|
+
encoding: "utf-8"
|
|
4817
|
+
});
|
|
4818
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
4819
|
+
return [];
|
|
4820
|
+
}
|
|
4821
|
+
const lines = result.stdout.trim().split(/\r?\n/);
|
|
4822
|
+
if (lines.length <= 1) {
|
|
4823
|
+
return [];
|
|
4824
|
+
}
|
|
4825
|
+
return lines.slice(1).map((line) => {
|
|
4826
|
+
const parts = line.trim().split(/\s+/);
|
|
4827
|
+
const command = parts[0] ?? "?";
|
|
4828
|
+
const pid = parts[1] ?? "?";
|
|
4829
|
+
const user = parts[2] ?? "?";
|
|
4830
|
+
const address = parts.slice(-2).join(" ") || "?";
|
|
4831
|
+
return {
|
|
4832
|
+
port,
|
|
4833
|
+
command,
|
|
4834
|
+
pid,
|
|
4835
|
+
user,
|
|
4836
|
+
address
|
|
4837
|
+
};
|
|
4838
|
+
});
|
|
4839
|
+
}
|
|
4840
|
+
var import_node_child_process6;
|
|
4841
|
+
var init_ports = __esm({
|
|
4842
|
+
"src/util/ports.ts"() {
|
|
4843
|
+
"use strict";
|
|
4844
|
+
import_node_child_process6 = require("child_process");
|
|
4845
|
+
}
|
|
4846
|
+
});
|
|
4847
|
+
|
|
4848
|
+
// src/commands/up.ts
|
|
4849
|
+
var up_exports = {};
|
|
4850
|
+
__export(up_exports, {
|
|
4851
|
+
runUpCommand: () => runUpCommand
|
|
4852
|
+
});
|
|
4853
|
+
function buildPortConflictMessage() {
|
|
4854
|
+
const listeners = [...findPortListeners(80), ...findPortListeners(443), ...findPortListeners(5432)];
|
|
4855
|
+
if (listeners.length === 0) {
|
|
4856
|
+
return "";
|
|
4857
|
+
}
|
|
4858
|
+
const details = listeners.map((listener) => {
|
|
4859
|
+
return `- port ${listener.port}: ${listener.command} (pid ${listener.pid}, user ${listener.user}, ${listener.address})`;
|
|
4860
|
+
}).join("\n");
|
|
4861
|
+
return `Cannot start devrouter because host ports 80/443/5432 are already in use:
|
|
4862
|
+
${details}
|
|
4863
|
+
|
|
4864
|
+
Mitigation:
|
|
4865
|
+
1) Stop the conflicting process/container
|
|
4866
|
+
2) Re-run: dev up
|
|
4867
|
+
|
|
4868
|
+
Debug commands:
|
|
4869
|
+
- lsof -nP -iTCP:80 -sTCP:LISTEN
|
|
4870
|
+
- lsof -nP -iTCP:443 -sTCP:LISTEN
|
|
4871
|
+
- lsof -nP -iTCP:5432 -sTCP:LISTEN`;
|
|
4872
|
+
}
|
|
4873
|
+
async function runUpCommand() {
|
|
4874
|
+
await ensureNetwork(DEVNET_NAME);
|
|
4875
|
+
ensureRouterFiles();
|
|
4876
|
+
const routerRunning = await isContainerRunning(ROUTER_CONTAINER_NAME);
|
|
4877
|
+
if (!routerRunning) {
|
|
4878
|
+
const message = buildPortConflictMessage();
|
|
4879
|
+
if (message) {
|
|
4880
|
+
throw new Error(message);
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
startRouterStack();
|
|
4884
|
+
process.stdout.write("devrouter is up.\n");
|
|
4885
|
+
}
|
|
4886
|
+
var init_up = __esm({
|
|
4887
|
+
"src/commands/up.ts"() {
|
|
4888
|
+
"use strict";
|
|
4889
|
+
init_docker();
|
|
4890
|
+
init_router();
|
|
4891
|
+
init_ports();
|
|
4892
|
+
}
|
|
4893
|
+
});
|
|
4894
|
+
|
|
4895
|
+
// src/commands/down.ts
|
|
4896
|
+
var down_exports = {};
|
|
4897
|
+
__export(down_exports, {
|
|
4898
|
+
runDownCommand: () => runDownCommand
|
|
4899
|
+
});
|
|
4900
|
+
async function runDownCommand() {
|
|
4901
|
+
stopRouterStack();
|
|
4902
|
+
clearActiveTcpProtocols();
|
|
4903
|
+
process.stdout.write("devrouter is down.\n");
|
|
4904
|
+
}
|
|
4905
|
+
var init_down = __esm({
|
|
4906
|
+
"src/commands/down.ts"() {
|
|
4907
|
+
"use strict";
|
|
4908
|
+
init_router();
|
|
4909
|
+
}
|
|
4910
|
+
});
|
|
4911
|
+
|
|
4912
|
+
// src/commands/status.ts
|
|
4913
|
+
var status_exports = {};
|
|
4914
|
+
__export(status_exports, {
|
|
4915
|
+
runStatusCommand: () => runStatusCommand
|
|
4916
|
+
});
|
|
4917
|
+
async function runStatusCommand(options) {
|
|
4918
|
+
const status = await collectRouterStatus(options.repo);
|
|
4919
|
+
if (options.json) {
|
|
4920
|
+
printJSON(status);
|
|
4921
|
+
return;
|
|
4922
|
+
}
|
|
4923
|
+
printStatus(status);
|
|
4924
|
+
}
|
|
4925
|
+
var init_status2 = __esm({
|
|
4926
|
+
"src/commands/status.ts"() {
|
|
4927
|
+
"use strict";
|
|
4928
|
+
init_output();
|
|
4929
|
+
init_status();
|
|
4930
|
+
}
|
|
4931
|
+
});
|
|
4932
|
+
|
|
4205
4933
|
// src/commands/doctor.ts
|
|
4206
4934
|
var doctor_exports = {};
|
|
4207
4935
|
__export(doctor_exports, {
|
|
@@ -4211,9 +4939,12 @@ async function runDoctorCommand(options) {
|
|
|
4211
4939
|
const report = await buildDoctorReport({ repo: options.repo });
|
|
4212
4940
|
if (options.json) {
|
|
4213
4941
|
printJSON(report);
|
|
4214
|
-
|
|
4942
|
+
} else {
|
|
4943
|
+
printDoctorReport(report);
|
|
4944
|
+
}
|
|
4945
|
+
if (report.summary.error > 0) {
|
|
4946
|
+
process.exitCode = 1;
|
|
4215
4947
|
}
|
|
4216
|
-
printDoctorReport(report);
|
|
4217
4948
|
}
|
|
4218
4949
|
var init_doctor2 = __esm({
|
|
4219
4950
|
"src/commands/doctor.ts"() {
|
|
@@ -4302,7 +5033,7 @@ async function runOpenCommand(name) {
|
|
|
4302
5033
|
);
|
|
4303
5034
|
return;
|
|
4304
5035
|
}
|
|
4305
|
-
const result = (0,
|
|
5036
|
+
const result = (0, import_node_child_process7.spawnSync)("open", [url], { encoding: "utf-8" });
|
|
4306
5037
|
if (result.status !== 0) {
|
|
4307
5038
|
const details = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
4308
5039
|
throw new Error(`Unable to open '${url}': ${details || "unknown error"}`);
|
|
@@ -4310,11 +5041,11 @@ async function runOpenCommand(name) {
|
|
|
4310
5041
|
process.stdout.write(`Opened ${url}
|
|
4311
5042
|
`);
|
|
4312
5043
|
}
|
|
4313
|
-
var
|
|
5044
|
+
var import_node_child_process7;
|
|
4314
5045
|
var init_open = __esm({
|
|
4315
5046
|
"src/commands/open.ts"() {
|
|
4316
5047
|
"use strict";
|
|
4317
|
-
|
|
5048
|
+
import_node_child_process7 = require("child_process");
|
|
4318
5049
|
init_docker();
|
|
4319
5050
|
init_host_routes();
|
|
4320
5051
|
init_repo_config();
|
|
@@ -4337,7 +5068,7 @@ async function runLogsCommand(options) {
|
|
|
4337
5068
|
const args = ["logs", "--tail", tail, ROUTER_CONTAINER_NAME];
|
|
4338
5069
|
if (options.follow) {
|
|
4339
5070
|
args.splice(1, 0, "-f");
|
|
4340
|
-
const child = (0,
|
|
5071
|
+
const child = (0, import_node_child_process8.spawn)("docker", args, { stdio: "inherit" });
|
|
4341
5072
|
const onSignal = () => {
|
|
4342
5073
|
child.kill("SIGTERM");
|
|
4343
5074
|
};
|
|
@@ -4351,17 +5082,17 @@ async function runLogsCommand(options) {
|
|
|
4351
5082
|
});
|
|
4352
5083
|
});
|
|
4353
5084
|
} else {
|
|
4354
|
-
const result = (0,
|
|
5085
|
+
const result = (0, import_node_child_process8.spawnSync)("docker", args, { stdio: "inherit" });
|
|
4355
5086
|
if (result.status !== 0) {
|
|
4356
5087
|
throw new Error("Failed to retrieve router logs.");
|
|
4357
5088
|
}
|
|
4358
5089
|
}
|
|
4359
5090
|
}
|
|
4360
|
-
var
|
|
5091
|
+
var import_node_child_process8;
|
|
4361
5092
|
var init_logs = __esm({
|
|
4362
5093
|
"src/commands/logs.ts"() {
|
|
4363
5094
|
"use strict";
|
|
4364
|
-
|
|
5095
|
+
import_node_child_process8 = require("child_process");
|
|
4365
5096
|
init_router();
|
|
4366
5097
|
init_docker();
|
|
4367
5098
|
}
|
|
@@ -4384,65 +5115,1327 @@ async function runRepoInitCommand(options) {
|
|
|
4384
5115
|
`);
|
|
4385
5116
|
process.stdout.write("Next: dev app add --name <name> --host <host.localhost> --protocol <http|tcp> --runtime <host|docker>\n");
|
|
4386
5117
|
}
|
|
4387
|
-
var init_repo_init = __esm({
|
|
4388
|
-
"src/commands/repo-init.ts"() {
|
|
5118
|
+
var init_repo_init = __esm({
|
|
5119
|
+
"src/commands/repo-init.ts"() {
|
|
5120
|
+
"use strict";
|
|
5121
|
+
init_repo_config();
|
|
5122
|
+
}
|
|
5123
|
+
});
|
|
5124
|
+
|
|
5125
|
+
// src/core/repo-inspect.ts
|
|
5126
|
+
function asRecord3(value) {
|
|
5127
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5128
|
+
return void 0;
|
|
5129
|
+
}
|
|
5130
|
+
return value;
|
|
5131
|
+
}
|
|
5132
|
+
function readJson(filePath) {
|
|
5133
|
+
if (!import_node_fs13.default.existsSync(filePath)) {
|
|
5134
|
+
return void 0;
|
|
5135
|
+
}
|
|
5136
|
+
try {
|
|
5137
|
+
return JSON.parse(import_node_fs13.default.readFileSync(filePath, "utf-8"));
|
|
5138
|
+
} catch {
|
|
5139
|
+
return void 0;
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
function relative(repoPath, filePath) {
|
|
5143
|
+
return import_node_path12.default.relative(repoPath, filePath) || ".";
|
|
5144
|
+
}
|
|
5145
|
+
function redactEnvAssignments(value) {
|
|
5146
|
+
return value.replace(
|
|
5147
|
+
/(^|\s)([A-Za-z_][A-Za-z0-9_]*)=("[^"]*"|'[^']*'|[^\s]+)/g,
|
|
5148
|
+
"$1$2=<redacted>"
|
|
5149
|
+
);
|
|
5150
|
+
}
|
|
5151
|
+
function sanitizeDiagnosticText(value) {
|
|
5152
|
+
return redactEnvAssignments(value).replace(/\bvalue\s+'[^']+'/g, "value '<redacted>'").replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s'"]+/gi, "<redacted-url>");
|
|
5153
|
+
}
|
|
5154
|
+
function parsePackageManager2(value) {
|
|
5155
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
5156
|
+
return void 0;
|
|
5157
|
+
}
|
|
5158
|
+
const trimmed = value.trim();
|
|
5159
|
+
const separator = trimmed.lastIndexOf("@");
|
|
5160
|
+
if (separator <= 0) {
|
|
5161
|
+
return { name: trimmed };
|
|
5162
|
+
}
|
|
5163
|
+
return {
|
|
5164
|
+
name: trimmed.slice(0, separator),
|
|
5165
|
+
version: trimmed.slice(separator + 1)
|
|
5166
|
+
};
|
|
5167
|
+
}
|
|
5168
|
+
function inspectPackageManager(repoPath, pkg) {
|
|
5169
|
+
const packageManager = parsePackageManager2(pkg?.packageManager);
|
|
5170
|
+
if (packageManager) {
|
|
5171
|
+
return {
|
|
5172
|
+
...packageManager,
|
|
5173
|
+
source: "package.json:packageManager"
|
|
5174
|
+
};
|
|
5175
|
+
}
|
|
5176
|
+
const lockfiles = [
|
|
5177
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
5178
|
+
["package-lock.json", "npm"],
|
|
5179
|
+
["yarn.lock", "yarn"],
|
|
5180
|
+
["bun.lockb", "bun"],
|
|
5181
|
+
["bun.lock", "bun"]
|
|
5182
|
+
];
|
|
5183
|
+
for (const [fileName, name] of lockfiles) {
|
|
5184
|
+
if (import_node_fs13.default.existsSync(import_node_path12.default.join(repoPath, fileName))) {
|
|
5185
|
+
return { name, source: fileName };
|
|
5186
|
+
}
|
|
5187
|
+
}
|
|
5188
|
+
return void 0;
|
|
5189
|
+
}
|
|
5190
|
+
function inspectNode(repoPath, pkg) {
|
|
5191
|
+
const volta = asRecord3(pkg?.volta);
|
|
5192
|
+
if (typeof volta?.node === "string") {
|
|
5193
|
+
return { version: volta.node, source: "package.json:volta.node" };
|
|
5194
|
+
}
|
|
5195
|
+
const engines = asRecord3(pkg?.engines);
|
|
5196
|
+
if (typeof engines?.node === "string") {
|
|
5197
|
+
return { version: engines.node, source: "package.json:engines.node" };
|
|
5198
|
+
}
|
|
5199
|
+
const nvmrc = import_node_path12.default.join(repoPath, ".nvmrc");
|
|
5200
|
+
if (import_node_fs13.default.existsSync(nvmrc)) {
|
|
5201
|
+
const version = import_node_fs13.default.readFileSync(nvmrc, "utf-8").trim();
|
|
5202
|
+
return { version, source: ".nvmrc" };
|
|
5203
|
+
}
|
|
5204
|
+
return void 0;
|
|
5205
|
+
}
|
|
5206
|
+
function inspectScripts(pkg) {
|
|
5207
|
+
const scripts = asRecord3(pkg?.scripts);
|
|
5208
|
+
if (!scripts) {
|
|
5209
|
+
return [];
|
|
5210
|
+
}
|
|
5211
|
+
return Object.entries(scripts).filter((entry) => typeof entry[1] === "string").map(([name, command]) => ({
|
|
5212
|
+
name,
|
|
5213
|
+
command: redactEnvAssignments(command),
|
|
5214
|
+
evidence: [`package.json:scripts.${name}`]
|
|
5215
|
+
}));
|
|
5216
|
+
}
|
|
5217
|
+
function inferPort(command) {
|
|
5218
|
+
const patterns = [
|
|
5219
|
+
/\bPORT=(\d{2,5})\b/,
|
|
5220
|
+
/--port[=\s]+(\d{2,5})\b/,
|
|
5221
|
+
/(?:^|\s)-p\s+(\d{2,5})\b/
|
|
5222
|
+
];
|
|
5223
|
+
for (const pattern of patterns) {
|
|
5224
|
+
const match = command.match(pattern);
|
|
5225
|
+
if (match) {
|
|
5226
|
+
return { port: Number(match[1]), confidence: "high", evidence: [`script command: ${match[0]}`] };
|
|
5227
|
+
}
|
|
5228
|
+
}
|
|
5229
|
+
const frameworkDefaults = [
|
|
5230
|
+
{ pattern: /\bnext\b/, port: 3e3, label: "next default port" },
|
|
5231
|
+
{ pattern: /\bvite\b/, port: 5173, label: "vite default port" },
|
|
5232
|
+
{ pattern: /\bastro\b/, port: 4321, label: "astro default port" },
|
|
5233
|
+
{ pattern: /\bnuxt\b/, port: 3e3, label: "nuxt default port" }
|
|
5234
|
+
];
|
|
5235
|
+
for (const entry of frameworkDefaults) {
|
|
5236
|
+
if (entry.pattern.test(command)) {
|
|
5237
|
+
return { port: entry.port, confidence: "medium", evidence: [entry.label] };
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5240
|
+
return { confidence: "low", evidence: ["No explicit port detected"] };
|
|
5241
|
+
}
|
|
5242
|
+
function inspectAppCandidates(scripts) {
|
|
5243
|
+
const candidates = scripts.filter(
|
|
5244
|
+
(script) => /(^dev$|:dev$|dev:|^start$|web|app|serve)/.test(script.name)
|
|
5245
|
+
);
|
|
5246
|
+
return candidates.map((script) => {
|
|
5247
|
+
const port = inferPort(script.command);
|
|
5248
|
+
return {
|
|
5249
|
+
name: script.name === "dev" || script.name === "start" ? "app" : script.name.replace(/[:_]/g, "-"),
|
|
5250
|
+
port: port.port,
|
|
5251
|
+
confidence: port.confidence,
|
|
5252
|
+
evidence: [...script.evidence, ...port.evidence]
|
|
5253
|
+
};
|
|
5254
|
+
});
|
|
5255
|
+
}
|
|
5256
|
+
function configuredComposeFiles(repoPath) {
|
|
5257
|
+
try {
|
|
5258
|
+
const config = loadRepoConfig(repoPath);
|
|
5259
|
+
const files = config.apps.filter((app) => app.runtime === "docker").flatMap((app) => app.docker.composeFiles).filter((fileName) => !import_node_path12.default.isAbsolute(fileName) && !import_node_path12.default.normalize(fileName).startsWith(".."));
|
|
5260
|
+
return Array.from(new Set(files));
|
|
5261
|
+
} catch {
|
|
5262
|
+
return [];
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5265
|
+
function composeFiles(repoPath) {
|
|
5266
|
+
const candidates = [
|
|
5267
|
+
"docker-compose.yml",
|
|
5268
|
+
"docker-compose.yaml",
|
|
5269
|
+
"compose.yml",
|
|
5270
|
+
"compose.yaml",
|
|
5271
|
+
".devcontainer/docker-compose.yml",
|
|
5272
|
+
".devcontainer/docker-compose.yaml",
|
|
5273
|
+
...configuredComposeFiles(repoPath)
|
|
5274
|
+
];
|
|
5275
|
+
return Array.from(new Set(candidates)).filter((fileName) => import_node_fs13.default.existsSync(import_node_path12.default.join(repoPath, fileName)));
|
|
5276
|
+
}
|
|
5277
|
+
function stringArray(value) {
|
|
5278
|
+
if (!Array.isArray(value)) {
|
|
5279
|
+
return [];
|
|
5280
|
+
}
|
|
5281
|
+
return value.map((entry) => typeof entry === "string" ? entry : JSON.stringify(entry));
|
|
5282
|
+
}
|
|
5283
|
+
function envNames(value) {
|
|
5284
|
+
if (Array.isArray(value)) {
|
|
5285
|
+
return value.filter((entry) => typeof entry === "string").map((entry) => entry.split("=")[0]?.trim()).filter((entry) => Boolean(entry));
|
|
5286
|
+
}
|
|
5287
|
+
const record = asRecord3(value);
|
|
5288
|
+
return record ? Object.keys(record).sort() : [];
|
|
5289
|
+
}
|
|
5290
|
+
function inferServiceKind(name, image) {
|
|
5291
|
+
const source = `${name} ${image ?? ""}`.toLowerCase();
|
|
5292
|
+
const rules = [
|
|
5293
|
+
["postgres", "postgres"],
|
|
5294
|
+
["redis", "redis"],
|
|
5295
|
+
["mysql", "mysql"],
|
|
5296
|
+
["mariadb", "mariadb"],
|
|
5297
|
+
["mock-oauth2", "oidc"],
|
|
5298
|
+
["oidc", "oidc"],
|
|
5299
|
+
["mailhog", "mail"]
|
|
5300
|
+
];
|
|
5301
|
+
for (const [needle, kind] of rules) {
|
|
5302
|
+
if (source.includes(needle)) {
|
|
5303
|
+
return { kind, confidence: "high" };
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
return { kind: "unknown", confidence: "low" };
|
|
5307
|
+
}
|
|
5308
|
+
function inspectServices(repoPath) {
|
|
5309
|
+
const services = [];
|
|
5310
|
+
for (const fileName of composeFiles(repoPath)) {
|
|
5311
|
+
try {
|
|
5312
|
+
const parsed = import_yaml5.default.parse(import_node_fs13.default.readFileSync(import_node_path12.default.join(repoPath, fileName), "utf-8"));
|
|
5313
|
+
const serviceMap = asRecord3(asRecord3(parsed)?.services);
|
|
5314
|
+
for (const [name, value] of Object.entries(serviceMap ?? {})) {
|
|
5315
|
+
const service = asRecord3(value);
|
|
5316
|
+
if (!service) {
|
|
5317
|
+
continue;
|
|
5318
|
+
}
|
|
5319
|
+
const image = typeof service.image === "string" ? service.image : void 0;
|
|
5320
|
+
const kind = inferServiceKind(name, image);
|
|
5321
|
+
services.push({
|
|
5322
|
+
name,
|
|
5323
|
+
kind: kind.kind,
|
|
5324
|
+
source: fileName,
|
|
5325
|
+
image,
|
|
5326
|
+
ports: stringArray(service.ports),
|
|
5327
|
+
hasHealthcheck: service.healthcheck !== void 0,
|
|
5328
|
+
envNames: envNames(service.environment),
|
|
5329
|
+
confidence: kind.confidence
|
|
5330
|
+
});
|
|
5331
|
+
}
|
|
5332
|
+
} catch {
|
|
5333
|
+
services.push({
|
|
5334
|
+
name: "(parse-error)",
|
|
5335
|
+
kind: "unknown",
|
|
5336
|
+
source: fileName,
|
|
5337
|
+
ports: [],
|
|
5338
|
+
hasHealthcheck: false,
|
|
5339
|
+
envNames: [],
|
|
5340
|
+
confidence: "low"
|
|
5341
|
+
});
|
|
5342
|
+
}
|
|
5343
|
+
}
|
|
5344
|
+
return services;
|
|
5345
|
+
}
|
|
5346
|
+
function inspectEnvFiles(repoPath) {
|
|
5347
|
+
const files = import_node_fs13.default.readdirSync(repoPath).filter((fileName) => /^\.env(\.|$)/.test(fileName)).sort().map((fileName) => {
|
|
5348
|
+
const content = import_node_fs13.default.readFileSync(import_node_path12.default.join(repoPath, fileName), "utf-8");
|
|
5349
|
+
const names = content.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#") && line.includes("=")).map((line) => line.split("=")[0]?.trim()).filter((name) => Boolean(name)).sort();
|
|
5350
|
+
return { path: fileName, names };
|
|
5351
|
+
});
|
|
5352
|
+
const allNames = Array.from(new Set(files.flatMap((file) => file.names))).sort();
|
|
5353
|
+
return {
|
|
5354
|
+
files,
|
|
5355
|
+
authLikeNames: allNames.filter((name) => /(AUTH|OIDC|ISSUER|CLERK|NEXTAUTH)/i.test(name)),
|
|
5356
|
+
databaseLikeNames: allNames.filter((name) => /(DATABASE|POSTGRES|REDIS|MYSQL|MARIADB|DB_)/i.test(name))
|
|
5357
|
+
};
|
|
5358
|
+
}
|
|
5359
|
+
function inspectDevcontainer(repoPath) {
|
|
5360
|
+
const dir = import_node_path12.default.join(repoPath, ".devcontainer");
|
|
5361
|
+
if (!import_node_fs13.default.existsSync(dir)) {
|
|
5362
|
+
return { exists: false, files: [] };
|
|
5363
|
+
}
|
|
5364
|
+
return {
|
|
5365
|
+
exists: true,
|
|
5366
|
+
files: import_node_fs13.default.readdirSync(dir).filter((fileName) => import_node_fs13.default.statSync(import_node_path12.default.join(dir, fileName)).isFile()).sort().map((fileName) => `.devcontainer/${fileName}`)
|
|
5367
|
+
};
|
|
5368
|
+
}
|
|
5369
|
+
function inspectDevrouter(repoPath) {
|
|
5370
|
+
const configPath = getRepoConfigPath(repoPath);
|
|
5371
|
+
if (!import_node_fs13.default.existsSync(configPath)) {
|
|
5372
|
+
return {
|
|
5373
|
+
exists: false,
|
|
5374
|
+
configPath,
|
|
5375
|
+
valid: false,
|
|
5376
|
+
appCount: 0,
|
|
5377
|
+
tcpAppCount: 0,
|
|
5378
|
+
apps: []
|
|
5379
|
+
};
|
|
5380
|
+
}
|
|
5381
|
+
try {
|
|
5382
|
+
const config = loadRepoConfig(repoPath);
|
|
5383
|
+
return {
|
|
5384
|
+
exists: true,
|
|
5385
|
+
configPath,
|
|
5386
|
+
valid: true,
|
|
5387
|
+
appCount: config.apps.length,
|
|
5388
|
+
tcpAppCount: config.apps.filter((app) => app.kind !== "dependency" && app.protocol === "tcp").length,
|
|
5389
|
+
apps: config.apps.map((app) => ({
|
|
5390
|
+
name: app.name,
|
|
5391
|
+
runtime: app.runtime,
|
|
5392
|
+
protocol: app.kind === "dependency" ? void 0 : app.protocol,
|
|
5393
|
+
host: app.kind === "dependency" ? void 0 : app.host
|
|
5394
|
+
}))
|
|
5395
|
+
};
|
|
5396
|
+
} catch (error) {
|
|
5397
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5398
|
+
return {
|
|
5399
|
+
exists: true,
|
|
5400
|
+
configPath,
|
|
5401
|
+
valid: false,
|
|
5402
|
+
appCount: 0,
|
|
5403
|
+
tcpAppCount: 0,
|
|
5404
|
+
apps: [],
|
|
5405
|
+
error: sanitizeDiagnosticText(message)
|
|
5406
|
+
};
|
|
5407
|
+
}
|
|
5408
|
+
}
|
|
5409
|
+
function inspectAgentGuidance(repoPath) {
|
|
5410
|
+
const results = [];
|
|
5411
|
+
for (const [fileName, kind] of [["AGENTS.md", "agents"], ["CLAUDE.md", "claude"]]) {
|
|
5412
|
+
if (import_node_fs13.default.existsSync(import_node_path12.default.join(repoPath, fileName))) {
|
|
5413
|
+
results.push({ path: fileName, kind });
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
const skillsDir = import_node_path12.default.join(repoPath, ".agents", "skills");
|
|
5417
|
+
if (import_node_fs13.default.existsSync(skillsDir)) {
|
|
5418
|
+
for (const name of import_node_fs13.default.readdirSync(skillsDir).sort()) {
|
|
5419
|
+
const skillPath = import_node_path12.default.join(skillsDir, name, "SKILL.md");
|
|
5420
|
+
if (import_node_fs13.default.existsSync(skillPath)) {
|
|
5421
|
+
results.push({ path: relative(repoPath, skillPath), kind: "skill" });
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
5424
|
+
}
|
|
5425
|
+
return results;
|
|
5426
|
+
}
|
|
5427
|
+
function buildIssues(report) {
|
|
5428
|
+
const issues = [];
|
|
5429
|
+
if (!report.packageManager) {
|
|
5430
|
+
issues.push({
|
|
5431
|
+
id: "repo.package-manager.missing",
|
|
5432
|
+
level: "warn",
|
|
5433
|
+
summary: "No package manager metadata or lockfile detected."
|
|
5434
|
+
});
|
|
5435
|
+
}
|
|
5436
|
+
if (!report.devcontainer.exists) {
|
|
5437
|
+
issues.push({
|
|
5438
|
+
id: "repo.devcontainer.missing",
|
|
5439
|
+
level: "warn",
|
|
5440
|
+
summary: "No .devcontainer directory found."
|
|
5441
|
+
});
|
|
5442
|
+
}
|
|
5443
|
+
if (!report.devrouter.exists) {
|
|
5444
|
+
issues.push({
|
|
5445
|
+
id: "repo.devrouter.missing",
|
|
5446
|
+
level: "warn",
|
|
5447
|
+
summary: "No .devrouter.yml found.",
|
|
5448
|
+
suggestion: `Run: dev repo init --repo ${report.repoPath}`
|
|
5449
|
+
});
|
|
5450
|
+
} else if (!report.devrouter.valid) {
|
|
5451
|
+
issues.push({
|
|
5452
|
+
id: "repo.devrouter.invalid",
|
|
5453
|
+
level: "error",
|
|
5454
|
+
summary: ".devrouter.yml exists but is invalid.",
|
|
5455
|
+
details: report.devrouter.error,
|
|
5456
|
+
suggestion: "Fix .devrouter.yml validation errors."
|
|
5457
|
+
});
|
|
5458
|
+
}
|
|
5459
|
+
return issues;
|
|
5460
|
+
}
|
|
5461
|
+
function inspectRepo(options = {}) {
|
|
5462
|
+
const repoPath = resolveRepoPath(options.repo);
|
|
5463
|
+
const pkg = readJson(import_node_path12.default.join(repoPath, "package.json"));
|
|
5464
|
+
const scripts = inspectScripts(pkg);
|
|
5465
|
+
const reportWithoutIssues = {
|
|
5466
|
+
repoPath,
|
|
5467
|
+
packageManager: inspectPackageManager(repoPath, pkg),
|
|
5468
|
+
node: inspectNode(repoPath, pkg),
|
|
5469
|
+
scripts,
|
|
5470
|
+
apps: inspectAppCandidates(scripts),
|
|
5471
|
+
services: inspectServices(repoPath),
|
|
5472
|
+
env: inspectEnvFiles(repoPath),
|
|
5473
|
+
devcontainer: inspectDevcontainer(repoPath),
|
|
5474
|
+
devrouter: inspectDevrouter(repoPath),
|
|
5475
|
+
agentGuidance: inspectAgentGuidance(repoPath)
|
|
5476
|
+
};
|
|
5477
|
+
return {
|
|
5478
|
+
...reportWithoutIssues,
|
|
5479
|
+
issues: buildIssues(reportWithoutIssues)
|
|
5480
|
+
};
|
|
5481
|
+
}
|
|
5482
|
+
var import_node_fs13, import_node_path12, import_yaml5;
|
|
5483
|
+
var init_repo_inspect = __esm({
|
|
5484
|
+
"src/core/repo-inspect.ts"() {
|
|
5485
|
+
"use strict";
|
|
5486
|
+
import_node_fs13 = __toESM(require("fs"));
|
|
5487
|
+
import_node_path12 = __toESM(require("path"));
|
|
5488
|
+
import_yaml5 = __toESM(require("yaml"));
|
|
5489
|
+
init_repo_config();
|
|
5490
|
+
}
|
|
5491
|
+
});
|
|
5492
|
+
|
|
5493
|
+
// src/commands/repo-inspect.ts
|
|
5494
|
+
var repo_inspect_exports = {};
|
|
5495
|
+
__export(repo_inspect_exports, {
|
|
5496
|
+
runRepoInspectCommand: () => runRepoInspectCommand
|
|
5497
|
+
});
|
|
5498
|
+
async function runRepoInspectCommand(options) {
|
|
5499
|
+
const report = inspectRepo({ repo: options.repo });
|
|
5500
|
+
if (options.json) {
|
|
5501
|
+
printJSON(report);
|
|
5502
|
+
return;
|
|
5503
|
+
}
|
|
5504
|
+
printRepoInspectionSummary(report);
|
|
5505
|
+
}
|
|
5506
|
+
function printRepoInspectionSummary(report) {
|
|
5507
|
+
const packageManager = report.packageManager ? `${report.packageManager.name}${report.packageManager.version ? `@${report.packageManager.version}` : ""}` : "not detected";
|
|
5508
|
+
const devrouter = report.devrouter.exists ? report.devrouter.valid ? `valid (${report.devrouter.appCount} app(s))` : "invalid" : "missing";
|
|
5509
|
+
process.stdout.write(`Repo: ${report.repoPath}
|
|
5510
|
+
`);
|
|
5511
|
+
process.stdout.write(`Package manager: ${packageManager}
|
|
5512
|
+
`);
|
|
5513
|
+
process.stdout.write(`Scripts: ${report.scripts.length}
|
|
5514
|
+
`);
|
|
5515
|
+
process.stdout.write(`App candidates: ${report.apps.length}
|
|
5516
|
+
`);
|
|
5517
|
+
process.stdout.write(`Compose services: ${report.services.length}
|
|
5518
|
+
`);
|
|
5519
|
+
process.stdout.write(`Devcontainer: ${report.devcontainer.exists ? "yes" : "no"}
|
|
5520
|
+
`);
|
|
5521
|
+
process.stdout.write(`Devrouter config: ${devrouter}
|
|
5522
|
+
`);
|
|
5523
|
+
if (report.issues.length > 0) {
|
|
5524
|
+
process.stdout.write("\nIssues:\n");
|
|
5525
|
+
for (const issue of report.issues) {
|
|
5526
|
+
process.stdout.write(`- ${issue.id} [${issue.level}]: ${issue.summary}
|
|
5527
|
+
`);
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
process.stdout.write("\nFor full agent-readable output, run: dev repo inspect --json\n");
|
|
5531
|
+
}
|
|
5532
|
+
var init_repo_inspect2 = __esm({
|
|
5533
|
+
"src/commands/repo-inspect.ts"() {
|
|
5534
|
+
"use strict";
|
|
5535
|
+
init_repo_inspect();
|
|
5536
|
+
init_output();
|
|
5537
|
+
}
|
|
5538
|
+
});
|
|
5539
|
+
|
|
5540
|
+
// src/commands/repo-agents.ts
|
|
5541
|
+
var repo_agents_exports = {};
|
|
5542
|
+
__export(repo_agents_exports, {
|
|
5543
|
+
runRepoAgentsCommand: () => runRepoAgentsCommand
|
|
5544
|
+
});
|
|
5545
|
+
async function runRepoAgentsCommand(options, deps = {}) {
|
|
5546
|
+
const repoPath = resolveRepoPath(options.repo);
|
|
5547
|
+
let linearMetadata = null;
|
|
5548
|
+
const skill = ensureSkillFile(repoPath);
|
|
5549
|
+
process.stdout.write(`Wrote skill to ${skill.path}
|
|
5550
|
+
`);
|
|
5551
|
+
if (options.withLinear) {
|
|
5552
|
+
linearMetadata = await (deps.collectLinearMetadata ?? collectLinearWorkflowMetadata)();
|
|
5553
|
+
if (linearMetadata.captureMode === "placeholder") {
|
|
5554
|
+
process.stdout.write(
|
|
5555
|
+
"Warning: non-interactive mode detected; wrote placeholder Linear mapping values. Re-run in a TTY to capture workspace/team/project.\n"
|
|
5556
|
+
);
|
|
5557
|
+
}
|
|
5558
|
+
const linearSkills = ensureLinearWorkflowSkillFiles(repoPath);
|
|
5559
|
+
for (const filePath of linearSkills.paths) {
|
|
5560
|
+
process.stdout.write(`Wrote Linear workflow artifact to ${filePath}
|
|
5561
|
+
`);
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5564
|
+
const result = ensureAgentsMdSection(repoPath);
|
|
5565
|
+
if (!result.written) {
|
|
5566
|
+
process.stdout.write(`devrouter section already present: ${result.path}
|
|
5567
|
+
`);
|
|
5568
|
+
} else {
|
|
5569
|
+
process.stdout.write(`Wrote devrouter section to ${result.path}
|
|
5570
|
+
`);
|
|
5571
|
+
}
|
|
5572
|
+
if (options.withLinear) {
|
|
5573
|
+
if (!linearMetadata) {
|
|
5574
|
+
throw new Error("Linear metadata was not collected.");
|
|
5575
|
+
}
|
|
5576
|
+
const linearAgents = ensureLinearWorkflowAgentsSection(repoPath, linearMetadata);
|
|
5577
|
+
if (!linearAgents.written) {
|
|
5578
|
+
process.stdout.write(`Linear workflow section already present: ${linearAgents.path}
|
|
5579
|
+
`);
|
|
5580
|
+
return;
|
|
5581
|
+
}
|
|
5582
|
+
process.stdout.write(`Wrote Linear workflow section to ${linearAgents.path}
|
|
5583
|
+
`);
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5586
|
+
var init_repo_agents = __esm({
|
|
5587
|
+
"src/commands/repo-agents.ts"() {
|
|
5588
|
+
"use strict";
|
|
5589
|
+
init_repo_config();
|
|
5590
|
+
init_agents_md();
|
|
5591
|
+
init_linear_onboarding();
|
|
5592
|
+
}
|
|
5593
|
+
});
|
|
5594
|
+
|
|
5595
|
+
// src/core/devcontainer-write.ts
|
|
5596
|
+
function sanitizeProjectName(value) {
|
|
5597
|
+
const sanitized = value.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
|
|
5598
|
+
return sanitized || "app";
|
|
5599
|
+
}
|
|
5600
|
+
function majorVersion(value, fallback) {
|
|
5601
|
+
const match = value?.match(/(\d+)/);
|
|
5602
|
+
return match ? match[1] : fallback;
|
|
5603
|
+
}
|
|
5604
|
+
function devrouterVersion(value) {
|
|
5605
|
+
return value && /^\d+\.\d+\.\d+$/.test(value) ? value : DEFAULT_DEVROUTER_VERSION;
|
|
5606
|
+
}
|
|
5607
|
+
function inferDevScript(repo) {
|
|
5608
|
+
const script = repo.scripts.find((entry) => entry.name === "dev") ?? repo.scripts.find((entry) => entry.name.endsWith(":dev"));
|
|
5609
|
+
if (!script) {
|
|
5610
|
+
return "pnpm dev";
|
|
5611
|
+
}
|
|
5612
|
+
return script.name === "dev" ? "pnpm dev" : `pnpm run -- ${shellSingleQuote(script.name)}`;
|
|
5613
|
+
}
|
|
5614
|
+
function inferPort2(repo) {
|
|
5615
|
+
return repo.apps.find((app) => app.port)?.port ?? 3e3;
|
|
5616
|
+
}
|
|
5617
|
+
function renderDockerfile(nodeMajor, pnpmVersion) {
|
|
5618
|
+
const pnpmPackageSpec = `pnpm@${pnpmVersion}`;
|
|
5619
|
+
return `# ${MANAGED_MARKER}
|
|
5620
|
+
FROM node:${nodeMajor}-bookworm-slim
|
|
5621
|
+
|
|
5622
|
+
RUN apt-get update \\
|
|
5623
|
+
&& apt-get install -y --no-install-recommends git ca-certificates curl procps openssl \\
|
|
5624
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
5625
|
+
|
|
5626
|
+
RUN npm install -g ${shellSingleQuote(pnpmPackageSpec)}
|
|
5627
|
+
|
|
5628
|
+
WORKDIR /workspaces/app
|
|
5629
|
+
`;
|
|
5630
|
+
}
|
|
5631
|
+
function renderCompose(projectName) {
|
|
5632
|
+
return `# ${MANAGED_MARKER}
|
|
5633
|
+
services:
|
|
5634
|
+
postgres:
|
|
5635
|
+
image: postgres:16-alpine
|
|
5636
|
+
environment:
|
|
5637
|
+
POSTGRES_USER: prisma
|
|
5638
|
+
POSTGRES_PASSWORD: prisma
|
|
5639
|
+
POSTGRES_DB: prisma
|
|
5640
|
+
healthcheck:
|
|
5641
|
+
test: ["CMD-SHELL", "pg_isready -U prisma -d prisma"]
|
|
5642
|
+
interval: 5s
|
|
5643
|
+
timeout: 3s
|
|
5644
|
+
retries: 20
|
|
5645
|
+
networks:
|
|
5646
|
+
default: {}
|
|
5647
|
+
devnet:
|
|
5648
|
+
aliases:
|
|
5649
|
+
- \${WORKSPACE:-${projectName}}-db
|
|
5650
|
+
volumes:
|
|
5651
|
+
- ./init-db.sh:/docker-entrypoint-initdb.d/10-create-shadow-db.sh:ro
|
|
5652
|
+
- pgdata:/var/lib/postgresql/data
|
|
5653
|
+
|
|
5654
|
+
app:
|
|
5655
|
+
build:
|
|
5656
|
+
context: .
|
|
5657
|
+
dockerfile: Dockerfile
|
|
5658
|
+
init: true
|
|
5659
|
+
env_file:
|
|
5660
|
+
- devcontainer.env
|
|
5661
|
+
command: sleep infinity
|
|
5662
|
+
networks:
|
|
5663
|
+
default: {}
|
|
5664
|
+
devnet:
|
|
5665
|
+
aliases:
|
|
5666
|
+
- \${WORKSPACE:-${projectName}}-app
|
|
5667
|
+
volumes:
|
|
5668
|
+
- ..:/workspaces/${projectName}:cached
|
|
5669
|
+
- node_modules:/workspaces/${projectName}/node_modules
|
|
5670
|
+
working_dir: /workspaces/${projectName}
|
|
5671
|
+
depends_on:
|
|
5672
|
+
postgres:
|
|
5673
|
+
condition: service_healthy
|
|
5674
|
+
|
|
5675
|
+
networks:
|
|
5676
|
+
devnet:
|
|
5677
|
+
external: true
|
|
5678
|
+
|
|
5679
|
+
volumes:
|
|
5680
|
+
pgdata:
|
|
5681
|
+
node_modules:
|
|
5682
|
+
`;
|
|
5683
|
+
}
|
|
5684
|
+
function renderInitDb() {
|
|
5685
|
+
return `#!/usr/bin/env bash
|
|
5686
|
+
# ${MANAGED_MARKER}
|
|
5687
|
+
set -euo pipefail
|
|
5688
|
+
|
|
5689
|
+
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<'SQL'
|
|
5690
|
+
SELECT 'CREATE DATABASE shadow'
|
|
5691
|
+
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'shadow')\\gexec
|
|
5692
|
+
SQL
|
|
5693
|
+
`;
|
|
5694
|
+
}
|
|
5695
|
+
function renderDevcontainerJson(projectName) {
|
|
5696
|
+
return `{
|
|
5697
|
+
"name": "${projectName}",
|
|
5698
|
+
"dockerComposeFile": "docker-compose.yml",
|
|
5699
|
+
"service": "app",
|
|
5700
|
+
"workspaceFolder": "/workspaces/${projectName}",
|
|
5701
|
+
"postCreateCommand": "bash .devcontainer/post-create.sh",
|
|
5702
|
+
"postStartCommand": "bash .devcontainer/post-start.sh",
|
|
5703
|
+
"customizations": {
|
|
5704
|
+
"devrouter": {
|
|
5705
|
+
"managed": "${MANAGED_MARKER}"
|
|
5706
|
+
},
|
|
5707
|
+
"vscode": {
|
|
5708
|
+
"extensions": []
|
|
5709
|
+
}
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5712
|
+
`;
|
|
5713
|
+
}
|
|
5714
|
+
function renderEnv(projectName, port) {
|
|
5715
|
+
return `# ${MANAGED_MARKER}
|
|
5716
|
+
WORKSPACE=${projectName}
|
|
5717
|
+
HOST=0.0.0.0
|
|
5718
|
+
HOSTNAME=0.0.0.0
|
|
5719
|
+
PORT=${port}
|
|
5720
|
+
DATABASE_URL=postgres://prisma:prisma@postgres:5432/prisma
|
|
5721
|
+
SHADOW_DATABASE_URL=postgres://prisma:prisma@postgres:5432/shadow
|
|
5722
|
+
`;
|
|
5723
|
+
}
|
|
5724
|
+
function renderPostCreate() {
|
|
5725
|
+
return `#!/usr/bin/env bash
|
|
5726
|
+
# ${MANAGED_MARKER}
|
|
5727
|
+
set -euo pipefail
|
|
5728
|
+
|
|
5729
|
+
export CI=true
|
|
5730
|
+
export npm_config_verify_deps_before_run=false
|
|
5731
|
+
|
|
5732
|
+
if [ -f package.json ]; then
|
|
5733
|
+
pnpm install --no-frozen-lockfile
|
|
5734
|
+
fi
|
|
5735
|
+
`;
|
|
5736
|
+
}
|
|
5737
|
+
function shellSingleQuote(value) {
|
|
5738
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
5739
|
+
}
|
|
5740
|
+
function renderPostStart(devCommand) {
|
|
5741
|
+
return `#!/usr/bin/env bash
|
|
5742
|
+
# ${MANAGED_MARKER}
|
|
5743
|
+
set -euo pipefail
|
|
5744
|
+
|
|
5745
|
+
export CI=true
|
|
5746
|
+
export npm_config_verify_deps_before_run=false
|
|
5747
|
+
set -a
|
|
5748
|
+
. .devcontainer/devcontainer.env
|
|
5749
|
+
set +a
|
|
5750
|
+
|
|
5751
|
+
if pgrep -f ${shellSingleQuote(devCommand)} >/dev/null 2>&1; then
|
|
5752
|
+
exit 0
|
|
5753
|
+
fi
|
|
5754
|
+
|
|
5755
|
+
setsid bash -lc ${shellSingleQuote(devCommand)} >/tmp/devrouter-app.log 2>&1 </dev/null &
|
|
5756
|
+
`;
|
|
5757
|
+
}
|
|
5758
|
+
function renderDevrouter(projectName, port, version) {
|
|
5759
|
+
return `# ${MANAGED_MARKER}
|
|
5760
|
+
version: 1
|
|
5761
|
+
devrouter:
|
|
5762
|
+
version: ${version}
|
|
5763
|
+
project:
|
|
5764
|
+
name: ${projectName}
|
|
5765
|
+
apps:
|
|
5766
|
+
- name: app
|
|
5767
|
+
host: ${projectName}.localhost
|
|
5768
|
+
protocol: http
|
|
5769
|
+
runtime: proxy
|
|
5770
|
+
upstream: \${WORKSPACE}-app:${port}
|
|
5771
|
+
|
|
5772
|
+
- name: db
|
|
5773
|
+
host: db.${projectName}.localhost
|
|
5774
|
+
protocol: tcp
|
|
5775
|
+
tcpProtocol: postgres
|
|
5776
|
+
runtime: proxy
|
|
5777
|
+
upstream: \${WORKSPACE}-db:5432
|
|
5778
|
+
`;
|
|
5779
|
+
}
|
|
5780
|
+
function renderReadme(projectName) {
|
|
5781
|
+
return `<!-- ${MANAGED_MARKER} -->
|
|
5782
|
+
# Devcontainer
|
|
5783
|
+
|
|
5784
|
+
Use this repo through the devcontainer, with devrouter providing stable local routes.
|
|
5785
|
+
|
|
5786
|
+
\`\`\`bash
|
|
5787
|
+
dev setup --yes
|
|
5788
|
+
devpod up .
|
|
5789
|
+
dev app run app --repo . --yes
|
|
5790
|
+
dev app run db --repo . --yes
|
|
5791
|
+
\`\`\`
|
|
5792
|
+
|
|
5793
|
+
- App: https://${projectName}.localhost
|
|
5794
|
+
- Postgres: db.${projectName}.localhost:5432 (TLS/SNI)
|
|
5795
|
+
`;
|
|
5796
|
+
}
|
|
5797
|
+
function postWriteNextSteps(repoPath) {
|
|
5798
|
+
const quotedRepoPath = shellSingleQuote(repoPath);
|
|
5799
|
+
return [
|
|
5800
|
+
`Run: dev setup --repo ${quotedRepoPath} --yes`,
|
|
5801
|
+
`Run: cd ${quotedRepoPath} && devpod up .`,
|
|
5802
|
+
`Run: dev app run app --repo ${quotedRepoPath} --yes`,
|
|
5803
|
+
`Run: dev app run db --repo ${quotedRepoPath} --yes`,
|
|
5804
|
+
`Optional: dev repo agents --repo ${quotedRepoPath}`
|
|
5805
|
+
];
|
|
5806
|
+
}
|
|
5807
|
+
function issueNextSteps(issues) {
|
|
5808
|
+
const steps = issues.filter((issue) => issue.level === "error").map((issue) => issue.suggestion).filter((suggestion) => Boolean(suggestion));
|
|
5809
|
+
if (steps.length > 0) {
|
|
5810
|
+
return [...steps, "Re-run: dev repo devcontainer write --dry-run --json"];
|
|
5811
|
+
}
|
|
5812
|
+
return ["Resolve reported errors, then re-run: dev repo devcontainer write --dry-run --json"];
|
|
5813
|
+
}
|
|
5814
|
+
function packageManagerIssues(repo) {
|
|
5815
|
+
if (!repo.packageManager) {
|
|
5816
|
+
return [
|
|
5817
|
+
{
|
|
5818
|
+
id: "repo.devcontainer.package-manager-unknown",
|
|
5819
|
+
level: "warn",
|
|
5820
|
+
summary: "Package manager could not be detected; pnpm scaffold will be generated.",
|
|
5821
|
+
suggestion: "Add packageManager: pnpm@<version> to package.json or add pnpm-lock.yaml before writing."
|
|
5822
|
+
}
|
|
5823
|
+
];
|
|
5824
|
+
}
|
|
5825
|
+
if (repo.packageManager.name !== "pnpm") {
|
|
5826
|
+
return [
|
|
5827
|
+
{
|
|
5828
|
+
id: "repo.devcontainer.package-manager-unsupported",
|
|
5829
|
+
level: "error",
|
|
5830
|
+
summary: `Only pnpm repositories are supported by this devcontainer scaffold; detected ${repo.packageManager.name}.`,
|
|
5831
|
+
details: repo.packageManager.source,
|
|
5832
|
+
suggestion: "Use a pnpm repo for this scaffold, or adapt the generated plan manually before writing."
|
|
5833
|
+
}
|
|
5834
|
+
];
|
|
5835
|
+
}
|
|
5836
|
+
if (repo.packageManager.version && !VALID_PACKAGE_VERSION_RE.test(repo.packageManager.version)) {
|
|
5837
|
+
return [
|
|
5838
|
+
{
|
|
5839
|
+
id: "repo.devcontainer.package-manager-version-unsupported",
|
|
5840
|
+
level: "error",
|
|
5841
|
+
summary: `Unsupported pnpm version '${repo.packageManager.version}' in packageManager.`,
|
|
5842
|
+
details: repo.packageManager.source,
|
|
5843
|
+
suggestion: "Use a pinned semver packageManager value such as pnpm@11.6.0 before writing the scaffold."
|
|
5844
|
+
}
|
|
5845
|
+
];
|
|
5846
|
+
}
|
|
5847
|
+
return [];
|
|
5848
|
+
}
|
|
5849
|
+
function plannedFiles(repoPath, version) {
|
|
5850
|
+
const repo = inspectRepo({ repo: repoPath });
|
|
5851
|
+
const projectName = sanitizeProjectName(import_node_path13.default.basename(repo.repoPath));
|
|
5852
|
+
const nodeMajor = majorVersion(repo.node?.version, "24");
|
|
5853
|
+
const pnpmVersion = repo.packageManager?.name === "pnpm" && repo.packageManager.version ? repo.packageManager.version : DEFAULT_PNPM_VERSION;
|
|
5854
|
+
const port = inferPort2(repo);
|
|
5855
|
+
const devCommand = inferDevScript(repo);
|
|
5856
|
+
const issues = packageManagerIssues(repo);
|
|
5857
|
+
return {
|
|
5858
|
+
projectName,
|
|
5859
|
+
issues,
|
|
5860
|
+
files: [
|
|
5861
|
+
{ relativePath: ".devcontainer/Dockerfile", content: renderDockerfile(nodeMajor, pnpmVersion) },
|
|
5862
|
+
{ relativePath: ".devcontainer/docker-compose.yml", content: renderCompose(projectName) },
|
|
5863
|
+
{ relativePath: ".devcontainer/init-db.sh", content: renderInitDb(), executable: true },
|
|
5864
|
+
{ relativePath: ".devcontainer/devcontainer.json", content: renderDevcontainerJson(projectName) },
|
|
5865
|
+
{ relativePath: ".devcontainer/devcontainer.env", content: renderEnv(projectName, port) },
|
|
5866
|
+
{ relativePath: ".devcontainer/post-create.sh", content: renderPostCreate(), executable: true },
|
|
5867
|
+
{ relativePath: ".devcontainer/post-start.sh", content: renderPostStart(devCommand), executable: true },
|
|
5868
|
+
{ relativePath: ".devcontainer/README.md", content: renderReadme(projectName) },
|
|
5869
|
+
{ relativePath: ".devrouter.yml", content: renderDevrouter(projectName, port, version) }
|
|
5870
|
+
]
|
|
5871
|
+
};
|
|
5872
|
+
}
|
|
5873
|
+
function classifyFile(repoPath, file) {
|
|
5874
|
+
const absolutePath = import_node_path13.default.join(repoPath, file.relativePath);
|
|
5875
|
+
if (!import_node_fs14.default.existsSync(absolutePath)) {
|
|
5876
|
+
return {
|
|
5877
|
+
path: file.relativePath,
|
|
5878
|
+
action: "create",
|
|
5879
|
+
reason: "file is missing",
|
|
5880
|
+
bytes: Buffer.byteLength(file.content)
|
|
5881
|
+
};
|
|
5882
|
+
}
|
|
5883
|
+
const current = import_node_fs14.default.readFileSync(absolutePath, "utf-8");
|
|
5884
|
+
if (!current.includes(MANAGED_MARKER)) {
|
|
5885
|
+
return {
|
|
5886
|
+
path: file.relativePath,
|
|
5887
|
+
action: "conflict",
|
|
5888
|
+
reason: "existing file is not marked as devrouter-managed"
|
|
5889
|
+
};
|
|
5890
|
+
}
|
|
5891
|
+
if (current === file.content) {
|
|
5892
|
+
return {
|
|
5893
|
+
path: file.relativePath,
|
|
5894
|
+
action: "skip",
|
|
5895
|
+
reason: "managed file already matches",
|
|
5896
|
+
bytes: Buffer.byteLength(file.content)
|
|
5897
|
+
};
|
|
5898
|
+
}
|
|
5899
|
+
return {
|
|
5900
|
+
path: file.relativePath,
|
|
5901
|
+
action: "update",
|
|
5902
|
+
reason: "managed file differs",
|
|
5903
|
+
bytes: Buffer.byteLength(file.content)
|
|
5904
|
+
};
|
|
5905
|
+
}
|
|
5906
|
+
function buildPlan(repoPath, dryRun, version) {
|
|
5907
|
+
const rendered = plannedFiles(repoPath, version);
|
|
5908
|
+
const files = rendered.files;
|
|
5909
|
+
const filePlans = files.map((file) => classifyFile(repoPath, file));
|
|
5910
|
+
const issues = [...rendered.issues];
|
|
5911
|
+
if (filePlans.some((file) => file.action === "conflict")) {
|
|
5912
|
+
issues.push({
|
|
5913
|
+
id: "repo.devcontainer.write-conflict",
|
|
5914
|
+
level: "error",
|
|
5915
|
+
summary: "One or more target files already exist and are not devrouter-managed.",
|
|
5916
|
+
suggestion: "Review the conflicts, move custom files aside, or merge the devrouter-managed section manually."
|
|
5917
|
+
});
|
|
5918
|
+
}
|
|
5919
|
+
filePlans.push({
|
|
5920
|
+
path: "AGENTS.md",
|
|
5921
|
+
action: "suggest",
|
|
5922
|
+
reason: "run dev repo agents after reviewing the scaffold"
|
|
5923
|
+
});
|
|
5924
|
+
return {
|
|
5925
|
+
files,
|
|
5926
|
+
plan: {
|
|
5927
|
+
repoPath,
|
|
5928
|
+
projectName: rendered.projectName,
|
|
5929
|
+
profile: "node-postgres",
|
|
5930
|
+
dryRun,
|
|
5931
|
+
files: filePlans,
|
|
5932
|
+
issues,
|
|
5933
|
+
nextSteps: issues.some((issue) => issue.level === "error") ? issueNextSteps(issues) : dryRun ? [`Review this plan, then run: dev repo devcontainer write --repo ${shellSingleQuote(repoPath)} --yes`] : postWriteNextSteps(repoPath)
|
|
5934
|
+
}
|
|
5935
|
+
};
|
|
5936
|
+
}
|
|
5937
|
+
function writeFile(repoPath, file) {
|
|
5938
|
+
const absolutePath = import_node_path13.default.join(repoPath, file.relativePath);
|
|
5939
|
+
import_node_fs14.default.mkdirSync(import_node_path13.default.dirname(absolutePath), { recursive: true });
|
|
5940
|
+
import_node_fs14.default.writeFileSync(absolutePath, file.content, "utf-8");
|
|
5941
|
+
if (file.executable) {
|
|
5942
|
+
import_node_fs14.default.chmodSync(absolutePath, 493);
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
function writeDevcontainer(options = {}) {
|
|
5946
|
+
const repoPath = resolveRepoPath(options.repo);
|
|
5947
|
+
const dryRun = Boolean(options.dryRun);
|
|
5948
|
+
const { plan, files } = buildPlan(repoPath, dryRun, devrouterVersion(options.installedVersion));
|
|
5949
|
+
if (dryRun || plan.issues.some((issue) => issue.level === "error")) {
|
|
5950
|
+
return plan;
|
|
5951
|
+
}
|
|
5952
|
+
if (!options.yes) {
|
|
5953
|
+
return {
|
|
5954
|
+
...plan,
|
|
5955
|
+
issues: [
|
|
5956
|
+
...plan.issues,
|
|
5957
|
+
{
|
|
5958
|
+
id: "repo.devcontainer.confirmation",
|
|
5959
|
+
level: "error",
|
|
5960
|
+
summary: "Writing devcontainer files requires --yes.",
|
|
5961
|
+
suggestion: `Run: dev repo devcontainer write --repo ${shellSingleQuote(repoPath)} --yes`
|
|
5962
|
+
}
|
|
5963
|
+
],
|
|
5964
|
+
nextSteps: [`Run: dev repo devcontainer write --repo ${shellSingleQuote(repoPath)} --yes`]
|
|
5965
|
+
};
|
|
5966
|
+
}
|
|
5967
|
+
for (const file of files) {
|
|
5968
|
+
const classification = classifyFile(repoPath, file);
|
|
5969
|
+
if (classification.action === "create" || classification.action === "update") {
|
|
5970
|
+
writeFile(repoPath, file);
|
|
5971
|
+
}
|
|
5972
|
+
}
|
|
5973
|
+
return {
|
|
5974
|
+
...plan,
|
|
5975
|
+
nextSteps: postWriteNextSteps(repoPath)
|
|
5976
|
+
};
|
|
5977
|
+
}
|
|
5978
|
+
var import_node_fs14, import_node_path13, MANAGED_MARKER, DEFAULT_DEVROUTER_VERSION, DEFAULT_PNPM_VERSION, VALID_PACKAGE_VERSION_RE;
|
|
5979
|
+
var init_devcontainer_write = __esm({
|
|
5980
|
+
"src/core/devcontainer-write.ts"() {
|
|
5981
|
+
"use strict";
|
|
5982
|
+
import_node_fs14 = __toESM(require("fs"));
|
|
5983
|
+
import_node_path13 = __toESM(require("path"));
|
|
5984
|
+
init_repo_inspect();
|
|
5985
|
+
init_repo_config();
|
|
5986
|
+
MANAGED_MARKER = "devrouter:managed devcontainer";
|
|
5987
|
+
DEFAULT_DEVROUTER_VERSION = "0.0.0";
|
|
5988
|
+
DEFAULT_PNPM_VERSION = "11.6.0";
|
|
5989
|
+
VALID_PACKAGE_VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
5990
|
+
}
|
|
5991
|
+
});
|
|
5992
|
+
|
|
5993
|
+
// src/core/concurrency.ts
|
|
5994
|
+
function routeUrl(host) {
|
|
5995
|
+
const scheme = isTLSEnabled() ? "https" : "http";
|
|
5996
|
+
return `${scheme}://${host}`;
|
|
5997
|
+
}
|
|
5998
|
+
function assertAppNotRunning(repoPath, app) {
|
|
5999
|
+
const conflict = reconcileRouteRunConflict(repoPath, app);
|
|
6000
|
+
if (!conflict) {
|
|
6001
|
+
return;
|
|
6002
|
+
}
|
|
6003
|
+
if (conflict.kind === "same-app") {
|
|
6004
|
+
throw new AppAlreadyRunningError(
|
|
6005
|
+
app.name,
|
|
6006
|
+
routeUrl(conflict.route.host),
|
|
6007
|
+
conflict.route.pid,
|
|
6008
|
+
conflict.route.repoPath
|
|
6009
|
+
);
|
|
6010
|
+
}
|
|
6011
|
+
throw new HostnameConflictError(
|
|
6012
|
+
app.host,
|
|
6013
|
+
conflict.route.name,
|
|
6014
|
+
conflict.route.repoPath,
|
|
6015
|
+
conflict.route.pid
|
|
6016
|
+
);
|
|
6017
|
+
}
|
|
6018
|
+
var AppAlreadyRunningError, HostnameConflictError;
|
|
6019
|
+
var init_concurrency = __esm({
|
|
6020
|
+
"src/core/concurrency.ts"() {
|
|
6021
|
+
"use strict";
|
|
6022
|
+
init_router();
|
|
6023
|
+
init_route_state();
|
|
6024
|
+
AppAlreadyRunningError = class extends Error {
|
|
6025
|
+
constructor(appName, url, pid, repoPath) {
|
|
6026
|
+
const lines = [
|
|
6027
|
+
`App "${appName}" is already running.`,
|
|
6028
|
+
` URL: ${url}`,
|
|
6029
|
+
pid ? ` PID: ${pid}` : null,
|
|
6030
|
+
` Repo: ${repoPath}`
|
|
6031
|
+
].filter(Boolean);
|
|
6032
|
+
super(lines.join("\n"));
|
|
6033
|
+
this.appName = appName;
|
|
6034
|
+
this.url = url;
|
|
6035
|
+
this.pid = pid;
|
|
6036
|
+
this.repoPath = repoPath;
|
|
6037
|
+
this.name = "AppAlreadyRunningError";
|
|
6038
|
+
}
|
|
6039
|
+
};
|
|
6040
|
+
HostnameConflictError = class extends Error {
|
|
6041
|
+
constructor(hostname, existingApp, existingRepoPath, existingPid) {
|
|
6042
|
+
const lines = [
|
|
6043
|
+
`Hostname "${hostname}" is already claimed by app "${existingApp}".`,
|
|
6044
|
+
existingPid ? ` PID: ${existingPid}` : null,
|
|
6045
|
+
` Repo: ${existingRepoPath}`
|
|
6046
|
+
].filter(Boolean);
|
|
6047
|
+
super(lines.join("\n"));
|
|
6048
|
+
this.hostname = hostname;
|
|
6049
|
+
this.existingApp = existingApp;
|
|
6050
|
+
this.existingRepoPath = existingRepoPath;
|
|
6051
|
+
this.existingPid = existingPid;
|
|
6052
|
+
this.name = "HostnameConflictError";
|
|
6053
|
+
}
|
|
6054
|
+
};
|
|
6055
|
+
}
|
|
6056
|
+
});
|
|
6057
|
+
|
|
6058
|
+
// src/core/devcontainer-verify.ts
|
|
6059
|
+
function collectSummary2(checks) {
|
|
6060
|
+
return checks.reduce(
|
|
6061
|
+
(acc, check) => {
|
|
6062
|
+
acc[check.level] += 1;
|
|
6063
|
+
return acc;
|
|
6064
|
+
},
|
|
6065
|
+
{ ok: 0, warn: 0, error: 0 }
|
|
6066
|
+
);
|
|
6067
|
+
}
|
|
6068
|
+
function collectNextSteps3(checks) {
|
|
6069
|
+
const steps = /* @__PURE__ */ new Set();
|
|
6070
|
+
for (const check of checks) {
|
|
6071
|
+
if (check.level !== "ok" && check.suggestion) {
|
|
6072
|
+
steps.add(check.suggestion);
|
|
6073
|
+
}
|
|
6074
|
+
}
|
|
6075
|
+
return Array.from(steps.values());
|
|
6076
|
+
}
|
|
6077
|
+
function proxyApps(config) {
|
|
6078
|
+
return (config?.apps ?? []).filter(
|
|
6079
|
+
(app) => app.kind !== "dependency" && app.runtime === "proxy"
|
|
6080
|
+
);
|
|
6081
|
+
}
|
|
6082
|
+
function routedApps(config) {
|
|
6083
|
+
return config.apps.filter((app) => app.kind !== "dependency");
|
|
6084
|
+
}
|
|
6085
|
+
function requiredFileChecks(repoPath) {
|
|
6086
|
+
const required = [
|
|
6087
|
+
".devcontainer/devcontainer.json",
|
|
6088
|
+
".devcontainer/docker-compose.yml",
|
|
6089
|
+
".devrouter.yml"
|
|
6090
|
+
];
|
|
6091
|
+
const missing = required.filter((fileName) => !import_node_fs15.default.existsSync(import_node_path14.default.join(repoPath, fileName)));
|
|
6092
|
+
return {
|
|
6093
|
+
id: "repo.devcontainer.verify-files",
|
|
6094
|
+
level: missing.length === 0 ? "ok" : "error",
|
|
6095
|
+
summary: missing.length === 0 ? "Required devcontainer/devrouter files are present." : `Missing required devcontainer/devrouter file(s): ${missing.join(", ")}.`,
|
|
6096
|
+
suggestion: missing.length === 0 ? void 0 : "Run: dev repo devcontainer write --dry-run --json"
|
|
6097
|
+
};
|
|
6098
|
+
}
|
|
6099
|
+
function proxyConfigCheck(apps) {
|
|
6100
|
+
return {
|
|
6101
|
+
id: "repo.devcontainer.verify-proxy-apps",
|
|
6102
|
+
level: apps.length > 0 ? "ok" : "error",
|
|
6103
|
+
summary: apps.length > 0 ? `Found ${apps.length} proxy app(s) for devcontainer routing.` : "No proxy app entries found for devcontainer routing.",
|
|
6104
|
+
suggestion: apps.length > 0 ? void 0 : "Add runtime: proxy app entries to .devrouter.yml or run: dev repo devcontainer write --dry-run --json"
|
|
6105
|
+
};
|
|
6106
|
+
}
|
|
6107
|
+
function workspaceTemplateCheck(apps) {
|
|
6108
|
+
const templated = apps.filter((app) => app.upstream.includes(WORKSPACE_PLACEHOLDER));
|
|
6109
|
+
return {
|
|
6110
|
+
id: "repo.devcontainer.verify-workspace-upstreams",
|
|
6111
|
+
level: apps.length > 0 && templated.length === apps.length ? "ok" : "warn",
|
|
6112
|
+
summary: apps.length === 0 ? "No proxy upstreams to inspect for workspace templating." : templated.length === apps.length ? "All proxy upstreams use the ${WORKSPACE} placeholder." : `${apps.length - templated.length} proxy upstream(s) do not use the \${WORKSPACE} placeholder.`,
|
|
6113
|
+
details: apps.length > 0 && templated.length !== apps.length ? apps.filter((app) => !app.upstream.includes(WORKSPACE_PLACEHOLDER)).map((app) => app.name).join(", ") : void 0,
|
|
6114
|
+
suggestion: apps.length > 0 && templated.length !== apps.length ? "Use ${WORKSPACE} in devcontainer proxy upstreams so parallel worktrees do not collide." : void 0
|
|
6115
|
+
};
|
|
6116
|
+
}
|
|
6117
|
+
function workspacePreview(repoPath, config) {
|
|
6118
|
+
const preview = applyWorkspace(config, "verify", repoPath);
|
|
6119
|
+
return routedApps(preview).map((app) => ({
|
|
6120
|
+
name: app.name,
|
|
6121
|
+
host: app.host,
|
|
6122
|
+
upstream: app.runtime === "proxy" ? app.upstream : void 0
|
|
6123
|
+
}));
|
|
6124
|
+
}
|
|
6125
|
+
function workspacePreviewCheck(preview) {
|
|
6126
|
+
const missingNamespacedHosts = (preview ?? []).filter((app) => !app.host.endsWith(".verify.localhost"));
|
|
6127
|
+
return {
|
|
6128
|
+
id: "repo.devcontainer.verify-workspace-preview",
|
|
6129
|
+
level: (preview ?? []).length > 0 && missingNamespacedHosts.length === 0 ? "ok" : "warn",
|
|
6130
|
+
summary: (preview ?? []).length === 0 ? "No routed app entries found for workspace preview." : missingNamespacedHosts.length === 0 ? "Workspace preview namespaces configured hosts without rewriting .devrouter.yml." : `${missingNamespacedHosts.length} workspace preview host(s) were not namespaced.`,
|
|
6131
|
+
details: missingNamespacedHosts.length > 0 ? missingNamespacedHosts.map((app) => `${app.name}: ${app.host}`).join(", ") : void 0,
|
|
6132
|
+
suggestion: missingNamespacedHosts.length > 0 ? "Use valid .localhost hosts and let devrouter namespace them at runtime." : void 0
|
|
6133
|
+
};
|
|
6134
|
+
}
|
|
6135
|
+
function blockingDoctorChecks(doctor) {
|
|
6136
|
+
return doctor.checks.filter(
|
|
6137
|
+
(check) => check.level === "error" && (check.id.startsWith("repo.devcontainer") || check.id === "repo.config" || check.id === "repo.tcp-tls" || check.id === "global.devnet")
|
|
6138
|
+
);
|
|
6139
|
+
}
|
|
6140
|
+
function doctorGateCheck(doctor) {
|
|
6141
|
+
const blocking = blockingDoctorChecks(doctor);
|
|
6142
|
+
return {
|
|
6143
|
+
id: "repo.devcontainer.verify-doctor",
|
|
6144
|
+
level: blocking.length === 0 ? "ok" : "error",
|
|
6145
|
+
summary: blocking.length === 0 ? "Doctor has no blocking devcontainer diagnostics." : `Doctor reported ${blocking.length} blocking devcontainer diagnostic(s).`,
|
|
6146
|
+
details: blocking.length > 0 ? blocking.map((check) => check.id).join(", ") : void 0,
|
|
6147
|
+
suggestion: blocking.length > 0 ? "Run: dev doctor --repo <path> --json" : void 0
|
|
6148
|
+
};
|
|
6149
|
+
}
|
|
6150
|
+
function routeUrl2(host) {
|
|
6151
|
+
return `${isTLSEnabled() ? "https" : "http"}://${host}`;
|
|
6152
|
+
}
|
|
6153
|
+
function curlRoute(host) {
|
|
6154
|
+
const result = (0, import_node_child_process9.spawnSync)("curl", ["-k", "-fsS", "--max-time", "5", routeUrl2(host)], {
|
|
6155
|
+
encoding: "utf-8"
|
|
6156
|
+
});
|
|
6157
|
+
if (result.status === 0) {
|
|
6158
|
+
return { ok: true, details: "HTTP route responded successfully." };
|
|
6159
|
+
}
|
|
6160
|
+
return {
|
|
6161
|
+
ok: false,
|
|
6162
|
+
details: (result.stderr || result.stdout || `curl exited with status ${String(result.status)}`).trim()
|
|
6163
|
+
};
|
|
6164
|
+
}
|
|
6165
|
+
function registerProxyRoute(repoPath, app, workspace) {
|
|
6166
|
+
ensureRouterFiles();
|
|
6167
|
+
const { port, upstreamHost: upstreamHost2 } = parseUpstream(app.upstream);
|
|
6168
|
+
if (app.protocol === "tcp" && !isTLSEnabled()) {
|
|
6169
|
+
throw new Error(
|
|
6170
|
+
`App "${app.name}" is a TCP proxy route, which requires TLS (SNI). Run \`dev tls install\` first.`
|
|
6171
|
+
);
|
|
6172
|
+
}
|
|
6173
|
+
if (app.protocol === "tcp") {
|
|
6174
|
+
const needsRestart = activateTcpProtocol(app.tcpProtocol);
|
|
6175
|
+
if (needsRestart) {
|
|
6176
|
+
startRouterStack();
|
|
6177
|
+
}
|
|
6178
|
+
}
|
|
6179
|
+
removeRouteForApp(repoPath, app.name);
|
|
6180
|
+
assertAppNotRunning(repoPath, { name: app.name, host: app.host });
|
|
6181
|
+
upsertHostRoute({
|
|
6182
|
+
name: app.name,
|
|
6183
|
+
host: app.host,
|
|
6184
|
+
protocol: app.protocol,
|
|
6185
|
+
tcpProtocol: app.protocol === "tcp" ? app.tcpProtocol : void 0,
|
|
6186
|
+
repoPath,
|
|
6187
|
+
port,
|
|
6188
|
+
upstreamHost: upstreamHost2,
|
|
6189
|
+
mode: "proxy",
|
|
6190
|
+
workspace
|
|
6191
|
+
});
|
|
6192
|
+
}
|
|
6193
|
+
async function liveChecks(repoPath, yes) {
|
|
6194
|
+
if (!yes) {
|
|
6195
|
+
return {
|
|
6196
|
+
routes: [],
|
|
6197
|
+
checks: [
|
|
6198
|
+
{
|
|
6199
|
+
id: "repo.devcontainer.verify-live-confirmation",
|
|
6200
|
+
level: "error",
|
|
6201
|
+
summary: "Live devcontainer verification requires --yes.",
|
|
6202
|
+
suggestion: "Run: dev repo devcontainer verify --live --yes --json"
|
|
6203
|
+
}
|
|
6204
|
+
]
|
|
6205
|
+
};
|
|
6206
|
+
}
|
|
6207
|
+
let runtime;
|
|
6208
|
+
try {
|
|
6209
|
+
runtime = loadRuntimeConfig(repoPath);
|
|
6210
|
+
} catch (error) {
|
|
6211
|
+
return {
|
|
6212
|
+
routes: [],
|
|
6213
|
+
checks: [
|
|
6214
|
+
{
|
|
6215
|
+
id: "repo.devcontainer.verify-live-config",
|
|
6216
|
+
level: "error",
|
|
6217
|
+
summary: "Could not load runtime config for live verification.",
|
|
6218
|
+
details: error instanceof Error ? error.message : String(error),
|
|
6219
|
+
suggestion: "Fix .devrouter.yml and re-run: dev repo devcontainer verify --live --yes --json"
|
|
6220
|
+
}
|
|
6221
|
+
]
|
|
6222
|
+
};
|
|
6223
|
+
}
|
|
6224
|
+
const apps = proxyApps(runtime.config);
|
|
6225
|
+
const checks = [];
|
|
6226
|
+
const routes = [];
|
|
6227
|
+
for (const app of apps) {
|
|
6228
|
+
try {
|
|
6229
|
+
registerProxyRoute(repoPath, app, runtime.workspace);
|
|
6230
|
+
if (app.protocol === "http") {
|
|
6231
|
+
const curl = curlRoute(app.host);
|
|
6232
|
+
routes.push({
|
|
6233
|
+
name: app.name,
|
|
6234
|
+
host: app.host,
|
|
6235
|
+
status: curl.ok ? "reachable" : "failed",
|
|
6236
|
+
details: curl.details
|
|
6237
|
+
});
|
|
6238
|
+
checks.push({
|
|
6239
|
+
id: `repo.devcontainer.verify-live-http.${app.name}`,
|
|
6240
|
+
level: curl.ok ? "ok" : "error",
|
|
6241
|
+
summary: curl.ok ? `HTTP proxy route '${app.name}' responded.` : `HTTP proxy route '${app.name}' did not respond.`,
|
|
6242
|
+
details: curl.ok ? void 0 : curl.details,
|
|
6243
|
+
suggestion: curl.ok ? void 0 : "Start the devcontainer app process, then re-run live verification."
|
|
6244
|
+
});
|
|
6245
|
+
} else {
|
|
6246
|
+
routes.push({
|
|
6247
|
+
name: app.name,
|
|
6248
|
+
host: app.host,
|
|
6249
|
+
status: "registered",
|
|
6250
|
+
details: `${app.tcpProtocol} route registered on port ${String(TCP_PROTOCOL_REGISTRY[app.tcpProtocol]?.port ?? 5432)}.`
|
|
6251
|
+
});
|
|
6252
|
+
checks.push({
|
|
6253
|
+
id: `repo.devcontainer.verify-live-tcp.${app.name}`,
|
|
6254
|
+
level: "ok",
|
|
6255
|
+
summary: `TCP proxy route '${app.name}' registered.`
|
|
6256
|
+
});
|
|
6257
|
+
}
|
|
6258
|
+
} catch (error) {
|
|
6259
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6260
|
+
routes.push({
|
|
6261
|
+
name: app.name,
|
|
6262
|
+
host: app.host,
|
|
6263
|
+
status: "failed",
|
|
6264
|
+
details: message
|
|
6265
|
+
});
|
|
6266
|
+
checks.push({
|
|
6267
|
+
id: `repo.devcontainer.verify-live-route.${app.name}`,
|
|
6268
|
+
level: "error",
|
|
6269
|
+
summary: `Could not register proxy route '${app.name}'.`,
|
|
6270
|
+
details: message,
|
|
6271
|
+
suggestion: "Run: dev setup --yes, start the devcontainer, then retry live verification."
|
|
6272
|
+
});
|
|
6273
|
+
}
|
|
6274
|
+
}
|
|
6275
|
+
return { checks, routes };
|
|
6276
|
+
}
|
|
6277
|
+
async function verifyDevcontainer(options = {}) {
|
|
6278
|
+
const repoPath = resolveRepoPath(options.repo);
|
|
6279
|
+
const doctor = await buildDoctorReport({ repo: repoPath });
|
|
6280
|
+
const checks = [doctorGateCheck(doctor), requiredFileChecks(repoPath)];
|
|
6281
|
+
let config;
|
|
6282
|
+
let workspaceEvidence;
|
|
6283
|
+
let apps = [];
|
|
6284
|
+
try {
|
|
6285
|
+
config = loadRepoConfig(repoPath);
|
|
6286
|
+
apps = proxyApps(config);
|
|
6287
|
+
workspaceEvidence = workspacePreview(repoPath, config);
|
|
6288
|
+
checks.push(proxyConfigCheck(apps));
|
|
6289
|
+
checks.push(workspaceTemplateCheck(apps));
|
|
6290
|
+
checks.push(workspacePreviewCheck(workspaceEvidence));
|
|
6291
|
+
} catch (error) {
|
|
6292
|
+
checks.push({
|
|
6293
|
+
id: "repo.devcontainer.verify-config",
|
|
6294
|
+
level: "error",
|
|
6295
|
+
summary: "Could not load .devrouter.yml for devcontainer verification.",
|
|
6296
|
+
details: error instanceof Error ? error.message : String(error),
|
|
6297
|
+
suggestion: "Fix .devrouter.yml and re-run: dev repo devcontainer verify --json"
|
|
6298
|
+
});
|
|
6299
|
+
}
|
|
6300
|
+
let liveRoutes;
|
|
6301
|
+
if (options.live) {
|
|
6302
|
+
const live = await liveChecks(repoPath, Boolean(options.yes));
|
|
6303
|
+
checks.push(...live.checks);
|
|
6304
|
+
liveRoutes = live.routes;
|
|
6305
|
+
}
|
|
6306
|
+
const summary = collectSummary2(checks);
|
|
6307
|
+
return {
|
|
6308
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6309
|
+
repoPath,
|
|
6310
|
+
live: Boolean(options.live),
|
|
6311
|
+
summary,
|
|
6312
|
+
checks,
|
|
6313
|
+
evidence: {
|
|
6314
|
+
doctorSummary: doctor.summary,
|
|
6315
|
+
blockingDoctorChecks: blockingDoctorChecks(doctor).map((check) => check.id),
|
|
6316
|
+
proxyApps: apps.map((app) => ({
|
|
6317
|
+
name: app.name,
|
|
6318
|
+
protocol: app.protocol,
|
|
6319
|
+
host: app.host,
|
|
6320
|
+
upstream: app.upstream
|
|
6321
|
+
})),
|
|
6322
|
+
workspacePreview: workspaceEvidence,
|
|
6323
|
+
liveRoutes
|
|
6324
|
+
},
|
|
6325
|
+
nextSteps: collectNextSteps3(checks)
|
|
6326
|
+
};
|
|
6327
|
+
}
|
|
6328
|
+
var import_node_child_process9, import_node_fs15, import_node_path14;
|
|
6329
|
+
var init_devcontainer_verify = __esm({
|
|
6330
|
+
"src/core/devcontainer-verify.ts"() {
|
|
4389
6331
|
"use strict";
|
|
6332
|
+
import_node_child_process9 = require("child_process");
|
|
6333
|
+
import_node_fs15 = __toESM(require("fs"));
|
|
6334
|
+
import_node_path14 = __toESM(require("path"));
|
|
4390
6335
|
init_repo_config();
|
|
6336
|
+
init_concurrency();
|
|
6337
|
+
init_host_routes();
|
|
6338
|
+
init_doctor();
|
|
6339
|
+
init_route_state();
|
|
6340
|
+
init_router();
|
|
6341
|
+
init_capabilities();
|
|
4391
6342
|
}
|
|
4392
6343
|
});
|
|
4393
6344
|
|
|
4394
|
-
// src/commands/repo-
|
|
4395
|
-
var
|
|
4396
|
-
__export(
|
|
4397
|
-
|
|
6345
|
+
// src/commands/repo-devcontainer.ts
|
|
6346
|
+
var repo_devcontainer_exports = {};
|
|
6347
|
+
__export(repo_devcontainer_exports, {
|
|
6348
|
+
runRepoDevcontainerVerifyCommand: () => runRepoDevcontainerVerifyCommand,
|
|
6349
|
+
runRepoDevcontainerWriteCommand: () => runRepoDevcontainerWriteCommand
|
|
4398
6350
|
});
|
|
4399
|
-
async function
|
|
4400
|
-
const
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
6351
|
+
async function runRepoDevcontainerWriteCommand(options) {
|
|
6352
|
+
const report = writeDevcontainer({
|
|
6353
|
+
repo: options.repo,
|
|
6354
|
+
dryRun: Boolean(options.dryRun),
|
|
6355
|
+
yes: Boolean(options.yes),
|
|
6356
|
+
installedVersion: options.installedVersion
|
|
6357
|
+
});
|
|
6358
|
+
if (options.json) {
|
|
6359
|
+
printJSON(report);
|
|
6360
|
+
} else {
|
|
6361
|
+
process.stdout.write(`Devcontainer profile: ${report.profile}
|
|
6362
|
+
`);
|
|
6363
|
+
for (const file of report.files) {
|
|
6364
|
+
process.stdout.write(`- ${file.action}: ${file.path} (${file.reason})
|
|
4404
6365
|
`);
|
|
4405
|
-
if (options.withLinear) {
|
|
4406
|
-
linearMetadata = await (deps.collectLinearMetadata ?? collectLinearWorkflowMetadata)();
|
|
4407
|
-
if (linearMetadata.captureMode === "placeholder") {
|
|
4408
|
-
process.stdout.write(
|
|
4409
|
-
"Warning: non-interactive mode detected; wrote placeholder Linear mapping values. Re-run in a TTY to capture workspace/team/project.\n"
|
|
4410
|
-
);
|
|
4411
6366
|
}
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
6367
|
+
if (report.issues.length > 0) {
|
|
6368
|
+
process.stdout.write("\nFindings:\n");
|
|
6369
|
+
for (const issue of report.issues) {
|
|
6370
|
+
process.stdout.write(`- ${issue.id} [${issue.level}]: ${issue.summary}
|
|
6371
|
+
`);
|
|
6372
|
+
if (issue.details) {
|
|
6373
|
+
process.stdout.write(` Details: ${issue.details}
|
|
6374
|
+
`);
|
|
6375
|
+
}
|
|
6376
|
+
if (issue.suggestion) {
|
|
6377
|
+
process.stdout.write(` Suggestion: ${issue.suggestion}
|
|
6378
|
+
`);
|
|
6379
|
+
}
|
|
6380
|
+
}
|
|
6381
|
+
}
|
|
6382
|
+
if (report.nextSteps.length > 0) {
|
|
6383
|
+
process.stdout.write("\nNext steps:\n");
|
|
6384
|
+
for (const step of report.nextSteps) {
|
|
6385
|
+
process.stdout.write(`- ${step}
|
|
4415
6386
|
`);
|
|
6387
|
+
}
|
|
4416
6388
|
}
|
|
4417
6389
|
}
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
6390
|
+
if (report.issues.some((issue) => issue.level === "error")) {
|
|
6391
|
+
process.exitCode = 1;
|
|
6392
|
+
}
|
|
6393
|
+
}
|
|
6394
|
+
async function runRepoDevcontainerVerifyCommand(options) {
|
|
6395
|
+
const report = await verifyDevcontainer({
|
|
6396
|
+
repo: options.repo,
|
|
6397
|
+
live: Boolean(options.live),
|
|
6398
|
+
yes: Boolean(options.yes)
|
|
6399
|
+
});
|
|
6400
|
+
if (options.json) {
|
|
6401
|
+
printJSON(report);
|
|
4422
6402
|
} else {
|
|
4423
|
-
|
|
4424
|
-
`);
|
|
6403
|
+
printVerifySummary(report);
|
|
4425
6404
|
}
|
|
4426
|
-
if (
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
6405
|
+
if (report.summary.error > 0) {
|
|
6406
|
+
process.exitCode = 1;
|
|
6407
|
+
}
|
|
6408
|
+
}
|
|
6409
|
+
function printVerifySummary(report) {
|
|
6410
|
+
process.stdout.write(`Repo: ${report.repoPath}
|
|
6411
|
+
`);
|
|
6412
|
+
process.stdout.write(`Mode: ${report.live ? "live" : "static"}
|
|
6413
|
+
`);
|
|
6414
|
+
process.stdout.write(`Checks: ${report.summary.ok} ok, ${report.summary.warn} warn, ${report.summary.error} error
|
|
6415
|
+
`);
|
|
6416
|
+
process.stdout.write(`Proxy apps: ${report.evidence.proxyApps.length}
|
|
6417
|
+
`);
|
|
6418
|
+
if (report.checks.some((check) => check.level !== "ok")) {
|
|
6419
|
+
process.stdout.write("\nFindings:\n");
|
|
6420
|
+
for (const check of report.checks.filter((entry) => entry.level !== "ok")) {
|
|
6421
|
+
process.stdout.write(`- ${check.id} [${check.level}]: ${check.summary}
|
|
4433
6422
|
`);
|
|
4434
|
-
return;
|
|
4435
6423
|
}
|
|
4436
|
-
|
|
6424
|
+
}
|
|
6425
|
+
if (report.nextSteps.length > 0) {
|
|
6426
|
+
process.stdout.write("\nNext steps:\n");
|
|
6427
|
+
for (const step of report.nextSteps) {
|
|
6428
|
+
process.stdout.write(`- ${step}
|
|
4437
6429
|
`);
|
|
6430
|
+
}
|
|
4438
6431
|
}
|
|
4439
6432
|
}
|
|
4440
|
-
var
|
|
4441
|
-
"src/commands/repo-
|
|
6433
|
+
var init_repo_devcontainer = __esm({
|
|
6434
|
+
"src/commands/repo-devcontainer.ts"() {
|
|
4442
6435
|
"use strict";
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
6436
|
+
init_output();
|
|
6437
|
+
init_devcontainer_write();
|
|
6438
|
+
init_devcontainer_verify();
|
|
4446
6439
|
}
|
|
4447
6440
|
});
|
|
4448
6441
|
|
|
@@ -4532,7 +6525,7 @@ function sanitizeRouterId(value) {
|
|
|
4532
6525
|
return value.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
4533
6526
|
}
|
|
4534
6527
|
function repoHash(repoPath) {
|
|
4535
|
-
return (0, import_node_crypto2.createHash)("sha1").update(
|
|
6528
|
+
return (0, import_node_crypto2.createHash)("sha1").update(import_node_path15.default.resolve(repoPath)).digest("hex").slice(0, 12);
|
|
4536
6529
|
}
|
|
4537
6530
|
function asDockerApp(app) {
|
|
4538
6531
|
return app.runtime === "docker";
|
|
@@ -4611,25 +6604,25 @@ function prepareDockerOverlay(repoPath, appName, apps, publishTcpPorts = false)
|
|
|
4611
6604
|
if (dockerApps.length === 0) {
|
|
4612
6605
|
throw new Error("No docker apps selected to prepare compose overlay.");
|
|
4613
6606
|
}
|
|
4614
|
-
const cachePath =
|
|
4615
|
-
|
|
4616
|
-
const overlayPath =
|
|
6607
|
+
const cachePath = import_node_path15.default.join(CACHE_DIR, repoHash(repoPath), sanitizeRouterId(appName));
|
|
6608
|
+
import_node_fs16.default.mkdirSync(cachePath, { recursive: true });
|
|
6609
|
+
const overlayPath = import_node_path15.default.join(cachePath, "compose.devrouter.yml");
|
|
4617
6610
|
const overlayDocument = buildOverlayDocument(dockerApps, publishTcpPorts);
|
|
4618
|
-
|
|
6611
|
+
import_node_fs16.default.writeFileSync(overlayPath, import_yaml6.default.stringify(overlayDocument, { lineWidth: 0 }), "utf-8");
|
|
4619
6612
|
return {
|
|
4620
6613
|
overlayPath,
|
|
4621
6614
|
composeFiles: ensureComposeFiles(dockerApps),
|
|
4622
6615
|
dockerApps
|
|
4623
6616
|
};
|
|
4624
6617
|
}
|
|
4625
|
-
function runDockerComposeUp(repoPath,
|
|
6618
|
+
function runDockerComposeUp(repoPath, composeFiles2, overlayPath, services) {
|
|
4626
6619
|
const fileArgs = [];
|
|
4627
|
-
for (const composeFile of
|
|
6620
|
+
for (const composeFile of composeFiles2) {
|
|
4628
6621
|
const resolved = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
|
|
4629
6622
|
fileArgs.push("-f", resolved);
|
|
4630
6623
|
}
|
|
4631
6624
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "up", "-d", "--wait", ...services];
|
|
4632
|
-
const result = (0,
|
|
6625
|
+
const result = (0, import_node_child_process10.spawnSync)("docker", args, {
|
|
4633
6626
|
encoding: "utf-8",
|
|
4634
6627
|
cwd: repoPath
|
|
4635
6628
|
});
|
|
@@ -4638,9 +6631,9 @@ function runDockerComposeUp(repoPath, composeFiles, overlayPath, services) {
|
|
|
4638
6631
|
throw new Error(`docker compose up failed: ${withDockerFailureGuidance(details || "unknown error")}`);
|
|
4639
6632
|
}
|
|
4640
6633
|
}
|
|
4641
|
-
function queryRunningComposeServices(repoPath,
|
|
6634
|
+
function queryRunningComposeServices(repoPath, composeFiles2, overlayPath, services) {
|
|
4642
6635
|
const fileArgs = [];
|
|
4643
|
-
for (const composeFile of
|
|
6636
|
+
for (const composeFile of composeFiles2) {
|
|
4644
6637
|
const resolved = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
|
|
4645
6638
|
fileArgs.push("-f", resolved);
|
|
4646
6639
|
}
|
|
@@ -4655,7 +6648,7 @@ function queryRunningComposeServices(repoPath, composeFiles, overlayPath, servic
|
|
|
4655
6648
|
"--services",
|
|
4656
6649
|
...services
|
|
4657
6650
|
];
|
|
4658
|
-
const result = (0,
|
|
6651
|
+
const result = (0, import_node_child_process10.spawnSync)("docker", args, {
|
|
4659
6652
|
encoding: "utf-8",
|
|
4660
6653
|
cwd: repoPath
|
|
4661
6654
|
});
|
|
@@ -4674,14 +6667,14 @@ function queryRunningComposeServices(repoPath, composeFiles, overlayPath, servic
|
|
|
4674
6667
|
runningServices
|
|
4675
6668
|
};
|
|
4676
6669
|
}
|
|
4677
|
-
function runDockerComposeStop(repoPath,
|
|
6670
|
+
function runDockerComposeStop(repoPath, composeFiles2, overlayPath, services) {
|
|
4678
6671
|
const fileArgs = [];
|
|
4679
|
-
for (const composeFile of
|
|
6672
|
+
for (const composeFile of composeFiles2) {
|
|
4680
6673
|
const resolved = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
|
|
4681
6674
|
fileArgs.push("-f", resolved);
|
|
4682
6675
|
}
|
|
4683
6676
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "stop", ...services];
|
|
4684
|
-
const result = (0,
|
|
6677
|
+
const result = (0, import_node_child_process10.spawnSync)("docker", args, {
|
|
4685
6678
|
encoding: "utf-8",
|
|
4686
6679
|
cwd: repoPath
|
|
4687
6680
|
});
|
|
@@ -4691,26 +6684,26 @@ function runDockerComposeStop(repoPath, composeFiles, overlayPath, services) {
|
|
|
4691
6684
|
`);
|
|
4692
6685
|
}
|
|
4693
6686
|
}
|
|
4694
|
-
function runDockerComposeLogs(repoPath,
|
|
6687
|
+
function runDockerComposeLogs(repoPath, composeFiles2, overlayPath, services, tail = 20) {
|
|
4695
6688
|
const fileArgs = [];
|
|
4696
|
-
for (const composeFile of
|
|
6689
|
+
for (const composeFile of composeFiles2) {
|
|
4697
6690
|
const resolved = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
|
|
4698
6691
|
fileArgs.push("-f", resolved);
|
|
4699
6692
|
}
|
|
4700
6693
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "logs", "--tail", String(tail), ...services];
|
|
4701
|
-
(0,
|
|
6694
|
+
(0, import_node_child_process10.spawnSync)("docker", args, {
|
|
4702
6695
|
stdio: "inherit",
|
|
4703
6696
|
cwd: repoPath
|
|
4704
6697
|
});
|
|
4705
6698
|
}
|
|
4706
|
-
function queryMappedPort(repoPath,
|
|
6699
|
+
function queryMappedPort(repoPath, composeFiles2, overlayPath, service, internalPort) {
|
|
4707
6700
|
const fileArgs = [];
|
|
4708
|
-
for (const composeFile of
|
|
6701
|
+
for (const composeFile of composeFiles2) {
|
|
4709
6702
|
const resolved = assertPathWithinRepo(composeFile, repoPath, "composeFiles");
|
|
4710
6703
|
fileArgs.push("-f", resolved);
|
|
4711
6704
|
}
|
|
4712
6705
|
const args = ["compose", ...fileArgs, "-f", overlayPath, "port", service, String(internalPort)];
|
|
4713
|
-
const result = (0,
|
|
6706
|
+
const result = (0, import_node_child_process10.spawnSync)("docker", args, {
|
|
4714
6707
|
encoding: "utf-8",
|
|
4715
6708
|
cwd: repoPath
|
|
4716
6709
|
});
|
|
@@ -4724,21 +6717,154 @@ function queryMappedPort(repoPath, composeFiles, overlayPath, service, internalP
|
|
|
4724
6717
|
const port = Number(match[1]);
|
|
4725
6718
|
return Number.isInteger(port) && port > 0 ? port : void 0;
|
|
4726
6719
|
}
|
|
4727
|
-
var
|
|
6720
|
+
var import_node_fs16, import_node_path15, import_node_crypto2, import_node_child_process10, import_yaml6;
|
|
4728
6721
|
var init_docker_run = __esm({
|
|
4729
6722
|
"src/core/docker-run.ts"() {
|
|
4730
6723
|
"use strict";
|
|
4731
|
-
|
|
4732
|
-
|
|
6724
|
+
import_node_fs16 = __toESM(require("fs"));
|
|
6725
|
+
import_node_path15 = __toESM(require("path"));
|
|
4733
6726
|
import_node_crypto2 = require("crypto");
|
|
4734
|
-
|
|
4735
|
-
|
|
6727
|
+
import_node_child_process10 = require("child_process");
|
|
6728
|
+
import_yaml6 = __toESM(require("yaml"));
|
|
4736
6729
|
init_router();
|
|
4737
6730
|
init_paths();
|
|
4738
6731
|
init_docker_error_guidance();
|
|
4739
6732
|
}
|
|
4740
6733
|
});
|
|
4741
6734
|
|
|
6735
|
+
// src/core/dependency-runtime-plan.ts
|
|
6736
|
+
function uniqueApps(apps) {
|
|
6737
|
+
const byName = /* @__PURE__ */ new Map();
|
|
6738
|
+
for (const app of apps) {
|
|
6739
|
+
byName.set(app.name, app);
|
|
6740
|
+
}
|
|
6741
|
+
return Array.from(byName.values());
|
|
6742
|
+
}
|
|
6743
|
+
function dependencyNames(apps) {
|
|
6744
|
+
return apps.map((entry) => entry.name).sort();
|
|
6745
|
+
}
|
|
6746
|
+
function planDependencyRuntime(options) {
|
|
6747
|
+
const selectedApps = uniqueApps([options.app, ...options.dependencies]);
|
|
6748
|
+
const selectedDockerApps = selectedApps.filter(
|
|
6749
|
+
(entry) => entry.runtime === "docker"
|
|
6750
|
+
);
|
|
6751
|
+
const services = selectedDockerApps.map((entry) => entry.docker.service);
|
|
6752
|
+
const dependencyServices = options.dependencies.filter((entry) => entry.runtime === "docker").map((entry) => entry.docker.service);
|
|
6753
|
+
const runningServices = options.runningServicesBefore?.status === "known" ? options.runningServicesBefore.runningServices : void 0;
|
|
6754
|
+
const allDependencyServicesRunning = runningServices !== void 0 && dependencyServices.every((service) => runningServices.has(service));
|
|
6755
|
+
const hasTcpDeps = options.app.runtime === "host" && selectedDockerApps.some(
|
|
6756
|
+
(entry) => entry.kind !== "dependency" && entry.protocol === "tcp"
|
|
6757
|
+
);
|
|
6758
|
+
return {
|
|
6759
|
+
app: options.app,
|
|
6760
|
+
dependencies: options.dependencies,
|
|
6761
|
+
selectedApps,
|
|
6762
|
+
selectedDockerApps,
|
|
6763
|
+
services,
|
|
6764
|
+
dependencyServices,
|
|
6765
|
+
stopPolicy: options.stopPolicy ?? "always-stop-selected",
|
|
6766
|
+
runningServicesBefore: options.runningServicesBefore,
|
|
6767
|
+
allDependencyServicesRunning,
|
|
6768
|
+
shouldPromptForDependencies: options.dependencies.length > 0 && !allDependencyServicesRunning,
|
|
6769
|
+
hasTcpDeps
|
|
6770
|
+
};
|
|
6771
|
+
}
|
|
6772
|
+
function planDependencyStart(runtimePlan, startDependencies) {
|
|
6773
|
+
const shouldRunComposeUp = runtimePlan.app.runtime === "docker" || startDependencies;
|
|
6774
|
+
if (!shouldRunComposeUp) {
|
|
6775
|
+
return {
|
|
6776
|
+
shouldRunComposeUp,
|
|
6777
|
+
startedServices: [],
|
|
6778
|
+
dependencyApps: []
|
|
6779
|
+
};
|
|
6780
|
+
}
|
|
6781
|
+
if (runtimePlan.stopPolicy === "stop-only-newly-started") {
|
|
6782
|
+
if (runtimePlan.runningServicesBefore?.status === "unknown") {
|
|
6783
|
+
return {
|
|
6784
|
+
shouldRunComposeUp,
|
|
6785
|
+
startedServices: [],
|
|
6786
|
+
dependencyApps: startDependencies ? dependencyNames(runtimePlan.dependencies) : [],
|
|
6787
|
+
ownershipWarning: `unable to determine which dependencies were already running before 'dev app exec'; leaving dependencies running after command exit to avoid stopping non-owned services. Details: ${runtimePlan.runningServicesBefore.reason}`
|
|
6788
|
+
};
|
|
6789
|
+
}
|
|
6790
|
+
const beforeSet2 = runtimePlan.runningServicesBefore?.status === "known" ? runtimePlan.runningServicesBefore.runningServices : void 0;
|
|
6791
|
+
return {
|
|
6792
|
+
shouldRunComposeUp,
|
|
6793
|
+
startedServices: beforeSet2 ? runtimePlan.services.filter((service) => !beforeSet2.has(service)) : [],
|
|
6794
|
+
dependencyApps: startDependencies ? dependencyNames(runtimePlan.dependencies) : []
|
|
6795
|
+
};
|
|
6796
|
+
}
|
|
6797
|
+
const beforeSet = runtimePlan.app.runtime === "docker" && runtimePlan.runningServicesBefore?.status === "known" ? runtimePlan.runningServicesBefore.runningServices : void 0;
|
|
6798
|
+
return {
|
|
6799
|
+
shouldRunComposeUp,
|
|
6800
|
+
startedServices: runtimePlan.app.runtime === "docker" ? runtimePlan.services.filter((service) => !beforeSet?.has(service)) : [...runtimePlan.services],
|
|
6801
|
+
dependencyApps: startDependencies ? dependencyNames(runtimePlan.dependencies) : []
|
|
6802
|
+
};
|
|
6803
|
+
}
|
|
6804
|
+
function buildTcpDepUrl(tcpProtocol, port) {
|
|
6805
|
+
switch (tcpProtocol) {
|
|
6806
|
+
case "postgres":
|
|
6807
|
+
return buildPostgresDependencyUrl(port);
|
|
6808
|
+
case "redis":
|
|
6809
|
+
return `redis://localhost:${port}`;
|
|
6810
|
+
case "mysql":
|
|
6811
|
+
case "mariadb":
|
|
6812
|
+
return `mysql://root@localhost:${port}`;
|
|
6813
|
+
default:
|
|
6814
|
+
return void 0;
|
|
6815
|
+
}
|
|
6816
|
+
}
|
|
6817
|
+
function buildTcpDepShadowUrl(tcpProtocol, port) {
|
|
6818
|
+
if (tcpProtocol === "postgres") {
|
|
6819
|
+
return buildPostgresDependencyShadowUrl(port);
|
|
6820
|
+
}
|
|
6821
|
+
return void 0;
|
|
6822
|
+
}
|
|
6823
|
+
function buildDependencyEnv(mappedDeps) {
|
|
6824
|
+
const depEnv = {};
|
|
6825
|
+
const [hostSuffix, portSuffix, urlSuffix, shadowUrlSuffix] = DEP_ENV_SUFFIXES;
|
|
6826
|
+
for (const { app, mappedPort } of mappedDeps) {
|
|
6827
|
+
if (mappedPort === void 0) {
|
|
6828
|
+
continue;
|
|
6829
|
+
}
|
|
6830
|
+
const envPrefix = app.name.toUpperCase().replace(/-/g, "_");
|
|
6831
|
+
depEnv[`${envPrefix}_${hostSuffix}`] = "localhost";
|
|
6832
|
+
depEnv[`${envPrefix}_${portSuffix}`] = String(mappedPort);
|
|
6833
|
+
const url = buildTcpDepUrl(app.tcpProtocol, mappedPort);
|
|
6834
|
+
if (url) {
|
|
6835
|
+
depEnv[`${envPrefix}_${urlSuffix}`] = url;
|
|
6836
|
+
}
|
|
6837
|
+
const shadowUrl = buildTcpDepShadowUrl(app.tcpProtocol, mappedPort);
|
|
6838
|
+
if (shadowUrl) {
|
|
6839
|
+
depEnv[`${envPrefix}_${shadowUrlSuffix}`] = shadowUrl;
|
|
6840
|
+
}
|
|
6841
|
+
}
|
|
6842
|
+
return depEnv;
|
|
6843
|
+
}
|
|
6844
|
+
function applyDependencyEnvMap(app, depEnv) {
|
|
6845
|
+
const mappedEnv = { ...depEnv };
|
|
6846
|
+
for (const depRef of app.dependencies) {
|
|
6847
|
+
if (!depRef.envMap) {
|
|
6848
|
+
continue;
|
|
6849
|
+
}
|
|
6850
|
+
for (const [target, source] of Object.entries(depRef.envMap)) {
|
|
6851
|
+
if (!(source in mappedEnv)) {
|
|
6852
|
+
throw new Error(
|
|
6853
|
+
`envMap on dependency '${depRef.app}': source variable '${source}' not found in dependency env. Available: ${Object.keys(mappedEnv).join(", ") || "(none)"}`
|
|
6854
|
+
);
|
|
6855
|
+
}
|
|
6856
|
+
mappedEnv[target] = mappedEnv[source];
|
|
6857
|
+
}
|
|
6858
|
+
}
|
|
6859
|
+
return mappedEnv;
|
|
6860
|
+
}
|
|
6861
|
+
var init_dependency_runtime_plan = __esm({
|
|
6862
|
+
"src/core/dependency-runtime-plan.ts"() {
|
|
6863
|
+
"use strict";
|
|
6864
|
+
init_capabilities();
|
|
6865
|
+
}
|
|
6866
|
+
});
|
|
6867
|
+
|
|
4742
6868
|
// src/core/app-run.ts
|
|
4743
6869
|
function wrapWithSecretManager(smCommand, reinjectEnv, userCommand, shell) {
|
|
4744
6870
|
const envPairs = Object.entries(reinjectEnv).map(([k, v]) => `${k}=${v}`);
|
|
@@ -4779,6 +6905,21 @@ function toError(error) {
|
|
|
4779
6905
|
}
|
|
4780
6906
|
return new Error(String(error));
|
|
4781
6907
|
}
|
|
6908
|
+
function dependencyNames2(apps) {
|
|
6909
|
+
return apps.map((entry) => entry.name).sort();
|
|
6910
|
+
}
|
|
6911
|
+
function observedRuntimeServices(result) {
|
|
6912
|
+
if (!result) {
|
|
6913
|
+
return void 0;
|
|
6914
|
+
}
|
|
6915
|
+
if (result.status === "known") {
|
|
6916
|
+
return { status: "known", runningServices: result.runningServices };
|
|
6917
|
+
}
|
|
6918
|
+
return { status: "unknown", reason: result.reason };
|
|
6919
|
+
}
|
|
6920
|
+
function isDependencyOnlyApp(app) {
|
|
6921
|
+
return app.kind === "dependency";
|
|
6922
|
+
}
|
|
4782
6923
|
function normalizeProcessEnv(env) {
|
|
4783
6924
|
const normalized = {};
|
|
4784
6925
|
for (const [key, value] of Object.entries(env)) {
|
|
@@ -4794,25 +6935,6 @@ function buildExecEnvironment(depEnv, processEnv = process.env) {
|
|
|
4794
6935
|
...depEnv
|
|
4795
6936
|
};
|
|
4796
6937
|
}
|
|
4797
|
-
function buildTcpDepUrl(tcpProtocol, port) {
|
|
4798
|
-
switch (tcpProtocol) {
|
|
4799
|
-
case "postgres":
|
|
4800
|
-
return `postgres://prisma:prisma@localhost:${port}/prisma`;
|
|
4801
|
-
case "redis":
|
|
4802
|
-
return `redis://localhost:${port}`;
|
|
4803
|
-
case "mysql":
|
|
4804
|
-
case "mariadb":
|
|
4805
|
-
return `mysql://root@localhost:${port}`;
|
|
4806
|
-
default:
|
|
4807
|
-
return void 0;
|
|
4808
|
-
}
|
|
4809
|
-
}
|
|
4810
|
-
function buildTcpDepShadowUrl(tcpProtocol, port) {
|
|
4811
|
-
if (tcpProtocol === "postgres") {
|
|
4812
|
-
return `postgres://prisma:prisma@localhost:${port}/shadow`;
|
|
4813
|
-
}
|
|
4814
|
-
return void 0;
|
|
4815
|
-
}
|
|
4816
6938
|
function isProcessRunning(pid) {
|
|
4817
6939
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
4818
6940
|
return false;
|
|
@@ -4825,7 +6947,7 @@ function isProcessRunning(pid) {
|
|
|
4825
6947
|
}
|
|
4826
6948
|
}
|
|
4827
6949
|
function readProcessTree(rootPid) {
|
|
4828
|
-
const result = (0,
|
|
6950
|
+
const result = (0, import_node_child_process11.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf-8" });
|
|
4829
6951
|
if (result.status !== 0) {
|
|
4830
6952
|
return [rootPid];
|
|
4831
6953
|
}
|
|
@@ -4907,7 +7029,7 @@ function detectListeningPorts(pids) {
|
|
|
4907
7029
|
if (pids.length === 0) {
|
|
4908
7030
|
return [];
|
|
4909
7031
|
}
|
|
4910
|
-
const result = (0,
|
|
7032
|
+
const result = (0, import_node_child_process11.spawnSync)(
|
|
4911
7033
|
"lsof",
|
|
4912
7034
|
["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", pids.join(",")],
|
|
4913
7035
|
{ encoding: "utf-8" }
|
|
@@ -4966,7 +7088,7 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
|
|
|
4966
7088
|
app.hostRun.command,
|
|
4967
7089
|
true
|
|
4968
7090
|
) : app.hostRun.command;
|
|
4969
|
-
const child = (0,
|
|
7091
|
+
const child = (0, import_node_child_process11.spawn)(spawnCommand, {
|
|
4970
7092
|
cwd: commandCwd,
|
|
4971
7093
|
stdio: "inherit",
|
|
4972
7094
|
shell: true,
|
|
@@ -5049,19 +7171,6 @@ async function runHostApp(repoPath, app, extraEnv = {}, secretManager, env, work
|
|
|
5049
7171
|
throw new Error(`Host command for '${app.name}' exited with code ${exit.code}.`);
|
|
5050
7172
|
}
|
|
5051
7173
|
}
|
|
5052
|
-
function uniqueApps(apps) {
|
|
5053
|
-
const byName = /* @__PURE__ */ new Map();
|
|
5054
|
-
for (const app of apps) {
|
|
5055
|
-
byName.set(app.name, app);
|
|
5056
|
-
}
|
|
5057
|
-
return Array.from(byName.values());
|
|
5058
|
-
}
|
|
5059
|
-
function dependencyNames(apps) {
|
|
5060
|
-
return apps.map((entry) => entry.name).sort();
|
|
5061
|
-
}
|
|
5062
|
-
function isDependencyOnlyApp(app) {
|
|
5063
|
-
return app.kind === "dependency";
|
|
5064
|
-
}
|
|
5065
7174
|
async function shouldStartDependencies(appName, dependencies, yes) {
|
|
5066
7175
|
if (dependencies.length === 0) {
|
|
5067
7176
|
return false;
|
|
@@ -5071,7 +7180,7 @@ async function shouldStartDependencies(appName, dependencies, yes) {
|
|
|
5071
7180
|
}
|
|
5072
7181
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
5073
7182
|
throw new Error(
|
|
5074
|
-
`App '${appName}' has dependencies (${
|
|
7183
|
+
`App '${appName}' has dependencies (${dependencyNames2(
|
|
5075
7184
|
dependencies
|
|
5076
7185
|
).join(", ")}). Re-run with --yes in non-interactive mode.`
|
|
5077
7186
|
);
|
|
@@ -5079,7 +7188,7 @@ async function shouldStartDependencies(appName, dependencies, yes) {
|
|
|
5079
7188
|
const rl = (0, import_promises2.createInterface)({ input: import_node_process2.stdin, output: import_node_process2.stdout });
|
|
5080
7189
|
try {
|
|
5081
7190
|
const answer = await rl.question(
|
|
5082
|
-
`Start dependencies for '${appName}' (${
|
|
7191
|
+
`Start dependencies for '${appName}' (${dependencyNames2(dependencies).join(", ")})? [y/N] `
|
|
5083
7192
|
);
|
|
5084
7193
|
return /^y(es)?$/i.test(answer.trim());
|
|
5085
7194
|
} finally {
|
|
@@ -5110,74 +7219,53 @@ async function startAppDependencies(options) {
|
|
|
5110
7219
|
const unsupportedDependencies = dependencies.filter((entry) => entry.runtime !== "docker");
|
|
5111
7220
|
if (unsupportedDependencies.length > 0) {
|
|
5112
7221
|
throw new Error(
|
|
5113
|
-
`App '${app.name}' has host-runtime dependencies (${
|
|
7222
|
+
`App '${app.name}' has host-runtime dependencies (${dependencyNames2(
|
|
5114
7223
|
unsupportedDependencies
|
|
5115
7224
|
).join(", ")}). v1 only auto-starts docker dependencies. Start host dependencies manually before running this app.`
|
|
5116
7225
|
);
|
|
5117
7226
|
}
|
|
5118
|
-
const selectedApps = uniqueApps([app, ...dependencies]);
|
|
5119
|
-
const selectedDockerApps = selectedApps.filter(
|
|
5120
|
-
(entry) => entry.runtime === "docker"
|
|
5121
|
-
);
|
|
5122
7227
|
const stopPolicy = options.stopPolicy ?? "always-stop-selected";
|
|
5123
7228
|
const startedServices = [];
|
|
5124
7229
|
let overlay;
|
|
5125
7230
|
let startDependencies = false;
|
|
5126
|
-
const
|
|
5127
|
-
|
|
5128
|
-
);
|
|
5129
|
-
if (selectedDockerApps.length > 0) {
|
|
5130
|
-
overlay = prepareDockerOverlay(repoPath, app.name, selectedDockerApps, hasTcpDeps);
|
|
5131
|
-
const
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
7231
|
+
const basePlan = planDependencyRuntime({ app, dependencies, stopPolicy });
|
|
7232
|
+
let observedPlan = basePlan;
|
|
7233
|
+
let startPlan = planDependencyStart(observedPlan, false);
|
|
7234
|
+
if (basePlan.selectedDockerApps.length > 0) {
|
|
7235
|
+
overlay = prepareDockerOverlay(repoPath, app.name, basePlan.selectedDockerApps, basePlan.hasTcpDeps);
|
|
7236
|
+
const preRunResult = basePlan.dependencyServices.length > 0 ? queryRunningComposeServices(repoPath, overlay.composeFiles, overlay.overlayPath, basePlan.dependencyServices) : void 0;
|
|
7237
|
+
observedPlan = planDependencyRuntime({
|
|
7238
|
+
app,
|
|
7239
|
+
dependencies,
|
|
7240
|
+
stopPolicy,
|
|
7241
|
+
runningServicesBefore: observedRuntimeServices(preRunResult)
|
|
7242
|
+
});
|
|
7243
|
+
if (observedPlan.allDependencyServicesRunning) {
|
|
5138
7244
|
startDependencies = false;
|
|
5139
|
-
} else if (
|
|
7245
|
+
} else if (observedPlan.shouldPromptForDependencies) {
|
|
5140
7246
|
startDependencies = await shouldStartDependencies(
|
|
5141
7247
|
app.name,
|
|
5142
7248
|
dependencies,
|
|
5143
7249
|
Boolean(options.yes)
|
|
5144
7250
|
);
|
|
5145
|
-
} else {
|
|
5146
|
-
startDependencies = false;
|
|
5147
7251
|
}
|
|
5148
|
-
|
|
5149
|
-
if (
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
`
|
|
5158
|
-
);
|
|
5159
|
-
}
|
|
5160
|
-
}
|
|
5161
|
-
runDockerComposeUp(repoPath, overlay.composeFiles, overlay.overlayPath, services);
|
|
5162
|
-
if (stopPolicy === "stop-only-newly-started") {
|
|
5163
|
-
const beforeSet = runningServicesBefore;
|
|
5164
|
-
if (ownershipKnown && beforeSet) {
|
|
5165
|
-
startedServices.push(...services.filter((service) => !beforeSet.has(service)));
|
|
5166
|
-
}
|
|
5167
|
-
} else if (app.runtime === "docker") {
|
|
5168
|
-
const beforeSet = preRunResult?.status === "known" ? preRunResult.runningServices : void 0;
|
|
5169
|
-
startedServices.push(...services.filter((service) => !beforeSet?.has(service)));
|
|
5170
|
-
} else {
|
|
5171
|
-
startedServices.push(...services);
|
|
5172
|
-
}
|
|
5173
|
-
runDockerComposeLogs(repoPath, overlay.composeFiles, overlay.overlayPath, services);
|
|
7252
|
+
startPlan = planDependencyStart(observedPlan, startDependencies);
|
|
7253
|
+
if (startPlan.ownershipWarning) {
|
|
7254
|
+
process.stderr.write(`Warning: ${startPlan.ownershipWarning}
|
|
7255
|
+
`);
|
|
7256
|
+
}
|
|
7257
|
+
if (startPlan.shouldRunComposeUp) {
|
|
7258
|
+
runDockerComposeUp(repoPath, overlay.composeFiles, overlay.overlayPath, observedPlan.services);
|
|
7259
|
+
startedServices.push(...startPlan.startedServices);
|
|
7260
|
+
runDockerComposeLogs(repoPath, overlay.composeFiles, overlay.overlayPath, observedPlan.services);
|
|
5174
7261
|
}
|
|
5175
7262
|
}
|
|
5176
|
-
|
|
5177
|
-
if (hasTcpDeps && overlay) {
|
|
5178
|
-
const tcpDeps = selectedDockerApps.filter(
|
|
7263
|
+
let depEnv = {};
|
|
7264
|
+
if (observedPlan.hasTcpDeps && overlay) {
|
|
7265
|
+
const tcpDeps = observedPlan.selectedDockerApps.filter(
|
|
5179
7266
|
(entry) => entry.kind !== "dependency" && entry.protocol === "tcp"
|
|
5180
7267
|
);
|
|
7268
|
+
const mappedDeps = [];
|
|
5181
7269
|
let routerRestarted = false;
|
|
5182
7270
|
for (const dep of tcpDeps) {
|
|
5183
7271
|
const needsRestart = activateTcpProtocol(dep.tcpProtocol);
|
|
@@ -5196,18 +7284,11 @@ async function startAppDependencies(options) {
|
|
|
5196
7284
|
dep.docker.service,
|
|
5197
7285
|
dep.docker.internalPort
|
|
5198
7286
|
);
|
|
7287
|
+
mappedDeps.push({ app: dep, mappedPort });
|
|
5199
7288
|
if (mappedPort !== void 0) {
|
|
5200
7289
|
const envPrefix = dep.name.toUpperCase().replace(/-/g, "_");
|
|
5201
|
-
depEnv[`${envPrefix}_HOST`] = "localhost";
|
|
5202
|
-
depEnv[`${envPrefix}_PORT`] = String(mappedPort);
|
|
5203
7290
|
const url = buildTcpDepUrl(dep.tcpProtocol, mappedPort);
|
|
5204
|
-
if (url) {
|
|
5205
|
-
depEnv[`${envPrefix}_URL`] = url;
|
|
5206
|
-
}
|
|
5207
7291
|
const shadowUrl = buildTcpDepShadowUrl(dep.tcpProtocol, mappedPort);
|
|
5208
|
-
if (shadowUrl) {
|
|
5209
|
-
depEnv[`${envPrefix}_SHADOW_URL`] = shadowUrl;
|
|
5210
|
-
}
|
|
5211
7292
|
process.stdout.write(`Dependency ${dep.name} available at localhost:${mappedPort}
|
|
5212
7293
|
`);
|
|
5213
7294
|
if (url) {
|
|
@@ -5220,19 +7301,9 @@ async function startAppDependencies(options) {
|
|
|
5220
7301
|
}
|
|
5221
7302
|
}
|
|
5222
7303
|
}
|
|
7304
|
+
depEnv = buildDependencyEnv(mappedDeps);
|
|
5223
7305
|
}
|
|
5224
|
-
|
|
5225
|
-
if (depRef.envMap) {
|
|
5226
|
-
for (const [target, source] of Object.entries(depRef.envMap)) {
|
|
5227
|
-
if (!(source in depEnv)) {
|
|
5228
|
-
throw new Error(
|
|
5229
|
-
`envMap on dependency '${depRef.app}': source variable '${source}' not found in dependency env. Available: ${Object.keys(depEnv).join(", ") || "(none)"}`
|
|
5230
|
-
);
|
|
5231
|
-
}
|
|
5232
|
-
depEnv[target] = depEnv[source];
|
|
5233
|
-
}
|
|
5234
|
-
}
|
|
5235
|
-
}
|
|
7306
|
+
depEnv = applyDependencyEnvMap(app, depEnv);
|
|
5236
7307
|
const stopDeps = () => {
|
|
5237
7308
|
if (startedServices.length > 0 && overlay) {
|
|
5238
7309
|
process.stdout.write(`Stopping dependencies (${startedServices.join(", ")})...
|
|
@@ -5254,12 +7325,12 @@ async function startAppDependencies(options) {
|
|
|
5254
7325
|
secretManager: config.secretManager,
|
|
5255
7326
|
overlay,
|
|
5256
7327
|
startedServices,
|
|
5257
|
-
dependencyApps:
|
|
7328
|
+
dependencyApps: startPlan.dependencyApps,
|
|
5258
7329
|
stopDeps
|
|
5259
7330
|
};
|
|
5260
7331
|
}
|
|
5261
|
-
function
|
|
5262
|
-
const { port, upstreamHost } = parseUpstream(app.upstream);
|
|
7332
|
+
function registerProxyRoute2(repoPath, app, workspace) {
|
|
7333
|
+
const { port, upstreamHost: upstreamHost2 } = parseUpstream(app.upstream);
|
|
5263
7334
|
if (app.protocol === "tcp" && !isTLSEnabled()) {
|
|
5264
7335
|
throw new Error(
|
|
5265
7336
|
`App "${app.name}" is a TCP proxy route, which requires TLS (SNI). Run \`dev tls install\` first.`
|
|
@@ -5273,7 +7344,7 @@ function registerProxyRoute(repoPath, app, workspace) {
|
|
|
5273
7344
|
startRouterStack();
|
|
5274
7345
|
}
|
|
5275
7346
|
}
|
|
5276
|
-
|
|
7347
|
+
removeRouteForApp(repoPath, app.name);
|
|
5277
7348
|
assertAppNotRunning(repoPath, { name: app.name, host: app.host });
|
|
5278
7349
|
upsertHostRoute({
|
|
5279
7350
|
name: app.name,
|
|
@@ -5282,7 +7353,7 @@ function registerProxyRoute(repoPath, app, workspace) {
|
|
|
5282
7353
|
tcpProtocol: app.protocol === "tcp" ? app.tcpProtocol : void 0,
|
|
5283
7354
|
repoPath,
|
|
5284
7355
|
port,
|
|
5285
|
-
upstreamHost,
|
|
7356
|
+
upstreamHost: upstreamHost2,
|
|
5286
7357
|
mode: "proxy",
|
|
5287
7358
|
workspace
|
|
5288
7359
|
});
|
|
@@ -5304,7 +7375,7 @@ async function runConfiguredApp(options) {
|
|
|
5304
7375
|
if (deps.app.runtime === "host") {
|
|
5305
7376
|
await runHostApp(deps.repoPath, deps.app, deps.depEnv, deps.secretManager, options.env, deps.workspace);
|
|
5306
7377
|
} else if (deps.app.runtime === "proxy") {
|
|
5307
|
-
|
|
7378
|
+
registerProxyRoute2(deps.repoPath, deps.app, deps.workspace);
|
|
5308
7379
|
} else if (deps.app.kind !== "dependency" && deps.app.protocol === "tcp") {
|
|
5309
7380
|
const registryEntry = TCP_PROTOCOL_REGISTRY[deps.app.tcpProtocol];
|
|
5310
7381
|
const port = registryEntry?.port ?? "?";
|
|
@@ -5354,7 +7425,7 @@ async function execWithAppEnv(options) {
|
|
|
5354
7425
|
options.command[0],
|
|
5355
7426
|
true
|
|
5356
7427
|
);
|
|
5357
|
-
child = (0,
|
|
7428
|
+
child = (0, import_node_child_process11.spawn)(wrapped, {
|
|
5358
7429
|
cwd: deps.repoPath,
|
|
5359
7430
|
stdio: "inherit",
|
|
5360
7431
|
shell: true,
|
|
@@ -5368,7 +7439,7 @@ async function execWithAppEnv(options) {
|
|
|
5368
7439
|
false
|
|
5369
7440
|
);
|
|
5370
7441
|
const [cmd, ...wrappedArgs] = wrapped;
|
|
5371
|
-
child = (0,
|
|
7442
|
+
child = (0, import_node_child_process11.spawn)(cmd, wrappedArgs, {
|
|
5372
7443
|
cwd: deps.repoPath,
|
|
5373
7444
|
stdio: "inherit",
|
|
5374
7445
|
shell: false,
|
|
@@ -5376,7 +7447,7 @@ async function execWithAppEnv(options) {
|
|
|
5376
7447
|
});
|
|
5377
7448
|
}
|
|
5378
7449
|
} else if (options.shell) {
|
|
5379
|
-
child = (0,
|
|
7450
|
+
child = (0, import_node_child_process11.spawn)(options.command[0], {
|
|
5380
7451
|
cwd: deps.repoPath,
|
|
5381
7452
|
stdio: "inherit",
|
|
5382
7453
|
shell: true,
|
|
@@ -5384,7 +7455,7 @@ async function execWithAppEnv(options) {
|
|
|
5384
7455
|
});
|
|
5385
7456
|
} else {
|
|
5386
7457
|
const [command, ...args] = options.command;
|
|
5387
|
-
child = (0,
|
|
7458
|
+
child = (0, import_node_child_process11.spawn)(command, args, {
|
|
5388
7459
|
cwd: deps.repoPath,
|
|
5389
7460
|
stdio: "inherit",
|
|
5390
7461
|
shell: false,
|
|
@@ -5403,22 +7474,25 @@ async function execWithAppEnv(options) {
|
|
|
5403
7474
|
deps.stopDeps();
|
|
5404
7475
|
}
|
|
5405
7476
|
}
|
|
5406
|
-
var import_node_net, import_promises2,
|
|
7477
|
+
var import_node_net, import_promises2, import_node_child_process11, import_node_process2, POLL_INTERVAL_MS, DEFAULT_PORT_TIMEOUT_MS, PROCESS_TERMINATION_GRACE_MS;
|
|
5407
7478
|
var init_app_run = __esm({
|
|
5408
7479
|
"src/core/app-run.ts"() {
|
|
5409
7480
|
"use strict";
|
|
5410
7481
|
import_node_net = __toESM(require("net"));
|
|
5411
7482
|
import_promises2 = require("readline/promises");
|
|
5412
|
-
|
|
7483
|
+
import_node_child_process11 = require("child_process");
|
|
5413
7484
|
import_node_process2 = require("process");
|
|
5414
7485
|
init_docker_run();
|
|
5415
7486
|
init_repo_config();
|
|
5416
7487
|
init_host_routes();
|
|
5417
7488
|
init_concurrency();
|
|
7489
|
+
init_route_state();
|
|
5418
7490
|
init_docker();
|
|
5419
7491
|
init_router();
|
|
5420
7492
|
init_paths();
|
|
5421
7493
|
init_tls();
|
|
7494
|
+
init_dependency_runtime_plan();
|
|
7495
|
+
init_dependency_runtime_plan();
|
|
5422
7496
|
POLL_INTERVAL_MS = 1e3;
|
|
5423
7497
|
DEFAULT_PORT_TIMEOUT_MS = 12e4;
|
|
5424
7498
|
PROCESS_TERMINATION_GRACE_MS = 3e3;
|
|
@@ -5490,31 +7564,27 @@ __export(app_rm_exports, {
|
|
|
5490
7564
|
async function runAppRmCommand(options) {
|
|
5491
7565
|
const repoPath = resolveRepoPath(options.repo);
|
|
5492
7566
|
if (options.keepConfig) {
|
|
5493
|
-
|
|
5494
|
-
removeHostRouteByName(options.name, repoPath);
|
|
7567
|
+
if (removeRouteForApp(repoPath, options.name).length > 0) {
|
|
5495
7568
|
process.stdout.write(`Freed route for '${options.name}' (config left intact)
|
|
5496
7569
|
`);
|
|
5497
|
-
|
|
5498
|
-
process.stdout.write(`No active route for '${options.name}' (config left intact)
|
|
5499
|
-
`);
|
|
7570
|
+
return;
|
|
5500
7571
|
}
|
|
7572
|
+
process.stdout.write(`No active route for '${options.name}' (config left intact)
|
|
7573
|
+
`);
|
|
5501
7574
|
return;
|
|
5502
7575
|
}
|
|
5503
7576
|
const result = removeRepoApp(repoPath, options.name);
|
|
5504
7577
|
if (!result.removed) {
|
|
5505
7578
|
throw new Error(`App '${options.name}' not found in ${result.configPath}.`);
|
|
5506
7579
|
}
|
|
5507
|
-
|
|
5508
|
-
removeHostRouteByName(options.name, repoPath);
|
|
5509
|
-
} catch {
|
|
5510
|
-
}
|
|
7580
|
+
removeRouteForApp(repoPath, options.name);
|
|
5511
7581
|
process.stdout.write(`Removed '${options.name}' from ${result.configPath}
|
|
5512
7582
|
`);
|
|
5513
7583
|
}
|
|
5514
7584
|
var init_app_rm = __esm({
|
|
5515
7585
|
"src/commands/app-rm.ts"() {
|
|
5516
7586
|
"use strict";
|
|
5517
|
-
|
|
7587
|
+
init_route_state();
|
|
5518
7588
|
init_repo_config();
|
|
5519
7589
|
}
|
|
5520
7590
|
});
|
|
@@ -5541,11 +7611,11 @@ var init_tls2 = __esm({
|
|
|
5541
7611
|
|
|
5542
7612
|
// src/core/workspace-lifecycle.ts
|
|
5543
7613
|
function hasDevpod() {
|
|
5544
|
-
const result = (0,
|
|
7614
|
+
const result = (0, import_node_child_process12.spawnSync)("devpod", ["version"], { encoding: "utf-8" });
|
|
5545
7615
|
return result.status === 0;
|
|
5546
7616
|
}
|
|
5547
7617
|
function listGitWorktrees(repoPath) {
|
|
5548
|
-
const result = (0,
|
|
7618
|
+
const result = (0, import_node_child_process12.spawnSync)("git", ["-C", repoPath, "worktree", "list", "--porcelain"], {
|
|
5549
7619
|
encoding: "utf-8"
|
|
5550
7620
|
});
|
|
5551
7621
|
if (result.status !== 0) {
|
|
@@ -5569,18 +7639,7 @@ function listGitWorktrees(repoPath) {
|
|
|
5569
7639
|
return worktrees;
|
|
5570
7640
|
}
|
|
5571
7641
|
function defaultWorktreePath(mainRepo, ws) {
|
|
5572
|
-
return
|
|
5573
|
-
}
|
|
5574
|
-
function comparablePath(filePath) {
|
|
5575
|
-
const resolved = import_node_path10.default.resolve(filePath);
|
|
5576
|
-
try {
|
|
5577
|
-
return import_node_fs12.default.realpathSync.native(resolved);
|
|
5578
|
-
} catch {
|
|
5579
|
-
return resolved;
|
|
5580
|
-
}
|
|
5581
|
-
}
|
|
5582
|
-
function samePath(left, right) {
|
|
5583
|
-
return comparablePath(left) === comparablePath(right);
|
|
7642
|
+
return import_node_path16.default.join(import_node_path16.default.dirname(mainRepo), `${import_node_path16.default.basename(mainRepo)}-${ws}`);
|
|
5584
7643
|
}
|
|
5585
7644
|
async function workspaceUp(branch, opts = {}) {
|
|
5586
7645
|
const mainRepo = resolveRepoPath(opts.repoPath);
|
|
@@ -5588,16 +7647,16 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
5588
7647
|
if (!ws) {
|
|
5589
7648
|
throw new Error(`Branch '${branch}' does not yield a valid workspace token.`);
|
|
5590
7649
|
}
|
|
5591
|
-
const worktreePath = opts.path ?
|
|
5592
|
-
if (
|
|
7650
|
+
const worktreePath = opts.path ? import_node_path16.default.resolve(opts.path) : defaultWorktreePath(mainRepo, ws);
|
|
7651
|
+
if (import_node_fs17.default.existsSync(worktreePath)) {
|
|
5593
7652
|
process.stdout.write(`Worktree already exists: ${worktreePath}
|
|
5594
7653
|
`);
|
|
5595
7654
|
} else {
|
|
5596
|
-
const add = (0,
|
|
7655
|
+
const add = (0, import_node_child_process12.spawnSync)("git", ["-C", mainRepo, "worktree", "add", worktreePath, branch], {
|
|
5597
7656
|
encoding: "utf-8"
|
|
5598
7657
|
});
|
|
5599
7658
|
if (add.status !== 0) {
|
|
5600
|
-
const addNew = (0,
|
|
7659
|
+
const addNew = (0, import_node_child_process12.spawnSync)("git", ["-C", mainRepo, "worktree", "add", "-b", branch, worktreePath], {
|
|
5601
7660
|
encoding: "utf-8"
|
|
5602
7661
|
});
|
|
5603
7662
|
if (addNew.status !== 0) {
|
|
@@ -5609,7 +7668,7 @@ async function workspaceUp(branch, opts = {}) {
|
|
|
5609
7668
|
`);
|
|
5610
7669
|
}
|
|
5611
7670
|
if (!opts.noDevpod && hasDevpod()) {
|
|
5612
|
-
const dp = (0,
|
|
7671
|
+
const dp = (0, import_node_child_process12.spawnSync)("devpod", ["up", worktreePath, "--id", ws, "--open-ide=false"], {
|
|
5613
7672
|
stdio: "inherit",
|
|
5614
7673
|
env: { ...process.env, WORKSPACE: ws }
|
|
5615
7674
|
});
|
|
@@ -5649,7 +7708,7 @@ ${urls.map((u) => ` ${u}`).join("\n")}
|
|
|
5649
7708
|
}
|
|
5650
7709
|
if (opts.open) {
|
|
5651
7710
|
for (const url of openUrls) {
|
|
5652
|
-
const opened = (0,
|
|
7711
|
+
const opened = (0, import_node_child_process12.spawnSync)("open", [url], { encoding: "utf-8" });
|
|
5653
7712
|
if (opened.status !== 0) {
|
|
5654
7713
|
const detail = [opened.stdout, opened.stderr].filter(Boolean).join("\n").trim();
|
|
5655
7714
|
throw new Error(`Unable to open '${url}': ${detail || "unknown error"}`);
|
|
@@ -5662,10 +7721,10 @@ ${urls.map((u) => ` ${u}`).join("\n")}
|
|
|
5662
7721
|
function workspaceLs(repoPath) {
|
|
5663
7722
|
const mainRepo = resolveRepoPath(repoPath);
|
|
5664
7723
|
const worktrees = listGitWorktrees(mainRepo);
|
|
5665
|
-
const
|
|
7724
|
+
const routesByWorktreePath = listRoutesForWorktreePaths(worktrees.map((worktree) => worktree.path));
|
|
5666
7725
|
return worktrees.map((wt) => {
|
|
5667
7726
|
const workspace = isLinkedWorktree(wt.path) && wt.branch ? wsFromBranch(wt.branch) : void 0;
|
|
5668
|
-
const wsRoutes =
|
|
7727
|
+
const wsRoutes = routesByWorktreePath.get(wt.path) ?? [];
|
|
5669
7728
|
return {
|
|
5670
7729
|
workspace,
|
|
5671
7730
|
branch: wt.branch,
|
|
@@ -5685,20 +7744,15 @@ function workspaceDown(target, opts = {}) {
|
|
|
5685
7744
|
(wt) => wt.branch && wsFromBranch(wt.branch) === ws
|
|
5686
7745
|
);
|
|
5687
7746
|
const worktreePath = match?.path ?? defaultWorktreePath(mainRepo, ws);
|
|
5688
|
-
const routes =
|
|
5689
|
-
(route) => route.workspace === ws && samePath(route.repoPath, worktreePath)
|
|
5690
|
-
);
|
|
5691
|
-
for (const route of routes) {
|
|
5692
|
-
removeHostRouteById(route.id);
|
|
5693
|
-
}
|
|
7747
|
+
const routes = removeWorkspaceRoutesForWorktree(ws, worktreePath);
|
|
5694
7748
|
process.stdout.write(`Freed ${routes.length} route(s) for workspace '${ws}'.
|
|
5695
7749
|
`);
|
|
5696
7750
|
if (!opts.keepDevpod && hasDevpod()) {
|
|
5697
|
-
(0,
|
|
7751
|
+
(0, import_node_child_process12.spawnSync)("devpod", ["stop", ws], { stdio: "inherit" });
|
|
5698
7752
|
}
|
|
5699
7753
|
if (!opts.keepWorktree) {
|
|
5700
|
-
if (
|
|
5701
|
-
const rm = (0,
|
|
7754
|
+
if (import_node_fs17.default.existsSync(worktreePath) && worktreePath !== mainRepo) {
|
|
7755
|
+
const rm = (0, import_node_child_process12.spawnSync)("git", ["-C", mainRepo, "worktree", "remove", worktreePath], {
|
|
5702
7756
|
encoding: "utf-8"
|
|
5703
7757
|
});
|
|
5704
7758
|
if (rm.status === 0) {
|
|
@@ -5714,15 +7768,15 @@ function workspaceDown(target, opts = {}) {
|
|
|
5714
7768
|
}
|
|
5715
7769
|
return { freedRoutes: routes.length, workspace: ws };
|
|
5716
7770
|
}
|
|
5717
|
-
var
|
|
7771
|
+
var import_node_fs17, import_node_path16, import_node_child_process12;
|
|
5718
7772
|
var init_workspace_lifecycle = __esm({
|
|
5719
7773
|
"src/core/workspace-lifecycle.ts"() {
|
|
5720
7774
|
"use strict";
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
7775
|
+
import_node_fs17 = __toESM(require("fs"));
|
|
7776
|
+
import_node_path16 = __toESM(require("path"));
|
|
7777
|
+
import_node_child_process12 = require("child_process");
|
|
5724
7778
|
init_app_run();
|
|
5725
|
-
|
|
7779
|
+
init_route_state();
|
|
5726
7780
|
init_repo_config();
|
|
5727
7781
|
init_workspace();
|
|
5728
7782
|
}
|
|
@@ -5812,12 +7866,12 @@ var init_version = __esm({
|
|
|
5812
7866
|
|
|
5813
7867
|
// src/cli.ts
|
|
5814
7868
|
var import_commander = require("commander");
|
|
5815
|
-
var CLI_VERSION = true ? "0.0.
|
|
7869
|
+
var CLI_VERSION = true ? "0.0.23" : "0.0.0-dev";
|
|
5816
7870
|
var VERSION_FLAGS = /* @__PURE__ */ new Set(["-V", "--version"]);
|
|
5817
|
-
function withErrorHandling(
|
|
7871
|
+
function withErrorHandling(action2) {
|
|
5818
7872
|
return async (...args) => {
|
|
5819
7873
|
try {
|
|
5820
|
-
await
|
|
7874
|
+
await action2(...args);
|
|
5821
7875
|
} catch (error) {
|
|
5822
7876
|
const message = error instanceof Error ? error.message : String(error);
|
|
5823
7877
|
process.stderr.write(`Error: ${message}
|
|
@@ -5837,6 +7891,10 @@ program.command("upgrade").description("Show upgrade targets from .devrouter.yml
|
|
|
5837
7891
|
const { runUpgradeCommand: runUpgradeCommand2 } = await Promise.resolve().then(() => (init_upgrade2(), upgrade_exports));
|
|
5838
7892
|
await runUpgradeCommand2({ targetVersion, repo: options.repo });
|
|
5839
7893
|
}));
|
|
7894
|
+
program.command("setup").description("Run first-time devrouter machine setup and report diagnostics").option("--repo <path>", "Repository path for final diagnostics (defaults to current directory)").option("--yes", "Confirm non-interactive setup actions").option("--json", "Output JSON").action(withErrorHandling(async (options) => {
|
|
7895
|
+
const { runSetupCommand: runSetupCommand2 } = await Promise.resolve().then(() => (init_setup2(), setup_exports));
|
|
7896
|
+
await runSetupCommand2(options);
|
|
7897
|
+
}));
|
|
5840
7898
|
program.command("up").description("Ensure devnet and start shared Traefik (reserves 80/443/5432)").action(withErrorHandling(async () => {
|
|
5841
7899
|
const { runUpCommand: runUpCommand2 } = await Promise.resolve().then(() => (init_up(), up_exports));
|
|
5842
7900
|
await runUpCommand2();
|
|
@@ -5870,10 +7928,23 @@ repoCommand.command("init").description("Initialize `.devrouter.yml` in a reposi
|
|
|
5870
7928
|
const { runRepoInitCommand: runRepoInitCommand2 } = await Promise.resolve().then(() => (init_repo_init(), repo_init_exports));
|
|
5871
7929
|
await runRepoInitCommand2({ ...options, installedVersion: CLI_VERSION });
|
|
5872
7930
|
}));
|
|
7931
|
+
repoCommand.command("inspect").description("Inspect repository stack facts for agent-native onboarding").option("--repo <path>", "Repository path (defaults to current directory)").option("--json", "Output JSON").action(withErrorHandling(async (options) => {
|
|
7932
|
+
const { runRepoInspectCommand: runRepoInspectCommand2 } = await Promise.resolve().then(() => (init_repo_inspect2(), repo_inspect_exports));
|
|
7933
|
+
await runRepoInspectCommand2(options);
|
|
7934
|
+
}));
|
|
5873
7935
|
repoCommand.command("agents").description("Write/update devrouter section in the repo's AGENTS.md").option("--repo <path>", "Repository path (defaults to current directory)").option("--with-linear", "Also install optional Linear workflow skill/assets and AGENTS section").action(withErrorHandling(async (options) => {
|
|
5874
7936
|
const { runRepoAgentsCommand: runRepoAgentsCommand2 } = await Promise.resolve().then(() => (init_repo_agents(), repo_agents_exports));
|
|
5875
7937
|
await runRepoAgentsCommand2(options);
|
|
5876
7938
|
}));
|
|
7939
|
+
var repoDevcontainerCommand = repoCommand.command("devcontainer").description("Manage devcontainer onboarding");
|
|
7940
|
+
repoDevcontainerCommand.command("write").description("Plan or write a conservative devcontainer scaffold").option("--repo <path>", "Repository path (defaults to current directory)").option("--dry-run", "Print the planned file changes without writing").option("--yes", "Write files when no conflicts are detected").option("--json", "Output JSON").action(withErrorHandling(async (options) => {
|
|
7941
|
+
const { runRepoDevcontainerWriteCommand: runRepoDevcontainerWriteCommand2 } = await Promise.resolve().then(() => (init_repo_devcontainer(), repo_devcontainer_exports));
|
|
7942
|
+
await runRepoDevcontainerWriteCommand2({ ...options, installedVersion: CLI_VERSION });
|
|
7943
|
+
}));
|
|
7944
|
+
repoDevcontainerCommand.command("verify").description("Verify devcontainer onboarding state and emit PR evidence").option("--repo <path>", "Repository path (defaults to current directory)").option("--live", "Register proxy routes and probe HTTP routes").option("--yes", "Confirm live verification actions").option("--json", "Output JSON").action(withErrorHandling(async (options) => {
|
|
7945
|
+
const { runRepoDevcontainerVerifyCommand: runRepoDevcontainerVerifyCommand2 } = await Promise.resolve().then(() => (init_repo_devcontainer(), repo_devcontainer_exports));
|
|
7946
|
+
await runRepoDevcontainerVerifyCommand2(options);
|
|
7947
|
+
}));
|
|
5877
7948
|
var appCommand = program.command("app").description("Manage app entries and runtime actions from `.devrouter.yml`");
|
|
5878
7949
|
appCommand.command("add").description("Add or update one app definition in `.devrouter.yml`").requiredOption("--name <name>", "App name").option("--kind <kind>", "app or dependency", "app").option("--host <host>", "Hostname ending with .localhost (required for --kind app)").option("--protocol <protocol>", "http or tcp (required for --kind app)").option("--runtime <runtime>", "host, docker, or proxy (required for --kind app, optional for --kind dependency)").option("--service <service>", "Docker service name (runtime=docker)").option("--port <port>", "Internal port (runtime=docker)", (value) => Number(value)).option("--upstream <host:port>", "Already-running upstream to route to (runtime=proxy)").option("--compose-file <file>", "Compose file path (repeatable)", (value, prev) => {
|
|
5879
7950
|
const next = prev ?? [];
|