@fluid-app/fluid-cli-mist 0.1.9 → 0.1.11

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.d.mts CHANGED
@@ -129,9 +129,10 @@ interface DropletUpdateRequest {
129
129
  }
130
130
  interface DropletRecord {
131
131
  /**
132
- * Numeric id is NOT returned by `POST /api/droplets` and isn't exposed
133
- * anywhere the CLI can read it — attach by `uuid` instead (see
134
- * `IntegrationPointCreateInput.integratable_uuid`).
132
+ * Numeric id is NOT returned by `POST /api/droplets` — attach by `uuid`
133
+ * instead (see `IntegrationPointCreateInput.integratable_uuid`). The
134
+ * `GET /api/droplets` list rows DO carry it, which is the only way to
135
+ * map a legacy integration point's `integratable_id` back to a uuid.
135
136
  */
136
137
  id?: number;
137
138
  uuid: string;
@@ -151,6 +152,50 @@ interface DropletShowResponse {
151
152
  meta?: ApiMeta;
152
153
  status?: number;
153
154
  }
155
+ /**
156
+ * Page-number pagination used by the company-level droplet + installation
157
+ * list endpoints (`?page=&per_page=`) — unlike the mist sub-resources,
158
+ * these do NOT use cursor pagination. Shape mirrors fluid-admin's
159
+ * `networking/droplets.api.ts` response schemas.
160
+ */
161
+ interface PagePaginationMeta {
162
+ request_id?: string;
163
+ timestamp?: string;
164
+ pagination?: {
165
+ current_page?: number;
166
+ total_pages?: number;
167
+ total_count?: number;
168
+ per_page?: number;
169
+ };
170
+ }
171
+ /** `GET /api/droplets` — the company's droplets, page-paginated. The list
172
+ * rows carry the numeric `id` (unlike `POST /api/droplets`), which is how
173
+ * a legacy integration point's `integratable_id` maps back to a uuid. */
174
+ interface DropletsListResponse {
175
+ droplets?: DropletRecord[];
176
+ meta?: PagePaginationMeta;
177
+ status?: number;
178
+ }
179
+ interface DropletInstallation {
180
+ /** The installation's own dri_ uuid (needed to uninstall later). */
181
+ uuid?: string;
182
+ /** The installed droplet's drp_ uuid. */
183
+ droplet_uuid?: string;
184
+ }
185
+ interface DropletInstallationsListResponse {
186
+ droplet_installations?: DropletInstallation[];
187
+ meta?: PagePaginationMeta;
188
+ status?: number;
189
+ }
190
+ interface DropletInstallationCreateRequest {
191
+ droplet_uuid: string;
192
+ scopes?: string[];
193
+ }
194
+ interface DropletInstallationCreateResponse {
195
+ droplet_installation?: DropletInstallation;
196
+ meta?: ApiMeta;
197
+ status?: number;
198
+ }
154
199
  /** Hyphenated wire kinds the integration-points API speaks. */
155
200
  type IntegrationPointKind = "droplet" | "drop-zone" | "mobile-embed";
156
201
  interface IntegrationPoint {
@@ -311,5 +356,5 @@ interface MistFile {
311
356
  //#region src/index.d.ts
312
357
  declare const plugin: FluidPlugin;
313
358
  //#endregion
314
- export { ApiCollection, ApiMeta, Deployment, DeploymentShowResponse, DeploymentState, DeploymentsListResponse, DropZoneCreateInput, DropZoneCreateRequest, DropZoneCreateResponse, DropZoneRecord, Droplet, DropletCreateInput, DropletCreateRequest, DropletCreateResponse, DropletRecord, DropletShowResponse, DropletUpdateInput, DropletUpdateRequest, EnvVar, EnvVarsListResponse, GitCredential, GitCredentialResponse, HostableType, IntegrationPoint, IntegrationPointCreateInput, IntegrationPointCreateRequest, IntegrationPointKind, IntegrationPointShowResponse, IntegrationPointsListResponse, LogEntry, LogsResponse, MANAGED_DROPLET_ENV_KEYS, ManagedDropletEnvKey, Mist, MistCreateRequest, MistFile, MistKind, MistListResponse, MistShowResponse, MistState, MistUpdateRequest, MobileWidgetCreateInput, MobileWidgetCreateRequest, MobileWidgetCreateResponse, MobileWidgetRecord, PaginationCursor, plugin as default };
359
+ export { ApiCollection, ApiMeta, Deployment, DeploymentShowResponse, DeploymentState, DeploymentsListResponse, DropZoneCreateInput, DropZoneCreateRequest, DropZoneCreateResponse, DropZoneRecord, Droplet, DropletCreateInput, DropletCreateRequest, DropletCreateResponse, DropletInstallation, DropletInstallationCreateRequest, DropletInstallationCreateResponse, DropletInstallationsListResponse, DropletRecord, DropletShowResponse, DropletUpdateInput, DropletUpdateRequest, DropletsListResponse, EnvVar, EnvVarsListResponse, GitCredential, GitCredentialResponse, HostableType, IntegrationPoint, IntegrationPointCreateInput, IntegrationPointCreateRequest, IntegrationPointKind, IntegrationPointShowResponse, IntegrationPointsListResponse, LogEntry, LogsResponse, MANAGED_DROPLET_ENV_KEYS, ManagedDropletEnvKey, Mist, MistCreateRequest, MistFile, MistKind, MistListResponse, MistShowResponse, MistState, MistUpdateRequest, MobileWidgetCreateInput, MobileWidgetCreateRequest, MobileWidgetCreateResponse, MobileWidgetRecord, PagePaginationMeta, PaginationCursor, plugin as default };
315
360
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/index.ts"],"mappings":";;;;;;AASA;;;;;KAAY,QAAA;AAAA,KAEA,SAAA;;;;AAeZ;;;;KAAY,YAAA;AAAA,UAEK,OAAA;EACf,EAAA;EACA,IAAA;EACA,IAAA;AAAA;;;;;AAWF;;;;cAAa,wBAAA;AAAA,KAMD,oBAAA,WAA+B,wBAAA;AAAA,UAE1B,IAAA;EACf,EAAA;EACA,IAAA;EAJyC;AAE3C;EAKE,IAAA;EACA,KAAA,EAAO,SAAA;EACP,IAAA,EAAM,QAAA;;;;;;EAMN,aAAA,GAAgB,YAAA;;EAEhB,WAAA;;;;;;EAMA,WAAA;EACA,UAAA;;EAEA,oBAAA;EACA,OAAA,EAAS,OAAA;EACT,cAAA;EACA,UAAA;EACA,UAAA;AAAA;AAAA,UAGe,gBAAA;EACf,MAAA;EACA,KAAA;EACA,WAAA;EACA,WAAA;AAAA;AAAA,UAGe,OAAA;EACf,UAAA;EACA,SAAA;EACA,UAAA,GAAa,gBAAA;AAAA;AAAA,UAGE,aAAA;EACf,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,KAGU,gBAAA;EACV,KAAA,EAAO,IAAA;EACP,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,KAGU,gBAAA;EACV,IAAA,EAAM,IAAA;EACN,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,UAGe,iBAAA;EACf,IAAA;IACE,IAAA;IACA,IAAA,GAAO,QAAA;EAAA;AAAA;AAfX;;;;;AAAA,UAwBiB,iBAAA;EACf,IAAA;IACE,IAAA;IACA,IAAA,GAAO,QAAA;EAAA;AAAA;AAAA,UAQM,kBAAA;EACf,IAAA;;;EAGA,SAAA;EACA,mBAAA;EACA,qBAAA;EACA,gBAAA;EACA,QAAA,GAAW,MAAA;AAAA;AAAA,UAGI,kBAAA;EACf,SAAA;EACA,mBAAA;EACA,qBAAA;AAAA;AAAA,UAGe,oBAAA;EACf,OAAA,EAAS,kBAAA;AAAA;AAAA,UAGM,oBAAA;EACf,OAAA,EAAS,kBAAA;AAAA;AAAA,UAGM,aAAA;EApCA;;;;;;EA2Cf,EAAA;EACA,IAAA;EACA,IAAA;EACA,SAAA;AAAA;AAAA,UAGe,qBAAA;EACf,OAAA,EAAS,aAAA;;;EAGT,cAAA;EACA,IAAA,GAAO,OAAA;EACP,MAAA;AAAA;AAAA,UAGe,mBAAA;EACf,OAAA,EAAS,aAAA;EACT,IAAA,GAAO,OAAA;EACP,MAAA;AAAA;;;;;;;UASe,kBAAA;EACf,UAAA;EACA,SAAA;EACA,UAAA;IACE,YAAA;IACA,WAAA;IACA,WAAA;IACA,QAAA;EAAA;AAAA;;AAzCJ;;UAgDiB,oBAAA;EACf,QAAA,GAAW,aAAA;EACX,IAAA,GAAO,kBAAA;EACP,MAAA;AAAA;AAAA,UAYe,mBAAA;;EAEf,IAAA;EApDF;EAsDE,YAAA;AAAA;AAAA,UAGe,gCAAA;EACf,qBAAA,GAAwB,mBAAA;EACxB,IAAA,GAAO,kBAAA;EACP,MAAA;AAAA;AAAA,UAGe,gCAAA;EACf,YAAA;EACA,MAAA;AAAA;AAAA,UAGe,iCAAA;EACf,oBAAA,GAAuB,mBAAA;EACvB,IAAA,GAAO,OAAA;EACP,MAAA;AAAA;;KASU,oBAAA;AAAA,UAEK,gBAAA;;EAEf,EAAA;EACA,IAAA,EAAM,oBAAA;EAhER;EAkEE,eAAA;;;;;;EAMA,iBAAA;EACA,iBAAA;EACA,OAAA;AAAA;;;AA5DF;;;;;;;UAwEiB,2BAAA;EACf,IAAA,EAAM,oBAAA;EACN,iBAAA;EACA,eAAA;AAAA;AAAA,UAGe,6BAAA;EACf,iBAAA,EAAmB,2BAAA;AAAA;AAAA,UAGJ,6BAAA;EACf,kBAAA,EAAoB,gBAAA;EACpB,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,UAGe,4BAAA;EACf,iBAAA,EAAmB,gBAAA;EACnB,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,UAOe,mBAAA;EACf,IAAA;EACA,SAAA;EAxEe;EA0Ef,QAAA;IAAa,IAAA;IAAe,IAAA;IAAe,MAAA;EAAA;AAAA;AAAA,UAG5B,qBAAA;EACf,SAAA,EAAW,mBAAA;AAAA;AAAA,UAGI,cAAA;EACf,IAAA;EACA,IAAA;EACA,SAAA;EACA,MAAA;EACA,QAAA,GAAW,MAAA;AAAA;AAAA,UAGI,sBAAA;EACf,SAAA,EAAW,cAAA;EACX,IAAA,GAAO,OAAA;EACP,MAAA;AAAA;AAAA,UAOe,uBAAA;EACf,IAAA;EACA,SAAA;;EAEA,eAAA;;EAEA,MAAA;EACA,UAAA;AAAA;AAAA,UAGe,yBAAA;EACf,aAAA,EAAe,uBAAA;AAAA;AAAA,UAGA,kBAAA;EACf,EAAA;EACA,IAAA;EACA,SAAA;EACA,eAAA;EACA,MAAA;AAAA;AAAA,UAGe,0BAAA;EACf,aAAA,EAAe,kBAAA;EACf,IAAA,GAAO,OAAA;EACP,MAAA;AAAA;AAAA,UAGe,MAAA;EACf,GAAA;EACA,KAAA;EACA,OAAA;EACA,UAAA;AAAA;AAAA,UAGe,mBAAA;EACf,QAAA,EAAU,MAAA;EACV,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,UAGe,aAAA;EACf,UAAA;EACA,UAAA;AAAA;AAAA,UAGe,qBAAA;EACf,cAAA,EAAgB,aAAA;EAChB,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,KAGU,eAAA;AAAA,UAOK,UAAA;EACf,EAAA;EACA,KAAA,EAAO,eAAA;EACP,GAAA;EACA,UAAA;EACA,cAAA;EACA,UAAA;EACA,MAAA;AAAA;AAAA,UAGe,uBAAA;EACf,WAAA,EAAa,UAAA;EACb,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,UAGe,sBAAA;EACf,UAAA,EAAY,UAAA;EACZ,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;AAAA,UAGe,QAAA;EACf,SAAA;EACA,KAAA;EACA,OAAA;EACA,MAAA;AAAA;AAAA,UAGe,YAAA;EACf,aAAA;EACA,IAAA,EAAM,QAAA;EACN,IAAA,EAAM,OAAA;EACN,MAAA;AAAA;;UAIe,QAAA;EACf,IAAA;;;;;;EAMA,WAAA;EACA,UAAA;AAAA;;;cC1ZI,MAAA,EAAQ,WAAA"}
package/dist/index.mjs CHANGED
@@ -131,28 +131,28 @@ async function resolveMistId(client, slugOrId) {
131
131
  }
132
132
  //#endregion
133
133
  //#region src/api/mists.ts
134
- const BASE$3 = "/api/v202604/mists";
134
+ const BASE$4 = "/api/v202604/mists";
135
135
  function listMists(client, params = {}) {
136
- return client.get(BASE$3, params);
136
+ return client.get(BASE$4, params);
137
137
  }
138
138
  async function showMist(client, slug) {
139
139
  const id = await resolveMistId(client, slug);
140
- return client.get(`${BASE$3}/${id}`);
140
+ return client.get(`${BASE$4}/${id}`);
141
141
  }
142
142
  function createMist(client, body) {
143
- return client.post(BASE$3, body);
143
+ return client.post(BASE$4, body);
144
144
  }
145
145
  async function updateMist(client, slug, body) {
146
146
  const id = await resolveMistId(client, slug);
147
- return client.patch(`${BASE$3}/${id}`, body);
147
+ return client.patch(`${BASE$4}/${id}`, body);
148
148
  }
149
149
  async function deleteMist(client, slug) {
150
150
  const id = await resolveMistId(client, slug);
151
- return client.delete(`${BASE$3}/${id}`);
151
+ return client.delete(`${BASE$4}/${id}`);
152
152
  }
153
153
  async function restoreMist(client, slug) {
154
154
  const id = await resolveMistId(client, slug);
155
- return client.post(`${BASE$3}/${id}/restore`);
155
+ return client.post(`${BASE$4}/${id}/restore`);
156
156
  }
157
157
  //#endregion
158
158
  //#region src/lib/hostable.ts
@@ -542,7 +542,7 @@ function createSilentReporter() {
542
542
  }
543
543
  //#endregion
544
544
  //#region src/api/droplets.ts
545
- const BASE$2 = "/api/droplets";
545
+ const BASE$3 = "/api/droplets";
546
546
  /**
547
547
  * Full scope set from `Droplet::Installation::VALID_SCOPES`. We request
548
548
  * them all by default so a Mist-created droplet's installations carry write
@@ -592,11 +592,33 @@ function buildDropletCreateInput(opts) {
592
592
  return input;
593
593
  }
594
594
  function createDroplet(client, body) {
595
- return client.post(BASE$2, body);
595
+ return client.post(BASE$3, body);
596
+ }
597
+ /** Page size + runaway guard for walking the page-paginated droplets list. */
598
+ const PER_PAGE$1 = 100;
599
+ const MAX_PAGES$1 = 25;
600
+ /**
601
+ * Recover a droplet's drp_ uuid from its numeric id by paging the company's
602
+ * droplets list (`GET /api/droplets?page=&per_page=` — the list rows carry
603
+ * `id`, unlike the create/show responses). Needed for legacy integration
604
+ * points that only echo `integratable_id`: the show endpoint resolves by
605
+ * uuid, so a numeric id can't be fetched directly. Returns null when no
606
+ * droplet matches within the page cap.
607
+ */
608
+ async function findDropletUuidByNumericId(client, id) {
609
+ for (let page = 1; page <= MAX_PAGES$1; page++) {
610
+ const resp = await client.get(`${BASE$3}?page=${page}&per_page=${PER_PAGE$1}`);
611
+ const batch = resp.droplets ?? [];
612
+ const match = batch.find((d) => d.id === id);
613
+ if (match) return match.uuid ?? null;
614
+ const totalPages = resp.meta?.pagination?.total_pages;
615
+ if (totalPages != null ? page >= totalPages : batch.length < PER_PAGE$1) break;
616
+ }
617
+ return null;
596
618
  }
597
619
  /** PATCH the droplet's embed URL (+ derived webhook URLs) once known. */
598
620
  function updateDroplet(client, uuid, input) {
599
- return client.patch(`${BASE$2}/${encodeURIComponent(uuid)}`, { droplet: input });
621
+ return client.patch(`${BASE$3}/${encodeURIComponent(uuid)}`, { droplet: input });
600
622
  }
601
623
  /** Convenience: set embed URL + derived webhook URLs in one PATCH. */
602
624
  function setDropletEmbedUrl(client, uuid, embedUrl) {
@@ -606,6 +628,43 @@ function setDropletEmbedUrl(client, uuid, embedUrl) {
606
628
  });
607
629
  }
608
630
  //#endregion
631
+ //#region src/api/droplet_installations.ts
632
+ const BASE$2 = "/api/droplet_installations";
633
+ /** Page size + runaway guard for walking the page-paginated list. */
634
+ const PER_PAGE = 100;
635
+ const MAX_PAGES = 25;
636
+ /**
637
+ * List ALL of the current company's droplet installations, following the
638
+ * endpoint's page-number pagination (`?page=&per_page=` — same shape as
639
+ * fluid-admin's `getCompanyDroplets`). A single-page fetch would miss rows
640
+ * beyond the first page, making the idempotency check (and the duplicate-
641
+ * rejection confirmation) report a false "not installed" forever.
642
+ */
643
+ async function listAllDropletInstallations(client) {
644
+ const rows = [];
645
+ for (let page = 1; page <= MAX_PAGES; page++) {
646
+ const resp = await client.get(`${BASE$2}?page=${page}&per_page=${PER_PAGE}`);
647
+ const batch = resp.droplet_installations ?? [];
648
+ rows.push(...batch);
649
+ const totalPages = resp.meta?.pagination?.total_pages;
650
+ if (totalPages != null ? page >= totalPages : batch.length < PER_PAGE) break;
651
+ }
652
+ return rows;
653
+ }
654
+ /** Install a droplet (by drp_ uuid) for the current company. */
655
+ function createDropletInstallation(client, input) {
656
+ return client.post(BASE$2, input);
657
+ }
658
+ /**
659
+ * The company's installation row for a droplet drp_ uuid, or undefined.
660
+ * The idempotency key for the install step: an existing row means the
661
+ * droplet is already connected (installed via a previous run, Mist Desktop,
662
+ * or the admin marketplace).
663
+ */
664
+ function findInstallationForDroplet(installations, dropletUuid) {
665
+ return (installations ?? []).find((i) => i.droplet_uuid === dropletUuid);
666
+ }
667
+ //#endregion
609
668
  //#region src/api/drop_zones.ts
610
669
  const BASE$1 = "/api/drop_zones";
611
670
  /**
@@ -717,6 +776,10 @@ const MANAGED_DROPLET_ENV_KEYS = [
717
776
  * - Only a droplet attach provisions managed env keys
718
777
  * (FLUID_DROPLET_UUID/_SECRET/_WEBHOOK_AUTH_TOKEN); this flow only
719
778
  * VERIFIES they landed — it never writes them (the API 422s on write).
779
+ * - A droplet must also be INSTALLED (POST /api/droplet_installations) —
780
+ * create + link alone leave it not-connected/inactive, so install-scoped
781
+ * features (dit/dex exchange, admin embeds, install webhooks) don't work.
782
+ * The install step is idempotent (skips when an installation exists).
720
783
  */
721
784
  const DEFAULT_TIMING = {
722
785
  publicUrlTimeoutMs: 9e4,
@@ -866,6 +929,21 @@ async function provisionSurface(client, options) {
866
929
  if (surfaceNumericId == null) surfaceNumericId = ipResp.integration_point.integratable_id ?? null;
867
930
  reporter.succeed(kind === "droplet" ? "Droplet attached — provisioning env + redeploy…" : `${label} attached`);
868
931
  }
932
+ let installation = null;
933
+ if (kind === "droplet") {
934
+ if (!surfaceUuid && surfaceNumericId != null) try {
935
+ surfaceUuid = await findDropletUuidByNumericId(client, surfaceNumericId);
936
+ } catch {}
937
+ if (!surfaceUuid) {
938
+ reporter.fail("Droplet uuid unknown — cannot install");
939
+ throw new Error("Couldn't determine the droplet uuid, so the droplet is linked but NOT installed — it stays inactive until an installation exists. Re-run with `--existing <droplet-uuid>`, or install manually: POST /api/droplet_installations with {\"droplet_uuid\":\"<uuid>\"}.");
940
+ }
941
+ installation = await ensureDropletInstalled(client, {
942
+ dropletUuid: surfaceUuid,
943
+ scopes: options.requestedScopes ?? DEFAULT_DROPLET_SCOPES,
944
+ reporter
945
+ });
946
+ }
869
947
  const managedEnv = kind === "droplet" ? await verifyManagedEnv(client, {
870
948
  slugOrId,
871
949
  reporter,
@@ -894,10 +972,79 @@ async function provisionSurface(client, options) {
894
972
  },
895
973
  surfaceCreated,
896
974
  embedUrlSet,
975
+ installation,
897
976
  managedEnv
898
977
  };
899
978
  }
900
979
  /**
980
+ * Install the droplet for the current company (idempotent). Checks the
981
+ * installations list first; treats the API's already-installed rejection as
982
+ * success too, so re-runs and races both converge. A hard failure throws with
983
+ * the droplet uuid + recovery steps — the created droplet and mist link stay
984
+ * in place, so an idempotent re-run finishes the job.
985
+ */
986
+ async function ensureDropletInstalled(client, args) {
987
+ const { reporter, dropletUuid } = args;
988
+ reporter.start("Installing droplet…");
989
+ let existing;
990
+ try {
991
+ existing = findInstallationForDroplet(await listAllDropletInstallations(client), dropletUuid);
992
+ } catch {}
993
+ if (existing) {
994
+ reporter.skip(`Droplet already installed${existing.uuid ? ` (${existing.uuid})` : ""} — skipping`);
995
+ return {
996
+ uuid: existing.uuid ?? null,
997
+ created: false
998
+ };
999
+ }
1000
+ try {
1001
+ const uuid = (await createDropletInstallation(client, {
1002
+ droplet_uuid: dropletUuid,
1003
+ scopes: [...args.scopes]
1004
+ })).droplet_installation?.uuid ?? null;
1005
+ reporter.succeed(`Droplet installed${uuid ? ` (${uuid})` : ""}`);
1006
+ return {
1007
+ uuid,
1008
+ created: true
1009
+ };
1010
+ } catch (err) {
1011
+ if (isAlreadyInstalledError(err)) {
1012
+ const confirmed = await lookupInstallation(client, dropletUuid);
1013
+ if (confirmed) {
1014
+ reporter.skip("Droplet already installed — skipping");
1015
+ return {
1016
+ uuid: confirmed.uuid ?? null,
1017
+ created: false
1018
+ };
1019
+ }
1020
+ }
1021
+ reporter.fail("Droplet install failed");
1022
+ throw normalizeInstallError(err, dropletUuid);
1023
+ }
1024
+ }
1025
+ function isAlreadyInstalledError(err) {
1026
+ return err instanceof MistApiError && (err.status === 409 || err.status === 422) && /already|taken|installed|exists/i.test(`${err.message} ${JSON.stringify(err.details ?? {})}`);
1027
+ }
1028
+ /** Best-effort installation lookup for a droplet; null when the row can't
1029
+ * be found or the list call fails. */
1030
+ async function lookupInstallation(client, dropletUuid) {
1031
+ try {
1032
+ return findInstallationForDroplet(await listAllDropletInstallations(client), dropletUuid) ?? null;
1033
+ } catch {
1034
+ return null;
1035
+ }
1036
+ }
1037
+ /**
1038
+ * Install failures must NOT read as "wired up" — an uninstalled droplet means
1039
+ * dit/dex token exchange, admin embeds, and install webhooks silently don't
1040
+ * work. Surface the droplet uuid + recovery path; keep the MistApiError
1041
+ * status so the error handler exits with the spec'd non-zero code.
1042
+ */
1043
+ function normalizeInstallError(err, dropletUuid) {
1044
+ const message = `Droplet ${dropletUuid} was created and linked to the mist, but installing it failed: ${err instanceof Error ? err.message : String(err)}. The droplet and its link were left in place — re-run this command to finish the install, or install manually: POST /api/droplet_installations with {"droplet_uuid":"${dropletUuid}"}.`;
1045
+ return err instanceof MistApiError ? new MistApiError(message, err.status, err.details) : new Error(message);
1046
+ }
1047
+ /**
901
1048
  * If the integration-points endpoint rejects the attach identifier (e.g. an
902
1049
  * older API that predates #19688's attach-by-uuid support), surface an
903
1050
  * actionable message noting the surface WAS created. Other errors pass through.
@@ -1031,6 +1178,10 @@ function printSurfaceSummary(result) {
1031
1178
  const ref = result.surface.uuid ?? (result.surface.id != null ? `#${result.surface.id}` : "(unknown)");
1032
1179
  const state = result.surfaceCreated ? "created" : "existing";
1033
1180
  console.log(chalk.dim(` ${KIND_LABEL[result.kind]}: ${ref} (${state}) · integration ${result.integrationPoint.id}`));
1181
+ if (result.kind === "droplet" && result.installation) {
1182
+ const ref = result.installation.uuid ?? "(uuid unknown)";
1183
+ console.log(chalk.dim(` installation: ${ref} (${result.installation.created ? "installed" : "already installed"})`));
1184
+ }
1034
1185
  if (result.kind === "droplet" && result.managedEnv) if (result.managedEnv.ready) console.log(chalk.dim(" env: managed keys provisioned ✓"));
1035
1186
  else console.log(chalk.yellow(" env: redeploy in progress — run `fluid mist env list` shortly to confirm managed keys"));
1036
1187
  }
@@ -2035,12 +2186,67 @@ const logsCommand = new Command("logs").description("Fetch deployment logs (use
2035
2186
  }
2036
2187
  }));
2037
2188
  //#endregion
2189
+ //#region src/lib/encrypted-env.ts
2190
+ /**
2191
+ * Detects Vercel-encrypted env var ciphertext.
2192
+ *
2193
+ * `GET /api/v202604/mists/:id/env_vars` is a thin proxy to the Mist's
2194
+ * Vercel project env — it returns values ENCRYPTED, never plaintext (see
2195
+ * apps/mist-desktop/src/main/services/mist-env-vars.service.ts:25-28,
2196
+ * which documents the same server behavior for the desktop app's copy of
2197
+ * this API). The ciphertext is a base64-encoded JSON envelope of shape
2198
+ * `{"v":"v2","c":"<base64 ciphertext>","k":[<byte array>]}`.
2199
+ *
2200
+ * `fluid mist env pull` uses this to avoid writing that ciphertext into
2201
+ * `.env.local` as if it were a usable value.
2202
+ */
2203
+ const BASE64_RE = /^[A-Za-z0-9+/]+={0,2}$/;
2204
+ function looksLikeBase64(value) {
2205
+ return value.length > 0 && value.length % 4 === 0 && BASE64_RE.test(value);
2206
+ }
2207
+ function hasEncryptedEnvelopeShape(value) {
2208
+ return typeof value === "object" && value !== null && !Array.isArray(value) && "v" in value && "c" in value && "k" in value;
2209
+ }
2210
+ /**
2211
+ * True when `value` is the Vercel encrypted-env envelope: a base64 string
2212
+ * that decodes to a JSON object with the `{ v, c, k }` shape. Returns
2213
+ * false for empty strings, non-base64 strings, and base64 strings that
2214
+ * decode to plaintext or to JSON that doesn't match the envelope shape —
2215
+ * so real plaintext secrets that happen to look like base64 are never
2216
+ * misclassified as ciphertext.
2217
+ */
2218
+ function isVercelEncryptedEnvValue(value) {
2219
+ if (!looksLikeBase64(value)) return false;
2220
+ let decoded;
2221
+ try {
2222
+ decoded = Buffer.from(value, "base64").toString("utf8");
2223
+ } catch {
2224
+ return false;
2225
+ }
2226
+ let parsed;
2227
+ try {
2228
+ parsed = JSON.parse(decoded);
2229
+ } catch {
2230
+ return false;
2231
+ }
2232
+ return hasEncryptedEnvelopeShape(parsed);
2233
+ }
2234
+ //#endregion
2038
2235
  //#region src/commands/env/list.ts
2039
2236
  /** `fluid mist env list` — list env vars; mark managed_by entries. Spec §4. */
2040
2237
  function maskValue(value) {
2041
2238
  if (value.length <= 8) return "•".repeat(value.length);
2042
2239
  return `${value.slice(0, 4)}${"•".repeat(8)}${value.slice(-2)}`;
2043
2240
  }
2241
+ /**
2242
+ * Values come back Vercel-encrypted ciphertext (see
2243
+ * `../../lib/encrypted-env.ts`) — never show that raw blob, masked or not,
2244
+ * since it isn't the real secret and can run to ~1KB of base64.
2245
+ */
2246
+ function displayValue(v, showValues) {
2247
+ if (isVercelEncryptedEnvValue(v.value)) return chalk.dim("<encrypted>");
2248
+ return showValues ? v.value : maskValue(v.value);
2249
+ }
2044
2250
  const envListCommand = new Command("list").description("List a mist's env vars").argument("[slug]", "The mist slug (defaults to .mist file in CWD)").option("--show-values", "Print full values instead of masking them").option("--json", "Emit raw API JSON").action((slug, opts) => runAction(async () => {
2045
2251
  const resolved = resolveSlug(slug);
2046
2252
  const response = await listEnvVars(createMistClient(), resolved);
@@ -2057,7 +2263,7 @@ const envListCommand = new Command("list").description("List a mist's env vars")
2057
2263
  chalk.bold("MANAGED")
2058
2264
  ], ...vars.map((v) => [
2059
2265
  v.key,
2060
- opts.showValues ? v.value : maskValue(v.value),
2266
+ displayValue(v, opts.showValues),
2061
2267
  v.targets.join(","),
2062
2268
  v.managed_by === "mist" ? chalk.magenta("mist") : chalk.dim("user")
2063
2269
  ])]);
@@ -2137,21 +2343,79 @@ const envUnsetCommand = new Command("unset").description("Delete one or more env
2137
2343
  * `fluid mist env pull` — writes `.env.local` with user-managed vars only.
2138
2344
  * Skips `managed_by: "mist"` entries (DATABASE_URL, FLUID_*) since local
2139
2345
  * dev uses PGlite, not the production database. Spec §4 + §5.
2346
+ *
2347
+ * Also skips user-managed vars whose value comes back as Vercel-encrypted
2348
+ * ciphertext (`GET .../env_vars` is a proxy to Vercel's project env, which
2349
+ * never returns plaintext — see `../../lib/encrypted-env.ts`). Writing that
2350
+ * ciphertext into `.env.local` verbatim would silently produce an unusable
2351
+ * file, so those keys are collected and surfaced as a warning instead.
2352
+ *
2353
+ * The output file is MERGED, not replaced: pulled plaintext values update
2354
+ * their existing lines (or are appended), while every other existing line
2355
+ * — comments, locally maintained secrets for encrypted/managed keys, and
2356
+ * local-only vars — is preserved verbatim. Replacing the file wholesale
2357
+ * would delete manually set values for skipped keys, and an all-encrypted
2358
+ * response would truncate the file to zero bytes right before telling the
2359
+ * user to set those same values locally.
2140
2360
  */
2141
2361
  function escapeValue(v) {
2142
2362
  if (/[\s#"']/.test(v)) return `"${v.replace(/(["\\$`])/g, "\\$1")}"`;
2143
2363
  return v;
2144
2364
  }
2365
+ /**
2366
+ * Splits the raw env-var list into what `env pull` can safely write vs.
2367
+ * what it must skip, and why. Pure — no I/O — so it's unit-testable
2368
+ * without mocking the API client or the filesystem.
2369
+ */
2370
+ function partitionPullEnvVars(envVars) {
2371
+ const plaintext = [];
2372
+ const encrypted = [];
2373
+ const managed = [];
2374
+ for (const v of envVars) if (v.managed_by === "mist") managed.push(v);
2375
+ else if (isVercelEncryptedEnvValue(v.value)) encrypted.push(v);
2376
+ else plaintext.push(v);
2377
+ return {
2378
+ plaintext,
2379
+ encrypted,
2380
+ managed
2381
+ };
2382
+ }
2383
+ /** Matches a `KEY=` / `export KEY=` assignment line and captures the key. */
2384
+ const ENV_ASSIGNMENT = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/;
2385
+ /**
2386
+ * Merges pulled plaintext vars into the existing env-file contents.
2387
+ * Existing assignment lines for pulled keys are updated in place; every
2388
+ * other line (comments, blank lines, locally maintained values for keys
2389
+ * the pull skipped) is preserved verbatim; new keys are appended. Pure —
2390
+ * no I/O — so it's unit-testable without touching the filesystem.
2391
+ */
2392
+ function mergeEnvContents(existing, plaintext) {
2393
+ const updates = new Map(plaintext.map((v) => [v.key, `${v.key}=${escapeValue(v.value)}`]));
2394
+ const lines = existing.length > 0 ? existing.split("\n") : [];
2395
+ if (lines.at(-1) === "") lines.pop();
2396
+ const out = [];
2397
+ for (const line of lines) {
2398
+ const key = ENV_ASSIGNMENT.exec(line)?.[1];
2399
+ const replacement = key === void 0 ? void 0 : updates.get(key);
2400
+ if (key !== void 0 && replacement !== void 0) {
2401
+ out.push(replacement);
2402
+ updates.delete(key);
2403
+ } else out.push(line);
2404
+ }
2405
+ for (const rendered of updates.values()) out.push(rendered);
2406
+ return out.length > 0 ? out.join("\n") + "\n" : "";
2407
+ }
2145
2408
  const envPullCommand = new Command("pull").description("Write user-managed env vars to .env.local in the CWD").argument("[slug]", "The mist slug (defaults to .mist file in CWD)").option("--out <path>", "Output path (default: .env.local)").action((slug, opts) => runAction(async () => {
2146
2409
  const resolved = resolveSlug(slug);
2147
2410
  const { env_vars } = await listEnvVars(createMistClient(), resolved);
2148
- const user = env_vars.filter((v) => v.managed_by === "user");
2149
- const skipped = env_vars.filter((v) => v.managed_by === "mist");
2150
- const target = join(process.cwd(), opts.out ?? ".env.local");
2151
- const body = user.map((v) => `${v.key}=${escapeValue(v.value)}`).join("\n");
2152
- writeFileSync(target, body + (body.length ? "\n" : ""), "utf8");
2153
- console.log(chalk.green(`Wrote ${user.length} var(s) to ${target.replace(process.cwd() + "/", "")}.`));
2154
- if (skipped.length > 0) console.log(chalk.dim(`Skipped ${skipped.length} Mist-managed var(s): ${skipped.map((v) => v.key).join(", ")} (local dev uses PGlite)`));
2411
+ const { plaintext, encrypted, managed } = partitionPullEnvVars(env_vars);
2412
+ const outName = opts.out ?? ".env.local";
2413
+ const target = join(process.cwd(), outName);
2414
+ const existing = existsSync(target) ? readFileSync(target, "utf8") : "";
2415
+ writeFileSync(target, mergeEnvContents(existing, plaintext), "utf8");
2416
+ console.log(chalk.green(`Wrote ${plaintext.length} var(s) to ${target.replace(process.cwd() + "/", "")}${existing.length > 0 ? " (existing entries for other keys preserved)" : ""}.`));
2417
+ if (managed.length > 0) console.log(chalk.dim(`Skipped ${managed.length} Mist-managed var(s): ${managed.map((v) => v.key).join(", ")} (local dev uses PGlite)`));
2418
+ if (encrypted.length > 0) console.log(chalk.yellow(`Skipped ${encrypted.length} var(s) whose values are stored encrypted and can't be pulled: ${encrypted.map((v) => v.key).join(", ")} — set them locally in ${outName} yourself (values are visible in the Vercel dashboard) or re-set them via \`fluid mist env set\`.`));
2155
2419
  }));
2156
2420
  //#endregion
2157
2421
  //#region src/commands/env/index.ts