craftar 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -5
- package/dist/cli.js +232 -6
- package/dist/cli.js.map +1 -1
- package/dist/core/capabilities.js +15 -0
- package/dist/core/capabilities.js.map +1 -1
- package/dist/core/doctor.js +143 -0
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/drift.js +2 -0
- package/dist/core/drift.js.map +1 -1
- package/dist/core/extract.js +13 -0
- package/dist/core/extract.js.map +1 -1
- package/dist/core/proc.js +108 -0
- package/dist/core/proc.js.map +1 -0
- package/dist/core/services.js +550 -0
- package/dist/core/services.js.map +1 -0
- package/dist/core/sync.js +23 -5
- package/dist/core/sync.js.map +1 -1
- package/dist/core/templates.js +217 -0
- package/dist/core/templates.js.map +1 -0
- package/dist/core/yaml-read.js +2 -2
- package/dist/core/yaml-read.js.map +1 -1
- package/dist/importers/drift-promote.js +5 -1
- package/dist/importers/drift-promote.js.map +1 -1
- package/dist/schema/index.js +257 -7
- package/dist/schema/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service ingredients — planning (spec 31 §4.1, §5.3) and the read-only core of `craftar services`.
|
|
3
|
+
* Planning: the files `sync` writes for service ingredients, called once after the target loop.
|
|
4
|
+
* Services core: recordedServices, upRefusal, downArgv, and the Compose/Docker helpers.
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { promises as fs } from "node:fs";
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { outName } from "../emitters/shared.js";
|
|
11
|
+
import { findSecrets } from "./secrets.js";
|
|
12
|
+
import { isUtf8, hashNormalized, stripBom, toLf } from "./text.js";
|
|
13
|
+
import { parseYamlText, yamlFault } from "./yaml-read.js";
|
|
14
|
+
import { normalizeDeclared, COMPOSE_MAPS, ComposeFragmentShape, ComposeStackShape, ComposePsRow, NO_PROTO_KEY, } from "../schema/index.js";
|
|
15
|
+
/** Whether a path is listed in the binary array. */
|
|
16
|
+
function isBinary(meta, file) {
|
|
17
|
+
const normalized = normalizeDeclared(file);
|
|
18
|
+
return meta.binary.some((b) => normalizeDeclared(b) === normalized);
|
|
19
|
+
}
|
|
20
|
+
/** Whether a path is listed in the executable array. */
|
|
21
|
+
function isExecutable(meta, file) {
|
|
22
|
+
const normalized = normalizeDeclared(file);
|
|
23
|
+
return meta.executable.some((e) => normalizeDeclared(e) === normalized);
|
|
24
|
+
}
|
|
25
|
+
/** Assert that the given bytes are valid UTF-8 text (no NUL). Throws with a contextual message. */
|
|
26
|
+
function assertText(ref, file, bytes, hint) {
|
|
27
|
+
if (!isUtf8(bytes) || bytes.includes(0)) {
|
|
28
|
+
throw new Error(`${ref} ${file}: ${hint}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse a Compose fragment from YAML text.
|
|
33
|
+
*
|
|
34
|
+
* Returns `{ ok: true, fragment }` on success, or `{ ok: false, reason }` on failure.
|
|
35
|
+
* The reason does NOT include a `<ref> <compose>: ` prefix — the caller adds it.
|
|
36
|
+
*
|
|
37
|
+
* Checks:
|
|
38
|
+
* 1. Valid YAML
|
|
39
|
+
* 2. Must be a YAML mapping (not null, array, scalar)
|
|
40
|
+
* 3. Must not declare top-level `name`
|
|
41
|
+
* 4. Must not declare `include`
|
|
42
|
+
* 5. Each of services/volumes/networks/configs/secrets must be a mapping (via ComposeFragmentShape)
|
|
43
|
+
* 6. __proto__ keys are refused (via noProtoKey in the schema)
|
|
44
|
+
* 7. Must declare at least one service
|
|
45
|
+
* 8. A service must not extend a file (extends: { file: ... }) — the lock would not record it
|
|
46
|
+
*/
|
|
47
|
+
export function parseFragment(text) {
|
|
48
|
+
// 1. Parse YAML — through parseYamlText so the error never includes the source line
|
|
49
|
+
// Compose honours merge keys (<<: *anchor), so parse with merge: true to see what Compose sees.
|
|
50
|
+
let parsed;
|
|
51
|
+
try {
|
|
52
|
+
parsed = parseYamlText("fragment", text, { merge: true });
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
return { ok: false, reason: `not valid YAML after rendering — ${yamlFault(e)}` };
|
|
56
|
+
}
|
|
57
|
+
// 2. Must be a mapping
|
|
58
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
59
|
+
return { ok: false, reason: "a fragment must be a YAML mapping" };
|
|
60
|
+
}
|
|
61
|
+
const obj = parsed;
|
|
62
|
+
// 3. Must not declare top-level name
|
|
63
|
+
if (Object.hasOwn(obj, "name")) {
|
|
64
|
+
return { ok: false, reason: "a fragment must not declare a top-level name — the project name is craftar's" };
|
|
65
|
+
}
|
|
66
|
+
// 4. Must not declare include
|
|
67
|
+
if (Object.hasOwn(obj, "include")) {
|
|
68
|
+
return { ok: false, reason: "a fragment must not declare include — every file of the stack is in craftar.lock" };
|
|
69
|
+
}
|
|
70
|
+
// 5 & 6. Validate shape through ComposeFragmentShape (catches __proto__ and non-mapping maps)
|
|
71
|
+
const result = ComposeFragmentShape.safeParse(obj);
|
|
72
|
+
if (!result.success) {
|
|
73
|
+
const issue = result.error.issues[0];
|
|
74
|
+
// The failing map is in issue.path[0]
|
|
75
|
+
const failingMap = issue.path[0];
|
|
76
|
+
// Check if it's a __proto__ issue (recognised by the message noProtoKey raises)
|
|
77
|
+
if (issue.message === NO_PROTO_KEY) {
|
|
78
|
+
return { ok: false, reason: NO_PROTO_KEY };
|
|
79
|
+
}
|
|
80
|
+
// Otherwise it's a shape issue (e.g., volumes: [])
|
|
81
|
+
return { ok: false, reason: `${failingMap} must be a mapping` };
|
|
82
|
+
}
|
|
83
|
+
const fragment = result.data;
|
|
84
|
+
// 7. Must declare at least one service
|
|
85
|
+
if (!fragment.services || Object.keys(fragment.services).length === 0) {
|
|
86
|
+
return { ok: false, reason: "a fragment must declare at least one service" };
|
|
87
|
+
}
|
|
88
|
+
// 8. A service must not extend a file — the lock would not record it
|
|
89
|
+
// Accepted: extends: 'svc' (a string) and extends: { service: 'svc' } (no file key)
|
|
90
|
+
// Refused: extends: { file: ..., service: ... }
|
|
91
|
+
for (const svcDef of Object.values(fragment.services)) {
|
|
92
|
+
if (svcDef && typeof svcDef === "object" && !Array.isArray(svcDef)) {
|
|
93
|
+
const ext = svcDef.extends;
|
|
94
|
+
if (ext && typeof ext === "object" && !Array.isArray(ext) && Object.hasOwn(ext, "file")) {
|
|
95
|
+
return { ok: false, reason: "a service must not extend a file — the lock would not record it" };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { ok: true, fragment };
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Plans the service files: `.craftar/compose.yaml`, `.craftar/.gitattributes`,
|
|
103
|
+
* and each service's fragment and files under `.craftar/services/<outName>/`.
|
|
104
|
+
*
|
|
105
|
+
* Called by `plan()` once after the target loop (spec 31 §5.3, choice 24).
|
|
106
|
+
* Reads the resolution's `service` ingredients directly — the single declared
|
|
107
|
+
* exception to "a planned file comes from `ctx.aimed`".
|
|
108
|
+
*/
|
|
109
|
+
export async function planServices(base) {
|
|
110
|
+
const { resolution, warn } = base;
|
|
111
|
+
// Get service ingredients from resolution (not through the walk — this is the declared exception)
|
|
112
|
+
const services = resolution.ingredients.filter((ing) => ing.meta.type === "service");
|
|
113
|
+
if (services.length === 0)
|
|
114
|
+
return [];
|
|
115
|
+
const files = [];
|
|
116
|
+
// Track declared keys for conflict detection across fragments
|
|
117
|
+
const declaredKeys = new Map(); // map name -> key -> ingredient ref
|
|
118
|
+
for (const mapName of COMPOSE_MAPS) {
|
|
119
|
+
declaredKeys.set(mapName, new Map());
|
|
120
|
+
}
|
|
121
|
+
// Track published host ports for warning (per ingredient, then across)
|
|
122
|
+
const publishedPorts = new Map(); // port -> ingredient ref
|
|
123
|
+
// Process each service
|
|
124
|
+
for (const ing of services) {
|
|
125
|
+
const meta = ing.meta;
|
|
126
|
+
const name = outName(meta);
|
|
127
|
+
const fragmentPath = meta.compose;
|
|
128
|
+
// Read and validate the fragment bytes first (for UTF-8 check)
|
|
129
|
+
const fragmentBytes = await base.bytes(ing, fragmentPath);
|
|
130
|
+
assertText(ing.ref, fragmentPath, fragmentBytes, "the fragment is not UTF-8 text");
|
|
131
|
+
// Render the fragment through ctx.text (sections, params)
|
|
132
|
+
const fragmentText = await base.text(ing, fragmentPath);
|
|
133
|
+
const fragmentContent = Buffer.from(toLf(stripBom(fragmentText)), "utf8");
|
|
134
|
+
// Parse the rendered fragment through parseFragment
|
|
135
|
+
const parseResult = parseFragment(fragmentText);
|
|
136
|
+
if (!parseResult.ok) {
|
|
137
|
+
throw new Error(`${ing.ref} ${fragmentPath}: ${parseResult.reason}`);
|
|
138
|
+
}
|
|
139
|
+
const fragment = parseResult.fragment;
|
|
140
|
+
// Check for duplicate keys in services, volumes, networks, configs, secrets
|
|
141
|
+
for (const mapName of COMPOSE_MAPS) {
|
|
142
|
+
const mapValue = fragment[mapName];
|
|
143
|
+
if (mapValue && typeof mapValue === "object" && !Array.isArray(mapValue)) {
|
|
144
|
+
const existing = declaredKeys.get(mapName);
|
|
145
|
+
for (const key of Object.keys(mapValue)) {
|
|
146
|
+
const prev = existing.get(key);
|
|
147
|
+
if (prev) {
|
|
148
|
+
throw new Error(`${prev} and ${ing.ref} both declare ${mapName}.${key} — Compose would keep one silently`);
|
|
149
|
+
}
|
|
150
|
+
existing.set(key, ing.ref);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Check for duplicate published host ports and warn (across different ingredients only)
|
|
155
|
+
// fragment.services is guaranteed to exist and be non-empty by parseFragment
|
|
156
|
+
const svcMap = fragment.services;
|
|
157
|
+
for (const svcDef of Object.values(svcMap)) {
|
|
158
|
+
if (svcDef && typeof svcDef === "object" && !Array.isArray(svcDef)) {
|
|
159
|
+
const ports = svcDef.ports;
|
|
160
|
+
if (Array.isArray(ports)) {
|
|
161
|
+
for (const port of ports) {
|
|
162
|
+
const hostPort = extractHostPort(port);
|
|
163
|
+
// Compare and print only when the host port is all digits (spec §4.1: a port written
|
|
164
|
+
// with a variable is not compared; a range like "8080-8090" is also not compared).
|
|
165
|
+
if (hostPort && /^\d+$/.test(hostPort)) {
|
|
166
|
+
const prev = publishedPorts.get(hostPort);
|
|
167
|
+
// Only warn for different ingredients (same ingredient = Compose's own error)
|
|
168
|
+
if (prev && prev !== ing.ref) {
|
|
169
|
+
warn(`${prev} and ${ing.ref} both publish host port ${hostPort}`);
|
|
170
|
+
}
|
|
171
|
+
else if (!prev) {
|
|
172
|
+
publishedPorts.set(hostPort, ing.ref);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Check for secrets in the rendered fragment
|
|
180
|
+
const findings = findSecrets(fragmentText);
|
|
181
|
+
for (const f of findings) {
|
|
182
|
+
warn(`${ing.ref} ${fragmentPath}: holds a secret-like value (${f.kind}) at line ${f.line}`);
|
|
183
|
+
}
|
|
184
|
+
// Add the fragment file
|
|
185
|
+
files.push({
|
|
186
|
+
path: path.posix.join(".craftar/services", name, fragmentPath),
|
|
187
|
+
content: fragmentContent,
|
|
188
|
+
target: "services",
|
|
189
|
+
ingredient: ing.ref,
|
|
190
|
+
});
|
|
191
|
+
// Process each file in the files list
|
|
192
|
+
for (const file of meta.files) {
|
|
193
|
+
const filePath = path.posix.join(".craftar/services", name, file);
|
|
194
|
+
if (isBinary(meta, file)) {
|
|
195
|
+
// Binary: copy as bytes
|
|
196
|
+
const bytes = await base.bytes(ing, file);
|
|
197
|
+
files.push({
|
|
198
|
+
path: filePath,
|
|
199
|
+
content: bytes,
|
|
200
|
+
target: "services",
|
|
201
|
+
ingredient: ing.ref,
|
|
202
|
+
...(isExecutable(meta, file) ? { mode: 0o755 } : {}),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// Text: validate UTF-8, render, check for secrets
|
|
207
|
+
const bytes = await base.bytes(ing, file);
|
|
208
|
+
assertText(ing.ref, file, bytes, "not UTF-8 text — list it under binary in ingredient.yaml");
|
|
209
|
+
const text = await base.text(ing, file);
|
|
210
|
+
const content = Buffer.from(toLf(stripBom(text)), "utf8");
|
|
211
|
+
// Check for secrets
|
|
212
|
+
const findings = findSecrets(text);
|
|
213
|
+
for (const f of findings) {
|
|
214
|
+
warn(`${ing.ref} ${file}: holds a secret-like value (${f.kind}) at line ${f.line}`);
|
|
215
|
+
}
|
|
216
|
+
files.push({
|
|
217
|
+
path: filePath,
|
|
218
|
+
content,
|
|
219
|
+
target: "services",
|
|
220
|
+
ingredient: ing.ref,
|
|
221
|
+
...(isExecutable(meta, file) ? { mode: 0o755 } : {}),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Generate the stack file
|
|
227
|
+
const stackLines = [
|
|
228
|
+
"# Generated by craftar from the Forge — do not edit; change the service ingredient and run `craftar sync`.",
|
|
229
|
+
"include:",
|
|
230
|
+
];
|
|
231
|
+
for (const ing of services) {
|
|
232
|
+
const name = outName(ing.meta);
|
|
233
|
+
const fragmentPath = ing.meta.compose;
|
|
234
|
+
stackLines.push(` - path: ${path.posix.join("services", name, fragmentPath)}`);
|
|
235
|
+
}
|
|
236
|
+
const stackContent = Buffer.from(stackLines.join("\n") + "\n", "utf8");
|
|
237
|
+
files.push({
|
|
238
|
+
path: ".craftar/compose.yaml",
|
|
239
|
+
content: stackContent,
|
|
240
|
+
target: "services",
|
|
241
|
+
ingredient: "service/*",
|
|
242
|
+
});
|
|
243
|
+
// Generate .gitattributes
|
|
244
|
+
files.push({
|
|
245
|
+
path: ".craftar/.gitattributes",
|
|
246
|
+
content: Buffer.from("* -text\n", "utf8"),
|
|
247
|
+
target: "services",
|
|
248
|
+
ingredient: "service/*",
|
|
249
|
+
});
|
|
250
|
+
return files;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Extract the host port from a port mapping.
|
|
254
|
+
* Handles short syntax ("8080:80", "127.0.0.1:8080:80", "8080:80/tcp")
|
|
255
|
+
* and long syntax ({ published: "8080", target: "80" }).
|
|
256
|
+
*/
|
|
257
|
+
function extractHostPort(port) {
|
|
258
|
+
if (typeof port === "string") {
|
|
259
|
+
// Short syntax: [IP:][HOST:]CONTAINER[/PROTOCOL]
|
|
260
|
+
const noProto = port.replace(/\/\w+$/, "");
|
|
261
|
+
const parts = noProto.split(":");
|
|
262
|
+
if (parts.length === 2) {
|
|
263
|
+
// HOST:CONTAINER
|
|
264
|
+
return parts[0];
|
|
265
|
+
}
|
|
266
|
+
else if (parts.length === 3) {
|
|
267
|
+
// IP:HOST:CONTAINER
|
|
268
|
+
return parts[1];
|
|
269
|
+
}
|
|
270
|
+
// Just CONTAINER, no host port
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
if (port !== null && typeof port === "object" && !Array.isArray(port)) {
|
|
274
|
+
const obj = port;
|
|
275
|
+
if (Object.hasOwn(obj, "published")) {
|
|
276
|
+
const published = obj.published;
|
|
277
|
+
return typeof published === "string" || typeof published === "number" ? String(published) : null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
/** The first Compose with `include` (spec 31 §4.6). */
|
|
283
|
+
export const MIN_COMPOSE = "2.20.0";
|
|
284
|
+
const STACK = ".craftar/compose.yaml";
|
|
285
|
+
const DOCKER_FIX = `install Docker with the Compose plugin ${MIN_COMPOSE} or later`;
|
|
286
|
+
/**
|
|
287
|
+
* The paths the planned stack includes — `.craftar/services/<name>/<compose>` for each service.
|
|
288
|
+
* Returns the workspace-relative POSIX paths (spec 31 §4.7: services-env scans only the planned fragments).
|
|
289
|
+
* Returns empty array for a stack that is not a valid craftar stack (invalid YAML, no includes, or an
|
|
290
|
+
* include path that is absolute or has a `..` segment).
|
|
291
|
+
*/
|
|
292
|
+
export function fragmentPaths(stackText) {
|
|
293
|
+
let doc;
|
|
294
|
+
try {
|
|
295
|
+
doc = parseYamlText("stack", stackText);
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
return []; // invalid YAML
|
|
299
|
+
}
|
|
300
|
+
const parsed = ComposeStackShape.safeParse(doc);
|
|
301
|
+
if (!parsed.success)
|
|
302
|
+
return [];
|
|
303
|
+
// Refuse an include path that is absolute or has a .. segment — not a craftar stack
|
|
304
|
+
for (const inc of parsed.data.include) {
|
|
305
|
+
const p = inc.path;
|
|
306
|
+
if (/^\/|^[A-Za-z]:/.test(p))
|
|
307
|
+
return []; // absolute
|
|
308
|
+
if (p.split("/").includes(".."))
|
|
309
|
+
return []; // .. segment
|
|
310
|
+
}
|
|
311
|
+
return parsed.data.include.map((inc) => path.posix.join(".craftar", inc.path));
|
|
312
|
+
}
|
|
313
|
+
/** `craftar-<slug>-<hash>` (spec 31 §4.5): computed from the workspace's real path, never stored. */
|
|
314
|
+
export function projectName(realRoot) {
|
|
315
|
+
const slug = path.basename(realRoot).toLowerCase().replace(/[^a-z0-9_-]/g, "-").slice(0, 40);
|
|
316
|
+
const hash = createHash("sha256").update(realRoot).digest("hex").slice(0, 8);
|
|
317
|
+
return /^-*$/.test(slug) ? `craftar--${hash}` : `craftar-${slug}-${hash}`;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* What the lock records under `.craftar/`, checked against the disk: every recorded file that is missing or
|
|
321
|
+
* whose normalized hash differs is pending, and so is a file the stack includes that the lock does not record.
|
|
322
|
+
* It reads the lock and the workspace only — no Forge, no plan, no Docker — and writes nothing.
|
|
323
|
+
*/
|
|
324
|
+
export async function recordedServices(root, lock) {
|
|
325
|
+
const all = lock?.files ?? [];
|
|
326
|
+
const entries = all.filter((e) => e.target === "services");
|
|
327
|
+
const out = { entries: entries.length, stack: entries.some((e) => e.path === STACK), services: [], pending: [], warnings: [] };
|
|
328
|
+
if (entries.length === 0)
|
|
329
|
+
return out;
|
|
330
|
+
if (!out.stack)
|
|
331
|
+
out.pending.push({ path: STACK, reason: "unrecorded" });
|
|
332
|
+
const intact = new Map();
|
|
333
|
+
for (const e of entries) {
|
|
334
|
+
const bytes = await fs.readFile(path.join(root, e.path)).catch(() => null);
|
|
335
|
+
if (bytes === null)
|
|
336
|
+
out.pending.push({ path: e.path, reason: "missing" });
|
|
337
|
+
else if (hashNormalized(bytes) !== e.hash)
|
|
338
|
+
out.pending.push({ path: e.path, reason: "edited" });
|
|
339
|
+
else
|
|
340
|
+
intact.set(e.path, bytes);
|
|
341
|
+
}
|
|
342
|
+
const stack = intact.get(STACK);
|
|
343
|
+
if (stack === undefined)
|
|
344
|
+
return out;
|
|
345
|
+
// Read include list through fragmentPaths (spec 31 §4.7: fragment reading is one place)
|
|
346
|
+
const includes = fragmentPaths(stack.toString("utf8"));
|
|
347
|
+
if (includes.length === 0)
|
|
348
|
+
throw new Error(`${STACK}: recorded by craftar.lock but not a craftar stack`);
|
|
349
|
+
for (const rel of includes) {
|
|
350
|
+
const entry = entries.find((e) => e.path === rel);
|
|
351
|
+
if (!entry) {
|
|
352
|
+
// The fragment was a collision at the last sync: a user's file, never handed to Compose.
|
|
353
|
+
out.pending.push({ path: rel, reason: "unrecorded" });
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const bytes = intact.get(rel);
|
|
357
|
+
if (bytes === undefined)
|
|
358
|
+
continue; // already pending
|
|
359
|
+
const f = parseFragment(bytes.toString("utf8"));
|
|
360
|
+
if (!f.ok)
|
|
361
|
+
out.warnings.push(`${rel}: cannot be read as a Compose fragment`);
|
|
362
|
+
else
|
|
363
|
+
for (const name of Object.keys(f.fragment.services ?? {}))
|
|
364
|
+
out.services.push({ name, ingredient: entry.ingredient });
|
|
365
|
+
}
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
/** The one test `up` (refuse) and `down` (pass `-f` or not) share (spec 31 §4.2, §4.3). */
|
|
369
|
+
export function canUseStack(r) {
|
|
370
|
+
return r.stack && r.pending.length === 0;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* The argv for `docker compose down`. Pure function for testability.
|
|
374
|
+
* @param project The project name (always passed with -p)
|
|
375
|
+
* @param r The recorded services state, or null if the lock could not be read
|
|
376
|
+
* @param d Docker version info
|
|
377
|
+
* @param stackPath The stack file path (e.g. /workspace/.craftar/compose.yaml)
|
|
378
|
+
* @param envFile The .env file path, or null if it does not exist
|
|
379
|
+
* @param volumes Whether to pass --volumes
|
|
380
|
+
*/
|
|
381
|
+
export function downArgv(project, r, d, stackPath, envFile, volumes) {
|
|
382
|
+
// Pass -f only when: r is not null AND canUseStack AND Compose is new enough
|
|
383
|
+
const useStack = r !== null && canUseStack(r) && !d.belowFloor;
|
|
384
|
+
const argv = ["compose", "-p", project];
|
|
385
|
+
if (useStack) {
|
|
386
|
+
argv.push("-f", stackPath);
|
|
387
|
+
if (envFile !== null)
|
|
388
|
+
argv.push("--env-file", envFile);
|
|
389
|
+
}
|
|
390
|
+
argv.push("down");
|
|
391
|
+
if (volumes)
|
|
392
|
+
argv.push("--volumes");
|
|
393
|
+
return argv;
|
|
394
|
+
}
|
|
395
|
+
/** One line per pending file, with the fix its reason calls for (spec 31 §4.2 step 2, §16.1). */
|
|
396
|
+
export function pendingLine(p) {
|
|
397
|
+
switch (p.reason) {
|
|
398
|
+
case "missing":
|
|
399
|
+
return `${p.path} — missing — run craftar sync`;
|
|
400
|
+
case "edited":
|
|
401
|
+
return `${p.path} — edited — run craftar drift discard ${p.path}: it regenerates the file, or removes it when the Forge no longer produces it`;
|
|
402
|
+
case "unrecorded":
|
|
403
|
+
return `${p.path} is not craftar's — run craftar status`;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/** Why `services up` will not run, before Docker is looked for; empty when it may (spec 31 §4.2 steps 1–2). */
|
|
407
|
+
export function upRefusal(r, names) {
|
|
408
|
+
const dash = names.find((n) => n.startsWith("-"));
|
|
409
|
+
if (dash !== undefined)
|
|
410
|
+
return [`service name ${JSON.stringify(dash)} starts with "-" — refused`];
|
|
411
|
+
if (r.entries === 0)
|
|
412
|
+
return ["no services in this workspace — craftar sync writes them when a recipe brings a service ingredient"];
|
|
413
|
+
return r.pending.map(pendingLine);
|
|
414
|
+
}
|
|
415
|
+
/** `docker compose` arguments, as an array for `execFile` / `spawn` — never a shell line. */
|
|
416
|
+
export function composeArgv(kind, o) {
|
|
417
|
+
const argv = ["compose", "-p", o.project];
|
|
418
|
+
if (o.stack !== null)
|
|
419
|
+
argv.push("-f", o.stack);
|
|
420
|
+
if (o.envFile !== null)
|
|
421
|
+
argv.push("--env-file", o.envFile);
|
|
422
|
+
if (kind === "ps")
|
|
423
|
+
return [...argv, "ps", "--all", "--format", "json"];
|
|
424
|
+
const names = o.services ?? [];
|
|
425
|
+
const dash = names.find((n) => n.startsWith("-"));
|
|
426
|
+
if (dash !== undefined)
|
|
427
|
+
throw new Error(`service name ${JSON.stringify(dash)} starts with "-" — refused`);
|
|
428
|
+
return [...argv, "up", "-d", ...(o.wait ? ["--wait"] : []), ...names];
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* `docker compose ps --format json`: a JSON array, or one object per line, or nothing. A row that is not a
|
|
432
|
+
* `ComposePsRow` is counted in `unread`, never dropped unseen; output that is neither form throws.
|
|
433
|
+
*/
|
|
434
|
+
export function readPs(stdout) {
|
|
435
|
+
const text = stdout.trim();
|
|
436
|
+
if (text === "")
|
|
437
|
+
return { containers: [], unread: 0 };
|
|
438
|
+
let rows;
|
|
439
|
+
try {
|
|
440
|
+
const whole = text.startsWith("[") ? JSON.parse(text) : text.split("\n").filter((l) => l.trim() !== "").map((l) => JSON.parse(l));
|
|
441
|
+
if (!Array.isArray(whole))
|
|
442
|
+
throw new Error("not an array");
|
|
443
|
+
rows = whole;
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
throw new Error("docker compose ps: output not understood");
|
|
447
|
+
}
|
|
448
|
+
const containers = [];
|
|
449
|
+
let unread = 0;
|
|
450
|
+
for (const row of rows) {
|
|
451
|
+
const r = ComposePsRow.safeParse(row);
|
|
452
|
+
if (!r.success) {
|
|
453
|
+
unread++;
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const ports = [];
|
|
457
|
+
for (const p of r.data.Publishers ?? []) {
|
|
458
|
+
// An unpublished port has PublishedPort 0; IPv4 and IPv6 publishers repeat one triple.
|
|
459
|
+
if (p.PublishedPort > 0 && !ports.some((x) => x.host === p.PublishedPort && x.container === p.TargetPort && x.protocol === p.Protocol))
|
|
460
|
+
ports.push({ host: p.PublishedPort, container: p.TargetPort, protocol: p.Protocol });
|
|
461
|
+
}
|
|
462
|
+
containers.push({ service: r.data.Service, state: r.data.State, health: r.data.Health ? r.data.Health : null, ports });
|
|
463
|
+
}
|
|
464
|
+
return { containers, unread };
|
|
465
|
+
}
|
|
466
|
+
/** Numeric, segment by segment: 2.9.0 is older than 2.20.0. */
|
|
467
|
+
export function compareVersions(a, b) {
|
|
468
|
+
const x = a.split(".").map(Number);
|
|
469
|
+
const y = b.split(".").map(Number);
|
|
470
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
471
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
472
|
+
if (d !== 0)
|
|
473
|
+
return d < 0 ? -1 : 1;
|
|
474
|
+
}
|
|
475
|
+
return 0;
|
|
476
|
+
}
|
|
477
|
+
/** `docker compose version --short` (spec 31 §4.6). The runner rejects when `docker` cannot be started. */
|
|
478
|
+
export async function dockerVersion(run) {
|
|
479
|
+
let r;
|
|
480
|
+
try {
|
|
481
|
+
r = await run("docker", ["compose", "version", "--short"]);
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
return { available: false, compose: null, belowFloor: false, reason: "no docker" };
|
|
485
|
+
}
|
|
486
|
+
const m = /^v?(\d+(?:\.\d+)*)/.exec(r.stdout.trim());
|
|
487
|
+
if (r.code !== 0 || m === null)
|
|
488
|
+
return { available: false, compose: null, belowFloor: false, reason: "no compose plugin" };
|
|
489
|
+
const compose = m[1]; // just the numeric version, no suffix like "-desktop.1"
|
|
490
|
+
const belowFloor = compareVersions(compose, MIN_COMPOSE) < 0;
|
|
491
|
+
return { available: true, compose, belowFloor, reason: belowFloor ? "too old" : null };
|
|
492
|
+
}
|
|
493
|
+
/** The message and fix for a Docker that cannot serve the command; null when it can. `down` and `status` do not need the floor. */
|
|
494
|
+
export function dockerFinding(d, needFloor) {
|
|
495
|
+
if (d.reason === "no docker")
|
|
496
|
+
return { message: "docker not found", fix: DOCKER_FIX };
|
|
497
|
+
if (d.reason === "no compose plugin")
|
|
498
|
+
return { message: "the docker compose plugin is missing", fix: DOCKER_FIX };
|
|
499
|
+
if (d.reason === "too old" && needFloor)
|
|
500
|
+
return { message: `Docker Compose ${d.compose} is older than ${MIN_COMPOSE}`, fix: "upgrade Docker Compose" };
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
/** The line and fix for a Docker that cannot serve the command; null when it can. `down` and `status` do not need the floor. */
|
|
504
|
+
export function dockerFault(d, needFloor) {
|
|
505
|
+
const finding = dockerFinding(d, needFloor);
|
|
506
|
+
return finding ? `${finding.message} — ${finding.fix}` : null;
|
|
507
|
+
}
|
|
508
|
+
/** Runs `docker` with an argument array and no shell; resolves with the exit code, rejects when it cannot start. */
|
|
509
|
+
export const dockerRunner = (file, args) => new Promise((resolve, reject) => {
|
|
510
|
+
execFile(file, args, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
511
|
+
if (error === null)
|
|
512
|
+
return resolve({ code: 0, stdout, stderr });
|
|
513
|
+
// A numeric code is the child's exit code; anything else (ENOENT, EACCES, a signal) means it did not run to an exit.
|
|
514
|
+
if (typeof error.code === "number")
|
|
515
|
+
return resolve({ code: error.code, stdout, stderr });
|
|
516
|
+
reject(error);
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
/** The report of `services status`. `ps` is null when Docker was not asked or did not answer; `psFault` says why. Pure. */
|
|
520
|
+
export function statusReport(realRoot, r, d, ps, psFault) {
|
|
521
|
+
const services = [];
|
|
522
|
+
const rows = ps?.containers ?? [];
|
|
523
|
+
for (const s of r.services) {
|
|
524
|
+
const mine = rows.filter((c) => c.service === s.name);
|
|
525
|
+
if (mine.length === 0)
|
|
526
|
+
services.push({ name: s.name, ingredient: s.ingredient, state: "not created", health: null, ports: [] });
|
|
527
|
+
for (const c of mine)
|
|
528
|
+
services.push({ name: s.name, ingredient: s.ingredient, state: c.state, health: c.health, ports: c.ports });
|
|
529
|
+
}
|
|
530
|
+
// A container of the project whose service no recorded fragment declares.
|
|
531
|
+
for (const c of rows) {
|
|
532
|
+
if (!r.services.some((s) => s.name === c.service))
|
|
533
|
+
services.push({ name: c.service, ingredient: null, state: c.state, health: c.health, ports: c.ports });
|
|
534
|
+
}
|
|
535
|
+
const warnings = [...r.warnings];
|
|
536
|
+
if (psFault !== null)
|
|
537
|
+
warnings.push(psFault);
|
|
538
|
+
if (ps !== null && ps.unread > 0)
|
|
539
|
+
warnings.push(`docker compose ps: ${ps.unread} row(s) not understood`);
|
|
540
|
+
return {
|
|
541
|
+
workspace: realRoot,
|
|
542
|
+
project: projectName(realRoot),
|
|
543
|
+
composeFile: r.stack ? STACK : null,
|
|
544
|
+
docker: { available: d.available, compose: d.compose },
|
|
545
|
+
services,
|
|
546
|
+
pending: r.pending,
|
|
547
|
+
warnings,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
//# sourceMappingURL=services.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"services.js","sourceRoot":"","sources":["../../src/core/services.ts"],"names":[],"mappings":"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"}
|
package/dist/core/sync.js
CHANGED
|
@@ -16,6 +16,7 @@ import { claudeCode } from "../emitters/claude-code.js";
|
|
|
16
16
|
import { kiro } from "../emitters/kiro.js";
|
|
17
17
|
import { agentsMd } from "../emitters/agents-md.js";
|
|
18
18
|
import { written } from "./capabilities.js";
|
|
19
|
+
import { planServices } from "./services.js";
|
|
19
20
|
export const WORKSPACE_FILE = "craftar.yaml";
|
|
20
21
|
export const LOCAL_FILE = "craftar.local.yaml";
|
|
21
22
|
export const LOCK_FILE = "craftar.lock";
|
|
@@ -359,8 +360,11 @@ export function assertInsideWorkspace(files) {
|
|
|
359
360
|
export async function plan(ws) {
|
|
360
361
|
const resolution = resolve(ws.forge, ws.config);
|
|
361
362
|
const warnings = [...ws.warnings, ...resolution.warnings];
|
|
363
|
+
const hasService = resolution.ingredients.some((ing) => ing.meta.type === "service");
|
|
362
364
|
if (resolution.targets.length === 0) {
|
|
363
|
-
warnings.push(
|
|
365
|
+
warnings.push(hasService
|
|
366
|
+
? "no target resolved — only the service files under .craftar/ are written"
|
|
367
|
+
: "no targets resolved — nothing will be emitted and every file in craftar.lock becomes an orphan (an empty list in craftar.local.yaml replaces the workspace's)");
|
|
364
368
|
}
|
|
365
369
|
const sections = await sectionPass(ws.forge, resolution, warnings);
|
|
366
370
|
/** Unresolved placeholder → refs of the ingredients citing it. */
|
|
@@ -421,6 +425,10 @@ export async function plan(ws) {
|
|
|
421
425
|
}
|
|
422
426
|
files.push(...(await emitFor(t, em, ctx)));
|
|
423
427
|
}
|
|
428
|
+
// Service files are planned after the target loop (spec 31 §5.3, choice 24) and before the
|
|
429
|
+
// containment check, so assertInsideWorkspace, the missing-param collection and the duplicate-path
|
|
430
|
+
// guard all cover the service files.
|
|
431
|
+
files.push(...(await planServices(ctx)));
|
|
424
432
|
assertInsideWorkspace(files);
|
|
425
433
|
const missing = [];
|
|
426
434
|
const declared = declaredWithoutValue({ resolution });
|
|
@@ -603,16 +611,22 @@ export async function apply(ws, p, statuses, opts = {}) {
|
|
|
603
611
|
break;
|
|
604
612
|
}
|
|
605
613
|
}
|
|
606
|
-
// Schema
|
|
607
|
-
const
|
|
608
|
-
|
|
614
|
+
// Schema 3 when any entry has target `services`; otherwise schema 2 (spec 31 §5.2, spec 13 §5.2).
|
|
615
|
+
const sortedEntries = entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
616
|
+
const hasServices = entries.some((e) => e.target === "services");
|
|
617
|
+
// Build the shared part once; the two branches differ only in `schema` and in the entry type.
|
|
618
|
+
const base = {
|
|
609
619
|
forge: { source: ws.config.forge, ref: ws.origin.ref, commit: ws.forge.commit },
|
|
610
620
|
profile: ws.config.profile,
|
|
611
621
|
recipes: p.resolution.recipes,
|
|
612
622
|
targets: p.resolution.targets,
|
|
613
623
|
generatedAt: new Date().toISOString(),
|
|
614
|
-
files: entries.sort((a, b) => a.path.localeCompare(b.path)),
|
|
615
624
|
};
|
|
625
|
+
const built = hasServices
|
|
626
|
+
? { schema: 3, ...base, files: sortedEntries }
|
|
627
|
+
// The narrowing cast is sound: hasServices is false, so no entry has target "services",
|
|
628
|
+
// thus every entry's target is a Target (claude-code | kiro | agents-md).
|
|
629
|
+
: { schema: 2, ...base, files: sortedEntries };
|
|
616
630
|
// Rewritten only when its content changes: generatedAt is when it last did (Ruling 10). A schema 1
|
|
617
631
|
// lock always differs, so the first sync with this craftar upgrades it.
|
|
618
632
|
const onDisk = opts.dryRun ? null : await readLock(ws.root);
|
|
@@ -629,5 +643,9 @@ async function writeFile(root, f) {
|
|
|
629
643
|
const abs = path.join(root, f.path);
|
|
630
644
|
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
631
645
|
await fs.writeFile(abs, f.content);
|
|
646
|
+
// Set mode after writing (fs.writeFile's mode applies only on creation, so chmod is needed for updates).
|
|
647
|
+
if (f.mode !== undefined && process.platform !== "win32") {
|
|
648
|
+
await fs.chmod(abs, f.mode);
|
|
649
|
+
}
|
|
632
650
|
}
|
|
633
651
|
//# sourceMappingURL=sync.js.map
|