@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluid-app/fluid-cli-droplets",
3
- "version": "0.1.199",
3
+ "version": "0.1.200",
4
4
  "description": "Fluid CLI commands for droplet management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -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
+ });
@@ -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: { json?: boolean; dryRun?: boolean; allowWidening?: boolean },
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;