@fluid-app/fluid-cli-portal 0.1.57 → 0.1.58

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,1633 +0,0 @@
1
- import { t as __exportAll } from "./rolldown-runtime-twds-ZHy.mjs";
2
- import { D as fluidOsV0UpdateFluidOsscreen, E as fluidOsV0UpdateFluidOsprofile, O as fluidOsV0UpdateFluidOstheme, S as fluidOsV0ListFluidOsversions, T as fluidOsV0UpdateFluidOsnavigationItem, _ as fluidOsV0ListFluidOsnavigationItems, a as fluidOsV0CreateFluidOsscreen, b as fluidOsV0ListFluidOsscreens, d as fluidOsV0DeleteFluidOsnavigationItem, f as fluidOsV0DeleteFluidOsprofile, g as fluidOsV0ListFluidOsdefinitions, h as fluidOsV0GetFluidOsscreen, i as fluidOsV0CreateFluidOsprofile, k as fluidOsV0UpdateFluidOsversion, m as fluidOsV0DeleteFluidOstheme, n as fluidOsV0CreateFluidOsnavigation, o as fluidOsV0CreateFluidOstheme, p as fluidOsV0DeleteFluidOsscreen, r as fluidOsV0CreateFluidOsnavigationItem, s as fluidOsV0CreateFluidOsversion, u as fluidOsV0DeleteFluidOsnavigation, v as fluidOsV0ListFluidOsnavigations, w as fluidOsV0UpdateFluidOsnavigation, x as fluidOsV0ListFluidOsthemes, y as fluidOsV0ListFluidOsprofiles } from "./sdk.gen-DuPhfdxN.mjs";
3
- import { Command } from "commander";
4
- import chalk from "chalk";
5
- import ora from "ora";
6
- import { dirname, join, relative, sep } from "node:path";
7
- import { existsSync } from "node:fs";
8
- import { lstat, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
9
- import { getActiveProfile, getAuthToken } from "@fluid-app/fluid-cli";
10
- import prompts from "prompts";
11
- import { createHash, randomBytes } from "node:crypto";
12
- import pLimit from "p-limit";
13
- //#region src/utils/standalone-portal.ts
14
- const STANDALONE_PORTAL_MARKER = ".fluid-portal.json";
15
- /**
16
- * Standalone portal definitions never synchronize with Fluid OS. Check only
17
- * for marker presence here: an unreadable or malformed marker must not turn a
18
- * local-only project into an accidental remote target.
19
- */
20
- async function assertStandalonePortalRemoteActionAllowed(projectDirectory, action) {
21
- try {
22
- await lstat(join(projectDirectory, STANDALONE_PORTAL_MARKER));
23
- } catch (error) {
24
- if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return;
25
- throw error;
26
- }
27
- throw new Error(`This is a standalone Portal SDK project. It is local-only in Fluid; portal/ is the source of truth, and Fluid portal pull, push, version, and publish commands do not apply. Local SDK portal publishing will be added with Vercel hosting. ${action} was refused.`);
28
- }
29
- //#endregion
30
- //#region src/commands/pull-definition.ts
31
- /** Pick a definition by explicit id, else by name.
32
- *
33
- * Mirrors clone.ts's resolver of the same name, and for the same
34
- * reason: definition NAMES REPEAT. clone's `--id` help says so
35
- * outright ("avoids ambiguity when names repeat") and clone.test.ts
36
- * pins it. pull only ever looked up by name — first match wins — so a
37
- * caller holding an unambiguous id had no way to use it, and Mist
38
- * called pull by name immediately after cloning by id.
39
- *
40
- * Returns null when neither is supplied, so the caller falls through
41
- * to the interactive selector. */
42
- function resolveDefinition(definitions, appName, rawDefinitionId) {
43
- if (rawDefinitionId !== void 0) {
44
- if (!/^[1-9]\d*$/.test(rawDefinitionId)) throw new Error("--id must be a positive integer");
45
- const definitionId = Number.parseInt(rawDefinitionId, 10);
46
- const match = definitions.find((d) => d.id === definitionId);
47
- if (!match) throw new Error(`No definition found with id ${definitionId}.`);
48
- return match;
49
- }
50
- if (appName) {
51
- const match = definitions.find((d) => d.name?.toLowerCase() === appName.toLowerCase());
52
- if (!match) {
53
- const availableNames = definitions.map((d) => chalk.cyan(d.name ?? "(unnamed)")).join(", ");
54
- throw new Error(`No definition found matching "${appName}". Available: ${availableNames}`);
55
- }
56
- return match;
57
- }
58
- return null;
59
- }
60
- //#endregion
61
- //#region ../../platform/api-client-core/src/api-error-shape.ts
62
- /**
63
- * Narrows a parsed JSON error payload to the envelope contract. Arrays and
64
- * primitives are not envelopes, so they resolve to `null` — their content is
65
- * still reachable through `ApiError.data`.
66
- */
67
- function toApiErrorBody(value) {
68
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
69
- return value;
70
- }
71
- /**
72
- * The single unwrap rule, shared by every producer of `ApiError`.
73
- *
74
- * `??` rather than `||` so an explicitly empty `errors` (`""`, `0`, `false`)
75
- * is preserved instead of silently falling back to the whole envelope.
76
- */
77
- function toApiErrorData(body) {
78
- if (!body) return null;
79
- return body.errors ?? body;
80
- }
81
- //#endregion
82
- //#region ../../platform/api-client-core/src/fetch-client.ts
83
- /**
84
- * API Error class compatible with fluid-admin's ApiError
85
- */
86
- var ApiError = class ApiError extends Error {
87
- status;
88
- /**
89
- * The unwrapped field-error bag. See `ApiErrorData`; the contract is
90
- * documented in `docs/api-error-data-contract.md`.
91
- */
92
- data;
93
- /**
94
- * The full error response envelope. Read `error_message`, `error`, `status`
95
- * and nested `errors` from here — `data` has already unwrapped one level.
96
- */
97
- body;
98
- requestId;
99
- constructor(message, status, data, requestId, body) {
100
- super(message);
101
- this.name = "ApiError";
102
- this.status = status;
103
- this.data = data ?? null;
104
- this.body = body ?? null;
105
- this.requestId = requestId;
106
- if ("captureStackTrace" in Error) Error.captureStackTrace(this, ApiError);
107
- }
108
- toJSON() {
109
- return {
110
- name: this.name,
111
- message: this.message,
112
- status: this.status,
113
- data: this.data,
114
- body: this.body,
115
- requestId: this.requestId
116
- };
117
- }
118
- };
119
- //#endregion
120
- //#region ../../platform/api-client-core/src/hey-api-error-policy.ts
121
- function getNonEmptyString(value) {
122
- if (typeof value !== "string") return;
123
- const trimmed = value.trim();
124
- return trimmed.length > 0 ? trimmed : void 0;
125
- }
126
- function getRequestId(response, error) {
127
- const meta = toApiErrorBody(error?.meta);
128
- return getNonEmptyString(response.headers.get("x-request-id")) ?? getNonEmptyString(response.headers.get("request-id")) ?? getNonEmptyString(error?.request_id) ?? getNonEmptyString(error?.requestId) ?? getNonEmptyString(meta?.request_id) ?? getNonEmptyString(meta?.requestId);
129
- }
130
- function getErrorMessage(error, fallback) {
131
- const nestedError = toApiErrorBody(error.error);
132
- return getNonEmptyString(error.message) ?? getNonEmptyString(error.error_message) ?? getNonEmptyString(nestedError?.message) ?? fallback;
133
- }
134
- /**
135
- * Creates an error interceptor compatible with generated Hey API fetch clients.
136
- * Consumer-specific auth and cancellation policy is supplied through callbacks,
137
- * keeping this package independent from generated client types.
138
- */
139
- function createHeyApiErrorInterceptor(options = {}) {
140
- return (error, response, request, requestOptions) => {
141
- if (!response) {
142
- if (error instanceof ApiError) return error;
143
- if (options.shouldPassThroughTransportError?.(error)) return error;
144
- if (!request) return error;
145
- return new ApiError(`Network error: ${error instanceof Error ? error.message : "Unknown network error"}`, 0, null);
146
- }
147
- if (response.ok && error instanceof SyntaxError) return new ApiError("Failed to parse response as JSON", response.status, null, getRequestId(response, null));
148
- options.onResponseError?.(response);
149
- const method = request?.method ?? requestOptions.method ?? "GET";
150
- const errorRecord = toApiErrorBody(error);
151
- const requestId = getRequestId(response, errorRecord);
152
- if (errorRecord) return new ApiError(getErrorMessage(errorRecord, `${method} request failed`), response.status, toApiErrorData(errorRecord), requestId, errorRecord);
153
- const isJson = response.headers.get("content-type")?.includes("application/json");
154
- const text = getNonEmptyString(error);
155
- return new ApiError(isJson && text ? text.slice(0, 200) : `${method} request failed with status ${response.status}`, response.status, isJson ? error : null, requestId, null);
156
- };
157
- }
158
- //#endregion
159
- //#region ../../api-clients/hey-api-fetch-runtime/src/generated/core/bodySerializer.gen.ts
160
- const jsonBodySerializer = { bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) };
161
- Object.entries({
162
- $body_: "body",
163
- $headers_: "headers",
164
- $path_: "path",
165
- $query_: "query"
166
- });
167
- //#endregion
168
- //#region ../../api-clients/hey-api-fetch-runtime/src/generated/core/serverSentEvents.gen.ts
169
- function createSseClient({ onRequest, onSseError, onSseEvent, responseTransformer, responseValidator, sseDefaultRetryDelay, sseMaxRetryAttempts, sseMaxRetryDelay, sseSleepFn, url, ...options }) {
170
- let lastEventId;
171
- const sleep = sseSleepFn ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
172
- const createStream = async function* () {
173
- let retryDelay = sseDefaultRetryDelay ?? 3e3;
174
- let attempt = 0;
175
- const signal = options.signal ?? new AbortController().signal;
176
- while (true) {
177
- if (signal.aborted) break;
178
- attempt++;
179
- const headers = options.headers instanceof Headers ? options.headers : new Headers(options.headers);
180
- if (lastEventId !== void 0) headers.set("Last-Event-ID", lastEventId);
181
- try {
182
- const requestInit = {
183
- redirect: "follow",
184
- ...options,
185
- body: options.serializedBody,
186
- headers,
187
- signal
188
- };
189
- let request = new Request(url, requestInit);
190
- if (onRequest) request = await onRequest(url, requestInit);
191
- const response = await (options.fetch ?? globalThis.fetch)(request);
192
- if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`);
193
- if (!response.body) throw new Error("No body in SSE response");
194
- const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
195
- let buffer = "";
196
- const abortHandler = () => {
197
- try {
198
- reader.cancel();
199
- } catch {}
200
- };
201
- signal.addEventListener("abort", abortHandler);
202
- try {
203
- while (true) {
204
- const { done, value } = await reader.read();
205
- if (done) break;
206
- buffer += value;
207
- buffer = buffer.replace(/\r\n?/g, "\n");
208
- const chunks = buffer.split("\n\n");
209
- buffer = chunks.pop() ?? "";
210
- for (const chunk of chunks) {
211
- const lines = chunk.split("\n");
212
- const dataLines = [];
213
- let eventName;
214
- for (const line of lines) if (line.startsWith("data:")) dataLines.push(line.replace(/^data:\s*/, ""));
215
- else if (line.startsWith("event:")) eventName = line.replace(/^event:\s*/, "");
216
- else if (line.startsWith("id:")) lastEventId = line.replace(/^id:\s*/, "");
217
- else if (line.startsWith("retry:")) {
218
- const parsed = Number.parseInt(line.replace(/^retry:\s*/, ""), 10);
219
- if (!Number.isNaN(parsed)) retryDelay = parsed;
220
- }
221
- let data;
222
- let parsedJson = false;
223
- if (dataLines.length) {
224
- const rawData = dataLines.join("\n");
225
- try {
226
- data = JSON.parse(rawData);
227
- parsedJson = true;
228
- } catch {
229
- data = rawData;
230
- }
231
- }
232
- if (parsedJson) {
233
- if (responseValidator) await responseValidator(data);
234
- if (responseTransformer) data = await responseTransformer(data);
235
- }
236
- onSseEvent?.({
237
- data,
238
- event: eventName,
239
- id: lastEventId,
240
- retry: retryDelay
241
- });
242
- if (dataLines.length) yield data;
243
- }
244
- }
245
- } finally {
246
- signal.removeEventListener("abort", abortHandler);
247
- reader.releaseLock();
248
- }
249
- break;
250
- } catch (error) {
251
- onSseError?.(error);
252
- if (sseMaxRetryAttempts !== void 0 && attempt >= sseMaxRetryAttempts) break;
253
- await sleep(Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 3e4));
254
- }
255
- }
256
- };
257
- return { stream: createStream() };
258
- }
259
- //#endregion
260
- //#region ../../api-clients/hey-api-fetch-runtime/src/generated/core/pathSerializer.gen.ts
261
- const separatorArrayExplode = (style) => {
262
- switch (style) {
263
- case "label": return ".";
264
- case "matrix": return ";";
265
- case "simple": return ",";
266
- default: return "&";
267
- }
268
- };
269
- const separatorArrayNoExplode = (style) => {
270
- switch (style) {
271
- case "form": return ",";
272
- case "pipeDelimited": return "|";
273
- case "spaceDelimited": return "%20";
274
- default: return ",";
275
- }
276
- };
277
- const separatorObjectExplode = (style) => {
278
- switch (style) {
279
- case "label": return ".";
280
- case "matrix": return ";";
281
- case "simple": return ",";
282
- default: return "&";
283
- }
284
- };
285
- const serializeArrayParam = ({ allowReserved, explode, name, style, value }) => {
286
- if (!explode) {
287
- const joinedValues = (allowReserved ? value : value.map((v) => encodeURIComponent(v))).join(separatorArrayNoExplode(style));
288
- switch (style) {
289
- case "label": return `.${joinedValues}`;
290
- case "matrix": return `;${name}=${joinedValues}`;
291
- case "simple": return joinedValues;
292
- default: return `${name}=${joinedValues}`;
293
- }
294
- }
295
- const separator = separatorArrayExplode(style);
296
- const joinedValues = value.map((v) => {
297
- if (style === "label" || style === "simple") return allowReserved ? v : encodeURIComponent(v);
298
- return serializePrimitiveParam({
299
- allowReserved,
300
- name,
301
- value: v
302
- });
303
- }).join(separator);
304
- return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
305
- };
306
- const serializePrimitiveParam = ({ allowReserved, name, value }) => {
307
- if (value === void 0 || value === null) return "";
308
- if (typeof value === "object") throw new Error("Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.");
309
- return `${name}=${allowReserved ? value : encodeURIComponent(value)}`;
310
- };
311
- const serializeObjectParam = ({ allowReserved, explode, name, style, value, valueOnly }) => {
312
- if (value instanceof Date) return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`;
313
- if (style !== "deepObject" && !explode) {
314
- let values = [];
315
- Object.entries(value).forEach(([key, v]) => {
316
- values = [
317
- ...values,
318
- key,
319
- allowReserved ? v : encodeURIComponent(v)
320
- ];
321
- });
322
- const joinedValues = values.join(",");
323
- switch (style) {
324
- case "form": return `${name}=${joinedValues}`;
325
- case "label": return `.${joinedValues}`;
326
- case "matrix": return `;${name}=${joinedValues}`;
327
- default: return joinedValues;
328
- }
329
- }
330
- const separator = separatorObjectExplode(style);
331
- const joinedValues = Object.entries(value).map(([key, v]) => serializePrimitiveParam({
332
- allowReserved,
333
- name: style === "deepObject" ? `${name}[${key}]` : key,
334
- value: v
335
- })).join(separator);
336
- return style === "label" || style === "matrix" ? separator + joinedValues : joinedValues;
337
- };
338
- //#endregion
339
- //#region ../../api-clients/hey-api-fetch-runtime/src/generated/core/utils.gen.ts
340
- const PATH_PARAM_RE = /\{[^{}]+\}/g;
341
- const defaultPathSerializer = ({ path, url: _url }) => {
342
- let url = _url;
343
- const matches = _url.match(PATH_PARAM_RE);
344
- if (matches) for (const match of matches) {
345
- let explode = false;
346
- let name = match.substring(1, match.length - 1);
347
- let style = "simple";
348
- if (name.endsWith("*")) {
349
- explode = true;
350
- name = name.substring(0, name.length - 1);
351
- }
352
- if (name.startsWith(".")) {
353
- name = name.substring(1);
354
- style = "label";
355
- } else if (name.startsWith(";")) {
356
- name = name.substring(1);
357
- style = "matrix";
358
- }
359
- const value = path[name];
360
- if (value === void 0 || value === null) continue;
361
- if (Array.isArray(value)) {
362
- url = url.replace(match, serializeArrayParam({
363
- explode,
364
- name,
365
- style,
366
- value
367
- }));
368
- continue;
369
- }
370
- if (typeof value === "object") {
371
- url = url.replace(match, serializeObjectParam({
372
- explode,
373
- name,
374
- style,
375
- value,
376
- valueOnly: true
377
- }));
378
- continue;
379
- }
380
- if (style === "matrix") {
381
- url = url.replace(match, `;${serializePrimitiveParam({
382
- name,
383
- value
384
- })}`);
385
- continue;
386
- }
387
- const replaceValue = encodeURIComponent(style === "label" ? `.${value}` : value);
388
- url = url.replace(match, replaceValue);
389
- }
390
- return url;
391
- };
392
- const getUrl = ({ baseUrl, path, query, querySerializer, url: _url }) => {
393
- const pathUrl = _url.startsWith("/") ? _url : `/${_url}`;
394
- let url = (baseUrl ?? "") + pathUrl;
395
- if (path) url = defaultPathSerializer({
396
- path,
397
- url
398
- });
399
- let search = query ? querySerializer(query) : "";
400
- if (search.startsWith("?")) search = search.substring(1);
401
- if (search) url += `?${search}`;
402
- return url;
403
- };
404
- function getValidRequestBody(options) {
405
- const hasBody = options.body !== void 0;
406
- if (hasBody && options.bodySerializer) {
407
- if ("serializedBody" in options) return options.serializedBody !== void 0 && options.serializedBody !== "" ? options.serializedBody : null;
408
- return options.body !== "" ? options.body : null;
409
- }
410
- if (hasBody) return options.body;
411
- }
412
- //#endregion
413
- //#region ../../api-clients/hey-api-fetch-runtime/src/generated/core/auth.gen.ts
414
- const getAuthToken$1 = async (auth, callback) => {
415
- const token = typeof callback === "function" ? await callback(auth) : callback;
416
- if (!token) return;
417
- if (auth.scheme === "bearer") return `Bearer ${token}`;
418
- if (auth.scheme === "basic") return `Basic ${btoa(token)}`;
419
- return token;
420
- };
421
- //#endregion
422
- //#region ../../api-clients/hey-api-fetch-runtime/src/generated/client/utils.gen.ts
423
- const createQuerySerializer = ({ parameters = {}, ...args } = {}) => {
424
- const querySerializer = (queryParams) => {
425
- const search = [];
426
- if (queryParams && typeof queryParams === "object") for (const name in queryParams) {
427
- const value = queryParams[name];
428
- if (value === void 0 || value === null) continue;
429
- const options = parameters[name] || args;
430
- if (Array.isArray(value)) {
431
- const serializedArray = serializeArrayParam({
432
- allowReserved: options.allowReserved,
433
- explode: true,
434
- name,
435
- style: "form",
436
- value,
437
- ...options.array
438
- });
439
- if (serializedArray) search.push(serializedArray);
440
- } else if (typeof value === "object") {
441
- const serializedObject = serializeObjectParam({
442
- allowReserved: options.allowReserved,
443
- explode: true,
444
- name,
445
- style: "deepObject",
446
- value,
447
- ...options.object
448
- });
449
- if (serializedObject) search.push(serializedObject);
450
- } else {
451
- const serializedPrimitive = serializePrimitiveParam({
452
- allowReserved: options.allowReserved,
453
- name,
454
- value
455
- });
456
- if (serializedPrimitive) search.push(serializedPrimitive);
457
- }
458
- }
459
- return search.join("&");
460
- };
461
- return querySerializer;
462
- };
463
- /**
464
- * Infers parseAs value from provided Content-Type header.
465
- */
466
- const getParseAs = (contentType) => {
467
- if (!contentType) return "stream";
468
- const cleanContent = contentType.split(";")[0]?.trim();
469
- if (!cleanContent) return;
470
- if (cleanContent.startsWith("application/json") || cleanContent.endsWith("+json")) return "json";
471
- if (cleanContent === "multipart/form-data") return "formData";
472
- if ([
473
- "application/",
474
- "audio/",
475
- "image/",
476
- "video/"
477
- ].some((type) => cleanContent.startsWith(type))) return "blob";
478
- if (cleanContent.startsWith("text/")) return "text";
479
- };
480
- const checkForExistence = (options, name) => {
481
- if (!name) return false;
482
- if (options.headers.has(name) || options.query?.[name] || options.headers.get("Cookie")?.includes(`${name}=`)) return true;
483
- return false;
484
- };
485
- async function setAuthParams(options) {
486
- for (const auth of options.security ?? []) {
487
- if (checkForExistence(options, auth.name)) continue;
488
- const token = await getAuthToken$1(auth, options.auth);
489
- if (!token) continue;
490
- const name = auth.name ?? "Authorization";
491
- switch (auth.in) {
492
- case "query":
493
- if (!options.query) options.query = {};
494
- options.query[name] = token;
495
- break;
496
- case "cookie":
497
- options.headers.append("Cookie", `${name}=${token}`);
498
- break;
499
- default:
500
- options.headers.set(name, token);
501
- break;
502
- }
503
- }
504
- }
505
- const buildUrl = (options) => getUrl({
506
- baseUrl: options.baseUrl,
507
- path: options.path,
508
- query: options.query,
509
- querySerializer: typeof options.querySerializer === "function" ? options.querySerializer : createQuerySerializer(options.querySerializer),
510
- url: options.url
511
- });
512
- const mergeConfigs = (a, b) => {
513
- const config = {
514
- ...a,
515
- ...b
516
- };
517
- if (config.baseUrl?.endsWith("/")) config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1);
518
- config.headers = mergeHeaders(a.headers, b.headers);
519
- return config;
520
- };
521
- const headersEntries = (headers) => {
522
- const entries = [];
523
- headers.forEach((value, key) => {
524
- entries.push([key, value]);
525
- });
526
- return entries;
527
- };
528
- const mergeHeaders = (...headers) => {
529
- const mergedHeaders = new Headers();
530
- for (const header of headers) {
531
- if (!header) continue;
532
- const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header);
533
- for (const [key, value] of iterator) if (value === null) mergedHeaders.delete(key);
534
- else if (Array.isArray(value)) for (const v of value) mergedHeaders.append(key, v);
535
- else if (value !== void 0) mergedHeaders.set(key, typeof value === "object" ? JSON.stringify(value) : value);
536
- }
537
- return mergedHeaders;
538
- };
539
- var Interceptors = class {
540
- fns = [];
541
- clear() {
542
- this.fns = [];
543
- }
544
- eject(id) {
545
- const index = this.getInterceptorIndex(id);
546
- if (this.fns[index]) this.fns[index] = null;
547
- }
548
- exists(id) {
549
- const index = this.getInterceptorIndex(id);
550
- return Boolean(this.fns[index]);
551
- }
552
- getInterceptorIndex(id) {
553
- if (typeof id === "number") return this.fns[id] ? id : -1;
554
- return this.fns.indexOf(id);
555
- }
556
- update(id, fn) {
557
- const index = this.getInterceptorIndex(id);
558
- if (this.fns[index]) {
559
- this.fns[index] = fn;
560
- return id;
561
- }
562
- return false;
563
- }
564
- use(fn) {
565
- this.fns.push(fn);
566
- return this.fns.length - 1;
567
- }
568
- };
569
- const createInterceptors = () => ({
570
- error: new Interceptors(),
571
- request: new Interceptors(),
572
- response: new Interceptors()
573
- });
574
- const defaultQuerySerializer = createQuerySerializer({
575
- allowReserved: false,
576
- array: {
577
- explode: true,
578
- style: "form"
579
- },
580
- object: {
581
- explode: true,
582
- style: "deepObject"
583
- }
584
- });
585
- const defaultHeaders = { "Content-Type": "application/json" };
586
- const createConfig = (override = {}) => ({
587
- ...jsonBodySerializer,
588
- headers: defaultHeaders,
589
- parseAs: "auto",
590
- querySerializer: defaultQuerySerializer,
591
- ...override
592
- });
593
- //#endregion
594
- //#region ../../api-clients/hey-api-fetch-runtime/src/generated/client/client.gen.ts
595
- const createClient$1 = (config = {}) => {
596
- let _config = mergeConfigs(createConfig(), config);
597
- const getConfig = () => ({ ..._config });
598
- const setConfig = (config) => {
599
- _config = mergeConfigs(_config, config);
600
- return getConfig();
601
- };
602
- const interceptors = createInterceptors();
603
- const beforeRequest = async (options) => {
604
- const opts = {
605
- ..._config,
606
- ...options,
607
- fetch: options.fetch ?? _config.fetch ?? globalThis.fetch,
608
- headers: mergeHeaders(_config.headers, options.headers),
609
- serializedBody: void 0
610
- };
611
- if (opts.security) await setAuthParams(opts);
612
- if (opts.requestValidator) await opts.requestValidator(opts);
613
- if (opts.body !== void 0 && opts.bodySerializer) opts.serializedBody = opts.bodySerializer(opts.body);
614
- if (opts.body === void 0 || opts.serializedBody === "") opts.headers.delete("Content-Type");
615
- const resolvedOpts = opts;
616
- return {
617
- opts: resolvedOpts,
618
- url: buildUrl(resolvedOpts)
619
- };
620
- };
621
- const request = async (options) => {
622
- const throwOnError = options.throwOnError ?? _config.throwOnError;
623
- const responseStyle = options.responseStyle ?? _config.responseStyle;
624
- let request;
625
- let response;
626
- try {
627
- const { opts, url } = await beforeRequest(options);
628
- const requestInit = {
629
- redirect: "follow",
630
- ...opts,
631
- body: getValidRequestBody(opts)
632
- };
633
- request = new Request(url, requestInit);
634
- for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
635
- const _fetch = opts.fetch;
636
- response = await _fetch(request);
637
- for (const fn of interceptors.response.fns) if (fn) response = await fn(response, request, opts);
638
- const result = {
639
- request,
640
- response
641
- };
642
- if (response.ok) {
643
- const parseAs = (opts.parseAs === "auto" ? getParseAs(response.headers.get("Content-Type")) : opts.parseAs) ?? "json";
644
- if (response.status === 204 || response.headers.get("Content-Length") === "0") {
645
- let emptyData;
646
- switch (parseAs) {
647
- case "arrayBuffer":
648
- case "blob":
649
- case "text":
650
- emptyData = await response[parseAs]();
651
- break;
652
- case "formData":
653
- emptyData = new FormData();
654
- break;
655
- case "stream":
656
- emptyData = response.body;
657
- break;
658
- default:
659
- emptyData = {};
660
- break;
661
- }
662
- return opts.responseStyle === "data" ? emptyData : {
663
- data: emptyData,
664
- ...result
665
- };
666
- }
667
- let data;
668
- switch (parseAs) {
669
- case "arrayBuffer":
670
- case "blob":
671
- case "formData":
672
- case "text":
673
- data = await response[parseAs]();
674
- break;
675
- case "json": {
676
- const text = await response.text();
677
- data = text ? JSON.parse(text) : {};
678
- break;
679
- }
680
- case "stream": return opts.responseStyle === "data" ? response.body : {
681
- data: response.body,
682
- ...result
683
- };
684
- }
685
- if (parseAs === "json") {
686
- if (opts.responseValidator) await opts.responseValidator(data);
687
- if (opts.responseTransformer) data = await opts.responseTransformer(data);
688
- }
689
- return opts.responseStyle === "data" ? data : {
690
- data,
691
- ...result
692
- };
693
- }
694
- const textError = await response.text();
695
- let jsonError;
696
- try {
697
- jsonError = JSON.parse(textError);
698
- } catch {}
699
- throw jsonError ?? textError;
700
- } catch (error) {
701
- let finalError = error;
702
- for (const fn of interceptors.error.fns) if (fn) finalError = await fn(finalError, response, request, options);
703
- finalError = finalError || {};
704
- if (throwOnError) throw finalError;
705
- return responseStyle === "data" ? void 0 : {
706
- error: finalError,
707
- request,
708
- response
709
- };
710
- }
711
- };
712
- const makeMethodFn = (method) => (options) => request({
713
- ...options,
714
- method
715
- });
716
- const makeSseFn = (method) => async (options) => {
717
- const { opts, url } = await beforeRequest(options);
718
- return createSseClient({
719
- ...opts,
720
- body: opts.body,
721
- method,
722
- onRequest: async (url, init) => {
723
- let request = new Request(url, init);
724
- for (const fn of interceptors.request.fns) if (fn) request = await fn(request, opts);
725
- return request;
726
- },
727
- serializedBody: getValidRequestBody(opts),
728
- url
729
- });
730
- };
731
- const _buildUrl = (options) => buildUrl({
732
- ..._config,
733
- ...options
734
- });
735
- return {
736
- buildUrl: _buildUrl,
737
- connect: makeMethodFn("CONNECT"),
738
- delete: makeMethodFn("DELETE"),
739
- get: makeMethodFn("GET"),
740
- getConfig,
741
- head: makeMethodFn("HEAD"),
742
- interceptors,
743
- options: makeMethodFn("OPTIONS"),
744
- patch: makeMethodFn("PATCH"),
745
- post: makeMethodFn("POST"),
746
- put: makeMethodFn("PUT"),
747
- request,
748
- setConfig,
749
- sse: {
750
- connect: makeSseFn("CONNECT"),
751
- delete: makeSseFn("DELETE"),
752
- get: makeSseFn("GET"),
753
- head: makeSseFn("HEAD"),
754
- options: makeSseFn("OPTIONS"),
755
- patch: makeSseFn("PATCH"),
756
- post: makeSseFn("POST"),
757
- put: makeSseFn("PUT"),
758
- trace: makeSseFn("TRACE")
759
- },
760
- trace: makeMethodFn("TRACE")
761
- };
762
- };
763
- //#endregion
764
- //#region ../../api-clients/fluidos/src/hey-api-client.ts
765
- function isAbortError(error) {
766
- return error instanceof DOMException && error.name === "AbortError" || error instanceof Error && error.name === "AbortError";
767
- }
768
- function createFluidOsHeyApiClient(config) {
769
- const client = createClient$1({
770
- auth: async () => await config.getAuthToken?.() ?? void 0,
771
- baseUrl: config.baseUrl,
772
- fetch: config.fetch,
773
- headers: {
774
- Accept: "application/json",
775
- ...config.defaultHeaders
776
- },
777
- throwOnError: true
778
- });
779
- client.interceptors.error.use(createHeyApiErrorInterceptor({
780
- onResponseError: (response) => {
781
- if (response.status === 401) config.onAuthError?.();
782
- },
783
- shouldPassThroughTransportError: isAbortError
784
- }));
785
- return client;
786
- }
787
- //#endregion
788
- //#region ../../api-clients/fluidos/src/json.ts
789
- function toFluidOsDynamicObject(value) {
790
- if (value === null) return null;
791
- const serialized = JSON.stringify(value);
792
- if (serialized === void 0) throw new TypeError("FluidOS payload cannot be serialized as JSON.");
793
- return JSON.parse(serialized);
794
- }
795
- function toNumberArray(value) {
796
- if (!Array.isArray(value)) return void 0;
797
- return value.filter((item) => typeof item === "number");
798
- }
799
- function toStringArray(value) {
800
- if (!Array.isArray(value)) return void 0;
801
- return value.filter((item) => typeof item === "string");
802
- }
803
- //#endregion
804
- //#region src/api/fluidos.ts
805
- function createFluidOsClient(options) {
806
- return createFluidOsHeyApiClient({
807
- baseUrl: options.baseUrl,
808
- getAuthToken: () => options.token
809
- });
810
- }
811
- const fluidOsApi = {
812
- listDefinitions: (client, query) => fluidOsV0ListFluidOsdefinitions({
813
- client,
814
- query
815
- }),
816
- listScreens: (client, definitionId, query) => fluidOsV0ListFluidOsscreens({
817
- client,
818
- path: { definition_id: definitionId },
819
- query
820
- }),
821
- getScreen: (client, definitionId, id) => fluidOsV0GetFluidOsscreen({
822
- client,
823
- path: {
824
- definition_id: definitionId,
825
- id
826
- }
827
- }),
828
- createScreen: (client, definitionId, body) => fluidOsV0CreateFluidOsscreen({
829
- body,
830
- client,
831
- path: { definition_id: definitionId }
832
- }),
833
- updateScreen: (client, definitionId, id, body) => fluidOsV0UpdateFluidOsscreen({
834
- body,
835
- client,
836
- path: {
837
- definition_id: definitionId,
838
- id
839
- }
840
- }),
841
- deleteScreen: (client, definitionId, id) => fluidOsV0DeleteFluidOsscreen({
842
- client,
843
- path: {
844
- definition_id: definitionId,
845
- id
846
- }
847
- }),
848
- listThemes: (client, definitionId, query) => fluidOsV0ListFluidOsthemes({
849
- client,
850
- path: { definition_id: definitionId },
851
- query
852
- }),
853
- createTheme: (client, definitionId, body) => fluidOsV0CreateFluidOstheme({
854
- body,
855
- client,
856
- path: { definition_id: definitionId }
857
- }),
858
- updateTheme: (client, definitionId, id, body) => fluidOsV0UpdateFluidOstheme({
859
- body,
860
- client,
861
- path: {
862
- definition_id: definitionId,
863
- id
864
- }
865
- }),
866
- deleteTheme: (client, definitionId, id) => fluidOsV0DeleteFluidOstheme({
867
- client,
868
- path: {
869
- definition_id: definitionId,
870
- id
871
- }
872
- }),
873
- listNavigations: (client, definitionId, query) => fluidOsV0ListFluidOsnavigations({
874
- client,
875
- path: { definition_id: definitionId },
876
- query
877
- }),
878
- createNavigation: (client, definitionId, body) => fluidOsV0CreateFluidOsnavigation({
879
- body,
880
- client,
881
- path: { definition_id: definitionId }
882
- }),
883
- updateNavigation: (client, definitionId, id, body) => fluidOsV0UpdateFluidOsnavigation({
884
- body,
885
- client,
886
- path: {
887
- definition_id: definitionId,
888
- id
889
- }
890
- }),
891
- deleteNavigation: (client, definitionId, id) => fluidOsV0DeleteFluidOsnavigation({
892
- client,
893
- path: {
894
- definition_id: definitionId,
895
- id
896
- }
897
- }),
898
- listNavigationItems: (client, definitionId, navigationId) => fluidOsV0ListFluidOsnavigationItems({
899
- client,
900
- path: {
901
- definition_id: definitionId,
902
- navigation_id: navigationId
903
- }
904
- }),
905
- createNavigationItem: (client, definitionId, navigationId, body) => fluidOsV0CreateFluidOsnavigationItem({
906
- body,
907
- client,
908
- path: {
909
- definition_id: definitionId,
910
- navigation_id: navigationId
911
- }
912
- }),
913
- updateNavigationItem: (client, definitionId, navigationId, id, body) => fluidOsV0UpdateFluidOsnavigationItem({
914
- body,
915
- client,
916
- path: {
917
- definition_id: definitionId,
918
- id,
919
- navigation_id: navigationId
920
- }
921
- }),
922
- deleteNavigationItem: (client, definitionId, navigationId, id) => fluidOsV0DeleteFluidOsnavigationItem({
923
- client,
924
- path: {
925
- definition_id: definitionId,
926
- id,
927
- navigation_id: navigationId
928
- }
929
- }),
930
- listProfiles: (client, definitionId, query) => fluidOsV0ListFluidOsprofiles({
931
- client,
932
- path: { definition_id: definitionId },
933
- query
934
- }),
935
- createProfile: (client, definitionId, body) => fluidOsV0CreateFluidOsprofile({
936
- body,
937
- client,
938
- path: { definition_id: definitionId }
939
- }),
940
- updateProfile: (client, definitionId, id, body) => fluidOsV0UpdateFluidOsprofile({
941
- body,
942
- client,
943
- path: {
944
- definition_id: definitionId,
945
- id
946
- }
947
- }),
948
- deleteProfile: (client, definitionId, id) => fluidOsV0DeleteFluidOsprofile({
949
- client,
950
- path: {
951
- definition_id: definitionId,
952
- id
953
- }
954
- }),
955
- listVersions: (client, definitionId, query) => fluidOsV0ListFluidOsversions({
956
- client,
957
- path: { definition_id: definitionId },
958
- query
959
- }),
960
- createVersion: (client, definitionId) => fluidOsV0CreateFluidOsversion({
961
- client,
962
- path: { definition_id: definitionId }
963
- }),
964
- updateVersion: (client, definitionId, id, body) => fluidOsV0UpdateFluidOsversion({
965
- body,
966
- client,
967
- path: {
968
- definition_id: definitionId,
969
- id
970
- }
971
- })
972
- };
973
- //#endregion
974
- //#region src/utils/atomic-write.ts
975
- /**
976
- * Atomic file write utility.
977
- *
978
- * Uses a write-then-rename pattern so readers always see either the old
979
- * file or the complete new file — never a partially-written state.
980
- */
981
- /**
982
- * Write a file atomically using a write-then-rename pattern.
983
- * Ensures readers always see either the old file or the complete new file.
984
- *
985
- * If `rename` fails after the temp file has been written, the temp file
986
- * is cleaned up on a best-effort basis before re-throwing the error.
987
- */
988
- async function atomicWriteFile(filePath, data) {
989
- const tmp = join(dirname(filePath), `.tmp-${randomBytes(6).toString("hex")}`);
990
- try {
991
- await writeFile(tmp, data, "utf-8");
992
- await rename(tmp, filePath);
993
- } catch (err) {
994
- await unlink(tmp).catch(() => {});
995
- throw err;
996
- }
997
- }
998
- //#endregion
999
- //#region src/utils/mappings.ts
1000
- /**
1001
- * Slug-to-ID mapping system for portal content sync.
1002
- *
1003
- * Maps human-readable slugs (derived from resource names) to API IDs,
1004
- * enabling the file system to use readable directory/file names while
1005
- * maintaining the link back to server-side resources.
1006
- *
1007
- * Persisted in `.portal-sync/mappings.json`.
1008
- */
1009
- /** Runtime check that a parsed value has the expected PortalMappings shape. */
1010
- function isMappings(value) {
1011
- if (!value || typeof value !== "object") return false;
1012
- const m = value;
1013
- const def = m.definition;
1014
- if (!def || typeof def !== "object") return false;
1015
- if (typeof def.name !== "string" || typeof def.id !== "number") return false;
1016
- 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;
1017
- }
1018
- const MAPPINGS_FILE = "mappings.json";
1019
- /**
1020
- * Read the mappings file from the `.portal-sync/` directory.
1021
- * Returns `null` if the file does not exist.
1022
- */
1023
- async function readMappings(portalSyncDir) {
1024
- try {
1025
- const filePath = join(portalSyncDir, MAPPINGS_FILE);
1026
- const content = await readFile(filePath, "utf-8");
1027
- const parsed = JSON.parse(content);
1028
- if (!isMappings(parsed)) throw new Error(`Malformed mappings file: ${filePath}`);
1029
- return parsed;
1030
- } catch (err) {
1031
- if (err.code === "ENOENT") return null;
1032
- throw err;
1033
- }
1034
- }
1035
- /**
1036
- * Write the mappings file to the `.portal-sync/` directory.
1037
- * Creates the directory if it does not exist.
1038
- */
1039
- async function writeMappings(portalSyncDir, mappings) {
1040
- const filePath = join(portalSyncDir, MAPPINGS_FILE);
1041
- await mkdir(dirname(filePath), { recursive: true });
1042
- await atomicWriteFile(filePath, JSON.stringify(mappings, null, 2) + "\n");
1043
- }
1044
- /**
1045
- * Derive a filesystem-safe slug from a resource name.
1046
- *
1047
- * - Lowercases the name
1048
- * - Replaces non-alphanumeric characters (except hyphens) with hyphens
1049
- * - Collapses consecutive hyphens
1050
- * - Trims leading/trailing hyphens
1051
- *
1052
- * If the derived slug collides with an existing slug in `existingSlugs`,
1053
- * a numeric suffix is appended (e.g., `home-2`, `home-3`).
1054
- */
1055
- function deriveSlug(name, existingSlugs = /* @__PURE__ */ new Set()) {
1056
- const base = name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "unnamed";
1057
- if (!existingSlugs.has(base)) return base;
1058
- let counter = 2;
1059
- while (existingSlugs.has(`${base}-${counter}`)) counter++;
1060
- return `${base}-${counter}`;
1061
- }
1062
- /**
1063
- * Resolve a slug to its server-side ID.
1064
- * Returns `undefined` if the slug is not mapped.
1065
- */
1066
- function resolveSlugToId(mappings, resourceType, slug) {
1067
- return mappings[resourceType][slug];
1068
- }
1069
- /**
1070
- * Resolve a server-side ID back to its slug.
1071
- * Returns `undefined` if no slug maps to the given ID.
1072
- */
1073
- function resolveIdToSlug(mappings, resourceType, id) {
1074
- const entries = mappings[resourceType];
1075
- for (const [slug, mappedId] of Object.entries(entries)) if (mappedId === id) return slug;
1076
- }
1077
- /**
1078
- * Add or update a mapping entry for a given resource type.
1079
- * Returns a new `PortalMappings` object (immutable update).
1080
- */
1081
- function updateMapping(mappings, resourceType, slug, id) {
1082
- return {
1083
- ...mappings,
1084
- [resourceType]: {
1085
- ...mappings[resourceType],
1086
- [slug]: id
1087
- }
1088
- };
1089
- }
1090
- /**
1091
- * Remove a mapping entry for a given resource type.
1092
- * Returns a new `PortalMappings` object (immutable update).
1093
- * If the slug does not exist, the original mappings are returned unchanged.
1094
- */
1095
- function removeMapping(mappings, resourceType, slug) {
1096
- if (!(slug in mappings[resourceType])) return mappings;
1097
- const { [slug]: _removed, ...rest } = mappings[resourceType];
1098
- return {
1099
- ...mappings,
1100
- [resourceType]: rest
1101
- };
1102
- }
1103
- //#endregion
1104
- //#region src/utils/snapshot.ts
1105
- /**
1106
- * Hash-based snapshot system for portal content sync.
1107
- *
1108
- * Tracks file hashes to detect local changes since the last pull,
1109
- * enabling efficient push operations that only send modified files.
1110
- *
1111
- * Persisted in `.portal-sync/snapshot.json`.
1112
- */
1113
- /** Maximum number of concurrent file operations. */
1114
- const MAX_CONCURRENCY = 20;
1115
- /**
1116
- * Run async tasks with bounded concurrency to avoid exhausting file descriptors.
1117
- */
1118
- async function mapWithLimit(items, limit, fn) {
1119
- const results = new Array(items.length);
1120
- let index = 0;
1121
- async function worker() {
1122
- while (index < items.length) {
1123
- const i = index++;
1124
- results[i] = await fn(items[i]);
1125
- }
1126
- }
1127
- const workers = Array.from({ length: Math.min(limit, items.length) }, () => worker());
1128
- await Promise.all(workers);
1129
- return results;
1130
- }
1131
- /** Runtime check that a parsed value has the expected Snapshot shape. */
1132
- function isSnapshot(value) {
1133
- if (!value || typeof value !== "object") return false;
1134
- const s = value;
1135
- return typeof s.definition === "string" && typeof s.definition_id === "number" && typeof s.pulled_at === "string" && typeof s.files === "object" && s.files !== null;
1136
- }
1137
- /**
1138
- * Compute the SHA-256 hash of a file's contents.
1139
- * Returns the hex-encoded digest.
1140
- */
1141
- async function computeFileHash(filePath) {
1142
- const content = await readFile(filePath);
1143
- return createHash("sha256").update(content).digest("hex");
1144
- }
1145
- const SNAPSHOT_FILE = "snapshot.json";
1146
- /**
1147
- * Read the snapshot file from the `.portal-sync/` directory.
1148
- * Returns `null` if the file does not exist.
1149
- */
1150
- async function readSnapshot(portalSyncDir) {
1151
- try {
1152
- const filePath = join(portalSyncDir, SNAPSHOT_FILE);
1153
- const content = await readFile(filePath, "utf-8");
1154
- const parsed = JSON.parse(content);
1155
- if (!isSnapshot(parsed)) throw new Error(`Malformed snapshot file: ${filePath}`);
1156
- return parsed;
1157
- } catch (err) {
1158
- if (err.code === "ENOENT") return null;
1159
- throw err;
1160
- }
1161
- }
1162
- /**
1163
- * Write the snapshot file to the `.portal-sync/` directory.
1164
- * Creates the directory if it does not exist.
1165
- */
1166
- async function writeSnapshot(portalSyncDir, snapshot) {
1167
- const filePath = join(portalSyncDir, SNAPSHOT_FILE);
1168
- await mkdir(dirname(filePath), { recursive: true });
1169
- await atomicWriteFile(filePath, JSON.stringify(snapshot, null, 2) + "\n");
1170
- }
1171
- /**
1172
- * Recursively collect all files in a directory, returning paths
1173
- * relative to `baseDir`.
1174
- */
1175
- async function collectFiles(dir, baseDir = dir) {
1176
- const entries = await readdir(dir, { withFileTypes: true });
1177
- const files = [];
1178
- for (const entry of entries) {
1179
- const fullPath = join(dir, entry.name);
1180
- if (entry.name === ".portal-sync") continue;
1181
- if (entry.isSymbolicLink()) {
1182
- const realStat = await stat(fullPath);
1183
- if (realStat.isDirectory()) files.push(...await collectFiles(fullPath, baseDir));
1184
- else if (realStat.isFile()) files.push(relative(baseDir, fullPath).split(sep).join("/"));
1185
- } else if (entry.isDirectory()) files.push(...await collectFiles(fullPath, baseDir));
1186
- else files.push(relative(baseDir, fullPath).split(sep).join("/"));
1187
- }
1188
- return files;
1189
- }
1190
- /**
1191
- * Compare the current portal directory contents against a snapshot.
1192
- *
1193
- * Returns lists of new, changed, and deleted files (relative paths).
1194
- */
1195
- async function diffAgainstSnapshot(portalDir, snapshot) {
1196
- const currentFiles = await collectFiles(portalDir);
1197
- const snapshotPaths = new Set(Object.keys(snapshot.files));
1198
- const newFiles = [];
1199
- const changedFiles = [];
1200
- await mapWithLimit(currentFiles, MAX_CONCURRENCY, async (filePath) => {
1201
- const hash = await computeFileHash(join(portalDir, filePath));
1202
- if (!snapshotPaths.has(filePath)) newFiles.push(filePath);
1203
- else if (snapshot.files[filePath] !== hash) changedFiles.push(filePath);
1204
- });
1205
- const currentSet = new Set(currentFiles);
1206
- const deletedFiles = [...snapshotPaths].filter((filePath) => !currentSet.has(filePath));
1207
- return {
1208
- new: [...newFiles].sort(),
1209
- changed: [...changedFiles].sort(),
1210
- deleted: [...deletedFiles].sort()
1211
- };
1212
- }
1213
- /**
1214
- * Build a fresh snapshot from the current portal directory contents.
1215
- *
1216
- * Hashes every file in `portalDir` (excluding `.portal-sync/`)
1217
- * and records the definition metadata.
1218
- */
1219
- async function buildSnapshot(portalDir, definitionName, definitionId) {
1220
- const hashEntries = await mapWithLimit(await collectFiles(portalDir), MAX_CONCURRENCY, async (filePath) => {
1221
- return [filePath, await computeFileHash(join(portalDir, filePath))];
1222
- });
1223
- hashEntries.sort(([a], [b]) => a.localeCompare(b));
1224
- const fileHashes = {};
1225
- for (const [path, hash] of hashEntries) fileHashes[path] = hash;
1226
- return {
1227
- definition: definitionName,
1228
- definition_id: definitionId,
1229
- pulled_at: (/* @__PURE__ */ new Date()).toISOString(),
1230
- files: fileHashes
1231
- };
1232
- }
1233
- //#endregion
1234
- //#region src/utils/transform.ts
1235
- /**
1236
- * Transform an API screen response into the local file format.
1237
- * Strips server-only fields and normalizes `component_tree` to always be an array.
1238
- */
1239
- function transformScreen(screen) {
1240
- const name = screen.name ?? "";
1241
- const rawTree = screen.component_tree;
1242
- let componentTree;
1243
- if (rawTree == null) componentTree = [];
1244
- else if (Array.isArray(rawTree)) componentTree = rawTree;
1245
- else componentTree = [rawTree];
1246
- return {
1247
- name,
1248
- component_tree: componentTree
1249
- };
1250
- }
1251
- /**
1252
- * Derive a slug for a screen. Screens use the existing `slug` field from the API.
1253
- * Falls back to slugifying the name if no slug exists.
1254
- */
1255
- function deriveScreenSlug(screen, existingSlugs) {
1256
- if (screen.slug) {
1257
- const safe = screen.slug.replace(/[^a-z0-9_-]/gi, "-");
1258
- if (!existingSlugs.has(safe)) return safe;
1259
- return deriveSlug(safe, existingSlugs);
1260
- }
1261
- return deriveSlug(screen.name ?? "unnamed", existingSlugs);
1262
- }
1263
- /**
1264
- * Transform an API theme response into the local file format.
1265
- * Passes theme config through as-is (hex conversion is a separate ticket).
1266
- */
1267
- function transformTheme(theme) {
1268
- return {
1269
- name: theme.name ?? "",
1270
- config: theme.config ?? {},
1271
- active: theme.active ?? false
1272
- };
1273
- }
1274
- /**
1275
- * Build an ID → slug lookup map by inverting a slug → id mapping.
1276
- */
1277
- function buildIdToSlugMap(mappings) {
1278
- const map = /* @__PURE__ */ new Map();
1279
- for (const [slug, id] of Object.entries(mappings)) map.set(id, slug);
1280
- return map;
1281
- }
1282
- /**
1283
- * Transform API navigation items into local format.
1284
- * Converts `screen_id` to `screen` slug reference. Keeps `id` for sync.
1285
- */
1286
- function transformNavigationItems(items, screenIdToSlug) {
1287
- return items.map((item) => ({
1288
- id: item.id,
1289
- icon: item.icon ?? null,
1290
- label: item.label ?? null,
1291
- screen: item.screen_id ? screenIdToSlug.get(item.screen_id) ?? null : null,
1292
- slug: item.slug ?? null,
1293
- source: item.source ?? "user",
1294
- position: item.position ?? null,
1295
- parent_id: item.parent_id ?? null,
1296
- children: transformNavigationItems(item.children ?? [], screenIdToSlug)
1297
- }));
1298
- }
1299
- /**
1300
- * Transform an API navigation with its items into the local file format.
1301
- */
1302
- function transformNavigation(nav, items, screenIdToSlug) {
1303
- return {
1304
- name: nav.name ?? "",
1305
- platform: nav.platform,
1306
- navigation_items: transformNavigationItems(items, screenIdToSlug)
1307
- };
1308
- }
1309
- /** Typed alias for call-site clarity. */
1310
- const buildNavigationIdToSlugMap = buildIdToSlugMap;
1311
- /** Typed alias for call-site clarity. */
1312
- const buildThemeIdToSlugMap = buildIdToSlugMap;
1313
- /**
1314
- * Transform an API profile response into the local file format.
1315
- * Converts navigation_id, mobile_navigation_id, and theme_ids to slug references.
1316
- *
1317
- * TODO (REP-842): Country/rank IDs in profile permissions need to be translated
1318
- * to human-readable names via the main Fluid API. For now, the permission arrays
1319
- * are written as-is with integer IDs.
1320
- */
1321
- function transformProfile(profile, navIdToSlug, themeIdToSlug) {
1322
- const navigation = profile.navigation ? navIdToSlug.get(profile.navigation.id) ?? null : null;
1323
- const mobileNavigation = profile.mobile_navigation ? navIdToSlug.get(profile.mobile_navigation.id) ?? null : null;
1324
- const themes = (profile.themes ?? []).map((t) => themeIdToSlug.get(t.id)).filter((slug) => slug != null);
1325
- const permissions = profile.permissions ?? {};
1326
- return {
1327
- name: profile.name ?? "",
1328
- default: profile.default ?? false,
1329
- navigation,
1330
- mobile_navigation: mobileNavigation,
1331
- themes,
1332
- permissions: {
1333
- ranks: toNumberArray(permissions.ranks) ?? [],
1334
- roles: toStringArray(permissions.roles) ?? [],
1335
- platform: toStringArray(permissions.platform) ?? [],
1336
- countries: toNumberArray(permissions.countries) ?? []
1337
- }
1338
- };
1339
- }
1340
- //#endregion
1341
- //#region src/commands/pull.ts
1342
- /**
1343
- * `fluid portal pull` command
1344
- *
1345
- * Pulls a Fluid OS definition's resources (screens, themes, navigations,
1346
- * profiles) from the API and writes them to the local `portal/` directory
1347
- * as JSON files, along with `.portal-sync/` metadata for push diffing.
1348
- */
1349
- var pull_exports = /* @__PURE__ */ __exportAll({ pullCommand: () => pullCommand });
1350
- const PORTAL_DIR = "portal";
1351
- const PORTAL_SYNC_DIR = ".portal-sync";
1352
- const PAGE_LIMIT = 500;
1353
- /**
1354
- * Create an authenticated FluidOS client using the stored CLI profile.
1355
- */
1356
- function createClient() {
1357
- const token = getAuthToken();
1358
- if (!token) {
1359
- const profile = getActiveProfile();
1360
- if (!profile) throw new Error("Not logged in. Run " + chalk.cyan("fluid login") + " first.");
1361
- throw new Error("No auth token found for profile " + chalk.cyan(profile.name) + ". Run " + chalk.cyan("fluid login") + " to re-authenticate.");
1362
- }
1363
- return createFluidOsClient({
1364
- baseUrl: process.env["FLUID_API_BASE"] ?? "https://api.fluid.app",
1365
- token
1366
- });
1367
- }
1368
- /**
1369
- * Select a definition interactively or by name.
1370
- */
1371
- async function fetchAllDefinitions(client) {
1372
- const all = [];
1373
- let page = 1;
1374
- while (true) {
1375
- const response = await fluidOsApi.listDefinitions(client, {
1376
- page,
1377
- per_page: PAGE_LIMIT
1378
- });
1379
- const batch = response.definitions ?? [];
1380
- all.push(...batch);
1381
- const totalPages = response.meta?.total_pages ?? 1;
1382
- if (page >= totalPages || batch.length === 0) break;
1383
- page++;
1384
- }
1385
- return all;
1386
- }
1387
- async function selectDefinition(client, appName, rawDefinitionId) {
1388
- const definitions = await fetchAllDefinitions(client);
1389
- if (definitions.length === 0) throw new Error("No Fluid OS definitions found. Create one in the admin dashboard first.");
1390
- const resolved = resolveDefinition(definitions, appName, rawDefinitionId);
1391
- if (resolved) return resolved;
1392
- const { definitionId } = await prompts({
1393
- type: "select",
1394
- name: "definitionId",
1395
- message: "Select a Fluid OS definition to pull",
1396
- choices: definitions.map((d) => ({
1397
- title: d.name ?? "(unnamed)",
1398
- value: d.id
1399
- }))
1400
- });
1401
- if (definitionId == null) throw new Error("No definition selected.");
1402
- const selected = definitions.find((d) => d.id === definitionId);
1403
- if (!selected) throw new Error("Selected definition not found.");
1404
- return selected;
1405
- }
1406
- /**
1407
- * Check for definition switching conflicts.
1408
- * Returns true if it's safe to proceed.
1409
- */
1410
- async function checkDefinitionSwitch(cwd, newDefinitionId, newDefinitionName, force) {
1411
- const portalDir = join(cwd, PORTAL_DIR);
1412
- const portalSyncDir = join(cwd, PORTAL_SYNC_DIR);
1413
- if (!existsSync(portalDir)) return true;
1414
- const snapshot = await readSnapshot(portalSyncDir);
1415
- if (!snapshot) return true;
1416
- if (snapshot.definition_id === newDefinitionId) return true;
1417
- const diff = await diffAgainstSnapshot(portalDir, snapshot);
1418
- if ((diff.new.length > 0 || diff.changed.length > 0 || diff.deleted.length > 0) && !force) {
1419
- console.log();
1420
- console.log(chalk.yellow("Warning:") + " Switching from " + chalk.cyan(snapshot.definition) + " to " + chalk.cyan(newDefinitionName) + " with unpushed local changes.");
1421
- console.log();
1422
- console.log(" Modified: " + diff.changed.length + " file(s)");
1423
- console.log(" New: " + diff.new.length + " file(s)");
1424
- console.log(" Deleted: " + diff.deleted.length + " file(s)");
1425
- console.log();
1426
- console.log("Use " + chalk.cyan("--force") + " to discard local changes and switch definitions.");
1427
- return false;
1428
- }
1429
- return true;
1430
- }
1431
- /**
1432
- * Write a JSON file to the portal directory, creating subdirectories as needed.
1433
- */
1434
- async function writePortalFile(portalDir, relativePath, data) {
1435
- const filePath = join(portalDir, relativePath);
1436
- await mkdir(join(filePath, ".."), { recursive: true });
1437
- await writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
1438
- }
1439
- 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) => {
1440
- const cwd = process.cwd();
1441
- await assertStandalonePortalRemoteActionAllowed(cwd, "portal pull");
1442
- console.log();
1443
- console.log(chalk.blue.bold("Fluid Portal Pull"));
1444
- console.log();
1445
- const spinner = ora();
1446
- spinner.start("Authenticating...");
1447
- let client;
1448
- try {
1449
- client = createClient();
1450
- spinner.succeed("Authenticated");
1451
- } catch (err) {
1452
- spinner.fail("Authentication failed");
1453
- console.log();
1454
- console.log(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
1455
- console.log();
1456
- process.exit(1);
1457
- }
1458
- let definition;
1459
- try {
1460
- definition = await selectDefinition(client, options.app, options.id);
1461
- } catch (err) {
1462
- console.log();
1463
- console.log(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
1464
- console.log();
1465
- process.exit(1);
1466
- }
1467
- const definitionId = definition.id;
1468
- const definitionName = definition.name ?? "(unnamed)";
1469
- console.log(chalk.gray("Definition: ") + chalk.white(definitionName) + chalk.gray(` (ID: ${definitionId})`));
1470
- console.log();
1471
- let canProceed;
1472
- try {
1473
- canProceed = await checkDefinitionSwitch(cwd, definitionId, definitionName, options.force ?? false);
1474
- } catch (err) {
1475
- console.log();
1476
- console.log(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
1477
- console.log();
1478
- process.exit(1);
1479
- }
1480
- if (!canProceed) process.exit(1);
1481
- spinner.start("Fetching resources...");
1482
- let screenBasics;
1483
- let themes;
1484
- let navigations;
1485
- let profiles;
1486
- let screens;
1487
- const navigationItemsMap = /* @__PURE__ */ new Map();
1488
- try {
1489
- const [screensResponse, themesResponse, navigationsResponse, profilesResponse] = await Promise.all([
1490
- fluidOsApi.listScreens(client, definitionId, { per_page: PAGE_LIMIT }),
1491
- fluidOsApi.listThemes(client, definitionId, { per_page: PAGE_LIMIT }),
1492
- fluidOsApi.listNavigations(client, definitionId, { per_page: PAGE_LIMIT }),
1493
- fluidOsApi.listProfiles(client, definitionId, { per_page: PAGE_LIMIT })
1494
- ]);
1495
- screenBasics = screensResponse.screens ?? [];
1496
- themes = themesResponse.themes ?? [];
1497
- navigations = navigationsResponse.navigations ?? [];
1498
- profiles = profilesResponse.profiles ?? [];
1499
- if (screenBasics.length === PAGE_LIMIT) console.warn(chalk.yellow(`Warning: screen count hit the ${PAGE_LIMIT}-item limit; some screens may be missing.`));
1500
- if (themes.length === PAGE_LIMIT) console.warn(chalk.yellow(`Warning: theme count hit the ${PAGE_LIMIT}-item limit; some themes may be missing.`));
1501
- if (navigations.length === PAGE_LIMIT) console.warn(chalk.yellow(`Warning: navigation count hit the ${PAGE_LIMIT}-item limit; some navigations may be missing.`));
1502
- if (profiles.length === PAGE_LIMIT) console.warn(chalk.yellow(`Warning: profile count hit the ${PAGE_LIMIT}-item limit; some profiles may be missing.`));
1503
- spinner.text = "Fetching screen details...";
1504
- const limit = pLimit(10);
1505
- screens = await Promise.all(screenBasics.map((s) => limit(async () => {
1506
- const res = await fluidOsApi.getScreen(client, definitionId, s.id);
1507
- if (!res.screen) throw new Error(`Failed to fetch details for screen ID ${s.id}`);
1508
- return res.screen;
1509
- })));
1510
- spinner.text = "Fetching navigation items...";
1511
- await Promise.all(navigations.map((nav) => limit(async () => {
1512
- const res = await fluidOsApi.listNavigationItems(client, definitionId, nav.id);
1513
- navigationItemsMap.set(nav.id, res.navigation_items ?? []);
1514
- })));
1515
- spinner.succeed(`Fetched ${screens.length} screen(s), ${themes.length} theme(s), ${navigations.length} navigation(s), ${profiles.length} profile(s)`);
1516
- } catch (err) {
1517
- spinner.fail("Failed to fetch resources");
1518
- console.log();
1519
- console.log(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
1520
- console.log();
1521
- process.exit(1);
1522
- }
1523
- spinner.start("Transforming resources...");
1524
- const screenSlugs = /* @__PURE__ */ new Set();
1525
- const screenMappings = {};
1526
- for (const screen of screens) {
1527
- const slug = deriveScreenSlug(screen, screenSlugs);
1528
- screenSlugs.add(slug);
1529
- screenMappings[slug] = screen.id;
1530
- }
1531
- const themeSlugs = /* @__PURE__ */ new Set();
1532
- const themeMappings = {};
1533
- for (const theme of themes) {
1534
- const slug = deriveSlug(theme.name ?? "unnamed", themeSlugs);
1535
- themeSlugs.add(slug);
1536
- themeMappings[slug] = theme.id;
1537
- }
1538
- const navSlugs = /* @__PURE__ */ new Set();
1539
- const navMappings = {};
1540
- for (const nav of navigations) {
1541
- const slug = deriveSlug(nav.name ?? "unnamed", navSlugs);
1542
- navSlugs.add(slug);
1543
- navMappings[slug] = nav.id;
1544
- }
1545
- const profileSlugs = /* @__PURE__ */ new Set();
1546
- const profileMappings = {};
1547
- for (const profile of profiles) {
1548
- const slug = deriveSlug(profile.name ?? "unnamed", profileSlugs);
1549
- profileSlugs.add(slug);
1550
- profileMappings[slug] = profile.id;
1551
- }
1552
- const screenIdToSlug = buildIdToSlugMap(screenMappings);
1553
- const navIdToSlug = buildNavigationIdToSlugMap(navMappings);
1554
- const themeIdToSlug = buildThemeIdToSlugMap(themeMappings);
1555
- const profileIdToSlug = buildIdToSlugMap(profileMappings);
1556
- spinner.succeed("Resources transformed");
1557
- spinner.start("Writing files...");
1558
- const portalDir = join(cwd, PORTAL_DIR);
1559
- const portalSyncDir = join(cwd, PORTAL_SYNC_DIR);
1560
- const tmpPortalDir = portalDir + ".tmp";
1561
- try {
1562
- if (existsSync(tmpPortalDir)) await rm(tmpPortalDir, { recursive: true });
1563
- await mkdir(tmpPortalDir, { recursive: true });
1564
- await writePortalFile(tmpPortalDir, "definition.json", { name: definitionName });
1565
- for (const screen of screens) {
1566
- const slug = screenIdToSlug.get(screen.id);
1567
- const local = transformScreen(screen);
1568
- await writePortalFile(tmpPortalDir, `screens/${slug}.json`, local);
1569
- }
1570
- for (const theme of themes) {
1571
- const slug = themeIdToSlug.get(theme.id);
1572
- const local = transformTheme(theme);
1573
- await writePortalFile(tmpPortalDir, `themes/${slug}.json`, local);
1574
- }
1575
- for (const nav of navigations) {
1576
- const slug = navIdToSlug.get(nav.id);
1577
- const local = transformNavigation(nav, navigationItemsMap.get(nav.id) ?? [], screenIdToSlug);
1578
- await writePortalFile(tmpPortalDir, `navigations/${slug}.json`, local);
1579
- }
1580
- for (const profile of profiles) {
1581
- const slug = profileIdToSlug.get(profile.id);
1582
- if (slug) {
1583
- const local = transformProfile(profile, navIdToSlug, themeIdToSlug);
1584
- await writePortalFile(tmpPortalDir, `profiles/${slug}.json`, local);
1585
- }
1586
- }
1587
- if (existsSync(portalDir)) await rm(portalDir, { recursive: true });
1588
- await rename(tmpPortalDir, portalDir);
1589
- } catch (err) {
1590
- if (existsSync(tmpPortalDir)) await rm(tmpPortalDir, { recursive: true }).catch(() => {});
1591
- spinner.fail("Failed to write files");
1592
- console.log();
1593
- console.log(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
1594
- console.log();
1595
- process.exit(1);
1596
- }
1597
- try {
1598
- await writeMappings(portalSyncDir, {
1599
- definition: {
1600
- name: definitionName,
1601
- id: definitionId
1602
- },
1603
- screens: screenMappings,
1604
- themes: themeMappings,
1605
- navigations: navMappings,
1606
- profiles: profileMappings,
1607
- countries: {},
1608
- ranks: {}
1609
- });
1610
- await writeSnapshot(portalSyncDir, await buildSnapshot(portalDir, definitionName, definitionId));
1611
- spinner.succeed("Files written");
1612
- } catch (err) {
1613
- spinner.warn("Files written (metadata sync failed)");
1614
- console.log();
1615
- console.log(chalk.yellow("Warning:") + " Portal files were written successfully, but metadata sync failed. Run " + chalk.cyan("fluid portal pull") + " again to regenerate metadata.");
1616
- console.log(chalk.gray(" Detail: ") + (err instanceof Error ? err.message : String(err)));
1617
- console.log();
1618
- }
1619
- console.log();
1620
- console.log(chalk.green.bold("Pull complete!"));
1621
- console.log();
1622
- console.log(chalk.gray(" portal/definition.json"));
1623
- console.log(chalk.gray(` portal/screens/ `) + chalk.white(`${screens.length} file(s)`));
1624
- console.log(chalk.gray(` portal/themes/ `) + chalk.white(`${themes.length} file(s)`));
1625
- console.log(chalk.gray(` portal/navigations/ `) + chalk.white(`${navigations.length} file(s)`));
1626
- console.log(chalk.gray(` portal/profiles/ `) + chalk.white(`${profiles.length} file(s)`));
1627
- console.log(chalk.gray(` .portal-sync/ `) + chalk.white("mappings.json + snapshot.json"));
1628
- console.log();
1629
- });
1630
- //#endregion
1631
- export { STANDALONE_PORTAL_MARKER as A, writeMappings as C, toFluidOsDynamicObject as D, fluidOsApi as E, createFluidOsHeyApiClient as O, updateMapping as S, createFluidOsClient as T, 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, assertStandalonePortalRemoteActionAllowed as j, ApiError 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, atomicWriteFile as w, resolveSlugToId as x, removeMapping as y };
1632
-
1633
- //# sourceMappingURL=pull-CUR8tbW9.mjs.map