@fluid-app/fluid-cli-portal 0.1.53 → 0.1.54

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