@fluid-app/fluid-cli-widget 0.1.26 → 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-BYRGkNyt.d.mts +91 -0
- package/dist/index-BYRGkNyt.d.mts.map +1 -0
- package/dist/index.d.mts +3 -15
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +739 -278
- package/dist/index.mjs.map +1 -1
- package/dist/src-CVadzcYM.mjs +1670 -0
- package/dist/src-CVadzcYM.mjs.map +1 -0
- package/dist/vite/plugin.d.mts +80 -3
- package/dist/vite/plugin.d.mts.map +1 -1
- package/dist/vite/plugin.mjs +124 -26
- package/dist/vite/plugin.mjs.map +1 -1
- package/package.json +6 -3
- package/templates/default/.gitignore.template +6 -0
- package/templates/default/package.json.template +3 -1
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
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
|
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
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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.
|
|
655
|
-
packageId: buildResult.
|
|
656
|
-
version: buildResult.
|
|
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
|
-
|
|
1637
|
+
await execa("git", [
|
|
1638
|
+
"reset",
|
|
1639
|
+
"-q",
|
|
1640
|
+
"--",
|
|
1641
|
+
":(top).mist",
|
|
1642
|
+
".mist"
|
|
1643
|
+
], { cwd });
|
|
1065
1644
|
} catch {}
|
|
1066
1645
|
try {
|
|
1067
|
-
|
|
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 =
|
|
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.26";
|
|
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
|
|
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, () =>
|
|
2416
|
+
await withRootWidgetConfigBridge(options.projectDir, () => publishDropletWidgetPackageArtifacts({
|
|
1989
2417
|
projectDir: options.projectDir,
|
|
1990
2418
|
outDir: options.outDir,
|
|
1991
|
-
|
|
1992
|
-
|
|
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, () =>
|
|
2455
|
+
const result = await withRootWidgetConfigBridge(projectDir, () => publishDropletWidgetPackageArtifacts({
|
|
2033
2456
|
projectDir,
|
|
2034
2457
|
outDir,
|
|
2035
|
-
|
|
2036
|
-
|
|
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));
|