@fluid-app/fluid-cli-widget 0.1.27 → 0.1.29

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,14 +1,16 @@
1
+ import { i as fluidOsV0WidgetPackageGitCredentialCreate, l as WidgetToolingError, n as loadAndValidateWidgetPackage, r as publishWidgetPackage$1, t as buildWidgetPackage$1 } from "./src-_wfDoBLp.mjs";
1
2
  import { Command } from "commander";
2
- import { buildSharedWidgetPackage, createAuthenticatedWidgetPackageClient, loadSourceWidgetPackages, printDryRunSessionPayload, printRuntimeOnlyNotice, printWidgetPublishSummary, publishWidgetRuntimeArtifacts, validateSingleSourceWidgetPackage, validateWidgetPublishOutDir } from "@fluid-app/fluid-cli-portal";
3
+ import path from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import { execa } from "execa";
6
+ import { fileURLToPath } from "node:url";
7
+ import { findProjectConfig, readConfig } from "@fluid-app/fluid-cli";
8
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
+ import { cp, mkdir, mkdtemp, readFile, readdir, rm, rmdir, stat, writeFile } from "node:fs/promises";
3
10
  import chalk from "chalk";
4
11
  import ora from "ora";
5
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
- import { cp, mkdir, mkdtemp, readFile, readdir, rm, rmdir, stat, writeFile } from "node:fs/promises";
7
- import path from "node:path";
8
12
  import { spawn, spawnSync } from "node:child_process";
9
13
  import os from "node:os";
10
- import { execa } from "execa";
11
- import { fileURLToPath } from "node:url";
12
14
  import prompts from "prompts";
13
15
  //#region src/utils/config.ts
14
16
  const WIDGET_CONFIG_FILE = "fluid.widget.config.ts";
@@ -317,22 +319,27 @@ async function validateDropletWidgetProject(projectDir, packageKeyOverride) {
317
319
  }]);
318
320
  const configErrors = await validateRootWidgetConfig(resolvedProjectDir, packageKeyOverride);
319
321
  if (configErrors.length > 0) throw new WidgetValidationError(configErrors);
320
- const sourcePackages = await withRootWidgetConfigBridge(resolvedProjectDir, async () => {
321
- const loadResult = await loadSourceWidgetPackages(resolvedProjectDir);
322
- if (!loadResult.success) throw new WidgetValidationError([{
323
- code: "INVALID_SOURCE_PACKAGE",
324
- path: sourceConfig.relativePath,
325
- message: appendDetails(loadResult.error.message, loadResult.error.details)
326
- }]);
327
- return loadResult.value;
328
- }, packageKeyOverride);
329
- const validation = validateSingleSourceWidgetPackage(sourcePackages, { owner: "droplet" });
330
- const errors = [...validation.errors, ...validateDropletMetadata(sourcePackages)];
331
- if (!validation.success || !validation.value || errors.length > 0) throw new WidgetValidationError(errors);
322
+ let validated;
323
+ try {
324
+ validated = (await withRootWidgetConfigBridge(resolvedProjectDir, () => loadAndValidateWidgetPackage({
325
+ projectDir: resolvedProjectDir,
326
+ owner: "droplet"
327
+ }), packageKeyOverride)).validated;
328
+ } catch (error) {
329
+ if (error instanceof WidgetToolingError) {
330
+ if (error.diagnostics) throw new WidgetValidationError(error.diagnostics);
331
+ throw new WidgetValidationError([{
332
+ code: "INVALID_SOURCE_PACKAGE",
333
+ path: sourceConfig.relativePath,
334
+ message: error.details ? `${error.message}\n${error.details}` : error.message
335
+ }]);
336
+ }
337
+ throw error;
338
+ }
332
339
  return {
333
340
  projectDir: resolvedProjectDir,
334
341
  sourceConfigPath: sourceConfig.relativePath,
335
- validated: validation.value
342
+ validated
336
343
  };
337
344
  }
338
345
  async function withRootWidgetConfigBridge(projectDir, callback, packageKeyOverride) {
@@ -411,29 +418,6 @@ async function validateRootWidgetConfig(projectDir, packageKeyOverride) {
411
418
  }];
412
419
  }
413
420
  }
414
- function validateDropletMetadata(sourcePackages) {
415
- if (sourcePackages.length !== 1) return [];
416
- const sourcePackage = sourcePackages[0];
417
- if (!isRecord$5(sourcePackage)) return [];
418
- const errors = [];
419
- if (sourcePackage["packageType"] !== "droplet") errors.push({
420
- code: "UNSUPPORTED_OWNER",
421
- path: "packageType",
422
- message: "Droplet widget packages must set packageType: \"droplet\" in package metadata."
423
- });
424
- if (!isNonEmptyString(sourcePackage["packageStableId"])) errors.push({
425
- code: "INVALID_PACKAGE_KEY",
426
- path: "packageStableId",
427
- message: "Droplet widget packages must set packageStableId so published widget types remain stable across builds."
428
- });
429
- const widgets = sourcePackage["widgets"];
430
- if (Array.isArray(widgets)) widgets.forEach((widget, index) => {
431
- if (!isRecord$5(widget)) return;
432
- validateJsonSerializable(widget["propertySchema"], `widgets[${index}].propertySchema`, errors);
433
- validateJsonSerializable(widget["defaultProps"], `widgets[${index}].defaultProps`, errors);
434
- });
435
- return errors;
436
- }
437
421
  function resolvePortalSourceConfig(projectDir) {
438
422
  for (const relativePath of SOURCE_CONFIG_CANDIDATES) {
439
423
  const candidate = path.join(projectDir, relativePath);
@@ -443,50 +427,6 @@ function resolvePortalSourceConfig(projectDir) {
443
427
  };
444
428
  }
445
429
  }
446
- function validateJsonSerializable(value, pathLabel, errors) {
447
- if (value === void 0) return;
448
- try {
449
- assertJsonSerializable(value, pathLabel);
450
- } catch (err) {
451
- errors.push({
452
- code: "INVALID_WIDGET_METADATA",
453
- path: pathLabel,
454
- message: err instanceof Error ? err.message : String(err)
455
- });
456
- }
457
- }
458
- function assertJsonSerializable(value, pathLabel) {
459
- if (value === null) return;
460
- const valueType = typeof value;
461
- if (valueType === "string" || valueType === "boolean") return;
462
- if (valueType === "number") {
463
- if (Number.isFinite(value)) return;
464
- throw new Error(`${pathLabel} must not contain NaN or Infinity.`);
465
- }
466
- if (Array.isArray(value)) {
467
- value.forEach((item, index) => {
468
- assertJsonSerializable(item, `${pathLabel}[${index}]`);
469
- });
470
- return;
471
- }
472
- if (isRecord$5(value)) {
473
- for (const [key, item] of Object.entries(value)) {
474
- if (item === void 0) throw new Error(`${pathLabel}.${key} must not be undefined.`);
475
- assertJsonSerializable(item, `${pathLabel}.${key}`);
476
- }
477
- return;
478
- }
479
- throw new Error(`${pathLabel} must contain only JSON-serializable values.`);
480
- }
481
- function appendDetails(message, details) {
482
- return details ? `${message}\n${details}` : message;
483
- }
484
- function isNonEmptyString(value) {
485
- return typeof value === "string" && value.trim().length > 0;
486
- }
487
- function isRecord$5(value) {
488
- return typeof value === "object" && value !== null && !Array.isArray(value);
489
- }
490
430
  function isNodeError(error) {
491
431
  return error instanceof Error && "code" in error;
492
432
  }
@@ -633,6 +573,138 @@ function isRecord$3(value) {
633
573
  return typeof value === "object" && value !== null && !Array.isArray(value);
634
574
  }
635
575
  //#endregion
576
+ //#region src/utils/template.ts
577
+ const EMBEDDED_PORTAL_SDK_VERSION = "0.1.463";
578
+ const EMBEDDED_CLI_CORE_VERSION = "0.1.14";
579
+ const EMBEDDED_CLI_WIDGET_VERSION = "0.1.29";
580
+ function getDefaultTemplatePath() {
581
+ return path.join(findPackageRoot(), "templates", "default");
582
+ }
583
+ async function directoryExists(filePath) {
584
+ try {
585
+ return (await stat(filePath)).isDirectory();
586
+ } catch {
587
+ return false;
588
+ }
589
+ }
590
+ async function copyWidgetTemplate(templatePath, targetPath, variables) {
591
+ const files = await getFiles(templatePath);
592
+ for (const file of files) {
593
+ const sourcePath = path.join(templatePath, file);
594
+ const isTemplate = file.endsWith(".template");
595
+ const outputFiles = getOutputFilenames(file);
596
+ const content = await readFile(sourcePath, "utf-8");
597
+ await writeOutputFiles(targetPath, outputFiles, isTemplate ? renderTemplate(content, variables) : content);
598
+ }
599
+ for (const skillFile of await getSharedTemplateSkillFiles()) await writeOutputFiles(targetPath, getOutputFilenames(skillFile.relativePath), skillFile.content);
600
+ }
601
+ async function getPortalSdkVersion(options = {}) {
602
+ return getWorkspacePackageVersion(["portal", "sdk"], EMBEDDED_PORTAL_SDK_VERSION, options);
603
+ }
604
+ async function getPortalCoreVersion(options = {}) {
605
+ return getWorkspacePackageVersion(["portal", "core"], void 0, options);
606
+ }
607
+ async function getCliVersion(options = {}) {
608
+ return getWorkspacePackageVersion(["cli", "core"], EMBEDDED_CLI_CORE_VERSION, options);
609
+ }
610
+ async function getWidgetCliVersion(options = {}) {
611
+ return getWorkspacePackageVersion(["cli", "widget"], EMBEDDED_CLI_WIDGET_VERSION, options);
612
+ }
613
+ async function getWorkspacePackageVersion(packagePathSegments, embeddedVersion, options) {
614
+ try {
615
+ const packageRoot = findPackageRoot();
616
+ const packagesRoot = path.join(packageRoot, "..", "..");
617
+ const targetPackageRoot = path.join(packagesRoot, ...packagePathSegments);
618
+ if (options.local) return `link:${(options.targetPath ? path.relative(options.targetPath, targetPackageRoot) : targetPackageRoot).replace(/\\/g, "/") || "."}`;
619
+ if (embeddedVersion) return embeddedVersion;
620
+ const packageJsonPath = path.join(targetPackageRoot, "package.json");
621
+ const content = await readFile(packageJsonPath, "utf-8");
622
+ const pkg = JSON.parse(content);
623
+ if (typeof pkg.version !== "string") throw new Error(`Missing version in ${packageJsonPath}`);
624
+ return pkg.version;
625
+ } catch (error) {
626
+ if (embeddedVersion) return embeddedVersion;
627
+ throw new Error(`Could not resolve the exact template package version for ${packagePathSegments.join("/")}`, { cause: error });
628
+ }
629
+ }
630
+ async function getFiles(dir, baseDir = dir) {
631
+ const entries = await readdir(dir, { withFileTypes: true });
632
+ const files = [];
633
+ for (const entry of entries) {
634
+ const fullPath = path.join(dir, entry.name);
635
+ if (entry.isDirectory()) files.push(...await getFiles(fullPath, baseDir));
636
+ else files.push(path.relative(baseDir, fullPath));
637
+ }
638
+ return files;
639
+ }
640
+ async function getSharedTemplateSkillFiles() {
641
+ const packageRoot = findFluidCliPackageRoot();
642
+ const skillsRoot = path.join(packageRoot, "template-skills");
643
+ const files = await getFiles(skillsRoot);
644
+ return Promise.all(files.map(async (file) => ({
645
+ relativePath: path.join("skills", file),
646
+ content: await readFile(path.join(skillsRoot, file), "utf-8")
647
+ })));
648
+ }
649
+ function findFluidCliPackageRoot() {
650
+ const workspacePackageRoot = path.join(findPackageRoot(), "..", "core");
651
+ if (existsSync(path.join(workspacePackageRoot, "template-skills"))) return workspacePackageRoot;
652
+ let dir = path.dirname(fileURLToPath(import.meta.resolve("@fluid-app/fluid-cli")));
653
+ while (!existsSync(path.join(dir, "package.json"))) {
654
+ const parent = path.dirname(dir);
655
+ if (parent === dir) throw new Error("Could not find Fluid CLI package root");
656
+ dir = parent;
657
+ }
658
+ return dir;
659
+ }
660
+ function renderTemplate(content, variables) {
661
+ return content.replace(/{{\s*#if\s+([a-zA-Z0-9_]+)\s*}}([\s\S]*?){{\s*\/if\s*}}/g, (match, key, block) => {
662
+ if (!isTemplateVariableKey(key)) return match;
663
+ return variables[key] ? String(block) : "";
664
+ }).replace(/{{\s*([a-zA-Z0-9_]+)\s*}}/g, (match, key) => {
665
+ if (isTemplateVariableKey(key)) return variables[key] ?? "";
666
+ return match;
667
+ });
668
+ }
669
+ function isTemplateVariableKey(key) {
670
+ return [
671
+ "projectName",
672
+ "widgetScope",
673
+ "widgetPackageVersion",
674
+ "sdkVersion",
675
+ "localCoreVersion",
676
+ "fluidCliVersion",
677
+ "widgetCliVersion",
678
+ "droplet"
679
+ ].includes(key);
680
+ }
681
+ async function writeOutputFiles(targetPath, outputFiles, content) {
682
+ for (const outputFile of outputFiles) {
683
+ const destinationPath = path.join(targetPath, outputFile);
684
+ await mkdir(path.dirname(destinationPath), { recursive: true });
685
+ await writeFile(destinationPath, content, "utf-8");
686
+ }
687
+ }
688
+ function getOutputFilenames(filename) {
689
+ const outputFilename = getOutputFilename(filename);
690
+ const normalized = outputFilename.replace(/\\/g, "/");
691
+ if (normalized === "AGENTS.md") return ["AGENTS.md", "CLAUDE.md"];
692
+ if (normalized.startsWith("skills/")) return [path.join(".agents", outputFilename), path.join(".claude", outputFilename)];
693
+ return [outputFilename];
694
+ }
695
+ function getOutputFilename(filename) {
696
+ return filename.endsWith(".template") ? filename.slice(0, -9) : filename;
697
+ }
698
+ function findPackageRoot() {
699
+ let dir = path.dirname(fileURLToPath(import.meta.url));
700
+ while (!existsSync(path.join(dir, "package.json"))) {
701
+ const parent = path.dirname(dir);
702
+ if (parent === dir) throw new Error("Could not find widget CLI package root");
703
+ dir = parent;
704
+ }
705
+ return dir;
706
+ }
707
+ //#endregion
636
708
  //#region src/commands/build.ts
637
709
  async function buildWidgetPackage(options) {
638
710
  await validateDropletWidgetProject(options.projectDir);
@@ -640,20 +712,23 @@ async function buildWidgetPackage(options) {
640
712
  projectDir: options.projectDir,
641
713
  scriptName: "typecheck"
642
714
  });
643
- const outDir = validateWidgetPublishOutDir(options.projectDir, options.outDir);
644
- const buildResult = await withRootWidgetConfigBridge(options.projectDir, () => buildSharedWidgetPackage({
645
- projectDir: options.projectDir,
646
- publishDir: outDir,
647
- owner: "droplet"
648
- }));
649
- if (!buildResult.success) {
650
- const details = buildResult.error.details ? `\n${buildResult.error.details}` : "";
651
- throw new Error(`${buildResult.error.message}${details}`);
715
+ let buildResult;
716
+ try {
717
+ const cliVersion = await getWidgetCliVersion();
718
+ buildResult = await withRootWidgetConfigBridge(options.projectDir, () => buildWidgetPackage$1({
719
+ projectDir: options.projectDir,
720
+ outDir: options.outDir,
721
+ owner: "droplet",
722
+ cliVersion
723
+ }));
724
+ } catch (error) {
725
+ if (error instanceof WidgetToolingError && error.details) throw new Error(`${error.message}\n${error.details}`, { cause: error });
726
+ throw error;
652
727
  }
653
728
  return {
654
- outDir: buildResult.value.publishDir,
655
- packageId: buildResult.value.packageId,
656
- version: buildResult.value.version
729
+ outDir: buildResult.publishDir,
730
+ packageId: buildResult.packageId,
731
+ version: buildResult.version
657
732
  };
658
733
  }
659
734
  const buildCommand = new Command("build").description("Validate and build the current droplet widget package").option("-o, --out-dir <dir>", "Widget artifact output directory", DEFAULT_WIDGET_OUT_DIR).option("--skip-typecheck", "Skip package typecheck script when present").action((options) => {
@@ -675,6 +750,769 @@ const buildCommand = new Command("build").description("Validate and build the cu
675
750
  });
676
751
  });
677
752
  //#endregion
753
+ //#region ../../platform/api-client-core/src/api-error-shape.ts
754
+ /**
755
+ * Narrows a parsed JSON error payload to the envelope contract. Arrays and
756
+ * primitives are not envelopes, so they resolve to `null` — their content is
757
+ * still reachable through `ApiError.data`.
758
+ */
759
+ function toApiErrorBody(value) {
760
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
761
+ return value;
762
+ }
763
+ /**
764
+ * The single unwrap rule, shared by every producer of `ApiError`.
765
+ *
766
+ * `??` rather than `||` so an explicitly empty `errors` (`""`, `0`, `false`)
767
+ * is preserved instead of silently falling back to the whole envelope.
768
+ */
769
+ function toApiErrorData(body) {
770
+ if (!body) return null;
771
+ return body.errors ?? body;
772
+ }
773
+ //#endregion
774
+ //#region ../../platform/api-client-core/src/fetch-client.ts
775
+ /**
776
+ * API Error class compatible with fluid-admin's ApiError
777
+ */
778
+ var ApiError = class ApiError extends Error {
779
+ status;
780
+ /**
781
+ * The unwrapped field-error bag. See `ApiErrorData`; the contract is
782
+ * documented in `docs/api-error-data-contract.md`.
783
+ */
784
+ data;
785
+ /**
786
+ * The full error response envelope. Read `error_message`, `error`, `status`
787
+ * and nested `errors` from here — `data` has already unwrapped one level.
788
+ */
789
+ body;
790
+ requestId;
791
+ constructor(message, status, data, requestId, body) {
792
+ super(message);
793
+ this.name = "ApiError";
794
+ this.status = status;
795
+ this.data = data ?? null;
796
+ this.body = body ?? null;
797
+ this.requestId = requestId;
798
+ if ("captureStackTrace" in Error) Error.captureStackTrace(this, ApiError);
799
+ }
800
+ toJSON() {
801
+ return {
802
+ name: this.name,
803
+ message: this.message,
804
+ status: this.status,
805
+ data: this.data,
806
+ body: this.body,
807
+ requestId: this.requestId
808
+ };
809
+ }
810
+ };
811
+ //#endregion
812
+ //#region ../../platform/api-client-core/src/hey-api-error-policy.ts
813
+ function getNonEmptyString(value) {
814
+ if (typeof value !== "string") return;
815
+ const trimmed = value.trim();
816
+ return trimmed.length > 0 ? trimmed : void 0;
817
+ }
818
+ function getRequestId(response, error) {
819
+ const meta = toApiErrorBody(error?.meta);
820
+ return getNonEmptyString(response.headers.get("x-request-id")) ?? getNonEmptyString(response.headers.get("request-id")) ?? getNonEmptyString(error?.request_id) ?? getNonEmptyString(error?.requestId) ?? getNonEmptyString(meta?.request_id) ?? getNonEmptyString(meta?.requestId);
821
+ }
822
+ function getErrorMessage(error, fallback) {
823
+ const nestedError = toApiErrorBody(error.error);
824
+ return getNonEmptyString(error.message) ?? getNonEmptyString(error.error_message) ?? getNonEmptyString(nestedError?.message) ?? fallback;
825
+ }
826
+ /**
827
+ * Creates an error interceptor compatible with generated Hey API fetch clients.
828
+ * Consumer-specific auth and cancellation policy is supplied through callbacks,
829
+ * keeping this package independent from generated client types.
830
+ */
831
+ function createHeyApiErrorInterceptor(options = {}) {
832
+ return (error, response, request, requestOptions) => {
833
+ if (!response) {
834
+ if (error instanceof ApiError) return error;
835
+ if (options.shouldPassThroughTransportError?.(error)) return error;
836
+ if (!request) return error;
837
+ return new ApiError(`Network error: ${error instanceof Error ? error.message : "Unknown network error"}`, 0, null);
838
+ }
839
+ if (response.ok && error instanceof SyntaxError) return new ApiError("Failed to parse response as JSON", response.status, null, getRequestId(response, null));
840
+ options.onResponseError?.(response);
841
+ const method = request?.method ?? requestOptions.method ?? "GET";
842
+ const errorRecord = toApiErrorBody(error);
843
+ const requestId = getRequestId(response, errorRecord);
844
+ if (errorRecord) return new ApiError(getErrorMessage(errorRecord, `${method} request failed`), response.status, toApiErrorData(errorRecord), requestId, errorRecord);
845
+ const isJson = response.headers.get("content-type")?.includes("application/json");
846
+ const text = getNonEmptyString(error);
847
+ return new ApiError(isJson && text ? text.slice(0, 200) : `${method} request failed with status ${response.status}`, response.status, isJson ? error : null, requestId, null);
848
+ };
849
+ }
850
+ //#endregion
851
+ //#region ../../api-clients/hey-api-fetch-runtime/src/generated/core/bodySerializer.gen.ts
852
+ const jsonBodySerializer = { bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) };
853
+ Object.entries({
854
+ $body_: "body",
855
+ $headers_: "headers",
856
+ $path_: "path",
857
+ $query_: "query"
858
+ });
859
+ //#endregion
860
+ //#region ../../api-clients/hey-api-fetch-runtime/src/generated/core/serverSentEvents.gen.ts
861
+ function createSseClient({ onRequest, onSseError, onSseEvent, responseTransformer, responseValidator, sseDefaultRetryDelay, sseMaxRetryAttempts, sseMaxRetryDelay, sseSleepFn, url, ...options }) {
862
+ let lastEventId;
863
+ const sleep = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
864
+ const createStream = async function* () {
865
+ let retryDelay = sseDefaultRetryDelay ?? 3e3;
866
+ let attempt = 0;
867
+ const signal = options.signal ?? new AbortController().signal;
868
+ while (true) {
869
+ if (signal.aborted) break;
870
+ attempt++;
871
+ const headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers);
872
+ if (lastEventId !== void 0) headers.set("Last-Event-ID", lastEventId);
873
+ try {
874
+ const requestInit = {
875
+ redirect: "follow",
876
+ ...options,
877
+ body: options.serializedBody,
878
+ headers,
879
+ signal
880
+ };
881
+ let request = new Request(url, requestInit);
882
+ if (onRequest) request = await onRequest(url, requestInit);
883
+ const response = await (options.fetch ?? globalThis.fetch)(request);
884
+ if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
885
+ if (!response.body) throw new Error("No body in SSE response");
886
+ const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
887
+ let buffer = "";
888
+ const abortHandler = () => {
889
+ try {
890
+ reader.cancel();
891
+ } catch {}
892
+ };
893
+ signal.addEventListener("abort", abortHandler);
894
+ try {
895
+ while (true) {
896
+ const { done, value } = await reader.read();
897
+ if (done) break;
898
+ buffer += value;
899
+ buffer = buffer.replace(/\r\n?/g, "\n");
900
+ const chunks = buffer.split("\n\n");
901
+ buffer = chunks.pop() ?? "";
902
+ for (const chunk of chunks) {
903
+ const lines = chunk.split("\n");
904
+ const dataLines = [];
905
+ let eventName;
906
+ for (const line of lines) if (line.startsWith("data:")) dataLines.push(line.replace(/^data:\s*/, ""));
907
+ else if (line.startsWith("event:")) eventName = line.replace(/^event:\s*/, "");
908
+ else if (line.startsWith("id:")) lastEventId = line.replace(/^id:\s*/, "");
909
+ else if (line.startsWith("retry:")) {
910
+ const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10);
911
+ if (!Number.isNaN(parsed)) retryDelay = parsed;
912
+ }
913
+ let data;
914
+ let parsedJson = false;
915
+ if (dataLines.length) {
916
+ const rawData = dataLines.join("\n");
917
+ try {
918
+ data = JSON.parse(rawData);
919
+ parsedJson = true;
920
+ } catch {
921
+ data = rawData;
922
+ }
923
+ }
924
+ if (parsedJson) {
925
+ if (responseValidator) await responseValidator(data);
926
+ if (responseTransformer) data = await responseTransformer(data);
927
+ }
928
+ onSseEvent?.({
929
+ data,
930
+ event: eventName,
931
+ id: lastEventId,
932
+ retry: retryDelay
933
+ });
934
+ if (dataLines.length) yield data;
935
+ }
936
+ }
937
+ } finally {
938
+ signal.removeEventListener("abort", abortHandler);
939
+ reader.releaseLock();
940
+ }
941
+ break;
942
+ } catch (error) {
943
+ onSseError?.(error);
944
+ if (sseMaxRetryAttempts !== void 0 && attempt >= sseMaxRetryAttempts) break;
945
+ await sleep(Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 3e4));
946
+ }
947
+ }
948
+ };
949
+ return { stream: createStream() };
950
+ }
951
+ //#endregion
952
+ //#region ../../api-clients/hey-api-fetch-runtime/src/generated/core/pathSerializer.gen.ts
953
+ const separatorArrayExplode = (style) => {
954
+ switch (style) {
955
+ case "label": return ".";
956
+ case "matrix": return ";";
957
+ case "simple": return ",";
958
+ default: return "&";
959
+ }
960
+ };
961
+ const separatorArrayNoExplode = (style) => {
962
+ switch (style) {
963
+ case "form": return ",";
964
+ case "pipeDelimited": return "|";
965
+ case "spaceDelimited": return "%20";
966
+ default: return ",";
967
+ }
968
+ };
969
+ const separatorObjectExplode = (style) => {
970
+ switch (style) {
971
+ case "label": return ".";
972
+ case "matrix": return ";";
973
+ case "simple": return ",";
974
+ default: return "&";
975
+ }
976
+ };
977
+ const serializeArrayParam = ({ allowReserved, explode, name, style, value }) => {
978
+ if (!explode) {
979
+ const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v))).join(separatorArrayNoExplode(style));
980
+ switch (style) {
981
+ case "label": return `.${joinedValues}`;
982
+ case "matrix": return `;${name}=${joinedValues}`;
983
+ case "simple": return joinedValues;
984
+ default: return `${name}=${joinedValues}`;
985
+ }
986
+ }
987
+ const separator = separatorArrayExplode(style);
988
+ const joinedValues = value.map((v) => {
989
+ if (style === "label" || style === "simple") return allowReserved ? v : encodeURIComponent(v);
990
+ return serializePrimitiveParam({
991
+ allowReserved,
992
+ name,
993
+ value: v
994
+ });
995
+ }).join(separator);
996
+ return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
997
+ };
998
+ const serializePrimitiveParam = ({ allowReserved, name, value }) => {
999
+ if (value === void 0 || value === null) return "";
1000
+ if (typeof value === "object") throw new Error("Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.");
1001
+ return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
1002
+ };
1003
+ const serializeObjectParam = ({ allowReserved, explode, name, style, value, valueOnly }) => {
1004
+ if (value instanceof Date) return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
1005
+ if (style !== "deepObject" && !explode) {
1006
+ let values = [];
1007
+ Object.entries(value).forEach(([key, v]) => {
1008
+ values = [
1009
+ ...values,
1010
+ key,
1011
+ allowReserved ? v : encodeURIComponent(v)
1012
+ ];
1013
+ });
1014
+ const joinedValues = values.join(",");
1015
+ switch (style) {
1016
+ case "form": return `${name}=${joinedValues}`;
1017
+ case "label": return `.${joinedValues}`;
1018
+ case "matrix": return `;${name}=${joinedValues}`;
1019
+ default: return joinedValues;
1020
+ }
1021
+ }
1022
+ const separator = separatorObjectExplode(style);
1023
+ const joinedValues = Object.entries(value).map(([key, v]) => serializePrimitiveParam({
1024
+ allowReserved,
1025
+ name: style === "deepObject" ? `${name}[${key}]` : key,
1026
+ value: v
1027
+ })).join(separator);
1028
+ return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
1029
+ };
1030
+ //#endregion
1031
+ //#region ../../api-clients/hey-api-fetch-runtime/src/generated/core/utils.gen.ts
1032
+ const PATH_PARAM_RE = /\{[^{}]+\}/g;
1033
+ const defaultPathSerializer = ({ path, url: _url }) => {
1034
+ let url = _url;
1035
+ const matches = _url.match(PATH_PARAM_RE);
1036
+ if (matches) for (const match of matches) {
1037
+ let explode = false;
1038
+ let name = match.substring(1, match.length - 1);
1039
+ let style = "simple";
1040
+ if (name.endsWith("*")) {
1041
+ explode = true;
1042
+ name = name.substring(0, name.length - 1);
1043
+ }
1044
+ if (name.startsWith(".")) {
1045
+ name = name.substring(1);
1046
+ style = "label";
1047
+ } else if (name.startsWith(";")) {
1048
+ name = name.substring(1);
1049
+ style = "matrix";
1050
+ }
1051
+ const value = path[name];
1052
+ if (value === void 0 || value === null) continue;
1053
+ if (Array.isArray(value)) {
1054
+ url = url.replace(match, serializeArrayParam({
1055
+ explode,
1056
+ name,
1057
+ style,
1058
+ value
1059
+ }));
1060
+ continue;
1061
+ }
1062
+ if (typeof value === "object") {
1063
+ url = url.replace(match, serializeObjectParam({
1064
+ explode,
1065
+ name,
1066
+ style,
1067
+ value,
1068
+ valueOnly: true
1069
+ }));
1070
+ continue;
1071
+ }
1072
+ if (style === "matrix") {
1073
+ url = url.replace(match, `;${serializePrimitiveParam({
1074
+ name,
1075
+ value
1076
+ })}`);
1077
+ continue;
1078
+ }
1079
+ const replaceValue = encodeURIComponent(style === "label" ? `.${value}` : value);
1080
+ url = url.replace(match, replaceValue);
1081
+ }
1082
+ return url;
1083
+ };
1084
+ const getUrl = ({ baseUrl, path, query, querySerializer, url: _url }) => {
1085
+ const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
1086
+ let url = (baseUrl ?? "") + pathUrl;
1087
+ if (path) url = defaultPathSerializer({
1088
+ path,
1089
+ url
1090
+ });
1091
+ let search = query ? querySerializer(query) : "";
1092
+ if (search.startsWith("?")) search = search.substring(1);
1093
+ if (search) url += `?${search}`;
1094
+ return url;
1095
+ };
1096
+ function getValidRequestBody(options) {
1097
+ const hasBody = options.body !== void 0;
1098
+ if (hasBody && options.bodySerializer) {
1099
+ if ("serializedBody" in options) return options.serializedBody !== void 0 && options.serializedBody !== "" ? options.serializedBody : null;
1100
+ return options.body !== "" ? options.body : null;
1101
+ }
1102
+ if (hasBody) return options.body;
1103
+ }
1104
+ //#endregion
1105
+ //#region ../../api-clients/hey-api-fetch-runtime/src/generated/core/auth.gen.ts
1106
+ const getAuthToken = async (auth, callback) => {
1107
+ const token = typeof callback === "function" ? await callback(auth) : callback;
1108
+ if (!token) return;
1109
+ if (auth.scheme === "bearer") return `Bearer ${token}`;
1110
+ if (auth.scheme === "basic") return `Basic ${btoa(token)}`;
1111
+ return token;
1112
+ };
1113
+ //#endregion
1114
+ //#region ../../api-clients/hey-api-fetch-runtime/src/generated/client/utils.gen.ts
1115
+ const createQuerySerializer = ({ parameters = {}, ...args } = {}) => {
1116
+ const querySerializer = (queryParams) => {
1117
+ const search = [];
1118
+ if (queryParams && typeof queryParams === "object") for (const name in queryParams) {
1119
+ const value = queryParams[name];
1120
+ if (value === void 0 || value === null) continue;
1121
+ const options = parameters[name] || args;
1122
+ if (Array.isArray(value)) {
1123
+ const serializedArray = serializeArrayParam({
1124
+ allowReserved: options.allowReserved,
1125
+ explode: true,
1126
+ name,
1127
+ style: "form",
1128
+ value,
1129
+ ...options.array
1130
+ });
1131
+ if (serializedArray) search.push(serializedArray);
1132
+ } else if (typeof value === "object") {
1133
+ const serializedObject = serializeObjectParam({
1134
+ allowReserved: options.allowReserved,
1135
+ explode: true,
1136
+ name,
1137
+ style: "deepObject",
1138
+ value,
1139
+ ...options.object
1140
+ });
1141
+ if (serializedObject) search.push(serializedObject);
1142
+ } else {
1143
+ const serializedPrimitive = serializePrimitiveParam({
1144
+ allowReserved: options.allowReserved,
1145
+ name,
1146
+ value
1147
+ });
1148
+ if (serializedPrimitive) search.push(serializedPrimitive);
1149
+ }
1150
+ }
1151
+ return search.join("&");
1152
+ };
1153
+ return querySerializer;
1154
+ };
1155
+ /**
1156
+ * Infers parseAs value from provided Content-Type header.
1157
+ */
1158
+ const getParseAs = (contentType) => {
1159
+ if (!contentType) return "stream";
1160
+ const cleanContent = contentType.split(";")[0]?.trim();
1161
+ if (!cleanContent) return;
1162
+ if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) return "json";
1163
+ if (cleanContent === "multipart/form-data") return "formData";
1164
+ if ([
1165
+ "application/",
1166
+ "audio/",
1167
+ "image/",
1168
+ "video/"
1169
+ ].some((type) => cleanContent.startsWith(type))) return "blob";
1170
+ if (cleanContent.startsWith("text/")) return "text";
1171
+ };
1172
+ const checkForExistence = (options, name) => {
1173
+ if (!name) return false;
1174
+ if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) return true;
1175
+ return false;
1176
+ };
1177
+ async function setAuthParams(options) {
1178
+ for (const auth of options.security ?? []) {
1179
+ if (checkForExistence(options, auth.name)) continue;
1180
+ const token = await getAuthToken(auth, options.auth);
1181
+ if (!token) continue;
1182
+ const name = auth.name ?? "Authorization";
1183
+ switch (auth.in) {
1184
+ case "query":
1185
+ if (!options.query) options.query = {};
1186
+ options.query[name] = token;
1187
+ break;
1188
+ case "cookie":
1189
+ options.headers.append("Cookie", `${name}=${token}`);
1190
+ break;
1191
+ default:
1192
+ options.headers.set(name, token);
1193
+ break;
1194
+ }
1195
+ }
1196
+ }
1197
+ const buildUrl = (options) => getUrl({
1198
+ baseUrl: options.baseUrl,
1199
+ path: options.path,
1200
+ query: options.query,
1201
+ querySerializer: typeof options.querySerializer === "function" ? options.querySerializer : createQuerySerializer(options.querySerializer),
1202
+ url: options.url
1203
+ });
1204
+ const mergeConfigs = (a, b) => {
1205
+ const config = {
1206
+ ...a,
1207
+ ...b
1208
+ };
1209
+ if (config.baseUrl?.endsWith("/")) config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
1210
+ config.headers = mergeHeaders(a.headers, b.headers);
1211
+ return config;
1212
+ };
1213
+ const headersEntries = (headers) => {
1214
+ const entries = [];
1215
+ headers.forEach((value, key) => {
1216
+ entries.push([key, value]);
1217
+ });
1218
+ return entries;
1219
+ };
1220
+ const mergeHeaders = (...headers) => {
1221
+ const mergedHeaders = new Headers();
1222
+ for (const header of headers) {
1223
+ if (!header) continue;
1224
+ const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
1225
+ for (const [key, value] of iterator) if (value === null) mergedHeaders.delete(key);
1226
+ else if (Array.isArray(value)) for (const v of value) mergedHeaders.append(key, v);
1227
+ else if (value !== void 0) mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : value);
1228
+ }
1229
+ return mergedHeaders;
1230
+ };
1231
+ var Interceptors = class {
1232
+ fns = [];
1233
+ clear() {
1234
+ this.fns = [];
1235
+ }
1236
+ eject(id) {
1237
+ const index = this.getInterceptorIndex(id);
1238
+ if (this.fns[index]) this.fns[index] = null;
1239
+ }
1240
+ exists(id) {
1241
+ const index = this.getInterceptorIndex(id);
1242
+ return Boolean(this.fns[index]);
1243
+ }
1244
+ getInterceptorIndex(id) {
1245
+ if (typeof id === "number") return this.fns[id] ? id : -1;
1246
+ return this.fns.indexOf(id);
1247
+ }
1248
+ update(id, fn) {
1249
+ const index = this.getInterceptorIndex(id);
1250
+ if (this.fns[index]) {
1251
+ this.fns[index] = fn;
1252
+ return id;
1253
+ }
1254
+ return false;
1255
+ }
1256
+ use(fn) {
1257
+ this.fns.push(fn);
1258
+ return this.fns.length - 1;
1259
+ }
1260
+ };
1261
+ const createInterceptors = () => ({
1262
+ error: new Interceptors(),
1263
+ request: new Interceptors(),
1264
+ response: new Interceptors()
1265
+ });
1266
+ const defaultQuerySerializer = createQuerySerializer({
1267
+ allowReserved: false,
1268
+ array: {
1269
+ explode: true,
1270
+ style: "form"
1271
+ },
1272
+ object: {
1273
+ explode: true,
1274
+ style: "deepObject"
1275
+ }
1276
+ });
1277
+ const defaultHeaders = { "Content-Type": "application/json" };
1278
+ const createConfig = (override = {}) => ({
1279
+ ...jsonBodySerializer,
1280
+ headers: defaultHeaders,
1281
+ parseAs: "auto",
1282
+ querySerializer: defaultQuerySerializer,
1283
+ ...override
1284
+ });
1285
+ //#endregion
1286
+ //#region ../../api-clients/hey-api-fetch-runtime/src/generated/client/client.gen.ts
1287
+ const createClient = (config = {}) => {
1288
+ let _config = mergeConfigs(createConfig(), config);
1289
+ const getConfig = () => ({ ..._config });
1290
+ const setConfig = (config) => {
1291
+ _config = mergeConfigs(_config, config);
1292
+ return getConfig();
1293
+ };
1294
+ const interceptors = createInterceptors();
1295
+ const beforeRequest = async (options) => {
1296
+ const opts = {
1297
+ ..._config,
1298
+ ...options,
1299
+ fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
1300
+ headers: mergeHeaders(_config.headers, options.headers),
1301
+ serializedBody: void 0
1302
+ };
1303
+ if (opts.security) await setAuthParams(opts);
1304
+ if (opts.requestValidator) await opts.requestValidator(opts);
1305
+ if (opts.body !== void 0 && opts.bodySerializer) opts.serializedBody = opts.bodySerializer(opts.body);
1306
+ if (opts.body === void 0 || opts.serializedBody === "") opts.headers.delete("Content-Type");
1307
+ const resolvedOpts = opts;
1308
+ return {
1309
+ opts: resolvedOpts,
1310
+ url: buildUrl(resolvedOpts)
1311
+ };
1312
+ };
1313
+ const request = async (options) => {
1314
+ const throwOnError = options.throwOnError ?? _config.throwOnError;
1315
+ const responseStyle = options.responseStyle ?? _config.responseStyle;
1316
+ let request;
1317
+ let response;
1318
+ try {
1319
+ const { opts, url } = await beforeRequest(options);
1320
+ const requestInit = {
1321
+ redirect: "follow",
1322
+ ...opts,
1323
+ body: getValidRequestBody(opts)
1324
+ };
1325
+ request = new Request(url, requestInit);
1326
+ for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
1327
+ const _fetch = opts.fetch;
1328
+ response = await _fetch(request);
1329
+ for (const fn of interceptors.response.fns) if (fn) response = await fn(response, request, opts);
1330
+ const result = {
1331
+ request,
1332
+ response
1333
+ };
1334
+ if (response.ok) {
1335
+ const parseAs = (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json";
1336
+ if (response.status === 204 || response.headers.get("Content-Length") === "0") {
1337
+ let emptyData;
1338
+ switch (parseAs) {
1339
+ case "arrayBuffer":
1340
+ case "blob":
1341
+ case "text":
1342
+ emptyData = await response[parseAs]();
1343
+ break;
1344
+ case "formData":
1345
+ emptyData = new FormData();
1346
+ break;
1347
+ case "stream":
1348
+ emptyData = response.body;
1349
+ break;
1350
+ default:
1351
+ emptyData = {};
1352
+ break;
1353
+ }
1354
+ return opts.responseStyle === "data" ? emptyData : {
1355
+ data: emptyData,
1356
+ ...result
1357
+ };
1358
+ }
1359
+ let data;
1360
+ switch (parseAs) {
1361
+ case "arrayBuffer":
1362
+ case "blob":
1363
+ case "formData":
1364
+ case "text":
1365
+ data = await response[parseAs]();
1366
+ break;
1367
+ case "json": {
1368
+ const text = await response.text();
1369
+ data = text ? JSON.parse(text) : {};
1370
+ break;
1371
+ }
1372
+ case "stream": return opts.responseStyle === "data" ? response.body : {
1373
+ data: response.body,
1374
+ ...result
1375
+ };
1376
+ }
1377
+ if (parseAs === "json") {
1378
+ if (opts.responseValidator) await opts.responseValidator(data);
1379
+ if (opts.responseTransformer) data = await opts.responseTransformer(data);
1380
+ }
1381
+ return opts.responseStyle === "data" ? data : {
1382
+ data,
1383
+ ...result
1384
+ };
1385
+ }
1386
+ const textError = await response.text();
1387
+ let jsonError;
1388
+ try {
1389
+ jsonError = JSON.parse(textError);
1390
+ } catch {}
1391
+ throw jsonError ?? textError;
1392
+ } catch (error) {
1393
+ let finalError = error;
1394
+ for (const fn of interceptors.error.fns) if (fn) finalError = await fn(finalError, response, request, options);
1395
+ finalError = finalError || {};
1396
+ if (throwOnError) throw finalError;
1397
+ return responseStyle === "data" ? void 0 : {
1398
+ error: finalError,
1399
+ request,
1400
+ response
1401
+ };
1402
+ }
1403
+ };
1404
+ const makeMethodFn = (method) => (options) => request({
1405
+ ...options,
1406
+ method
1407
+ });
1408
+ const makeSseFn = (method) => async (options) => {
1409
+ const { opts, url } = await beforeRequest(options);
1410
+ return createSseClient({
1411
+ ...opts,
1412
+ body: opts.body,
1413
+ method,
1414
+ onRequest: async (url, init) => {
1415
+ let request = new Request(url, init);
1416
+ for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
1417
+ return request;
1418
+ },
1419
+ serializedBody: getValidRequestBody(opts),
1420
+ url
1421
+ });
1422
+ };
1423
+ const _buildUrl = (options) => buildUrl({
1424
+ ..._config,
1425
+ ...options
1426
+ });
1427
+ return {
1428
+ buildUrl: _buildUrl,
1429
+ connect: makeMethodFn("CONNECT"),
1430
+ delete: makeMethodFn("DELETE"),
1431
+ get: makeMethodFn("GET"),
1432
+ getConfig,
1433
+ head: makeMethodFn("HEAD"),
1434
+ interceptors,
1435
+ options: makeMethodFn("OPTIONS"),
1436
+ patch: makeMethodFn("PATCH"),
1437
+ post: makeMethodFn("POST"),
1438
+ put: makeMethodFn("PUT"),
1439
+ request,
1440
+ setConfig,
1441
+ sse: {
1442
+ connect: makeSseFn("CONNECT"),
1443
+ delete: makeSseFn("DELETE"),
1444
+ get: makeSseFn("GET"),
1445
+ head: makeSseFn("HEAD"),
1446
+ options: makeSseFn("OPTIONS"),
1447
+ patch: makeSseFn("PATCH"),
1448
+ post: makeSseFn("POST"),
1449
+ put: makeSseFn("PUT"),
1450
+ trace: makeSseFn("TRACE")
1451
+ },
1452
+ trace: makeMethodFn("TRACE")
1453
+ };
1454
+ };
1455
+ //#endregion
1456
+ //#region ../../api-clients/fluidos/src/hey-api-client.ts
1457
+ function isAbortError(error) {
1458
+ return error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
1459
+ }
1460
+ function createFluidOsHeyApiClient(config) {
1461
+ const client = createClient({
1462
+ auth: async () => await config.getAuthToken?.() ?? void 0,
1463
+ baseUrl: config.baseUrl,
1464
+ fetch: config.fetch,
1465
+ headers: {
1466
+ Accept: "application/json",
1467
+ ...config.defaultHeaders
1468
+ },
1469
+ throwOnError: true
1470
+ });
1471
+ client.interceptors.error.use(createHeyApiErrorInterceptor({
1472
+ onResponseError: (response) => {
1473
+ if (response.status === 401) config.onAuthError?.();
1474
+ },
1475
+ shouldPassThroughTransportError: isAbortError
1476
+ }));
1477
+ return client;
1478
+ }
1479
+ //#endregion
1480
+ //#region src/utils/auth-client.ts
1481
+ function createAuthenticatedClient() {
1482
+ return { client: createAuthenticatedWidgetPackageClient() };
1483
+ }
1484
+ function createAuthenticatedWidgetPackageClient() {
1485
+ const { profile, profileName, token } = resolveAuthenticatedProfile();
1486
+ if (!token) {
1487
+ if (!profileName) throw new Error("Not logged in. Run " + chalk.cyan("fluid login") + " first.");
1488
+ throw new Error("No auth token found for profile " + chalk.cyan(profileName) + ". Run " + chalk.cyan("fluid login") + " to re-authenticate.");
1489
+ }
1490
+ return createFluidOsHeyApiClient({
1491
+ baseUrl: profile?.baseUrl ?? process.env["FLUID_API_BASE"] ?? "https://api.fluid.app",
1492
+ getAuthToken: () => token
1493
+ });
1494
+ }
1495
+ function resolveAuthenticatedProfile() {
1496
+ const config = readConfig();
1497
+ const projectConfig = findProjectConfig(process.cwd());
1498
+ if (projectConfig?.profile) {
1499
+ const profile = config.profiles[projectConfig.profile];
1500
+ if (profile) return {
1501
+ profile,
1502
+ profileName: projectConfig.profile,
1503
+ token: profile.token
1504
+ };
1505
+ }
1506
+ if (!config.activeProfile) return {};
1507
+ const profile = config.profiles[config.activeProfile];
1508
+ if (!profile) return {};
1509
+ return {
1510
+ profile,
1511
+ profileName: config.activeProfile,
1512
+ token: profile.token
1513
+ };
1514
+ }
1515
+ //#endregion
678
1516
  //#region src/lib/git.ts
679
1517
  /**
680
1518
  * Git wrapper for the Fluid widget CLI — shells out to the system `git`
@@ -994,50 +1832,268 @@ async function gitAdoptFetchHead(cwd) {
994
1832
  throw wrapGitError(err, "adopt remote history");
995
1833
  }
996
1834
  }
997
- /** Fast-forward `HEAD` to `FETCH_HEAD`. Fails if not fast-forwardable. */
998
- async function gitMergeFastForward(cwd) {
1835
+ /** Fast-forward `HEAD` to `FETCH_HEAD`. Fails if not fast-forwardable. */
1836
+ async function gitMergeFastForward(cwd) {
1837
+ try {
1838
+ await execa("git", [
1839
+ "merge",
1840
+ "--ff-only",
1841
+ "FETCH_HEAD"
1842
+ ], { cwd });
1843
+ } catch (err) {
1844
+ throw wrapGitError(err, "fast-forward merge");
1845
+ }
1846
+ }
1847
+ /** Are there uncommitted changes (working tree or index) in this repo? */
1848
+ async function hasUncommittedChanges(cwd) {
1849
+ try {
1850
+ const { stdout } = await execa("git", ["status", "--porcelain"], { cwd });
1851
+ return stdout.trim().length > 0;
1852
+ } catch {
1853
+ return false;
1854
+ }
1855
+ }
1856
+ /** Stash working-tree + untracked changes with a tagged message. */
1857
+ async function gitStash(cwd, message) {
1858
+ try {
1859
+ await execa("git", [
1860
+ "stash",
1861
+ "push",
1862
+ "--include-untracked",
1863
+ "--message",
1864
+ message
1865
+ ], { cwd });
1866
+ } catch (err) {
1867
+ throw wrapGitError(err, "stash push");
1868
+ }
1869
+ }
1870
+ /** Pop the most recent stash. Throws on conflict so callers can surface it. */
1871
+ async function gitStashPop(cwd) {
1872
+ try {
1873
+ await execa("git", ["stash", "pop"], { cwd });
1874
+ } catch (err) {
1875
+ const missing = gitMissingError(err);
1876
+ if (missing) throw missing;
1877
+ throw new GitError("Couldn't restore your local changes after pulling — the edits collided with a teammate's. Run `git stash list` and resolve manually.", err.exitCode ?? 1);
1878
+ }
1879
+ }
1880
+ /**
1881
+ * Local-only state that must never reach a widget's provisioned repo.
1882
+ *
1883
+ * `.mist-desktop/` is Mist Desktop's per-machine working state for the
1884
+ * project directory it opened — chat transcripts, user attachments,
1885
+ * screenshots, source baselines, media cache. When Desktop drives
1886
+ * `fluid widget push`, that directory sits in the project root, so a
1887
+ * whole-tree `git add -A` publishes every private conversation.
1888
+ * `.mist` is the sibling clone/slug marker.
1889
+ *
1890
+ * Mirrors the portal CLI (`packages/cli/portal/src/lib/git.ts`) and the
1891
+ * Mist CLI (`packages/cli/mist/src/lib/git.ts`).
1892
+ */
1893
+ const LOCAL_ONLY_GIT_PATHS = [".mist", ".mist-desktop/"];
1894
+ /**
1895
+ * Append {@link LOCAL_ONLY_GIT_PATHS} to `.git/info/exclude` when absent.
1896
+ *
1897
+ * Why the local exclude and not `.gitignore`: `.gitignore` ships from the
1898
+ * widget starter template, so editing it would never reach the clones
1899
+ * already on people's machines. `.git/info/exclude` is per-checkout, so
1900
+ * writing it on every sync heals a pre-existing clone on its next push.
1901
+ *
1902
+ * Idempotent and best-effort: never throws. {@link gitAutoCommit} also
1903
+ * cleans these paths out of the index after staging.
1904
+ */
1905
+ function excludeLocalStateFromGit(cwd) {
1906
+ try {
1907
+ const gitDir = path.join(cwd, ".git");
1908
+ if (!existsSync(gitDir)) return;
1909
+ const infoDir = path.join(gitDir, "info");
1910
+ if (!existsSync(infoDir)) mkdirSync(infoDir, { recursive: true });
1911
+ const excludePath = path.join(infoDir, "exclude");
1912
+ const existing = existsSync(excludePath) ? readFileSync(excludePath, "utf8") : "";
1913
+ const existingLines = new Set(existing.split("\n").map((line) => line.trim()).filter(Boolean));
1914
+ const missing = LOCAL_ONLY_GIT_PATHS.filter((entry) => !existingLines.has(entry));
1915
+ if (missing.length === 0) return;
1916
+ writeFileSync(excludePath, `${existing}${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}${missing.join("\n")}\n`, "utf8");
1917
+ } catch {}
1918
+ }
1919
+ /**
1920
+ * Absolute path of the repository containing `cwd`, or `cwd` when
1921
+ * resolution fails. Every local-state defense anchors here: `git add -A`
1922
+ * stages the WHOLE repository, so a push run from a subdirectory would
1923
+ * otherwise look for `<subdir>/.git`, skip the exclude, and sweep the
1924
+ * root `.mist-desktop/` in anyway.
1925
+ */
1926
+ async function resolveRepoRoot(cwd) {
1927
+ try {
1928
+ const { stdout } = await execa("git", ["rev-parse", "--show-toplevel"], { cwd });
1929
+ const root = stdout.trim();
1930
+ if (root.length > 0) return root;
1931
+ } catch {}
1932
+ return cwd;
1933
+ }
1934
+ /**
1935
+ * Repository-root-anchored {@link excludeLocalStateFromGit}. Run before
1936
+ * any working-tree cleanliness check: while `.mist-desktop/` is neither
1937
+ * tracked nor excluded it shows as untracked in `git status --porcelain`,
1938
+ * so a Desktop-driven push needlessly stashes/pops around a pull.
1939
+ */
1940
+ async function ensureLocalStateExcluded(cwd) {
1941
+ excludeLocalStateFromGit(await resolveRepoRoot(cwd));
1942
+ }
1943
+ /** Paths currently tracked under `.mist-desktop/`, or [] when none. */
1944
+ async function trackedLocalStatePaths(repoRoot) {
999
1945
  try {
1000
- await execa("git", [
1001
- "merge",
1002
- "--ff-only",
1003
- "FETCH_HEAD"
1004
- ], { cwd });
1005
- } catch (err) {
1006
- throw wrapGitError(err, "fast-forward merge");
1946
+ const { stdout } = await execa("git", [
1947
+ "ls-files",
1948
+ "--",
1949
+ ".mist-desktop"
1950
+ ], { cwd: repoRoot });
1951
+ return stdout.split("\n").filter((l) => l.trim().length > 0);
1952
+ } catch {
1953
+ return [];
1007
1954
  }
1008
1955
  }
1009
- /** Are there uncommitted changes (working tree or index) in this repo? */
1010
- async function hasUncommittedChanges(cwd) {
1956
+ /**
1957
+ * Untrack private Mist Desktop state and, if anything was tracked,
1958
+ * commit ONLY that removal so the next push deletes it from the remote.
1959
+ * Returns true when a purge commit was made.
1960
+ *
1961
+ * Must run regardless of working-tree cleanliness AND of `--no-commit`:
1962
+ * an older CLI could have committed chats to a checkout that is now
1963
+ * clean, and gating on either would skip the cleanup forever, leaving
1964
+ * the chats published.
1965
+ *
1966
+ * The commit is built through a THROWAWAY index (`GIT_INDEX_FILE`), not
1967
+ * the user's: `read-tree HEAD` → `rm --cached` → `write-tree` →
1968
+ * `commit-tree` → `update-ref`. A plain `git commit` would capture the
1969
+ * user's entire index, so anything they had pre-staged would be swept
1970
+ * into the purge commit and pushed — the exact thing `--no-commit`
1971
+ * exists to prevent. A pathspec commit (`git commit -- .mist-desktop`)
1972
+ * is also wrong: with `--only` git takes the *working tree* content of
1973
+ * those paths, and the chats are still on disk, so it would re-add
1974
+ * exactly what we are removing.
1975
+ *
1976
+ * The user's real index then gets the same scoped `rm --cached`, so any
1977
+ * other staged work stays staged and the working tree is never touched.
1978
+ */
1979
+ async function purgeLocalStateFromIndex(cwd, message, identity = DEFAULT_COMMIT_IDENTITY) {
1980
+ const repoRoot = await resolveRepoRoot(cwd);
1981
+ if ((await trackedLocalStatePaths(repoRoot)).length === 0) return false;
1982
+ let head;
1011
1983
  try {
1012
- const { stdout } = await execa("git", ["status", "--porcelain"], { cwd });
1013
- return stdout.trim().length > 0;
1984
+ const { stdout } = await execa("git", ["rev-parse", "HEAD"], { cwd: repoRoot });
1985
+ head = stdout.trim();
1986
+ if (!head) return false;
1014
1987
  } catch {
1015
1988
  return false;
1016
1989
  }
1017
- }
1018
- /** Stash working-tree + untracked changes with a tagged message. */
1019
- async function gitStash(cwd, message) {
1990
+ const tmpIndex = path.join(os.tmpdir(), `fluid-widget-purge-${process.pid}-${randomUUID()}.index`);
1991
+ const indexEnv = {
1992
+ ...process.env,
1993
+ GIT_INDEX_FILE: tmpIndex
1994
+ };
1995
+ let commitSha;
1020
1996
  try {
1997
+ await execa("git", ["read-tree", head], {
1998
+ cwd: repoRoot,
1999
+ env: indexEnv
2000
+ });
1021
2001
  await execa("git", [
1022
- "stash",
1023
- "push",
1024
- "--include-untracked",
1025
- "--message",
2002
+ "rm",
2003
+ "-q",
2004
+ "-r",
2005
+ "--cached",
2006
+ "--ignore-unmatch",
2007
+ "--",
2008
+ ".mist-desktop"
2009
+ ], {
2010
+ cwd: repoRoot,
2011
+ env: indexEnv
2012
+ });
2013
+ const { stdout: tree } = await execa("git", ["write-tree"], {
2014
+ cwd: repoRoot,
2015
+ env: indexEnv
2016
+ });
2017
+ const { stdout: sha } = await execa("git", [
2018
+ "commit-tree",
2019
+ tree.trim(),
2020
+ "-p",
2021
+ head,
2022
+ "-m",
1026
2023
  message
1027
- ], { cwd });
2024
+ ], {
2025
+ cwd: repoRoot,
2026
+ env: {
2027
+ ...indexEnv,
2028
+ ...await identityEnv(repoRoot, identity)
2029
+ }
2030
+ });
2031
+ commitSha = sha.trim();
2032
+ if (!commitSha) return false;
1028
2033
  } catch (err) {
1029
- throw wrapGitError(err, "stash push");
2034
+ const missing = gitMissingError(err);
2035
+ if (missing) throw missing;
2036
+ throw wrapGitError(err, "purge local state");
2037
+ } finally {
2038
+ rmSync(tmpIndex, { force: true });
1030
2039
  }
1031
- }
1032
- /** Pop the most recent stash. Throws on conflict so callers can surface it. */
1033
- async function gitStashPop(cwd) {
1034
2040
  try {
1035
- await execa("git", ["stash", "pop"], { cwd });
2041
+ await execa("git", [
2042
+ "update-ref",
2043
+ "HEAD",
2044
+ commitSha,
2045
+ head
2046
+ ], { cwd: repoRoot });
1036
2047
  } catch (err) {
1037
- const missing = gitMissingError(err);
1038
- if (missing) throw missing;
1039
- throw new GitError("Couldn't restore your local changes after pulling — the edits collided with a teammate's. Run `git stash list` and resolve manually.", err.exitCode ?? 1);
2048
+ throw wrapGitError(err, "purge local state");
2049
+ }
2050
+ try {
2051
+ await execa("git", [
2052
+ "rm",
2053
+ "-q",
2054
+ "-r",
2055
+ "--cached",
2056
+ "--ignore-unmatch",
2057
+ "--",
2058
+ ".mist-desktop"
2059
+ ], { cwd: repoRoot });
2060
+ } catch {}
2061
+ return true;
2062
+ }
2063
+ /** `GIT_{AUTHOR,COMMITTER}_*` overrides for `commit-tree`, which (unlike
2064
+ * `git commit`) fails outright rather than prompting when the repo has
2065
+ * no identity. Only set what the repo itself doesn't define. */
2066
+ async function identityEnv(cwd, identity) {
2067
+ const env = {};
2068
+ const has = async (key) => {
2069
+ try {
2070
+ return (await execa("git", ["config", key], { cwd })).stdout.trim().length > 0;
2071
+ } catch {
2072
+ return false;
2073
+ }
2074
+ };
2075
+ if (!await has("user.name")) {
2076
+ env["GIT_AUTHOR_NAME"] = identity.name;
2077
+ env["GIT_COMMITTER_NAME"] = identity.name;
2078
+ }
2079
+ if (!await has("user.email")) {
2080
+ env["GIT_AUTHOR_EMAIL"] = identity.email;
2081
+ env["GIT_COMMITTER_EMAIL"] = identity.email;
1040
2082
  }
2083
+ return env;
2084
+ }
2085
+ /** Shared `-c user.*` args: prefer the repo's configured identity, else
2086
+ * the supplied synthetic one so the commit isn't rejected. */
2087
+ async function resolveIdentityArgs(cwd, identity) {
2088
+ let hasName = false;
2089
+ let hasEmail = false;
2090
+ try {
2091
+ hasName = (await execa("git", ["config", "user.name"], { cwd })).stdout.trim().length > 0;
2092
+ } catch {}
2093
+ try {
2094
+ hasEmail = (await execa("git", ["config", "user.email"], { cwd })).stdout.trim().length > 0;
2095
+ } catch {}
2096
+ return [...hasName ? [] : ["-c", `user.name=${identity.name}`], ...hasEmail ? [] : ["-c", `user.email=${identity.email}`]];
1041
2097
  }
1042
2098
  const DEFAULT_COMMIT_IDENTITY = {
1043
2099
  name: "Fluid CLI",
@@ -1051,22 +2107,54 @@ const DEFAULT_COMMIT_IDENTITY = {
1051
2107
  * commit per push. If the repo has no configured user.name / user.email we
1052
2108
  * pass a synthetic identity so the commit isn't rejected. "nothing to commit"
1053
2109
  * is treated as a no-op rather than an error.
2110
+ *
2111
+ * Scope: `add -A` stages the whole work tree, bounded by the project
2112
+ * `.gitignore` — which can't cover tool-local state written INTO the
2113
+ * project by whatever opened it (Mist Desktop's `.mist-desktop/` chats and
2114
+ * the `.mist` marker). Belt and suspenders, all repo-root-anchored:
2115
+ *
2116
+ * 1. {@link excludeLocalStateFromGit} — idempotent `.git/info/exclude`
2117
+ * write so `git add` never sees the paths. Heals old clones.
2118
+ * 2. `git reset -- ':(top).mist' .mist` — unstages the marker if step 1
2119
+ * silently failed. We deliberately do NOT pass `:(exclude)` to
2120
+ * `git add`: when the path is also in `.git/info/exclude`, git exits
2121
+ * 1 with "paths are ignored by one of your .gitignore files", which
2122
+ * would turn the already-healed case into a hard failure.
2123
+ * 3. `git rm --cached -r --ignore-unmatch .mist-desktop` — untracks
2124
+ * chats an older push already committed (ignore rules can't untrack).
2125
+ * `--cached` keeps every file on disk.
2126
+ *
2127
+ * Steps 2 and 3 are best-effort and never fail the push.
1054
2128
  */
1055
2129
  async function gitAutoCommit(cwd, message, identity = DEFAULT_COMMIT_IDENTITY) {
2130
+ const repoRoot = await resolveRepoRoot(cwd);
2131
+ excludeLocalStateFromGit(repoRoot);
1056
2132
  try {
1057
2133
  await execa("git", ["add", "-A"], { cwd });
1058
2134
  } catch (err) {
1059
2135
  throw wrapGitError(err, "add");
1060
2136
  }
1061
- let hasName = false;
1062
- let hasEmail = false;
1063
2137
  try {
1064
- hasName = (await execa("git", ["config", "user.name"], { cwd })).stdout.trim().length > 0;
2138
+ await execa("git", [
2139
+ "reset",
2140
+ "-q",
2141
+ "--",
2142
+ ":(top).mist",
2143
+ ".mist"
2144
+ ], { cwd });
1065
2145
  } catch {}
1066
2146
  try {
1067
- hasEmail = (await execa("git", ["config", "user.email"], { cwd })).stdout.trim().length > 0;
2147
+ await execa("git", [
2148
+ "rm",
2149
+ "-q",
2150
+ "-r",
2151
+ "--cached",
2152
+ "--ignore-unmatch",
2153
+ "--",
2154
+ ".mist-desktop"
2155
+ ], { cwd: repoRoot });
1068
2156
  } catch {}
1069
- const identityArgs = [...hasName ? [] : ["-c", `user.name=${identity.name}`], ...hasEmail ? [] : ["-c", `user.email=${identity.email}`]];
2157
+ const identityArgs = await resolveIdentityArgs(cwd, identity);
1070
2158
  try {
1071
2159
  await execa("git", [
1072
2160
  ...identityArgs,
@@ -1091,6 +2179,10 @@ async function gitAutoCommit(cwd, message, identity = DEFAULT_COMMIT_IDENTITY) {
1091
2179
  }
1092
2180
  //#endregion
1093
2181
  //#region src/utils/git-credential.ts
2182
+ function unwrapResponseData(response) {
2183
+ if (response !== null && typeof response === "object" && "data" in response && !("git_credential" in response)) return response.data;
2184
+ return response;
2185
+ }
1094
2186
  /**
1095
2187
  * Mint a git credential for a widget package.
1096
2188
  *
@@ -1103,8 +2195,13 @@ async function gitAutoCommit(cwd, message, identity = DEFAULT_COMMIT_IDENTITY) {
1103
2195
  * decide whether to degrade gracefully (see `isGitSyncUnavailable`).
1104
2196
  */
1105
2197
  async function mintWidgetGitCredential(options) {
1106
- const path = options.droplet ? `/api/droplets/${encodeURIComponent(options.droplet)}/widget_package/git_credential` : "/api/company/fluid_os/widget_package/git_credential";
1107
- const response = await options.client.post(path);
2198
+ const response = unwrapResponseData(options.droplet ? await options.client.post({
2199
+ responseStyle: "data",
2200
+ url: `/api/droplets/${encodeURIComponent(options.droplet)}/widget_package/git_credential`
2201
+ }) : await fluidOsV0WidgetPackageGitCredentialCreate({
2202
+ client: options.client,
2203
+ responseStyle: "data"
2204
+ }));
1108
2205
  const remoteUrl = response?.git_credential?.remote_url;
1109
2206
  if (typeof remoteUrl !== "string" || remoteUrl.length === 0) throw new Error("The git credential response did not include a remote URL. Update the Fluid CLI, or contact support if this persists.");
1110
2207
  const expiresAtRaw = response.git_credential?.expires_at;
@@ -1235,138 +2332,6 @@ const cloneCommand = new Command("clone").description("Clone a widget package's
1235
2332
  console.log(chalk.dim(` cd ${path.relative(process.cwd(), targetDir) || "."}`));
1236
2333
  if (options.skipInstall === true) console.log(chalk.dim(` ${installCommand}`));
1237
2334
  }));
1238
- //#endregion
1239
- //#region src/utils/template.ts
1240
- const EMBEDDED_PORTAL_SDK_VERSION = "0.1.461";
1241
- const EMBEDDED_CLI_CORE_VERSION = "0.1.14";
1242
- const EMBEDDED_CLI_WIDGET_VERSION = "0.1.27";
1243
- function getDefaultTemplatePath() {
1244
- return path.join(findPackageRoot(), "templates", "default");
1245
- }
1246
- async function directoryExists(filePath) {
1247
- try {
1248
- return (await stat(filePath)).isDirectory();
1249
- } catch {
1250
- return false;
1251
- }
1252
- }
1253
- async function copyWidgetTemplate(templatePath, targetPath, variables) {
1254
- const files = await getFiles(templatePath);
1255
- for (const file of files) {
1256
- const sourcePath = path.join(templatePath, file);
1257
- const isTemplate = file.endsWith(".template");
1258
- const outputFiles = getOutputFilenames(file);
1259
- const content = await readFile(sourcePath, "utf-8");
1260
- await writeOutputFiles(targetPath, outputFiles, isTemplate ? renderTemplate(content, variables) : content);
1261
- }
1262
- for (const skillFile of await getSharedTemplateSkillFiles()) await writeOutputFiles(targetPath, getOutputFilenames(skillFile.relativePath), skillFile.content);
1263
- }
1264
- async function getPortalSdkVersion(options = {}) {
1265
- return getWorkspacePackageVersion(["portal", "sdk"], EMBEDDED_PORTAL_SDK_VERSION, options);
1266
- }
1267
- async function getPortalCoreVersion(options = {}) {
1268
- return getWorkspacePackageVersion(["portal", "core"], void 0, options);
1269
- }
1270
- async function getCliVersion(options = {}) {
1271
- return getWorkspacePackageVersion(["cli", "core"], EMBEDDED_CLI_CORE_VERSION, options);
1272
- }
1273
- async function getWidgetCliVersion(options = {}) {
1274
- return getWorkspacePackageVersion(["cli", "widget"], EMBEDDED_CLI_WIDGET_VERSION, options);
1275
- }
1276
- async function getWorkspacePackageVersion(packagePathSegments, embeddedVersion, options) {
1277
- try {
1278
- const packageRoot = findPackageRoot();
1279
- const packagesRoot = path.join(packageRoot, "..", "..");
1280
- const targetPackageRoot = path.join(packagesRoot, ...packagePathSegments);
1281
- if (options.local) return `link:${(options.targetPath ? path.relative(options.targetPath, targetPackageRoot) : targetPackageRoot).replace(/\\/g, "/") || "."}`;
1282
- if (embeddedVersion) return embeddedVersion;
1283
- const packageJsonPath = path.join(targetPackageRoot, "package.json");
1284
- const content = await readFile(packageJsonPath, "utf-8");
1285
- const pkg = JSON.parse(content);
1286
- if (typeof pkg.version !== "string") throw new Error(`Missing version in ${packageJsonPath}`);
1287
- return pkg.version;
1288
- } catch (error) {
1289
- if (embeddedVersion) return embeddedVersion;
1290
- throw new Error(`Could not resolve the exact template package version for ${packagePathSegments.join("/")}`, { cause: error });
1291
- }
1292
- }
1293
- async function getFiles(dir, baseDir = dir) {
1294
- const entries = await readdir(dir, { withFileTypes: true });
1295
- const files = [];
1296
- for (const entry of entries) {
1297
- const fullPath = path.join(dir, entry.name);
1298
- if (entry.isDirectory()) files.push(...await getFiles(fullPath, baseDir));
1299
- else files.push(path.relative(baseDir, fullPath));
1300
- }
1301
- return files;
1302
- }
1303
- async function getSharedTemplateSkillFiles() {
1304
- const packageRoot = findFluidCliPackageRoot();
1305
- const skillsRoot = path.join(packageRoot, "template-skills");
1306
- const files = await getFiles(skillsRoot);
1307
- return Promise.all(files.map(async (file) => ({
1308
- relativePath: path.join("skills", file),
1309
- content: await readFile(path.join(skillsRoot, file), "utf-8")
1310
- })));
1311
- }
1312
- function findFluidCliPackageRoot() {
1313
- const workspacePackageRoot = path.join(findPackageRoot(), "..", "core");
1314
- if (existsSync(path.join(workspacePackageRoot, "template-skills"))) return workspacePackageRoot;
1315
- let dir = path.dirname(fileURLToPath(import.meta.resolve("@fluid-app/fluid-cli")));
1316
- while (!existsSync(path.join(dir, "package.json"))) {
1317
- const parent = path.dirname(dir);
1318
- if (parent === dir) throw new Error("Could not find Fluid CLI package root");
1319
- dir = parent;
1320
- }
1321
- return dir;
1322
- }
1323
- function renderTemplate(content, variables) {
1324
- return content.replace(/{{\s*#if\s+([a-zA-Z0-9_]+)\s*}}([\s\S]*?){{\s*\/if\s*}}/g, (match, key, block) => {
1325
- if (!isTemplateVariableKey(key)) return match;
1326
- return variables[key] ? String(block) : "";
1327
- }).replace(/{{\s*([a-zA-Z0-9_]+)\s*}}/g, (match, key) => {
1328
- if (isTemplateVariableKey(key)) return variables[key] ?? "";
1329
- return match;
1330
- });
1331
- }
1332
- function isTemplateVariableKey(key) {
1333
- return [
1334
- "projectName",
1335
- "widgetScope",
1336
- "widgetPackageVersion",
1337
- "sdkVersion",
1338
- "localCoreVersion",
1339
- "fluidCliVersion",
1340
- "widgetCliVersion",
1341
- "droplet"
1342
- ].includes(key);
1343
- }
1344
- async function writeOutputFiles(targetPath, outputFiles, content) {
1345
- for (const outputFile of outputFiles) {
1346
- const destinationPath = path.join(targetPath, outputFile);
1347
- await mkdir(path.dirname(destinationPath), { recursive: true });
1348
- await writeFile(destinationPath, content, "utf-8");
1349
- }
1350
- }
1351
- function getOutputFilenames(filename) {
1352
- const outputFilename = getOutputFilename(filename);
1353
- const normalized = outputFilename.replace(/\\/g, "/");
1354
- if (normalized === "AGENTS.md") return ["AGENTS.md", "CLAUDE.md"];
1355
- if (normalized.startsWith("skills/")) return [path.join(".agents", outputFilename), path.join(".claude", outputFilename)];
1356
- return [outputFilename];
1357
- }
1358
- function getOutputFilename(filename) {
1359
- return filename.endsWith(".template") ? filename.slice(0, -9) : filename;
1360
- }
1361
- function findPackageRoot() {
1362
- let dir = path.dirname(fileURLToPath(import.meta.url));
1363
- while (!existsSync(path.join(dir, "package.json"))) {
1364
- const parent = path.dirname(dir);
1365
- if (parent === dir) throw new Error("Could not find widget CLI package root");
1366
- dir = parent;
1367
- }
1368
- return dir;
1369
- }
1370
2335
  /**
1371
2336
  * The template repo to clone. Overridable via `FLUID_WIDGET_TEMPLATE_REPO`
1372
2337
  * (read at call time so tests can point at a local repo). Falls back to the
@@ -1697,11 +2662,6 @@ function isRecord$2(value) {
1697
2662
  return typeof value === "object" && value !== null && !Array.isArray(value);
1698
2663
  }
1699
2664
  //#endregion
1700
- //#region src/utils/auth-client.ts
1701
- function createAuthenticatedClient() {
1702
- return { client: createAuthenticatedWidgetPackageClient() };
1703
- }
1704
- //#endregion
1705
2665
  //#region src/utils/droplets.ts
1706
2666
  const MAX_DROPLET_PAGES = 100;
1707
2667
  async function promptForDroplet(client) {
@@ -1720,10 +2680,18 @@ async function promptForDroplet(client) {
1720
2680
  }
1721
2681
  async function listDroplets(client) {
1722
2682
  try {
1723
- const firstResponse = await client.get("/api/droplets", { page: 1 });
2683
+ const firstResponse = await client.get({
2684
+ query: { page: 1 },
2685
+ responseStyle: "data",
2686
+ url: "/api/droplets"
2687
+ });
1724
2688
  const totalPages = Math.min(readTotalPages(firstResponse) ?? 1, MAX_DROPLET_PAGES);
1725
2689
  const responses = [firstResponse];
1726
- for (let page = 2; page <= totalPages; page += 1) responses.push(await client.get("/api/droplets", { page }));
2690
+ for (let page = 2; page <= totalPages; page += 1) responses.push(await client.get({
2691
+ query: { page },
2692
+ responseStyle: "data",
2693
+ url: "/api/droplets"
2694
+ }));
1727
2695
  return responses.flatMap(normalizeDropletsResponse);
1728
2696
  } catch (err) {
1729
2697
  if (isApiLikeError(err)) {
@@ -1820,10 +2788,17 @@ const linkCommand = new Command("link").description("Link this widget project to
1820
2788
  //#endregion
1821
2789
  //#region src/utils/droplet-api.ts
1822
2790
  async function fetchDropletWidgetStatus(options) {
1823
- return extractArray(await options.client.get(`/api/droplets/${encodeURIComponent(options.droplet)}/widget_package_versions`)).map(normalizeVersionSummary);
2791
+ return extractArray(await options.client.get({
2792
+ responseStyle: "data",
2793
+ url: `/api/droplets/${encodeURIComponent(options.droplet)}/widget_package_versions`
2794
+ })).map(normalizeVersionSummary);
1824
2795
  }
1825
2796
  async function fetchDropletWidgetLogs(options) {
1826
- return extractArray(await options.client.get(`/api/droplets/${encodeURIComponent(options.droplet)}/widget_package_versions/logs`, options.limit ? { limit: options.limit } : void 0)).map(normalizeLogEntry);
2797
+ return extractArray(await options.client.get({
2798
+ query: options.limit ? { limit: options.limit } : void 0,
2799
+ responseStyle: "data",
2800
+ url: `/api/droplets/${encodeURIComponent(options.droplet)}/widget_package_versions/logs`
2801
+ })).map(normalizeLogEntry);
1827
2802
  }
1828
2803
  function formatStatusRows(versions) {
1829
2804
  if (versions.length === 0) return "No widget package versions found.";
@@ -1955,29 +2930,7 @@ function parseLimit(value) {
1955
2930
  }
1956
2931
  //#endregion
1957
2932
  //#region src/commands/publish.ts
1958
- async function publishWidgetPackage(options, dependencies) {
1959
- if (dependencies) {
1960
- await dependencies.buildWidgetPackage({
1961
- projectDir: options.projectDir,
1962
- outDir: options.outDir
1963
- });
1964
- const dryRun = options.dryRun === true;
1965
- if (dryRun) return {
1966
- dryRun,
1967
- uploaded: false,
1968
- outDir: options.outDir
1969
- };
1970
- await dependencies.uploadWidgetPackage({
1971
- outDir: options.outDir,
1972
- droplet: options.droplet,
1973
- client: options.client
1974
- });
1975
- return {
1976
- dryRun,
1977
- uploaded: true,
1978
- outDir: options.outDir
1979
- };
1980
- }
2933
+ async function publishWidgetPackage(options) {
1981
2934
  const droplet = await resolvePublishDroplet(options.projectDir, options.droplet);
1982
2935
  await validateDropletWidgetProject(options.projectDir, droplet);
1983
2936
  if (options.skipTypecheck !== true) await runPackageScriptIfAvailable({
@@ -1985,16 +2938,11 @@ async function publishWidgetPackage(options, dependencies) {
1985
2938
  scriptName: "typecheck"
1986
2939
  });
1987
2940
  const dryRun = options.dryRun === true;
1988
- await withRootWidgetConfigBridge(options.projectDir, () => publishWidgetRuntimeArtifacts({
2941
+ await withRootWidgetConfigBridge(options.projectDir, () => publishDropletWidgetPackageArtifacts({
1989
2942
  projectDir: options.projectDir,
1990
2943
  outDir: options.outDir,
1991
- buildOwner: "droplet",
1992
- uploadOwner: {
1993
- kind: "droplet",
1994
- uuid: droplet
1995
- },
1996
- dryRun,
1997
- ...dryRun ? {} : { client: createAuthenticatedWidgetPackageClient() }
2944
+ droplet,
2945
+ dryRun
1998
2946
  }), droplet);
1999
2947
  return {
2000
2948
  dryRun,
@@ -2029,16 +2977,11 @@ const publishCommand = new Command("publish").description("Publish a Fluid widge
2029
2977
  else typecheckSpinner.info("No typecheck script found; skipping");
2030
2978
  }
2031
2979
  const publishSpinner = ora(dryRun ? "Building and preparing dry-run upload payload..." : "Building and publishing widget artifacts...").start();
2032
- const result = await withRootWidgetConfigBridge(projectDir, () => publishWidgetRuntimeArtifacts({
2980
+ const result = await withRootWidgetConfigBridge(projectDir, () => publishDropletWidgetPackageArtifacts({
2033
2981
  projectDir,
2034
2982
  outDir,
2035
- buildOwner: "droplet",
2036
- uploadOwner: {
2037
- kind: "droplet",
2038
- uuid: droplet
2039
- },
2040
- dryRun,
2041
- ...dryRun ? {} : { client: createAuthenticatedWidgetPackageClient() }
2983
+ droplet,
2984
+ dryRun
2042
2985
  }), droplet);
2043
2986
  publishSpinner.succeed(dryRun ? "Prepared dry-run upload payload" : "Published widget package");
2044
2987
  if (dryRun) {
@@ -2064,6 +3007,47 @@ async function resolvePublishDroplet(projectDir, override) {
2064
3007
  }
2065
3008
  throw new Error("Missing droplet UUID. Pass --droplet <uuid> or set droplet in fluid.widget.config.ts.");
2066
3009
  }
3010
+ async function publishDropletWidgetPackageArtifacts(options) {
3011
+ const common = {
3012
+ projectDir: options.projectDir,
3013
+ outDir: options.outDir,
3014
+ owner: "droplet",
3015
+ uploadOwner: {
3016
+ kind: "droplet",
3017
+ uuid: options.droplet
3018
+ },
3019
+ cliVersion: await getWidgetCliVersion()
3020
+ };
3021
+ if (options.dryRun) return publishWidgetPackage$1({
3022
+ ...common,
3023
+ dryRun: true
3024
+ });
3025
+ return publishWidgetPackage$1({
3026
+ ...common,
3027
+ dryRun: false,
3028
+ client: createAuthenticatedWidgetPackageClient()
3029
+ });
3030
+ }
3031
+ function printRuntimeOnlyNotice() {
3032
+ console.log(chalk.yellow("This publishes widget runtime artifacts only; it does not deploy the hosted portal shell."));
3033
+ console.log();
3034
+ }
3035
+ function printDryRunSessionPayload(result) {
3036
+ console.log(chalk.bold("Dry-run upload session payload:"));
3037
+ console.log(JSON.stringify(result.publish.sessionPayload, null, 2));
3038
+ console.log();
3039
+ }
3040
+ function printWidgetPublishSummary(options) {
3041
+ console.log();
3042
+ console.log(chalk.green.bold(`${options.title} complete`));
3043
+ console.log();
3044
+ console.log(chalk.gray("Owner: ") + chalk.white(options.ownerLabel));
3045
+ console.log(chalk.gray("Package: ") + chalk.white(options.result.build.packageId));
3046
+ console.log(chalk.gray("Version: ") + chalk.white(options.result.build.version));
3047
+ console.log(chalk.gray("Output: ") + chalk.cyan(options.outDir));
3048
+ console.log(chalk.gray("Artifacts: ") + chalk.white(String(options.result.request.artifacts.length)));
3049
+ console.log();
3050
+ }
2067
3051
  //#endregion
2068
3052
  //#region src/utils/me.ts
2069
3053
  function str(value) {
@@ -2071,7 +3055,10 @@ function str(value) {
2071
3055
  return null;
2072
3056
  }
2073
3057
  async function fetchCurrentUser(client) {
2074
- const raw = await client.get("/api/me");
3058
+ const raw = await client.get({
3059
+ responseStyle: "data",
3060
+ url: "/api/me"
3061
+ });
2075
3062
  const composed = [str(raw.first_name), str(raw.last_name)].filter((part) => part !== null).join(" ").trim();
2076
3063
  const idValue = str(raw.public_id) ?? (typeof raw.id === "number" && Number.isFinite(raw.id) ? String(raw.id) : null);
2077
3064
  return {
@@ -2095,6 +3082,7 @@ async function syncWidgetGitRepo(options) {
2095
3082
  const { cwd, client } = options;
2096
3083
  const branch = options.branch ?? "main";
2097
3084
  if (!await isGitRepository(cwd)) await gitInit(cwd, branch);
3085
+ await ensureLocalStateExcluded(cwd);
2098
3086
  const cred = await mintWidgetGitCredential({
2099
3087
  client,
2100
3088
  droplet: options.droplet
@@ -2121,6 +3109,7 @@ async function syncWidgetGitRepo(options) {
2121
3109
  }
2122
3110
  } else pulled = await gitAdoptFetchHead(cwd);
2123
3111
  }
3112
+ await purgeLocalStateFromIndex(cwd, "Remove private Mist Desktop state from the widget repository", await resolveCommitIdentity(client));
2124
3113
  let committed = false;
2125
3114
  if (options.commit !== false && await hasUncommittedChanges(cwd)) {
2126
3115
  await gitAutoCommit(cwd, await buildCommitMessage(client, options.message), await resolveCommitIdentity(client));