@datumos/datum-cli 0.0.0 → 0.589.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.
@@ -0,0 +1,495 @@
1
+ /**
2
+ * `datum space-manifest pull|push|init <dir>` (#1809) — a space's
3
+ * configuration kept as files in a repository, without Terraform and
4
+ * without a state file (docs/space-admin/space-repository.md).
5
+ *
6
+ * Hand-written next to the derived `space-manifest list|set`, because each
7
+ * is a walk of a directory around one API call: `pull` turns
8
+ * GET /api/space-manifest into reviewable files, `push` assembles them back
9
+ * and sends PUT /api/space-manifest (a dry run plans, without it applies),
10
+ * `init` writes the repository around them. The layout itself is
11
+ * spaceManifestDir.ts; the repository template is spaceRepoTemplate.ts.
12
+ *
13
+ * The server stays the authority: it plans against the live space, checks
14
+ * references and refuses organisation-tier kinds. The CLI only refuses
15
+ * what must never be sent — secret material — and an org-tier kind early.
16
+ */
17
+ import fs from "node:fs";
18
+ import path from "node:path";
19
+ import { apiSurface, UsageError } from "./invoke.js";
20
+ import { SPACE_FILE, SpaceManifestLayoutError, buildManifest, checkManifest, countWrites, isLayoutPath, jsonFile, renderPlan, splitManifest, } from "./spaceManifestDir.js";
21
+ import { SPACE_REPO_DATUM_OWNED, SPACE_REPO_KEY_SECRET, SPACE_REPO_MANAGED_BLOCK_FILES, recommendedSpaceRepoName, refreshManagedFile, spaceRepoFiles, } from "./spaceRepoTemplate.js";
22
+ const SPACE_MANIFEST_PATH = "/api/space-manifest";
23
+ /** The server's space-slug contract (`spaceSlugProblem`,
24
+ * server/src/spaces/spaces.interfaces.ts), mirrored because the CLI package
25
+ * cannot import the server; `spaceManifestCommand.test.ts` pins the two to
26
+ * the same verdicts. `init` must not write a repository for a slug no
27
+ * space can have. */
28
+ const SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
29
+ export const RESERVED_SPACE_SLUGS_MIRROR = ["www", "api", "auth", "status", "docs", "admin", "login", "llm", "relay", "push", "gw"];
30
+ export function spaceSlugProblemMirror(slug) {
31
+ if (slug.length < 2 || slug.length > 40)
32
+ return "slug must be 2-40 characters";
33
+ if (!SLUG_RE.test(slug))
34
+ return "slug must be lowercase letters, digits and inner hyphens";
35
+ if (RESERVED_SPACE_SLUGS_MIRROR.includes(slug))
36
+ return `slug "${slug}" is reserved`;
37
+ if (slug.startsWith("xn--"))
38
+ return 'slug must not start with "xn--" (reserved for internationalised names)';
39
+ return null;
40
+ }
41
+ function line(stream, text) {
42
+ stream.write(`${text}\n`);
43
+ }
44
+ /** A refusal from the server, already human-readable. */
45
+ class ManifestRefused extends Error {
46
+ }
47
+ /** The contract and the shape check on the space's host (#1824) — readable
48
+ * by any member, unlike the manifest itself. */
49
+ const SPACE_MANIFEST_SCHEMA_PATH = `${SPACE_MANIFEST_PATH}/schema`;
50
+ const SPACE_MANIFEST_VALIDATE_PATH = `${SPACE_MANIFEST_PATH}/validate`;
51
+ /** One JSON call on the space-manifest surface; a non-2xx becomes a
52
+ * ManifestRefused with the server's issues and a hint for the admin 403. */
53
+ async function callJson(method, apiPath, config, io, body) {
54
+ const surface = apiSurface(config);
55
+ const url = surface.url(apiPath);
56
+ const res = await (io.fetchImpl ?? fetch)(url, {
57
+ method,
58
+ headers: {
59
+ ...surface.headers,
60
+ accept: "application/json",
61
+ ...(body !== undefined ? { "content-type": "application/json" } : {}),
62
+ },
63
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
64
+ redirect: "manual",
65
+ });
66
+ const text = await res.text();
67
+ let parsed = {};
68
+ try {
69
+ parsed = JSON.parse(text);
70
+ }
71
+ catch {
72
+ // not JSON — the message below carries the status and a snippet
73
+ }
74
+ if (!res.ok) {
75
+ const detail = typeof parsed.error === "string" ? parsed.error : text.slice(0, 200).trim();
76
+ const code = typeof parsed.code === "string" ? ` [${parsed.code}]` : "";
77
+ const issues = Array.isArray(parsed.issues)
78
+ ? parsed.issues.map((i) => `\n ${i.path ?? "(document)"}: ${i.message ?? ""}`).join("")
79
+ : "";
80
+ const hint = res.status === 403 && /\b(?:admin|operator)\b/.test(detail)
81
+ ? "\n Reading and planning the manifest need the operator role in the space, applying it the admin role" +
82
+ " (#1833): `datum whoami --space <slug>` shows yours. A space repository applies with a key bound to" +
83
+ ` the space with role admin (its ${SPACE_REPO_KEY_SECRET}); plan with --dry-run.`
84
+ : "";
85
+ throw new ManifestRefused(`HTTP ${res.status}${code} — ${detail || "no detail"} (${method} ${url})${issues}${hint}`);
86
+ }
87
+ return { status: res.status, parsed, text, url };
88
+ }
89
+ async function callManifest(method, config, io, body) {
90
+ const { status, parsed, text, url } = await callJson(method, SPACE_MANIFEST_PATH, config, io, body);
91
+ // A 2xx that is not the expected envelope (a proxy's HTML page, a body
92
+ // cut short) must never pass as an empty manifest: `pull` would clear
93
+ // the directory, `push` would report an empty plan as success.
94
+ const valid = method === "GET"
95
+ ? typeof parsed.apiVersion === "string" && typeof parsed.spaceId === "string"
96
+ : typeof parsed.spaceId === "string" && Array.isArray(parsed.plan) && typeof parsed.applied === "boolean";
97
+ if (!valid) {
98
+ throw new ManifestRefused(`HTTP ${status} with an unexpected answer — not a space manifest response (${method} ${url}): ${text.slice(0, 200).trim() || "(empty body)"}`);
99
+ }
100
+ return parsed;
101
+ }
102
+ /** `pull` records which space the directory mirrors (its id); `push`
103
+ * refuses to send the files to another one. A dotfile, so the layout
104
+ * never reads it as a kind. */
105
+ export const SPACE_ID_MARKER = ".space-id";
106
+ /** Every file under `dir` the layout owns (known kinds only), relative and `/`-separated. */
107
+ function readLayout(dir, extraKinds = new Set()) {
108
+ const files = {};
109
+ for (const name of fs.readdirSync(dir)) {
110
+ if (name.startsWith("."))
111
+ continue;
112
+ const abs = path.join(dir, name);
113
+ const stat = fs.lstatSync(abs);
114
+ if (stat.isFile() && isLayoutPath(name, extraKinds))
115
+ files[name] = fs.readFileSync(abs, "utf8");
116
+ if (!stat.isDirectory())
117
+ continue;
118
+ for (const child of fs.readdirSync(abs)) {
119
+ const rel = `${name}/${child}`;
120
+ const childAbs = path.join(abs, child);
121
+ if (fs.lstatSync(childAbs).isFile() && isLayoutPath(rel, extraKinds)) {
122
+ files[rel] = fs.readFileSync(childAbs, "utf8");
123
+ }
124
+ }
125
+ }
126
+ return files;
127
+ }
128
+ /** The first symlink among <dir>, the kind folders and the files `rels`
129
+ * name (relative to <dir>) — null when every existing component is real. */
130
+ function symlinkOnPath(dir, rels) {
131
+ const isLink = (p) => {
132
+ try {
133
+ return fs.lstatSync(p).isSymbolicLink();
134
+ }
135
+ catch {
136
+ return false; // absent — created as a real file or folder
137
+ }
138
+ };
139
+ if (isLink(dir))
140
+ return dir;
141
+ const seen = new Set();
142
+ for (const rel of rels) {
143
+ const parts = rel.split("/");
144
+ for (let i = 1; i <= parts.length; i += 1) {
145
+ const p = path.join(dir, ...parts.slice(0, i));
146
+ if (seen.has(p))
147
+ continue;
148
+ seen.add(p);
149
+ if (isLink(p))
150
+ return p;
151
+ }
152
+ }
153
+ // Kind folders the layout would read (and empty) even if the space no
154
+ // longer has them.
155
+ for (const name of fs.existsSync(dir) ? fs.readdirSync(dir) : []) {
156
+ const p = path.join(dir, name);
157
+ if (isLayoutPath(`${name}/x.json`) && isLink(p))
158
+ return p;
159
+ }
160
+ return null;
161
+ }
162
+ function readMarker(dir) {
163
+ try {
164
+ return fs.readFileSync(path.join(dir, SPACE_ID_MARKER), "utf8").trim() || null;
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ }
170
+ // -------------------------------------------------------------------- pull
171
+ export async function runSpaceManifestPull(args, config, io) {
172
+ const dir = path.resolve(args.dir);
173
+ if (fs.existsSync(dir)) {
174
+ if (!fs.statSync(dir).isDirectory()) {
175
+ line(io.err, `datum: ${args.dir} is not a directory`);
176
+ return 2;
177
+ }
178
+ // Never clear a directory that is not a manifest directory: `pull .` in
179
+ // a repository root must not touch the README, the workflows or src/.
180
+ const all = fs.readdirSync(dir);
181
+ if (all.includes(".git") || all.includes(".github")) {
182
+ line(io.err, `datum: ${args.dir} is a repository root — pull into a folder of it, e.g. \`datum space-manifest pull space\``);
183
+ return 2;
184
+ }
185
+ const entries = all.filter((n) => !n.startsWith("."));
186
+ if (entries.length > 0 && !entries.includes(SPACE_FILE)) {
187
+ line(io.err, `datum: ${args.dir} holds files but no ${SPACE_FILE} — pull writes into an empty directory or one it wrote before (e.g. \`datum space-manifest pull space\`)`);
188
+ return 2;
189
+ }
190
+ }
191
+ let live;
192
+ try {
193
+ live = await callManifest("GET", config, io);
194
+ }
195
+ catch (err) {
196
+ return reportFailure(err, io);
197
+ }
198
+ const { files, skipped } = splitManifest(live);
199
+ const spaceId = typeof live.spaceId === "string" ? live.spaceId : null;
200
+ const marker = fs.existsSync(dir) ? readMarker(dir) : null;
201
+ if (marker && spaceId && marker !== spaceId) {
202
+ line(io.err, `datum: ${args.dir} mirrors the space ${marker}, but this credential answers for the space ${spaceId} — ` +
203
+ "a key is bound to ONE space whatever --space says; use that space's key (or pull into another directory)");
204
+ return 1;
205
+ }
206
+ // Every write below follows the path it is given — a symlinked <dir>,
207
+ // kind folder or file (a checked-in `space/agents -> ../..`) would carry
208
+ // them outside the directory, on a self-hosted runner too. Refuse before
209
+ // the first deletion.
210
+ const symlink = symlinkOnPath(dir, [...Object.keys(files), SPACE_ID_MARKER]);
211
+ if (symlink) {
212
+ line(io.err, `datum: ${symlink} is a symbolic link — pull writes only real files and folders inside ${args.dir}`);
213
+ return 1;
214
+ }
215
+ // The directory mirrors the space: a layout file whose entry is gone
216
+ // disappears (and is named, so an uncommitted hand-written entry is not
217
+ // lost silently); files outside the layout are never touched.
218
+ const removed = [];
219
+ if (fs.existsSync(dir)) {
220
+ for (const rel of Object.keys(readLayout(dir))) {
221
+ if (!(rel in files))
222
+ removed.push(rel);
223
+ fs.rmSync(path.join(dir, rel));
224
+ }
225
+ for (const name of fs.readdirSync(dir)) {
226
+ const abs = path.join(dir, name);
227
+ if (isLayoutPath(`${name}/x.json`) && fs.lstatSync(abs).isDirectory() && fs.readdirSync(abs).length === 0) {
228
+ fs.rmdirSync(abs);
229
+ }
230
+ }
231
+ }
232
+ for (const [rel, content] of Object.entries(files)) {
233
+ const target = path.join(dir, rel);
234
+ fs.mkdirSync(path.dirname(target), { recursive: true });
235
+ fs.writeFileSync(target, content);
236
+ }
237
+ if (spaceId)
238
+ fs.writeFileSync(path.join(dir, SPACE_ID_MARKER), `${spaceId}\n`);
239
+ line(io.out, `pulled ${Object.keys(files).length} files into ${args.dir}${spaceId ? ` (space ${spaceId})` : ""}`);
240
+ for (const rel of removed.sort())
241
+ line(io.out, ` removed ${rel} — not in the space`);
242
+ const org = skipped.filter((s) => s.reason.startsWith("organisation")).map((s) => s.kind);
243
+ const empty = skipped.filter((s) => s.reason.startsWith("empty")).map((s) => s.kind);
244
+ const unknown = skipped.filter((s) => s.reason.startsWith("unknown")).map((s) => s.kind);
245
+ if (org.length)
246
+ line(io.out, ` left out (organisation tier, the org admin's): ${org.join(", ")}`);
247
+ if (empty.length)
248
+ line(io.out, ` left out (empty in the space): ${empty.join(", ")}`);
249
+ if (unknown.length) {
250
+ line(io.err, `warning: left out kinds this CLI does not know — run \`datum update\`, then pull again: ${unknown.join(", ")}`);
251
+ }
252
+ return 0;
253
+ }
254
+ export async function runSpaceManifestPush(args, config, io) {
255
+ const dir = path.resolve(args.dir);
256
+ if (!fs.existsSync(path.join(dir, SPACE_FILE))) {
257
+ line(io.err, `datum: ${args.dir} has no ${SPACE_FILE} — run \`datum space-manifest pull ${args.dir}\` first`);
258
+ return 2;
259
+ }
260
+ let spec;
261
+ try {
262
+ spec = buildManifest(readLayout(dir));
263
+ }
264
+ catch (err) {
265
+ if (err instanceof SpaceManifestLayoutError) {
266
+ line(io.err, `datum: ${args.dir}/${err.message}`);
267
+ return 1;
268
+ }
269
+ throw err;
270
+ }
271
+ const { errors, warnings } = checkManifest(spec);
272
+ for (const w of warnings)
273
+ line(io.err, `warning: ${w}`);
274
+ if (errors.length > 0) {
275
+ for (const e of errors)
276
+ line(io.err, `datum: ${e}`);
277
+ return 1;
278
+ }
279
+ // A key is bound to ONE space whatever host or --space it is sent to, so
280
+ // the wrong repository secret would apply this space's files to another
281
+ // space. The directory remembers which space `pull` read: a dry run
282
+ // first names the space the credential answers for, and an apply goes
283
+ // ahead only when that is the same one.
284
+ const marker = readMarker(dir);
285
+ if (!marker) {
286
+ line(io.err, `datum: ${args.dir}/${SPACE_ID_MARKER} is missing — it names the space these files belong to; ` +
287
+ `\`datum space-manifest pull ${args.dir}\` writes it (with the key of that space)`);
288
+ return 2;
289
+ }
290
+ let answer;
291
+ try {
292
+ const body = { spec, prune: args.prune };
293
+ answer = (await callManifest("PUT", config, io, { ...body, dryRun: true }));
294
+ if (answer.spaceId !== marker) {
295
+ line(io.err, `datum: ${args.dir} mirrors the space ${marker}, but this credential answers for the space ${answer.spaceId} — ` +
296
+ "nothing was written; use the key bound to the directory's space");
297
+ return 1;
298
+ }
299
+ if (!args.dryRun)
300
+ answer = (await callManifest("PUT", config, io, { ...body, dryRun: false }));
301
+ }
302
+ catch (err) {
303
+ return reportFailure(err, io);
304
+ }
305
+ const markdown = renderPlan(answer, args.dryRun ? "Datum space plan" : "Datum space apply");
306
+ if (args.markdown)
307
+ fs.writeFileSync(args.markdown, markdown);
308
+ io.out.write(args.json ? jsonFile(answer) : markdown);
309
+ if (args.expectEmpty && countWrites(answer) > 0) {
310
+ line(io.err, `datum: expected an empty plan, got ${countWrites(answer)} write(s) — the space and the files disagree ` +
311
+ "(a value the server normalises: write it the way the plan shows; or an edit racing this run)");
312
+ return 1;
313
+ }
314
+ return 0;
315
+ }
316
+ // ---------------------------------------------------------------- validate
317
+ /**
318
+ * `validate <dir>` (#1824): assembles the files the way `push` does — the
319
+ * `$file` references, the local refusals of key material and org-tier
320
+ * kinds — and sends the document to the shape check on the space's host.
321
+ * Any member may run it (no admin role, no `.space-id`, nothing written):
322
+ * the check an agent runs before it opens a pull request. References bound
323
+ * to the live space (credential labels, board queries) wait for the plan.
324
+ */
325
+ export async function runSpaceManifestValidate(args, config, io) {
326
+ const dir = path.resolve(args.dir);
327
+ if (!fs.existsSync(path.join(dir, SPACE_FILE))) {
328
+ line(io.err, `datum: ${args.dir} has no ${SPACE_FILE} — validate reads a directory \`pull\` wrote (e.g. space/)`);
329
+ return 2;
330
+ }
331
+ let spec;
332
+ try {
333
+ spec = buildManifest(readLayout(dir));
334
+ }
335
+ catch (err) {
336
+ if (err instanceof SpaceManifestLayoutError) {
337
+ line(io.err, `datum: ${args.dir}/${err.message}`);
338
+ return 1;
339
+ }
340
+ throw err;
341
+ }
342
+ const { errors, warnings } = checkManifest(spec);
343
+ for (const w of warnings)
344
+ line(io.err, `warning: ${w}`);
345
+ if (errors.length > 0) {
346
+ for (const e of errors)
347
+ line(io.err, `datum: ${e}`);
348
+ return 1;
349
+ }
350
+ let checked;
351
+ try {
352
+ const { parsed, text, url } = await callJson("POST", SPACE_MANIFEST_VALIDATE_PATH, config, io, { spec });
353
+ if (parsed.ok !== true) {
354
+ throw new ManifestRefused(`unexpected answer from POST ${url}: ${text.slice(0, 200).trim() || "(empty body)"}`);
355
+ }
356
+ checked = parsed.checked;
357
+ }
358
+ catch (err) {
359
+ return reportFailure(err, io);
360
+ }
361
+ const kinds = Object.keys(spec).filter((k) => k !== "apiVersion");
362
+ const entries = kinds.reduce((n, k) => n + (Array.isArray(spec[k]) ? spec[k].length : 1), 0);
363
+ // Below admin the server checks the shape only (its reference checks read
364
+ // admin/org data) — say so, so "valid" is not read as "the plan will pass".
365
+ const scope = checked === "shape"
366
+ ? ` (shape only — run \`datum space-manifest push ${args.dir} --dry-run\` as a space admin for the full check)`
367
+ : checked === "full"
368
+ ? " (the dry run's full check, without the plan)"
369
+ : "";
370
+ line(io.out, `${args.dir}: valid${scope} — ${kinds.length} kinds, ${entries} entries. Next: \`datum space-manifest push ${args.dir} --dry-run\` (or a pull request) for the plan against the live space.`);
371
+ return 0;
372
+ }
373
+ // ------------------------------------------------------------------ schema
374
+ /**
375
+ * `schema [kind]` (#1824): the manifest's JSON Schema from the space's
376
+ * host — whole, or the one kind an agent is about to write (its fields,
377
+ * their descriptions and the x-datum-* metadata). An unknown kind lists
378
+ * the kinds this instance offers.
379
+ */
380
+ export async function runSpaceManifestSchema(args, config, io) {
381
+ let schema;
382
+ try {
383
+ ({ parsed: schema } = await callJson("GET", SPACE_MANIFEST_SCHEMA_PATH, config, io));
384
+ }
385
+ catch (err) {
386
+ return reportFailure(err, io);
387
+ }
388
+ const properties = (schema.properties ?? {});
389
+ if (args.kind === undefined) {
390
+ io.out.write(jsonFile(schema));
391
+ return 0;
392
+ }
393
+ if (!(args.kind in properties)) {
394
+ const kinds = Object.keys(properties).filter((k) => k !== "apiVersion").sort();
395
+ line(io.err, `datum: no manifest kind "${args.kind}" on this instance — kinds: ${kinds.join(", ")}`);
396
+ return 2;
397
+ }
398
+ io.out.write(jsonFile(properties[args.kind]));
399
+ return 0;
400
+ }
401
+ // -------------------------------------------------------------------- init
402
+ export function runSpaceManifestInit(args, io) {
403
+ if (!args.space) {
404
+ line(io.err, "datum: init needs the space — pass --space <slug> (or set DATUM_SPACE)");
405
+ return 2;
406
+ }
407
+ const slugProblem = spaceSlugProblemMirror(args.space);
408
+ if (slugProblem) {
409
+ line(io.err, `datum: "${args.space}" is not a space slug — ${slugProblem}`);
410
+ return 2;
411
+ }
412
+ const dir = path.resolve(args.dir);
413
+ const files = spaceRepoFiles(args.space);
414
+ const symlink = symlinkOnPath(dir, Object.keys(files));
415
+ if (symlink) {
416
+ line(io.err, `datum: ${symlink} is a symbolic link — init writes only real files and folders inside ${args.dir}`);
417
+ return 1;
418
+ }
419
+ const written = [];
420
+ const refreshed = [];
421
+ const kept = [];
422
+ for (const [rel, content] of Object.entries(files)) {
423
+ const target = path.join(dir, rel);
424
+ if (fs.existsSync(target)) {
425
+ // AGENTS.md / CLAUDE.md: Datum's block is refreshed on every run
426
+ // (#1824) — the rules move with Datum, the team's notes stay.
427
+ if (SPACE_REPO_MANAGED_BLOCK_FILES.has(rel)) {
428
+ const next = refreshManagedFile(fs.readFileSync(target, "utf8"), content);
429
+ if (next === null) {
430
+ kept.push(rel);
431
+ }
432
+ else {
433
+ fs.writeFileSync(target, next);
434
+ refreshed.push(rel);
435
+ }
436
+ continue;
437
+ }
438
+ // The workflows: Datum's whole, replaced only on --refresh.
439
+ if (args.refresh && SPACE_REPO_DATUM_OWNED.has(rel)) {
440
+ if (fs.readFileSync(target, "utf8") !== content) {
441
+ fs.writeFileSync(target, content);
442
+ refreshed.push(rel);
443
+ }
444
+ else {
445
+ kept.push(rel);
446
+ }
447
+ continue;
448
+ }
449
+ // A repository created in Cloud Market may already carry a
450
+ // README or a .gitignore: never overwrite a person's file.
451
+ kept.push(rel);
452
+ continue;
453
+ }
454
+ fs.mkdirSync(path.dirname(target), { recursive: true });
455
+ fs.writeFileSync(target, content);
456
+ written.push(rel);
457
+ }
458
+ for (const rel of written)
459
+ line(io.out, `wrote ${rel}`);
460
+ for (const rel of refreshed)
461
+ line(io.out, `refreshed ${rel} (Datum's part, current version)`);
462
+ for (const rel of kept) {
463
+ const hint = SPACE_REPO_DATUM_OWNED.has(rel) && !args.refresh
464
+ ? " — `--refresh` replaces it with the current version"
465
+ : SPACE_REPO_MANAGED_BLOCK_FILES.has(rel)
466
+ ? " — already current"
467
+ : " — compare it with `datum space-manifest init <empty dir>`";
468
+ line(io.out, `kept ${rel} (exists${hint})`);
469
+ }
470
+ if (refreshed.length > 0 && written.length === 0) {
471
+ line(io.out, "");
472
+ line(io.out, "Commit the refreshed files in a pull request.");
473
+ return 0;
474
+ }
475
+ line(io.out, "");
476
+ line(io.out, `Next, for the space "${args.space}" (recommended repository name: ${recommendedSpaceRepoName(args.space)}):`);
477
+ line(io.out, " 1. commit and push these files;");
478
+ line(io.out, " 2. repository variable DATUM_BASE_URL = the organisation host;");
479
+ line(io.out, ` 3. a space admin mints a key bound to the space (panel: Settings → API keys, role admin) and stores it as the repository secret ${SPACE_REPO_KEY_SECRET} — never in a file or a chat;`);
480
+ line(io.out, ' 4. Settings → Actions → General → "Allow GitHub Actions to create and approve pull requests" (the sync opens pull requests);');
481
+ line(io.out, " 5. Actions → Datum space sync → Run workflow, then merge the import pull request.");
482
+ return 0;
483
+ }
484
+ function reportFailure(err, io) {
485
+ if (err instanceof ManifestRefused) {
486
+ line(io.err, `datum: ${err.message}`);
487
+ return 1;
488
+ }
489
+ if (err instanceof UsageError) {
490
+ line(io.err, `datum: ${err.message}`);
491
+ return 2;
492
+ }
493
+ throw err;
494
+ }
495
+ //# sourceMappingURL=spaceManifestCommand.js.map