@fluid-app/fluid-cli-droplets 0.1.197 → 0.1.199

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
@@ -1,6 +1,574 @@
1
- import { createDomainCommand } from "@fluid-app/fluid-cli";
1
+ import { createDomainCommand, getFluidApiBase } from "@fluid-app/fluid-cli";
2
+ import { open } from "node:fs/promises";
3
+ import Ajv2020 from "ajv/dist/2020.js";
4
+ import { createHash } from "node:crypto";
2
5
  import { createInterface } from "node:readline/promises";
3
6
  import "commander";
7
+ //#region ../../../apps/rails/config/droplet_manifest/v1.schema.json
8
+ var v1_schema_default = {
9
+ $schema: "https://json-schema.org/draft/2020-12/schema",
10
+ $id: "https://schemas.fluid.app/droplet/manifest/v1.json",
11
+ title: "Fluid droplet manifest v1",
12
+ type: "object",
13
+ additionalProperties: false,
14
+ required: [
15
+ "manifest_version",
16
+ "droplet",
17
+ "version",
18
+ "scopes",
19
+ "origin"
20
+ ],
21
+ $defs: {
22
+ "path": {
23
+ "type": "string",
24
+ "maxLength": 2048,
25
+ "pattern": "^/(?!/)[^@\\\\?#\\s]*$"
26
+ },
27
+ "semver": {
28
+ "type": "string",
29
+ "maxLength": 128,
30
+ "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(-(0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*)(\\.(0|[1-9]\\d*|\\d*[A-Za-z-][0-9A-Za-z-]*))*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$"
31
+ },
32
+ "namespace": {
33
+ "type": "string",
34
+ "maxLength": 64,
35
+ "pattern": "^[a-z][a-z0-9_]{0,63}$"
36
+ },
37
+ "name": {
38
+ "type": "string",
39
+ "maxLength": 64,
40
+ "pattern": "^[a-z][a-z0-9_.]{0,63}$"
41
+ }
42
+ },
43
+ properties: {
44
+ "manifest_version": { "const": 1 },
45
+ "droplet": {
46
+ "type": "string",
47
+ "minLength": 1,
48
+ "maxLength": 64
49
+ },
50
+ "version": { "$ref": "#/$defs/semver" },
51
+ "min_supported_version": { "$ref": "#/$defs/semver" },
52
+ "origin": {
53
+ "type": "string",
54
+ "maxLength": 253,
55
+ "pattern": "^https://[A-Za-z0-9.-]+(:[0-9]{1,5})?$"
56
+ },
57
+ "scopes": {
58
+ "type": "array",
59
+ "maxItems": 64,
60
+ "uniqueItems": true,
61
+ "items": {
62
+ "type": "string",
63
+ "minLength": 1,
64
+ "maxLength": 64
65
+ }
66
+ },
67
+ "webhooks": {
68
+ "type": "array",
69
+ "maxItems": 256,
70
+ "items": {
71
+ "type": "object",
72
+ "additionalProperties": false,
73
+ "required": ["topic", "url_path"],
74
+ "properties": {
75
+ "topic": {
76
+ "type": "string",
77
+ "minLength": 1,
78
+ "maxLength": 128,
79
+ "pattern": "^[a-z][a-z0-9_]*$"
80
+ },
81
+ "url_path": { "$ref": "#/$defs/path" }
82
+ }
83
+ }
84
+ },
85
+ "callbacks": {
86
+ "type": "array",
87
+ "maxItems": 64,
88
+ "items": {
89
+ "type": "object",
90
+ "additionalProperties": false,
91
+ "required": ["definition", "url_path"],
92
+ "properties": {
93
+ "definition": {
94
+ "type": "string",
95
+ "minLength": 1,
96
+ "maxLength": 128
97
+ },
98
+ "url_path": { "$ref": "#/$defs/path" },
99
+ "country_codes": {
100
+ "type": "array",
101
+ "maxItems": 300,
102
+ "uniqueItems": true,
103
+ "items": {
104
+ "type": "string",
105
+ "pattern": "^[A-Z]{2}$"
106
+ }
107
+ }
108
+ }
109
+ }
110
+ },
111
+ "embeds": {
112
+ "type": "object",
113
+ "additionalProperties": false,
114
+ "properties": {
115
+ "detail_page": { "$ref": "#/$defs/path" },
116
+ "drop_zones": {
117
+ "type": "array",
118
+ "maxItems": 64,
119
+ "uniqueItems": true,
120
+ "items": { "$ref": "#/$defs/path" }
121
+ }
122
+ }
123
+ },
124
+ "metafields": {
125
+ "type": "object",
126
+ "additionalProperties": false,
127
+ "properties": {
128
+ "owns": {
129
+ "type": "array",
130
+ "maxItems": 64,
131
+ "uniqueItems": true,
132
+ "items": { "$ref": "#/$defs/namespace" }
133
+ },
134
+ "reads": {
135
+ "type": "array",
136
+ "maxItems": 64,
137
+ "items": {
138
+ "type": "object",
139
+ "additionalProperties": false,
140
+ "required": ["droplet", "namespace"],
141
+ "properties": {
142
+ "droplet": {
143
+ "type": "string",
144
+ "minLength": 1,
145
+ "maxLength": 64
146
+ },
147
+ "namespace": { "$ref": "#/$defs/namespace" }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ },
153
+ "publishes": {
154
+ "type": "object",
155
+ "additionalProperties": false,
156
+ "properties": {
157
+ "events": {
158
+ "type": "array",
159
+ "maxItems": 256,
160
+ "items": {
161
+ "type": "object",
162
+ "additionalProperties": false,
163
+ "required": ["name", "major"],
164
+ "properties": {
165
+ "name": { "$ref": "#/$defs/name" },
166
+ "major": {
167
+ "type": "integer",
168
+ "minimum": 1
169
+ },
170
+ "description": {
171
+ "type": "string",
172
+ "maxLength": 1024
173
+ },
174
+ "schema": { "type": "object" }
175
+ }
176
+ }
177
+ },
178
+ "actions": {
179
+ "type": "array",
180
+ "maxItems": 256,
181
+ "items": {
182
+ "type": "object",
183
+ "additionalProperties": false,
184
+ "required": ["name"],
185
+ "properties": {
186
+ "name": {
187
+ "type": "string",
188
+ "maxLength": 64,
189
+ "pattern": "^[a-z][a-z0-9_]{0,63}$"
190
+ },
191
+ "description": {
192
+ "type": "string",
193
+ "maxLength": 1024
194
+ },
195
+ "input_schema": { "type": "object" },
196
+ "output_schema": { "type": "object" },
197
+ "effects": { "enum": ["none", "writes"] },
198
+ "async": { "type": "boolean" },
199
+ "budgets": {
200
+ "type": "object",
201
+ "additionalProperties": false,
202
+ "properties": {
203
+ "concurrency": {
204
+ "type": "integer",
205
+ "minimum": 1
206
+ },
207
+ "timeout_ms": {
208
+ "type": "integer",
209
+ "minimum": 1
210
+ },
211
+ "bytes": {
212
+ "type": "integer",
213
+ "minimum": 1
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ },
222
+ "subscribes": {
223
+ "type": "object",
224
+ "additionalProperties": false,
225
+ "properties": { "events": {
226
+ "type": "array",
227
+ "maxItems": 256,
228
+ "items": {
229
+ "type": "object",
230
+ "additionalProperties": false,
231
+ "required": [
232
+ "publisher",
233
+ "event",
234
+ "major",
235
+ "required"
236
+ ],
237
+ "properties": {
238
+ "publisher": {
239
+ "type": "string",
240
+ "minLength": 1,
241
+ "maxLength": 64
242
+ },
243
+ "event": { "$ref": "#/$defs/name" },
244
+ "major": {
245
+ "type": "integer",
246
+ "minimum": 1
247
+ },
248
+ "required": { "type": "boolean" }
249
+ }
250
+ }
251
+ } }
252
+ },
253
+ "mcp": {
254
+ "type": "object",
255
+ "additionalProperties": false,
256
+ "properties": { "tools": {
257
+ "type": "array",
258
+ "maxItems": 256,
259
+ "items": {
260
+ "type": "object",
261
+ "additionalProperties": false,
262
+ "required": ["action", "class"],
263
+ "properties": {
264
+ "action": {
265
+ "type": "string",
266
+ "maxLength": 64,
267
+ "pattern": "^[a-z][a-z0-9_]{0,63}$"
268
+ },
269
+ "description": {
270
+ "type": "string",
271
+ "maxLength": 1024
272
+ },
273
+ "class": { "enum": ["read", "write"] }
274
+ }
275
+ }
276
+ } }
277
+ },
278
+ "health": {
279
+ "type": "object",
280
+ "additionalProperties": false,
281
+ "properties": { "path": {
282
+ "$ref": "#/$defs/path",
283
+ "default": "/api/health"
284
+ } }
285
+ },
286
+ "lifecycle": {
287
+ "type": "object",
288
+ "additionalProperties": false,
289
+ "properties": {
290
+ "install_webhook_path": { "$ref": "#/$defs/path" },
291
+ "uninstall_webhook_path": { "$ref": "#/$defs/path" },
292
+ "events_webhook_path": { "$ref": "#/$defs/path" }
293
+ }
294
+ }
295
+ }
296
+ };
297
+ //#endregion
298
+ //#region src/manifest/validate.ts
299
+ const AUTHORITY = "The server is authoritative and checks additional catalogs, scope ceilings and publisher identity.";
300
+ const checkSchema = new Ajv2020({
301
+ allErrors: true,
302
+ strict: false
303
+ }).compile(v1_schema_default);
304
+ function compareVersion(a, b) {
305
+ const split = (v) => v.split("+")[0].match(/^(\d+)\.(\d+)\.(\d+)(?:-(.*))?$/).slice(1);
306
+ const aa = split(a), bb = split(b);
307
+ const compare = (x, y) => {
308
+ if (x === y) return 0;
309
+ const xn = /^\d+$/.test(x), yn = /^\d+$/.test(y);
310
+ if (xn && yn) return BigInt(x) < BigInt(y) ? -1 : 1;
311
+ return xn !== yn ? xn ? -1 : 1 : x < y ? -1 : 1;
312
+ };
313
+ for (let i = 0; i < 3; i++) {
314
+ const n = compare(aa[i], bb[i]);
315
+ if (n) return n;
316
+ }
317
+ const ap = aa[3], bp = bb[3];
318
+ if (ap === void 0 || bp === void 0) return ap === bp ? 0 : ap === void 0 ? 1 : -1;
319
+ const ax = ap.split("."), bx = bp.split(".");
320
+ for (let i = 0; i < Math.max(ax.length, bx.length); i++) {
321
+ if (ax[i] === void 0 || bx[i] === void 0) return ax.length < bx.length ? -1 : 1;
322
+ const n = compare(ax[i], bx[i]);
323
+ if (n) return n;
324
+ }
325
+ return 0;
326
+ }
327
+ /** Public: Validate locally expressible manifest rules; the server checks the rest. */
328
+ function validateManifest(text) {
329
+ const errors = [];
330
+ const add = (pointer, code, detail = code) => errors.push({
331
+ pointer,
332
+ code,
333
+ detail
334
+ });
335
+ if (Buffer.byteLength(text) > 256 * 1024) {
336
+ add("/", "manifest_too_large");
337
+ return { errors };
338
+ }
339
+ let value;
340
+ try {
341
+ value = JSON.parse(text);
342
+ const stack = [];
343
+ for (const token of text.matchAll(/"(?:[^"\\]|\\.)*"\s*:?|[{}]/g)) {
344
+ const t = token[0];
345
+ if (t === "{") stack.push(/* @__PURE__ */ new Set());
346
+ else if (t === "}") stack.pop();
347
+ else if (t.endsWith(":")) {
348
+ const key = JSON.parse(t.slice(0, -1));
349
+ if (stack.at(-1)?.has(key)) throw new Error("duplicate key");
350
+ stack.at(-1)?.add(key);
351
+ }
352
+ }
353
+ } catch {
354
+ add("", "invalid_json", "Invalid JSON or duplicate object keys");
355
+ return { errors };
356
+ }
357
+ function walk(node, pointer) {
358
+ if (typeof node === "number" && (!Number.isFinite(node) || Math.abs(node) > Number.MAX_SAFE_INTEGER)) add(pointer, "invalid_number");
359
+ if (typeof node === "string" && !node.isWellFormed()) add(pointer, "invalid_json", "Invalid Unicode");
360
+ if (node && typeof node === "object") for (const [key, item] of Object.entries(node)) {
361
+ if (!key.isWellFormed()) add(pointer, "invalid_json", "Invalid Unicode key");
362
+ walk(item, `${pointer}/${key.replaceAll("~", "~0").replaceAll("/", "~1")}`);
363
+ }
364
+ if (typeof node === "string" && /^\/(webhooks\/\d+\/url_path|callbacks\/\d+\/url_path|embeds\/(detail_page|drop_zones\/\d+)|health\/path|lifecycle\/(install|uninstall|events)_webhook_path)$/.test(pointer)) {
365
+ if (!/^\/(?!\/)[^@\\?#\s]*$/.test(node) || /[^\x21-\x7e]|%(?![\da-f]{2})|[<>"{}|^`[\]]/i.test(node) || /%(2f|5c)/i.test(node) || /(?:^|\/)(?:\.|%2e){1,2}(?:\/|$)/i.test(node)) add(pointer, "invalid_path");
366
+ }
367
+ }
368
+ walk(value, "");
369
+ if (value && typeof value === "object" && "manifest_version" in value && value.manifest_version !== 1) {
370
+ add("/manifest_version", "unsupported_manifest_version");
371
+ return { errors };
372
+ }
373
+ if (!checkSchema(value)) {
374
+ for (const e of checkSchema.errors ?? []) add(e.instancePath, "schema_violation", e.message ?? e.keyword);
375
+ return { errors };
376
+ }
377
+ try {
378
+ const origin = new URL(String(value.origin));
379
+ if (origin.protocol !== "https:" || origin.username || origin.password || !/^https:\/\/[^/]+$/.test(String(value.origin))) throw new Error();
380
+ } catch {
381
+ add("/origin", "origin_not_https");
382
+ }
383
+ if (typeof value.min_supported_version === "string" && compareVersion(value.min_supported_version, value.version) > 0) add("/min_supported_version", "min_supported_version_exceeds_version");
384
+ for (const [field, key, code] of [[
385
+ "callbacks",
386
+ "definition",
387
+ "definition_not_unique"
388
+ ], [
389
+ "webhooks",
390
+ "topic",
391
+ "webhook_not_unique"
392
+ ]]) {
393
+ const seen = /* @__PURE__ */ new Set();
394
+ const rows = value[field];
395
+ if (Array.isArray(rows)) rows.forEach((row, i) => {
396
+ if (seen.has(row[key])) add(`/${field}/${i}/${key}`, code);
397
+ seen.add(row[key]);
398
+ });
399
+ }
400
+ return {
401
+ manifest: errors.length ? void 0 : value,
402
+ errors
403
+ };
404
+ }
405
+ //#endregion
406
+ //#region src/manifest/canonical.ts
407
+ /** Public: Serialize JSON using RFC 8785 UTF-16 ordering and ECMAScript numbers. */
408
+ function canonicalize(value) {
409
+ if (typeof value === "string") {
410
+ if (!value.isWellFormed()) throw new Error("string is not valid Unicode");
411
+ return JSON.stringify(value);
412
+ }
413
+ if (typeof value === "number") {
414
+ if (!Number.isFinite(value)) throw new Error("non-finite number");
415
+ return JSON.stringify(value);
416
+ }
417
+ if (value === null || typeof value === "boolean") return JSON.stringify(value);
418
+ if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
419
+ if (typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) return `{${Object.keys(value).sort().map((key) => `${canonicalize(key)}:${canonicalize(Reflect.get(value, key))}`).join(",")}}`;
420
+ throw new Error("unsupported JSON value");
421
+ }
422
+ /** Public: Compute the manifest ledger key over its canonical UTF-8 bytes. */
423
+ function manifestDigest(value) {
424
+ return `sha256:${createHash("sha256").update(canonicalize(value)).digest("hex")}`;
425
+ }
426
+ //#endregion
427
+ //#region src/manifest/publish.ts
428
+ var ManifestError = class extends Error {
429
+ constructor(code, exitCode = 1, details) {
430
+ super(code);
431
+ this.code = code;
432
+ this.exitCode = exitCode;
433
+ this.details = details;
434
+ }
435
+ };
436
+ async function request(url, token, body) {
437
+ let response;
438
+ try {
439
+ response = await fetch(url, {
440
+ method: body === void 0 ? "GET" : "POST",
441
+ redirect: "error",
442
+ signal: AbortSignal.timeout(3e4),
443
+ headers: {
444
+ Authorization: `Bearer ${token}`,
445
+ "Content-Type": "application/json"
446
+ },
447
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
448
+ });
449
+ } catch {
450
+ throw new ManifestError("Publisher request failed");
451
+ }
452
+ let result;
453
+ try {
454
+ result = await response.json();
455
+ } catch {
456
+ throw new ManifestError("Invalid publisher response");
457
+ }
458
+ if (!response.ok) {
459
+ const code = result?.error?.details?.code ?? `HTTP ${response.status}`;
460
+ throw new ManifestError(code, code === "widening_not_allowed" ? 3 : code === "manifest_invalid" ? 2 : 1, code === "manifest_invalid" && Array.isArray(result?.error?.details?.details) ? result.error.details.details.map(({ pointer, code, detail }) => ({
461
+ pointer,
462
+ code,
463
+ detail
464
+ })) : void 0);
465
+ }
466
+ return result;
467
+ }
468
+ /** Public: Publish or preview using only GitHub OIDC and a single-use grant. */
469
+ async function publishManifest(manifest, baseUrl, dryRun, allowWidening) {
470
+ const base = new URL(baseUrl);
471
+ if (!(base.origin === "https://api.fluid.app" && !base.username && !base.password || ["http:", "https:"].includes(base.protocol) && [
472
+ "localhost",
473
+ "127.0.0.1",
474
+ "[::1]"
475
+ ].includes(base.hostname))) throw new ManifestError(`Rejected publisher host ${base.hostname}; allowed: https://api.fluid.app; HTTP/HTTPS localhost, 127.0.0.1, [::1]`);
476
+ const missing = ["ACTIONS_ID_TOKEN_REQUEST_URL", "ACTIONS_ID_TOKEN_REQUEST_TOKEN"].filter((key) => !process.env[key]);
477
+ if (process.env.GITHUB_ACTIONS !== "true") missing.unshift("GITHUB_ACTIONS=true");
478
+ if (missing.length) throw new ManifestError(`Actions only: missing ${missing.join(", ")}; configure permissions: id-token: write`);
479
+ const digest = manifestDigest(manifest);
480
+ const oidcUrl = new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL);
481
+ oidcUrl.searchParams.set("audience", "https://api.fluid.app/droplets/manifest");
482
+ const oidc = await request(oidcUrl.toString(), process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN);
483
+ if (typeof oidc.value !== "string" || !oidc.value) throw new ManifestError("Missing OIDC token");
484
+ const prefix = `${baseUrl.replace(/\/$/, "")}/api/droplets/v2026-10`;
485
+ const token = (await request(`${prefix}/publish-grants`, oidc.value, {
486
+ droplet_uuid: manifest.droplet,
487
+ digest
488
+ })).publish_grant?.token;
489
+ if (typeof token !== "string" || !token) throw new ManifestError("Missing publish grant");
490
+ return request(`${prefix}/manifest-publications${dryRun ? "?dry_run=true" : ""}`, token, {
491
+ manifest,
492
+ digest,
493
+ ...dryRun || allowWidening ? { allow_widening: true } : {}
494
+ });
495
+ }
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
4
572
  //#region src/install/api.ts
5
573
  /** A failure the CLI reports as-is, with a non-zero exit. */
6
574
  var DropletsCliError = class extends Error {
@@ -228,6 +796,27 @@ function registerInstallCommands(parent, ctx) {
228
796
  }));
229
797
  }
230
798
  //#endregion
799
+ //#region src/commands/rollback.ts
800
+ /** Public: Register confirmed rollbacks for one installation or all of a droplet. */
801
+ function registerRollbackCommands(parent, ctx) {
802
+ parent.command("rollback <version>").description("Roll back live config; would_await_consent still requires merchant consent").option("--install <uuid>", "Installation to roll back").option("--all", "Roll back all installations").option("--droplet <uuid>", "Owned droplet (required with --all)").option("--yes", "Confirm without a prompt", false).action(async (version, opts) => {
803
+ try {
804
+ if (Boolean(opts.install) === Boolean(opts.all) || opts.all && !opts.droplet || opts.install && opts.droplet) throw new DropletsCliError("Choose exactly one of --install <uuid> or --all --droplet <uuid>.");
805
+ const targetVersion = Number(version);
806
+ if (!/^\d+$/.test(version) || !Number.isSafeInteger(targetVersion) || targetVersion <= 0) throw new DropletsCliError("Version must be a positive safe integer.");
807
+ await confirmChange(`Roll back ${opts.install ?? opts.droplet} to ${version}?`, opts.yes);
808
+ const client = await ctx.getClient();
809
+ const target = opts.install ? `installations/${encodeURIComponent(opts.install)}` : `droplets/${encodeURIComponent(opts.droplet)}`;
810
+ ctx.output(await client.post(`/api/droplets/v2026-10/${target}/rollback`, {
811
+ version: targetVersion,
812
+ ...opts.all ? { all: true } : {}
813
+ }));
814
+ } catch (error) {
815
+ fail(error);
816
+ }
817
+ });
818
+ }
819
+ //#endregion
231
820
  //#region src/publish/api.ts
232
821
  /**
233
822
  * Asking Fluid to publish a droplet, and a root admin's review of those
@@ -490,6 +1079,8 @@ const plugin = {
490
1079
  register(ctx) {
491
1080
  ctx.program.addCommand(createDomainCommand("droplets", "Manage Fluid droplets and installations", (parent, cmdCtx) => {
492
1081
  registerAllCommands(parent, cmdCtx);
1082
+ registerManifestCommands(parent, cmdCtx);
1083
+ registerRollbackCommands(parent, cmdCtx);
493
1084
  registerInstallCommands(parent, cmdCtx);
494
1085
  registerPublishCommands(parent, cmdCtx);
495
1086
  }));
@@ -8,3 +8,8 @@ export function createDomainCommand(): never {
8
8
  "createDomainCommand is not available in unit tests (droplets)",
9
9
  );
10
10
  }
11
+
12
+ /** Public: Resolve the API origin without loading credentials. */
13
+ export function getFluidApiBase(): string {
14
+ return process.env.FLUID_API_BASE ?? "https://api.fluid.app";
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluid-app/fluid-cli-droplets",
3
- "version": "0.1.197",
3
+ "version": "0.1.199",
4
4
  "description": "Fluid CLI commands for droplet management",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -17,7 +17,8 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@fluid-app/fluid-cli": "0.1.196",
20
+ "@fluid-app/fluid-cli": "0.1.197",
21
+ "ajv": "8.17.1",
21
22
  "commander": "^12.0.0"
22
23
  },
23
24
  "devDependencies": {