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

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,15 +1,243 @@
1
+ import { a as WidgetToolingError, n as loadAndValidateWidgetPackage, r as publishWidgetPackage$1, t as buildWidgetPackage$1 } from "./src-CVadzcYM.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";
3
8
  import chalk from "chalk";
4
9
  import ora from "ora";
5
- import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
11
  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";
15
+ //#region ../../platform/api-client-core/src/fetch-client.ts
16
+ /**
17
+ * API Error class compatible with fluid-admin's ApiError
18
+ */
19
+ var ApiError = class ApiError extends Error {
20
+ status;
21
+ data;
22
+ requestId;
23
+ constructor(message, status, data, requestId) {
24
+ super(message);
25
+ this.name = "ApiError";
26
+ this.status = status;
27
+ this.data = data;
28
+ this.requestId = requestId;
29
+ if ("captureStackTrace" in Error) Error.captureStackTrace(this, ApiError);
30
+ }
31
+ toJSON() {
32
+ return {
33
+ name: this.name,
34
+ message: this.message,
35
+ status: this.status,
36
+ data: this.data,
37
+ requestId: this.requestId
38
+ };
39
+ }
40
+ };
41
+ function getStringRequestId(value) {
42
+ if (typeof value !== "string") return;
43
+ const trimmed = value.trim();
44
+ return trimmed.length > 0 ? trimmed : void 0;
45
+ }
46
+ function getRequestIdFromHeaders(headers) {
47
+ return getStringRequestId(headers.get("x-request-id")) ?? getStringRequestId(headers.get("request-id")) ?? getStringRequestId(headers.get("X-Request-ID"));
48
+ }
49
+ function getRequestIdFromJsonBody(body) {
50
+ if (!body || typeof body !== "object" || Array.isArray(body)) return;
51
+ const record = body;
52
+ const meta = record.meta;
53
+ return getStringRequestId(record.request_id) ?? getStringRequestId(record.requestId) ?? (meta && typeof meta === "object" && !Array.isArray(meta) ? getStringRequestId(meta.request_id) ?? getStringRequestId(meta.requestId) : void 0);
54
+ }
55
+ /**
56
+ * Creates a configured fetch client instance
57
+ */
58
+ function createFetchClient(config) {
59
+ const { baseUrl, getAuthToken, onAuthError, defaultHeaders = {}, credentials, cache, networkRetry, throwOnInvalidJson = false } = config;
60
+ const maxNetworkRetries = Math.max(0, networkRetry?.maxRetries ?? 0);
61
+ const baseNetworkRetryDelayMs = Math.max(0, networkRetry?.baseDelayMs ?? 0);
62
+ /**
63
+ * Build headers for a request
64
+ */
65
+ async function buildHeaders(customHeaders) {
66
+ const headers = {
67
+ Accept: "application/json",
68
+ "Content-Type": "application/json",
69
+ ...defaultHeaders,
70
+ ...customHeaders
71
+ };
72
+ if (getAuthToken) {
73
+ const token = await getAuthToken();
74
+ if (token) headers.Authorization = `Bearer ${token}`;
75
+ }
76
+ return headers;
77
+ }
78
+ /**
79
+ * Join baseUrl + endpoint via string concatenation (matches fetchApi).
80
+ * Using `new URL(endpoint, baseUrl)` would strip any path prefix from
81
+ * baseUrl (e.g. "/api") when the endpoint starts with "/".
82
+ */
83
+ function joinUrl(endpoint) {
84
+ return `${baseUrl}${endpoint}`;
85
+ }
86
+ /**
87
+ * Build URL with query parameters for GET requests
88
+ * Compatible with fluid-admin's query param handling
89
+ */
90
+ function buildUrl(endpoint, params) {
91
+ const fullUrl = joinUrl(endpoint);
92
+ if (!params || Object.keys(params).length === 0) return fullUrl;
93
+ const queryString = new URLSearchParams();
94
+ Object.entries(params).forEach(([key, value]) => {
95
+ if (value === void 0 || value === null) return;
96
+ if (Array.isArray(value)) value.forEach((item) => queryString.append(`${key}[]`, String(item)));
97
+ else if (typeof value === "object") Object.entries(value).forEach(([subKey, subValue]) => {
98
+ if (subValue === void 0 || subValue === null) return;
99
+ if (Array.isArray(subValue)) subValue.forEach((item) => queryString.append(`${key}[${subKey}][]`, String(item)));
100
+ else queryString.append(`${key}[${subKey}]`, String(subValue));
101
+ });
102
+ else queryString.append(key, String(value));
103
+ });
104
+ const qs = queryString.toString();
105
+ return qs ? `${fullUrl}?${qs}` : fullUrl;
106
+ }
107
+ /**
108
+ * Shared response handler for both JSON and FormData requests.
109
+ * Handles auth errors, non-OK responses, 204 No Content, and JSON parsing.
110
+ */
111
+ async function handleResponse(response, method, _url) {
112
+ const headerRequestId = getRequestIdFromHeaders(response.headers);
113
+ if (response.status === 401 && onAuthError) onAuthError();
114
+ if (!response.ok) {
115
+ const errorText = await response.text().catch(() => "");
116
+ if (response.headers.get("content-type")?.includes("application/json")) {
117
+ let data;
118
+ try {
119
+ data = JSON.parse(errorText);
120
+ } catch {
121
+ throw new ApiError(errorText.slice(0, 200) || `${method} request failed with status ${response.status}`, response.status, null, headerRequestId);
122
+ }
123
+ const nestedError = typeof data.error === "object" && data.error !== null ? data.error.message : void 0;
124
+ const directError = typeof data.error === "string" ? data.error : void 0;
125
+ const message = typeof data.message === "string" ? data.message : void 0;
126
+ const errorMessage = typeof data.error_message === "string" ? data.error_message : void 0;
127
+ throw new ApiError(message || errorMessage || directError || (typeof nestedError === "string" ? nestedError : void 0) || `${method} request failed with status ${response.status}`, response.status, data.errors || data, headerRequestId ?? getRequestIdFromJsonBody(data));
128
+ } else throw new ApiError(`${method} request failed with status ${response.status}`, response.status, null, headerRequestId);
129
+ }
130
+ if (response.status === 204 || response.headers.get("content-length") === "0") return null;
131
+ if (response.headers.get("content-type")?.includes("application/json")) {
132
+ const responseText = await response.text();
133
+ try {
134
+ return JSON.parse(responseText);
135
+ } catch {
136
+ if (throwOnInvalidJson) throw new ApiError("Failed to parse response as JSON", response.status, null, headerRequestId);
137
+ return responseText ? responseText : null;
138
+ }
139
+ }
140
+ return null;
141
+ }
142
+ function getNetworkRetryDelayMs(retryAttempt) {
143
+ return baseNetworkRetryDelayMs * 2 ** (retryAttempt - 1);
144
+ }
145
+ async function waitForNetworkRetry(retryAttempt) {
146
+ const delayMs = getNetworkRetryDelayMs(retryAttempt);
147
+ if (delayMs <= 0) return;
148
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
149
+ }
150
+ async function fetchWithNetworkRetry(url, fetchOptions, signal) {
151
+ let retryCount = 0;
152
+ while (true) try {
153
+ return await fetch(url, fetchOptions);
154
+ } catch (networkError) {
155
+ if (signal?.aborted || retryCount >= maxNetworkRetries) throw networkError;
156
+ retryCount += 1;
157
+ await waitForNetworkRetry(retryCount);
158
+ if (signal?.aborted) throw networkError;
159
+ }
160
+ }
161
+ /**
162
+ * Main request function
163
+ */
164
+ async function request(endpoint, options = {}) {
165
+ const { method = "GET", headers: customHeaders, params, body, signal, priority } = options;
166
+ const url = params ? buildUrl(endpoint, params) : joinUrl(endpoint);
167
+ const headers = await buildHeaders(customHeaders);
168
+ let response;
169
+ try {
170
+ const fetchOptions = {
171
+ method,
172
+ headers
173
+ };
174
+ if (credentials) fetchOptions.credentials = credentials;
175
+ if (cache) fetchOptions.cache = cache;
176
+ if (priority) fetchOptions.priority = priority;
177
+ const serializedBody = body && method !== "GET" ? JSON.stringify(body) : null;
178
+ if (serializedBody) fetchOptions.body = serializedBody;
179
+ if (signal) fetchOptions.signal = signal;
180
+ response = await fetchWithNetworkRetry(url, fetchOptions, signal);
181
+ } catch (networkError) {
182
+ throw new ApiError(`Network error: ${networkError instanceof Error ? networkError.message : "Unknown network error"}`, 0, null);
183
+ }
184
+ return handleResponse(response, method, url);
185
+ }
186
+ /**
187
+ * Request with FormData (for file uploads)
188
+ */
189
+ async function requestWithFormData(endpoint, formData, options = {}) {
190
+ const { method = "POST", headers: customHeaders, signal, priority } = options;
191
+ const url = joinUrl(endpoint);
192
+ const headers = await buildHeaders(customHeaders);
193
+ delete headers["Content-Type"];
194
+ let response;
195
+ try {
196
+ const fetchOptions = {
197
+ method,
198
+ headers,
199
+ body: formData
200
+ };
201
+ if (credentials) fetchOptions.credentials = credentials;
202
+ if (cache) fetchOptions.cache = cache;
203
+ if (priority) fetchOptions.priority = priority;
204
+ if (signal) fetchOptions.signal = signal;
205
+ response = await fetchWithNetworkRetry(url, fetchOptions, signal);
206
+ } catch (networkError) {
207
+ throw new ApiError(`Network error: ${networkError instanceof Error ? networkError.message : "Unknown network error"}`, 0, null);
208
+ }
209
+ return handleResponse(response, method, url);
210
+ }
211
+ return {
212
+ request,
213
+ requestWithFormData,
214
+ get: (endpoint, params, options) => request(endpoint, {
215
+ ...options,
216
+ method: "GET",
217
+ ...params && { params }
218
+ }),
219
+ post: (endpoint, body, options) => request(endpoint, {
220
+ ...options,
221
+ method: "POST",
222
+ body
223
+ }),
224
+ put: (endpoint, body, options) => request(endpoint, {
225
+ ...options,
226
+ method: "PUT",
227
+ body
228
+ }),
229
+ patch: (endpoint, body, options) => request(endpoint, {
230
+ ...options,
231
+ method: "PATCH",
232
+ body
233
+ }),
234
+ delete: (endpoint, options) => request(endpoint, {
235
+ ...options,
236
+ method: "DELETE"
237
+ })
238
+ };
239
+ }
240
+ //#endregion
13
241
  //#region src/utils/config.ts
14
242
  const WIDGET_CONFIG_FILE = "fluid.widget.config.ts";
15
243
  var WidgetConfigParseError = class extends Error {
@@ -317,22 +545,27 @@ async function validateDropletWidgetProject(projectDir, packageKeyOverride) {
317
545
  }]);
318
546
  const configErrors = await validateRootWidgetConfig(resolvedProjectDir, packageKeyOverride);
319
547
  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);
548
+ let validated;
549
+ try {
550
+ validated = (await withRootWidgetConfigBridge(resolvedProjectDir, () => loadAndValidateWidgetPackage({
551
+ projectDir: resolvedProjectDir,
552
+ owner: "droplet"
553
+ }), packageKeyOverride)).validated;
554
+ } catch (error) {
555
+ if (error instanceof WidgetToolingError) {
556
+ if (error.diagnostics) throw new WidgetValidationError(error.diagnostics);
557
+ throw new WidgetValidationError([{
558
+ code: "INVALID_SOURCE_PACKAGE",
559
+ path: sourceConfig.relativePath,
560
+ message: error.details ? `${error.message}\n${error.details}` : error.message
561
+ }]);
562
+ }
563
+ throw error;
564
+ }
332
565
  return {
333
566
  projectDir: resolvedProjectDir,
334
567
  sourceConfigPath: sourceConfig.relativePath,
335
- validated: validation.value
568
+ validated
336
569
  };
337
570
  }
338
571
  async function withRootWidgetConfigBridge(projectDir, callback, packageKeyOverride) {
@@ -411,29 +644,6 @@ async function validateRootWidgetConfig(projectDir, packageKeyOverride) {
411
644
  }];
412
645
  }
413
646
  }
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
647
  function resolvePortalSourceConfig(projectDir) {
438
648
  for (const relativePath of SOURCE_CONFIG_CANDIDATES) {
439
649
  const candidate = path.join(projectDir, relativePath);
@@ -443,50 +653,6 @@ function resolvePortalSourceConfig(projectDir) {
443
653
  };
444
654
  }
445
655
  }
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
656
  function isNodeError(error) {
491
657
  return error instanceof Error && "code" in error;
492
658
  }
@@ -633,6 +799,138 @@ function isRecord$3(value) {
633
799
  return typeof value === "object" && value !== null && !Array.isArray(value);
634
800
  }
635
801
  //#endregion
802
+ //#region src/utils/template.ts
803
+ const EMBEDDED_PORTAL_SDK_VERSION = "0.1.462";
804
+ const EMBEDDED_CLI_CORE_VERSION = "0.1.14";
805
+ const EMBEDDED_CLI_WIDGET_VERSION = "0.1.28";
806
+ function getDefaultTemplatePath() {
807
+ return path.join(findPackageRoot(), "templates", "default");
808
+ }
809
+ async function directoryExists(filePath) {
810
+ try {
811
+ return (await stat(filePath)).isDirectory();
812
+ } catch {
813
+ return false;
814
+ }
815
+ }
816
+ async function copyWidgetTemplate(templatePath, targetPath, variables) {
817
+ const files = await getFiles(templatePath);
818
+ for (const file of files) {
819
+ const sourcePath = path.join(templatePath, file);
820
+ const isTemplate = file.endsWith(".template");
821
+ const outputFiles = getOutputFilenames(file);
822
+ const content = await readFile(sourcePath, "utf-8");
823
+ await writeOutputFiles(targetPath, outputFiles, isTemplate ? renderTemplate(content, variables) : content);
824
+ }
825
+ for (const skillFile of await getSharedTemplateSkillFiles()) await writeOutputFiles(targetPath, getOutputFilenames(skillFile.relativePath), skillFile.content);
826
+ }
827
+ async function getPortalSdkVersion(options = {}) {
828
+ return getWorkspacePackageVersion(["portal", "sdk"], EMBEDDED_PORTAL_SDK_VERSION, options);
829
+ }
830
+ async function getPortalCoreVersion(options = {}) {
831
+ return getWorkspacePackageVersion(["portal", "core"], void 0, options);
832
+ }
833
+ async function getCliVersion(options = {}) {
834
+ return getWorkspacePackageVersion(["cli", "core"], EMBEDDED_CLI_CORE_VERSION, options);
835
+ }
836
+ async function getWidgetCliVersion(options = {}) {
837
+ return getWorkspacePackageVersion(["cli", "widget"], EMBEDDED_CLI_WIDGET_VERSION, options);
838
+ }
839
+ async function getWorkspacePackageVersion(packagePathSegments, embeddedVersion, options) {
840
+ try {
841
+ const packageRoot = findPackageRoot();
842
+ const packagesRoot = path.join(packageRoot, "..", "..");
843
+ const targetPackageRoot = path.join(packagesRoot, ...packagePathSegments);
844
+ if (options.local) return `link:${(options.targetPath ? path.relative(options.targetPath, targetPackageRoot) : targetPackageRoot).replace(/\\/g, "/") || "."}`;
845
+ if (embeddedVersion) return embeddedVersion;
846
+ const packageJsonPath = path.join(targetPackageRoot, "package.json");
847
+ const content = await readFile(packageJsonPath, "utf-8");
848
+ const pkg = JSON.parse(content);
849
+ if (typeof pkg.version !== "string") throw new Error(`Missing version in ${packageJsonPath}`);
850
+ return pkg.version;
851
+ } catch (error) {
852
+ if (embeddedVersion) return embeddedVersion;
853
+ throw new Error(`Could not resolve the exact template package version for ${packagePathSegments.join("/")}`, { cause: error });
854
+ }
855
+ }
856
+ async function getFiles(dir, baseDir = dir) {
857
+ const entries = await readdir(dir, { withFileTypes: true });
858
+ const files = [];
859
+ for (const entry of entries) {
860
+ const fullPath = path.join(dir, entry.name);
861
+ if (entry.isDirectory()) files.push(...await getFiles(fullPath, baseDir));
862
+ else files.push(path.relative(baseDir, fullPath));
863
+ }
864
+ return files;
865
+ }
866
+ async function getSharedTemplateSkillFiles() {
867
+ const packageRoot = findFluidCliPackageRoot();
868
+ const skillsRoot = path.join(packageRoot, "template-skills");
869
+ const files = await getFiles(skillsRoot);
870
+ return Promise.all(files.map(async (file) => ({
871
+ relativePath: path.join("skills", file),
872
+ content: await readFile(path.join(skillsRoot, file), "utf-8")
873
+ })));
874
+ }
875
+ function findFluidCliPackageRoot() {
876
+ const workspacePackageRoot = path.join(findPackageRoot(), "..", "core");
877
+ if (existsSync(path.join(workspacePackageRoot, "template-skills"))) return workspacePackageRoot;
878
+ let dir = path.dirname(fileURLToPath(import.meta.resolve("@fluid-app/fluid-cli")));
879
+ while (!existsSync(path.join(dir, "package.json"))) {
880
+ const parent = path.dirname(dir);
881
+ if (parent === dir) throw new Error("Could not find Fluid CLI package root");
882
+ dir = parent;
883
+ }
884
+ return dir;
885
+ }
886
+ function renderTemplate(content, variables) {
887
+ return content.replace(/{{\s*#if\s+([a-zA-Z0-9_]+)\s*}}([\s\S]*?){{\s*\/if\s*}}/g, (match, key, block) => {
888
+ if (!isTemplateVariableKey(key)) return match;
889
+ return variables[key] ? String(block) : "";
890
+ }).replace(/{{\s*([a-zA-Z0-9_]+)\s*}}/g, (match, key) => {
891
+ if (isTemplateVariableKey(key)) return variables[key] ?? "";
892
+ return match;
893
+ });
894
+ }
895
+ function isTemplateVariableKey(key) {
896
+ return [
897
+ "projectName",
898
+ "widgetScope",
899
+ "widgetPackageVersion",
900
+ "sdkVersion",
901
+ "localCoreVersion",
902
+ "fluidCliVersion",
903
+ "widgetCliVersion",
904
+ "droplet"
905
+ ].includes(key);
906
+ }
907
+ async function writeOutputFiles(targetPath, outputFiles, content) {
908
+ for (const outputFile of outputFiles) {
909
+ const destinationPath = path.join(targetPath, outputFile);
910
+ await mkdir(path.dirname(destinationPath), { recursive: true });
911
+ await writeFile(destinationPath, content, "utf-8");
912
+ }
913
+ }
914
+ function getOutputFilenames(filename) {
915
+ const outputFilename = getOutputFilename(filename);
916
+ const normalized = outputFilename.replace(/\\/g, "/");
917
+ if (normalized === "AGENTS.md") return ["AGENTS.md", "CLAUDE.md"];
918
+ if (normalized.startsWith("skills/")) return [path.join(".agents", outputFilename), path.join(".claude", outputFilename)];
919
+ return [outputFilename];
920
+ }
921
+ function getOutputFilename(filename) {
922
+ return filename.endsWith(".template") ? filename.slice(0, -9) : filename;
923
+ }
924
+ function findPackageRoot() {
925
+ let dir = path.dirname(fileURLToPath(import.meta.url));
926
+ while (!existsSync(path.join(dir, "package.json"))) {
927
+ const parent = path.dirname(dir);
928
+ if (parent === dir) throw new Error("Could not find widget CLI package root");
929
+ dir = parent;
930
+ }
931
+ return dir;
932
+ }
933
+ //#endregion
636
934
  //#region src/commands/build.ts
637
935
  async function buildWidgetPackage(options) {
638
936
  await validateDropletWidgetProject(options.projectDir);
@@ -640,20 +938,23 @@ async function buildWidgetPackage(options) {
640
938
  projectDir: options.projectDir,
641
939
  scriptName: "typecheck"
642
940
  });
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}`);
941
+ let buildResult;
942
+ try {
943
+ const cliVersion = await getWidgetCliVersion();
944
+ buildResult = await withRootWidgetConfigBridge(options.projectDir, () => buildWidgetPackage$1({
945
+ projectDir: options.projectDir,
946
+ outDir: options.outDir,
947
+ owner: "droplet",
948
+ cliVersion
949
+ }));
950
+ } catch (error) {
951
+ if (error instanceof WidgetToolingError && error.details) throw new Error(`${error.message}\n${error.details}`, { cause: error });
952
+ throw error;
652
953
  }
653
954
  return {
654
- outDir: buildResult.value.publishDir,
655
- packageId: buildResult.value.packageId,
656
- version: buildResult.value.version
955
+ outDir: buildResult.publishDir,
956
+ packageId: buildResult.packageId,
957
+ version: buildResult.version
657
958
  };
658
959
  }
659
960
  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 +976,42 @@ const buildCommand = new Command("build").description("Validate and build the cu
675
976
  });
676
977
  });
677
978
  //#endregion
979
+ //#region src/utils/auth-client.ts
980
+ function createAuthenticatedClient() {
981
+ return { client: createAuthenticatedWidgetPackageClient() };
982
+ }
983
+ function createAuthenticatedWidgetPackageClient() {
984
+ const { profile, profileName, token } = resolveAuthenticatedProfile();
985
+ if (!token) {
986
+ if (!profileName) throw new Error("Not logged in. Run " + chalk.cyan("fluid login") + " first.");
987
+ throw new Error("No auth token found for profile " + chalk.cyan(profileName) + ". Run " + chalk.cyan("fluid login") + " to re-authenticate.");
988
+ }
989
+ return createFetchClient({
990
+ baseUrl: profile?.baseUrl ?? process.env["FLUID_API_BASE"] ?? "https://api.fluid.app",
991
+ getAuthToken: () => token
992
+ });
993
+ }
994
+ function resolveAuthenticatedProfile() {
995
+ const config = readConfig();
996
+ const projectConfig = findProjectConfig(process.cwd());
997
+ if (projectConfig?.profile) {
998
+ const profile = config.profiles[projectConfig.profile];
999
+ if (profile) return {
1000
+ profile,
1001
+ profileName: projectConfig.profile,
1002
+ token: profile.token
1003
+ };
1004
+ }
1005
+ if (!config.activeProfile) return {};
1006
+ const profile = config.profiles[config.activeProfile];
1007
+ if (!profile) return {};
1008
+ return {
1009
+ profile,
1010
+ profileName: config.activeProfile,
1011
+ token: profile.token
1012
+ };
1013
+ }
1014
+ //#endregion
678
1015
  //#region src/lib/git.ts
679
1016
  /**
680
1017
  * Git wrapper for the Fluid widget CLI — shells out to the system `git`
@@ -1039,6 +1376,224 @@ async function gitStashPop(cwd) {
1039
1376
  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);
1040
1377
  }
1041
1378
  }
1379
+ /**
1380
+ * Local-only state that must never reach a widget's provisioned repo.
1381
+ *
1382
+ * `.mist-desktop/` is Mist Desktop's per-machine working state for the
1383
+ * project directory it opened — chat transcripts, user attachments,
1384
+ * screenshots, source baselines, media cache. When Desktop drives
1385
+ * `fluid widget push`, that directory sits in the project root, so a
1386
+ * whole-tree `git add -A` publishes every private conversation.
1387
+ * `.mist` is the sibling clone/slug marker.
1388
+ *
1389
+ * Mirrors the portal CLI (`packages/cli/portal/src/lib/git.ts`) and the
1390
+ * Mist CLI (`packages/cli/mist/src/lib/git.ts`).
1391
+ */
1392
+ const LOCAL_ONLY_GIT_PATHS = [".mist", ".mist-desktop/"];
1393
+ /**
1394
+ * Append {@link LOCAL_ONLY_GIT_PATHS} to `.git/info/exclude` when absent.
1395
+ *
1396
+ * Why the local exclude and not `.gitignore`: `.gitignore` ships from the
1397
+ * widget starter template, so editing it would never reach the clones
1398
+ * already on people's machines. `.git/info/exclude` is per-checkout, so
1399
+ * writing it on every sync heals a pre-existing clone on its next push.
1400
+ *
1401
+ * Idempotent and best-effort: never throws. {@link gitAutoCommit} also
1402
+ * cleans these paths out of the index after staging.
1403
+ */
1404
+ function excludeLocalStateFromGit(cwd) {
1405
+ try {
1406
+ const gitDir = path.join(cwd, ".git");
1407
+ if (!existsSync(gitDir)) return;
1408
+ const infoDir = path.join(gitDir, "info");
1409
+ if (!existsSync(infoDir)) mkdirSync(infoDir, { recursive: true });
1410
+ const excludePath = path.join(infoDir, "exclude");
1411
+ const existing = existsSync(excludePath) ? readFileSync(excludePath, "utf8") : "";
1412
+ const existingLines = new Set(existing.split("\n").map((line) => line.trim()).filter(Boolean));
1413
+ const missing = LOCAL_ONLY_GIT_PATHS.filter((entry) => !existingLines.has(entry));
1414
+ if (missing.length === 0) return;
1415
+ writeFileSync(excludePath, `${existing}${existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""}${missing.join("\n")}\n`, "utf8");
1416
+ } catch {}
1417
+ }
1418
+ /**
1419
+ * Absolute path of the repository containing `cwd`, or `cwd` when
1420
+ * resolution fails. Every local-state defense anchors here: `git add -A`
1421
+ * stages the WHOLE repository, so a push run from a subdirectory would
1422
+ * otherwise look for `<subdir>/.git`, skip the exclude, and sweep the
1423
+ * root `.mist-desktop/` in anyway.
1424
+ */
1425
+ async function resolveRepoRoot(cwd) {
1426
+ try {
1427
+ const { stdout } = await execa("git", ["rev-parse", "--show-toplevel"], { cwd });
1428
+ const root = stdout.trim();
1429
+ if (root.length > 0) return root;
1430
+ } catch {}
1431
+ return cwd;
1432
+ }
1433
+ /**
1434
+ * Repository-root-anchored {@link excludeLocalStateFromGit}. Run before
1435
+ * any working-tree cleanliness check: while `.mist-desktop/` is neither
1436
+ * tracked nor excluded it shows as untracked in `git status --porcelain`,
1437
+ * so a Desktop-driven push needlessly stashes/pops around a pull.
1438
+ */
1439
+ async function ensureLocalStateExcluded(cwd) {
1440
+ excludeLocalStateFromGit(await resolveRepoRoot(cwd));
1441
+ }
1442
+ /** Paths currently tracked under `.mist-desktop/`, or [] when none. */
1443
+ async function trackedLocalStatePaths(repoRoot) {
1444
+ try {
1445
+ const { stdout } = await execa("git", [
1446
+ "ls-files",
1447
+ "--",
1448
+ ".mist-desktop"
1449
+ ], { cwd: repoRoot });
1450
+ return stdout.split("\n").filter((l) => l.trim().length > 0);
1451
+ } catch {
1452
+ return [];
1453
+ }
1454
+ }
1455
+ /**
1456
+ * Untrack private Mist Desktop state and, if anything was tracked,
1457
+ * commit ONLY that removal so the next push deletes it from the remote.
1458
+ * Returns true when a purge commit was made.
1459
+ *
1460
+ * Must run regardless of working-tree cleanliness AND of `--no-commit`:
1461
+ * an older CLI could have committed chats to a checkout that is now
1462
+ * clean, and gating on either would skip the cleanup forever, leaving
1463
+ * the chats published.
1464
+ *
1465
+ * The commit is built through a THROWAWAY index (`GIT_INDEX_FILE`), not
1466
+ * the user's: `read-tree HEAD` → `rm --cached` → `write-tree` →
1467
+ * `commit-tree` → `update-ref`. A plain `git commit` would capture the
1468
+ * user's entire index, so anything they had pre-staged would be swept
1469
+ * into the purge commit and pushed — the exact thing `--no-commit`
1470
+ * exists to prevent. A pathspec commit (`git commit -- .mist-desktop`)
1471
+ * is also wrong: with `--only` git takes the *working tree* content of
1472
+ * those paths, and the chats are still on disk, so it would re-add
1473
+ * exactly what we are removing.
1474
+ *
1475
+ * The user's real index then gets the same scoped `rm --cached`, so any
1476
+ * other staged work stays staged and the working tree is never touched.
1477
+ */
1478
+ async function purgeLocalStateFromIndex(cwd, message, identity = DEFAULT_COMMIT_IDENTITY) {
1479
+ const repoRoot = await resolveRepoRoot(cwd);
1480
+ if ((await trackedLocalStatePaths(repoRoot)).length === 0) return false;
1481
+ let head;
1482
+ try {
1483
+ const { stdout } = await execa("git", ["rev-parse", "HEAD"], { cwd: repoRoot });
1484
+ head = stdout.trim();
1485
+ if (!head) return false;
1486
+ } catch {
1487
+ return false;
1488
+ }
1489
+ const tmpIndex = path.join(os.tmpdir(), `fluid-widget-purge-${process.pid}-${randomUUID()}.index`);
1490
+ const indexEnv = {
1491
+ ...process.env,
1492
+ GIT_INDEX_FILE: tmpIndex
1493
+ };
1494
+ let commitSha;
1495
+ try {
1496
+ await execa("git", ["read-tree", head], {
1497
+ cwd: repoRoot,
1498
+ env: indexEnv
1499
+ });
1500
+ await execa("git", [
1501
+ "rm",
1502
+ "-q",
1503
+ "-r",
1504
+ "--cached",
1505
+ "--ignore-unmatch",
1506
+ "--",
1507
+ ".mist-desktop"
1508
+ ], {
1509
+ cwd: repoRoot,
1510
+ env: indexEnv
1511
+ });
1512
+ const { stdout: tree } = await execa("git", ["write-tree"], {
1513
+ cwd: repoRoot,
1514
+ env: indexEnv
1515
+ });
1516
+ const { stdout: sha } = await execa("git", [
1517
+ "commit-tree",
1518
+ tree.trim(),
1519
+ "-p",
1520
+ head,
1521
+ "-m",
1522
+ message
1523
+ ], {
1524
+ cwd: repoRoot,
1525
+ env: {
1526
+ ...indexEnv,
1527
+ ...await identityEnv(repoRoot, identity)
1528
+ }
1529
+ });
1530
+ commitSha = sha.trim();
1531
+ if (!commitSha) return false;
1532
+ } catch (err) {
1533
+ const missing = gitMissingError(err);
1534
+ if (missing) throw missing;
1535
+ throw wrapGitError(err, "purge local state");
1536
+ } finally {
1537
+ rmSync(tmpIndex, { force: true });
1538
+ }
1539
+ try {
1540
+ await execa("git", [
1541
+ "update-ref",
1542
+ "HEAD",
1543
+ commitSha,
1544
+ head
1545
+ ], { cwd: repoRoot });
1546
+ } catch (err) {
1547
+ throw wrapGitError(err, "purge local state");
1548
+ }
1549
+ try {
1550
+ await execa("git", [
1551
+ "rm",
1552
+ "-q",
1553
+ "-r",
1554
+ "--cached",
1555
+ "--ignore-unmatch",
1556
+ "--",
1557
+ ".mist-desktop"
1558
+ ], { cwd: repoRoot });
1559
+ } catch {}
1560
+ return true;
1561
+ }
1562
+ /** `GIT_{AUTHOR,COMMITTER}_*` overrides for `commit-tree`, which (unlike
1563
+ * `git commit`) fails outright rather than prompting when the repo has
1564
+ * no identity. Only set what the repo itself doesn't define. */
1565
+ async function identityEnv(cwd, identity) {
1566
+ const env = {};
1567
+ const has = async (key) => {
1568
+ try {
1569
+ return (await execa("git", ["config", key], { cwd })).stdout.trim().length > 0;
1570
+ } catch {
1571
+ return false;
1572
+ }
1573
+ };
1574
+ if (!await has("user.name")) {
1575
+ env["GIT_AUTHOR_NAME"] = identity.name;
1576
+ env["GIT_COMMITTER_NAME"] = identity.name;
1577
+ }
1578
+ if (!await has("user.email")) {
1579
+ env["GIT_AUTHOR_EMAIL"] = identity.email;
1580
+ env["GIT_COMMITTER_EMAIL"] = identity.email;
1581
+ }
1582
+ return env;
1583
+ }
1584
+ /** Shared `-c user.*` args: prefer the repo's configured identity, else
1585
+ * the supplied synthetic one so the commit isn't rejected. */
1586
+ async function resolveIdentityArgs(cwd, identity) {
1587
+ let hasName = false;
1588
+ let hasEmail = false;
1589
+ try {
1590
+ hasName = (await execa("git", ["config", "user.name"], { cwd })).stdout.trim().length > 0;
1591
+ } catch {}
1592
+ try {
1593
+ hasEmail = (await execa("git", ["config", "user.email"], { cwd })).stdout.trim().length > 0;
1594
+ } catch {}
1595
+ return [...hasName ? [] : ["-c", `user.name=${identity.name}`], ...hasEmail ? [] : ["-c", `user.email=${identity.email}`]];
1596
+ }
1042
1597
  const DEFAULT_COMMIT_IDENTITY = {
1043
1598
  name: "Fluid CLI",
1044
1599
  email: "cli@fluid.app"
@@ -1051,22 +1606,54 @@ const DEFAULT_COMMIT_IDENTITY = {
1051
1606
  * commit per push. If the repo has no configured user.name / user.email we
1052
1607
  * pass a synthetic identity so the commit isn't rejected. "nothing to commit"
1053
1608
  * is treated as a no-op rather than an error.
1609
+ *
1610
+ * Scope: `add -A` stages the whole work tree, bounded by the project
1611
+ * `.gitignore` — which can't cover tool-local state written INTO the
1612
+ * project by whatever opened it (Mist Desktop's `.mist-desktop/` chats and
1613
+ * the `.mist` marker). Belt and suspenders, all repo-root-anchored:
1614
+ *
1615
+ * 1. {@link excludeLocalStateFromGit} — idempotent `.git/info/exclude`
1616
+ * write so `git add` never sees the paths. Heals old clones.
1617
+ * 2. `git reset -- ':(top).mist' .mist` — unstages the marker if step 1
1618
+ * silently failed. We deliberately do NOT pass `:(exclude)` to
1619
+ * `git add`: when the path is also in `.git/info/exclude`, git exits
1620
+ * 1 with "paths are ignored by one of your .gitignore files", which
1621
+ * would turn the already-healed case into a hard failure.
1622
+ * 3. `git rm --cached -r --ignore-unmatch .mist-desktop` — untracks
1623
+ * chats an older push already committed (ignore rules can't untrack).
1624
+ * `--cached` keeps every file on disk.
1625
+ *
1626
+ * Steps 2 and 3 are best-effort and never fail the push.
1054
1627
  */
1055
1628
  async function gitAutoCommit(cwd, message, identity = DEFAULT_COMMIT_IDENTITY) {
1629
+ const repoRoot = await resolveRepoRoot(cwd);
1630
+ excludeLocalStateFromGit(repoRoot);
1056
1631
  try {
1057
1632
  await execa("git", ["add", "-A"], { cwd });
1058
1633
  } catch (err) {
1059
1634
  throw wrapGitError(err, "add");
1060
1635
  }
1061
- let hasName = false;
1062
- let hasEmail = false;
1063
1636
  try {
1064
- hasName = (await execa("git", ["config", "user.name"], { cwd })).stdout.trim().length > 0;
1637
+ await execa("git", [
1638
+ "reset",
1639
+ "-q",
1640
+ "--",
1641
+ ":(top).mist",
1642
+ ".mist"
1643
+ ], { cwd });
1065
1644
  } catch {}
1066
1645
  try {
1067
- hasEmail = (await execa("git", ["config", "user.email"], { cwd })).stdout.trim().length > 0;
1646
+ await execa("git", [
1647
+ "rm",
1648
+ "-q",
1649
+ "-r",
1650
+ "--cached",
1651
+ "--ignore-unmatch",
1652
+ "--",
1653
+ ".mist-desktop"
1654
+ ], { cwd: repoRoot });
1068
1655
  } catch {}
1069
- const identityArgs = [...hasName ? [] : ["-c", `user.name=${identity.name}`], ...hasEmail ? [] : ["-c", `user.email=${identity.email}`]];
1656
+ const identityArgs = await resolveIdentityArgs(cwd, identity);
1070
1657
  try {
1071
1658
  await execa("git", [
1072
1659
  ...identityArgs,
@@ -1235,138 +1822,6 @@ const cloneCommand = new Command("clone").description("Clone a widget package's
1235
1822
  console.log(chalk.dim(` cd ${path.relative(process.cwd(), targetDir) || "."}`));
1236
1823
  if (options.skipInstall === true) console.log(chalk.dim(` ${installCommand}`));
1237
1824
  }));
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
1825
  /**
1371
1826
  * The template repo to clone. Overridable via `FLUID_WIDGET_TEMPLATE_REPO`
1372
1827
  * (read at call time so tests can point at a local repo). Falls back to the
@@ -1697,11 +2152,6 @@ function isRecord$2(value) {
1697
2152
  return typeof value === "object" && value !== null && !Array.isArray(value);
1698
2153
  }
1699
2154
  //#endregion
1700
- //#region src/utils/auth-client.ts
1701
- function createAuthenticatedClient() {
1702
- return { client: createAuthenticatedWidgetPackageClient() };
1703
- }
1704
- //#endregion
1705
2155
  //#region src/utils/droplets.ts
1706
2156
  const MAX_DROPLET_PAGES = 100;
1707
2157
  async function promptForDroplet(client) {
@@ -1955,29 +2405,7 @@ function parseLimit(value) {
1955
2405
  }
1956
2406
  //#endregion
1957
2407
  //#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
- }
2408
+ async function publishWidgetPackage(options) {
1981
2409
  const droplet = await resolvePublishDroplet(options.projectDir, options.droplet);
1982
2410
  await validateDropletWidgetProject(options.projectDir, droplet);
1983
2411
  if (options.skipTypecheck !== true) await runPackageScriptIfAvailable({
@@ -1985,16 +2413,11 @@ async function publishWidgetPackage(options, dependencies) {
1985
2413
  scriptName: "typecheck"
1986
2414
  });
1987
2415
  const dryRun = options.dryRun === true;
1988
- await withRootWidgetConfigBridge(options.projectDir, () => publishWidgetRuntimeArtifacts({
2416
+ await withRootWidgetConfigBridge(options.projectDir, () => publishDropletWidgetPackageArtifacts({
1989
2417
  projectDir: options.projectDir,
1990
2418
  outDir: options.outDir,
1991
- buildOwner: "droplet",
1992
- uploadOwner: {
1993
- kind: "droplet",
1994
- uuid: droplet
1995
- },
1996
- dryRun,
1997
- ...dryRun ? {} : { client: createAuthenticatedWidgetPackageClient() }
2419
+ droplet,
2420
+ dryRun
1998
2421
  }), droplet);
1999
2422
  return {
2000
2423
  dryRun,
@@ -2029,16 +2452,11 @@ const publishCommand = new Command("publish").description("Publish a Fluid widge
2029
2452
  else typecheckSpinner.info("No typecheck script found; skipping");
2030
2453
  }
2031
2454
  const publishSpinner = ora(dryRun ? "Building and preparing dry-run upload payload..." : "Building and publishing widget artifacts...").start();
2032
- const result = await withRootWidgetConfigBridge(projectDir, () => publishWidgetRuntimeArtifacts({
2455
+ const result = await withRootWidgetConfigBridge(projectDir, () => publishDropletWidgetPackageArtifacts({
2033
2456
  projectDir,
2034
2457
  outDir,
2035
- buildOwner: "droplet",
2036
- uploadOwner: {
2037
- kind: "droplet",
2038
- uuid: droplet
2039
- },
2040
- dryRun,
2041
- ...dryRun ? {} : { client: createAuthenticatedWidgetPackageClient() }
2458
+ droplet,
2459
+ dryRun
2042
2460
  }), droplet);
2043
2461
  publishSpinner.succeed(dryRun ? "Prepared dry-run upload payload" : "Published widget package");
2044
2462
  if (dryRun) {
@@ -2064,6 +2482,47 @@ async function resolvePublishDroplet(projectDir, override) {
2064
2482
  }
2065
2483
  throw new Error("Missing droplet UUID. Pass --droplet <uuid> or set droplet in fluid.widget.config.ts.");
2066
2484
  }
2485
+ async function publishDropletWidgetPackageArtifacts(options) {
2486
+ const common = {
2487
+ projectDir: options.projectDir,
2488
+ outDir: options.outDir,
2489
+ owner: "droplet",
2490
+ uploadOwner: {
2491
+ kind: "droplet",
2492
+ uuid: options.droplet
2493
+ },
2494
+ cliVersion: await getWidgetCliVersion()
2495
+ };
2496
+ if (options.dryRun) return publishWidgetPackage$1({
2497
+ ...common,
2498
+ dryRun: true
2499
+ });
2500
+ return publishWidgetPackage$1({
2501
+ ...common,
2502
+ dryRun: false,
2503
+ client: createAuthenticatedWidgetPackageClient()
2504
+ });
2505
+ }
2506
+ function printRuntimeOnlyNotice() {
2507
+ console.log(chalk.yellow("This publishes widget runtime artifacts only; it does not deploy the hosted portal shell."));
2508
+ console.log();
2509
+ }
2510
+ function printDryRunSessionPayload(result) {
2511
+ console.log(chalk.bold("Dry-run upload session payload:"));
2512
+ console.log(JSON.stringify(result.publish.sessionPayload, null, 2));
2513
+ console.log();
2514
+ }
2515
+ function printWidgetPublishSummary(options) {
2516
+ console.log();
2517
+ console.log(chalk.green.bold(`${options.title} complete`));
2518
+ console.log();
2519
+ console.log(chalk.gray("Owner: ") + chalk.white(options.ownerLabel));
2520
+ console.log(chalk.gray("Package: ") + chalk.white(options.result.build.packageId));
2521
+ console.log(chalk.gray("Version: ") + chalk.white(options.result.build.version));
2522
+ console.log(chalk.gray("Output: ") + chalk.cyan(options.outDir));
2523
+ console.log(chalk.gray("Artifacts: ") + chalk.white(String(options.result.request.artifacts.length)));
2524
+ console.log();
2525
+ }
2067
2526
  //#endregion
2068
2527
  //#region src/utils/me.ts
2069
2528
  function str(value) {
@@ -2095,6 +2554,7 @@ async function syncWidgetGitRepo(options) {
2095
2554
  const { cwd, client } = options;
2096
2555
  const branch = options.branch ?? "main";
2097
2556
  if (!await isGitRepository(cwd)) await gitInit(cwd, branch);
2557
+ await ensureLocalStateExcluded(cwd);
2098
2558
  const cred = await mintWidgetGitCredential({
2099
2559
  client,
2100
2560
  droplet: options.droplet
@@ -2121,6 +2581,7 @@ async function syncWidgetGitRepo(options) {
2121
2581
  }
2122
2582
  } else pulled = await gitAdoptFetchHead(cwd);
2123
2583
  }
2584
+ await purgeLocalStateFromIndex(cwd, "Remove private Mist Desktop state from the widget repository", await resolveCommitIdentity(client));
2124
2585
  let committed = false;
2125
2586
  if (options.commit !== false && await hasUncommittedChanges(cwd)) {
2126
2587
  await gitAutoCommit(cwd, await buildCommitMessage(client, options.message), await resolveCommitIdentity(client));