@fluid-app/fluid-cli-mysite 0.1.22 → 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +4 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -123,8 +123,10 @@ function createFetchClient(config) {
|
|
|
123
123
|
const queryString = new URLSearchParams();
|
|
124
124
|
Object.entries(params).forEach(([key, value]) => {
|
|
125
125
|
if (value === void 0 || value === null) return;
|
|
126
|
-
if (Array.isArray(value))
|
|
127
|
-
|
|
126
|
+
if (Array.isArray(value)) {
|
|
127
|
+
const arrayKey = key.endsWith("[]") ? key : `${key}[]`;
|
|
128
|
+
value.forEach((item) => queryString.append(arrayKey, String(item)));
|
|
129
|
+
} else if (typeof value === "object") Object.entries(value).forEach(([subKey, subValue]) => {
|
|
128
130
|
if (subValue === void 0 || subValue === null) return;
|
|
129
131
|
if (Array.isArray(subValue)) subValue.forEach((item) => queryString.append(`${key}[${subKey}][]`, String(item)));
|
|
130
132
|
else queryString.append(`${key}[${subKey}]`, String(subValue));
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../platform/api-client-core/src/api-error-shape.ts","../../../platform/api-client-core/src/fetch-client.ts","../src/api.ts","../src/commands/list.ts","../src/theme-files.ts","../src/commands/pull.ts","../src/commands/dev-copy-name.ts","../src/dev-server/hot-reload.ts","../src/dev-server/proxy.ts","../src/dev-server/sse.ts","../src/commands/dev.ts","../src/commands/publish-gate.ts","../src/commands/publish.ts","../src/commands/mysite.ts","../src/index.ts"],"sourcesContent":["/**\n * The shape contract for `ApiError.body` and `ApiError.data`.\n *\n * One decision, in one place: `body` is the response envelope exactly as the\n * server sent it, `data` is the unwrapped field-error bag. Rationale and the\n * alternatives considered live in `docs/api-error-data-contract.md`.\n */\n\n/**\n * The parsed JSON error response body, exactly as the server sent it.\n *\n * This is the envelope: `errors`, `error_message`, `error`, `status`, `meta`\n * and `request_id` all live here. `null` when the response carried no JSON\n * object — HTML, an empty body, a JSON primitive, or unparseable text.\n */\nexport type ApiErrorBody = Record<string, unknown> | null;\n\n/**\n * The field-level error bag: the envelope's `errors` value when the body has\n * one, otherwise the body itself.\n *\n * `errors` is declared `never` because unwrapping already consumed it — read\n * the envelope from `ApiError.body`, not from here. That makes the historical\n * mistake (`error.data.errors.some_field`, which silently resolved to\n * `undefined`) a typecheck failure instead of a degraded error message.\n */\nexport type ApiErrorFieldErrors = Record<string, unknown> & {\n errors?: never;\n};\n\n/**\n * Every value `ApiError.data` can hold. Fluid endpoints return an object bag,\n * but `errors` is occasionally an array or a string, and non-JSON responses\n * carry no data at all.\n */\nexport type ApiErrorData =\n | ApiErrorFieldErrors\n | readonly unknown[]\n | string\n | number\n | boolean\n | null;\n\n/**\n * Narrows a parsed JSON error payload to the envelope contract. Arrays and\n * primitives are not envelopes, so they resolve to `null` — their content is\n * still reachable through `ApiError.data`.\n */\nexport function toApiErrorBody(value: unknown): ApiErrorBody {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return null;\n }\n\n return value as Record<string, unknown>;\n}\n\n/**\n * The single unwrap rule, shared by every producer of `ApiError`.\n *\n * `??` rather than `||` so an explicitly empty `errors` (`\"\"`, `0`, `false`)\n * is preserved instead of silently falling back to the whole envelope.\n */\nexport function toApiErrorData(body: ApiErrorBody): ApiErrorData {\n if (!body) {\n return null;\n }\n\n return (body.errors ?? body) as ApiErrorData;\n}\n","/**\n * Minimal, framework-agnostic fetch client for Fluid APIs\n * Compatible with fluid-admin patterns but usable standalone\n */\n\nimport type { ApiErrorBody, ApiErrorData } from \"./api-error-shape\";\nimport { toApiErrorBody, toApiErrorData } from \"./api-error-shape\";\n\nexport interface FetchClientConfig {\n /**\n * Base URL for all requests (e.g., \"https://api.fluid.app/api\")\n */\n baseUrl: string;\n\n /**\n * Optional function to get auth token\n * Return null/undefined if no token available\n */\n getAuthToken?: () => string | null | Promise<string | null>;\n\n /**\n * Optional callback when 401 auth error occurs\n */\n onAuthError?: () => void;\n\n /**\n * Default headers to include in all requests\n * Example: { \"x-fluid-client\": \"admin\" }\n */\n defaultHeaders?: Record<string, string>;\n\n /**\n * Credentials mode for fetch requests.\n * Set to `\"include\"` for cookie-based (same-origin BFF) authentication.\n * @default undefined (browser default: \"same-origin\")\n */\n credentials?: RequestCredentials;\n\n /**\n * Request cache mode for fetch requests.\n * @default undefined (browser default)\n */\n cache?: RequestCache;\n\n /**\n * Retry configuration for thrown network errors from fetch.\n * Does not retry HTTP error responses or aborted requests.\n * @default undefined (no retries)\n */\n networkRetry?: {\n maxRetries?: number;\n baseDelayMs?: number;\n };\n\n /**\n * Throw ApiError when a successful response declares JSON but cannot be parsed.\n * Defaults to false to preserve the legacy generated-client behavior.\n * @default false\n */\n throwOnInvalidJson?: boolean;\n}\n\nexport interface RequestOptions {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\";\n headers?: Record<string, string>;\n params?: Record<string, unknown>;\n body?: unknown;\n signal?: AbortSignal;\n priority?: RequestInit[\"priority\"];\n}\n\n/**\n * API Error class compatible with fluid-admin's ApiError\n */\nexport class ApiError extends Error {\n public readonly status: number;\n\n /**\n * The unwrapped field-error bag. See `ApiErrorData`; the contract is\n * documented in `docs/api-error-data-contract.md`.\n */\n public readonly data: ApiErrorData;\n\n /**\n * The full error response envelope. Read `error_message`, `error`, `status`\n * and nested `errors` from here — `data` has already unwrapped one level.\n */\n public readonly body: ApiErrorBody;\n\n public readonly requestId?: string;\n\n constructor(\n message: string,\n status: number,\n data?: ApiErrorData,\n requestId?: string,\n body?: ApiErrorBody,\n ) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.data = data ?? null;\n this.body = body ?? null;\n this.requestId = requestId;\n\n if (\"captureStackTrace\" in Error) {\n (\n Error as {\n captureStackTrace: (\n target: Error,\n constructor: NewableFunction,\n ) => void;\n }\n ).captureStackTrace(this, ApiError);\n }\n }\n\n toJSON(): {\n name: string;\n message: string;\n status: number;\n data: ApiErrorData;\n body: ApiErrorBody;\n requestId?: string;\n } {\n return {\n name: this.name,\n message: this.message,\n status: this.status,\n data: this.data,\n body: this.body,\n requestId: this.requestId,\n };\n }\n}\n\nfunction getStringRequestId(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction getRequestIdFromHeaders(headers: Headers): string | undefined {\n return (\n getStringRequestId(headers.get(\"x-request-id\")) ??\n getStringRequestId(headers.get(\"request-id\")) ??\n getStringRequestId(headers.get(\"X-Request-ID\"))\n );\n}\n\nfunction getRequestIdFromJsonBody(body: unknown): string | undefined {\n if (!body || typeof body !== \"object\" || Array.isArray(body)) {\n return undefined;\n }\n\n const record = body as Record<string, unknown>;\n const meta = record.meta;\n\n return (\n getStringRequestId(record.request_id) ??\n getStringRequestId(record.requestId) ??\n (meta && typeof meta === \"object\" && !Array.isArray(meta)\n ? (getStringRequestId((meta as Record<string, unknown>).request_id) ??\n getStringRequestId((meta as Record<string, unknown>).requestId))\n : undefined)\n );\n}\n\n/**\n * Type guard for ApiError\n */\nexport function isApiError(error: unknown): error is ApiError {\n return error instanceof ApiError;\n}\n\nexport interface FetchClientInstance {\n request: <TResponse = unknown>(\n endpoint: string,\n options?: RequestOptions,\n ) => Promise<TResponse>;\n requestWithFormData: <TResponse = unknown>(\n endpoint: string,\n formData: FormData,\n options?: Omit<RequestOptions, \"body\" | \"params\"> & {\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n },\n ) => Promise<TResponse>;\n get: <TResponse = unknown>(\n endpoint: string,\n params?: Record<string, unknown>,\n options?: Omit<RequestOptions, \"method\" | \"params\">,\n ) => Promise<TResponse>;\n post: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n put: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n patch: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n delete: <TResponse = unknown>(\n endpoint: string,\n options?: Omit<RequestOptions, \"method\">,\n ) => Promise<TResponse>;\n}\n\n/**\n * Creates a configured fetch client instance\n */\nexport function createFetchClient(\n config: FetchClientConfig,\n): FetchClientInstance {\n const {\n baseUrl,\n getAuthToken,\n onAuthError,\n defaultHeaders = {},\n credentials,\n cache,\n networkRetry,\n throwOnInvalidJson = false,\n } = config;\n const maxNetworkRetries = Math.max(0, networkRetry?.maxRetries ?? 0);\n const baseNetworkRetryDelayMs = Math.max(0, networkRetry?.baseDelayMs ?? 0);\n\n /**\n * Build headers for a request\n */\n async function buildHeaders(\n customHeaders?: Record<string, string>,\n ): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...defaultHeaders,\n ...customHeaders,\n };\n\n // Add auth token if available\n if (getAuthToken) {\n const token = await getAuthToken();\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n }\n\n return headers;\n }\n\n /**\n * Join baseUrl + endpoint via string concatenation (matches fetchApi).\n * Using `new URL(endpoint, baseUrl)` would strip any path prefix from\n * baseUrl (e.g. \"/api\") when the endpoint starts with \"/\".\n */\n function joinUrl(endpoint: string): string {\n return `${baseUrl}${endpoint}`;\n }\n\n /**\n * Build URL with query parameters for GET requests\n * Compatible with fluid-admin's query param handling\n */\n function buildUrl(\n endpoint: string,\n params?: Record<string, unknown>,\n ): string {\n const fullUrl = joinUrl(endpoint);\n\n if (!params || Object.keys(params).length === 0) {\n return fullUrl;\n }\n\n const queryString = new URLSearchParams();\n\n Object.entries(params).forEach(([key, value]) => {\n if (value === undefined || value === null) {\n return; // Skip undefined/null values\n }\n\n if (Array.isArray(value)) {\n // Handle arrays like Rails expects: key[]\n value.forEach((item) => queryString.append(`${key}[]`, String(item)));\n } else if (typeof value === \"object\") {\n // Handle nested objects: key[subkey]\n Object.entries(value).forEach(([subKey, subValue]) => {\n if (subValue === undefined || subValue === null) {\n return;\n }\n\n if (Array.isArray(subValue)) {\n subValue.forEach((item) =>\n queryString.append(`${key}[${subKey}][]`, String(item)),\n );\n } else {\n queryString.append(`${key}[${subKey}]`, String(subValue));\n }\n });\n } else {\n queryString.append(key, String(value));\n }\n });\n\n const qs = queryString.toString();\n return qs ? `${fullUrl}?${qs}` : fullUrl;\n }\n\n /**\n * Shared response handler for both JSON and FormData requests.\n * Handles auth errors, non-OK responses, 204 No Content, and JSON parsing.\n */\n async function handleResponse<TResponse>(\n response: Response,\n method: string,\n _url: string,\n ): Promise<TResponse> {\n const headerRequestId = getRequestIdFromHeaders(response.headers);\n\n if (response.status === 401 && onAuthError) {\n onAuthError();\n }\n\n if (!response.ok) {\n // Read body as text first to avoid SyntaxError from response.json()\n // when server returns non-JSON bodies with application/json content-type.\n const errorText = await response.text().catch(() => \"\");\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType?.includes(\"application/json\")) {\n // Parsed as `unknown` and narrowed before any property read: a body of\n // `null` (or a bare string/number/array) is valid JSON, and reading\n // `.error` off it directly threw a TypeError instead of an ApiError.\n let parsed: unknown;\n try {\n parsed = JSON.parse(errorText);\n } catch {\n throw new ApiError(\n errorText.slice(0, 200) ||\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\n headerRequestId,\n );\n }\n\n const body = toApiErrorBody(parsed);\n // Only an object envelope can carry a message. Some Rails BFF endpoints\n // return `{ error: { message, details } }` instead of `{ message }` or\n // `{ error_message }`, so that shape is tried last.\n const msg = body\n ? (() => {\n const nestedError =\n typeof body.error === \"object\" && body.error !== null\n ? (body.error as { message?: unknown }).message\n : undefined;\n const directError =\n typeof body.error === \"string\" ? body.error : undefined;\n const message =\n typeof body.message === \"string\" ? body.message : undefined;\n const errorMessage =\n typeof body.error_message === \"string\"\n ? body.error_message\n : undefined;\n return (\n message ||\n errorMessage ||\n directError ||\n (typeof nestedError === \"string\" ? nestedError : undefined)\n );\n })()\n : undefined;\n\n throw new ApiError(\n msg || `${method} request failed with status ${response.status}`,\n response.status,\n body ? toApiErrorData(body) : (parsed as ApiErrorData),\n headerRequestId ?? getRequestIdFromJsonBody(parsed),\n body,\n );\n } else {\n throw new ApiError(\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\n headerRequestId,\n );\n }\n }\n\n if (\n response.status === 204 ||\n response.headers.get(\"content-length\") === \"0\"\n ) {\n return null as TResponse;\n }\n\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType?.includes(\"application/json\")) {\n const responseText = await response.text();\n\n try {\n const data = JSON.parse(responseText);\n return data as TResponse;\n } catch {\n if (throwOnInvalidJson) {\n throw new ApiError(\n \"Failed to parse response as JSON\",\n response.status,\n null,\n headerRequestId,\n );\n }\n\n // API declared JSON content-type but body isn't valid JSON.\n // Return the raw payload to preserve the legacy non-strict path.\n return responseText ? (responseText as TResponse) : (null as TResponse);\n }\n }\n\n // Non-JSON response (text/plain, text/html, etc.)\n return null as TResponse;\n }\n\n function getNetworkRetryDelayMs(retryAttempt: number): number {\n return baseNetworkRetryDelayMs * 2 ** (retryAttempt - 1);\n }\n\n async function waitForNetworkRetry(retryAttempt: number): Promise<void> {\n const delayMs = getNetworkRetryDelayMs(retryAttempt);\n if (delayMs <= 0) {\n return;\n }\n\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n\n async function fetchWithNetworkRetry(\n url: string,\n fetchOptions: RequestInit,\n signal?: AbortSignal,\n ): Promise<Response> {\n let retryCount = 0;\n\n while (true) {\n try {\n return await fetch(url, fetchOptions);\n } catch (networkError) {\n if (signal?.aborted || retryCount >= maxNetworkRetries) {\n throw networkError;\n }\n\n retryCount += 1;\n await waitForNetworkRetry(retryCount);\n\n if (signal?.aborted) {\n throw networkError;\n }\n }\n }\n }\n\n /**\n * Main request function\n */\n async function request<TResponse = unknown>(\n endpoint: string,\n options: RequestOptions = {},\n ): Promise<TResponse> {\n const {\n method = \"GET\",\n headers: customHeaders,\n params,\n body,\n signal,\n priority,\n } = options;\n\n const url = params ? buildUrl(endpoint, params) : joinUrl(endpoint);\n\n const headers = await buildHeaders(customHeaders);\n\n let response: Response;\n\n try {\n const fetchOptions: RequestInit = { method, headers };\n if (credentials) fetchOptions.credentials = credentials;\n if (cache) fetchOptions.cache = cache;\n if (priority) fetchOptions.priority = priority;\n const serializedBody =\n body && method !== \"GET\" ? JSON.stringify(body) : null;\n if (serializedBody) fetchOptions.body = serializedBody;\n if (signal) fetchOptions.signal = signal;\n response = await fetchWithNetworkRetry(url, fetchOptions, signal);\n } catch (networkError) {\n throw new ApiError(\n `Network error: ${networkError instanceof Error ? networkError.message : \"Unknown network error\"}`,\n 0,\n null,\n );\n }\n\n return handleResponse<TResponse>(response, method, url);\n }\n\n /**\n * Request with FormData (for file uploads)\n */\n async function requestWithFormData<TResponse = unknown>(\n endpoint: string,\n formData: FormData,\n options: Omit<RequestOptions, \"body\" | \"params\"> & {\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n } = {},\n ): Promise<TResponse> {\n const {\n method = \"POST\",\n headers: customHeaders,\n signal,\n priority,\n } = options;\n\n const url = joinUrl(endpoint);\n const headers = await buildHeaders(customHeaders);\n\n // Remove Content-Type to let browser set it with boundary\n delete headers[\"Content-Type\"];\n\n let response: Response;\n\n try {\n const fetchOptions: RequestInit = { method, headers, body: formData };\n if (credentials) fetchOptions.credentials = credentials;\n if (cache) fetchOptions.cache = cache;\n if (priority) fetchOptions.priority = priority;\n if (signal) fetchOptions.signal = signal;\n response = await fetchWithNetworkRetry(url, fetchOptions, signal);\n } catch (networkError) {\n throw new ApiError(\n `Network error: ${networkError instanceof Error ? networkError.message : \"Unknown network error\"}`,\n 0,\n null,\n );\n }\n\n return handleResponse<TResponse>(response, method, url);\n }\n\n // Return client with convenience methods\n return {\n request: request,\n requestWithFormData: requestWithFormData,\n\n // Convenience methods for common HTTP verbs\n get: <TResponse = unknown>(\n endpoint: string,\n params?: Record<string, unknown>,\n options?: Omit<RequestOptions, \"method\" | \"params\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"GET\" as const,\n ...(params && { params }),\n }),\n\n post: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"POST\",\n body,\n }),\n\n put: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"PUT\",\n body,\n }),\n\n patch: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"PATCH\",\n body,\n }),\n\n delete: <TResponse = unknown>(\n endpoint: string,\n options?: Omit<RequestOptions, \"method\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"DELETE\",\n }),\n };\n}\n\nexport type FetchClient = FetchClientInstance;\n","import {\n createFetchClient,\n type FetchClientInstance,\n} from \"@fluid-app/api-client-core\";\nimport { getAuthToken } from \"@fluid-app/fluid-cli\";\n\nexport type ApiClient = FetchClientInstance;\n\n/** Base URL for all API calls. Set FLUID_API_BASE to point at staging. */\nfunction getApiBase(): string {\n return process.env[\"FLUID_API_BASE\"] ?? \"https://api.fluid.app\";\n}\n\n/**\n * Resolve the auth token the same way the core CLI does (see\n * `resolveToken` in packages/cli/core/src/domain/context.ts): the\n * FLUID_TOKEN / FLUID_API_TOKEN env vars win over the stored profile\n * from `fluid login`. Without the env-var step, automation that exports\n * FLUID_TOKEN to pick a company would silently operate against whatever\n * profile happens to be active instead.\n */\nexport function resolveAuthToken(): string | null {\n const envToken = process.env[\"FLUID_TOKEN\"] ?? process.env[\"FLUID_API_TOKEN\"];\n if (envToken) return envToken;\n return getAuthToken();\n}\n\nexport function createApiClient(tokenOverride?: string): ApiClient {\n return createFetchClient({\n baseUrl: getApiBase(),\n getAuthToken: () => tokenOverride ?? resolveAuthToken(),\n });\n}\n\nexport function requireToken(): string {\n const token = resolveAuthToken();\n if (!token) {\n console.error(\n \"Not logged in. Run `fluid login` first (or set FLUID_TOKEN).\",\n );\n process.exit(1);\n }\n return token;\n}\n\n// ── Legacy (MySite) theme API ────────────────────────────────────────\n//\n// A MySite theme is a Themes::LegacyTheme with exactly ONE template\n// carrying three Liquid/CSS blobs: content, stylesheet, head. There is\n// no draft state through this API — PATCHing a template publishes it\n// immediately (Rails calls template.publish on every save). That is\n// why `mysite dev` syncs to a scratch copy and `mysite publish` is the\n// only command that touches the real theme.\n\nexport interface MysiteTemplate {\n id?: number;\n name?: string | null;\n content: string | null;\n stylesheet: string | null;\n head: string | null;\n status?: string | null;\n}\n\nexport interface MysiteTheme {\n id: number;\n name: string;\n description?: string | null;\n image_url?: string | null;\n public?: boolean;\n company_id?: number;\n template?: MysiteTemplate | null;\n}\n\nexport interface MysiteFiles {\n content: string;\n stylesheet: string;\n head: string;\n}\n\nexport async function listMysiteThemes(api: ApiClient): Promise<MysiteTheme[]> {\n const res = await api.get<{ legacy_themes?: MysiteTheme[] }>(\n \"/api/legacy_themes\",\n );\n return res.legacy_themes ?? [];\n}\n\nexport async function getMysiteTheme(\n api: ApiClient,\n id: number,\n): Promise<MysiteTheme> {\n const res = await api.get<{ legacy_theme: MysiteTheme }>(\n `/api/legacy_themes/${id}`,\n );\n return res.legacy_theme;\n}\n\nexport async function createMysiteTheme(\n api: ApiClient,\n args: {\n name: string;\n description?: string;\n template: MysiteFiles;\n /** Dev copies send false so they stay out of the member-facing\n * theme picker. Requires the Rails create action to accept the\n * param (fluid PR pairs with this one); older servers ignore it\n * and force public — visible but otherwise harmless. */\n public?: boolean;\n },\n): Promise<MysiteTheme> {\n const res = await api.post<{ legacy_theme: MysiteTheme }>(\n \"/api/legacy_themes\",\n { legacy_theme: args },\n );\n return res.legacy_theme;\n}\n\n/** PATCH the theme's template blobs. WARNING: this publishes\n * immediately — the server has no draft state for MySite themes. */\nexport async function updateMysiteTemplate(\n api: ApiClient,\n id: number,\n template: MysiteFiles,\n): Promise<MysiteTheme> {\n const res = await api.patch<{ legacy_theme: MysiteTheme }>(\n `/api/legacy_themes/${id}`,\n { legacy_theme: { template } },\n );\n return res.legacy_theme;\n}\n\n// ── Current user ─────────────────────────────────────────────────────\n\n/** `GET /api/me` exposes `mysite_url` = `{company base_url}my/{username}`\n * for the active company — exactly the page the dev server should\n * preview. Returns null when the user has no username/membership in\n * the company (no MySite to preview). */\nexport async function fetchMysiteUrl(api: ApiClient): Promise<URL | null> {\n const me = await api.get<{ mysite_url?: string | null }>(\"/api/me\");\n if (!me.mysite_url) return null;\n try {\n return new URL(me.mysite_url);\n } catch {\n return null;\n }\n}\n","import { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport { createApiClient, listMysiteThemes, requireToken } from \"../api.js\";\n\nexport function createListCommand(): Command {\n return new Command(\"list\")\n .description(\"List the company's MySite themes\")\n .action(async () => {\n requireToken();\n const api = createApiClient();\n const themes = await listMysiteThemes(api);\n if (themes.length === 0) {\n console.log(\"No MySite themes found for this company.\");\n return;\n }\n for (const t of themes) {\n console.log(\n `${chalk.cyan(String(t.id).padStart(8))} ${t.name}${\n t.public ? chalk.dim(\" (public)\") : \"\"\n }`,\n );\n }\n console.log(\n chalk.dim(\n `\\nClone one locally: fluid mysite pull --theme <id> --root <dir>`,\n ),\n );\n });\n}\n","import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { MysiteFiles, MysiteTemplate } from \"./api.js\";\n\n// Local layout for a pulled MySite theme. A MySite theme is a single\n// template with three blobs, so the whole \"project\" is three files\n// plus a manifest:\n//\n// content.liquid — the page body (Liquid)\n// head.liquid — extra <head> markup (Liquid)\n// stylesheet.css — theme CSS\n// .fluid-mysite.json — theme id/name + the dev-copy id `mysite dev`\n// syncs against\n\nexport const MANIFEST_FILE = \".fluid-mysite.json\";\n\nexport const THEME_FILES = {\n content: \"content.liquid\",\n head: \"head.liquid\",\n stylesheet: \"stylesheet.css\",\n} as const;\n\nexport interface MysiteManifest {\n themeId: number;\n name: string;\n /** Scratch copy `mysite dev` syncs to (created lazily on first dev\n * run). Kept out of the way of the real theme because template\n * updates publish immediately server-side. */\n devThemeId?: number;\n}\n\nexport function manifestPath(root: string): string {\n return path.join(root, MANIFEST_FILE);\n}\n\nexport async function readManifest(\n root: string,\n): Promise<MysiteManifest | null> {\n const p = manifestPath(root);\n if (!existsSync(p)) return null;\n try {\n const parsed = JSON.parse(await readFile(p, \"utf8\")) as MysiteManifest;\n return typeof parsed.themeId === \"number\" ? parsed : null;\n } catch {\n return null;\n }\n}\n\nexport async function writeManifest(\n root: string,\n manifest: MysiteManifest,\n): Promise<void> {\n await mkdir(root, { recursive: true });\n await writeFile(\n manifestPath(root),\n JSON.stringify(manifest, null, 2) + \"\\n\",\n \"utf8\",\n );\n}\n\nexport async function writeThemeFiles(\n root: string,\n template: MysiteTemplate | null | undefined,\n): Promise<string[]> {\n await mkdir(root, { recursive: true });\n const written: string[] = [];\n for (const key of Object.keys(THEME_FILES) as Array<keyof MysiteFiles>) {\n const file = THEME_FILES[key];\n const target = path.join(root, file);\n const blob = template?.[key];\n // A blob the server did not send is NOT an instruction to empty the\n // local file. `?? \"\"` used to make those indistinguishable, so a\n // response missing `stylesheet` (or a null one) blanked a real\n // stylesheet.css on disk — and since `mysite publish` PATCHes the\n // live theme with whatever is on disk, the next publish pushed that\n // empty file live.\n //\n // This is the exact mirror of the contract readThemeFiles already\n // holds on the way out, whose tests say it \"must never substitute ''\n // for a file it couldn't read — that silently wipes the live\n // content\". The same reasoning applies inbound.\n if (blob == null && existsSync(target)) continue;\n await writeFile(target, blob ?? \"\", \"utf8\");\n written.push(file);\n }\n return written;\n}\n\n/**\n * Thrown by `readThemeFiles` when any of the three theme files is\n * missing or unreadable. The server has no draft state — a PATCH with a\n * blank blob immediately wipes the corresponding LIVE content — so\n * callers must treat this as fatal and never fall back to \"\".\n */\nexport class MissingThemeFilesError extends Error {\n readonly missing: readonly string[];\n\n constructor(missing: readonly string[]) {\n super(`Missing or unreadable local theme file(s): ${missing.join(\", \")}`);\n this.name = \"MissingThemeFilesError\";\n this.missing = missing;\n }\n}\n\n/**\n * Read the three local theme files, failing loudly (with every problem\n * file named) instead of silently substituting \"\" for a missing one —\n * an empty blob sent to the update API overwrites the live content\n * with nothing. Intentionally empty files are still fine; only\n * missing/unreadable ones are errors.\n */\nexport async function readThemeFiles(root: string): Promise<MysiteFiles> {\n const problems: string[] = [];\n const read = async (file: string): Promise<string> => {\n const p = path.join(root, file);\n if (!existsSync(p)) {\n problems.push(file);\n return \"\";\n }\n try {\n return await readFile(p, \"utf8\");\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n problems.push(`${file} (${reason})`);\n return \"\";\n }\n };\n const files: MysiteFiles = {\n content: await read(THEME_FILES.content),\n head: await read(THEME_FILES.head),\n stylesheet: await read(THEME_FILES.stylesheet),\n };\n if (problems.length > 0) throw new MissingThemeFilesError(problems);\n return files;\n}\n\nexport function themeFilePaths(root: string): string[] {\n return Object.values(THEME_FILES).map((f) => path.join(root, f));\n}\n","import path from \"node:path\";\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport { createApiClient, getMysiteTheme, requireToken } from \"../api.js\";\nimport {\n readManifest,\n writeManifest,\n writeThemeFiles,\n MANIFEST_FILE,\n} from \"../theme-files.js\";\n\nexport function createPullCommand(): Command {\n return new Command(\"pull\")\n .description(\n \"Download a MySite theme's Liquid + CSS into a local directory\",\n )\n .requiredOption(\"-t, --theme <id>\", \"MySite theme id (see `mysite list`)\")\n .option(\"--root <dir>\", \"Target directory\", \".\")\n .option(\"--yes\", \"Overwrite existing local files without asking\", false)\n .action(async (opts: { theme: string; root: string; yes: boolean }) => {\n requireToken();\n const themeId = Number(opts.theme);\n if (!Number.isInteger(themeId)) {\n console.error(`Invalid theme id: ${opts.theme}`);\n process.exit(1);\n }\n const root = path.resolve(opts.root);\n\n const existing = await readManifest(root);\n if (existing && existing.themeId !== themeId && !opts.yes) {\n console.error(\n `${root} already tracks MySite theme #${existing.themeId} (${existing.name}).\\n` +\n `Re-run with --yes to overwrite it with theme #${themeId}, or pick a different --root.`,\n );\n process.exit(1);\n }\n\n const api = createApiClient();\n const theme = await getMysiteTheme(api, themeId);\n const written = await writeThemeFiles(root, theme.template);\n await writeManifest(root, {\n themeId: theme.id,\n name: theme.name,\n // Preserve an existing dev copy when re-pulling the same theme\n // so `mysite dev` keeps reusing it instead of littering the\n // company with scratch themes.\n ...(existing && existing.themeId === theme.id && existing.devThemeId\n ? { devThemeId: existing.devThemeId }\n : {}),\n });\n\n console.log(\n `${chalk.green(\"✔\")} Pulled ${chalk.bold(theme.name)} (#${theme.id}) into ${root}`,\n );\n for (const f of written) console.log(` ${chalk.dim(\"wrote\")} ${f}`);\n console.log(` ${chalk.dim(\"wrote\")} ${MANIFEST_FILE}`);\n console.log(\n `\\nNext: ${chalk.cyan(\"fluid mysite dev\")} to preview edits live, ` +\n `${chalk.cyan(\"fluid mysite publish\")} to push them to the live theme.`,\n );\n });\n}\n","/**\n * Naming for the `fluid mysite dev` scratch theme.\n *\n * Its own module, with no chalk/API imports, so the safety property\n * below can be unit-tested without standing up the command.\n */\n\n/** MySite theme names are capped at 50 characters server-side. */\nconst MAX_THEME_NAME = 50;\n/** Keep at least this much of the theme name so the copy is still\n * recognisable in `fluid mysite list`. */\nconst MIN_BASE = 8;\n\n/**\n * Name for the `fluid mysite dev` scratch copy — see ensureDevCopy for\n * why it must differ from the live theme's name.\n *\n * The BASE name is truncated, not the finished string. Truncating the\n * finished string is what broke the invariant: at exactly\n * MAX_THEME_NAME characters `${name} (dev …)`.slice(0, 50) returns the\n * live theme's name byte for byte, and from ~24 characters up (on a\n * typical hostname) the random half was already being cut off, so two\n * machines could mint the same name.\n *\n * The host is shortened only as far as it has to be — it keeps its\n * full length whenever there is room, so short theme names produce\n * exactly the name they always did. Only the random hex is\n * unconditional, because that is the entropy the invariant rests on.\n */\nexport function devCopyName(\n themeName: string,\n host: string,\n hex: string,\n): string {\n // \" (dev \" + \"-\" + hex + \")\" — everything in the tail that is not\n // the host.\n const fixed = \" (dev \".length + 1 + hex.length + 1;\n const hostRoom = Math.max(3, MAX_THEME_NAME - MIN_BASE - fixed);\n const shortHost = (host.split(\".\")[0] || \"dev\").slice(0, hostRoom) || \"dev\";\n const tail = ` (dev ${shortHost}-${hex})`;\n const base = themeName\n .slice(0, Math.max(0, MAX_THEME_NAME - tail.length))\n .trimEnd();\n return `${base}${tail}`;\n}\n","export type ReloadMode = \"full-page\" | \"off\";\n\nexport function buildHotReloadScript(mode: ReloadMode): string {\n return `\n<script>\n(() => {\n window.__FLUID_CLI_ENV__ = ${JSON.stringify({ mode })};\n\n class HotReload {\n static reloadMode() { return window.__FLUID_CLI_ENV__.mode; }\n static isActive() { return HotReload.reloadMode() !== \"off\"; }\n static refresh() {\n console.log(\"[HotReload] Refreshing page\");\n window.location.reload();\n }\n }\n\n class SSEClient {\n constructor(url, handler) {\n if (typeof EventSource === \"undefined\") {\n console.error(\"[HotReload] EventSource not supported in this browser.\");\n return;\n }\n console.log(\"[HotReload] Initializing…\");\n this.url = url;\n this.handler = handler;\n }\n connect() {\n const es = new EventSource(this.url);\n es.onopen = () => console.log(\"[HotReload] SSE connected.\");\n es.onerror = () => {\n console.log(\"[HotReload] SSE closed. Reconnecting in 5s…\");\n es.close();\n setTimeout(() => this.connect(), 5000);\n };\n es.onmessage = (msg) => {\n let data;\n try {\n data = JSON.parse(msg.data);\n } catch {\n return; // non-JSON frame (keep-alive etc.) — ignore\n }\n this.handler(data);\n };\n }\n }\n\n if (HotReload.isActive()) {\n new SSEClient(\"/hot-reload\", (data) => {\n if (data.reload_page) HotReload.refresh();\n }).connect();\n }\n})();\n</script>`;\n}\n\nexport function injectHotReload(html: string, mode: ReloadMode): string {\n const script = buildHotReloadScript(mode);\n if (html.includes(\"</body>\")) {\n return html.replace(\"</body>\", `${script}\\n</body>`);\n }\n return html + script;\n}\n","import https from \"node:https\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { injectHotReload, type ReloadMode } from \"./hot-reload.js\";\n\n// Localhost → production proxy for MySite previews. MySite pages are\n// rendered server-side (Liquid via PageBuilder), and the ONLY draft\n// mechanism is the `preview_theme_id` query param on the public\n// visitor route (`/my/:username`). A raw preview URL loses that param\n// on every internal link click — the proxy pins it on every request\n// instead, and injects a live-reload SSE client into HTML responses\n// so saves refresh the page.\n//\n// Adapted from theme-dev's proxy; MySite needs neither the\n// `x-fluid-theme` header nor the `replace_templates` trick (drafts\n// live on a synced scratch theme, not in pending local files), so\n// this version is a straight pass-through plus one query param.\n\nconst HOP_BY_HOP = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n \"content-security-policy\",\n]);\n\nexport interface ProxyOptions {\n /** Production host serving the MySite (from /api/me's mysite_url). */\n host: string;\n /** Scratch theme id to pin via preview_theme_id. */\n previewThemeId: number;\n reloadMode: ReloadMode;\n}\n\nexport async function proxyRequest(\n req: IncomingMessage,\n res: ServerResponse,\n opts: ProxyOptions,\n): Promise<void> {\n const headers: Record<string, string> = {};\n for (const [k, v] of Object.entries(req.headers)) {\n if (!HOP_BY_HOP.has(k.toLowerCase()) && typeof v === \"string\") {\n headers[k] = v;\n }\n }\n headers[\"host\"] = opts.host;\n headers[\"user-agent\"] = \"Fluid CLI\";\n headers[\"accept-encoding\"] = \"identity\";\n\n const url = new URL(req.url ?? \"/\", `http://${req.headers.host}`);\n // Harmless on asset/API requests; the mysites controller reads it\n // on page requests to render the scratch theme.\n url.searchParams.set(\"preview_theme_id\", String(opts.previewThemeId));\n\n const isGet = req.method === \"GET\" || req.method === \"HEAD\";\n let body: Buffer | undefined;\n if (!isGet) {\n body = await readBody(req);\n if (body.length > 0) {\n headers[\"content-length\"] = String(body.length);\n }\n }\n\n return new Promise((resolve, reject) => {\n const options: https.RequestOptions = {\n hostname: opts.host,\n port: 443,\n path: url.pathname + (url.search || \"\"),\n method: req.method ?? \"GET\",\n headers,\n };\n\n const [hostname, portStr] = opts.host.split(\":\");\n const port = portStr ? Number(portStr) : 443;\n options.hostname = hostname;\n options.port = port;\n\n const proxyReq = https.request(options, (proxyRes) => {\n // Upstream can drop mid-stream (network blip, server restart) —\n // without this the \"error\" event is unhandled, which crashes the\n // process and leaves the promise hung.\n proxyRes.on(\"error\", reject);\n const contentType = proxyRes.headers[\"content-type\"] ?? \"\";\n const isHtml = contentType.includes(\"text/html\");\n\n const responseHeaders: Record<string, string | string[]> = {};\n for (const [k, v] of Object.entries(proxyRes.headers)) {\n if (!HOP_BY_HOP.has(k.toLowerCase()) && v !== undefined) {\n responseHeaders[k] = v as string | string[];\n }\n }\n\n if (isHtml) {\n const chunks: Buffer[] = [];\n proxyRes.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n proxyRes.on(\"end\", () => {\n let html = Buffer.concat(chunks).toString(\"utf-8\");\n html = injectHotReload(html, opts.reloadMode);\n responseHeaders[\"content-length\"] = String(Buffer.byteLength(html));\n res.writeHead(proxyRes.statusCode ?? 200, responseHeaders);\n res.end(html);\n resolve();\n });\n } else {\n res.writeHead(proxyRes.statusCode ?? 200, responseHeaders);\n proxyRes.pipe(res);\n proxyRes.on(\"end\", resolve);\n }\n });\n\n proxyReq.on(\"error\", (err) => {\n reject(err);\n });\n\n if (body) proxyReq.write(body);\n proxyReq.end();\n });\n}\n\nfunction readBody(req: IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n","import type { ServerResponse } from \"node:http\";\n\nexport class SSEStream {\n private responses = new Set<ServerResponse>();\n\n add(res: ServerResponse): void {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n \"Access-Control-Allow-Origin\": \"*\",\n });\n res.write(\":\\n\\n\");\n this.responses.add(res);\n res.on(\"close\", () => this.responses.delete(res));\n }\n\n broadcast(data: string): void {\n const payload = `data: ${data}\\n\\n`;\n for (const res of this.responses) {\n try {\n res.write(payload);\n } catch {\n this.responses.delete(res);\n }\n }\n }\n\n close(): void {\n for (const res of this.responses) {\n try {\n res.end();\n } catch {\n // ignore\n }\n }\n this.responses.clear();\n }\n\n get size(): number {\n return this.responses.size;\n }\n}\n","import { devCopyName } from \"./dev-copy-name.js\";\nimport http from \"node:http\";\nimport path from \"node:path\";\nimport { hostname } from \"node:os\";\nimport { randomBytes } from \"node:crypto\";\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport chokidar from \"chokidar\";\nimport {\n createApiClient,\n createMysiteTheme,\n fetchMysiteUrl,\n getMysiteTheme,\n requireToken,\n updateMysiteTemplate,\n type ApiClient,\n type MysiteFiles,\n} from \"../api.js\";\nimport {\n MissingThemeFilesError,\n readManifest,\n readThemeFiles,\n themeFilePaths,\n writeManifest,\n type MysiteManifest,\n} from \"../theme-files.js\";\nimport { proxyRequest } from \"../dev-server/proxy.js\";\nimport { SSEStream } from \"../dev-server/sse.js\";\nimport type { ReloadMode } from \"../dev-server/hot-reload.js\";\n\nconst DEFAULT_PORT = 9293;\n\n// Server-side rendering is the only way to see a MySite theme (Liquid\n// + drops like fluid_affiliate resolve on the Rails side), and template\n// PATCHes publish immediately — so \"test before committing\" means:\n//\n// 1. keep a scratch copy of the theme (created once, reused, and\n// never assigned to anyone — only reachable via preview_theme_id)\n// 2. PATCH the scratch copy on every local save\n// 3. proxy localhost → the user's real /my/:username page with\n// preview_theme_id pinned, live-reloading on sync\n//\n// The real theme is only touched by `fluid mysite publish`.\n\nexport function createDevCommand(): Command {\n return new Command(\"dev\")\n .description(\n \"Preview local MySite edits live without touching the published theme\",\n )\n .option(\"--root <dir>\", \"Theme directory (from `mysite pull`)\", \".\")\n .option(\"--port <port>\", \"Local preview port\", String(DEFAULT_PORT))\n .option(\n \"--live-reload <mode>\",\n \"Reload browser on save: full-page | off\",\n \"full-page\",\n )\n .action(\n async (opts: { root: string; port: string; liveReload: string }) => {\n requireToken();\n const root = path.resolve(opts.root);\n const port = Number(opts.port) || DEFAULT_PORT;\n const reloadMode: ReloadMode =\n opts.liveReload === \"off\" ? \"off\" : \"full-page\";\n\n const manifest = await readManifest(root);\n if (!manifest) {\n console.error(\n `No ${chalk.bold(\".fluid-mysite.json\")} in ${root}. ` +\n `Run ${chalk.cyan(\"fluid mysite pull --theme <id>\")} first.`,\n );\n process.exit(1);\n }\n\n // Fail fast (before any theme is created or synced) when a local\n // theme file is missing — syncing would blank the corresponding\n // content on the dev copy. The validated snapshot is what a\n // fresh dev copy is seeded from, so there's no second unguarded\n // read between here and the copy's creation.\n const files = await readThemeFilesOrExit(root);\n\n const api = createApiClient();\n\n const mysiteUrl = await fetchMysiteUrl(api);\n if (!mysiteUrl) {\n console.error(\n \"Couldn't determine your MySite URL — your user needs a username \" +\n \"in this company to have a /my page to preview against.\",\n );\n process.exit(1);\n }\n\n const devThemeId = await ensureDevCopy(api, root, manifest, files);\n\n // Initial sync so the scratch copy reflects local files even if\n // they were edited while dev wasn't running.\n await syncToDevCopy(api, root, devThemeId, \"initial sync\");\n\n const sse = new SSEStream();\n const server = http.createServer((req, res) => {\n if (req.url === \"/hot-reload\") {\n sse.add(res);\n return;\n }\n // Send the bare origin to the user's MySite page so consumers\n // that only know the port (Mist's preview webview, a human\n // typing localhost:9293) land on the right path without\n // knowing the username.\n if ((req.url ?? \"/\").split(\"?\")[0] === \"/\") {\n res.writeHead(302, { location: mysiteUrl.pathname });\n res.end();\n return;\n }\n void proxyRequest(req, res, {\n host: mysiteUrl.host,\n previewThemeId: devThemeId,\n reloadMode,\n }).catch((err) => {\n if (!res.headersSent) res.writeHead(502);\n res.end(`Proxy error: ${err instanceof Error ? err.message : err}`);\n });\n });\n\n const stopWatcher = watchThemeFiles(root, async () => {\n const ok = await syncToDevCopy(api, root, devThemeId, \"sync\");\n if (ok) sse.broadcast(JSON.stringify({ reload_page: true }));\n });\n\n // Walk forward from the requested port if it's taken — the\n // team runs multiple previews side by side (and Mist will\n // spawn these programmatically), so EADDRINUSE shouldn't be\n // a crash. An explicitly passed --port that's busy still\n // gets a clear message about where we actually bound.\n const boundPort = await listenOnAvailablePort(server, port);\n if (boundPort !== port) {\n console.log(\n chalk.dim(`Port ${port} is in use — using ${boundPort} instead.`),\n );\n }\n {\n const localUrl = `http://127.0.0.1:${boundPort}${mysiteUrl.pathname}`;\n const prodPreview = `${mysiteUrl.href}?preview_theme_id=${devThemeId}`;\n console.log(\n `\\n${chalk.green(\"✔\")} Dev copy ${chalk.bold(`#${devThemeId}`)} ready — the live theme (#${manifest.themeId}) stays untouched.\\n`,\n );\n console.log(\n ` ${chalk.bold(\"Local preview:\")} ${chalk.cyan(localUrl)}`,\n );\n console.log(\n ` ${chalk.bold(\"Production preview:\")} ${chalk.cyan(prodPreview)}`,\n );\n console.log(\n ` ${chalk.bold(\"Live site:\")} ${chalk.dim(mysiteUrl.href)}\\n`,\n );\n console.log(\n chalk.dim(\n \"Edits to content.liquid / head.liquid / stylesheet.css sync on save. \" +\n \"Run `fluid mysite publish` to push them live. Ctrl+C to stop.\",\n ),\n );\n }\n\n const shutdown = async () => {\n sse.close();\n server.close();\n await stopWatcher();\n process.exit(0);\n };\n process.on(\"SIGINT\", () => void shutdown());\n process.on(\"SIGTERM\", () => void shutdown());\n },\n );\n}\n\n/** Bind to `startPort`, walking forward on EADDRINUSE (up to\n * `attempts` ports) so parallel previews coexist. Resolves with the\n * port actually bound; rejects on non-port errors. */\nfunction listenOnAvailablePort(\n server: http.Server,\n startPort: number,\n attempts = 20,\n): Promise<number> {\n return new Promise((resolve, reject) => {\n let port = startPort;\n const tryListen = () => {\n const onError = (err: NodeJS.ErrnoException) => {\n server.removeListener(\"listening\", onListening);\n if (err.code === \"EADDRINUSE\" && port - startPort < attempts) {\n port += 1;\n tryListen();\n } else {\n reject(err);\n }\n };\n const onListening = () => {\n server.removeListener(\"error\", onError);\n resolve(port);\n };\n server.once(\"error\", onError);\n server.once(\"listening\", onListening);\n server.listen(port, \"127.0.0.1\");\n };\n tryListen();\n });\n}\n\n/** Read the theme files, exiting with a clean, user-facing message\n * (instead of an unhandled rejection) when any are missing. */\nasync function readThemeFilesOrExit(root: string): Promise<MysiteFiles> {\n try {\n return await readThemeFiles(root);\n } catch (err) {\n if (err instanceof MissingThemeFilesError) {\n console.error(\n `${chalk.red(\"✖\")} ${err.message}\\n` +\n `Restore the file(s), or re-run ${chalk.cyan(\"fluid mysite pull\")} ` +\n \"to re-download the theme, then try again.\",\n );\n process.exit(1);\n }\n throw err;\n }\n}\n\n/** Find or create the scratch theme this directory syncs against,\n * seeding a fresh copy from the already-validated `files` snapshot\n * (no re-read, so no window where a just-deleted file could crash or\n * blank the copy).\n *\n * The name MUST be unique within the company: the Rails update action\n * resolves templates by (name, themeable_type, company), so a scratch\n * copy sharing the real theme's name would silently write to the REAL\n * template. A random suffix keeps them apart. */\nasync function ensureDevCopy(\n api: ApiClient,\n root: string,\n manifest: MysiteManifest,\n files: MysiteFiles,\n): Promise<number> {\n if (manifest.devThemeId) {\n try {\n const existing = await getMysiteTheme(api, manifest.devThemeId);\n return existing.id;\n } catch {\n console.log(\n chalk.dim(\n `Stored dev copy #${manifest.devThemeId} is gone — creating a fresh one.`,\n ),\n );\n }\n }\n\n // See devCopyName: the base name is truncated, not the finished\n // string, so the random suffix always survives.\n const name = devCopyName(\n manifest.name,\n hostname(),\n randomBytes(2).toString(\"hex\"),\n );\n const created = await createMysiteTheme(api, {\n name,\n description:\n \"Fluid CLI development copy — auto-created by `fluid mysite dev`. Safe to delete.\",\n template: files,\n public: false,\n });\n await writeManifest(root, { ...manifest, devThemeId: created.id });\n return created.id;\n}\n\nasync function syncToDevCopy(\n api: ApiClient,\n root: string,\n devThemeId: number,\n label: string,\n): Promise<boolean> {\n const started = Date.now();\n try {\n const files = await readThemeFiles(root);\n await updateMysiteTemplate(api, devThemeId, files);\n console.log(\n `${chalk.green(\"✔\")} ${label} ${chalk.dim(`(${Date.now() - started}ms)`)}`,\n );\n return true;\n } catch (err) {\n console.error(\n `${chalk.red(\"✖\")} ${label} failed: ${err instanceof Error ? err.message : err}`,\n );\n return false;\n }\n}\n\n/** Watch the three theme files, coalescing rapid saves into one sync. */\nfunction watchThemeFiles(\n root: string,\n onChange: () => Promise<void>,\n): () => Promise<void> {\n const watcher = chokidar.watch(themeFilePaths(root), {\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 },\n });\n let pending = Promise.resolve();\n const enqueue = () => {\n pending = pending.then(onChange).catch(() => {});\n };\n watcher.on(\"change\", enqueue);\n watcher.on(\"add\", enqueue);\n return () => watcher.close();\n}\n","/** Whether to publish, and whether to ask first.\n *\n * `prompts` writes its question and then waits on stdin. With stdin on\n * /dev/null — which is exactly how the agent's `run_cli` spawns a child\n * (`stdio: [\"ignore\", \"pipe\", \"pipe\"]`) — the promise NEVER SETTLES,\n * the event loop drains, and node exits 0. Not even \"Aborted.\" is\n * printed. So `fluid mysite publish` reported success while publishing\n * nothing, and the agent told the user their MySite was live.\n *\n * Refusing loudly is the only safe answer: publishing goes straight to\n * the LIVE theme with no draft state, so silently continuing without a\n * confirmation would be worse than stopping.\n *\n * The portal CLI is the only other place in packages/cli that considers\n * this, and it says why: \"Non-interactive mode: if stdin is not a TTY\n * and there are remaining prompts, return safe defaults instead of\n * hanging on interactive input.\"\n *\n * Its own module so the test can import it without dragging commander,\n * prompts and chalk into jest. */\nexport type PublishGate =\n | { proceed: true; prompt: boolean }\n | { proceed: false; reason: string };\n\nexport function publishGate(opts: {\n yes: boolean;\n isTTY: boolean;\n}): PublishGate {\n if (opts.yes) return { proceed: true, prompt: false };\n if (!opts.isTTY) {\n return {\n proceed: false,\n reason:\n \"Refusing to publish without a confirmation.\\n\" +\n \"Nothing was published — this is a non-interactive session \" +\n \"(stdin is not a TTY), so the confirmation prompt cannot be \" +\n \"answered. Re-run with --yes to publish straight to the LIVE \" +\n \"MySite theme.\",\n };\n }\n return { proceed: true, prompt: true };\n}\n","import path from \"node:path\";\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport prompts from \"prompts\";\nimport {\n createApiClient,\n fetchMysiteUrl,\n requireToken,\n updateMysiteTemplate,\n type MysiteFiles,\n} from \"../api.js\";\nimport { publishGate } from \"./publish-gate.js\";\nimport {\n MissingThemeFilesError,\n readManifest,\n readThemeFiles,\n} from \"../theme-files.js\";\n\nexport function createPublishCommand(): Command {\n return new Command(\"publish\")\n .description(\n \"Push local files to the LIVE MySite theme (immediate — no draft state)\",\n )\n .option(\"--root <dir>\", \"Theme directory (from `mysite pull`)\", \".\")\n .option(\"--yes\", \"Skip the confirmation prompt\", false)\n .action(async (opts: { root: string; yes: boolean }) => {\n requireToken();\n const root = path.resolve(opts.root);\n const manifest = await readManifest(root);\n if (!manifest) {\n console.error(\n `No ${chalk.bold(\".fluid-mysite.json\")} in ${root}. ` +\n `Run ${chalk.cyan(\"fluid mysite pull --theme <id>\")} first.`,\n );\n process.exit(1);\n }\n\n // Validate BEFORE the confirmation prompt and before any remote\n // write: publishing goes straight to the LIVE theme with no draft\n // state, so a missing local file must abort — never PATCH a blank\n // blob over live content.\n let files: MysiteFiles;\n try {\n files = await readThemeFiles(root);\n } catch (err) {\n if (err instanceof MissingThemeFilesError) {\n console.error(\n `${chalk.red(\"✖\")} ${err.message}\\n` +\n \"Publishing would overwrite the live theme's content with \" +\n \"nothing, so nothing was published.\\n\" +\n `Restore the file(s), or re-run ${chalk.cyan(\"fluid mysite pull\")} ` +\n \"to re-download the theme, then try again.\",\n );\n process.exit(1);\n }\n throw err;\n }\n\n const gate = publishGate({ yes: opts.yes, isTTY: !!process.stdin.isTTY });\n if (!gate.proceed) {\n console.error(`${chalk.red(\"✖\")} ${gate.reason}`);\n process.exit(1);\n }\n\n if (gate.prompt) {\n const { confirmed } = await prompts({\n type: \"confirm\",\n name: \"confirmed\",\n message:\n `Publish local files to \"${manifest.name}\" (#${manifest.themeId})? ` +\n \"MySite pages using this theme update immediately.\",\n initial: false,\n });\n if (!confirmed) {\n console.log(\"Aborted.\");\n return;\n }\n }\n\n const api = createApiClient();\n await updateMysiteTemplate(api, manifest.themeId, files);\n console.log(\n `${chalk.green(\"✔\")} Published ${chalk.bold(manifest.name)} (#${manifest.themeId})`,\n );\n\n // Best-effort convenience link; publish already succeeded.\n try {\n const url = await fetchMysiteUrl(api);\n if (url)\n console.log(` ${chalk.dim(\"Live at\")} ${chalk.cyan(url.href)}`);\n } catch {\n // ignore\n }\n });\n}\n","import { Command } from \"commander\";\nimport type { PluginContext } from \"@fluid-app/fluid-cli\";\nimport { createListCommand } from \"./list.js\";\nimport { createPullCommand } from \"./pull.js\";\nimport { createDevCommand } from \"./dev.js\";\nimport { createPublishCommand } from \"./publish.js\";\n\nexport function registerMysiteCommand(ctx: PluginContext): void {\n const cmd = new Command(\"mysite\").description(\n \"MySite theme workflow — list, pull, dev preview server, publish\",\n );\n\n cmd.addCommand(createListCommand());\n cmd.addCommand(createPullCommand());\n cmd.addCommand(createDevCommand());\n cmd.addCommand(createPublishCommand());\n\n ctx.program.addCommand(cmd);\n}\n","import type { FluidPlugin, PluginContext } from \"@fluid-app/fluid-cli\";\nimport { registerMysiteCommand } from \"./commands/mysite.js\";\n\nconst plugin: FluidPlugin = {\n name: \"@fluid-app/fluid-cli-mysite\",\n version: \"0.1.0\",\n register(ctx: PluginContext) {\n registerMysiteCommand(ctx);\n },\n};\n\nexport default plugin;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../platform/api-client-core/src/api-error-shape.ts","../../../platform/api-client-core/src/fetch-client.ts","../src/api.ts","../src/commands/list.ts","../src/theme-files.ts","../src/commands/pull.ts","../src/commands/dev-copy-name.ts","../src/dev-server/hot-reload.ts","../src/dev-server/proxy.ts","../src/dev-server/sse.ts","../src/commands/dev.ts","../src/commands/publish-gate.ts","../src/commands/publish.ts","../src/commands/mysite.ts","../src/index.ts"],"sourcesContent":["/**\n * The shape contract for `ApiError.body` and `ApiError.data`.\n *\n * One decision, in one place: `body` is the response envelope exactly as the\n * server sent it, `data` is the unwrapped field-error bag. Rationale and the\n * alternatives considered live in `docs/api-error-data-contract.md`.\n */\n\n/**\n * The parsed JSON error response body, exactly as the server sent it.\n *\n * This is the envelope: `errors`, `error_message`, `error`, `status`, `meta`\n * and `request_id` all live here. `null` when the response carried no JSON\n * object — HTML, an empty body, a JSON primitive, or unparseable text.\n */\nexport type ApiErrorBody = Record<string, unknown> | null;\n\n/**\n * The field-level error bag: the envelope's `errors` value when the body has\n * one, otherwise the body itself.\n *\n * `errors` is declared `never` because unwrapping already consumed it — read\n * the envelope from `ApiError.body`, not from here. That makes the historical\n * mistake (`error.data.errors.some_field`, which silently resolved to\n * `undefined`) a typecheck failure instead of a degraded error message.\n */\nexport type ApiErrorFieldErrors = Record<string, unknown> & {\n errors?: never;\n};\n\n/**\n * Every value `ApiError.data` can hold. Fluid endpoints return an object bag,\n * but `errors` is occasionally an array or a string, and non-JSON responses\n * carry no data at all.\n */\nexport type ApiErrorData =\n | ApiErrorFieldErrors\n | readonly unknown[]\n | string\n | number\n | boolean\n | null;\n\n/**\n * Narrows a parsed JSON error payload to the envelope contract. Arrays and\n * primitives are not envelopes, so they resolve to `null` — their content is\n * still reachable through `ApiError.data`.\n */\nexport function toApiErrorBody(value: unknown): ApiErrorBody {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return null;\n }\n\n return value as Record<string, unknown>;\n}\n\n/**\n * The single unwrap rule, shared by every producer of `ApiError`.\n *\n * `??` rather than `||` so an explicitly empty `errors` (`\"\"`, `0`, `false`)\n * is preserved instead of silently falling back to the whole envelope.\n */\nexport function toApiErrorData(body: ApiErrorBody): ApiErrorData {\n if (!body) {\n return null;\n }\n\n return (body.errors ?? body) as ApiErrorData;\n}\n","/**\n * Minimal, framework-agnostic fetch client for Fluid APIs\n * Compatible with fluid-admin patterns but usable standalone\n */\n\nimport type { ApiErrorBody, ApiErrorData } from \"./api-error-shape\";\nimport { toApiErrorBody, toApiErrorData } from \"./api-error-shape\";\n\nexport interface FetchClientConfig {\n /**\n * Base URL for all requests (e.g., \"https://api.fluid.app/api\")\n */\n baseUrl: string;\n\n /**\n * Optional function to get auth token\n * Return null/undefined if no token available\n */\n getAuthToken?: () => string | null | Promise<string | null>;\n\n /**\n * Optional callback when 401 auth error occurs\n */\n onAuthError?: () => void;\n\n /**\n * Default headers to include in all requests\n * Example: { \"x-fluid-client\": \"admin\" }\n */\n defaultHeaders?: Record<string, string>;\n\n /**\n * Credentials mode for fetch requests.\n * Set to `\"include\"` for cookie-based (same-origin BFF) authentication.\n * @default undefined (browser default: \"same-origin\")\n */\n credentials?: RequestCredentials;\n\n /**\n * Request cache mode for fetch requests.\n * @default undefined (browser default)\n */\n cache?: RequestCache;\n\n /**\n * Retry configuration for thrown network errors from fetch.\n * Does not retry HTTP error responses or aborted requests.\n * @default undefined (no retries)\n */\n networkRetry?: {\n maxRetries?: number;\n baseDelayMs?: number;\n };\n\n /**\n * Throw ApiError when a successful response declares JSON but cannot be parsed.\n * Defaults to false to preserve the legacy generated-client behavior.\n * @default false\n */\n throwOnInvalidJson?: boolean;\n}\n\nexport interface RequestOptions {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\";\n headers?: Record<string, string>;\n params?: Record<string, unknown>;\n body?: unknown;\n signal?: AbortSignal;\n priority?: RequestInit[\"priority\"];\n}\n\n/**\n * API Error class compatible with fluid-admin's ApiError\n */\nexport class ApiError extends Error {\n public readonly status: number;\n\n /**\n * The unwrapped field-error bag. See `ApiErrorData`; the contract is\n * documented in `docs/api-error-data-contract.md`.\n */\n public readonly data: ApiErrorData;\n\n /**\n * The full error response envelope. Read `error_message`, `error`, `status`\n * and nested `errors` from here — `data` has already unwrapped one level.\n */\n public readonly body: ApiErrorBody;\n\n public readonly requestId?: string;\n\n constructor(\n message: string,\n status: number,\n data?: ApiErrorData,\n requestId?: string,\n body?: ApiErrorBody,\n ) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.data = data ?? null;\n this.body = body ?? null;\n this.requestId = requestId;\n\n if (\"captureStackTrace\" in Error) {\n (\n Error as {\n captureStackTrace: (\n target: Error,\n constructor: NewableFunction,\n ) => void;\n }\n ).captureStackTrace(this, ApiError);\n }\n }\n\n toJSON(): {\n name: string;\n message: string;\n status: number;\n data: ApiErrorData;\n body: ApiErrorBody;\n requestId?: string;\n } {\n return {\n name: this.name,\n message: this.message,\n status: this.status,\n data: this.data,\n body: this.body,\n requestId: this.requestId,\n };\n }\n}\n\nfunction getStringRequestId(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction getRequestIdFromHeaders(headers: Headers): string | undefined {\n return (\n getStringRequestId(headers.get(\"x-request-id\")) ??\n getStringRequestId(headers.get(\"request-id\")) ??\n getStringRequestId(headers.get(\"X-Request-ID\"))\n );\n}\n\nfunction getRequestIdFromJsonBody(body: unknown): string | undefined {\n if (!body || typeof body !== \"object\" || Array.isArray(body)) {\n return undefined;\n }\n\n const record = body as Record<string, unknown>;\n const meta = record.meta;\n\n return (\n getStringRequestId(record.request_id) ??\n getStringRequestId(record.requestId) ??\n (meta && typeof meta === \"object\" && !Array.isArray(meta)\n ? (getStringRequestId((meta as Record<string, unknown>).request_id) ??\n getStringRequestId((meta as Record<string, unknown>).requestId))\n : undefined)\n );\n}\n\n/**\n * Type guard for ApiError\n */\nexport function isApiError(error: unknown): error is ApiError {\n return error instanceof ApiError;\n}\n\nexport interface FetchClientInstance {\n request: <TResponse = unknown>(\n endpoint: string,\n options?: RequestOptions,\n ) => Promise<TResponse>;\n requestWithFormData: <TResponse = unknown>(\n endpoint: string,\n formData: FormData,\n options?: Omit<RequestOptions, \"body\" | \"params\"> & {\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n },\n ) => Promise<TResponse>;\n get: <TResponse = unknown>(\n endpoint: string,\n params?: Record<string, unknown>,\n options?: Omit<RequestOptions, \"method\" | \"params\">,\n ) => Promise<TResponse>;\n post: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n put: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n patch: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n delete: <TResponse = unknown>(\n endpoint: string,\n options?: Omit<RequestOptions, \"method\">,\n ) => Promise<TResponse>;\n}\n\n/**\n * Creates a configured fetch client instance\n */\nexport function createFetchClient(\n config: FetchClientConfig,\n): FetchClientInstance {\n const {\n baseUrl,\n getAuthToken,\n onAuthError,\n defaultHeaders = {},\n credentials,\n cache,\n networkRetry,\n throwOnInvalidJson = false,\n } = config;\n const maxNetworkRetries = Math.max(0, networkRetry?.maxRetries ?? 0);\n const baseNetworkRetryDelayMs = Math.max(0, networkRetry?.baseDelayMs ?? 0);\n\n /**\n * Build headers for a request\n */\n async function buildHeaders(\n customHeaders?: Record<string, string>,\n ): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...defaultHeaders,\n ...customHeaders,\n };\n\n // Add auth token if available\n if (getAuthToken) {\n const token = await getAuthToken();\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n }\n\n return headers;\n }\n\n /**\n * Join baseUrl + endpoint via string concatenation (matches fetchApi).\n * Using `new URL(endpoint, baseUrl)` would strip any path prefix from\n * baseUrl (e.g. \"/api\") when the endpoint starts with \"/\".\n */\n function joinUrl(endpoint: string): string {\n return `${baseUrl}${endpoint}`;\n }\n\n /**\n * Build URL with query parameters for GET requests\n * Compatible with fluid-admin's query param handling\n */\n function buildUrl(\n endpoint: string,\n params?: Record<string, unknown>,\n ): string {\n const fullUrl = joinUrl(endpoint);\n\n if (!params || Object.keys(params).length === 0) {\n return fullUrl;\n }\n\n const queryString = new URLSearchParams();\n\n Object.entries(params).forEach(([key, value]) => {\n if (value === undefined || value === null) {\n return; // Skip undefined/null values\n }\n\n if (Array.isArray(value)) {\n // Handle arrays like Rails expects: key[]. The OpenAPI-declared\n // param name is sometimes already bracketed (e.g. \"product_ids[]\",\n // \"filters[integration_class][]\") to match Rails' own convention;\n // appending another \"[]\" in that case would double-bracket the wire\n // format (key[][]=1), which Rails does not parse as the array.\n const arrayKey = key.endsWith(\"[]\") ? key : `${key}[]`;\n value.forEach((item) => queryString.append(arrayKey, String(item)));\n } else if (typeof value === \"object\") {\n // Handle nested objects: key[subkey]\n Object.entries(value).forEach(([subKey, subValue]) => {\n if (subValue === undefined || subValue === null) {\n return;\n }\n\n if (Array.isArray(subValue)) {\n subValue.forEach((item) =>\n queryString.append(`${key}[${subKey}][]`, String(item)),\n );\n } else {\n queryString.append(`${key}[${subKey}]`, String(subValue));\n }\n });\n } else {\n queryString.append(key, String(value));\n }\n });\n\n const qs = queryString.toString();\n return qs ? `${fullUrl}?${qs}` : fullUrl;\n }\n\n /**\n * Shared response handler for both JSON and FormData requests.\n * Handles auth errors, non-OK responses, 204 No Content, and JSON parsing.\n */\n async function handleResponse<TResponse>(\n response: Response,\n method: string,\n _url: string,\n ): Promise<TResponse> {\n const headerRequestId = getRequestIdFromHeaders(response.headers);\n\n if (response.status === 401 && onAuthError) {\n onAuthError();\n }\n\n if (!response.ok) {\n // Read body as text first to avoid SyntaxError from response.json()\n // when server returns non-JSON bodies with application/json content-type.\n const errorText = await response.text().catch(() => \"\");\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType?.includes(\"application/json\")) {\n // Parsed as `unknown` and narrowed before any property read: a body of\n // `null` (or a bare string/number/array) is valid JSON, and reading\n // `.error` off it directly threw a TypeError instead of an ApiError.\n let parsed: unknown;\n try {\n parsed = JSON.parse(errorText);\n } catch {\n throw new ApiError(\n errorText.slice(0, 200) ||\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\n headerRequestId,\n );\n }\n\n const body = toApiErrorBody(parsed);\n // Only an object envelope can carry a message. Some Rails BFF endpoints\n // return `{ error: { message, details } }` instead of `{ message }` or\n // `{ error_message }`, so that shape is tried last.\n const msg = body\n ? (() => {\n const nestedError =\n typeof body.error === \"object\" && body.error !== null\n ? (body.error as { message?: unknown }).message\n : undefined;\n const directError =\n typeof body.error === \"string\" ? body.error : undefined;\n const message =\n typeof body.message === \"string\" ? body.message : undefined;\n const errorMessage =\n typeof body.error_message === \"string\"\n ? body.error_message\n : undefined;\n return (\n message ||\n errorMessage ||\n directError ||\n (typeof nestedError === \"string\" ? nestedError : undefined)\n );\n })()\n : undefined;\n\n throw new ApiError(\n msg || `${method} request failed with status ${response.status}`,\n response.status,\n body ? toApiErrorData(body) : (parsed as ApiErrorData),\n headerRequestId ?? getRequestIdFromJsonBody(parsed),\n body,\n );\n } else {\n throw new ApiError(\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\n headerRequestId,\n );\n }\n }\n\n if (\n response.status === 204 ||\n response.headers.get(\"content-length\") === \"0\"\n ) {\n return null as TResponse;\n }\n\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType?.includes(\"application/json\")) {\n const responseText = await response.text();\n\n try {\n const data = JSON.parse(responseText);\n return data as TResponse;\n } catch {\n if (throwOnInvalidJson) {\n throw new ApiError(\n \"Failed to parse response as JSON\",\n response.status,\n null,\n headerRequestId,\n );\n }\n\n // API declared JSON content-type but body isn't valid JSON.\n // Return the raw payload to preserve the legacy non-strict path.\n return responseText ? (responseText as TResponse) : (null as TResponse);\n }\n }\n\n // Non-JSON response (text/plain, text/html, etc.)\n return null as TResponse;\n }\n\n function getNetworkRetryDelayMs(retryAttempt: number): number {\n return baseNetworkRetryDelayMs * 2 ** (retryAttempt - 1);\n }\n\n async function waitForNetworkRetry(retryAttempt: number): Promise<void> {\n const delayMs = getNetworkRetryDelayMs(retryAttempt);\n if (delayMs <= 0) {\n return;\n }\n\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n\n async function fetchWithNetworkRetry(\n url: string,\n fetchOptions: RequestInit,\n signal?: AbortSignal,\n ): Promise<Response> {\n let retryCount = 0;\n\n while (true) {\n try {\n return await fetch(url, fetchOptions);\n } catch (networkError) {\n if (signal?.aborted || retryCount >= maxNetworkRetries) {\n throw networkError;\n }\n\n retryCount += 1;\n await waitForNetworkRetry(retryCount);\n\n if (signal?.aborted) {\n throw networkError;\n }\n }\n }\n }\n\n /**\n * Main request function\n */\n async function request<TResponse = unknown>(\n endpoint: string,\n options: RequestOptions = {},\n ): Promise<TResponse> {\n const {\n method = \"GET\",\n headers: customHeaders,\n params,\n body,\n signal,\n priority,\n } = options;\n\n const url = params ? buildUrl(endpoint, params) : joinUrl(endpoint);\n\n const headers = await buildHeaders(customHeaders);\n\n let response: Response;\n\n try {\n const fetchOptions: RequestInit = { method, headers };\n if (credentials) fetchOptions.credentials = credentials;\n if (cache) fetchOptions.cache = cache;\n if (priority) fetchOptions.priority = priority;\n const serializedBody =\n body && method !== \"GET\" ? JSON.stringify(body) : null;\n if (serializedBody) fetchOptions.body = serializedBody;\n if (signal) fetchOptions.signal = signal;\n response = await fetchWithNetworkRetry(url, fetchOptions, signal);\n } catch (networkError) {\n throw new ApiError(\n `Network error: ${networkError instanceof Error ? networkError.message : \"Unknown network error\"}`,\n 0,\n null,\n );\n }\n\n return handleResponse<TResponse>(response, method, url);\n }\n\n /**\n * Request with FormData (for file uploads)\n */\n async function requestWithFormData<TResponse = unknown>(\n endpoint: string,\n formData: FormData,\n options: Omit<RequestOptions, \"body\" | \"params\"> & {\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n } = {},\n ): Promise<TResponse> {\n const {\n method = \"POST\",\n headers: customHeaders,\n signal,\n priority,\n } = options;\n\n const url = joinUrl(endpoint);\n const headers = await buildHeaders(customHeaders);\n\n // Remove Content-Type to let browser set it with boundary\n delete headers[\"Content-Type\"];\n\n let response: Response;\n\n try {\n const fetchOptions: RequestInit = { method, headers, body: formData };\n if (credentials) fetchOptions.credentials = credentials;\n if (cache) fetchOptions.cache = cache;\n if (priority) fetchOptions.priority = priority;\n if (signal) fetchOptions.signal = signal;\n response = await fetchWithNetworkRetry(url, fetchOptions, signal);\n } catch (networkError) {\n throw new ApiError(\n `Network error: ${networkError instanceof Error ? networkError.message : \"Unknown network error\"}`,\n 0,\n null,\n );\n }\n\n return handleResponse<TResponse>(response, method, url);\n }\n\n // Return client with convenience methods\n return {\n request: request,\n requestWithFormData: requestWithFormData,\n\n // Convenience methods for common HTTP verbs\n get: <TResponse = unknown>(\n endpoint: string,\n params?: Record<string, unknown>,\n options?: Omit<RequestOptions, \"method\" | \"params\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"GET\" as const,\n ...(params && { params }),\n }),\n\n post: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"POST\",\n body,\n }),\n\n put: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"PUT\",\n body,\n }),\n\n patch: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"PATCH\",\n body,\n }),\n\n delete: <TResponse = unknown>(\n endpoint: string,\n options?: Omit<RequestOptions, \"method\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"DELETE\",\n }),\n };\n}\n\nexport type FetchClient = FetchClientInstance;\n","import {\n createFetchClient,\n type FetchClientInstance,\n} from \"@fluid-app/api-client-core\";\nimport { getAuthToken } from \"@fluid-app/fluid-cli\";\n\nexport type ApiClient = FetchClientInstance;\n\n/** Base URL for all API calls. Set FLUID_API_BASE to point at staging. */\nfunction getApiBase(): string {\n return process.env[\"FLUID_API_BASE\"] ?? \"https://api.fluid.app\";\n}\n\n/**\n * Resolve the auth token the same way the core CLI does (see\n * `resolveToken` in packages/cli/core/src/domain/context.ts): the\n * FLUID_TOKEN / FLUID_API_TOKEN env vars win over the stored profile\n * from `fluid login`. Without the env-var step, automation that exports\n * FLUID_TOKEN to pick a company would silently operate against whatever\n * profile happens to be active instead.\n */\nexport function resolveAuthToken(): string | null {\n const envToken = process.env[\"FLUID_TOKEN\"] ?? process.env[\"FLUID_API_TOKEN\"];\n if (envToken) return envToken;\n return getAuthToken();\n}\n\nexport function createApiClient(tokenOverride?: string): ApiClient {\n return createFetchClient({\n baseUrl: getApiBase(),\n getAuthToken: () => tokenOverride ?? resolveAuthToken(),\n });\n}\n\nexport function requireToken(): string {\n const token = resolveAuthToken();\n if (!token) {\n console.error(\n \"Not logged in. Run `fluid login` first (or set FLUID_TOKEN).\",\n );\n process.exit(1);\n }\n return token;\n}\n\n// ── Legacy (MySite) theme API ────────────────────────────────────────\n//\n// A MySite theme is a Themes::LegacyTheme with exactly ONE template\n// carrying three Liquid/CSS blobs: content, stylesheet, head. There is\n// no draft state through this API — PATCHing a template publishes it\n// immediately (Rails calls template.publish on every save). That is\n// why `mysite dev` syncs to a scratch copy and `mysite publish` is the\n// only command that touches the real theme.\n\nexport interface MysiteTemplate {\n id?: number;\n name?: string | null;\n content: string | null;\n stylesheet: string | null;\n head: string | null;\n status?: string | null;\n}\n\nexport interface MysiteTheme {\n id: number;\n name: string;\n description?: string | null;\n image_url?: string | null;\n public?: boolean;\n company_id?: number;\n template?: MysiteTemplate | null;\n}\n\nexport interface MysiteFiles {\n content: string;\n stylesheet: string;\n head: string;\n}\n\nexport async function listMysiteThemes(api: ApiClient): Promise<MysiteTheme[]> {\n const res = await api.get<{ legacy_themes?: MysiteTheme[] }>(\n \"/api/legacy_themes\",\n );\n return res.legacy_themes ?? [];\n}\n\nexport async function getMysiteTheme(\n api: ApiClient,\n id: number,\n): Promise<MysiteTheme> {\n const res = await api.get<{ legacy_theme: MysiteTheme }>(\n `/api/legacy_themes/${id}`,\n );\n return res.legacy_theme;\n}\n\nexport async function createMysiteTheme(\n api: ApiClient,\n args: {\n name: string;\n description?: string;\n template: MysiteFiles;\n /** Dev copies send false so they stay out of the member-facing\n * theme picker. Requires the Rails create action to accept the\n * param (fluid PR pairs with this one); older servers ignore it\n * and force public — visible but otherwise harmless. */\n public?: boolean;\n },\n): Promise<MysiteTheme> {\n const res = await api.post<{ legacy_theme: MysiteTheme }>(\n \"/api/legacy_themes\",\n { legacy_theme: args },\n );\n return res.legacy_theme;\n}\n\n/** PATCH the theme's template blobs. WARNING: this publishes\n * immediately — the server has no draft state for MySite themes. */\nexport async function updateMysiteTemplate(\n api: ApiClient,\n id: number,\n template: MysiteFiles,\n): Promise<MysiteTheme> {\n const res = await api.patch<{ legacy_theme: MysiteTheme }>(\n `/api/legacy_themes/${id}`,\n { legacy_theme: { template } },\n );\n return res.legacy_theme;\n}\n\n// ── Current user ─────────────────────────────────────────────────────\n\n/** `GET /api/me` exposes `mysite_url` = `{company base_url}my/{username}`\n * for the active company — exactly the page the dev server should\n * preview. Returns null when the user has no username/membership in\n * the company (no MySite to preview). */\nexport async function fetchMysiteUrl(api: ApiClient): Promise<URL | null> {\n const me = await api.get<{ mysite_url?: string | null }>(\"/api/me\");\n if (!me.mysite_url) return null;\n try {\n return new URL(me.mysite_url);\n } catch {\n return null;\n }\n}\n","import { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport { createApiClient, listMysiteThemes, requireToken } from \"../api.js\";\n\nexport function createListCommand(): Command {\n return new Command(\"list\")\n .description(\"List the company's MySite themes\")\n .action(async () => {\n requireToken();\n const api = createApiClient();\n const themes = await listMysiteThemes(api);\n if (themes.length === 0) {\n console.log(\"No MySite themes found for this company.\");\n return;\n }\n for (const t of themes) {\n console.log(\n `${chalk.cyan(String(t.id).padStart(8))} ${t.name}${\n t.public ? chalk.dim(\" (public)\") : \"\"\n }`,\n );\n }\n console.log(\n chalk.dim(\n `\\nClone one locally: fluid mysite pull --theme <id> --root <dir>`,\n ),\n );\n });\n}\n","import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { MysiteFiles, MysiteTemplate } from \"./api.js\";\n\n// Local layout for a pulled MySite theme. A MySite theme is a single\n// template with three blobs, so the whole \"project\" is three files\n// plus a manifest:\n//\n// content.liquid — the page body (Liquid)\n// head.liquid — extra <head> markup (Liquid)\n// stylesheet.css — theme CSS\n// .fluid-mysite.json — theme id/name + the dev-copy id `mysite dev`\n// syncs against\n\nexport const MANIFEST_FILE = \".fluid-mysite.json\";\n\nexport const THEME_FILES = {\n content: \"content.liquid\",\n head: \"head.liquid\",\n stylesheet: \"stylesheet.css\",\n} as const;\n\nexport interface MysiteManifest {\n themeId: number;\n name: string;\n /** Scratch copy `mysite dev` syncs to (created lazily on first dev\n * run). Kept out of the way of the real theme because template\n * updates publish immediately server-side. */\n devThemeId?: number;\n}\n\nexport function manifestPath(root: string): string {\n return path.join(root, MANIFEST_FILE);\n}\n\nexport async function readManifest(\n root: string,\n): Promise<MysiteManifest | null> {\n const p = manifestPath(root);\n if (!existsSync(p)) return null;\n try {\n const parsed = JSON.parse(await readFile(p, \"utf8\")) as MysiteManifest;\n return typeof parsed.themeId === \"number\" ? parsed : null;\n } catch {\n return null;\n }\n}\n\nexport async function writeManifest(\n root: string,\n manifest: MysiteManifest,\n): Promise<void> {\n await mkdir(root, { recursive: true });\n await writeFile(\n manifestPath(root),\n JSON.stringify(manifest, null, 2) + \"\\n\",\n \"utf8\",\n );\n}\n\nexport async function writeThemeFiles(\n root: string,\n template: MysiteTemplate | null | undefined,\n): Promise<string[]> {\n await mkdir(root, { recursive: true });\n const written: string[] = [];\n for (const key of Object.keys(THEME_FILES) as Array<keyof MysiteFiles>) {\n const file = THEME_FILES[key];\n const target = path.join(root, file);\n const blob = template?.[key];\n // A blob the server did not send is NOT an instruction to empty the\n // local file. `?? \"\"` used to make those indistinguishable, so a\n // response missing `stylesheet` (or a null one) blanked a real\n // stylesheet.css on disk — and since `mysite publish` PATCHes the\n // live theme with whatever is on disk, the next publish pushed that\n // empty file live.\n //\n // This is the exact mirror of the contract readThemeFiles already\n // holds on the way out, whose tests say it \"must never substitute ''\n // for a file it couldn't read — that silently wipes the live\n // content\". The same reasoning applies inbound.\n if (blob == null && existsSync(target)) continue;\n await writeFile(target, blob ?? \"\", \"utf8\");\n written.push(file);\n }\n return written;\n}\n\n/**\n * Thrown by `readThemeFiles` when any of the three theme files is\n * missing or unreadable. The server has no draft state — a PATCH with a\n * blank blob immediately wipes the corresponding LIVE content — so\n * callers must treat this as fatal and never fall back to \"\".\n */\nexport class MissingThemeFilesError extends Error {\n readonly missing: readonly string[];\n\n constructor(missing: readonly string[]) {\n super(`Missing or unreadable local theme file(s): ${missing.join(\", \")}`);\n this.name = \"MissingThemeFilesError\";\n this.missing = missing;\n }\n}\n\n/**\n * Read the three local theme files, failing loudly (with every problem\n * file named) instead of silently substituting \"\" for a missing one —\n * an empty blob sent to the update API overwrites the live content\n * with nothing. Intentionally empty files are still fine; only\n * missing/unreadable ones are errors.\n */\nexport async function readThemeFiles(root: string): Promise<MysiteFiles> {\n const problems: string[] = [];\n const read = async (file: string): Promise<string> => {\n const p = path.join(root, file);\n if (!existsSync(p)) {\n problems.push(file);\n return \"\";\n }\n try {\n return await readFile(p, \"utf8\");\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n problems.push(`${file} (${reason})`);\n return \"\";\n }\n };\n const files: MysiteFiles = {\n content: await read(THEME_FILES.content),\n head: await read(THEME_FILES.head),\n stylesheet: await read(THEME_FILES.stylesheet),\n };\n if (problems.length > 0) throw new MissingThemeFilesError(problems);\n return files;\n}\n\nexport function themeFilePaths(root: string): string[] {\n return Object.values(THEME_FILES).map((f) => path.join(root, f));\n}\n","import path from \"node:path\";\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport { createApiClient, getMysiteTheme, requireToken } from \"../api.js\";\nimport {\n readManifest,\n writeManifest,\n writeThemeFiles,\n MANIFEST_FILE,\n} from \"../theme-files.js\";\n\nexport function createPullCommand(): Command {\n return new Command(\"pull\")\n .description(\n \"Download a MySite theme's Liquid + CSS into a local directory\",\n )\n .requiredOption(\"-t, --theme <id>\", \"MySite theme id (see `mysite list`)\")\n .option(\"--root <dir>\", \"Target directory\", \".\")\n .option(\"--yes\", \"Overwrite existing local files without asking\", false)\n .action(async (opts: { theme: string; root: string; yes: boolean }) => {\n requireToken();\n const themeId = Number(opts.theme);\n if (!Number.isInteger(themeId)) {\n console.error(`Invalid theme id: ${opts.theme}`);\n process.exit(1);\n }\n const root = path.resolve(opts.root);\n\n const existing = await readManifest(root);\n if (existing && existing.themeId !== themeId && !opts.yes) {\n console.error(\n `${root} already tracks MySite theme #${existing.themeId} (${existing.name}).\\n` +\n `Re-run with --yes to overwrite it with theme #${themeId}, or pick a different --root.`,\n );\n process.exit(1);\n }\n\n const api = createApiClient();\n const theme = await getMysiteTheme(api, themeId);\n const written = await writeThemeFiles(root, theme.template);\n await writeManifest(root, {\n themeId: theme.id,\n name: theme.name,\n // Preserve an existing dev copy when re-pulling the same theme\n // so `mysite dev` keeps reusing it instead of littering the\n // company with scratch themes.\n ...(existing && existing.themeId === theme.id && existing.devThemeId\n ? { devThemeId: existing.devThemeId }\n : {}),\n });\n\n console.log(\n `${chalk.green(\"✔\")} Pulled ${chalk.bold(theme.name)} (#${theme.id}) into ${root}`,\n );\n for (const f of written) console.log(` ${chalk.dim(\"wrote\")} ${f}`);\n console.log(` ${chalk.dim(\"wrote\")} ${MANIFEST_FILE}`);\n console.log(\n `\\nNext: ${chalk.cyan(\"fluid mysite dev\")} to preview edits live, ` +\n `${chalk.cyan(\"fluid mysite publish\")} to push them to the live theme.`,\n );\n });\n}\n","/**\n * Naming for the `fluid mysite dev` scratch theme.\n *\n * Its own module, with no chalk/API imports, so the safety property\n * below can be unit-tested without standing up the command.\n */\n\n/** MySite theme names are capped at 50 characters server-side. */\nconst MAX_THEME_NAME = 50;\n/** Keep at least this much of the theme name so the copy is still\n * recognisable in `fluid mysite list`. */\nconst MIN_BASE = 8;\n\n/**\n * Name for the `fluid mysite dev` scratch copy — see ensureDevCopy for\n * why it must differ from the live theme's name.\n *\n * The BASE name is truncated, not the finished string. Truncating the\n * finished string is what broke the invariant: at exactly\n * MAX_THEME_NAME characters `${name} (dev …)`.slice(0, 50) returns the\n * live theme's name byte for byte, and from ~24 characters up (on a\n * typical hostname) the random half was already being cut off, so two\n * machines could mint the same name.\n *\n * The host is shortened only as far as it has to be — it keeps its\n * full length whenever there is room, so short theme names produce\n * exactly the name they always did. Only the random hex is\n * unconditional, because that is the entropy the invariant rests on.\n */\nexport function devCopyName(\n themeName: string,\n host: string,\n hex: string,\n): string {\n // \" (dev \" + \"-\" + hex + \")\" — everything in the tail that is not\n // the host.\n const fixed = \" (dev \".length + 1 + hex.length + 1;\n const hostRoom = Math.max(3, MAX_THEME_NAME - MIN_BASE - fixed);\n const shortHost = (host.split(\".\")[0] || \"dev\").slice(0, hostRoom) || \"dev\";\n const tail = ` (dev ${shortHost}-${hex})`;\n const base = themeName\n .slice(0, Math.max(0, MAX_THEME_NAME - tail.length))\n .trimEnd();\n return `${base}${tail}`;\n}\n","export type ReloadMode = \"full-page\" | \"off\";\n\nexport function buildHotReloadScript(mode: ReloadMode): string {\n return `\n<script>\n(() => {\n window.__FLUID_CLI_ENV__ = ${JSON.stringify({ mode })};\n\n class HotReload {\n static reloadMode() { return window.__FLUID_CLI_ENV__.mode; }\n static isActive() { return HotReload.reloadMode() !== \"off\"; }\n static refresh() {\n console.log(\"[HotReload] Refreshing page\");\n window.location.reload();\n }\n }\n\n class SSEClient {\n constructor(url, handler) {\n if (typeof EventSource === \"undefined\") {\n console.error(\"[HotReload] EventSource not supported in this browser.\");\n return;\n }\n console.log(\"[HotReload] Initializing…\");\n this.url = url;\n this.handler = handler;\n }\n connect() {\n const es = new EventSource(this.url);\n es.onopen = () => console.log(\"[HotReload] SSE connected.\");\n es.onerror = () => {\n console.log(\"[HotReload] SSE closed. Reconnecting in 5s…\");\n es.close();\n setTimeout(() => this.connect(), 5000);\n };\n es.onmessage = (msg) => {\n let data;\n try {\n data = JSON.parse(msg.data);\n } catch {\n return; // non-JSON frame (keep-alive etc.) — ignore\n }\n this.handler(data);\n };\n }\n }\n\n if (HotReload.isActive()) {\n new SSEClient(\"/hot-reload\", (data) => {\n if (data.reload_page) HotReload.refresh();\n }).connect();\n }\n})();\n</script>`;\n}\n\nexport function injectHotReload(html: string, mode: ReloadMode): string {\n const script = buildHotReloadScript(mode);\n if (html.includes(\"</body>\")) {\n return html.replace(\"</body>\", `${script}\\n</body>`);\n }\n return html + script;\n}\n","import https from \"node:https\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { injectHotReload, type ReloadMode } from \"./hot-reload.js\";\n\n// Localhost → production proxy for MySite previews. MySite pages are\n// rendered server-side (Liquid via PageBuilder), and the ONLY draft\n// mechanism is the `preview_theme_id` query param on the public\n// visitor route (`/my/:username`). A raw preview URL loses that param\n// on every internal link click — the proxy pins it on every request\n// instead, and injects a live-reload SSE client into HTML responses\n// so saves refresh the page.\n//\n// Adapted from theme-dev's proxy; MySite needs neither the\n// `x-fluid-theme` header nor the `replace_templates` trick (drafts\n// live on a synced scratch theme, not in pending local files), so\n// this version is a straight pass-through plus one query param.\n\nconst HOP_BY_HOP = new Set([\n \"connection\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n \"content-security-policy\",\n]);\n\nexport interface ProxyOptions {\n /** Production host serving the MySite (from /api/me's mysite_url). */\n host: string;\n /** Scratch theme id to pin via preview_theme_id. */\n previewThemeId: number;\n reloadMode: ReloadMode;\n}\n\nexport async function proxyRequest(\n req: IncomingMessage,\n res: ServerResponse,\n opts: ProxyOptions,\n): Promise<void> {\n const headers: Record<string, string> = {};\n for (const [k, v] of Object.entries(req.headers)) {\n if (!HOP_BY_HOP.has(k.toLowerCase()) && typeof v === \"string\") {\n headers[k] = v;\n }\n }\n headers[\"host\"] = opts.host;\n headers[\"user-agent\"] = \"Fluid CLI\";\n headers[\"accept-encoding\"] = \"identity\";\n\n const url = new URL(req.url ?? \"/\", `http://${req.headers.host}`);\n // Harmless on asset/API requests; the mysites controller reads it\n // on page requests to render the scratch theme.\n url.searchParams.set(\"preview_theme_id\", String(opts.previewThemeId));\n\n const isGet = req.method === \"GET\" || req.method === \"HEAD\";\n let body: Buffer | undefined;\n if (!isGet) {\n body = await readBody(req);\n if (body.length > 0) {\n headers[\"content-length\"] = String(body.length);\n }\n }\n\n return new Promise((resolve, reject) => {\n const options: https.RequestOptions = {\n hostname: opts.host,\n port: 443,\n path: url.pathname + (url.search || \"\"),\n method: req.method ?? \"GET\",\n headers,\n };\n\n const [hostname, portStr] = opts.host.split(\":\");\n const port = portStr ? Number(portStr) : 443;\n options.hostname = hostname;\n options.port = port;\n\n const proxyReq = https.request(options, (proxyRes) => {\n // Upstream can drop mid-stream (network blip, server restart) —\n // without this the \"error\" event is unhandled, which crashes the\n // process and leaves the promise hung.\n proxyRes.on(\"error\", reject);\n const contentType = proxyRes.headers[\"content-type\"] ?? \"\";\n const isHtml = contentType.includes(\"text/html\");\n\n const responseHeaders: Record<string, string | string[]> = {};\n for (const [k, v] of Object.entries(proxyRes.headers)) {\n if (!HOP_BY_HOP.has(k.toLowerCase()) && v !== undefined) {\n responseHeaders[k] = v as string | string[];\n }\n }\n\n if (isHtml) {\n const chunks: Buffer[] = [];\n proxyRes.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n proxyRes.on(\"end\", () => {\n let html = Buffer.concat(chunks).toString(\"utf-8\");\n html = injectHotReload(html, opts.reloadMode);\n responseHeaders[\"content-length\"] = String(Buffer.byteLength(html));\n res.writeHead(proxyRes.statusCode ?? 200, responseHeaders);\n res.end(html);\n resolve();\n });\n } else {\n res.writeHead(proxyRes.statusCode ?? 200, responseHeaders);\n proxyRes.pipe(res);\n proxyRes.on(\"end\", resolve);\n }\n });\n\n proxyReq.on(\"error\", (err) => {\n reject(err);\n });\n\n if (body) proxyReq.write(body);\n proxyReq.end();\n });\n}\n\nfunction readBody(req: IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n","import type { ServerResponse } from \"node:http\";\n\nexport class SSEStream {\n private responses = new Set<ServerResponse>();\n\n add(res: ServerResponse): void {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n \"Access-Control-Allow-Origin\": \"*\",\n });\n res.write(\":\\n\\n\");\n this.responses.add(res);\n res.on(\"close\", () => this.responses.delete(res));\n }\n\n broadcast(data: string): void {\n const payload = `data: ${data}\\n\\n`;\n for (const res of this.responses) {\n try {\n res.write(payload);\n } catch {\n this.responses.delete(res);\n }\n }\n }\n\n close(): void {\n for (const res of this.responses) {\n try {\n res.end();\n } catch {\n // ignore\n }\n }\n this.responses.clear();\n }\n\n get size(): number {\n return this.responses.size;\n }\n}\n","import { devCopyName } from \"./dev-copy-name.js\";\nimport http from \"node:http\";\nimport path from \"node:path\";\nimport { hostname } from \"node:os\";\nimport { randomBytes } from \"node:crypto\";\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport chokidar from \"chokidar\";\nimport {\n createApiClient,\n createMysiteTheme,\n fetchMysiteUrl,\n getMysiteTheme,\n requireToken,\n updateMysiteTemplate,\n type ApiClient,\n type MysiteFiles,\n} from \"../api.js\";\nimport {\n MissingThemeFilesError,\n readManifest,\n readThemeFiles,\n themeFilePaths,\n writeManifest,\n type MysiteManifest,\n} from \"../theme-files.js\";\nimport { proxyRequest } from \"../dev-server/proxy.js\";\nimport { SSEStream } from \"../dev-server/sse.js\";\nimport type { ReloadMode } from \"../dev-server/hot-reload.js\";\n\nconst DEFAULT_PORT = 9293;\n\n// Server-side rendering is the only way to see a MySite theme (Liquid\n// + drops like fluid_affiliate resolve on the Rails side), and template\n// PATCHes publish immediately — so \"test before committing\" means:\n//\n// 1. keep a scratch copy of the theme (created once, reused, and\n// never assigned to anyone — only reachable via preview_theme_id)\n// 2. PATCH the scratch copy on every local save\n// 3. proxy localhost → the user's real /my/:username page with\n// preview_theme_id pinned, live-reloading on sync\n//\n// The real theme is only touched by `fluid mysite publish`.\n\nexport function createDevCommand(): Command {\n return new Command(\"dev\")\n .description(\n \"Preview local MySite edits live without touching the published theme\",\n )\n .option(\"--root <dir>\", \"Theme directory (from `mysite pull`)\", \".\")\n .option(\"--port <port>\", \"Local preview port\", String(DEFAULT_PORT))\n .option(\n \"--live-reload <mode>\",\n \"Reload browser on save: full-page | off\",\n \"full-page\",\n )\n .action(\n async (opts: { root: string; port: string; liveReload: string }) => {\n requireToken();\n const root = path.resolve(opts.root);\n const port = Number(opts.port) || DEFAULT_PORT;\n const reloadMode: ReloadMode =\n opts.liveReload === \"off\" ? \"off\" : \"full-page\";\n\n const manifest = await readManifest(root);\n if (!manifest) {\n console.error(\n `No ${chalk.bold(\".fluid-mysite.json\")} in ${root}. ` +\n `Run ${chalk.cyan(\"fluid mysite pull --theme <id>\")} first.`,\n );\n process.exit(1);\n }\n\n // Fail fast (before any theme is created or synced) when a local\n // theme file is missing — syncing would blank the corresponding\n // content on the dev copy. The validated snapshot is what a\n // fresh dev copy is seeded from, so there's no second unguarded\n // read between here and the copy's creation.\n const files = await readThemeFilesOrExit(root);\n\n const api = createApiClient();\n\n const mysiteUrl = await fetchMysiteUrl(api);\n if (!mysiteUrl) {\n console.error(\n \"Couldn't determine your MySite URL — your user needs a username \" +\n \"in this company to have a /my page to preview against.\",\n );\n process.exit(1);\n }\n\n const devThemeId = await ensureDevCopy(api, root, manifest, files);\n\n // Initial sync so the scratch copy reflects local files even if\n // they were edited while dev wasn't running.\n await syncToDevCopy(api, root, devThemeId, \"initial sync\");\n\n const sse = new SSEStream();\n const server = http.createServer((req, res) => {\n if (req.url === \"/hot-reload\") {\n sse.add(res);\n return;\n }\n // Send the bare origin to the user's MySite page so consumers\n // that only know the port (Mist's preview webview, a human\n // typing localhost:9293) land on the right path without\n // knowing the username.\n if ((req.url ?? \"/\").split(\"?\")[0] === \"/\") {\n res.writeHead(302, { location: mysiteUrl.pathname });\n res.end();\n return;\n }\n void proxyRequest(req, res, {\n host: mysiteUrl.host,\n previewThemeId: devThemeId,\n reloadMode,\n }).catch((err) => {\n if (!res.headersSent) res.writeHead(502);\n res.end(`Proxy error: ${err instanceof Error ? err.message : err}`);\n });\n });\n\n const stopWatcher = watchThemeFiles(root, async () => {\n const ok = await syncToDevCopy(api, root, devThemeId, \"sync\");\n if (ok) sse.broadcast(JSON.stringify({ reload_page: true }));\n });\n\n // Walk forward from the requested port if it's taken — the\n // team runs multiple previews side by side (and Mist will\n // spawn these programmatically), so EADDRINUSE shouldn't be\n // a crash. An explicitly passed --port that's busy still\n // gets a clear message about where we actually bound.\n const boundPort = await listenOnAvailablePort(server, port);\n if (boundPort !== port) {\n console.log(\n chalk.dim(`Port ${port} is in use — using ${boundPort} instead.`),\n );\n }\n {\n const localUrl = `http://127.0.0.1:${boundPort}${mysiteUrl.pathname}`;\n const prodPreview = `${mysiteUrl.href}?preview_theme_id=${devThemeId}`;\n console.log(\n `\\n${chalk.green(\"✔\")} Dev copy ${chalk.bold(`#${devThemeId}`)} ready — the live theme (#${manifest.themeId}) stays untouched.\\n`,\n );\n console.log(\n ` ${chalk.bold(\"Local preview:\")} ${chalk.cyan(localUrl)}`,\n );\n console.log(\n ` ${chalk.bold(\"Production preview:\")} ${chalk.cyan(prodPreview)}`,\n );\n console.log(\n ` ${chalk.bold(\"Live site:\")} ${chalk.dim(mysiteUrl.href)}\\n`,\n );\n console.log(\n chalk.dim(\n \"Edits to content.liquid / head.liquid / stylesheet.css sync on save. \" +\n \"Run `fluid mysite publish` to push them live. Ctrl+C to stop.\",\n ),\n );\n }\n\n const shutdown = async () => {\n sse.close();\n server.close();\n await stopWatcher();\n process.exit(0);\n };\n process.on(\"SIGINT\", () => void shutdown());\n process.on(\"SIGTERM\", () => void shutdown());\n },\n );\n}\n\n/** Bind to `startPort`, walking forward on EADDRINUSE (up to\n * `attempts` ports) so parallel previews coexist. Resolves with the\n * port actually bound; rejects on non-port errors. */\nfunction listenOnAvailablePort(\n server: http.Server,\n startPort: number,\n attempts = 20,\n): Promise<number> {\n return new Promise((resolve, reject) => {\n let port = startPort;\n const tryListen = () => {\n const onError = (err: NodeJS.ErrnoException) => {\n server.removeListener(\"listening\", onListening);\n if (err.code === \"EADDRINUSE\" && port - startPort < attempts) {\n port += 1;\n tryListen();\n } else {\n reject(err);\n }\n };\n const onListening = () => {\n server.removeListener(\"error\", onError);\n resolve(port);\n };\n server.once(\"error\", onError);\n server.once(\"listening\", onListening);\n server.listen(port, \"127.0.0.1\");\n };\n tryListen();\n });\n}\n\n/** Read the theme files, exiting with a clean, user-facing message\n * (instead of an unhandled rejection) when any are missing. */\nasync function readThemeFilesOrExit(root: string): Promise<MysiteFiles> {\n try {\n return await readThemeFiles(root);\n } catch (err) {\n if (err instanceof MissingThemeFilesError) {\n console.error(\n `${chalk.red(\"✖\")} ${err.message}\\n` +\n `Restore the file(s), or re-run ${chalk.cyan(\"fluid mysite pull\")} ` +\n \"to re-download the theme, then try again.\",\n );\n process.exit(1);\n }\n throw err;\n }\n}\n\n/** Find or create the scratch theme this directory syncs against,\n * seeding a fresh copy from the already-validated `files` snapshot\n * (no re-read, so no window where a just-deleted file could crash or\n * blank the copy).\n *\n * The name MUST be unique within the company: the Rails update action\n * resolves templates by (name, themeable_type, company), so a scratch\n * copy sharing the real theme's name would silently write to the REAL\n * template. A random suffix keeps them apart. */\nasync function ensureDevCopy(\n api: ApiClient,\n root: string,\n manifest: MysiteManifest,\n files: MysiteFiles,\n): Promise<number> {\n if (manifest.devThemeId) {\n try {\n const existing = await getMysiteTheme(api, manifest.devThemeId);\n return existing.id;\n } catch {\n console.log(\n chalk.dim(\n `Stored dev copy #${manifest.devThemeId} is gone — creating a fresh one.`,\n ),\n );\n }\n }\n\n // See devCopyName: the base name is truncated, not the finished\n // string, so the random suffix always survives.\n const name = devCopyName(\n manifest.name,\n hostname(),\n randomBytes(2).toString(\"hex\"),\n );\n const created = await createMysiteTheme(api, {\n name,\n description:\n \"Fluid CLI development copy — auto-created by `fluid mysite dev`. Safe to delete.\",\n template: files,\n public: false,\n });\n await writeManifest(root, { ...manifest, devThemeId: created.id });\n return created.id;\n}\n\nasync function syncToDevCopy(\n api: ApiClient,\n root: string,\n devThemeId: number,\n label: string,\n): Promise<boolean> {\n const started = Date.now();\n try {\n const files = await readThemeFiles(root);\n await updateMysiteTemplate(api, devThemeId, files);\n console.log(\n `${chalk.green(\"✔\")} ${label} ${chalk.dim(`(${Date.now() - started}ms)`)}`,\n );\n return true;\n } catch (err) {\n console.error(\n `${chalk.red(\"✖\")} ${label} failed: ${err instanceof Error ? err.message : err}`,\n );\n return false;\n }\n}\n\n/** Watch the three theme files, coalescing rapid saves into one sync. */\nfunction watchThemeFiles(\n root: string,\n onChange: () => Promise<void>,\n): () => Promise<void> {\n const watcher = chokidar.watch(themeFilePaths(root), {\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 10 },\n });\n let pending = Promise.resolve();\n const enqueue = () => {\n pending = pending.then(onChange).catch(() => {});\n };\n watcher.on(\"change\", enqueue);\n watcher.on(\"add\", enqueue);\n return () => watcher.close();\n}\n","/** Whether to publish, and whether to ask first.\n *\n * `prompts` writes its question and then waits on stdin. With stdin on\n * /dev/null — which is exactly how the agent's `run_cli` spawns a child\n * (`stdio: [\"ignore\", \"pipe\", \"pipe\"]`) — the promise NEVER SETTLES,\n * the event loop drains, and node exits 0. Not even \"Aborted.\" is\n * printed. So `fluid mysite publish` reported success while publishing\n * nothing, and the agent told the user their MySite was live.\n *\n * Refusing loudly is the only safe answer: publishing goes straight to\n * the LIVE theme with no draft state, so silently continuing without a\n * confirmation would be worse than stopping.\n *\n * The portal CLI is the only other place in packages/cli that considers\n * this, and it says why: \"Non-interactive mode: if stdin is not a TTY\n * and there are remaining prompts, return safe defaults instead of\n * hanging on interactive input.\"\n *\n * Its own module so the test can import it without dragging commander,\n * prompts and chalk into jest. */\nexport type PublishGate =\n | { proceed: true; prompt: boolean }\n | { proceed: false; reason: string };\n\nexport function publishGate(opts: {\n yes: boolean;\n isTTY: boolean;\n}): PublishGate {\n if (opts.yes) return { proceed: true, prompt: false };\n if (!opts.isTTY) {\n return {\n proceed: false,\n reason:\n \"Refusing to publish without a confirmation.\\n\" +\n \"Nothing was published — this is a non-interactive session \" +\n \"(stdin is not a TTY), so the confirmation prompt cannot be \" +\n \"answered. Re-run with --yes to publish straight to the LIVE \" +\n \"MySite theme.\",\n };\n }\n return { proceed: true, prompt: true };\n}\n","import path from \"node:path\";\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport prompts from \"prompts\";\nimport {\n createApiClient,\n fetchMysiteUrl,\n requireToken,\n updateMysiteTemplate,\n type MysiteFiles,\n} from \"../api.js\";\nimport { publishGate } from \"./publish-gate.js\";\nimport {\n MissingThemeFilesError,\n readManifest,\n readThemeFiles,\n} from \"../theme-files.js\";\n\nexport function createPublishCommand(): Command {\n return new Command(\"publish\")\n .description(\n \"Push local files to the LIVE MySite theme (immediate — no draft state)\",\n )\n .option(\"--root <dir>\", \"Theme directory (from `mysite pull`)\", \".\")\n .option(\"--yes\", \"Skip the confirmation prompt\", false)\n .action(async (opts: { root: string; yes: boolean }) => {\n requireToken();\n const root = path.resolve(opts.root);\n const manifest = await readManifest(root);\n if (!manifest) {\n console.error(\n `No ${chalk.bold(\".fluid-mysite.json\")} in ${root}. ` +\n `Run ${chalk.cyan(\"fluid mysite pull --theme <id>\")} first.`,\n );\n process.exit(1);\n }\n\n // Validate BEFORE the confirmation prompt and before any remote\n // write: publishing goes straight to the LIVE theme with no draft\n // state, so a missing local file must abort — never PATCH a blank\n // blob over live content.\n let files: MysiteFiles;\n try {\n files = await readThemeFiles(root);\n } catch (err) {\n if (err instanceof MissingThemeFilesError) {\n console.error(\n `${chalk.red(\"✖\")} ${err.message}\\n` +\n \"Publishing would overwrite the live theme's content with \" +\n \"nothing, so nothing was published.\\n\" +\n `Restore the file(s), or re-run ${chalk.cyan(\"fluid mysite pull\")} ` +\n \"to re-download the theme, then try again.\",\n );\n process.exit(1);\n }\n throw err;\n }\n\n const gate = publishGate({ yes: opts.yes, isTTY: !!process.stdin.isTTY });\n if (!gate.proceed) {\n console.error(`${chalk.red(\"✖\")} ${gate.reason}`);\n process.exit(1);\n }\n\n if (gate.prompt) {\n const { confirmed } = await prompts({\n type: \"confirm\",\n name: \"confirmed\",\n message:\n `Publish local files to \"${manifest.name}\" (#${manifest.themeId})? ` +\n \"MySite pages using this theme update immediately.\",\n initial: false,\n });\n if (!confirmed) {\n console.log(\"Aborted.\");\n return;\n }\n }\n\n const api = createApiClient();\n await updateMysiteTemplate(api, manifest.themeId, files);\n console.log(\n `${chalk.green(\"✔\")} Published ${chalk.bold(manifest.name)} (#${manifest.themeId})`,\n );\n\n // Best-effort convenience link; publish already succeeded.\n try {\n const url = await fetchMysiteUrl(api);\n if (url)\n console.log(` ${chalk.dim(\"Live at\")} ${chalk.cyan(url.href)}`);\n } catch {\n // ignore\n }\n });\n}\n","import { Command } from \"commander\";\nimport type { PluginContext } from \"@fluid-app/fluid-cli\";\nimport { createListCommand } from \"./list.js\";\nimport { createPullCommand } from \"./pull.js\";\nimport { createDevCommand } from \"./dev.js\";\nimport { createPublishCommand } from \"./publish.js\";\n\nexport function registerMysiteCommand(ctx: PluginContext): void {\n const cmd = new Command(\"mysite\").description(\n \"MySite theme workflow — list, pull, dev preview server, publish\",\n );\n\n cmd.addCommand(createListCommand());\n cmd.addCommand(createPullCommand());\n cmd.addCommand(createDevCommand());\n cmd.addCommand(createPublishCommand());\n\n ctx.program.addCommand(cmd);\n}\n","import type { FluidPlugin, PluginContext } from \"@fluid-app/fluid-cli\";\nimport { registerMysiteCommand } from \"./commands/mysite.js\";\n\nconst plugin: FluidPlugin = {\n name: \"@fluid-app/fluid-cli-mysite\",\n version: \"0.1.0\",\n register(ctx: PluginContext) {\n registerMysiteCommand(ctx);\n },\n};\n\nexport default plugin;\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluid-app/fluid-cli-mysite",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"description": "Fluid CLI plugin for MySite theme workflows — pull, dev preview server, publish",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@fluid-app/fluid-cli": "0.1.
|
|
24
|
+
"@fluid-app/fluid-cli": "0.1.35",
|
|
25
25
|
"chalk": "^5.6.2",
|
|
26
26
|
"chokidar": "^4.0.0",
|
|
27
27
|
"commander": "^12.0.0",
|