@fluid-app/fluid-cli-portal 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +594 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1123 -34
- package/dist/index.mjs.map +1 -1
- package/dist/pull-p1mSVa5W.mjs +1148 -0
- package/dist/pull-p1mSVa5W.mjs.map +1 -0
- package/dist/vite-plugin.d.mts +63 -0
- package/dist/vite-plugin.d.mts.map +1 -0
- package/dist/vite-plugin.mjs +227 -0
- package/dist/vite-plugin.mjs.map +1 -0
- package/package.json +6 -1
|
@@ -0,0 +1,1148 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
5
|
+
import { mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
6
|
+
import prompts from "prompts";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { getActiveProfile, getAuthToken } from "@fluid-app/fluid-cli";
|
|
9
|
+
import pLimit from "p-limit";
|
|
10
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
11
|
+
//#region \0rolldown/runtime.js
|
|
12
|
+
var __defProp = Object.defineProperty;
|
|
13
|
+
var __exportAll = (all, no_symbols) => {
|
|
14
|
+
let target = {};
|
|
15
|
+
for (var name in all) __defProp(target, name, {
|
|
16
|
+
get: all[name],
|
|
17
|
+
enumerable: true
|
|
18
|
+
});
|
|
19
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
20
|
+
return target;
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region ../../platform/api-client-core/src/fetch-client.ts
|
|
24
|
+
/**
|
|
25
|
+
* API Error class compatible with fluid-admin's ApiError
|
|
26
|
+
*/
|
|
27
|
+
var ApiError = class ApiError extends Error {
|
|
28
|
+
status;
|
|
29
|
+
data;
|
|
30
|
+
constructor(message, status, data) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = "ApiError";
|
|
33
|
+
this.status = status;
|
|
34
|
+
this.data = data;
|
|
35
|
+
if ("captureStackTrace" in Error) Error.captureStackTrace(this, ApiError);
|
|
36
|
+
}
|
|
37
|
+
toJSON() {
|
|
38
|
+
return {
|
|
39
|
+
name: this.name,
|
|
40
|
+
message: this.message,
|
|
41
|
+
status: this.status,
|
|
42
|
+
data: this.data
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Creates a configured fetch client instance
|
|
48
|
+
*/
|
|
49
|
+
function createFetchClient(config) {
|
|
50
|
+
const { baseUrl, getAuthToken, onAuthError, defaultHeaders = {} } = config;
|
|
51
|
+
/**
|
|
52
|
+
* Build headers for a request
|
|
53
|
+
*/
|
|
54
|
+
async function buildHeaders(customHeaders) {
|
|
55
|
+
const headers = {
|
|
56
|
+
Accept: "application/json",
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
...defaultHeaders,
|
|
59
|
+
...customHeaders
|
|
60
|
+
};
|
|
61
|
+
if (getAuthToken) {
|
|
62
|
+
const token = await getAuthToken();
|
|
63
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
64
|
+
}
|
|
65
|
+
return headers;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Join baseUrl + endpoint via string concatenation (matches fetchApi).
|
|
69
|
+
* Using `new URL(endpoint, baseUrl)` would strip any path prefix from
|
|
70
|
+
* baseUrl (e.g. "/api") when the endpoint starts with "/".
|
|
71
|
+
*/
|
|
72
|
+
function joinUrl(endpoint) {
|
|
73
|
+
return `${baseUrl}${endpoint}`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build URL with query parameters for GET requests
|
|
77
|
+
* Compatible with fluid-admin's query param handling
|
|
78
|
+
*/
|
|
79
|
+
function buildUrl(endpoint, params) {
|
|
80
|
+
const fullUrl = joinUrl(endpoint);
|
|
81
|
+
if (!params || Object.keys(params).length === 0) return fullUrl;
|
|
82
|
+
const queryString = new URLSearchParams();
|
|
83
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
84
|
+
if (value === void 0 || value === null) return;
|
|
85
|
+
if (Array.isArray(value)) value.forEach((item) => queryString.append(`${key}[]`, String(item)));
|
|
86
|
+
else if (typeof value === "object") Object.entries(value).forEach(([subKey, subValue]) => {
|
|
87
|
+
if (subValue === void 0 || subValue === null) return;
|
|
88
|
+
if (Array.isArray(subValue)) subValue.forEach((item) => queryString.append(`${key}[${subKey}][]`, String(item)));
|
|
89
|
+
else queryString.append(`${key}[${subKey}]`, String(subValue));
|
|
90
|
+
});
|
|
91
|
+
else queryString.append(key, String(value));
|
|
92
|
+
});
|
|
93
|
+
const qs = queryString.toString();
|
|
94
|
+
return qs ? `${fullUrl}?${qs}` : fullUrl;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Shared response handler for both JSON and FormData requests.
|
|
98
|
+
* Handles auth errors, non-OK responses, 204 No Content, and JSON parsing.
|
|
99
|
+
*/
|
|
100
|
+
async function handleResponse(response, method, _url) {
|
|
101
|
+
if (response.status === 401 && onAuthError) onAuthError();
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
const errorText = await response.text().catch(() => "");
|
|
104
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
105
|
+
let data;
|
|
106
|
+
try {
|
|
107
|
+
data = JSON.parse(errorText);
|
|
108
|
+
} catch {
|
|
109
|
+
throw new ApiError(errorText.slice(0, 200) || `${method} request failed with status ${response.status}`, response.status, null);
|
|
110
|
+
}
|
|
111
|
+
throw new ApiError(data.message || data.error_message || `${method} request failed`, response.status, data.errors || data);
|
|
112
|
+
} else throw new ApiError(`${method} request failed with status ${response.status}`, response.status, null);
|
|
113
|
+
}
|
|
114
|
+
if (response.status === 204 || response.headers.get("content-length") === "0") return null;
|
|
115
|
+
if (response.headers.get("content-type")?.includes("application/json")) try {
|
|
116
|
+
return await response.json();
|
|
117
|
+
} catch {
|
|
118
|
+
try {
|
|
119
|
+
return await response.text();
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Main request function
|
|
128
|
+
*/
|
|
129
|
+
async function request(endpoint, options = {}) {
|
|
130
|
+
const { method = "GET", headers: customHeaders, params, body, signal } = options;
|
|
131
|
+
const url = params ? buildUrl(endpoint, params) : joinUrl(endpoint);
|
|
132
|
+
const headers = await buildHeaders(customHeaders);
|
|
133
|
+
let response;
|
|
134
|
+
try {
|
|
135
|
+
const fetchOptions = {
|
|
136
|
+
method,
|
|
137
|
+
headers
|
|
138
|
+
};
|
|
139
|
+
const serializedBody = body && method !== "GET" ? JSON.stringify(body) : null;
|
|
140
|
+
if (serializedBody) fetchOptions.body = serializedBody;
|
|
141
|
+
if (signal) fetchOptions.signal = signal;
|
|
142
|
+
response = await fetch(url, fetchOptions);
|
|
143
|
+
} catch (networkError) {
|
|
144
|
+
throw new ApiError(`Network error: ${networkError instanceof Error ? networkError.message : "Unknown network error"}`, 0, null);
|
|
145
|
+
}
|
|
146
|
+
return handleResponse(response, method, url);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Request with FormData (for file uploads)
|
|
150
|
+
*/
|
|
151
|
+
async function requestWithFormData(endpoint, formData, options = {}) {
|
|
152
|
+
const { method = "POST", headers: customHeaders, signal } = options;
|
|
153
|
+
const url = joinUrl(endpoint);
|
|
154
|
+
const headers = await buildHeaders(customHeaders);
|
|
155
|
+
delete headers["Content-Type"];
|
|
156
|
+
let response;
|
|
157
|
+
try {
|
|
158
|
+
const fetchOptions = {
|
|
159
|
+
method,
|
|
160
|
+
headers,
|
|
161
|
+
body: formData
|
|
162
|
+
};
|
|
163
|
+
if (signal) fetchOptions.signal = signal;
|
|
164
|
+
response = await fetch(url, fetchOptions);
|
|
165
|
+
} catch (networkError) {
|
|
166
|
+
throw new ApiError(`Network error: ${networkError instanceof Error ? networkError.message : "Unknown network error"}`, 0, null);
|
|
167
|
+
}
|
|
168
|
+
return handleResponse(response, method, url);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
request,
|
|
172
|
+
requestWithFormData,
|
|
173
|
+
get: (endpoint, params, options) => request(endpoint, {
|
|
174
|
+
...options,
|
|
175
|
+
method: "GET",
|
|
176
|
+
...params && { params }
|
|
177
|
+
}),
|
|
178
|
+
post: (endpoint, body, options) => request(endpoint, {
|
|
179
|
+
...options,
|
|
180
|
+
method: "POST",
|
|
181
|
+
body
|
|
182
|
+
}),
|
|
183
|
+
put: (endpoint, body, options) => request(endpoint, {
|
|
184
|
+
...options,
|
|
185
|
+
method: "PUT",
|
|
186
|
+
body
|
|
187
|
+
}),
|
|
188
|
+
patch: (endpoint, body, options) => request(endpoint, {
|
|
189
|
+
...options,
|
|
190
|
+
method: "PATCH",
|
|
191
|
+
body
|
|
192
|
+
}),
|
|
193
|
+
delete: (endpoint, options) => request(endpoint, {
|
|
194
|
+
...options,
|
|
195
|
+
method: "DELETE"
|
|
196
|
+
})
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
//#endregion
|
|
200
|
+
//#region ../../api-clients/fluidos/src/namespaces/fluid_os.ts
|
|
201
|
+
/**
|
|
202
|
+
* List Fluid OS definitions
|
|
203
|
+
* Retrieve a list of Fluid OS definitions for the current company
|
|
204
|
+
*
|
|
205
|
+
* @param client - Fetch client instance
|
|
206
|
+
* @param params? - params?
|
|
207
|
+
*/
|
|
208
|
+
async function listFluidOSDefinitions(client, params) {
|
|
209
|
+
return client.get(`/api/company/fluid_os/definitions`, params);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* List navigation items for a navigation
|
|
213
|
+
* Retrieve a list of navigation items for a specific navigation
|
|
214
|
+
*
|
|
215
|
+
* @param client - Fetch client instance
|
|
216
|
+
* @param definition_id - definition_id
|
|
217
|
+
* @param navigation_id - navigation_id
|
|
218
|
+
*/
|
|
219
|
+
async function listFluidOSNavigationItems(client, definition_id, navigation_id) {
|
|
220
|
+
return client.get(`/api/company/fluid_os/definitions/${definition_id}/navigations/${navigation_id}/navigation_items`);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Create a navigation item
|
|
224
|
+
* Create a new navigation item for a navigation
|
|
225
|
+
*
|
|
226
|
+
* @param client - Fetch client instance
|
|
227
|
+
* @param definition_id - definition_id
|
|
228
|
+
* @param navigation_id - navigation_id
|
|
229
|
+
* @param body - body
|
|
230
|
+
*/
|
|
231
|
+
async function createFluidOSNavigationItem(client, definition_id, navigation_id, body) {
|
|
232
|
+
return client.post(`/api/company/fluid_os/definitions/${definition_id}/navigations/${navigation_id}/navigation_items`, body);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Update a navigation item
|
|
236
|
+
* Update an existing navigation item
|
|
237
|
+
*
|
|
238
|
+
* @param client - Fetch client instance
|
|
239
|
+
* @param definition_id - definition_id
|
|
240
|
+
* @param navigation_id - navigation_id
|
|
241
|
+
* @param id - id
|
|
242
|
+
* @param body - body
|
|
243
|
+
*/
|
|
244
|
+
async function updateFluidOSNavigationItem(client, definition_id, navigation_id, id, body) {
|
|
245
|
+
return client.put(`/api/company/fluid_os/definitions/${definition_id}/navigations/${navigation_id}/navigation_items/${id}`, body);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Delete a navigation item
|
|
249
|
+
* Delete a navigation item
|
|
250
|
+
*
|
|
251
|
+
* @param client - Fetch client instance
|
|
252
|
+
* @param definition_id - definition_id
|
|
253
|
+
* @param navigation_id - navigation_id
|
|
254
|
+
* @param id - id
|
|
255
|
+
*/
|
|
256
|
+
async function deleteFluidOSNavigationItem(client, definition_id, navigation_id, id) {
|
|
257
|
+
return client.delete(`/api/company/fluid_os/definitions/${definition_id}/navigations/${navigation_id}/navigation_items/${id}`);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* List navigations for a Fluid OS definition
|
|
261
|
+
* Retrieve a list of navigations for a specific Fluid OS definition
|
|
262
|
+
*
|
|
263
|
+
* @param client - Fetch client instance
|
|
264
|
+
* @param definition_id - definition_id
|
|
265
|
+
* @param params? - params?
|
|
266
|
+
*/
|
|
267
|
+
async function listFluidOSNavigations(client, definition_id, params) {
|
|
268
|
+
return client.get(`/api/company/fluid_os/definitions/${definition_id}/navigations`, params);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Create a navigation for a Fluid OS definition
|
|
272
|
+
* Create a new navigation for a Fluid OS definition
|
|
273
|
+
*
|
|
274
|
+
* @param client - Fetch client instance
|
|
275
|
+
* @param definition_id - definition_id
|
|
276
|
+
* @param body - body
|
|
277
|
+
*/
|
|
278
|
+
async function createFluidOSNavigation(client, definition_id, body) {
|
|
279
|
+
return client.post(`/api/company/fluid_os/definitions/${definition_id}/navigations`, body);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Update a navigation
|
|
283
|
+
* Update an existing navigation
|
|
284
|
+
*
|
|
285
|
+
* @param client - Fetch client instance
|
|
286
|
+
* @param definition_id - definition_id
|
|
287
|
+
* @param id - id
|
|
288
|
+
* @param body - body
|
|
289
|
+
*/
|
|
290
|
+
async function updateFluidOSNavigation(client, definition_id, id, body) {
|
|
291
|
+
return client.put(`/api/company/fluid_os/definitions/${definition_id}/navigations/${id}`, body);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Delete a navigation
|
|
295
|
+
* Delete a navigation
|
|
296
|
+
*
|
|
297
|
+
* @param client - Fetch client instance
|
|
298
|
+
* @param definition_id - definition_id
|
|
299
|
+
* @param id - id
|
|
300
|
+
*/
|
|
301
|
+
async function deleteFluidOSNavigation(client, definition_id, id) {
|
|
302
|
+
return client.delete(`/api/company/fluid_os/definitions/${definition_id}/navigations/${id}`);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* List profiles for a Fluid OS definition
|
|
306
|
+
* Retrieve a list of profiles for a specific Fluid OS definition
|
|
307
|
+
*
|
|
308
|
+
* @param client - Fetch client instance
|
|
309
|
+
* @param definition_id - definition_id
|
|
310
|
+
* @param params? - params?
|
|
311
|
+
*/
|
|
312
|
+
async function listFluidOSProfiles(client, definition_id, params) {
|
|
313
|
+
return client.get(`/api/company/fluid_os/definitions/${definition_id}/profiles`, params);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Create a profile for a Fluid OS definition
|
|
317
|
+
* Create a new profile for a Fluid OS definition
|
|
318
|
+
*
|
|
319
|
+
* @param client - Fetch client instance
|
|
320
|
+
* @param definition_id - definition_id
|
|
321
|
+
* @param body - body
|
|
322
|
+
*/
|
|
323
|
+
async function createFluidOSProfile(client, definition_id, body) {
|
|
324
|
+
return client.post(`/api/company/fluid_os/definitions/${definition_id}/profiles`, body);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Update a profile
|
|
328
|
+
* Update an existing profile
|
|
329
|
+
*
|
|
330
|
+
* @param client - Fetch client instance
|
|
331
|
+
* @param definition_id - definition_id
|
|
332
|
+
* @param id - id
|
|
333
|
+
* @param body - body
|
|
334
|
+
*/
|
|
335
|
+
async function updateFluidOSProfile(client, definition_id, id, body) {
|
|
336
|
+
return client.put(`/api/company/fluid_os/definitions/${definition_id}/profiles/${id}`, body);
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Delete a profile
|
|
340
|
+
* Delete a profile
|
|
341
|
+
*
|
|
342
|
+
* @param client - Fetch client instance
|
|
343
|
+
* @param definition_id - definition_id
|
|
344
|
+
* @param id - id
|
|
345
|
+
*/
|
|
346
|
+
async function deleteFluidOSProfile(client, definition_id, id) {
|
|
347
|
+
return client.delete(`/api/company/fluid_os/definitions/${definition_id}/profiles/${id}`);
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* List screens for a Fluid OS definition
|
|
351
|
+
* Retrieve a list of screens for a specific Fluid OS definition
|
|
352
|
+
*
|
|
353
|
+
* @param client - Fetch client instance
|
|
354
|
+
* @param definition_id - definition_id
|
|
355
|
+
* @param params? - params?
|
|
356
|
+
*/
|
|
357
|
+
async function listFluidOSScreens(client, definition_id, params) {
|
|
358
|
+
return client.get(`/api/company/fluid_os/definitions/${definition_id}/screens`, params);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Create a screen for a Fluid OS definition
|
|
362
|
+
* Create a new screen for a Fluid OS definition
|
|
363
|
+
*
|
|
364
|
+
* @param client - Fetch client instance
|
|
365
|
+
* @param definition_id - definition_id
|
|
366
|
+
* @param body - body
|
|
367
|
+
*/
|
|
368
|
+
async function createFluidOSScreen(client, definition_id, body) {
|
|
369
|
+
return client.post(`/api/company/fluid_os/definitions/${definition_id}/screens`, body);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Get a specific screen
|
|
373
|
+
* Retrieve a specific screen
|
|
374
|
+
*
|
|
375
|
+
* @param client - Fetch client instance
|
|
376
|
+
* @param definition_id - definition_id
|
|
377
|
+
* @param id - id
|
|
378
|
+
*/
|
|
379
|
+
async function getFluidOSScreen(client, definition_id, id) {
|
|
380
|
+
return client.get(`/api/company/fluid_os/definitions/${definition_id}/screens/${id}`);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Update a screen
|
|
384
|
+
* Update an existing screen
|
|
385
|
+
*
|
|
386
|
+
* @param client - Fetch client instance
|
|
387
|
+
* @param definition_id - definition_id
|
|
388
|
+
* @param id - id
|
|
389
|
+
* @param body - body
|
|
390
|
+
*/
|
|
391
|
+
async function updateFluidOSScreen(client, definition_id, id, body) {
|
|
392
|
+
return client.put(`/api/company/fluid_os/definitions/${definition_id}/screens/${id}`, body);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Delete a screen
|
|
396
|
+
* Delete a screen
|
|
397
|
+
*
|
|
398
|
+
* @param client - Fetch client instance
|
|
399
|
+
* @param definition_id - definition_id
|
|
400
|
+
* @param id - id
|
|
401
|
+
*/
|
|
402
|
+
async function deleteFluidOSScreen(client, definition_id, id) {
|
|
403
|
+
return client.delete(`/api/company/fluid_os/definitions/${definition_id}/screens/${id}`);
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* List themes for a Fluid OS definition
|
|
407
|
+
* Retrieve a list of themes for a specific Fluid OS definition
|
|
408
|
+
*
|
|
409
|
+
* @param client - Fetch client instance
|
|
410
|
+
* @param definition_id - definition_id
|
|
411
|
+
* @param params? - params?
|
|
412
|
+
*/
|
|
413
|
+
async function listFluidOSThemes(client, definition_id, params) {
|
|
414
|
+
return client.get(`/api/company/fluid_os/definitions/${definition_id}/themes`, params);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Create a theme for a Fluid OS definition
|
|
418
|
+
* Create a new theme for a Fluid OS definition
|
|
419
|
+
*
|
|
420
|
+
* @param client - Fetch client instance
|
|
421
|
+
* @param definition_id - definition_id
|
|
422
|
+
* @param body - body
|
|
423
|
+
*/
|
|
424
|
+
async function createFluidOSTheme(client, definition_id, body) {
|
|
425
|
+
return client.post(`/api/company/fluid_os/definitions/${definition_id}/themes`, body);
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Update a theme
|
|
429
|
+
* Update an existing theme
|
|
430
|
+
*
|
|
431
|
+
* @param client - Fetch client instance
|
|
432
|
+
* @param definition_id - definition_id
|
|
433
|
+
* @param id - id
|
|
434
|
+
* @param body - body
|
|
435
|
+
*/
|
|
436
|
+
async function updateFluidOSTheme(client, definition_id, id, body) {
|
|
437
|
+
return client.put(`/api/company/fluid_os/definitions/${definition_id}/themes/${id}`, body);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Delete a theme
|
|
441
|
+
* Delete a theme
|
|
442
|
+
*
|
|
443
|
+
* @param client - Fetch client instance
|
|
444
|
+
* @param definition_id - definition_id
|
|
445
|
+
* @param id - id
|
|
446
|
+
*/
|
|
447
|
+
async function deleteFluidOSTheme(client, definition_id, id) {
|
|
448
|
+
return client.delete(`/api/company/fluid_os/definitions/${definition_id}/themes/${id}`);
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* List versions for a Fluid OS definition
|
|
452
|
+
* Retrieve a list of published versions for a specific Fluid OS definition
|
|
453
|
+
*
|
|
454
|
+
* @param client - Fetch client instance
|
|
455
|
+
* @param definition_id - definition_id
|
|
456
|
+
* @param params? - params?
|
|
457
|
+
*/
|
|
458
|
+
async function listFluidOSVersions(client, definition_id, params) {
|
|
459
|
+
return client.get(`/api/company/fluid_os/definitions/${definition_id}/versions`, params);
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Publish a new version of a Fluid OS definition
|
|
463
|
+
* Publish a new version of the Fluid OS definition. This creates a snapshot of the current definition state including all screens, profiles, themes, and navigations. No request body is required - the manifest is built automatically from the current definition state.
|
|
464
|
+
*
|
|
465
|
+
* @param client - Fetch client instance
|
|
466
|
+
* @param definition_id - definition_id
|
|
467
|
+
*/
|
|
468
|
+
async function createFluidOSVersion(client, definition_id) {
|
|
469
|
+
return client.post(`/api/company/fluid_os/definitions/${definition_id}/versions`);
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Update a version
|
|
473
|
+
* Update a version. Currently only supports activating/deactivating a version.
|
|
474
|
+
*
|
|
475
|
+
* @param client - Fetch client instance
|
|
476
|
+
* @param definition_id - definition_id
|
|
477
|
+
* @param id - id
|
|
478
|
+
* @param body - body
|
|
479
|
+
*/
|
|
480
|
+
async function updateFluidOSVersion(client, definition_id, id, body) {
|
|
481
|
+
return client.put(`/api/company/fluid_os/definitions/${definition_id}/versions/${id}`, body);
|
|
482
|
+
}
|
|
483
|
+
//#endregion
|
|
484
|
+
//#region src/utils/atomic-write.ts
|
|
485
|
+
/**
|
|
486
|
+
* Atomic file write utility.
|
|
487
|
+
*
|
|
488
|
+
* Uses a write-then-rename pattern so readers always see either the old
|
|
489
|
+
* file or the complete new file — never a partially-written state.
|
|
490
|
+
*/
|
|
491
|
+
/**
|
|
492
|
+
* Write a file atomically using a write-then-rename pattern.
|
|
493
|
+
* Ensures readers always see either the old file or the complete new file.
|
|
494
|
+
*
|
|
495
|
+
* If `rename` fails after the temp file has been written, the temp file
|
|
496
|
+
* is cleaned up on a best-effort basis before re-throwing the error.
|
|
497
|
+
*/
|
|
498
|
+
async function atomicWriteFile(filePath, data) {
|
|
499
|
+
const tmp = join(dirname(filePath), `.tmp-${randomBytes(6).toString("hex")}`);
|
|
500
|
+
try {
|
|
501
|
+
await writeFile(tmp, data, "utf-8");
|
|
502
|
+
await rename(tmp, filePath);
|
|
503
|
+
} catch (err) {
|
|
504
|
+
await unlink(tmp).catch(() => {});
|
|
505
|
+
throw err;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region src/utils/mappings.ts
|
|
510
|
+
/**
|
|
511
|
+
* Slug-to-ID mapping system for portal content sync.
|
|
512
|
+
*
|
|
513
|
+
* Maps human-readable slugs (derived from resource names) to API IDs,
|
|
514
|
+
* enabling the file system to use readable directory/file names while
|
|
515
|
+
* maintaining the link back to server-side resources.
|
|
516
|
+
*
|
|
517
|
+
* Persisted in `.portal-sync/mappings.json`.
|
|
518
|
+
*/
|
|
519
|
+
/** Runtime check that a parsed value has the expected PortalMappings shape. */
|
|
520
|
+
function isMappings(value) {
|
|
521
|
+
if (!value || typeof value !== "object") return false;
|
|
522
|
+
const m = value;
|
|
523
|
+
const def = m.definition;
|
|
524
|
+
if (!def || typeof def !== "object") return false;
|
|
525
|
+
if (typeof def.name !== "string" || typeof def.id !== "number") return false;
|
|
526
|
+
return typeof m.screens === "object" && m.screens !== null && typeof m.themes === "object" && m.themes !== null && typeof m.navigations === "object" && m.navigations !== null && typeof m.profiles === "object" && m.profiles !== null && typeof m.countries === "object" && m.countries !== null && typeof m.ranks === "object" && m.ranks !== null;
|
|
527
|
+
}
|
|
528
|
+
const MAPPINGS_FILE = "mappings.json";
|
|
529
|
+
/**
|
|
530
|
+
* Read the mappings file from the `.portal-sync/` directory.
|
|
531
|
+
* Returns `null` if the file does not exist.
|
|
532
|
+
*/
|
|
533
|
+
async function readMappings(portalSyncDir) {
|
|
534
|
+
try {
|
|
535
|
+
const filePath = join(portalSyncDir, MAPPINGS_FILE);
|
|
536
|
+
const content = await readFile(filePath, "utf-8");
|
|
537
|
+
const parsed = JSON.parse(content);
|
|
538
|
+
if (!isMappings(parsed)) throw new Error(`Malformed mappings file: ${filePath}`);
|
|
539
|
+
return parsed;
|
|
540
|
+
} catch (err) {
|
|
541
|
+
if (err.code === "ENOENT") return null;
|
|
542
|
+
throw err;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Write the mappings file to the `.portal-sync/` directory.
|
|
547
|
+
* Creates the directory if it does not exist.
|
|
548
|
+
*/
|
|
549
|
+
async function writeMappings(portalSyncDir, mappings) {
|
|
550
|
+
const filePath = join(portalSyncDir, MAPPINGS_FILE);
|
|
551
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
552
|
+
await atomicWriteFile(filePath, JSON.stringify(mappings, null, 2) + "\n");
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Derive a filesystem-safe slug from a resource name.
|
|
556
|
+
*
|
|
557
|
+
* - Lowercases the name
|
|
558
|
+
* - Replaces non-alphanumeric characters (except hyphens) with hyphens
|
|
559
|
+
* - Collapses consecutive hyphens
|
|
560
|
+
* - Trims leading/trailing hyphens
|
|
561
|
+
*
|
|
562
|
+
* If the derived slug collides with an existing slug in `existingSlugs`,
|
|
563
|
+
* a numeric suffix is appended (e.g., `home-2`, `home-3`).
|
|
564
|
+
*/
|
|
565
|
+
function deriveSlug(name, existingSlugs = /* @__PURE__ */ new Set()) {
|
|
566
|
+
const base = name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "unnamed";
|
|
567
|
+
if (!existingSlugs.has(base)) return base;
|
|
568
|
+
let counter = 2;
|
|
569
|
+
while (existingSlugs.has(`${base}-${counter}`)) counter++;
|
|
570
|
+
return `${base}-${counter}`;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Resolve a slug to its server-side ID.
|
|
574
|
+
* Returns `undefined` if the slug is not mapped.
|
|
575
|
+
*/
|
|
576
|
+
function resolveSlugToId(mappings, resourceType, slug) {
|
|
577
|
+
return mappings[resourceType][slug];
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Resolve a server-side ID back to its slug.
|
|
581
|
+
* Returns `undefined` if no slug maps to the given ID.
|
|
582
|
+
*/
|
|
583
|
+
function resolveIdToSlug(mappings, resourceType, id) {
|
|
584
|
+
const entries = mappings[resourceType];
|
|
585
|
+
for (const [slug, mappedId] of Object.entries(entries)) if (mappedId === id) return slug;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Add or update a mapping entry for a given resource type.
|
|
589
|
+
* Returns a new `PortalMappings` object (immutable update).
|
|
590
|
+
*/
|
|
591
|
+
function updateMapping(mappings, resourceType, slug, id) {
|
|
592
|
+
return {
|
|
593
|
+
...mappings,
|
|
594
|
+
[resourceType]: {
|
|
595
|
+
...mappings[resourceType],
|
|
596
|
+
[slug]: id
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Remove a mapping entry for a given resource type.
|
|
602
|
+
* Returns a new `PortalMappings` object (immutable update).
|
|
603
|
+
* If the slug does not exist, the original mappings are returned unchanged.
|
|
604
|
+
*/
|
|
605
|
+
function removeMapping(mappings, resourceType, slug) {
|
|
606
|
+
if (!(slug in mappings[resourceType])) return mappings;
|
|
607
|
+
const { [slug]: _removed, ...rest } = mappings[resourceType];
|
|
608
|
+
return {
|
|
609
|
+
...mappings,
|
|
610
|
+
[resourceType]: rest
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region src/utils/snapshot.ts
|
|
615
|
+
/**
|
|
616
|
+
* Hash-based snapshot system for portal content sync.
|
|
617
|
+
*
|
|
618
|
+
* Tracks file hashes to detect local changes since the last pull,
|
|
619
|
+
* enabling efficient push operations that only send modified files.
|
|
620
|
+
*
|
|
621
|
+
* Persisted in `.portal-sync/snapshot.json`.
|
|
622
|
+
*/
|
|
623
|
+
/** Maximum number of concurrent file operations. */
|
|
624
|
+
const MAX_CONCURRENCY = 20;
|
|
625
|
+
/**
|
|
626
|
+
* Run async tasks with bounded concurrency to avoid exhausting file descriptors.
|
|
627
|
+
*/
|
|
628
|
+
async function mapWithLimit(items, limit, fn) {
|
|
629
|
+
const results = new Array(items.length);
|
|
630
|
+
let index = 0;
|
|
631
|
+
async function worker() {
|
|
632
|
+
while (index < items.length) {
|
|
633
|
+
const i = index++;
|
|
634
|
+
results[i] = await fn(items[i]);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
|
|
638
|
+
await Promise.all(workers);
|
|
639
|
+
return results;
|
|
640
|
+
}
|
|
641
|
+
/** Runtime check that a parsed value has the expected Snapshot shape. */
|
|
642
|
+
function isSnapshot(value) {
|
|
643
|
+
if (!value || typeof value !== "object") return false;
|
|
644
|
+
const s = value;
|
|
645
|
+
return typeof s.definition === "string" && typeof s.definition_id === "number" && typeof s.pulled_at === "string" && typeof s.files === "object" && s.files !== null;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Compute the SHA-256 hash of a file's contents.
|
|
649
|
+
* Returns the hex-encoded digest.
|
|
650
|
+
*/
|
|
651
|
+
async function computeFileHash(filePath) {
|
|
652
|
+
const content = await readFile(filePath);
|
|
653
|
+
return createHash("sha256").update(content).digest("hex");
|
|
654
|
+
}
|
|
655
|
+
const SNAPSHOT_FILE = "snapshot.json";
|
|
656
|
+
/**
|
|
657
|
+
* Read the snapshot file from the `.portal-sync/` directory.
|
|
658
|
+
* Returns `null` if the file does not exist.
|
|
659
|
+
*/
|
|
660
|
+
async function readSnapshot(portalSyncDir) {
|
|
661
|
+
try {
|
|
662
|
+
const filePath = join(portalSyncDir, SNAPSHOT_FILE);
|
|
663
|
+
const content = await readFile(filePath, "utf-8");
|
|
664
|
+
const parsed = JSON.parse(content);
|
|
665
|
+
if (!isSnapshot(parsed)) throw new Error(`Malformed snapshot file: ${filePath}`);
|
|
666
|
+
return parsed;
|
|
667
|
+
} catch (err) {
|
|
668
|
+
if (err.code === "ENOENT") return null;
|
|
669
|
+
throw err;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Write the snapshot file to the `.portal-sync/` directory.
|
|
674
|
+
* Creates the directory if it does not exist.
|
|
675
|
+
*/
|
|
676
|
+
async function writeSnapshot(portalSyncDir, snapshot) {
|
|
677
|
+
const filePath = join(portalSyncDir, SNAPSHOT_FILE);
|
|
678
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
679
|
+
await atomicWriteFile(filePath, JSON.stringify(snapshot, null, 2) + "\n");
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Recursively collect all files in a directory, returning paths
|
|
683
|
+
* relative to `baseDir`.
|
|
684
|
+
*/
|
|
685
|
+
async function collectFiles(dir, baseDir = dir) {
|
|
686
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
687
|
+
const files = [];
|
|
688
|
+
for (const entry of entries) {
|
|
689
|
+
const fullPath = join(dir, entry.name);
|
|
690
|
+
if (entry.name === ".portal-sync") continue;
|
|
691
|
+
if (entry.isSymbolicLink()) {
|
|
692
|
+
const realStat = await stat(fullPath);
|
|
693
|
+
if (realStat.isDirectory()) files.push(...await collectFiles(fullPath, baseDir));
|
|
694
|
+
else if (realStat.isFile()) files.push(relative(baseDir, fullPath).split(sep).join("/"));
|
|
695
|
+
} else if (entry.isDirectory()) files.push(...await collectFiles(fullPath, baseDir));
|
|
696
|
+
else files.push(relative(baseDir, fullPath).split(sep).join("/"));
|
|
697
|
+
}
|
|
698
|
+
return files;
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Compare the current portal directory contents against a snapshot.
|
|
702
|
+
*
|
|
703
|
+
* Returns lists of new, changed, and deleted files (relative paths).
|
|
704
|
+
*/
|
|
705
|
+
async function diffAgainstSnapshot(portalDir, snapshot) {
|
|
706
|
+
const currentFiles = await collectFiles(portalDir);
|
|
707
|
+
const snapshotPaths = new Set(Object.keys(snapshot.files));
|
|
708
|
+
const newFiles = [];
|
|
709
|
+
const changedFiles = [];
|
|
710
|
+
await mapWithLimit(currentFiles, MAX_CONCURRENCY, async (filePath) => {
|
|
711
|
+
const hash = await computeFileHash(join(portalDir, filePath));
|
|
712
|
+
if (!snapshotPaths.has(filePath)) newFiles.push(filePath);
|
|
713
|
+
else if (snapshot.files[filePath] !== hash) changedFiles.push(filePath);
|
|
714
|
+
});
|
|
715
|
+
const currentSet = new Set(currentFiles);
|
|
716
|
+
const deletedFiles = [...snapshotPaths].filter((filePath) => !currentSet.has(filePath));
|
|
717
|
+
return {
|
|
718
|
+
new: [...newFiles].sort(),
|
|
719
|
+
changed: [...changedFiles].sort(),
|
|
720
|
+
deleted: [...deletedFiles].sort()
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Build a fresh snapshot from the current portal directory contents.
|
|
725
|
+
*
|
|
726
|
+
* Hashes every file in `portalDir` (excluding `.portal-sync/`)
|
|
727
|
+
* and records the definition metadata.
|
|
728
|
+
*/
|
|
729
|
+
async function buildSnapshot(portalDir, definitionName, definitionId) {
|
|
730
|
+
const hashEntries = await mapWithLimit(await collectFiles(portalDir), MAX_CONCURRENCY, async (filePath) => {
|
|
731
|
+
return [filePath, await computeFileHash(join(portalDir, filePath))];
|
|
732
|
+
});
|
|
733
|
+
hashEntries.sort(([a], [b]) => a.localeCompare(b));
|
|
734
|
+
const fileHashes = {};
|
|
735
|
+
for (const [path, hash] of hashEntries) fileHashes[path] = hash;
|
|
736
|
+
return {
|
|
737
|
+
definition: definitionName,
|
|
738
|
+
definition_id: definitionId,
|
|
739
|
+
pulled_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
740
|
+
files: fileHashes
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
//#endregion
|
|
744
|
+
//#region src/utils/transform.ts
|
|
745
|
+
/**
|
|
746
|
+
* Transform an API screen response into the local file format.
|
|
747
|
+
* Strips server-only fields and normalizes `component_tree` to always be an array.
|
|
748
|
+
*/
|
|
749
|
+
function transformScreen(screen) {
|
|
750
|
+
const name = screen.name ?? "";
|
|
751
|
+
const rawTree = screen.component_tree;
|
|
752
|
+
let componentTree;
|
|
753
|
+
if (rawTree == null) componentTree = [];
|
|
754
|
+
else if (Array.isArray(rawTree)) componentTree = rawTree;
|
|
755
|
+
else componentTree = [rawTree];
|
|
756
|
+
return {
|
|
757
|
+
name,
|
|
758
|
+
component_tree: componentTree
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Derive a slug for a screen. Screens use the existing `slug` field from the API.
|
|
763
|
+
* Falls back to slugifying the name if no slug exists.
|
|
764
|
+
*/
|
|
765
|
+
function deriveScreenSlug(screen, existingSlugs) {
|
|
766
|
+
if (screen.slug) {
|
|
767
|
+
const safe = screen.slug.replace(/[^a-z0-9_-]/gi, "-");
|
|
768
|
+
if (!existingSlugs.has(safe)) return safe;
|
|
769
|
+
return deriveSlug(safe, existingSlugs);
|
|
770
|
+
}
|
|
771
|
+
return deriveSlug(screen.name ?? "unnamed", existingSlugs);
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Transform an API theme response into the local file format.
|
|
775
|
+
* Passes theme config through as-is (hex conversion is a separate ticket).
|
|
776
|
+
*/
|
|
777
|
+
function transformTheme(theme) {
|
|
778
|
+
return {
|
|
779
|
+
name: theme.name ?? "",
|
|
780
|
+
config: theme.config ?? {},
|
|
781
|
+
active: theme.active ?? false
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Build an ID → slug lookup map by inverting a slug → id mapping.
|
|
786
|
+
*/
|
|
787
|
+
function buildIdToSlugMap(mappings) {
|
|
788
|
+
const map = /* @__PURE__ */ new Map();
|
|
789
|
+
for (const [slug, id] of Object.entries(mappings)) map.set(id, slug);
|
|
790
|
+
return map;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Transform API navigation items into local format.
|
|
794
|
+
* Converts `screen_id` to `screen` slug reference. Keeps `id` for sync.
|
|
795
|
+
*/
|
|
796
|
+
function transformNavigationItems(items, screenIdToSlug) {
|
|
797
|
+
return items.map((item) => ({
|
|
798
|
+
id: item.id,
|
|
799
|
+
icon: item.icon ?? null,
|
|
800
|
+
label: item.label ?? null,
|
|
801
|
+
screen: item.screen_id ? screenIdToSlug.get(item.screen_id) ?? null : null,
|
|
802
|
+
slug: item.slug ?? null,
|
|
803
|
+
source: item.source,
|
|
804
|
+
position: item.position ?? null,
|
|
805
|
+
parent_id: item.parent_id ?? null,
|
|
806
|
+
children: transformNavigationItems(item.children ?? [], screenIdToSlug)
|
|
807
|
+
}));
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Transform an API navigation with its items into the local file format.
|
|
811
|
+
*/
|
|
812
|
+
function transformNavigation(nav, items, screenIdToSlug) {
|
|
813
|
+
return {
|
|
814
|
+
name: nav.name ?? "",
|
|
815
|
+
platform: nav.platform,
|
|
816
|
+
navigation_items: transformNavigationItems(items, screenIdToSlug)
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
/** Typed alias for call-site clarity. */
|
|
820
|
+
const buildNavigationIdToSlugMap = buildIdToSlugMap;
|
|
821
|
+
/** Typed alias for call-site clarity. */
|
|
822
|
+
const buildThemeIdToSlugMap = buildIdToSlugMap;
|
|
823
|
+
/**
|
|
824
|
+
* Transform an API profile response into the local file format.
|
|
825
|
+
* Converts navigation_id, mobile_navigation_id, and theme_ids to slug references.
|
|
826
|
+
*
|
|
827
|
+
* TODO (REP-842): Country/rank IDs in profile permissions need to be translated
|
|
828
|
+
* to human-readable names via the main Fluid API. For now, the permission arrays
|
|
829
|
+
* are written as-is with integer IDs.
|
|
830
|
+
*/
|
|
831
|
+
function transformProfile(profile, navIdToSlug, themeIdToSlug) {
|
|
832
|
+
const navigation = profile.navigation ? navIdToSlug.get(profile.navigation.id) ?? null : null;
|
|
833
|
+
const mobileNavigation = profile.mobile_navigation ? navIdToSlug.get(profile.mobile_navigation.id) ?? null : null;
|
|
834
|
+
const themes = (profile.themes ?? []).map((t) => themeIdToSlug.get(t.id)).filter((slug) => slug != null);
|
|
835
|
+
const permissions = profile.permissions ?? {};
|
|
836
|
+
return {
|
|
837
|
+
name: profile.name ?? "",
|
|
838
|
+
default: profile.default ?? false,
|
|
839
|
+
navigation,
|
|
840
|
+
mobile_navigation: mobileNavigation,
|
|
841
|
+
themes,
|
|
842
|
+
permissions: {
|
|
843
|
+
ranks: permissions.ranks ?? [],
|
|
844
|
+
roles: permissions.roles ?? [],
|
|
845
|
+
platform: permissions.platform ?? [],
|
|
846
|
+
countries: permissions.countries ?? []
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
//#endregion
|
|
851
|
+
//#region src/commands/pull.ts
|
|
852
|
+
/**
|
|
853
|
+
* `fluid portal pull` command
|
|
854
|
+
*
|
|
855
|
+
* Pulls a Fluid OS definition's resources (screens, themes, navigations,
|
|
856
|
+
* profiles) from the API and writes them to the local `portal/` directory
|
|
857
|
+
* as JSON files, along with `.portal-sync/` metadata for push diffing.
|
|
858
|
+
*/
|
|
859
|
+
var pull_exports = /* @__PURE__ */ __exportAll({ pullCommand: () => pullCommand });
|
|
860
|
+
const PORTAL_DIR = "portal";
|
|
861
|
+
const PORTAL_SYNC_DIR = ".portal-sync";
|
|
862
|
+
const PAGE_LIMIT = 500;
|
|
863
|
+
/**
|
|
864
|
+
* Create an authenticated FetchClient using the stored CLI profile.
|
|
865
|
+
*/
|
|
866
|
+
function createClient() {
|
|
867
|
+
const token = getAuthToken();
|
|
868
|
+
if (!token) {
|
|
869
|
+
const profile = getActiveProfile();
|
|
870
|
+
if (!profile) throw new Error("Not logged in. Run " + chalk.cyan("fluid login") + " first.");
|
|
871
|
+
throw new Error("No auth token found for profile " + chalk.cyan(profile.name) + ". Run " + chalk.cyan("fluid login") + " to re-authenticate.");
|
|
872
|
+
}
|
|
873
|
+
return createFetchClient({
|
|
874
|
+
baseUrl: process.env["FLUID_API_BASE"] ?? "https://api.fluid.app",
|
|
875
|
+
getAuthToken: () => token
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Select a definition interactively or by name.
|
|
880
|
+
*/
|
|
881
|
+
async function fetchAllDefinitions(client) {
|
|
882
|
+
const all = [];
|
|
883
|
+
let page = 1;
|
|
884
|
+
while (true) {
|
|
885
|
+
const response = await listFluidOSDefinitions(client, {
|
|
886
|
+
page,
|
|
887
|
+
per_page: PAGE_LIMIT
|
|
888
|
+
});
|
|
889
|
+
const batch = response.definitions ?? [];
|
|
890
|
+
all.push(...batch);
|
|
891
|
+
const totalPages = response.meta?.total_pages ?? 1;
|
|
892
|
+
if (page >= totalPages || batch.length === 0) break;
|
|
893
|
+
page++;
|
|
894
|
+
}
|
|
895
|
+
return all;
|
|
896
|
+
}
|
|
897
|
+
async function selectDefinition(client, appName) {
|
|
898
|
+
const definitions = await fetchAllDefinitions(client);
|
|
899
|
+
if (definitions.length === 0) throw new Error("No Fluid OS definitions found. Create one in the admin dashboard first.");
|
|
900
|
+
if (appName) {
|
|
901
|
+
const match = definitions.find((d) => d.name?.toLowerCase() === appName.toLowerCase());
|
|
902
|
+
if (!match) {
|
|
903
|
+
const availableNames = definitions.map((d) => chalk.cyan(d.name ?? "(unnamed)")).join(", ");
|
|
904
|
+
throw new Error(`No definition found matching "${appName}". Available: ${availableNames}`);
|
|
905
|
+
}
|
|
906
|
+
return match;
|
|
907
|
+
}
|
|
908
|
+
const { definitionId } = await prompts({
|
|
909
|
+
type: "select",
|
|
910
|
+
name: "definitionId",
|
|
911
|
+
message: "Select a Fluid OS definition to pull",
|
|
912
|
+
choices: definitions.map((d) => ({
|
|
913
|
+
title: d.name ?? "(unnamed)",
|
|
914
|
+
value: d.id
|
|
915
|
+
}))
|
|
916
|
+
});
|
|
917
|
+
if (definitionId == null) throw new Error("No definition selected.");
|
|
918
|
+
const selected = definitions.find((d) => d.id === definitionId);
|
|
919
|
+
if (!selected) throw new Error("Selected definition not found.");
|
|
920
|
+
return selected;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Check for definition switching conflicts.
|
|
924
|
+
* Returns true if it's safe to proceed.
|
|
925
|
+
*/
|
|
926
|
+
async function checkDefinitionSwitch(cwd, newDefinitionId, newDefinitionName, force) {
|
|
927
|
+
const portalDir = join(cwd, PORTAL_DIR);
|
|
928
|
+
const portalSyncDir = join(cwd, PORTAL_SYNC_DIR);
|
|
929
|
+
if (!existsSync(portalDir)) return true;
|
|
930
|
+
const snapshot = await readSnapshot(portalSyncDir);
|
|
931
|
+
if (!snapshot) return true;
|
|
932
|
+
if (snapshot.definition_id === newDefinitionId) return true;
|
|
933
|
+
const diff = await diffAgainstSnapshot(portalDir, snapshot);
|
|
934
|
+
if ((diff.new.length > 0 || diff.changed.length > 0 || diff.deleted.length > 0) && !force) {
|
|
935
|
+
console.log();
|
|
936
|
+
console.log(chalk.yellow("Warning:") + " Switching from " + chalk.cyan(snapshot.definition) + " to " + chalk.cyan(newDefinitionName) + " with unpushed local changes.");
|
|
937
|
+
console.log();
|
|
938
|
+
console.log(" Modified: " + diff.changed.length + " file(s)");
|
|
939
|
+
console.log(" New: " + diff.new.length + " file(s)");
|
|
940
|
+
console.log(" Deleted: " + diff.deleted.length + " file(s)");
|
|
941
|
+
console.log();
|
|
942
|
+
console.log("Use " + chalk.cyan("--force") + " to discard local changes and switch definitions.");
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Write a JSON file to the portal directory, creating subdirectories as needed.
|
|
949
|
+
*/
|
|
950
|
+
async function writePortalFile(portalDir, relativePath, data) {
|
|
951
|
+
const filePath = join(portalDir, relativePath);
|
|
952
|
+
await mkdir(join(filePath, ".."), { recursive: true });
|
|
953
|
+
await writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
954
|
+
}
|
|
955
|
+
const pullCommand = new Command("pull").description("Pull a Fluid OS definition's resources to the local portal/ directory").option("--app <name>", "Definition name (skips interactive selector)").option("--force", "Overwrite local changes when switching definitions").action(async (options) => {
|
|
956
|
+
const cwd = process.cwd();
|
|
957
|
+
console.log();
|
|
958
|
+
console.log(chalk.blue.bold("Fluid Portal Pull"));
|
|
959
|
+
console.log();
|
|
960
|
+
const spinner = ora();
|
|
961
|
+
spinner.start("Authenticating...");
|
|
962
|
+
let client;
|
|
963
|
+
try {
|
|
964
|
+
client = createClient();
|
|
965
|
+
spinner.succeed("Authenticated");
|
|
966
|
+
} catch (err) {
|
|
967
|
+
spinner.fail("Authentication failed");
|
|
968
|
+
console.log();
|
|
969
|
+
console.log(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
|
|
970
|
+
console.log();
|
|
971
|
+
process.exit(1);
|
|
972
|
+
}
|
|
973
|
+
let definition;
|
|
974
|
+
try {
|
|
975
|
+
definition = await selectDefinition(client, options.app);
|
|
976
|
+
} catch (err) {
|
|
977
|
+
console.log();
|
|
978
|
+
console.log(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
|
|
979
|
+
console.log();
|
|
980
|
+
process.exit(1);
|
|
981
|
+
}
|
|
982
|
+
const definitionId = definition.id;
|
|
983
|
+
const definitionName = definition.name ?? "(unnamed)";
|
|
984
|
+
console.log(chalk.gray("Definition: ") + chalk.white(definitionName) + chalk.gray(` (ID: ${definitionId})`));
|
|
985
|
+
console.log();
|
|
986
|
+
let canProceed;
|
|
987
|
+
try {
|
|
988
|
+
canProceed = await checkDefinitionSwitch(cwd, definitionId, definitionName, options.force ?? false);
|
|
989
|
+
} catch (err) {
|
|
990
|
+
console.log();
|
|
991
|
+
console.log(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
|
|
992
|
+
console.log();
|
|
993
|
+
process.exit(1);
|
|
994
|
+
}
|
|
995
|
+
if (!canProceed) process.exit(1);
|
|
996
|
+
spinner.start("Fetching resources...");
|
|
997
|
+
let screenBasics;
|
|
998
|
+
let themes;
|
|
999
|
+
let navigations;
|
|
1000
|
+
let profiles;
|
|
1001
|
+
let screens;
|
|
1002
|
+
const navigationItemsMap = /* @__PURE__ */ new Map();
|
|
1003
|
+
try {
|
|
1004
|
+
const [screensResponse, themesResponse, navigationsResponse, profilesResponse] = await Promise.all([
|
|
1005
|
+
listFluidOSScreens(client, definitionId, { per_page: PAGE_LIMIT }),
|
|
1006
|
+
listFluidOSThemes(client, definitionId, { per_page: PAGE_LIMIT }),
|
|
1007
|
+
listFluidOSNavigations(client, definitionId, { per_page: PAGE_LIMIT }),
|
|
1008
|
+
listFluidOSProfiles(client, definitionId, { per_page: PAGE_LIMIT })
|
|
1009
|
+
]);
|
|
1010
|
+
screenBasics = screensResponse.screens ?? [];
|
|
1011
|
+
themes = themesResponse.themes ?? [];
|
|
1012
|
+
navigations = navigationsResponse.navigations ?? [];
|
|
1013
|
+
profiles = profilesResponse.profiles ?? [];
|
|
1014
|
+
if (screenBasics.length === PAGE_LIMIT) console.warn(chalk.yellow(`Warning: screen count hit the ${PAGE_LIMIT}-item limit; some screens may be missing.`));
|
|
1015
|
+
if (themes.length === PAGE_LIMIT) console.warn(chalk.yellow(`Warning: theme count hit the ${PAGE_LIMIT}-item limit; some themes may be missing.`));
|
|
1016
|
+
if (navigations.length === PAGE_LIMIT) console.warn(chalk.yellow(`Warning: navigation count hit the ${PAGE_LIMIT}-item limit; some navigations may be missing.`));
|
|
1017
|
+
if (profiles.length === PAGE_LIMIT) console.warn(chalk.yellow(`Warning: profile count hit the ${PAGE_LIMIT}-item limit; some profiles may be missing.`));
|
|
1018
|
+
spinner.text = "Fetching screen details...";
|
|
1019
|
+
const limit = pLimit(10);
|
|
1020
|
+
screens = await Promise.all(screenBasics.map((s) => limit(async () => {
|
|
1021
|
+
const res = await getFluidOSScreen(client, definitionId, s.id);
|
|
1022
|
+
if (!res.screen) throw new Error(`Failed to fetch details for screen ID ${s.id}`);
|
|
1023
|
+
return res.screen;
|
|
1024
|
+
})));
|
|
1025
|
+
spinner.text = "Fetching navigation items...";
|
|
1026
|
+
await Promise.all(navigations.map((nav) => limit(async () => {
|
|
1027
|
+
const res = await listFluidOSNavigationItems(client, definitionId, nav.id);
|
|
1028
|
+
navigationItemsMap.set(nav.id, res.navigation_items ?? []);
|
|
1029
|
+
})));
|
|
1030
|
+
spinner.succeed(`Fetched ${screens.length} screen(s), ${themes.length} theme(s), ${navigations.length} navigation(s), ${profiles.length} profile(s)`);
|
|
1031
|
+
} catch (err) {
|
|
1032
|
+
spinner.fail("Failed to fetch resources");
|
|
1033
|
+
console.log();
|
|
1034
|
+
console.log(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
|
|
1035
|
+
console.log();
|
|
1036
|
+
process.exit(1);
|
|
1037
|
+
}
|
|
1038
|
+
spinner.start("Transforming resources...");
|
|
1039
|
+
const screenSlugs = /* @__PURE__ */ new Set();
|
|
1040
|
+
const screenMappings = {};
|
|
1041
|
+
for (const screen of screens) {
|
|
1042
|
+
const slug = deriveScreenSlug(screen, screenSlugs);
|
|
1043
|
+
screenSlugs.add(slug);
|
|
1044
|
+
screenMappings[slug] = screen.id;
|
|
1045
|
+
}
|
|
1046
|
+
const themeSlugs = /* @__PURE__ */ new Set();
|
|
1047
|
+
const themeMappings = {};
|
|
1048
|
+
for (const theme of themes) {
|
|
1049
|
+
const slug = deriveSlug(theme.name ?? "unnamed", themeSlugs);
|
|
1050
|
+
themeSlugs.add(slug);
|
|
1051
|
+
themeMappings[slug] = theme.id;
|
|
1052
|
+
}
|
|
1053
|
+
const navSlugs = /* @__PURE__ */ new Set();
|
|
1054
|
+
const navMappings = {};
|
|
1055
|
+
for (const nav of navigations) {
|
|
1056
|
+
const slug = deriveSlug(nav.name ?? "unnamed", navSlugs);
|
|
1057
|
+
navSlugs.add(slug);
|
|
1058
|
+
navMappings[slug] = nav.id;
|
|
1059
|
+
}
|
|
1060
|
+
const profileSlugs = /* @__PURE__ */ new Set();
|
|
1061
|
+
const profileMappings = {};
|
|
1062
|
+
for (const profile of profiles) {
|
|
1063
|
+
const slug = deriveSlug(profile.name ?? "unnamed", profileSlugs);
|
|
1064
|
+
profileSlugs.add(slug);
|
|
1065
|
+
profileMappings[slug] = profile.id;
|
|
1066
|
+
}
|
|
1067
|
+
const screenIdToSlug = buildIdToSlugMap(screenMappings);
|
|
1068
|
+
const navIdToSlug = buildNavigationIdToSlugMap(navMappings);
|
|
1069
|
+
const themeIdToSlug = buildThemeIdToSlugMap(themeMappings);
|
|
1070
|
+
const profileIdToSlug = buildIdToSlugMap(profileMappings);
|
|
1071
|
+
spinner.succeed("Resources transformed");
|
|
1072
|
+
spinner.start("Writing files...");
|
|
1073
|
+
const portalDir = join(cwd, PORTAL_DIR);
|
|
1074
|
+
const portalSyncDir = join(cwd, PORTAL_SYNC_DIR);
|
|
1075
|
+
const tmpPortalDir = portalDir + ".tmp";
|
|
1076
|
+
try {
|
|
1077
|
+
if (existsSync(tmpPortalDir)) await rm(tmpPortalDir, { recursive: true });
|
|
1078
|
+
await mkdir(tmpPortalDir, { recursive: true });
|
|
1079
|
+
await writePortalFile(tmpPortalDir, "definition.json", { name: definitionName });
|
|
1080
|
+
for (const screen of screens) {
|
|
1081
|
+
const slug = screenIdToSlug.get(screen.id);
|
|
1082
|
+
const local = transformScreen(screen);
|
|
1083
|
+
await writePortalFile(tmpPortalDir, `screens/${slug}.json`, local);
|
|
1084
|
+
}
|
|
1085
|
+
for (const theme of themes) {
|
|
1086
|
+
const slug = themeIdToSlug.get(theme.id);
|
|
1087
|
+
const local = transformTheme(theme);
|
|
1088
|
+
await writePortalFile(tmpPortalDir, `themes/${slug}.json`, local);
|
|
1089
|
+
}
|
|
1090
|
+
for (const nav of navigations) {
|
|
1091
|
+
const slug = navIdToSlug.get(nav.id);
|
|
1092
|
+
const local = transformNavigation(nav, navigationItemsMap.get(nav.id) ?? [], screenIdToSlug);
|
|
1093
|
+
await writePortalFile(tmpPortalDir, `navigations/${slug}.json`, local);
|
|
1094
|
+
}
|
|
1095
|
+
for (const profile of profiles) {
|
|
1096
|
+
const slug = profileIdToSlug.get(profile.id);
|
|
1097
|
+
if (slug) {
|
|
1098
|
+
const local = transformProfile(profile, navIdToSlug, themeIdToSlug);
|
|
1099
|
+
await writePortalFile(tmpPortalDir, `profiles/${slug}.json`, local);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
if (existsSync(portalDir)) await rm(portalDir, { recursive: true });
|
|
1103
|
+
await rename(tmpPortalDir, portalDir);
|
|
1104
|
+
} catch (err) {
|
|
1105
|
+
if (existsSync(tmpPortalDir)) await rm(tmpPortalDir, { recursive: true }).catch(() => {});
|
|
1106
|
+
spinner.fail("Failed to write files");
|
|
1107
|
+
console.log();
|
|
1108
|
+
console.log(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
|
|
1109
|
+
console.log();
|
|
1110
|
+
process.exit(1);
|
|
1111
|
+
}
|
|
1112
|
+
try {
|
|
1113
|
+
await writeMappings(portalSyncDir, {
|
|
1114
|
+
definition: {
|
|
1115
|
+
name: definitionName,
|
|
1116
|
+
id: definitionId
|
|
1117
|
+
},
|
|
1118
|
+
screens: screenMappings,
|
|
1119
|
+
themes: themeMappings,
|
|
1120
|
+
navigations: navMappings,
|
|
1121
|
+
profiles: profileMappings,
|
|
1122
|
+
countries: {},
|
|
1123
|
+
ranks: {}
|
|
1124
|
+
});
|
|
1125
|
+
await writeSnapshot(portalSyncDir, await buildSnapshot(portalDir, definitionName, definitionId));
|
|
1126
|
+
spinner.succeed("Files written");
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
spinner.warn("Files written (metadata sync failed)");
|
|
1129
|
+
console.log();
|
|
1130
|
+
console.log(chalk.yellow("Warning:") + " Portal files were written successfully, but metadata sync failed. Run " + chalk.cyan("fluid portal pull") + " again to regenerate metadata.");
|
|
1131
|
+
console.log(chalk.gray(" Detail: ") + (err instanceof Error ? err.message : String(err)));
|
|
1132
|
+
console.log();
|
|
1133
|
+
}
|
|
1134
|
+
console.log();
|
|
1135
|
+
console.log(chalk.green.bold("Pull complete!"));
|
|
1136
|
+
console.log();
|
|
1137
|
+
console.log(chalk.gray(" portal/definition.json"));
|
|
1138
|
+
console.log(chalk.gray(` portal/screens/ `) + chalk.white(`${screens.length} file(s)`));
|
|
1139
|
+
console.log(chalk.gray(` portal/themes/ `) + chalk.white(`${themes.length} file(s)`));
|
|
1140
|
+
console.log(chalk.gray(` portal/navigations/ `) + chalk.white(`${navigations.length} file(s)`));
|
|
1141
|
+
console.log(chalk.gray(` portal/profiles/ `) + chalk.white(`${profiles.length} file(s)`));
|
|
1142
|
+
console.log(chalk.gray(` .portal-sync/ `) + chalk.white("mappings.json + snapshot.json"));
|
|
1143
|
+
console.log();
|
|
1144
|
+
});
|
|
1145
|
+
//#endregion
|
|
1146
|
+
export { deleteFluidOSNavigation as A, updateFluidOSScreen as B, writeMappings as C, createFluidOSScreen as D, createFluidOSProfile as E, listFluidOSNavigationItems as F, updateFluidOSVersion as H, listFluidOSVersions as I, updateFluidOSNavigation as L, deleteFluidOSProfile as M, deleteFluidOSScreen as N, createFluidOSTheme as O, deleteFluidOSTheme as P, updateFluidOSNavigationItem as R, updateMapping as S, createFluidOSNavigationItem as T, createFetchClient as U, updateFluidOSTheme as V, deriveSlug as _, buildThemeIdToSlugMap as a, resolveIdToSlug as b, transformNavigationItems as c, transformTheme as d, buildSnapshot as f, writeSnapshot as g, readSnapshot as h, buildNavigationIdToSlugMap as i, deleteFluidOSNavigationItem as j, createFluidOSVersion as k, transformProfile as l, diffAgainstSnapshot as m, pull_exports as n, deriveScreenSlug as o, computeFileHash as p, buildIdToSlugMap as r, transformNavigation as s, pullCommand as t, transformScreen as u, readMappings as v, createFluidOSNavigation as w, resolveSlugToId as x, removeMapping as y, updateFluidOSProfile as z };
|
|
1147
|
+
|
|
1148
|
+
//# sourceMappingURL=pull-p1mSVa5W.mjs.map
|