@fluid-app/fluid-cli-droplets 0.1.199 → 0.1.200
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/dist/index.mjs +86 -75
- package/package.json +1 -1
- package/src/__tests__/manifest.test.ts +40 -1
- package/src/commands/manifest.ts +41 -1
package/dist/index.mjs
CHANGED
|
@@ -494,81 +494,6 @@ async function publishManifest(manifest, baseUrl, dryRun, allowWidening) {
|
|
|
494
494
|
});
|
|
495
495
|
}
|
|
496
496
|
//#endregion
|
|
497
|
-
//#region src/commands/manifest.ts
|
|
498
|
-
/** Public: Register local validation and Actions-only manifest publication commands. */
|
|
499
|
-
function registerManifestCommands(parent, ctx) {
|
|
500
|
-
const manifest = parent.command("manifest").description(`Droplet manifests. ${AUTHORITY}`);
|
|
501
|
-
for (const verb of [
|
|
502
|
-
"validate",
|
|
503
|
-
"publish",
|
|
504
|
-
"diff"
|
|
505
|
-
]) {
|
|
506
|
-
const cmd = manifest.command(`${verb} [path]`).description(`${verb === "validate" ? "Validate locally" : verb === "diff" ? "Preview in GitHub Actions; exit 3 for widening (permissions: id-token: write)" : "Publish in GitHub Actions (permissions: id-token: write)"}. ${AUTHORITY}`).option("--json", "Print one JSON document");
|
|
507
|
-
if (verb === "publish") cmd.option("--allow-widening", "Permit widening when publishing; previews always report widening (exit 3)");
|
|
508
|
-
if (verb === "publish") cmd.option("--dry-run", "Preview without publishing");
|
|
509
|
-
cmd.action(async (path, opts, command) => {
|
|
510
|
-
const output = (data) => opts.json ? process.stdout.write(JSON.stringify(data) + "\n") : ctx.output(data);
|
|
511
|
-
try {
|
|
512
|
-
const file = path ?? "./fluid.droplet.json";
|
|
513
|
-
const limit = 256 * 1024;
|
|
514
|
-
const buffer = Buffer.alloc(limit + 1);
|
|
515
|
-
let length = 0;
|
|
516
|
-
const handle = await open(file, "r");
|
|
517
|
-
try {
|
|
518
|
-
while (length < buffer.length) {
|
|
519
|
-
const { bytesRead } = await handle.read(buffer, length, buffer.length - length, null);
|
|
520
|
-
if (bytesRead === 0) break;
|
|
521
|
-
length += bytesRead;
|
|
522
|
-
}
|
|
523
|
-
} finally {
|
|
524
|
-
await handle.close();
|
|
525
|
-
}
|
|
526
|
-
if (length > limit) {
|
|
527
|
-
const code = "manifest_too_large";
|
|
528
|
-
throw new ManifestError("manifest_invalid", 2, [{
|
|
529
|
-
pointer: "/",
|
|
530
|
-
code,
|
|
531
|
-
detail: code
|
|
532
|
-
}]);
|
|
533
|
-
}
|
|
534
|
-
const bytes = buffer.subarray(0, length);
|
|
535
|
-
let text;
|
|
536
|
-
try {
|
|
537
|
-
text = new TextDecoder("utf-8", {
|
|
538
|
-
fatal: true,
|
|
539
|
-
ignoreBOM: true
|
|
540
|
-
}).decode(bytes);
|
|
541
|
-
} catch {
|
|
542
|
-
throw new ManifestError("invalid_json", 2);
|
|
543
|
-
}
|
|
544
|
-
const result = validateManifest(text);
|
|
545
|
-
if (!result.manifest) throw new ManifestError("manifest_invalid", 2, result.errors);
|
|
546
|
-
if (verb === "validate") {
|
|
547
|
-
output({
|
|
548
|
-
valid: true,
|
|
549
|
-
note: AUTHORITY
|
|
550
|
-
});
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
const dryRun = verb === "diff" || Boolean(opts.dryRun);
|
|
554
|
-
const response = await publishManifest(result.manifest, command.optsWithGlobals().baseUrl ?? getFluidApiBase(), dryRun, Boolean(opts.allowWidening));
|
|
555
|
-
output(response);
|
|
556
|
-
if (dryRun && Array.isArray(response.widening_atoms) && response.widening_atoms.length) process.exitCode = 3;
|
|
557
|
-
} catch (error) {
|
|
558
|
-
const failure = error instanceof ManifestError ? error : new ManifestError("Unable to read or process manifest");
|
|
559
|
-
output({
|
|
560
|
-
error: {
|
|
561
|
-
code: failure.code,
|
|
562
|
-
...failure.details ? { details: failure.details } : {}
|
|
563
|
-
},
|
|
564
|
-
note: AUTHORITY
|
|
565
|
-
});
|
|
566
|
-
process.exitCode = failure.exitCode;
|
|
567
|
-
}
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
//#endregion
|
|
572
497
|
//#region src/install/api.ts
|
|
573
498
|
/** A failure the CLI reports as-is, with a non-zero exit. */
|
|
574
499
|
var DropletsCliError = class extends Error {
|
|
@@ -796,6 +721,92 @@ function registerInstallCommands(parent, ctx) {
|
|
|
796
721
|
}));
|
|
797
722
|
}
|
|
798
723
|
//#endregion
|
|
724
|
+
//#region src/commands/manifest.ts
|
|
725
|
+
/** Public: Register local validation and Actions-only manifest publication commands. */
|
|
726
|
+
function registerManifestCommands(parent, ctx) {
|
|
727
|
+
const manifest = parent.command("manifest").description(`Droplet manifests. ${AUTHORITY}`);
|
|
728
|
+
for (const verb of [
|
|
729
|
+
"validate",
|
|
730
|
+
"publish",
|
|
731
|
+
"diff"
|
|
732
|
+
]) {
|
|
733
|
+
const cmd = manifest.command(`${verb} [path]`).description(`${verb === "validate" ? "Validate locally" : verb === "diff" ? "Preview in GitHub Actions; exit 3 for widening (permissions: id-token: write)" : "Publish in GitHub Actions (permissions: id-token: write)"}. ${AUTHORITY}`).option("--json", "Print one JSON document");
|
|
734
|
+
if (verb === "publish") cmd.option("--allow-widening", "Permit widening when publishing; previews always report widening (exit 3)");
|
|
735
|
+
if (verb === "publish") cmd.option("--dry-run", "Preview without publishing");
|
|
736
|
+
if (verb === "diff") cmd.option("--against <version>", "Read-only: diff the droplet's latest ledger version against this one, with your session (needs --droplet)").option("--droplet <uuid>", "Owned droplet, with --against");
|
|
737
|
+
cmd.action(async (path, opts, command) => {
|
|
738
|
+
const output = (data) => opts.json ? process.stdout.write(JSON.stringify(data) + "\n") : ctx.output(data);
|
|
739
|
+
if (verb === "diff" && (opts.against !== void 0 || opts.droplet)) {
|
|
740
|
+
try {
|
|
741
|
+
if (path !== void 0 || !opts.droplet || opts.against === void 0) throw new DropletsCliError("Use --against <version> with --droplet <uuid> and no manifest path.");
|
|
742
|
+
if (!/^[1-9]\d{0,8}$/.test(opts.against)) throw new DropletsCliError("Version must be a positive safe integer.");
|
|
743
|
+
output(await (await ctx.getClient()).get(`/api/droplets/v2026-10/droplets/${encodeURIComponent(opts.droplet)}/manifest-diff`, { against: opts.against }));
|
|
744
|
+
} catch (error) {
|
|
745
|
+
fail(error);
|
|
746
|
+
}
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
try {
|
|
750
|
+
const file = path ?? "./fluid.droplet.json";
|
|
751
|
+
const limit = 256 * 1024;
|
|
752
|
+
const buffer = Buffer.alloc(limit + 1);
|
|
753
|
+
let length = 0;
|
|
754
|
+
const handle = await open(file, "r");
|
|
755
|
+
try {
|
|
756
|
+
while (length < buffer.length) {
|
|
757
|
+
const { bytesRead } = await handle.read(buffer, length, buffer.length - length, null);
|
|
758
|
+
if (bytesRead === 0) break;
|
|
759
|
+
length += bytesRead;
|
|
760
|
+
}
|
|
761
|
+
} finally {
|
|
762
|
+
await handle.close();
|
|
763
|
+
}
|
|
764
|
+
if (length > limit) {
|
|
765
|
+
const code = "manifest_too_large";
|
|
766
|
+
throw new ManifestError("manifest_invalid", 2, [{
|
|
767
|
+
pointer: "/",
|
|
768
|
+
code,
|
|
769
|
+
detail: code
|
|
770
|
+
}]);
|
|
771
|
+
}
|
|
772
|
+
const bytes = buffer.subarray(0, length);
|
|
773
|
+
let text;
|
|
774
|
+
try {
|
|
775
|
+
text = new TextDecoder("utf-8", {
|
|
776
|
+
fatal: true,
|
|
777
|
+
ignoreBOM: true
|
|
778
|
+
}).decode(bytes);
|
|
779
|
+
} catch {
|
|
780
|
+
throw new ManifestError("invalid_json", 2);
|
|
781
|
+
}
|
|
782
|
+
const result = validateManifest(text);
|
|
783
|
+
if (!result.manifest) throw new ManifestError("manifest_invalid", 2, result.errors);
|
|
784
|
+
if (verb === "validate") {
|
|
785
|
+
output({
|
|
786
|
+
valid: true,
|
|
787
|
+
note: AUTHORITY
|
|
788
|
+
});
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const dryRun = verb === "diff" || Boolean(opts.dryRun);
|
|
792
|
+
const response = await publishManifest(result.manifest, command.optsWithGlobals().baseUrl ?? getFluidApiBase(), dryRun, Boolean(opts.allowWidening));
|
|
793
|
+
output(response);
|
|
794
|
+
if (dryRun && Array.isArray(response.widening_atoms) && response.widening_atoms.length) process.exitCode = 3;
|
|
795
|
+
} catch (error) {
|
|
796
|
+
const failure = error instanceof ManifestError ? error : new ManifestError("Unable to read or process manifest");
|
|
797
|
+
output({
|
|
798
|
+
error: {
|
|
799
|
+
code: failure.code,
|
|
800
|
+
...failure.details ? { details: failure.details } : {}
|
|
801
|
+
},
|
|
802
|
+
note: AUTHORITY
|
|
803
|
+
});
|
|
804
|
+
process.exitCode = failure.exitCode;
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
//#endregion
|
|
799
810
|
//#region src/commands/rollback.ts
|
|
800
811
|
/** Public: Register confirmed rollbacks for one installation or all of a droplet. */
|
|
801
812
|
function registerRollbackCommands(parent, ctx) {
|
package/package.json
CHANGED
|
@@ -102,10 +102,11 @@ test.each(
|
|
|
102
102
|
);
|
|
103
103
|
|
|
104
104
|
const post = jest.fn();
|
|
105
|
+
const get = jest.fn();
|
|
105
106
|
const output = jest.fn();
|
|
106
107
|
const ctx: CommandContext = {
|
|
107
108
|
output,
|
|
108
|
-
getClient: jest.fn(async () => ({ post }) as never),
|
|
109
|
+
getClient: jest.fn(async () => ({ post, get }) as never),
|
|
109
110
|
getClientFor: jest.fn(),
|
|
110
111
|
parseBody: JSON.parse,
|
|
111
112
|
verbose: false,
|
|
@@ -342,3 +343,41 @@ test.each(
|
|
|
342
343
|
expect(fetchMock).toHaveBeenCalledTimes(entry[0] === "+" ? 3 : 0);
|
|
343
344
|
expect(process.exitCode).toBe(entry[0] === "+" ? undefined : 1);
|
|
344
345
|
});
|
|
346
|
+
|
|
347
|
+
test("diff --against reads the ledger with the session client and no OIDC", async () => {
|
|
348
|
+
get.mockResolvedValue({ base_version: 1, target_version: 3, added: [] });
|
|
349
|
+
await run("manifest", "diff", "--droplet", "drp_1", "--against", "1");
|
|
350
|
+
expect(get).toHaveBeenCalledWith(
|
|
351
|
+
"/api/droplets/v2026-10/droplets/drp_1/manifest-diff",
|
|
352
|
+
{ against: "1" },
|
|
353
|
+
);
|
|
354
|
+
expect(output).toHaveBeenCalledWith({
|
|
355
|
+
base_version: 1,
|
|
356
|
+
target_version: 3,
|
|
357
|
+
added: [],
|
|
358
|
+
});
|
|
359
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
360
|
+
expect(post).not.toHaveBeenCalled();
|
|
361
|
+
expect(process.exitCode).toBeUndefined();
|
|
362
|
+
});
|
|
363
|
+
test.each([
|
|
364
|
+
[["--against", "0", "--droplet", "drp_1"]],
|
|
365
|
+
[["--against", "1.5", "--droplet", "drp_1"]],
|
|
366
|
+
[["--against", "abc", "--droplet", "drp_1"]],
|
|
367
|
+
[["--against", "1"]],
|
|
368
|
+
[["--droplet", "drp_1"]],
|
|
369
|
+
[[path, "--against", "1", "--droplet", "drp_1"]],
|
|
370
|
+
])("diff --against %j makes no request", async (args) => {
|
|
371
|
+
await run("manifest", "diff", ...args);
|
|
372
|
+
expect(process.exitCode).toBe(1);
|
|
373
|
+
expect(get).not.toHaveBeenCalled();
|
|
374
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
375
|
+
});
|
|
376
|
+
test("diff without --against still previews through Actions OIDC", async () => {
|
|
377
|
+
seed({ digest: "result", widening_atoms: ["scope:x"] });
|
|
378
|
+
await run("--base-url", "https://api.fluid.app", "manifest", "diff", path);
|
|
379
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
380
|
+
expect(get).not.toHaveBeenCalled();
|
|
381
|
+
expect(ctx.getClient).not.toHaveBeenCalled();
|
|
382
|
+
expect(process.exitCode).toBe(3);
|
|
383
|
+
});
|
package/src/commands/manifest.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { Command } from "commander";
|
|
|
3
3
|
import { getFluidApiBase, type CommandContext } from "@fluid-app/fluid-cli";
|
|
4
4
|
import { AUTHORITY, validateManifest } from "../manifest/validate.js";
|
|
5
5
|
import { ManifestError, publishManifest } from "../manifest/publish.js";
|
|
6
|
+
import { DropletsCliError } from "../install/api.js";
|
|
7
|
+
import { fail } from "./install.js";
|
|
6
8
|
|
|
7
9
|
/** Public: Register local validation and Actions-only manifest publication commands. */
|
|
8
10
|
export function registerManifestCommands(
|
|
@@ -26,16 +28,54 @@ export function registerManifestCommands(
|
|
|
26
28
|
);
|
|
27
29
|
if (verb === "publish")
|
|
28
30
|
cmd.option("--dry-run", "Preview without publishing");
|
|
31
|
+
if (verb === "diff")
|
|
32
|
+
cmd
|
|
33
|
+
.option(
|
|
34
|
+
"--against <version>",
|
|
35
|
+
"Read-only: diff the droplet's latest ledger version against this one, with your session (needs --droplet)",
|
|
36
|
+
)
|
|
37
|
+
.option("--droplet <uuid>", "Owned droplet, with --against");
|
|
29
38
|
cmd.action(
|
|
30
39
|
async (
|
|
31
40
|
path: string | undefined,
|
|
32
|
-
opts: {
|
|
41
|
+
opts: {
|
|
42
|
+
json?: boolean;
|
|
43
|
+
dryRun?: boolean;
|
|
44
|
+
allowWidening?: boolean;
|
|
45
|
+
against?: string;
|
|
46
|
+
droplet?: string;
|
|
47
|
+
},
|
|
33
48
|
command: Command,
|
|
34
49
|
) => {
|
|
35
50
|
const output = (data: Record<string, unknown>) =>
|
|
36
51
|
opts.json
|
|
37
52
|
? process.stdout.write(JSON.stringify(data) + "\n")
|
|
38
53
|
: ctx.output(data);
|
|
54
|
+
if (verb === "diff" && (opts.against !== undefined || opts.droplet)) {
|
|
55
|
+
try {
|
|
56
|
+
if (
|
|
57
|
+
path !== undefined ||
|
|
58
|
+
!opts.droplet ||
|
|
59
|
+
opts.against === undefined
|
|
60
|
+
)
|
|
61
|
+
throw new DropletsCliError(
|
|
62
|
+
"Use --against <version> with --droplet <uuid> and no manifest path.",
|
|
63
|
+
);
|
|
64
|
+
if (!/^[1-9]\d{0,8}$/.test(opts.against))
|
|
65
|
+
throw new DropletsCliError(
|
|
66
|
+
"Version must be a positive safe integer.",
|
|
67
|
+
);
|
|
68
|
+
const client = await ctx.getClient();
|
|
69
|
+
const response = await client.get(
|
|
70
|
+
`/api/droplets/v2026-10/droplets/${encodeURIComponent(opts.droplet)}/manifest-diff`,
|
|
71
|
+
{ against: opts.against },
|
|
72
|
+
);
|
|
73
|
+
output(response as Record<string, unknown>);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
fail(error);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
39
79
|
try {
|
|
40
80
|
const file = path ?? "./fluid.droplet.json";
|
|
41
81
|
const limit = 256 * 1024;
|