@miroshnik/elves 2026.1018.0 → 2026.1018.1
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/docs/cloud-sessions.md +3 -2
- package/package.json +2 -2
- package/skills/est/SKILL.md +2 -1
- package/skills/est/scripts/est.ts +16 -3
- package/skills/task-env/SKILL.md +4 -6
- package/skills/task-env/proxy/compose.yaml +5 -8
- package/skills/task-env/reference.md +19 -27
- package/skills/task-env/scripts/task-env.ts +27 -63
package/docs/cloud-sessions.md
CHANGED
|
@@ -149,8 +149,9 @@ and prints the same steps:
|
|
|
149
149
|
|
|
150
150
|
1. `est fact <N>` computes this session's part of the actual from its
|
|
151
151
|
transcript in the container (linking — the branch `<type>/<N>-…` or the
|
|
152
|
-
only `#N` in the first prompt, section "The brief"
|
|
153
|
-
`
|
|
152
|
+
only `#N` in the first prompt, section "The brief"; the session's PR in
|
|
153
|
+
its own repository, by the clone's `origin`, doesn't break it, #617) and
|
|
154
|
+
prints an `Actual (cloud)` comment
|
|
154
155
|
with hours, tokens, a link to the session and a part marker — post it to
|
|
155
156
|
the issue with a GitHub tool. Project fields (`Actual, h`, tokens, cost)
|
|
156
157
|
are set by the next local session: `est fact <N> --write` or
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miroshnik/elves",
|
|
3
|
-
"version": "2026.1018.
|
|
3
|
+
"version": "2026.1018.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Elves — a development flow with AI agents: shared rules and skills for any agent; install — bunx @miroshnik/elves install -g; the installer and skill scripts — TypeScript under Bun, tests — bun test in tests/",
|
|
6
6
|
"repository": {
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"eslint-plugin-boundaries": "^7.2.0",
|
|
67
67
|
"knip": "^6.41.0"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "28a9863fbdf5acfd5706b44b8d3c3ecd87fd39fd"
|
|
70
70
|
}
|
package/skills/est/SKILL.md
CHANGED
|
@@ -136,7 +136,8 @@ its own part; GraphQL and GitHub Projects are closed there. When a task is
|
|
|
136
136
|
closed, `est fact <N>` in the cloud takes this session's transcript in the
|
|
137
137
|
container (linking — the branch `<type>/<N>-…` or the only "#N" in the first
|
|
138
138
|
prompt, its background block included — `docs/cloud-sessions.md`, "The brief";
|
|
139
|
-
PRs and commits aren't visible from there
|
|
139
|
+
PRs and commits aren't visible from there: a PR of the clone's `origin`
|
|
140
|
+
repository keeps the link, another repository's breaks it) and prints the comment
|
|
140
141
|
«Actual (cloud): …» with hours, tokens and a link to the session, with a part
|
|
141
142
|
marker (`cloud`) (or «Actual unavailable (cloud)», if the transcript isn't linked
|
|
142
143
|
to the task) — the agent writes it into the issue with a GitHub tool. Other
|
|
@@ -2732,6 +2732,8 @@ export function computeFact(repo: FactRepo, number: number, prObjs: PR[], closer
|
|
|
2732
2732
|
return head ? (s.wt_branches ?? []).findIndex((bs) => bs.includes(head)) + 1 : 0;
|
|
2733
2733
|
};
|
|
2734
2734
|
const anchors: Anchor[] = [];
|
|
2735
|
+
// a PR of our repository that isn't the task's is a foreign anchor: every PR of the repository is known here. Mirror:
|
|
2736
|
+
// cloudOwnPart — the cloud knows none, its own repository's pr-links aren't anchors there
|
|
2735
2737
|
for (const [ts, pr] of links) {
|
|
2736
2738
|
const own = ourPrs.has(pr);
|
|
2737
2739
|
// Claude Code repeats the pr-link of the PR bound to the session even after the merge — that's the app's status,
|
|
@@ -3338,15 +3340,26 @@ const cloudSid = (sessionId: string | undefined) => (sessionId ? "session_" + se
|
|
|
3338
3340
|
/**
|
|
3339
3341
|
* This cloud session's part of the actual — from its transcript in the container (the work directory is the git
|
|
3340
3342
|
* root). Without GitHub: linking is by the `<type>/<N>-…` branch, a "#N" first prompt, a subagent assignment; PRs and
|
|
3341
|
-
* commits aren't visible from here.
|
|
3343
|
+
* commits aren't visible from here. The repository — `owner/repo` of the clone's origin, as for a local one (#617):
|
|
3344
|
+
* otherwise a PR of its own repository the session opened looks like another repository's and unlinks the first
|
|
3345
|
+
* prompt's period; no origin — "cloud", any PR with a repository is then another's.
|
|
3342
3346
|
*/
|
|
3343
3347
|
function cloudOwnPart(number: number, session: string, gap: number): CloudPart | null {
|
|
3344
3348
|
const top = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
3345
3349
|
const cwd = top.status === 0 && top.stdout.trim() ? top.stdout.trim() : process.cwd();
|
|
3346
|
-
const
|
|
3350
|
+
const origin = spawnSync("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" });
|
|
3351
|
+
const full = (origin.status === 0 && originRepo(origin.stdout)) || "cloud";
|
|
3352
|
+
// The cloud sees no PRs, so a PR of its own repository is neither the task's nor another task's: computeFact would
|
|
3353
|
+
// take it for a foreign anchor and end the first prompt's period when the session opened it — CI, the merge and the
|
|
3354
|
+
// actual after it are the task's too. Mirror: computeFact, the pr-link anchors — locally every PR is known.
|
|
3355
|
+
const ownLink = (r?: string) => !r || r.toLowerCase() === full.toLowerCase();
|
|
3356
|
+
const sessions = transcriptFilesIn([cwd])
|
|
3357
|
+
.map((f) => parseSessionFile(f))
|
|
3358
|
+
.filter((x) => x.ev.length) // don't discard a routine one: a cloud session is often a single prompt
|
|
3359
|
+
.map((s) => ({ ...s, prlinks: s.prlinks.filter(([, , r]) => !ownLink(r)) }));
|
|
3347
3360
|
if (!sessions.length) return null;
|
|
3348
3361
|
const stub: FactRepo = {
|
|
3349
|
-
full
|
|
3362
|
+
full,
|
|
3350
3363
|
prs: () => new Map(),
|
|
3351
3364
|
openPrs: () => ({ at: 0, items: {} }),
|
|
3352
3365
|
closers: () => ({ oid: {}, pr: {} }),
|
package/skills/task-env/SKILL.md
CHANGED
|
@@ -125,9 +125,9 @@ removes it for good). Routes follow Docker events — no reload; an unknown host
|
|
|
125
125
|
skill update brings the container to the new compose file. Its network, health and the API the dashboard reads
|
|
126
126
|
addresses from — reference.md, "The proxy".
|
|
127
127
|
|
|
128
|
-
**Before the rename** the proxy was `ai-dev-proxy`.
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
**Before the rename** the proxy was `ai-dev-proxy`. `proxy up` (and `up`, which starts the proxy) takes its place:
|
|
129
|
+
removes its container, which holds the port. Its network the proxy doesn't join: a stack brought up before the rename
|
|
130
|
+
gets its addresses back by `up` again — reference.md, "The proxy before the rename".
|
|
131
131
|
|
|
132
132
|
## Compose file isolation rules
|
|
133
133
|
|
|
@@ -146,9 +146,7 @@ each with `task-env check`, a violation is code 1 of `check`.
|
|
|
146
146
|
|
|
147
147
|
A bind mount of a machine resource — the Docker socket, `/etc/localtime`, a
|
|
148
148
|
device (a source in `/var/run`, `/run`, `/etc`, `/dev`, `/proc`, `/sys`) — is
|
|
149
|
-
not code and is fine; `~/…` is an absolute path.
|
|
150
|
-
(the proxy before the rename, written by hand) — its own `❌` line: remove it,
|
|
151
|
-
`up` adds the proxy's network to the `env.http` services itself.
|
|
149
|
+
not code and is fine; `~/…` is an absolute path.
|
|
152
150
|
|
|
153
151
|
`task-env check` reads `docker compose config`, not the YAML, so every form of writing is covered, and needs no Docker
|
|
154
152
|
daemon — reference.md, "What the compose file check reads".
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
# The machine's proxy (the task-env skill, SKILL.md, «The proxy»): one Traefik per machine routes
|
|
2
2
|
# http://<service>.<N>.<project>.localhost to containers of task stacks by their labels. Started by
|
|
3
|
-
# `task-env proxy up`. The name — the Compose project and the network
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
# script. The network of the proxy before the rename (`FORMER_PROXY` of the script) the script adds on Compose's stdin
|
|
7
|
-
# when it is on the machine — it isn't always, so it isn't here.
|
|
3
|
+
# `task-env proxy up`. The name — the Compose project and the network — and the port come from ELVES_PROXY and
|
|
4
|
+
# ELVES_PROXY_PORT, which scripts/task-env.ts passes explicitly (`composeEnv`): their defaults live only there, so the
|
|
5
|
+
# file has none (`${…:?}`) and doesn't come up without the script.
|
|
8
6
|
name: ${ELVES_PROXY:?set by task-env.ts}
|
|
9
7
|
services:
|
|
10
8
|
traefik:
|
|
@@ -17,9 +15,8 @@ services:
|
|
|
17
15
|
# a container is published only with traefik.enable=true
|
|
18
16
|
- --providers.docker.exposedByDefault=false
|
|
19
17
|
# task containers sit in several networks: a task stack's service names the proxy's one by the label
|
|
20
|
-
# traefik.docker.network; a container without the label is reached through
|
|
21
|
-
|
|
22
|
-
- --providers.docker.network=${ELVES_PROXY_DEFAULT_NETWORK:?set by task-env.ts}
|
|
18
|
+
# traefik.docker.network; a container without the label is reached through the proxy's own
|
|
19
|
+
- --providers.docker.network=${ELVES_PROXY:?set by task-env.ts}
|
|
23
20
|
# the API — the routes for the dashboard: on the web entry point by the host <name>.localhost (labels below),
|
|
24
21
|
# without Traefik's own UI
|
|
25
22
|
- --api=true
|
|
@@ -83,7 +83,7 @@ unavailable or (`prune`) a task's state isn't checked; `proxy status` — 0 runn
|
|
|
83
83
|
| image | `traefik:v3.7` |
|
|
84
84
|
| provider | Docker, `exposedByDefault: false` — a container without `traefik.enable=true` isn't published |
|
|
85
85
|
| network | `elves-proxy`: the proxy reaches a task stack's service through it — the service names it by the label `traefik.docker.network` |
|
|
86
|
-
| default network | `providers.docker.network` — for a container without the label: `elves-proxy`,
|
|
86
|
+
| default network | `providers.docker.network` — for a container without the label: `elves-proxy`, the proxy's own |
|
|
87
87
|
| entry point | `web` — HTTP on `127.0.0.1:80`; not on all interfaces: task apps aren't visible on the local network |
|
|
88
88
|
| restart | `unless-stopped` — comes back with Docker; `proxy down` removes it for good |
|
|
89
89
|
| health | `traefik healthcheck --ping`: `proxy up` returns when the proxy serves |
|
|
@@ -110,39 +110,28 @@ drop.
|
|
|
110
110
|
## The proxy before the rename
|
|
111
111
|
|
|
112
112
|
Before the rename of the flow (ai-dev → Elves) the proxy was the Compose
|
|
113
|
-
project `ai-dev-proxy` with the network of that name
|
|
114
|
-
|
|
115
|
-
release `proxy up` — and `up`, which starts a proxy that isn't running —
|
|
116
|
-
takes its place:
|
|
113
|
+
project `ai-dev-proxy` with the network of that name. `proxy up` — and `up`,
|
|
114
|
+
which starts a proxy that isn't running — takes its place:
|
|
117
115
|
|
|
118
116
|
- **its container goes** — it holds the port; the new one is created first
|
|
119
117
|
(the image pulled), so a failed pull leaves the former proxy serving:
|
|
120
118
|
`proxy ai-dev-proxy: removed — the proxy before the rename; elves-proxy
|
|
121
|
-
takes its port
|
|
122
|
-
- **its network
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
stack in its `default` and the proxy's network answers or not by chance. So
|
|
131
|
-
a task stack's service names the network by the label (section "Labels of
|
|
132
|
-
a task stack's service"), and the provider's default is `ai-dev-proxy`
|
|
133
|
-
while the proxy is in it — the stacks before the rename carry no label;
|
|
134
|
-
- **`check`** names a network `ai-dev-proxy` written in a project's compose
|
|
135
|
-
file (`networks: [default, ai-dev-proxy]`, `external: true`) with its fix:
|
|
136
|
-
remove it, `up` adds the proxy's network itself. The project moves by a
|
|
137
|
-
task of its own; until then the network is there and its stack comes up;
|
|
119
|
+
takes its port`;
|
|
120
|
+
- **its network isn't joined** — the proxy is in its own network only, even
|
|
121
|
+
with `ai-dev-proxy` on the machine; a stack brought up before the rename,
|
|
122
|
+
left in `ai-dev-proxy` alone, gets its addresses back by `up` again: the
|
|
123
|
+
override puts its HTTP services into `elves-proxy`;
|
|
124
|
+
- **`check`** holds a network `ai-dev-proxy` written in a project's compose
|
|
125
|
+
file (`networks: [default, ai-dev-proxy]`, `external: true`) to the rule of
|
|
126
|
+
any external network; the fix — remove it, `up` adds the proxy's network
|
|
127
|
+
itself;
|
|
138
128
|
- **the port** — `ELVES_PROXY_PORT`, unset — `AI_DEV_PROXY_PORT` of the
|
|
139
129
|
profile; **the settings** — `.agents/elves.json`, before `update` moves it
|
|
140
130
|
— `.agents/ai-dev.json`.
|
|
141
131
|
|
|
142
132
|
The former proxy is `ai-dev-proxy` only for the proxy of the default name;
|
|
143
133
|
a proxy of another name (a test's) has `<name>-former`, so a test never
|
|
144
|
-
touches the machine's proxy.
|
|
145
|
-
still in `ai-dev-proxy` alone lose their addresses — `up` again moves them.
|
|
134
|
+
touches the machine's proxy.
|
|
146
135
|
|
|
147
136
|
## Labels of a task stack's service
|
|
148
137
|
|
|
@@ -183,9 +172,12 @@ it by hand shares the proxy's network as `external`, and `check` names it.
|
|
|
183
172
|
- The network is `external`: the proxy creates it, so `proxy up` goes before
|
|
184
173
|
the task stack — `up` and `e2e` start it themselves.
|
|
185
174
|
- `traefik.docker.network` — the network the proxy reaches the service
|
|
186
|
-
through: the service is in its own networks too, and
|
|
187
|
-
|
|
188
|
-
|
|
175
|
+
through: the service is in its own networks too, and the label names the
|
|
176
|
+
proxy's one, so the pick doesn't depend on the provider's default network.
|
|
177
|
+
Without the label a container outside that default gets from Traefik "the
|
|
178
|
+
first" of its networks in Go's map order — a random one per configuration,
|
|
179
|
+
and a stack in its `default` and the proxy's network would answer or not by
|
|
180
|
+
chance.
|
|
189
181
|
- Networks: the service's own as Compose normalized them (one without
|
|
190
182
|
`networks` is in `default`) plus the proxy's — listed networks replace the
|
|
191
183
|
implicit `default`, and a key with null keeps the project's aliases.
|
|
@@ -33,12 +33,10 @@
|
|
|
33
33
|
* another. The defaults live only here: the script passes both to the compose file explicitly (`composeEnv`), the file
|
|
34
34
|
* has none (`${…:?}`); `check -g` of the installer imports `proxyName`.
|
|
35
35
|
*
|
|
36
|
-
* The rename of the flow to Elves renamed the proxy: `FORMER_PROXY` → `PROXY`.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* stack's service names it by a label, the provider's default is the former network while the proxy is in it
|
|
41
|
-
* (`proxyLabels`, `up`).
|
|
36
|
+
* The rename of the flow to Elves renamed the proxy: `FORMER_PROXY` → `PROXY`. `proxy up` takes the former proxy's
|
|
37
|
+
* place — removes its container, which holds the port — but not its network: the proxy is in its own network only, and
|
|
38
|
+
* `check` holds a hand-written former network to the rule of any external one (#564). A task stack's service names the
|
|
39
|
+
* proxy's network by a label (`proxyLabels`).
|
|
42
40
|
*
|
|
43
41
|
* The proxy's API (Traefik `/api/http/routers`) answers on its port by the host `<name>.localhost`: `proxyHosts` reads
|
|
44
42
|
* the hosts of its routes, `taskAddress` tells a task's address among them — the dashboard shows a task's services by
|
|
@@ -98,9 +96,8 @@ Examples:
|
|
|
98
96
|
|
|
99
97
|
Holds the compose file in <dir> (default — the current directory) to the task isolation rules: no container_name, no
|
|
100
98
|
fixed host port, volumes and networks without name and external, code by paths relative to the compose file; one ❌ line
|
|
101
|
-
per violation
|
|
102
|
-
|
|
103
|
-
config doesn't parse).
|
|
99
|
+
per violation. Code 0 — the rules hold or there is no compose file, 1 — violations, 2 — not checked (no docker compose,
|
|
100
|
+
the config doesn't parse).
|
|
104
101
|
|
|
105
102
|
Examples:
|
|
106
103
|
task-env check
|
|
@@ -108,8 +105,8 @@ Examples:
|
|
|
108
105
|
"proxy up": `task-env proxy up
|
|
109
106
|
|
|
110
107
|
Starts the machine's proxy (Traefik) or brings it to the skill's compose file and waits until it is healthy; port 80 busy
|
|
111
|
-
— another one in ELVES_PROXY_PORT. The proxy before the rename (ai-dev-proxy) — its container removed
|
|
112
|
-
|
|
108
|
+
— another one in ELVES_PROXY_PORT. The proxy before the rename (ai-dev-proxy) holds the port — its container removed;
|
|
109
|
+
stacks brought up before the rename get their addresses back by task-env up.
|
|
113
110
|
|
|
114
111
|
Examples:
|
|
115
112
|
task-env proxy up`,
|
|
@@ -158,9 +155,9 @@ export const ENTRY_POINT = "web";
|
|
|
158
155
|
export const PROXY = "elves-proxy";
|
|
159
156
|
|
|
160
157
|
/**
|
|
161
|
-
* The proxy's default name before the rename of the flow (ai-dev → Elves), read for the transition
|
|
162
|
-
*
|
|
163
|
-
*
|
|
158
|
+
* The proxy's default name before the rename of the flow (ai-dev → Elves), read for the transition (#545): `proxy up`
|
|
159
|
+
* removes its container, which holds the port, `check -g` of the installer names a proxy left under this name. Its
|
|
160
|
+
* network the proxy no longer joins (#564).
|
|
164
161
|
*/
|
|
165
162
|
export const FORMER_PROXY = "ai-dev-proxy";
|
|
166
163
|
|
|
@@ -183,15 +180,13 @@ export const formerProxyName = (env: NodeJS.ProcessEnv = process.env) => {
|
|
|
183
180
|
export const proxyPort = (env: NodeJS.ProcessEnv = process.env) => env.ELVES_PROXY_PORT || env.AI_DEV_PROXY_PORT || "80";
|
|
184
181
|
|
|
185
182
|
/**
|
|
186
|
-
* The environment of the proxy's compose file: the name
|
|
187
|
-
*
|
|
188
|
-
* network is the proxy's own unless `up` joins the former proxy's network (`up`); not a setting — the script sets it.
|
|
183
|
+
* The environment of the proxy's compose file: the name and the port are passed explicitly — the file has no defaults
|
|
184
|
+
* of its own (`${…:?}`), they live only here (#396, question 1).
|
|
189
185
|
*/
|
|
190
|
-
export const composeEnv = (env: NodeJS.ProcessEnv = process.env
|
|
186
|
+
export const composeEnv = (env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv => ({
|
|
191
187
|
...env,
|
|
192
188
|
ELVES_PROXY: proxyName(env),
|
|
193
189
|
ELVES_PROXY_PORT: proxyPort(env),
|
|
194
|
-
ELVES_PROXY_DEFAULT_NETWORK: network,
|
|
195
190
|
});
|
|
196
191
|
|
|
197
192
|
/** The host of the proxy's API: a router of the proxy's own labels on the `web` entry point (proxy/compose.yaml). */
|
|
@@ -307,8 +302,7 @@ export interface ProxyRoute {
|
|
|
307
302
|
* one service (`--scale`) carry the same labels — Traefik balances between them. The network for reaching the
|
|
308
303
|
* container — by a label (#545): without it Traefik takes the provider's default network, and when the container isn't
|
|
309
304
|
* in that one — the first of its networks in Go's map order, a random one per configuration, and a stack in its
|
|
310
|
-
* `default` and the proxy's network would answer or not by chance
|
|
311
|
-
* the proxy is in it (`up`).
|
|
305
|
+
* `default` and the proxy's network would answer or not by chance.
|
|
312
306
|
*/
|
|
313
307
|
export function proxyLabels(r: ProxyRoute): Record<string, string> {
|
|
314
308
|
const id = `${r.stack}-${r.service}`;
|
|
@@ -344,17 +338,9 @@ function hint(reason: string, env: NodeJS.ProcessEnv) {
|
|
|
344
338
|
return "";
|
|
345
339
|
}
|
|
346
340
|
|
|
347
|
-
/**
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
*/
|
|
351
|
-
function compose(args: string[], env: NodeJS.ProcessEnv, former?: string) {
|
|
352
|
-
const override = former ? { services: { [SERVICE]: { networks: { proxy: null, former: null } } }, networks: { former: { name: former, external: true } } } : undefined;
|
|
353
|
-
return spawnSync("docker", ["compose", "-f", PROXY_FILE, ...(override ? ["-f", "-"] : []), ...args], {
|
|
354
|
-
encoding: "utf8",
|
|
355
|
-
input: override ? JSON.stringify(override) : undefined,
|
|
356
|
-
env: composeEnv(env, former),
|
|
357
|
-
});
|
|
341
|
+
/** `docker compose` of the proxy. */
|
|
342
|
+
function compose(args: string[], env: NodeJS.ProcessEnv) {
|
|
343
|
+
return spawnSync("docker", ["compose", "-f", PROXY_FILE, ...args], { encoding: "utf8", env: composeEnv(env) });
|
|
358
344
|
}
|
|
359
345
|
|
|
360
346
|
/** The proxy containers of a Compose project in any state — a `format` line each; Docker unavailable — the reason. */
|
|
@@ -371,31 +357,18 @@ export function proxyRunning(env: NodeJS.ProcessEnv = process.env): boolean | st
|
|
|
371
357
|
return typeof states === "string" ? states : states.includes("running");
|
|
372
358
|
}
|
|
373
359
|
|
|
374
|
-
/** Whether a network is on the machine; Docker unavailable — the reason. */
|
|
375
|
-
function networkExists(name: string, env: NodeJS.ProcessEnv): boolean | string {
|
|
376
|
-
const r = spawnSync("docker", ["network", "ls", "--filter", `name=^${name}$`, "--format", "{{.Name}}"], { encoding: "utf8", env, timeout: 10_000 });
|
|
377
|
-
if (r.error || r.status !== 0) return why(r);
|
|
378
|
-
return r.stdout.split("\n").includes(name);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
360
|
function status(env: NodeJS.ProcessEnv): Result {
|
|
382
361
|
const name = proxyName(env);
|
|
383
|
-
const
|
|
384
|
-
if (typeof
|
|
385
|
-
const running = found.map((l) => l.split("\t")).find(([state]) => state === "running");
|
|
362
|
+
const running = proxyRunning(env);
|
|
363
|
+
if (typeof running === "string") return { code: 2, out: `proxy ${name}: Docker unavailable — ${running}${hint(running, env)}` };
|
|
386
364
|
if (!running) return { code: 1, out: `proxy ${name}: not running — start: ${START}` };
|
|
387
|
-
|
|
388
|
-
const also = running[1]?.split(",").includes(former) ? `; and ${former} — of the proxy before the rename, with the stacks brought up before it` : "";
|
|
389
|
-
return { code: 0, out: `proxy ${name}: running — ${addressUrl("<service>.<N>.<project>.localhost", proxyPort(env))} on 127.0.0.1, routes by container labels (network ${name}${also})` };
|
|
365
|
+
return { code: 0, out: `proxy ${name}: running — ${addressUrl("<service>.<N>.<project>.localhost", proxyPort(env))} on 127.0.0.1, routes by container labels (network ${name})` };
|
|
390
366
|
}
|
|
391
367
|
|
|
392
368
|
/**
|
|
393
|
-
* `proxy up`: the proxy brought to the compose file and healthy
|
|
394
|
-
* (#545
|
|
395
|
-
*
|
|
396
|
-
* the provider's default network: their services carry no network label, and a container outside the default network
|
|
397
|
-
* gets a random one of its networks (`proxyLabels`). No former network on the machine — the proxy is in its own only,
|
|
398
|
-
* the former one isn't created.
|
|
369
|
+
* `proxy up`: the proxy brought to the compose file and healthy, in its own network only. The proxy before the rename
|
|
370
|
+
* (#545) holds the port: its container is removed once the new one is created (a failed pull leaves the former proxy
|
|
371
|
+
* serving). Its network the proxy doesn't join (#564): stacks left only in it get their addresses back by `up`.
|
|
399
372
|
*/
|
|
400
373
|
function up(env: NodeJS.ProcessEnv): Result {
|
|
401
374
|
const name = proxyName(env);
|
|
@@ -404,17 +377,14 @@ function up(env: NodeJS.ProcessEnv): Result {
|
|
|
404
377
|
const fail = (reason: string) => ({ code: 2, out: [...out, `proxy ${name}: not started — ${reason}${hint(reason, env)}`].join("\n") });
|
|
405
378
|
const old = proxyContainers(former, "{{.ID}}", env);
|
|
406
379
|
if (typeof old === "string") return fail(old);
|
|
407
|
-
const joined = networkExists(former, env);
|
|
408
|
-
if (typeof joined === "string") return fail(joined);
|
|
409
|
-
const network = joined ? former : undefined;
|
|
410
380
|
if (old.length) {
|
|
411
|
-
const created = compose(["up", "--no-start", "--quiet-pull"], env
|
|
381
|
+
const created = compose(["up", "--no-start", "--quiet-pull"], env);
|
|
412
382
|
if (created.error || created.status !== 0) return fail(why(created));
|
|
413
383
|
const rm = spawnSync("docker", ["rm", "--force", ...old], { encoding: "utf8", env });
|
|
414
384
|
if (rm.error || rm.status !== 0) return fail(`the proxy before the rename, ${former}, not removed: ${why(rm)}`);
|
|
415
|
-
out.push(`proxy ${former}: removed — the proxy before the rename; ${name} takes its port
|
|
385
|
+
out.push(`proxy ${former}: removed — the proxy before the rename; ${name} takes its port`);
|
|
416
386
|
}
|
|
417
|
-
const r = compose(["up", "--detach", "--wait", "--quiet-pull"], env
|
|
387
|
+
const r = compose(["up", "--detach", "--wait", "--quiet-pull"], env);
|
|
418
388
|
if (r.error || r.status !== 0) return fail(why(r));
|
|
419
389
|
const s = status(env);
|
|
420
390
|
return { code: s.code, out: [...out, s.out].join("\n") };
|
|
@@ -515,12 +485,6 @@ function violations(config: Config, file: string): string[] {
|
|
|
515
485
|
}
|
|
516
486
|
for (const [key, r] of Object.entries(config.networks ?? {})) {
|
|
517
487
|
const users = services.filter(([, s]) => Object.hasOwn(s.networks ?? {}, key)).map(([n]) => n);
|
|
518
|
-
// the proxy's network under its name before the rename (#545): a compose file wrote it by hand, as the example of
|
|
519
|
-
// reference.md did; the proxy stays in that network one release (`up`), and a machine without it fails the stack
|
|
520
|
-
if (r.name === FORMER_PROXY) {
|
|
521
|
-
line(`network ${key} (${list(users)})`, `${FORMER_PROXY} is the machine's proxy before the rename, now ${PROXY}: remove the network from the compose file and its services; task-env up adds the proxy's network to the HTTP services of env.http itself`);
|
|
522
|
-
continue;
|
|
523
|
-
}
|
|
524
488
|
shared("network", key, r, users, "task stacks share it and reach each other's services");
|
|
525
489
|
}
|
|
526
490
|
return out;
|