@fluid-app/fluid-cli-mysite 0.1.0 → 0.1.2

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 CHANGED
@@ -238,16 +238,29 @@ function createFetchClient(config) {
238
238
  function getApiBase() {
239
239
  return process.env["FLUID_API_BASE"] ?? "https://api.fluid.app";
240
240
  }
241
+ /**
242
+ * Resolve the auth token the same way the core CLI does (see
243
+ * `resolveToken` in packages/cli/core/src/domain/context.ts): the
244
+ * FLUID_TOKEN / FLUID_API_TOKEN env vars win over the stored profile
245
+ * from `fluid login`. Without the env-var step, automation that exports
246
+ * FLUID_TOKEN to pick a company would silently operate against whatever
247
+ * profile happens to be active instead.
248
+ */
249
+ function resolveAuthToken() {
250
+ const envToken = process.env["FLUID_TOKEN"] ?? process.env["FLUID_API_TOKEN"];
251
+ if (envToken) return envToken;
252
+ return getAuthToken();
253
+ }
241
254
  function createApiClient(tokenOverride) {
242
255
  return createFetchClient({
243
256
  baseUrl: getApiBase(),
244
- getAuthToken: () => tokenOverride ?? getAuthToken() ?? null
257
+ getAuthToken: () => tokenOverride ?? resolveAuthToken()
245
258
  });
246
259
  }
247
260
  function requireToken() {
248
- const token = getAuthToken();
261
+ const token = resolveAuthToken();
249
262
  if (!token) {
250
- console.error("Not logged in. Run `fluid login` first.");
263
+ console.error("Not logged in. Run `fluid login` first (or set FLUID_TOKEN).");
251
264
  process.exit(1);
252
265
  }
253
266
  return token;
@@ -333,17 +346,50 @@ async function writeThemeFiles(root, template) {
333
346
  }
334
347
  return written;
335
348
  }
349
+ /**
350
+ * Thrown by `readThemeFiles` when any of the three theme files is
351
+ * missing or unreadable. The server has no draft state — a PATCH with a
352
+ * blank blob immediately wipes the corresponding LIVE content — so
353
+ * callers must treat this as fatal and never fall back to "".
354
+ */
355
+ var MissingThemeFilesError = class extends Error {
356
+ missing;
357
+ constructor(missing) {
358
+ super(`Missing or unreadable local theme file(s): ${missing.join(", ")}`);
359
+ this.name = "MissingThemeFilesError";
360
+ this.missing = missing;
361
+ }
362
+ };
363
+ /**
364
+ * Read the three local theme files, failing loudly (with every problem
365
+ * file named) instead of silently substituting "" for a missing one —
366
+ * an empty blob sent to the update API overwrites the live content
367
+ * with nothing. Intentionally empty files are still fine; only
368
+ * missing/unreadable ones are errors.
369
+ */
336
370
  async function readThemeFiles(root) {
371
+ const problems = [];
337
372
  const read = async (file) => {
338
373
  const p = path.join(root, file);
339
- if (!existsSync(p)) return "";
340
- return readFile(p, "utf8");
374
+ if (!existsSync(p)) {
375
+ problems.push(file);
376
+ return "";
377
+ }
378
+ try {
379
+ return await readFile(p, "utf8");
380
+ } catch (err) {
381
+ const reason = err instanceof Error ? err.message : String(err);
382
+ problems.push(`${file} (${reason})`);
383
+ return "";
384
+ }
341
385
  };
342
- return {
386
+ const files = {
343
387
  content: await read(THEME_FILES.content),
344
388
  head: await read(THEME_FILES.head),
345
389
  stylesheet: await read(THEME_FILES.stylesheet)
346
390
  };
391
+ if (problems.length > 0) throw new MissingThemeFilesError(problems);
392
+ return files;
347
393
  }
348
394
  function themeFilePaths(root) {
349
395
  return Object.values(THEME_FILES).map((f) => path.join(root, f));
@@ -560,13 +606,14 @@ function createDevCommand() {
560
606
  console.error(`No ${chalk.bold(".fluid-mysite.json")} in ${root}. Run ${chalk.cyan("fluid mysite pull --theme <id>")} first.`);
561
607
  process.exit(1);
562
608
  }
609
+ const files = await readThemeFilesOrExit(root);
563
610
  const api = createApiClient();
564
611
  const mysiteUrl = await fetchMysiteUrl(api);
565
612
  if (!mysiteUrl) {
566
613
  console.error("Couldn't determine your MySite URL — your user needs a username in this company to have a /my page to preview against.");
567
614
  process.exit(1);
568
615
  }
569
- const devThemeId = await ensureDevCopy(api, root, manifest);
616
+ const devThemeId = await ensureDevCopy(api, root, manifest, files);
570
617
  await syncToDevCopy(api, root, devThemeId, "initial sync");
571
618
  const sse = new SSEStream();
572
619
  const server = http.createServer((req, res) => {
@@ -637,19 +684,34 @@ function listenOnAvailablePort(server, startPort, attempts = 20) {
637
684
  tryListen();
638
685
  });
639
686
  }
640
- /** Find or create the scratch theme this directory syncs against.
687
+ /** Read the theme files, exiting with a clean, user-facing message
688
+ * (instead of an unhandled rejection) when any are missing. */
689
+ async function readThemeFilesOrExit(root) {
690
+ try {
691
+ return await readThemeFiles(root);
692
+ } catch (err) {
693
+ if (err instanceof MissingThemeFilesError) {
694
+ console.error(`${chalk.red("✖")} ${err.message}\nRestore the file(s), or re-run ${chalk.cyan("fluid mysite pull")} to re-download the theme, then try again.`);
695
+ process.exit(1);
696
+ }
697
+ throw err;
698
+ }
699
+ }
700
+ /** Find or create the scratch theme this directory syncs against,
701
+ * seeding a fresh copy from the already-validated `files` snapshot
702
+ * (no re-read, so no window where a just-deleted file could crash or
703
+ * blank the copy).
641
704
  *
642
705
  * The name MUST be unique within the company: the Rails update action
643
706
  * resolves templates by (name, themeable_type, company), so a scratch
644
707
  * copy sharing the real theme's name would silently write to the REAL
645
708
  * template. A random suffix keeps them apart. */
646
- async function ensureDevCopy(api, root, manifest) {
709
+ async function ensureDevCopy(api, root, manifest, files) {
647
710
  if (manifest.devThemeId) try {
648
711
  return (await getMysiteTheme(api, manifest.devThemeId)).id;
649
712
  } catch {
650
713
  console.log(chalk.dim(`Stored dev copy #${manifest.devThemeId} is gone — creating a fresh one.`));
651
714
  }
652
- const files = await readThemeFiles(root);
653
715
  const suffix = `${hostname().split(".")[0] ?? "dev"}-${randomBytes(2).toString("hex")}`;
654
716
  const created = await createMysiteTheme(api, {
655
717
  name: `${manifest.name} (dev ${suffix})`.slice(0, 50),
@@ -702,6 +764,17 @@ function createPublishCommand() {
702
764
  console.error(`No ${chalk.bold(".fluid-mysite.json")} in ${root}. Run ${chalk.cyan("fluid mysite pull --theme <id>")} first.`);
703
765
  process.exit(1);
704
766
  }
767
+ let files;
768
+ try {
769
+ files = await readThemeFiles(root);
770
+ } catch (err) {
771
+ if (err instanceof MissingThemeFilesError) {
772
+ console.error(`${chalk.red("✖")} ${err.message}\nPublishing would overwrite the live theme's content with nothing, so nothing was published.
773
+ Restore the file(s), or re-run ${chalk.cyan("fluid mysite pull")} to re-download the theme, then try again.`);
774
+ process.exit(1);
775
+ }
776
+ throw err;
777
+ }
705
778
  if (!opts.yes) {
706
779
  const { confirmed } = await prompts({
707
780
  type: "confirm",
@@ -715,7 +788,6 @@ function createPublishCommand() {
715
788
  }
716
789
  }
717
790
  const api = createApiClient();
718
- const files = await readThemeFiles(root);
719
791
  await updateMysiteTemplate(api, manifest.themeId, files);
720
792
  console.log(`${chalk.green("✔")} Published ${chalk.bold(manifest.name)} (#${manifest.themeId})`);
721
793
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../platform/api-client-core/src/fetch-client.ts","../src/api.ts","../src/commands/list.ts","../src/theme-files.ts","../src/commands/pull.ts","../src/dev-server/hot-reload.ts","../src/dev-server/proxy.ts","../src/dev-server/sse.ts","../src/commands/dev.ts","../src/commands/publish.ts","../src/commands/mysite.ts","../src/index.ts"],"sourcesContent":["/**\n * Minimal, framework-agnostic fetch client for Fluid APIs\n * Compatible with fluid-admin patterns but usable standalone\n */\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 public readonly data: unknown;\n public readonly requestId?: string;\n\n constructor(\n message: string,\n status: number,\n data?: unknown,\n requestId?: string,\n ) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.data = data;\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: unknown;\n requestId?: string;\n } {\n return {\n name: this.name,\n message: this.message,\n status: this.status,\n data: this.data,\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 let data: Record<string, unknown>;\n try {\n data = 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 // Some Rails BFF endpoints return `{ error: { message, details } }`\n // instead of `{ message }` or `{ error_message }`. Fall through to\n // that shape last so callers always surface the real reason.\n const nestedError =\n typeof data.error === \"object\" && data.error !== null\n ? (data.error as { message?: unknown }).message\n : undefined;\n const msg =\n (data.message as string | undefined) ||\n (data.error_message as string | undefined) ||\n (typeof nestedError === \"string\" ? nestedError : undefined);\n throw new ApiError(\n msg || `${method} request failed`,\n response.status,\n data.errors || data,\n headerRequestId ?? getRequestIdFromJsonBody(data),\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\nexport function createApiClient(tokenOverride?: string): ApiClient {\n return createFetchClient({\n baseUrl: getApiBase(),\n getAuthToken: () => tokenOverride ?? getAuthToken() ?? null,\n });\n}\n\nexport function requireToken(): string {\n const token = getAuthToken();\n if (!token) {\n console.error(\"Not logged in. Run `fluid login` first.\");\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 const blobs: MysiteFiles = {\n content: template?.content ?? \"\",\n head: template?.head ?? \"\",\n stylesheet: template?.stylesheet ?? \"\",\n };\n for (const key of Object.keys(THEME_FILES) as Array<keyof MysiteFiles>) {\n const file = THEME_FILES[key];\n await writeFile(path.join(root, file), blobs[key], \"utf8\");\n written.push(file);\n }\n return written;\n}\n\nexport async function readThemeFiles(root: string): Promise<MysiteFiles> {\n const read = async (file: string): Promise<string> => {\n const p = path.join(root, file);\n if (!existsSync(p)) return \"\";\n return readFile(p, \"utf8\");\n };\n return {\n content: await read(THEME_FILES.content),\n head: await read(THEME_FILES.head),\n stylesheet: await read(THEME_FILES.stylesheet),\n };\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","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 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} from \"../api.js\";\nimport {\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 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);\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/** Find or create the scratch theme this directory syncs against.\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): 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 const files = await readThemeFiles(root);\n const suffix = `${hostname().split(\".\")[0] ?? \"dev\"}-${randomBytes(2).toString(\"hex\")}`;\n const name = `${manifest.name} (dev ${suffix})`.slice(0, 50);\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","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} from \"../api.js\";\nimport { readManifest, readThemeFiles } 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 if (!opts.yes) {\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 const files = await readThemeFiles(root);\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/fetch-client.ts","../src/api.ts","../src/commands/list.ts","../src/theme-files.ts","../src/commands/pull.ts","../src/dev-server/hot-reload.ts","../src/dev-server/proxy.ts","../src/dev-server/sse.ts","../src/commands/dev.ts","../src/commands/publish.ts","../src/commands/mysite.ts","../src/index.ts"],"sourcesContent":["/**\n * Minimal, framework-agnostic fetch client for Fluid APIs\n * Compatible with fluid-admin patterns but usable standalone\n */\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 public readonly data: unknown;\n public readonly requestId?: string;\n\n constructor(\n message: string,\n status: number,\n data?: unknown,\n requestId?: string,\n ) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.data = data;\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: unknown;\n requestId?: string;\n } {\n return {\n name: this.name,\n message: this.message,\n status: this.status,\n data: this.data,\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 let data: Record<string, unknown>;\n try {\n data = 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 // Some Rails BFF endpoints return `{ error: { message, details } }`\n // instead of `{ message }` or `{ error_message }`. Fall through to\n // that shape last so callers always surface the real reason.\n const nestedError =\n typeof data.error === \"object\" && data.error !== null\n ? (data.error as { message?: unknown }).message\n : undefined;\n const msg =\n (data.message as string | undefined) ||\n (data.error_message as string | undefined) ||\n (typeof nestedError === \"string\" ? nestedError : undefined);\n throw new ApiError(\n msg || `${method} request failed`,\n response.status,\n data.errors || data,\n headerRequestId ?? getRequestIdFromJsonBody(data),\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 const blobs: MysiteFiles = {\n content: template?.content ?? \"\",\n head: template?.head ?? \"\",\n stylesheet: template?.stylesheet ?? \"\",\n };\n for (const key of Object.keys(THEME_FILES) as Array<keyof MysiteFiles>) {\n const file = THEME_FILES[key];\n await writeFile(path.join(root, file), blobs[key], \"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","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 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 const suffix = `${hostname().split(\".\")[0] ?? \"dev\"}-${randomBytes(2).toString(\"hex\")}`;\n const name = `${manifest.name} (dev ${suffix})`.slice(0, 50);\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","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 {\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 if (!opts.yes) {\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.0",
3
+ "version": "0.1.2",
4
4
  "description": "Fluid CLI plugin for MySite theme workflows — pull, dev preview server, publish",
5
5
  "files": [
6
6
  "dist",
@@ -25,11 +25,15 @@
25
25
  "chokidar": "^4.0.0",
26
26
  "commander": "^12.0.0",
27
27
  "prompts": "^2.4.2",
28
- "@fluid-app/fluid-cli": "0.1.12"
28
+ "@fluid-app/fluid-cli": "0.1.14"
29
29
  },
30
30
  "devDependencies": {
31
+ "@swc/core": "^1.15.18",
32
+ "@swc/jest": "^0.2.39",
33
+ "@types/jest": "^29.5.14",
31
34
  "@types/node": "24.10.12",
32
35
  "@types/prompts": "^2.4.9",
36
+ "jest": "^29.7.0",
33
37
  "tsdown": "^0.21.0",
34
38
  "typescript": "^5",
35
39
  "@fluid-app/api-client-core": "0.1.0",
@@ -42,6 +46,7 @@
42
46
  "build": "tsdown",
43
47
  "dev": "tsdown --watch",
44
48
  "typecheck": "tsgo --noEmit",
45
- "lint": "oxlint"
49
+ "lint": "oxlint",
50
+ "test": "jest"
46
51
  }
47
52
  }