@monster-orange/den 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +156 -0
- package/README.md +1 -1
- package/bin/den +3 -2
- package/docs/distributions.md +101 -9
- package/package.json +1 -1
- package/service/install.sh +14 -2
- package/src/shared/den-config.ts +69 -0
- package/src/shared/environment-manager/service-runner.ts +218 -4
- package/src/shared/manifest/placement.ts +15 -3
- package/src/shared/manifest/validate-layers.ts +40 -1
- package/src/shared/stacks/manifest-commands.ts +11 -1
- package/src/shared/stacks/runtime.ts +20 -0
- package/src/ui/cli/flag-parser.ts +2 -1
- package/src/ui/cli/index.ts +238 -45
- package/src/ui/cli/mcp-connect.ts +350 -0
- package/src/ui/cli/named-target.ts +11 -0
- package/src/ui/cli/service-command.ts +131 -0
- package/src/ui/cli/stack-command.ts +9 -0
- package/src/ui/mcp/api.ts +2 -0
- package/src/ui/mcp/connect.ts +304 -0
- package/src/ui/mcp/guards.ts +6 -2
- package/src/ui/mcp/server.ts +84 -1
- package/src/ui/mcp/tools.ts +89 -10
- package/src/ui/server/audit.ts +69 -3
- package/src/ui/server/index.ts +83 -2
- package/src/ui/server/lifecycle-exec.ts +188 -0
- package/src/ui/server/public/graph.css +4 -0
- package/src/ui/server/public/panel.js +6 -3
- package/src/ui/server/readonly-routes.ts +11 -2
- package/src/ui/server/workspace-routes.ts +1 -1
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { hasRuntime, repoKind } from "../repo-kind.js";
|
|
10
10
|
import { effectiveLocalCommands } from "../stacks/manifest-commands.js";
|
|
11
|
+
import { manifestStackName } from "../stacks/config.js";
|
|
11
12
|
import fs from "fs";
|
|
12
13
|
import path from "path";
|
|
13
14
|
import { readManifest } from "../manifest/read-manifest.js";
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
getActiveWorkspaces,
|
|
41
42
|
getFocusedWorkspaceName,
|
|
42
43
|
getWorkspaceByName,
|
|
44
|
+
parseAddress,
|
|
43
45
|
resolveServiceSource,
|
|
44
46
|
listWorkspaceServices,
|
|
45
47
|
} from "./workspaces.js";
|
|
@@ -49,7 +51,12 @@ import {
|
|
|
49
51
|
type ServiceHealthResult,
|
|
50
52
|
} from "./health.js";
|
|
51
53
|
import { EventEmitter } from "events";
|
|
52
|
-
import type {
|
|
54
|
+
import type {
|
|
55
|
+
CommandDef,
|
|
56
|
+
ResolvedService,
|
|
57
|
+
ServiceCatalog,
|
|
58
|
+
WorkspaceConfig,
|
|
59
|
+
} from "../types/index.js";
|
|
53
60
|
import type { ChildProcess } from "child_process";
|
|
54
61
|
import { getStateDir } from "../paths.js";
|
|
55
62
|
|
|
@@ -658,7 +665,95 @@ export function resolveService(
|
|
|
658
665
|
: getActiveWorkspace();
|
|
659
666
|
if (!ws) return null;
|
|
660
667
|
const catalog = readCatalog();
|
|
668
|
+
const listed = resolveListedService(address, ws, catalog);
|
|
669
|
+
if (listed || address.includes("/")) return listed;
|
|
670
|
+
// Not a repo or a listed address: a service key or a stack name, when it
|
|
671
|
+
// names exactly one service in the workspace.
|
|
672
|
+
const candidates = aliasCandidates(address, ws, catalog);
|
|
673
|
+
return candidates.length === 1
|
|
674
|
+
? resolveListedService(candidates[0], ws, catalog)
|
|
675
|
+
: null;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
type NamedWorkspace = { name: string; config: WorkspaceConfig };
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* The addresses `alias` could mean in `ws`: every service whose manifest key
|
|
682
|
+
* is `alias`, and the repo of every compose stack named `alias` (a stack
|
|
683
|
+
* stands for its repo; when that repo also has a service named `alias`, the
|
|
684
|
+
* service is the one match). Sorted; empty for a `repo/service` address and
|
|
685
|
+
* for a catalog repo's name.
|
|
686
|
+
*/
|
|
687
|
+
function aliasCandidates(
|
|
688
|
+
alias: string,
|
|
689
|
+
ws: NamedWorkspace,
|
|
690
|
+
catalog: ServiceCatalog,
|
|
691
|
+
): string[] {
|
|
692
|
+
// A repo name is never an alias, in the workspace or not.
|
|
693
|
+
if (alias.includes("/") || catalog[alias]) return [];
|
|
694
|
+
const services = new Set<string>();
|
|
695
|
+
const stackRepos = new Set<string>();
|
|
696
|
+
for (const s of listWorkspaceServices(ws.config, catalog, ws.name)) {
|
|
697
|
+
if (s.subsystem === alias) services.add(s.address);
|
|
698
|
+
const manifest = readManifest(s.repoPath);
|
|
699
|
+
if (manifest?.compose && manifestStackName(manifest, s.repoName) === alias)
|
|
700
|
+
stackRepos.add(s.repoName);
|
|
701
|
+
}
|
|
702
|
+
const withService = new Set(
|
|
703
|
+
[...services].map((a) => parseAddress(a, catalog).repoName),
|
|
704
|
+
);
|
|
705
|
+
return [
|
|
706
|
+
...services,
|
|
707
|
+
...[...stackRepos].filter((r) => !withService.has(r)),
|
|
708
|
+
].sort();
|
|
709
|
+
}
|
|
661
710
|
|
|
711
|
+
function namedWorkspace(workspaceName?: string): NamedWorkspace | null {
|
|
712
|
+
return workspaceName
|
|
713
|
+
? getWorkspaceByName(workspaceName)
|
|
714
|
+
: getActiveWorkspace();
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* What `alias` could address in the workspace (the focused one by default)
|
|
719
|
+
* when it is not a repo or a listed address: see resolveService.
|
|
720
|
+
*/
|
|
721
|
+
export function serviceAliasCandidates(
|
|
722
|
+
alias: string,
|
|
723
|
+
workspaceName?: string,
|
|
724
|
+
): string[] {
|
|
725
|
+
const ws = namedWorkspace(workspaceName);
|
|
726
|
+
if (!ws) return [];
|
|
727
|
+
const catalog = readCatalog();
|
|
728
|
+
if (resolveListedService(alias, ws, catalog)) return [];
|
|
729
|
+
return aliasCandidates(alias, ws, catalog);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* The address to use for what someone typed: an unambiguous alias becomes
|
|
734
|
+
* the service's own address; anything else is returned as is. An ambiguous
|
|
735
|
+
* alias throws, naming the candidates.
|
|
736
|
+
*/
|
|
737
|
+
export function canonicalServiceAddress(
|
|
738
|
+
address: string,
|
|
739
|
+
workspaceName?: string,
|
|
740
|
+
): string {
|
|
741
|
+
const candidates = serviceAliasCandidates(address, workspaceName);
|
|
742
|
+
if (candidates.length > 1) {
|
|
743
|
+
const ws = namedWorkspace(workspaceName)?.name ?? "default";
|
|
744
|
+
throw new Error(
|
|
745
|
+
`"${address}" is ambiguous in workspace "${ws}": ${candidates.join(", ")}; name one of them`,
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
return candidates[0] ?? address;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** resolveService for a repo name or a listed address (no aliases). */
|
|
752
|
+
function resolveListedService(
|
|
753
|
+
address: string,
|
|
754
|
+
ws: NamedWorkspace,
|
|
755
|
+
catalog: ServiceCatalog,
|
|
756
|
+
): ResolvedService | null {
|
|
662
757
|
let resolved = resolveServiceSource(address, ws.name, ws.config, catalog);
|
|
663
758
|
|
|
664
759
|
// Bare multi-service repo registered ONLY by subsystem (e.g. "shop/db" +
|
|
@@ -998,6 +1093,109 @@ export interface RunCommandResult {
|
|
|
998
1093
|
};
|
|
999
1094
|
}
|
|
1000
1095
|
|
|
1096
|
+
/**
|
|
1097
|
+
* A service with no commands of its own is started by its repo's up
|
|
1098
|
+
* (placement's repo-level rule). For `up` that is the repo's up (`address`
|
|
1099
|
+
* set); any other command would reach the repo's other services too, so it
|
|
1100
|
+
* points at the repo instead: a refusal, except that down succeeds (nothing
|
|
1101
|
+
* of the service's own runs, and a workspace Down sends every service).
|
|
1102
|
+
* Null when the rule does not apply.
|
|
1103
|
+
*/
|
|
1104
|
+
function repoStartedFallback(
|
|
1105
|
+
resolved: ResolvedService,
|
|
1106
|
+
command: string,
|
|
1107
|
+
workspace: string | undefined,
|
|
1108
|
+
): { address?: string; note: string } | null {
|
|
1109
|
+
const repoEntry = resolveService(resolved.repoName, workspace);
|
|
1110
|
+
if (!repoEntry || repoEntry.subsystem === resolved.subsystem) return null;
|
|
1111
|
+
const manifest = readManifest(resolved.repoPath);
|
|
1112
|
+
if (!manifest) return null;
|
|
1113
|
+
const repoCommands = effectiveLocalCommands(
|
|
1114
|
+
manifest,
|
|
1115
|
+
repoEntry.subsystem,
|
|
1116
|
+
resolved.repoName,
|
|
1117
|
+
workspace ?? "default",
|
|
1118
|
+
);
|
|
1119
|
+
if (!repoCommands["up"]) return null;
|
|
1120
|
+
if (command === "up")
|
|
1121
|
+
return {
|
|
1122
|
+
address: resolved.repoName,
|
|
1123
|
+
note: `${resolved.address} has no up of its own; running ${resolved.repoName}'s up, which starts it`,
|
|
1124
|
+
};
|
|
1125
|
+
return command === "down"
|
|
1126
|
+
? {
|
|
1127
|
+
note:
|
|
1128
|
+
`${resolved.address} has no down of its own; it is stopped by ${resolved.repoName}'s down. ` +
|
|
1129
|
+
`Run \`${product().cli} down ${resolved.repoName}\` (that stops every service it starts).`,
|
|
1130
|
+
}
|
|
1131
|
+
: {
|
|
1132
|
+
note:
|
|
1133
|
+
`${resolved.address} has no ${command} of its own; it is started by ${resolved.repoName}'s up. ` +
|
|
1134
|
+
`Run \`${product().cli} ${command} ${resolved.repoName}\` (that reaches every service it starts).`,
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/** What a run of `command` on `address` would do, without running it. */
|
|
1139
|
+
export type ServiceCommandPreview =
|
|
1140
|
+
| { kind: "run"; address: string }
|
|
1141
|
+
| { kind: "via-repo"; address: string; note: string }
|
|
1142
|
+
| { kind: "nothing"; address: string; note: string }
|
|
1143
|
+
| { kind: "error"; address: string; note: string };
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* The dry-run twin of runServiceCommand's command lookup: the same
|
|
1147
|
+
* resolution, own-command, restart-synthesis and repo-started rules, so a
|
|
1148
|
+
* dry run says what a real run does.
|
|
1149
|
+
*/
|
|
1150
|
+
export function previewServiceCommand(
|
|
1151
|
+
address: string,
|
|
1152
|
+
command: string,
|
|
1153
|
+
workspace?: string,
|
|
1154
|
+
): ServiceCommandPreview {
|
|
1155
|
+
const resolved = resolveService(address, workspace);
|
|
1156
|
+
if (!resolved) {
|
|
1157
|
+
const candidates = serviceAliasCandidates(address, workspace);
|
|
1158
|
+
return {
|
|
1159
|
+
kind: "error",
|
|
1160
|
+
address,
|
|
1161
|
+
note:
|
|
1162
|
+
`service "${address}" not found in workspace "${workspace ?? "default"}"` +
|
|
1163
|
+
(candidates.length > 1
|
|
1164
|
+
? `; it is ambiguous: ${candidates.join(", ")}`
|
|
1165
|
+
: ""),
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
const manifest = readManifest(resolved.repoPath);
|
|
1169
|
+
const own = manifest
|
|
1170
|
+
? effectiveLocalCommands(
|
|
1171
|
+
manifest,
|
|
1172
|
+
resolved.subsystem,
|
|
1173
|
+
resolved.repoName,
|
|
1174
|
+
workspace ?? "default",
|
|
1175
|
+
)
|
|
1176
|
+
: {};
|
|
1177
|
+
if (own[command] || (command === "restart" && own["down"] && own["up"]))
|
|
1178
|
+
return { kind: "run", address };
|
|
1179
|
+
const viaRepo = repoStartedFallback(resolved, command, workspace);
|
|
1180
|
+
if (viaRepo?.address)
|
|
1181
|
+
return { kind: "via-repo", address: viaRepo.address, note: viaRepo.note };
|
|
1182
|
+
if (viaRepo)
|
|
1183
|
+
return command === "down"
|
|
1184
|
+
? { kind: "nothing", address, note: viaRepo.note }
|
|
1185
|
+
: { kind: "error", address, note: viaRepo.note };
|
|
1186
|
+
if (command === "down")
|
|
1187
|
+
return {
|
|
1188
|
+
kind: "nothing",
|
|
1189
|
+
address,
|
|
1190
|
+
note: "no down command: nothing to stop",
|
|
1191
|
+
};
|
|
1192
|
+
return {
|
|
1193
|
+
kind: "error",
|
|
1194
|
+
address,
|
|
1195
|
+
note: `command "${command}" not found for service "${address}"`,
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1001
1199
|
/**
|
|
1002
1200
|
* Resolve and execute a service command.
|
|
1003
1201
|
* Returns the spawned child process and whether it is a long-running (detachable) command.
|
|
@@ -1048,8 +1246,12 @@ export function runServiceCommand(
|
|
|
1048
1246
|
|
|
1049
1247
|
const resolved = resolveService(address, workspace);
|
|
1050
1248
|
if (!resolved) {
|
|
1249
|
+
const candidates = serviceAliasCandidates(address, workspace);
|
|
1051
1250
|
onLine(
|
|
1052
|
-
`Error: service "${address}" not found in catalog or workspace "${workspace}"
|
|
1251
|
+
`Error: service "${address}" not found in catalog or workspace "${workspace}"` +
|
|
1252
|
+
(candidates.length > 1
|
|
1253
|
+
? `; it is ambiguous: ${candidates.join(", ")}`
|
|
1254
|
+
: ""),
|
|
1053
1255
|
);
|
|
1054
1256
|
closeAsync(1);
|
|
1055
1257
|
return dummy();
|
|
@@ -1063,7 +1265,7 @@ export function runServiceCommand(
|
|
|
1063
1265
|
resolved.address,
|
|
1064
1266
|
servicePlacement(readManifest(repoPath), subsystem, "local"),
|
|
1065
1267
|
(e) => isProductionEnv(e) === true,
|
|
1066
|
-
command
|
|
1268
|
+
command,
|
|
1067
1269
|
);
|
|
1068
1270
|
if (refusal) {
|
|
1069
1271
|
onLine(refusal);
|
|
@@ -1094,7 +1296,9 @@ export function runServiceCommand(
|
|
|
1094
1296
|
workspace,
|
|
1095
1297
|
];
|
|
1096
1298
|
onLine(
|
|
1097
|
-
|
|
1299
|
+
command === "restart"
|
|
1300
|
+
? `restarting the running instance (shared) — owners: [${owners.join(", ")}] at ${resolved.repoPath}`
|
|
1301
|
+
: `already running (shared) — owners: [${owners.join(", ")}] at ${resolved.repoPath}`,
|
|
1098
1302
|
);
|
|
1099
1303
|
// restart still needs to bounce; only short-circuit pure `up`.
|
|
1100
1304
|
if (command === "up") {
|
|
@@ -1279,6 +1483,16 @@ export function runServiceCommand(
|
|
|
1279
1483
|
}
|
|
1280
1484
|
|
|
1281
1485
|
if (!cmdDef) {
|
|
1486
|
+
const viaRepo = repoStartedFallback(resolved, command, workspace);
|
|
1487
|
+
if (viaRepo?.address) {
|
|
1488
|
+
onLine(viaRepo.note);
|
|
1489
|
+
return runServiceCommand({ ...options, address: viaRepo.address });
|
|
1490
|
+
}
|
|
1491
|
+
if (viaRepo) {
|
|
1492
|
+
onLine(command === "down" ? viaRepo.note : `Error: ${viaRepo.note}`);
|
|
1493
|
+
closeAsync(command === "down" ? 0 : 1);
|
|
1494
|
+
return dummy();
|
|
1495
|
+
}
|
|
1282
1496
|
// Down is opctl-conventional: when no container is running AND the manifest
|
|
1283
1497
|
// declares no down command, there is simply nothing to stop — that's a
|
|
1284
1498
|
// successful down, not an error (keeps the ⋮ menu / workspace Down safe to
|
|
@@ -34,6 +34,8 @@ export interface PlacementTarget {
|
|
|
34
34
|
env: string;
|
|
35
35
|
account?: string;
|
|
36
36
|
url?: string;
|
|
37
|
+
/** The status codes that count as up for `url`, as declared. */
|
|
38
|
+
expect?: number | number[];
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export interface Placement {
|
|
@@ -84,6 +86,7 @@ function targetFor(svc: ServiceManagementConfig, env: string): PlacementTarget {
|
|
|
84
86
|
env: tEnv,
|
|
85
87
|
...(own?.account ? { account: own.account } : {}),
|
|
86
88
|
...(url ? { url } : {}),
|
|
89
|
+
...(own?.url && own.expect !== undefined ? { expect: own.expect } : {}),
|
|
87
90
|
};
|
|
88
91
|
}
|
|
89
92
|
|
|
@@ -186,16 +189,25 @@ export function isProtectedRemote(p: Placement, isProduction: IsProduction) {
|
|
|
186
189
|
return p.runs === "remote" && !!p.target && isProduction(p.target.env);
|
|
187
190
|
}
|
|
188
191
|
|
|
192
|
+
/** How a refusal names the command: up → "start", exec → `run "exec" on`. */
|
|
193
|
+
function refusedVerb(command: string): string {
|
|
194
|
+
if (command === "up" || command === "start") return "start";
|
|
195
|
+
if (command === "down" || command === "stop") return "stop";
|
|
196
|
+
if (command === "restart") return "restart";
|
|
197
|
+
return `run "${command}" on`;
|
|
198
|
+
}
|
|
199
|
+
|
|
189
200
|
/**
|
|
190
|
-
* Why Den must not run
|
|
191
|
-
* remote service is refused ("runs in dev (AWS); nothing to
|
|
201
|
+
* Why Den must not run `command` on `address`, or null. Every command on a
|
|
202
|
+
* remote service is refused ("runs in dev (AWS); nothing to restart here").
|
|
192
203
|
*/
|
|
193
204
|
export function placementRefusal(
|
|
194
205
|
address: string,
|
|
195
206
|
p: Placement,
|
|
196
207
|
isProduction: IsProduction,
|
|
197
|
-
|
|
208
|
+
command = "up",
|
|
198
209
|
): string | null {
|
|
210
|
+
const verb = refusedVerb(command);
|
|
199
211
|
if (p.runs !== "remote") return null;
|
|
200
212
|
const where = p.target
|
|
201
213
|
? `${p.target.env}${p.target.account ? ` (${p.target.account})` : ""}`
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import * as fs from "fs";
|
|
18
|
+
import * as path from "path";
|
|
18
19
|
import { z } from "zod";
|
|
19
20
|
import type { ValidationError, ValidationResult } from "../types/index.js";
|
|
20
21
|
import {
|
|
@@ -24,11 +25,14 @@ import {
|
|
|
24
25
|
type ManifestMap,
|
|
25
26
|
} from "./merge-override.js";
|
|
26
27
|
import {
|
|
28
|
+
heldManifestPath,
|
|
27
29
|
loadManifest,
|
|
28
30
|
orphanOverrideMessage,
|
|
31
|
+
overrideSuffix,
|
|
29
32
|
parseManifestText,
|
|
30
33
|
type ManifestLayer,
|
|
31
34
|
} from "./read-manifest.js";
|
|
35
|
+
import { product } from "../product.js";
|
|
32
36
|
|
|
33
37
|
/** What layered validation needs from the schema module. */
|
|
34
38
|
export interface SchemaTools {
|
|
@@ -170,6 +174,31 @@ function readLayer(file: string): { value: unknown } | { error: string } {
|
|
|
170
174
|
}
|
|
171
175
|
}
|
|
172
176
|
|
|
177
|
+
/**
|
|
178
|
+
* A warning for a held manifest the repo's own manifest has made redundant:
|
|
179
|
+
* the repo's file always wins, so the held one is never read.
|
|
180
|
+
*/
|
|
181
|
+
function redundantHeldWarning(
|
|
182
|
+
heldFile: string,
|
|
183
|
+
overrideFile: string | null,
|
|
184
|
+
): ValidationError {
|
|
185
|
+
const yml = product().manifestYml;
|
|
186
|
+
const name = path.basename(heldFile);
|
|
187
|
+
const key = name.endsWith(yml) ? name.slice(0, -yml.length) : name;
|
|
188
|
+
const asOverride = path.join(
|
|
189
|
+
path.dirname(heldFile),
|
|
190
|
+
`${key}${overrideSuffix()}`,
|
|
191
|
+
);
|
|
192
|
+
return {
|
|
193
|
+
path: "",
|
|
194
|
+
file: heldFile,
|
|
195
|
+
message: `${heldFile} is not used: the repo now carries its own manifest, which always wins`,
|
|
196
|
+
suggestion: overrideFile
|
|
197
|
+
? `delete it; ${overrideFile} already layers onto the repo's manifest`
|
|
198
|
+
: `delete it, or turn it into an override (${asOverride}) that keeps only what this distribution changes`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
173
202
|
/** Validate every manifest layer of `repoPath`, then the merged result. */
|
|
174
203
|
export function validateLayers(
|
|
175
204
|
tools: SchemaTools,
|
|
@@ -189,10 +218,20 @@ export function validateLayers(
|
|
|
189
218
|
}
|
|
190
219
|
|
|
191
220
|
const base = readLayer(baseFile);
|
|
192
|
-
|
|
221
|
+
let baseResult =
|
|
193
222
|
"error" in base
|
|
194
223
|
? failure(tools.version, baseFile, base.error)
|
|
195
224
|
: withFile(tools.validate(base.value), baseFile);
|
|
225
|
+
const redundantHeld =
|
|
226
|
+
baseLayer === "repo" ? heldManifestPath(repoPath) : null;
|
|
227
|
+
if (redundantHeld)
|
|
228
|
+
baseResult = {
|
|
229
|
+
...baseResult,
|
|
230
|
+
warnings: [
|
|
231
|
+
...baseResult.warnings,
|
|
232
|
+
redundantHeldWarning(redundantHeld, overrideFile),
|
|
233
|
+
],
|
|
234
|
+
};
|
|
196
235
|
layers.push({ layer: baseLayer, file: baseFile, result: baseResult });
|
|
197
236
|
|
|
198
237
|
if (overrideFile) {
|
|
@@ -31,13 +31,23 @@ export function composeCommands(
|
|
|
31
31
|
repoName: string,
|
|
32
32
|
workspace: string,
|
|
33
33
|
): Record<string, CommandDef> {
|
|
34
|
-
if (!manifest.compose
|
|
34
|
+
if (!manifest.compose) return {};
|
|
35
35
|
// `docker compose up -d` returns once the containers are started, so these
|
|
36
36
|
// finish on their own: never daemonize them.
|
|
37
37
|
const stack = (sub: string) => ({
|
|
38
38
|
cmd: `${denBin()} stack ${sub} ${manifestStackName(manifest, repoName)} -w ${workspace}`,
|
|
39
39
|
detachable: false,
|
|
40
40
|
});
|
|
41
|
+
// Any other manifest service names a compose service: it restarts on its
|
|
42
|
+
// own (`docker compose restart`, same container and env). Up and down stay
|
|
43
|
+
// the stack's, through the primary service.
|
|
44
|
+
if (!isPrimary(manifest, subsystem))
|
|
45
|
+
return {
|
|
46
|
+
restart: {
|
|
47
|
+
...stack("restart"),
|
|
48
|
+
cmd: `${stack("restart").cmd} --service ${JSON.stringify(subsystem)}`,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
41
51
|
return {
|
|
42
52
|
up: stack("up"),
|
|
43
53
|
down: stack("down"),
|
|
@@ -266,6 +266,26 @@ export function stackDown(ctx: StackContext, s: StackConfig): void {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Restart one compose service of a running stack in place (`docker compose
|
|
271
|
+
* restart`): the same container, so the same env; nothing is recreated.
|
|
272
|
+
*/
|
|
273
|
+
export function stackRestartService(
|
|
274
|
+
ctx: StackContext,
|
|
275
|
+
s: StackConfig,
|
|
276
|
+
service: string,
|
|
277
|
+
): void {
|
|
278
|
+
const override = path.join(stackDir(ctx, s), "override.yml");
|
|
279
|
+
const args = fs.existsSync(override)
|
|
280
|
+
? overrideArgs(ctx, s)
|
|
281
|
+
: baseArgs(ctx, s);
|
|
282
|
+
must("docker", [...args, "restart", service], {
|
|
283
|
+
cwd: repoDir(s),
|
|
284
|
+
env: composeEnv(ctx, s),
|
|
285
|
+
inherit: true,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
269
289
|
export function stackPs(ctx: StackContext, s: StackConfig): string {
|
|
270
290
|
return run(
|
|
271
291
|
"docker",
|
|
@@ -176,8 +176,9 @@ const COMMAND_FLAGS: Record<string, readonly string[]> = {
|
|
|
176
176
|
"--force",
|
|
177
177
|
"-A",
|
|
178
178
|
"--app",
|
|
179
|
+
"--with-deps",
|
|
179
180
|
],
|
|
180
|
-
restart: ["-A", "--app"],
|
|
181
|
+
restart: ["-A", "--app", "-n", "--dry-run"],
|
|
181
182
|
logs: [],
|
|
182
183
|
setup: ["-f", "--force"],
|
|
183
184
|
sync: [],
|