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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,18 +1,19 @@
1
- import { A as fluid_os_v0_create_fluid_ostheme, B as fluid_os_v0_list_fluid_osversions, C as writeMappings, D as fluid_os_v0_create_fluid_osnavigation_item, E as fluid_os_v0_create_fluid_osnavigation, F as fluid_os_v0_delete_fluid_osprofile, G as fluid_os_v0_update_fluid_ostheme, H as fluid_os_v0_update_fluid_osnavigation_item, I as fluid_os_v0_delete_fluid_osscreen, K as fluid_os_v0_update_fluid_osversion, L as fluid_os_v0_delete_fluid_ostheme, M as fluid_os_v0_create_widget_package_version, N as fluid_os_v0_delete_fluid_osnavigation, O as fluid_os_v0_create_fluid_osprofile, P as fluid_os_v0_delete_fluid_osnavigation_item, R as fluid_os_v0_list_fluid_osdefinitions, S as updateMapping, T as fluid_os_v0_complete_widget_package_version_upload, U as fluid_os_v0_update_fluid_osprofile, V as fluid_os_v0_update_fluid_osnavigation, W as fluid_os_v0_update_fluid_osscreen, _ as deriveSlug, a as buildThemeIdToSlugMap, b as resolveIdToSlug, c as transformNavigationItems, d as transformTheme, f as buildSnapshot, g as writeSnapshot, h as readSnapshot, i as buildNavigationIdToSlugMap, j as fluid_os_v0_create_fluid_osversion, k as fluid_os_v0_create_fluid_osscreen, l as transformProfile, m as diffAgainstSnapshot, o as deriveScreenSlug, p as computeFileHash, q as createFetchClient, r as buildIdToSlugMap, s as transformNavigation, t as pullCommand, u as transformScreen, v as readMappings, w as atomicWriteFile, x as resolveSlugToId, y as removeMapping, z as fluid_os_v0_list_fluid_osnavigation_items } from "./pull-C3yj9L07.mjs";
2
- import { createRequire } from "node:module";
1
+ import { A as STANDALONE_PORTAL_MARKER, C as writeMappings, D as toFluidOsDynamicObject, E as fluidOsApi, O as createFluidOsHeyApiClient, S as updateMapping, T as createFluidOsClient, _ as deriveSlug, a as buildThemeIdToSlugMap, b as resolveIdToSlug, c as transformNavigationItems, d as transformTheme, f as buildSnapshot, g as writeSnapshot, h as readSnapshot, i as buildNavigationIdToSlugMap, j as assertStandalonePortalRemoteActionAllowed, k as ApiError, l as transformProfile, m as diffAgainstSnapshot, o as deriveScreenSlug, p as computeFileHash, r as buildIdToSlugMap, s as transformNavigation, t as pullCommand, u as transformScreen, v as readMappings, w as atomicWriteFile, x as resolveSlugToId, y as removeMapping } from "./pull-CUR8tbW9.mjs";
2
+ import { l as fluidOsV0DefinitionGitCredentialCreate } from "./sdk.gen-DuPhfdxN.mjs";
3
+ import { a as resolvePublishVersion, i as readCompanyWidgetPackages, n as preflightCompanyPackage, o as loadAndValidateWidgetPackage, r as readCompanyWidgetPackage, s as publishWidgetPackage, t as formatPackageKeyMismatch } from "./src-CpH5SpDH.mjs";
3
4
  import { Command } from "commander";
4
5
  import chalk from "chalk";
5
6
  import ora from "ora";
6
7
  import path, { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
- import { fileURLToPath, pathToFileURL } from "node:url";
8
- import { chmodSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
8
+ import { fileURLToPath } from "node:url";
9
+ import { chmodSync, constants, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
9
10
  import { copyFile, lstat, mkdir, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
10
11
  import os, { tmpdir } from "node:os";
11
12
  import { failure, findProjectConfig, getActiveProfile, getAuthToken, listProfileNames, readConfig, success } from "@fluid-app/fluid-cli";
12
13
  import prompts from "prompts";
13
14
  import Handlebars from "handlebars";
14
15
  import { execa } from "execa";
15
- import { createHash, randomUUID } from "node:crypto";
16
+ import { randomUUID } from "node:crypto";
16
17
  import fs from "fs-extra";
17
18
  //#region src/types.ts
18
19
  /**
@@ -95,9 +96,9 @@ async function promptProjectConfig(projectName, options) {
95
96
  //#endregion
96
97
  //#region src/utils/file-system.ts
97
98
  const _currentDir$1 = dirname(fileURLToPath(import.meta.url));
98
- const EMBEDDED_PORTAL_SDK_VERSION = "0.1.462";
99
+ const EMBEDDED_PORTAL_SDK_VERSION = "0.1.463";
99
100
  const EMBEDDED_CLI_CORE_VERSION = "0.1.14";
100
- const EMBEDDED_CLI_PORTAL_VERSION = "0.1.53";
101
+ const EMBEDDED_CLI_PORTAL_VERSION = "0.1.54";
101
102
  /**
102
103
  * Find the package root by walking up from the current directory to the nearest package.json.
103
104
  * Works whether running from dist/ (bundled) or src/utils/ (tsx dev mode).
@@ -1598,6 +1599,15 @@ function portalDevServerOptions(port, exposeToNetwork, cwd = process.cwd()) {
1598
1599
  watch: { ignored: createPortalWatchIgnored(cwd) }
1599
1600
  };
1600
1601
  }
1602
+ function canFallbackToBareVite(cwd) {
1603
+ try {
1604
+ lstatSync(join(cwd, STANDALONE_PORTAL_MARKER));
1605
+ return false;
1606
+ } catch (error) {
1607
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") return true;
1608
+ throw error;
1609
+ }
1610
+ }
1601
1611
  /**
1602
1612
  * Check if the portal directory exists and has content files.
1603
1613
  * Returns true if at minimum `portal/definition.json` exists.
@@ -1637,7 +1647,7 @@ async function autoPull(cwd) {
1637
1647
  console.log(chalk.yellow("No portal/ directory found.") + " Attempting to pull content...");
1638
1648
  console.log();
1639
1649
  try {
1640
- const { pullCommand } = await import("./pull-C3yj9L07.mjs").then((n) => n.n);
1650
+ const { pullCommand } = await import("./pull-CUR8tbW9.mjs").then((n) => n.n);
1641
1651
  await pullCommand.parseAsync([], { from: "user" });
1642
1652
  return hasPortalContent(cwd);
1643
1653
  } catch (err) {
@@ -1690,12 +1700,17 @@ const devCommand = new Command("dev").description("Start the development server
1690
1700
  const port = Number.isFinite(parsedPort) ? parsedPort : 5173;
1691
1701
  try {
1692
1702
  const { createServer } = await import("vite");
1693
- const { portalDevPlugin } = await import("./portal-dev-plugin-ByYelgFg.mjs").then((n) => n.n);
1694
- const { backendDevPlugin } = await import("./backend-dev-plugin-B9-S4Is-.mjs").then((n) => n.n);
1703
+ const { portalDevPlugin } = await import("./portal-dev-plugin-CHlxpXc5.mjs").then((n) => n.n);
1704
+ const { backendDevPlugin } = await import("./backend-dev-plugin-CY3yxgJ1.mjs").then((n) => n.n);
1705
+ const { portalWidgetDevPlugin } = await import("./portal-widget-dev-plugin-B53Lf9Ke.mjs").then((n) => n.n);
1695
1706
  const server = await createServer({
1696
1707
  configFile: viteConfigPath,
1697
1708
  envPrefix: ["VITE_", "NEXT_PUBLIC_"],
1698
- plugins: [portalDevPlugin(), backendDevPlugin()],
1709
+ plugins: [
1710
+ portalDevPlugin(),
1711
+ backendDevPlugin(),
1712
+ ...portalWidgetDevPlugin()
1713
+ ],
1699
1714
  server: portalDevServerOptions(port, options.host === true, cwd)
1700
1715
  });
1701
1716
  await server.listen();
@@ -1707,6 +1722,12 @@ const devCommand = new Command("dev").description("Start the development server
1707
1722
  process.on("SIGTERM", shutdown);
1708
1723
  await new Promise(() => {});
1709
1724
  } catch (error) {
1725
+ if (!canFallbackToBareVite(cwd)) {
1726
+ console.error(chalk.red("Could not start the enriched standalone portal dev server."));
1727
+ console.error(chalk.yellow("Standalone portals require the local manifest and widget plugins; bare Vite cannot serve this project correctly."));
1728
+ console.error(error instanceof Error ? error.message : String(error));
1729
+ process.exit(1);
1730
+ }
1710
1731
  console.warn(chalk.yellow("Could not start the enriched dev server; falling back to `vite`. Local portal content requires portalDevPlugin in vite.config for the fallback path."), error instanceof Error ? error.message : String(error));
1711
1732
  try {
1712
1733
  await execa("pnpm", viteArgs, {
@@ -1760,6 +1781,43 @@ const buildCommand = new Command("build").description("Build the application for
1760
1781
  }
1761
1782
  });
1762
1783
  //#endregion
1784
+ //#region ../../portal/core/src/registries/container-registry.ts
1785
+ /**
1786
+ * Default container registry with LayoutWidget and ContainerWidget
1787
+ */
1788
+ const DEFAULT_CONTAINER_REGISTRY = new Map([
1789
+ ["LayoutWidget", {
1790
+ isContainer: true,
1791
+ getChildren: (props) => props.children || [],
1792
+ setChildren: (props, children) => ({
1793
+ ...props,
1794
+ children
1795
+ })
1796
+ }],
1797
+ ["CardWidget", {
1798
+ isContainer: true,
1799
+ getChildren: (props) => props.children || [],
1800
+ setChildren: (props, children) => ({
1801
+ ...props,
1802
+ children
1803
+ })
1804
+ }],
1805
+ ["ContainerWidget", {
1806
+ isContainer: true,
1807
+ getChildren: (props) => props.children || [],
1808
+ setChildren: (props, children) => ({
1809
+ ...props,
1810
+ children
1811
+ })
1812
+ }]
1813
+ ]);
1814
+ /**
1815
+ * Check if a widget type is a container
1816
+ */
1817
+ function isContainerWidget(widgetType, containerRegistry) {
1818
+ return containerRegistry.has(widgetType);
1819
+ }
1820
+ //#endregion
1763
1821
  //#region src/utils/push-validation.ts
1764
1822
  /**
1765
1823
  * Cross-reference validation and change categorization utilities for the push command.
@@ -1829,10 +1887,67 @@ function categorizeChanges(diff) {
1829
1887
  return result;
1830
1888
  }
1831
1889
  /**
1890
+ * Faults in a screen's widget tree, as `component_tree[1].props.children[0]`
1891
+ * style paths.
1892
+ *
1893
+ * The screen check used to stop at "name is a string, component_tree is an
1894
+ * array" and never look inside. A node missing its `id` published happily —
1895
+ * `✔ Portal files valid`, live version activated — and then the admin App
1896
+ * Builder rendered the whole portal blank, in a different application, with
1897
+ * nothing pointing back at the screen. The builder backfills a missing id
1898
+ * (`widget-utils.ts`), but regenerates it per render while identity lookups
1899
+ * match on it, so such a node never reconciles.
1900
+ *
1901
+ * This matters because hand-written `component_tree` JSON is a first-class
1902
+ * path, not an edge case: it is how Mist's agent places widgets, and how
1903
+ * anything the builder cannot express gets authored.
1904
+ *
1905
+ * Deliberately conservative about what counts as a node. Only entries of
1906
+ * `component_tree` and of a node's `props.children` are checked — the two
1907
+ * places every container in `DEFAULT_CONTAINER_REGISTRY` keeps them. A blind
1908
+ * walk of every nested object would be wrong: screens are full of `type`-
1909
+ * bearing values that are NOT widgets (`background: { type: "solid" }`,
1910
+ * `dataSource: { type: "api" }`), and demanding an `id` of those would reject
1911
+ * screens that work today. A container using some other prop key is a missed
1912
+ * check, which is the safe direction for a gate that blocks publishing.
1913
+ */
1914
+ function collectWidgetNodeErrors(nodes) {
1915
+ return walkWidgetNodes(nodes, "component_tree", false, /* @__PURE__ */ new Map());
1916
+ }
1917
+ function walkWidgetNodes(nodes, path, nullIsAnEmptySlot, seenIds) {
1918
+ const errors = [];
1919
+ nodes.forEach((node, index) => {
1920
+ const at = `${path}[${index}]`;
1921
+ if (node === null || node === void 0) {
1922
+ if (!nullIsAnEmptySlot) errors.push(`${at} must be a widget object.`);
1923
+ return;
1924
+ }
1925
+ if (typeof node !== "object" || Array.isArray(node)) {
1926
+ errors.push(`${at} must be a widget object.`);
1927
+ return;
1928
+ }
1929
+ const record = node;
1930
+ if (typeof record.type !== "string" || record.type.trim().length === 0) errors.push(`${at} is missing a "type".`);
1931
+ else if (record.type !== record.type.trim()) errors.push(`${at} has a "type" with surrounding whitespace (${JSON.stringify(record.type)}). Widget types are matched exactly, so this resolves to no component and renders blank.`);
1932
+ if (typeof record.id !== "string" || record.id.trim().length === 0) errors.push(`${at} is missing an "id". Every widget needs a stable id — without one the screen publishes but renders blank in the app builder.`);
1933
+ else {
1934
+ const firstClaim = seenIds.get(record.id);
1935
+ if (firstClaim === void 0) seenIds.set(record.id, at);
1936
+ else errors.push(`${at} reuses the id ${JSON.stringify(record.id)}, already used by ${firstClaim}. Ids must be unique within a screen — the builder resolves a widget by id and takes the first match, so duplicates make select, delete and drag-drop act on the wrong widget.`);
1937
+ }
1938
+ const props = record.props;
1939
+ if (typeof props !== "object" || props === null || Array.isArray(props)) return;
1940
+ const children = props.children;
1941
+ if (Array.isArray(children)) errors.push(...walkWidgetNodes(children, `${at}.props.children`, true, seenIds));
1942
+ });
1943
+ return errors;
1944
+ }
1945
+ /**
1832
1946
  * Validate that all cross-references between local portal files are valid.
1833
1947
  *
1834
1948
  * Checks:
1835
1949
  * - New and changed resources contain the required local-file fields
1950
+ * - Every widget node in a changed screen has a string "id" and "type"
1836
1951
  * - New and changed themes include the required config object
1837
1952
  * - Navigation items' "screen" slugs reference existing screen files or mappings
1838
1953
  * - Profile "navigation" and "mobile_navigation" slugs reference existing nav files or mappings
@@ -1853,6 +1968,16 @@ async function validateCrossReferences(portalDir, mappings, changes) {
1853
1968
  file,
1854
1969
  message: "Screen requires a string \"name\" and array \"component_tree\"."
1855
1970
  });
1971
+ else {
1972
+ if (screen.component_tree.length > 1) errors.push({
1973
+ file,
1974
+ message: `"component_tree" has ${screen.component_tree.length} top-level nodes, and pushing cannot preserve that. More than one root is serialized into an untyped wrapper the admin builder cannot render, so the screen would arrive blank and uneditable. Wrap them in a single ${CONTAINER_TYPE_LIST} node and move them into its "props.children".`
1975
+ });
1976
+ for (const error of collectWidgetNodeErrors(screen.component_tree)) errors.push({
1977
+ file,
1978
+ message: error
1979
+ });
1980
+ }
1856
1981
  } catch {
1857
1982
  errors.push({
1858
1983
  file,
@@ -1921,6 +2046,59 @@ async function validateCrossReferences(portalDir, mappings, changes) {
1921
2046
  return errors;
1922
2047
  }
1923
2048
  /**
2049
+ * Warns about screens the admin visual builder will not accept drops on.
2050
+ *
2051
+ * Only container widgets receive the builder's edit-mode child-management
2052
+ * callbacks (`onAddChild`, child select/delete/duplicate), so a screen whose
2053
+ * `component_tree` has no container at the top level renders perfectly — in
2054
+ * local preview and in the live portal — while offering no drop zones at all.
2055
+ * A human opening it in the builder cannot drag anything onto the page.
2056
+ *
2057
+ * That combination (correct everywhere you can look, broken in the one place
2058
+ * you cannot) is why this is worth reporting at push time. It is the most
2059
+ * likely defect in a hand- or agent-authored screen.
2060
+ *
2061
+ * Deliberately a WARNING, never an error. Screens without a container root are
2062
+ * valid definitions that render correctly, and a repository may hold ones that
2063
+ * predate this check; failing their push would break working portals to enforce
2064
+ * an authoring convention.
2065
+ *
2066
+ * Silent on anything it cannot parse. Malformed and unreadable screens are
2067
+ * already reported as hard errors by {@link validateCrossReferences}, and a
2068
+ * second complaint about a file that never parsed would be noise.
2069
+ */
2070
+ async function validateScreenStructure(portalDir, changes) {
2071
+ const warnings = [];
2072
+ const files = [...changes.screens.new, ...changes.screens.changed];
2073
+ for (const file of files) {
2074
+ let screen;
2075
+ try {
2076
+ screen = await readPortalFile$1(portalDir, file);
2077
+ } catch {
2078
+ continue;
2079
+ }
2080
+ const tree = screen.component_tree;
2081
+ if (!Array.isArray(tree) || tree.length === 0) continue;
2082
+ if (tree.length !== 1) continue;
2083
+ const root = tree[0];
2084
+ const rootType = root && typeof root === "object" ? root.type : void 0;
2085
+ if (typeof rootType === "string" && isContainerWidget(rootType, CONTAINER_REGISTRY)) continue;
2086
+ warnings.push({
2087
+ file,
2088
+ message: `No container at the top of "component_tree", so the admin builder will show no drop zones on this screen. Wrap the widgets in a ${CONTAINER_TYPE_LIST} node and move them into its "props.children".`
2089
+ });
2090
+ }
2091
+ return warnings;
2092
+ }
2093
+ /**
2094
+ * Shared with the portal runtime so this check cannot drift from the behaviour
2095
+ * it predicts. `DEFAULT_CONTAINER_REGISTRY` is the same map the builder consults
2096
+ * via `isContainerWidget`.
2097
+ */
2098
+ const CONTAINER_REGISTRY = DEFAULT_CONTAINER_REGISTRY;
2099
+ /** Rendered into the warning so it names the real options, not a stale list. */
2100
+ const CONTAINER_TYPE_LIST = [...DEFAULT_CONTAINER_REGISTRY.keys()].map((type) => `"${type}"`).join(" / ");
2101
+ /**
1924
2102
  * Build a set of valid slugs for a resource type by combining
1925
2103
  * existing mapping slugs with local file slugs on disk.
1926
2104
  */
@@ -1949,6 +2127,26 @@ function validateNavigationItems(items, file, validScreenSlugs, errors) {
1949
2127
  //#endregion
1950
2128
  //#region src/api/git-credential.ts
1951
2129
  /**
2130
+ * Portal git-credential adapter (the "invisible git" seam).
2131
+ *
2132
+ * Mints a short-lived (~1h) remote URL with an embedded installation
2133
+ * token for the portal's provisioned GitHub repo. The URL is used
2134
+ * immediately for one git op and discarded — NEVER persisted or logged.
2135
+ *
2136
+ * The backend endpoint (GitSync phase 006, portal adapter) mirrors the
2137
+ * widget contract exactly:
2138
+ *
2139
+ * POST <endpoint> -> 201 { git_credential: { remote_url, expires_at }, status, meta }
2140
+ * errors: 409 not_provisioned/not_connected, 502 vendor_error, 422 else
2141
+ * { error_message, errors: { type: [type] }, meta }
2142
+ *
2143
+ * The exact path is still being finalized as 006 lands, so it lives in
2144
+ * one constant here. Until the endpoint ships to prod a request 404s —
2145
+ * which we surface as `unavailable` so callers degrade gracefully
2146
+ * (warn + skip the git sync, proceed with the normal content push).
2147
+ */
2148
+ /** Minted credential — token embedded in `remote_url`. Treat as secret. */
2149
+ /**
1952
2150
  * Build the portal git-credential path for a definition id.
1953
2151
  *
1954
2152
  * Matches GitSync Phase 006 (fluid-commerce/fluid #19596): a member
@@ -1969,7 +2167,8 @@ const DEFAULT_RETRY_DELAYS_MS = [
1969
2167
  15e3
1970
2168
  ];
1971
2169
  const DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
1972
- const RETRYABLE_CONFLICT_TYPES = new Set(["not_provisioned"]);
2170
+ const RETRYABLE_CONFLICT_TYPES = new Set(["not_provisioned", "not_connected"]);
2171
+ const DURABLE_REPO_STATUSES = new Set(["disconnected"]);
1973
2172
  function nonEmptyString(value) {
1974
2173
  return typeof value === "string" && value.length > 0 ? value : void 0;
1975
2174
  }
@@ -1977,6 +2176,13 @@ function firstString(value) {
1977
2176
  if (Array.isArray(value)) return nonEmptyString(value[0]);
1978
2177
  return nonEmptyString(value);
1979
2178
  }
2179
+ function apiErrorBody(error) {
2180
+ if (error.data !== null && typeof error.data === "object" && ("error" in error.data || "error_message" in error.data || "errors" in error.data)) return error.data;
2181
+ return {
2182
+ error_message: error.message,
2183
+ errors: error.data
2184
+ };
2185
+ }
1980
2186
  function extractErrorDetails(body, fallback) {
1981
2187
  if (!body || typeof body !== "object") return { reason: fallback };
1982
2188
  const envelope = body;
@@ -1985,9 +2191,11 @@ function extractErrorDetails(body, fallback) {
1985
2191
  const errors = envelope.errors && typeof envelope.errors === "object" ? envelope.errors : void 0;
1986
2192
  const reason = nonEmptyString(envelope.error_message) ?? nonEmptyString(error?.message) ?? nonEmptyString(envelope.error) ?? fallback;
1987
2193
  const failureType = firstString(errors?.type) ?? nonEmptyString(nestedDetails?.type);
2194
+ const repoStatus = firstString(errors?.repo_status) ?? nonEmptyString(nestedDetails?.repo_status);
1988
2195
  return {
1989
2196
  reason,
1990
- ...failureType ? { failureType } : {}
2197
+ ...failureType ? { failureType } : {},
2198
+ ...repoStatus ? { repoStatus } : {}
1991
2199
  };
1992
2200
  }
1993
2201
  function sleep(delayMs) {
@@ -2034,64 +2242,64 @@ async function createPortalGitCredential(options) {
2034
2242
  const { baseUrl, token, definitionId } = options;
2035
2243
  const fetchImpl = options.fetchImpl ?? fetch;
2036
2244
  const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
2037
- const url = `${baseUrl.replace(/\/$/, "")}${portalGitCredentialPath(definitionId)}`;
2038
2245
  const abortController = new AbortController();
2039
2246
  let requestTimedOut = false;
2040
2247
  const timeout = setTimeout(() => {
2041
2248
  requestTimedOut = true;
2042
2249
  abortController.abort();
2043
2250
  }, requestTimeoutMs);
2044
- let resp;
2045
- let body = null;
2046
2251
  try {
2047
- resp = await fetchImpl(url, {
2048
- method: "POST",
2049
- headers: {
2050
- Authorization: `Bearer ${token}`,
2051
- Accept: "application/json"
2052
- },
2252
+ const credential = (await fluidOsV0DefinitionGitCredentialCreate({
2253
+ client: createFluidOsHeyApiClient({
2254
+ baseUrl,
2255
+ fetch: fetchImpl,
2256
+ getAuthToken: () => token
2257
+ }),
2258
+ path: { id: definitionId },
2053
2259
  signal: abortController.signal
2054
- });
2055
- if (resp.status !== 404) try {
2056
- body = await resp.json();
2057
- } catch (err) {
2058
- if (requestTimedOut) throw err;
2059
- }
2060
- } catch (err) {
2061
- return {
2260
+ })).git_credential;
2261
+ if (!credential?.remote_url) return {
2062
2262
  status: "error",
2063
- reason: requestTimedOut ? `git credential request timed out after ${requestTimeoutMs}ms` : err instanceof Error ? err.message : String(err)
2263
+ reason: "git credential response missing remote_url"
2064
2264
  };
2065
- } finally {
2066
- clearTimeout(timeout);
2067
- }
2068
- if (resp.status === 404) return {
2069
- status: "unavailable",
2070
- reason: "git sync isn't available for portals on this Fluid API yet",
2071
- retryable: false
2072
- };
2073
- if (!resp.ok) {
2074
- const details = extractErrorDetails(body, `git credential request failed (HTTP ${resp.status})`);
2075
- if (resp.status === 409) return {
2076
- status: "unavailable",
2077
- reason: details.reason,
2078
- retryable: details.failureType !== void 0 && RETRYABLE_CONFLICT_TYPES.has(details.failureType),
2079
- ...details.failureType !== void 0 ? { conflictType: details.failureType } : {}
2265
+ return {
2266
+ status: "ok",
2267
+ credential
2080
2268
  };
2269
+ } catch (err) {
2270
+ if (requestTimedOut) return {
2271
+ status: "error",
2272
+ reason: `git credential request timed out after ${requestTimeoutMs}ms`
2273
+ };
2274
+ if (err instanceof ApiError) {
2275
+ if (err.status === 404) return {
2276
+ status: "unavailable",
2277
+ reason: "git sync isn't available for portals on this Fluid API yet",
2278
+ retryable: false
2279
+ };
2280
+ const details = extractErrorDetails(apiErrorBody(err), `git credential request failed (HTTP ${err.status})`);
2281
+ if (err.status === 409) {
2282
+ const durable = details.repoStatus !== void 0 && DURABLE_REPO_STATUSES.has(details.repoStatus);
2283
+ return {
2284
+ status: "unavailable",
2285
+ reason: details.reason,
2286
+ retryable: !durable && details.failureType !== void 0 && RETRYABLE_CONFLICT_TYPES.has(details.failureType),
2287
+ ...details.failureType !== void 0 ? { conflictType: details.failureType } : {},
2288
+ ...details.repoStatus !== void 0 ? { repoStatus: details.repoStatus } : {}
2289
+ };
2290
+ }
2291
+ return {
2292
+ status: "error",
2293
+ reason: details.reason
2294
+ };
2295
+ }
2081
2296
  return {
2082
2297
  status: "error",
2083
- reason: details.reason
2298
+ reason: err instanceof Error ? err.message : String(err)
2084
2299
  };
2300
+ } finally {
2301
+ clearTimeout(timeout);
2085
2302
  }
2086
- const credential = body?.git_credential;
2087
- if (!credential?.remote_url) return {
2088
- status: "error",
2089
- reason: "git credential response missing remote_url"
2090
- };
2091
- return {
2092
- status: "ok",
2093
- credential
2094
- };
2095
2303
  }
2096
2304
  //#endregion
2097
2305
  //#region src/lib/git-sync.ts
@@ -2226,7 +2434,7 @@ function toApiComponentTree(tree) {
2226
2434
  return { children: tree };
2227
2435
  }
2228
2436
  /**
2229
- * Create an authenticated FetchClient using the stored CLI profile.
2437
+ * Create an authenticated FluidOsClient using the stored CLI profile.
2230
2438
  */
2231
2439
  function createClient$2() {
2232
2440
  const token = getAuthToken();
@@ -2235,15 +2443,15 @@ function createClient$2() {
2235
2443
  if (!profile) throw new Error("Not logged in. Run " + chalk.cyan("fluid login") + " first.");
2236
2444
  throw new Error("No auth token found for profile " + chalk.cyan(profile.name) + ". Run " + chalk.cyan("fluid login") + " to re-authenticate.");
2237
2445
  }
2238
- return createFetchClient({
2446
+ return createFluidOsClient({
2239
2447
  baseUrl: process.env["FLUID_API_BASE"] ?? "https://api.fluid.app",
2240
- getAuthToken: () => token
2448
+ token
2241
2449
  });
2242
2450
  }
2243
2451
  async function createAndActivateVersion(client, definitionId) {
2244
- const versionId = (await fluid_os_v0_create_fluid_osversion(client, definitionId)).version?.id;
2452
+ const versionId = (await fluidOsApi.createVersion(client, definitionId)).version?.id;
2245
2453
  if (!versionId) throw new Error("the version API returned no version ID");
2246
- await fluid_os_v0_update_fluid_osversion(client, definitionId, versionId, { version: { active: true } });
2454
+ await fluidOsApi.updateVersion(client, definitionId, versionId, { version: { active: true } });
2247
2455
  return String(versionId);
2248
2456
  }
2249
2457
  async function activateDraft(definitionId, existingClient) {
@@ -2331,10 +2539,10 @@ async function pushScreens(client, defId, portalDir, changes, mappings) {
2331
2539
  const slug = slugFromPath(file);
2332
2540
  try {
2333
2541
  const local = await readPortalFile(portalDir, file);
2334
- const newId = (await fluid_os_v0_create_fluid_osscreen(client, defId, { screen: {
2542
+ const newId = (await fluidOsApi.createScreen(client, defId, { screen: {
2335
2543
  name: local.name,
2336
2544
  slug,
2337
- component_tree: toApiComponentTree(local.component_tree)
2545
+ component_tree: toFluidOsDynamicObject(toApiComponentTree(local.component_tree))
2338
2546
  } })).screen?.id;
2339
2547
  if (newId != null) currentMappings = updateMapping(currentMappings, "screens", slug, newId);
2340
2548
  results.push({
@@ -2372,10 +2580,10 @@ async function pushScreens(client, defId, portalDir, changes, mappings) {
2372
2580
  }
2373
2581
  try {
2374
2582
  const local = await readPortalFile(portalDir, file);
2375
- await fluid_os_v0_update_fluid_osscreen(client, defId, screenId, { screen: {
2583
+ await fluidOsApi.updateScreen(client, defId, screenId, { screen: {
2376
2584
  name: local.name,
2377
2585
  slug,
2378
- component_tree: toApiComponentTree(local.component_tree)
2586
+ component_tree: toFluidOsDynamicObject(toApiComponentTree(local.component_tree))
2379
2587
  } });
2380
2588
  results.push({
2381
2589
  file,
@@ -2411,7 +2619,7 @@ async function pushScreens(client, defId, portalDir, changes, mappings) {
2411
2619
  };
2412
2620
  }
2413
2621
  try {
2414
- await fluid_os_v0_delete_fluid_osscreen(client, defId, screenId);
2622
+ await fluidOsApi.deleteScreen(client, defId, screenId);
2415
2623
  currentMappings = removeMapping(currentMappings, "screens", slug);
2416
2624
  results.push({
2417
2625
  file,
@@ -2446,10 +2654,10 @@ async function pushThemes(client, defId, portalDir, changes, mappings) {
2446
2654
  const slug = slugFromPath(file);
2447
2655
  try {
2448
2656
  const local = await readPortalFile(portalDir, file);
2449
- const newId = (await fluid_os_v0_create_fluid_ostheme(client, defId, { theme: {
2657
+ const newId = (await fluidOsApi.createTheme(client, defId, { theme: {
2450
2658
  name: local.name,
2451
2659
  active: local.active,
2452
- config: local.config
2660
+ config: toFluidOsDynamicObject(local.config)
2453
2661
  } })).theme?.id;
2454
2662
  if (newId != null) currentMappings = updateMapping(currentMappings, "themes", slug, newId);
2455
2663
  results.push({
@@ -2487,10 +2695,10 @@ async function pushThemes(client, defId, portalDir, changes, mappings) {
2487
2695
  }
2488
2696
  try {
2489
2697
  const local = await readPortalFile(portalDir, file);
2490
- await fluid_os_v0_update_fluid_ostheme(client, defId, themeId, { theme: {
2698
+ await fluidOsApi.updateTheme(client, defId, themeId, { theme: {
2491
2699
  name: local.name,
2492
2700
  active: local.active,
2493
- config: local.config
2701
+ config: toFluidOsDynamicObject(local.config)
2494
2702
  } });
2495
2703
  results.push({
2496
2704
  file,
@@ -2526,7 +2734,7 @@ async function pushThemes(client, defId, portalDir, changes, mappings) {
2526
2734
  };
2527
2735
  }
2528
2736
  try {
2529
- await fluid_os_v0_delete_fluid_ostheme(client, defId, themeId);
2737
+ await fluidOsApi.deleteTheme(client, defId, themeId);
2530
2738
  currentMappings = removeMapping(currentMappings, "themes", slug);
2531
2739
  results.push({
2532
2740
  file,
@@ -2616,7 +2824,7 @@ async function resolveCreatedNavigationItemId(client, defId, navigationId, respo
2616
2824
  }
2617
2825
  let response;
2618
2826
  try {
2619
- response = await fluid_os_v0_list_fluid_osnavigation_items(client, defId, navigationId);
2827
+ response = await fluidOsApi.listNavigationItems(client, defId, navigationId);
2620
2828
  } catch (recoveryError) {
2621
2829
  throw new NavigationRemoteStateUnknownError(`Remote navigation state is unknown because item creation succeeded without returning its required ID, and the recovery read failed: ${enrichedErrorMessage(recoveryError)}. Back up ${file}, run fluid portal pull, then reapply pending edits before pushing again.`);
2622
2830
  }
@@ -2666,7 +2874,7 @@ async function persistCreatedNavigationItemId(client, defId, navigationId, porta
2666
2874
  await atomicWriteFile(join(portalDir, file), JSON.stringify(updated, null, 2) + "\n");
2667
2875
  } catch (persistError) {
2668
2876
  try {
2669
- await fluid_os_v0_delete_fluid_osnavigation_item(client, defId, navigationId, serverId);
2877
+ await fluidOsApi.deleteNavigationItem(client, defId, navigationId, serverId);
2670
2878
  } catch (rollbackError) {
2671
2879
  throw new NavigationRemoteStateUnknownError(`Failed to persist navigation item ${serverId} locally: ${enrichedErrorMessage(persistError)}. Automatic rollback also failed: ${enrichedErrorMessage(rollbackError)}. Back up ${file}, run fluid portal pull, then reapply pending edits before pushing again.`);
2672
2880
  }
@@ -2680,7 +2888,7 @@ async function persistCreatedNavigationItemId(client, defId, navigationId, porta
2680
2888
  * must be reconciled before the push snapshot hashes it.
2681
2889
  */
2682
2890
  async function reconcileNavigationFile(client, defId, navId, portalDir, file, local, mappings) {
2683
- const response = await fluid_os_v0_list_fluid_osnavigation_items(client, defId, navId);
2891
+ const response = await fluidOsApi.listNavigationItems(client, defId, navId);
2684
2892
  const reconciled = {
2685
2893
  ...local,
2686
2894
  navigation_items: transformNavigationItems(response.navigation_items ?? [], buildIdToSlugMap(mappings.screens))
@@ -2700,7 +2908,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
2700
2908
  let remoteChangesComplete = false;
2701
2909
  try {
2702
2910
  let local = await readPortalFile(portalDir, file);
2703
- const newId = (await fluid_os_v0_create_fluid_osnavigation(client, defId, { navigation: {
2911
+ const newId = (await fluidOsApi.createNavigation(client, defId, { navigation: {
2704
2912
  name: local.name,
2705
2913
  platform: local.platform
2706
2914
  } })).navigation?.id;
@@ -2720,7 +2928,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
2720
2928
  source: item.source ?? void 0,
2721
2929
  parent_id: resolvedParentId
2722
2930
  };
2723
- const createdId = await resolveCreatedNavigationItemId(client, defId, newId, (await fluid_os_v0_create_fluid_osnavigation_item(client, defId, newId, { navigation_item: createBody })).navigation_item?.id, knownServerIds, createBody, file);
2931
+ const createdId = await resolveCreatedNavigationItemId(client, defId, newId, (await fluidOsApi.createNavigationItem(client, defId, newId, { navigation_item: createBody })).navigation_item?.id, knownServerIds, createBody, file);
2724
2932
  local = await persistCreatedNavigationItemId(client, defId, newId, portalDir, file, local, itemIndex, createdId);
2725
2933
  serverIdByItemIndex.set(itemIndex, createdId);
2726
2934
  }
@@ -2763,12 +2971,12 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
2763
2971
  let remoteChangesComplete = false;
2764
2972
  try {
2765
2973
  let local = await readPortalFile(portalDir, file);
2766
- await fluid_os_v0_update_fluid_osnavigation(client, defId, navId, { navigation: {
2974
+ await fluidOsApi.updateNavigation(client, defId, navId, { navigation: {
2767
2975
  name: local.name,
2768
2976
  platform: local.platform
2769
2977
  } });
2770
2978
  const resolvedItems = flattenNavigationItems(resolveNavigationItemScreenIds(local.navigation_items, currentMappings));
2771
- const serverItems = flattenServerNavigationItems((await fluid_os_v0_list_fluid_osnavigation_items(client, defId, navId)).navigation_items ?? []);
2979
+ const serverItems = flattenServerNavigationItems((await fluidOsApi.listNavigationItems(client, defId, navId)).navigation_items ?? []);
2772
2980
  const serverById = new Map(serverItems.map((s) => [s.id, s]));
2773
2981
  const knownServerIds = new Set(serverById.keys());
2774
2982
  const localIds = new Set(resolvedItems.filter((i) => i.id).map((i) => i.id));
@@ -2785,7 +2993,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
2785
2993
  parent_id: resolvedParentId
2786
2994
  };
2787
2995
  if (item.id && serverById.has(item.id)) {
2788
- await fluid_os_v0_update_fluid_osnavigation_item(client, defId, navId, item.id, { navigation_item: body });
2996
+ await fluidOsApi.updateNavigationItem(client, defId, navId, item.id, { navigation_item: body });
2789
2997
  serverIdByItemIndex.set(itemIndex, item.id);
2790
2998
  } else {
2791
2999
  const createBody = {
@@ -2793,12 +3001,12 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
2793
3001
  label: body.label ?? "",
2794
3002
  position: body.position ?? 0
2795
3003
  };
2796
- const createdId = await resolveCreatedNavigationItemId(client, defId, navId, (await fluid_os_v0_create_fluid_osnavigation_item(client, defId, navId, { navigation_item: createBody })).navigation_item?.id, knownServerIds, createBody, file);
3004
+ const createdId = await resolveCreatedNavigationItemId(client, defId, navId, (await fluidOsApi.createNavigationItem(client, defId, navId, { navigation_item: createBody })).navigation_item?.id, knownServerIds, createBody, file);
2797
3005
  local = await persistCreatedNavigationItemId(client, defId, navId, portalDir, file, local, itemIndex, createdId);
2798
3006
  serverIdByItemIndex.set(itemIndex, createdId);
2799
3007
  }
2800
3008
  }
2801
- for (const serverItem of serverItems) if (!localIds.has(serverItem.id)) await fluid_os_v0_delete_fluid_osnavigation_item(client, defId, navId, serverItem.id);
3009
+ for (const serverItem of serverItems) if (!localIds.has(serverItem.id)) await fluidOsApi.deleteNavigationItem(client, defId, navId, serverItem.id);
2802
3010
  remoteChangesComplete = true;
2803
3011
  await reconcileNavigationFile(client, defId, navId, portalDir, file, local, currentMappings);
2804
3012
  results.push({
@@ -2835,7 +3043,7 @@ async function pushNavigations(client, defId, portalDir, changes, mappings) {
2835
3043
  };
2836
3044
  }
2837
3045
  try {
2838
- await fluid_os_v0_delete_fluid_osnavigation(client, defId, navId);
3046
+ await fluidOsApi.deleteNavigation(client, defId, navId);
2839
3047
  currentMappings = removeMapping(currentMappings, "navigations", slug);
2840
3048
  results.push({
2841
3049
  file,
@@ -2869,7 +3077,8 @@ async function pushProfiles(client, defId, portalDir, changes, mappings) {
2869
3077
  for (const file of changes.new) {
2870
3078
  const slug = slugFromPath(file);
2871
3079
  try {
2872
- const newId = (await fluid_os_v0_create_fluid_osprofile(client, defId, { profile: resolveProfileBody(await readPortalFile(portalDir, file), currentMappings) })).profile?.id;
3080
+ const body = resolveProfileBody(await readPortalFile(portalDir, file), currentMappings);
3081
+ const newId = (await fluidOsApi.createProfile(client, defId, { profile: body })).profile?.id;
2873
3082
  if (newId != null) currentMappings = updateMapping(currentMappings, "profiles", slug, newId);
2874
3083
  results.push({
2875
3084
  file,
@@ -2905,7 +3114,8 @@ async function pushProfiles(client, defId, portalDir, changes, mappings) {
2905
3114
  };
2906
3115
  }
2907
3116
  try {
2908
- await fluid_os_v0_update_fluid_osprofile(client, defId, profileId, { profile: resolveProfileBody(await readPortalFile(portalDir, file), currentMappings) });
3117
+ const body = resolveProfileBody(await readPortalFile(portalDir, file), currentMappings);
3118
+ await fluidOsApi.updateProfile(client, defId, profileId, { profile: body });
2909
3119
  results.push({
2910
3120
  file,
2911
3121
  action: "updated",
@@ -2940,7 +3150,7 @@ async function pushProfiles(client, defId, portalDir, changes, mappings) {
2940
3150
  };
2941
3151
  }
2942
3152
  try {
2943
- await fluid_os_v0_delete_fluid_osprofile(client, defId, profileId);
3153
+ await fluidOsApi.deleteProfile(client, defId, profileId);
2944
3154
  currentMappings = removeMapping(currentMappings, "profiles", slug);
2945
3155
  results.push({
2946
3156
  file,
@@ -3013,6 +3223,7 @@ function printPushReport(results, skippedPhases) {
3013
3223
  }
3014
3224
  const pushCommand = new Command("push").description("Push local portal content changes to the Fluid OS API").option("--yes", "Skip confirmation prompt").option("--activate", "Create and activate a live version after a successful push").action(async (options) => {
3015
3225
  const cwd = process.cwd();
3226
+ await assertStandalonePortalRemoteActionAllowed(cwd, "portal push");
3016
3227
  const portalDir = join(cwd, PORTAL_DIR);
3017
3228
  const portalSyncDir = join(cwd, PORTAL_SYNC_DIR$1);
3018
3229
  console.log();
@@ -3085,6 +3296,12 @@ const pushCommand = new Command("push").description("Push local portal content c
3085
3296
  process.exit(1);
3086
3297
  }
3087
3298
  spinner.succeed("Portal files valid");
3299
+ const structureWarnings = await validateScreenStructure(portalDir, changes);
3300
+ if (structureWarnings.length > 0) {
3301
+ console.log();
3302
+ for (const warning of structureWarnings) console.log(chalk.yellow(" warning ") + chalk.white(warning.file + ": ") + warning.message);
3303
+ console.log();
3304
+ }
3088
3305
  spinner.start("Authenticating...");
3089
3306
  let client;
3090
3307
  try {
@@ -3193,13 +3410,14 @@ const pushCommand = new Command("push").description("Push local portal content c
3193
3410
  * against the portal git-credential endpoint.
3194
3411
  *
3195
3412
  * When the credential endpoint reports that no repository is available
3196
- * (the company's Git integration isn't connected, the repo is still
3197
- * unprovisioned after the retry budget, or the endpoint isn't deployed),
3198
- * clone falls back to scaffolding the portal locally from the bundled
3199
- * starter template instead of failing. Local files plus API sync
3413
+ * (the repo was torn down, provisioning failed, it is still not ready
3414
+ * after the retry budget, or the endpoint isn't deployed), clone falls
3415
+ * back to scaffolding the portal locally from the bundled starter
3416
+ * template instead of failing. Local files plus API sync
3200
3417
  * (`fluid portal push`/`pull`) are a fully functional portal — Git is
3201
3418
  * an optional sync channel, not a prerequisite — so the user still
3202
- * lands in a working project and Git history can attach later.
3419
+ * lands in a working project and Git history can attach later via
3420
+ * `fluid portal push`, which adopts the remote's history.
3203
3421
  */
3204
3422
  function getBaseUrl() {
3205
3423
  return process.env["FLUID_API_BASE"] ?? "https://api.fluid.app";
@@ -3216,9 +3434,9 @@ async function removeIncompleteCheckout(targetDir) {
3216
3434
  }
3217
3435
  }
3218
3436
  function createClient$1(token) {
3219
- return createFetchClient({
3437
+ return createFluidOsClient({
3220
3438
  baseUrl: getBaseUrl(),
3221
- getAuthToken: () => token
3439
+ token
3222
3440
  });
3223
3441
  }
3224
3442
  /**
@@ -3228,9 +3446,12 @@ function createClient$1(token) {
3228
3446
  * `not_provisioned` genuinely may resolve later, so it says so.
3229
3447
  */
3230
3448
  function gitSyncUnavailableNotice(outcome) {
3231
- if (outcome.conflictType === "not_connected") return `This company's Git integration isn't connected, so the portal has no Git repository to clone. Creating the portal locally from the starter template instead — ${chalk.cyan("fluid portal push")}/${chalk.cyan("pull")} sync content without Git, and Git sync can attach once the integration is connected.`;
3232
- if (outcome.conflictType === "not_provisioned") return "The portal's Git repository isn't provisioned yet. Creating the portal locally from the starter template so you can start now — Git sync can attach once the repository is ready.";
3233
- return `Git sync isn't available (${outcome.reason}). Creating the portal locally from the starter template instead.`;
3449
+ const attachHint = `Creating the portal locally from the starter template so you can start now — ${chalk.cyan("fluid portal push")}/${chalk.cyan("pull")} sync content without Git, and Git history can attach later with ${chalk.cyan("fluid portal push")}.`;
3450
+ if (outcome.repoStatus === "disconnected") return `The portal's Git repository was disconnected, so there is nothing to clone. ${attachHint}`;
3451
+ if (outcome.repoStatus === "error") return `The portal's Git repository failed to provision. ${attachHint}`;
3452
+ if (outcome.conflictType === "not_connected") return "The portal's Git repository is still being provisioned — it didn't come online in time. " + attachHint;
3453
+ if (outcome.conflictType === "not_provisioned") return `The portal's Git repository isn't provisioned yet. ${attachHint}`;
3454
+ return `Git sync isn't available (${outcome.reason}). ${attachHint}`;
3234
3455
  }
3235
3456
  /**
3236
3457
  * A git-less fallback scaffold that failed or was interrupted leaves the
@@ -3274,7 +3495,7 @@ async function selectDefinition(client, appName, rawDefinitionId) {
3274
3495
  const definitions = [];
3275
3496
  let page = 1;
3276
3497
  while (true) {
3277
- const response = await fluid_os_v0_list_fluid_osdefinitions(client, {
3498
+ const response = await fluidOsApi.listDefinitions(client, {
3278
3499
  page,
3279
3500
  per_page: 500
3280
3501
  });
@@ -3414,6 +3635,115 @@ const cloneCommand = new Command("clone").description("Clone a portal definition
3414
3635
  }
3415
3636
  });
3416
3637
  //#endregion
3638
+ //#region src/commands/widget-package-publish.ts
3639
+ async function publishWidgetRuntimeArtifacts(options) {
3640
+ await assertStandalonePortalRemoteActionAllowed(options.projectDir, "portal widget package publish");
3641
+ const isDryRun = options.dryRun === true;
3642
+ const client = options.client;
3643
+ if (!isDryRun && !client) throw new Error("A FluidOS API client is required when dryRun is false.");
3644
+ const cliVersion = await getPortalCliVersion();
3645
+ if (isDryRun) return await publishWidgetPackage({
3646
+ projectDir: options.projectDir,
3647
+ outDir: options.outDir,
3648
+ owner: options.buildOwner,
3649
+ uploadOwner: options.uploadOwner,
3650
+ cliVersion,
3651
+ dryRun: true
3652
+ });
3653
+ if (!client) throw new Error("A FluidOS API client is required when dryRun is false.");
3654
+ return await publishWidgetPackage({
3655
+ projectDir: options.projectDir,
3656
+ outDir: options.outDir,
3657
+ owner: options.buildOwner,
3658
+ uploadOwner: options.uploadOwner,
3659
+ cliVersion,
3660
+ ...options.version ? { version: options.version } : {},
3661
+ client
3662
+ });
3663
+ }
3664
+ /**
3665
+ * Prints a publish failure with its `details` block, which is where the
3666
+ * validation tooling names the offending widget and field.
3667
+ */
3668
+ function printWidgetPublishError(error) {
3669
+ console.error(chalk.red("Error:") + " " + (error instanceof Error ? error.message : String(error)));
3670
+ if (typeof error === "object" && error !== null && "details" in error && typeof error.details === "string" && error.details.length > 0) console.error(error.details);
3671
+ }
3672
+ const DEFAULT_API_BASE = "https://api.fluid.app";
3673
+ /**
3674
+ * Builds the client used to publish widget artifacts.
3675
+ *
3676
+ * `FLUID_TOKEN` / `FLUID_API_TOKEN` win over any stored profile, matching the
3677
+ * resolution order the rest of the CLI documents and implements (see
3678
+ * `getAuthToken` in `@fluid-app/fluid-cli`). This path used to read only
3679
+ * `~/.fluid/config.json`, which made it the one command a caller could not
3680
+ * point at a chosen company: a host that spawns the CLI would publish to
3681
+ * whichever profile the terminal happened to have active, and shipping a
3682
+ * company's widgets to the wrong tenant is not a failure they could detect from
3683
+ * the output.
3684
+ *
3685
+ * With the env token set, the base URL comes from `FLUID_API_BASE` rather than
3686
+ * the stored profile's — a token issued elsewhere has no relationship to the
3687
+ * host that profile points at.
3688
+ */
3689
+ function createAuthenticatedWidgetPackageClient() {
3690
+ const envToken = process.env["FLUID_TOKEN"] ?? process.env["FLUID_API_TOKEN"] ?? "";
3691
+ if (envToken.length > 0) return createFluidOsHeyApiClient({
3692
+ baseUrl: process.env["FLUID_API_BASE"] ?? DEFAULT_API_BASE,
3693
+ getAuthToken: () => envToken
3694
+ });
3695
+ const { profile, profileName, token } = resolveAuthenticatedProfile();
3696
+ if (!token) {
3697
+ if (!profileName) throw new Error("Not logged in. Run " + chalk.cyan("fluid login") + " first.");
3698
+ throw new Error("No auth token found for profile " + chalk.cyan(profileName) + ". Run " + chalk.cyan("fluid login") + " to re-authenticate.");
3699
+ }
3700
+ return createFluidOsHeyApiClient({
3701
+ baseUrl: profile?.baseUrl ?? process.env["FLUID_API_BASE"] ?? DEFAULT_API_BASE,
3702
+ getAuthToken: () => token
3703
+ });
3704
+ }
3705
+ function resolveAuthenticatedProfile() {
3706
+ const config = readConfig();
3707
+ const projectConfig = findProjectConfig(process.cwd());
3708
+ if (projectConfig?.profile) {
3709
+ const profile = config.profiles[projectConfig.profile];
3710
+ if (profile) return {
3711
+ profile,
3712
+ profileName: projectConfig.profile,
3713
+ token: profile.token
3714
+ };
3715
+ }
3716
+ if (!config.activeProfile) return {};
3717
+ const profile = config.profiles[config.activeProfile];
3718
+ if (!profile) return {};
3719
+ return {
3720
+ profile,
3721
+ profileName: config.activeProfile,
3722
+ token: profile.token
3723
+ };
3724
+ }
3725
+ function printWidgetPublishSummary(options) {
3726
+ console.log();
3727
+ console.log(chalk.green.bold(`${options.title} complete`));
3728
+ console.log();
3729
+ console.log(chalk.gray("Owner: ") + chalk.white(options.ownerLabel));
3730
+ if (options.environment) console.log(chalk.gray("Environment: ") + chalk.white(options.environment));
3731
+ console.log(chalk.gray("Package: ") + chalk.white(options.packageId));
3732
+ console.log(chalk.gray("Version: ") + chalk.white(options.version));
3733
+ console.log(chalk.gray("Output: ") + chalk.cyan(options.outDir));
3734
+ console.log(chalk.gray("Artifacts: ") + chalk.white(String(options.artifactCount)));
3735
+ console.log();
3736
+ }
3737
+ function printRuntimeOnlyNotice() {
3738
+ console.log(chalk.yellow("This publishes widget runtime artifacts only; it does not deploy the hosted portal shell."));
3739
+ console.log();
3740
+ }
3741
+ function printDryRunSessionPayload(sessionPayload) {
3742
+ console.log(chalk.bold("Dry-run upload session payload:"));
3743
+ console.log(JSON.stringify(sessionPayload, null, 2));
3744
+ console.log();
3745
+ }
3746
+ //#endregion
3417
3747
  //#region src/utils/widget-helpers.ts
3418
3748
  /**
3419
3749
  * Pure helper functions for widget scaffolding.
@@ -3503,12 +3833,14 @@ function insertIntoWidgetPackage(source, exportName) {
3503
3833
  }
3504
3834
  //#endregion
3505
3835
  //#region src/commands/widget-create.ts
3506
- const WIDGET_CONFIG_PATH$1 = path.join("src", "widgets.config.ts");
3836
+ const WIDGET_CONFIG_PATH = path.join("src", "widgets.config.ts");
3507
3837
  const createSubcommand = new Command("create").description("Scaffold a new Remote DOM widget").argument("<name>", "Widget name in kebab-case (e.g., stock-ticker)").option("-c, --category <category>", "Widget category for palette grouping", "components").action(async (name, options) => {
3508
3838
  const cwd = process.cwd();
3509
3839
  const packageJsonPath = path.join(cwd, "package.json");
3510
- const configPath = path.join(cwd, WIDGET_CONFIG_PATH$1);
3840
+ const configPath = path.join(cwd, WIDGET_CONFIG_PATH);
3511
3841
  const widgetDir = path.join(cwd, "src", "widgets", name);
3842
+ let stylesAppended = false;
3843
+ let resolvedPackageKey = null;
3512
3844
  const validationError = validateWidgetName(name);
3513
3845
  if (validationError) {
3514
3846
  console.error(chalk.red(`Error: ${validationError}`));
@@ -3540,28 +3872,38 @@ const createSubcommand = new Command("create").description("Scaffold a new Remot
3540
3872
  displayName,
3541
3873
  category
3542
3874
  }), "utf-8");
3875
+ await appendWidgetStyles(cwd, name);
3876
+ stylesAppended = true;
3543
3877
  if (await fs.pathExists(configPath)) {
3544
3878
  const configSource = await fs.readFile(configPath, "utf-8");
3545
- if (isEmptyWidgetConfig(configSource)) await fs.writeFile(configPath, createWidgetConfigSource(importLine, exportName), "utf-8");
3546
- else {
3879
+ if (isEmptyWidgetConfig(configSource)) {
3880
+ resolvedPackageKey = await resolveCompanyPackageKey();
3881
+ await fs.writeFile(configPath, createWidgetConfigSource(importLine, exportName, resolvedPackageKey ?? void 0), "utf-8");
3882
+ } else {
3547
3883
  const withImport = insertImport(configSource, importLine);
3548
3884
  const updated = withImport ? insertIntoWidgetPackage(withImport, exportName) : null;
3549
- if (!updated) throw new Error(`${WIDGET_CONFIG_PATH$1} must export the starter widgetPackages placeholder or contain a defineWidgetPackage({ widgets: [...] }) declaration.`);
3885
+ if (!updated) throw new Error(`${WIDGET_CONFIG_PATH} must export the starter widgetPackages placeholder or contain a defineWidgetPackage({ widgets: [...] }) declaration.`);
3550
3886
  await fs.writeFile(configPath, updated, "utf-8");
3551
3887
  }
3552
- } else await fs.writeFile(configPath, createWidgetConfigSource(importLine, exportName), "utf-8");
3888
+ } else {
3889
+ resolvedPackageKey = await resolveCompanyPackageKey();
3890
+ await fs.writeFile(configPath, createWidgetConfigSource(importLine, exportName, resolvedPackageKey ?? void 0), "utf-8");
3891
+ }
3553
3892
  } catch (error) {
3554
3893
  await fs.remove(widgetDir).catch(() => {});
3894
+ if (stylesAppended) await removeWidgetStyles(cwd, name).catch(() => {});
3555
3895
  console.error(chalk.red("Error: Failed to scaffold Remote DOM widget."));
3556
3896
  console.error(error);
3557
3897
  process.exit(1);
3558
3898
  }
3559
3899
  console.log();
3560
3900
  console.log(chalk.green("Created widget:") + ` src/widgets/${name}/index.tsx`);
3561
- console.log(chalk.green("Registered") + ` in ${chalk.cyan(WIDGET_CONFIG_PATH$1)}`);
3901
+ console.log(chalk.green("Registered") + ` in ${chalk.cyan(WIDGET_CONFIG_PATH)}`);
3902
+ if (resolvedPackageKey) console.log(chalk.green("Package key:") + ` ${chalk.cyan(resolvedPackageKey)} (this company's existing widget package)`);
3562
3903
  console.log();
3563
3904
  console.log(chalk.yellow("Next steps:"));
3564
3905
  console.log(` 1. Edit ${chalk.cyan(`src/widgets/${name}/index.tsx`)}`);
3906
+ console.log(` Styles live in ${chalk.cyan("styles.css")} at the project root — widgets must not import CSS.`);
3565
3907
  console.log(` 2. Run ${chalk.cyan("fluid portal deploy --dry-run")} to verify the package`);
3566
3908
  });
3567
3909
  function createWidgetSource(options) {
@@ -3571,11 +3913,23 @@ interface ${options.widgetType}Props {
3571
3913
  readonly title?: string;
3572
3914
  }
3573
3915
 
3916
+ // Styling: a stylesheet is the ONLY way to style a widget. The host sanitizes
3917
+ // every Remote DOM mutation before it reaches the shadow root, so a
3918
+ // \`style={{ ... }}\` prop is silently dropped and \`<style>\` is a banned tag.
3919
+ // Give elements classNames and put the rules in the project-root styles.css
3920
+ // (this widget's rules were appended there when it was scaffolded).
3921
+ //
3922
+ // Do NOT add \`import "./styles.css"\` here. In dev, Vite answers a CSS import
3923
+ // with a browser style-injector that loads \`/@vite/client\`, which touches
3924
+ // HTMLElement — absent in a Web Worker — and throws while the package is being
3925
+ // evaluated, taking down every widget in the project, not just this one.
3574
3926
  function ${options.componentName}({ title = ${JSON.stringify(options.displayName)} }: ${options.widgetType}Props) {
3575
3927
  return (
3576
- <section>
3577
- <h3>{title}</h3>
3578
- <p>Edit this widget in src/widgets/${options.name}/index.tsx</p>
3928
+ <section className="${options.name}">
3929
+ <h3 className="${options.name}__title">{title}</h3>
3930
+ <p className="${options.name}__body">
3931
+ Edit this widget in src/widgets/${options.name}/index.tsx
3932
+ </p>
3579
3933
  </section>
3580
3934
  );
3581
3935
  }
@@ -3596,1743 +3950,283 @@ export const ${options.exportName} = defineWidget({
3596
3950
  });
3597
3951
  `;
3598
3952
  }
3953
+ /**
3954
+ * Appends a scaffolded widget's rules to the project-root `styles.css`,
3955
+ * creating the file when a project has none yet.
3956
+ *
3957
+ * The root stylesheet is the single sheet the tooling delivers — dev serves it
3958
+ * as the descriptor's `cssUrls`, and the build entry imports it — so every
3959
+ * widget in a project shares it. Appending (rather than overwriting) keeps
3960
+ * earlier widgets' rules intact; re-scaffolding a name that is already present
3961
+ * is a no-op rather than a duplicate block.
3962
+ */
3963
+ /**
3964
+ * Removes a widget's appended rules from the project-root `styles.css`.
3965
+ *
3966
+ * Only used to undo a failed scaffold — `appendWidgetStyles` runs before the
3967
+ * config update, so a config failure would otherwise strand rules for a widget
3968
+ * directory that has just been deleted. Matches the exact block the append
3969
+ * writes and leaves everything else, including a project's own rules and other
3970
+ * widgets', untouched.
3971
+ */
3972
+ async function removeWidgetStyles(projectDir, name) {
3973
+ const stylesPath = path.join(projectDir, "styles.css");
3974
+ if (!await fs.pathExists(stylesPath)) return;
3975
+ const existing = await fs.readFile(stylesPath, "utf-8");
3976
+ const block = createWidgetStylesSource(name);
3977
+ if (!existing.includes(block)) return;
3978
+ const withoutBlock = existing.replace(block, "");
3979
+ await fs.writeFile(stylesPath, withoutBlock.replace(/\n{3,}/gu, "\n\n"), "utf-8");
3980
+ }
3981
+ async function appendWidgetStyles(projectDir, name) {
3982
+ const stylesPath = path.join(projectDir, "styles.css");
3983
+ const block = createWidgetStylesSource(name);
3984
+ if (!await fs.pathExists(stylesPath)) {
3985
+ await fs.writeFile(stylesPath, `${ROOT_STYLES_HEADER}\n${block}`, "utf-8");
3986
+ return;
3987
+ }
3988
+ const existing = await fs.readFile(stylesPath, "utf-8");
3989
+ if (existing.includes(`.${name} {`)) return;
3990
+ const separator = existing.endsWith("\n") ? "\n" : "\n\n";
3991
+ await fs.appendFile(stylesPath, `${separator}${block}`, "utf-8");
3992
+ }
3993
+ const ROOT_STYLES_HEADER = `/* Runtime styles for this portal's widgets.
3994
+ *
3995
+ * One sheet per project: dev serves it to the widget shadow root as the
3996
+ * package descriptor's cssUrls, and the package build imports it. Widget
3997
+ * modules must NOT import CSS themselves — in dev that pulls \`/@vite/client\`
3998
+ * into the Web Worker, which throws and stops every widget in the project from
3999
+ * registering.
4000
+ */
4001
+ `;
4002
+ /**
4003
+ * Starter rules for a scaffolded widget.
4004
+ *
4005
+ * Colors, type, and radii come from the host portal's semantic CSS variables so
4006
+ * the widget matches whatever portal it lands on, in both light and dark,
4007
+ * without a second hand-written palette. Each `var()` carries a fallback so the
4008
+ * widget stays legible in a host that has not adopted the portal theme.
4009
+ *
4010
+ * Selectors are prefixed with the widget name so nothing leaks into the host.
4011
+ */
4012
+ function createWidgetStylesSource(name) {
4013
+ return `.${name} {
4014
+ padding: 24px;
4015
+ border: 1px solid var(--border, #e5e7eb);
4016
+ border-radius: var(--radius-lg, 12px);
4017
+ background: var(--card, #ffffff);
4018
+ color: var(--card-foreground, #111827);
4019
+ font-family: var(--font-body, ui-sans-serif, system-ui, sans-serif);
4020
+ }
4021
+
4022
+ .${name}__title {
4023
+ margin: 0 0 8px;
4024
+ color: var(--card-foreground, #111827);
4025
+ font-family: var(--font-header, inherit);
4026
+ font-size: 1.25rem;
4027
+ font-weight: 700;
4028
+ }
4029
+
4030
+ .${name}__body {
4031
+ margin: 0;
4032
+ color: var(--muted-foreground, #6b7280);
4033
+ }
4034
+ `;
4035
+ }
3599
4036
  function isEmptyWidgetConfig(source) {
3600
4037
  return /export\s+const\s+widgetPackages\s*=\s*\[\]\s+as\s+const\s*;/u.test(source);
3601
4038
  }
3602
- function createWidgetConfigSource(importLine, exportName) {
4039
+ /**
4040
+ * The key written when the company's real one cannot be resolved.
4041
+ *
4042
+ * A guess, and one that fails for any company that has already published — a
4043
+ * company has exactly ONE widget package and its key is fixed by whichever
4044
+ * project published first, so this value is only ever correct by accident.
4045
+ */
4046
+ const PLACEHOLDER_PACKAGE_KEY = "portal-widgets";
4047
+ function createWidgetConfigSource(importLine, exportName, packageKey = PLACEHOLDER_PACKAGE_KEY) {
3603
4048
  return `import { defineWidgetPackage } from "@fluid-app/portal-sdk/widgets/worker";
3604
4049
  ${importLine}
3605
4050
 
3606
4051
  export const widgetPackage = defineWidgetPackage({
3607
4052
  scope: "company",
3608
- packageStableId: "portal-widgets",
4053
+ packageStableId: ${JSON.stringify(packageKey)},
3609
4054
  version: "0.0.0-dev",
3610
4055
  widgets: [${exportName}],
3611
4056
  });
3612
4057
  `;
3613
4058
  }
3614
- const widgetCommand = new Command("widget").description("Manage Remote DOM portal widgets").addCommand(createSubcommand);
3615
- //#endregion
3616
- //#region ../widget-tooling/src/errors.ts
3617
- var WidgetToolingError = class extends Error {
3618
- code;
3619
- details;
3620
- diagnostics;
3621
- constructor(code, message, options = {}) {
3622
- super(message, options);
3623
- this.name = "WidgetToolingError";
3624
- this.code = code;
3625
- this.details = options.details;
3626
- this.diagnostics = options.diagnostics;
4059
+ /**
4060
+ * Reads the company's existing widget package key, for a config being CREATED.
4061
+ *
4062
+ * Scaffolding is the one moment this can be done safely. A widget's type is
4063
+ * `company.<key>.<Name>` and those strings are written into screen JSON, so
4064
+ * changing the key later orphans every screen using it — which is why
4065
+ * `fluid portal deploy` reports a mismatch rather than rewriting it. At
4066
+ * `widget create` no screen references the type yet, so resolving it here
4067
+ * prevents the mismatch instead of explaining it afterwards.
4068
+ *
4069
+ * Best-effort by design: this command made no network calls before, and a
4070
+ * scaffold must not fail because a lookup did. Every uncertainty — no token, no
4071
+ * network, a company that has never published, a slow answer — falls back to the
4072
+ * placeholder, which is exactly today's behaviour.
4073
+ *
4074
+ * The deadline is deliberate here, unlike the SDK's dev-server probe where a
4075
+ * timeout discarded a real answer. The fallback IS the correct result for an
4076
+ * offline scaffold, so bounding the wait costs nothing and stops a hung API
4077
+ * from wedging a local command.
4078
+ */
4079
+ async function resolveCompanyPackageKey() {
4080
+ try {
4081
+ const client = createAuthenticatedWidgetPackageClient();
4082
+ return (await Promise.race([readCompanyWidgetPackages(client), new Promise((resolve) => {
4083
+ setTimeout(() => resolve([]), PACKAGE_KEY_LOOKUP_TIMEOUT_MS);
4084
+ })]))[0]?.key ?? null;
4085
+ } catch {
4086
+ return null;
3627
4087
  }
3628
- };
3629
- const CONTENT_TYPES = {
3630
- ".css": "text/css",
3631
- ".js": "application/javascript",
3632
- ".json": "application/json",
3633
- ".map": "application/json"
3634
- };
3635
- const CSS_ARTIFACT_PATH_PATTERN = /^[A-Za-z0-9._~-]+\.css$/;
3636
- const JS_MAP_ARTIFACT_PATH_PATTERN = /^[A-Za-z0-9._~-]+\.js\.map$/;
3637
- async function computeArtifactMetadata(filePath, baseDir) {
3638
- const relativePath = toPosixPath$1(path.relative(baseDir, filePath));
3639
- assertAllowedArtifactPath(relativePath);
3640
- await assertRegularArtifactFile(filePath, relativePath);
3641
- const buffer = await fs.readFile(filePath);
3642
- return {
3643
- path: relativePath,
3644
- sha256: sha256(buffer),
3645
- bytes: buffer.byteLength,
3646
- contentType: getArtifactContentType(filePath)
3647
- };
3648
- }
3649
- async function collectWidgetPackageArtifacts(publishDir) {
3650
- const artifactPaths = await findArtifactPaths(publishDir);
3651
- const artifacts = await Promise.all(artifactPaths.map((artifactPath) => computeArtifactMetadata(artifactPath, publishDir)));
3652
- assertRequiredUploadArtifacts(artifacts);
3653
- return artifacts.sort((a, b) => a.path.localeCompare(b.path));
3654
- }
3655
- function createPublishManifestMetadata(options) {
3656
- return {
3657
- packageId: options.packageId,
3658
- version: options.version,
3659
- builderVersion: options.builderVersion ?? "0.1.0",
3660
- cliVersion: options.cliVersion,
3661
- runtimeVersion: options.runtimeVersion,
3662
- manifestHash: sha256(options.manifestContent),
3663
- artifacts: options.artifacts
3664
- };
3665
4088
  }
3666
- function getArtifactContentType(filePath) {
3667
- if (path.basename(filePath).endsWith(".js.map")) return "application/json";
3668
- return CONTENT_TYPES[path.extname(filePath)] ?? "application/octet-stream";
3669
- }
3670
- function sha256(content) {
3671
- return createHash("sha256").update(content).digest("hex");
3672
- }
3673
- function isWidgetPackageCssArtifactPath(relativePath) {
3674
- return CSS_ARTIFACT_PATH_PATTERN.test(relativePath);
3675
- }
3676
- function isWidgetPackageJsMapArtifactPath(relativePath) {
3677
- return JS_MAP_ARTIFACT_PATH_PATTERN.test(relativePath);
3678
- }
3679
- async function assertRegularArtifactFile(filePath, relativePath) {
3680
- const stat = await fs.lstat(filePath);
3681
- if (stat.isSymbolicLink()) throw new Error(`Widget publish artifact ${JSON.stringify(relativePath)} must be a regular file and must not be a symbolic link.`);
3682
- if (!stat.isFile()) throw new Error(`Widget publish artifact ${JSON.stringify(relativePath)} must be a regular file.`);
3683
- }
3684
- async function findArtifactPaths(dir, baseDir = dir) {
3685
- if (!await fs.pathExists(dir)) return [];
3686
- const entries = await fs.readdir(dir, { withFileTypes: true });
3687
- return (await Promise.all(entries.map(async (entry) => {
3688
- const entryPath = path.join(dir, entry.name);
3689
- const relativePath = toPosixPath$1(path.relative(baseDir, entryPath));
3690
- if (entry.isSymbolicLink()) throw new Error(`Widget publish artifact ${JSON.stringify(relativePath)} must be a regular file and must not be a symbolic link.`);
3691
- if (isUploadArtifact(relativePath)) {
3692
- if (!entry.isFile()) throw new Error(`Widget publish artifact ${JSON.stringify(relativePath)} must be a regular file.`);
3693
- return [entryPath];
4089
+ const PACKAGE_KEY_LOOKUP_TIMEOUT_MS = 5e3;
4090
+ const widgetCommand = new Command("widget").description("Manage Remote DOM portal widgets").addCommand(createSubcommand);
4091
+ //#endregion
4092
+ //#region src/commands/deploy.ts
4093
+ const DEFAULT_OUT_DIR$1 = ".fluid/widget-dist";
4094
+ const DEFAULT_ENVIRONMENT = "production";
4095
+ const deployCommand = new Command("deploy").description("Publish company-owned portal widget runtime artifacts").option("-e, --environment <name>", "Target environment label for output reporting", DEFAULT_ENVIRONMENT).option("-o, --out-dir <dir>", "Widget artifact output directory", DEFAULT_OUT_DIR$1).option("--dry-run", "Build and validate upload payload without publishing").action(async (options) => {
4096
+ const environment = options.environment ?? DEFAULT_ENVIRONMENT;
4097
+ const outDir = options.outDir ?? DEFAULT_OUT_DIR$1;
4098
+ const dryRun = options.dryRun === true;
4099
+ console.log();
4100
+ console.log(chalk.blue.bold("Fluid Portal Deploy"));
4101
+ console.log();
4102
+ printRuntimeOnlyNotice();
4103
+ console.log(chalk.gray("Environment: ") + chalk.white(environment));
4104
+ console.log(chalk.gray("Output: ") + chalk.cyan(outDir));
4105
+ if (dryRun) console.log(chalk.yellow("Dry run: no upload will be created."));
4106
+ console.log();
4107
+ const spinner = ora("Building company-owned widget package...").start();
4108
+ const result = await runPortalDeploy({
4109
+ projectDir: process.cwd(),
4110
+ outDir,
4111
+ environment,
4112
+ dryRun,
4113
+ onProgress: (label) => {
4114
+ spinner.text = label;
3694
4115
  }
3695
- if (entry.isDirectory()) {
3696
- const nestedPaths = await findArtifactPaths(entryPath, baseDir);
3697
- if (nestedPaths.length === 0) throwUnsupportedArtifactPath(`${relativePath}/`);
3698
- return nestedPaths;
4116
+ });
4117
+ if (!result.ok) {
4118
+ spinner.fail("Portal deploy failed");
4119
+ console.log();
4120
+ if (result.code === "PACKAGE_KEY_MISMATCH") console.log(chalk.red("Package key does not match this company."));
4121
+ else console.error(chalk.red("Error:") + " " + result.message);
4122
+ if (result.hint) {
4123
+ console.log();
4124
+ console.log(result.hint);
3699
4125
  }
3700
- if (isAllowedGeneratedFile(relativePath)) return [];
3701
- throwUnsupportedArtifactPath(relativePath);
3702
- }))).flat();
3703
- }
3704
- function assertRequiredUploadArtifacts(artifacts) {
3705
- const artifactPaths = new Set(artifacts.map((artifact) => artifact.path));
3706
- for (const requiredPath of ["widget.js", "manifest.json"]) {
3707
- if (artifactPaths.has(requiredPath)) continue;
3708
- throw new Error(`Widget package publish output must include regular file ${JSON.stringify(requiredPath)}.`);
4126
+ console.log();
4127
+ process.exit(1);
3709
4128
  }
3710
- }
3711
- function assertAllowedArtifactPath(relativePath) {
3712
- if (isUploadArtifact(relativePath) || isAllowedGeneratedFile(relativePath)) return;
3713
- throwUnsupportedArtifactPath(relativePath);
3714
- }
3715
- function isUploadArtifact(relativePath) {
3716
- return relativePath === "widget.js" || relativePath === "manifest.json" || isWidgetPackageCssArtifactPath(relativePath) || isWidgetPackageJsMapArtifactPath(relativePath);
3717
- }
3718
- function isAllowedGeneratedFile(relativePath) {
3719
- return isFlatArtifactPath(relativePath) && relativePath === "publish-manifest.json";
3720
- }
3721
- function isFlatArtifactPath(relativePath) {
3722
- return relativePath.length > 0 && !relativePath.includes("/") && !relativePath.includes("\\");
3723
- }
3724
- function throwUnsupportedArtifactPath(relativePath) {
3725
- throw new Error(`Unsupported widget publish artifact ${JSON.stringify(relativePath)}. Allowed files are widget.js, manifest.json, top-level [A-Za-z0-9._~-]+.css, top-level [A-Za-z0-9._~-]+.js.map, and publish-manifest.json.`);
3726
- }
3727
- function toPosixPath$1(value) {
3728
- return value.split(path.sep).join("/");
3729
- }
3730
- //#endregion
3731
- //#region ../../portal/core/src/remote-dom-widget-package.ts
3732
- const UNSAFE_JSON_KEYS = new Set([
3733
- "__proto__",
3734
- "constructor",
3735
- "prototype"
3736
- ]);
3737
- function isJsonValue(value) {
3738
- return isJsonValueAtDepth(value, /* @__PURE__ */ new WeakSet(), 0);
3739
- }
3740
- function isJsonValueAtDepth(value, ancestors, depth) {
3741
- if (value === null || typeof value === "string" || typeof value === "boolean") return true;
3742
- if (typeof value === "number") return Number.isFinite(value);
3743
- if (typeof value !== "object" || depth > 50 || ancestors.has(value)) return false;
3744
- ancestors.add(value);
3745
- const prototype = Object.getPrototypeOf(value);
3746
- const isJson = Array.isArray(value) ? value.every((item) => isJsonValueAtDepth(item, ancestors, depth + 1)) : (prototype === Object.prototype || prototype === null) && isRecord$5(value) && Object.entries(value).every(([key, item]) => !UNSAFE_JSON_KEYS.has(key) && isJsonValueAtDepth(item, ancestors, depth + 1));
3747
- ancestors.delete(value);
3748
- return isJson;
3749
- }
3750
- function isRecord$5(value) {
3751
- return typeof value === "object" && value !== null && !Array.isArray(value);
3752
- }
3753
- //#endregion
3754
- //#region ../widget-tooling/src/internal/widget-package-validation.ts
3755
- const URL_SAFE_SEGMENT_SOURCE = "[A-Za-z0-9][A-Za-z0-9_~-]*";
3756
- const URL_SAFE_WIDGET_NAME_PATTERN = new RegExp(`^${URL_SAFE_SEGMENT_SOURCE}$`);
3757
- const URL_SAFE_DOTTED_IDENTIFIER_PATTERN = new RegExp(`^${URL_SAFE_SEGMENT_SOURCE}(?:\\.${URL_SAFE_SEGMENT_SOURCE})*$`);
3758
- const SEMVER_CORE_PATTERN = "(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)";
3759
- const SEMVER_PRERELEASE_IDENTIFIER_PATTERN = "(?:0|[1-9]\\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)";
3760
- const SEMVER_URL_SAFE_PATTERN = new RegExp(`^${SEMVER_CORE_PATTERN}(?:-${SEMVER_PRERELEASE_IDENTIFIER_PATTERN}(?:\\.${SEMVER_PRERELEASE_IDENTIFIER_PATTERN})*)?$`);
3761
- const WIDGET_PACKAGE_MANIFEST_VERSION = 1;
3762
- const DEFAULT_WIDGET_ICON = "box";
3763
- const DEFAULT_WIDGET_CATEGORY = "components";
3764
- const DEFAULT_WIDGET_CONTAINER = "block";
3765
- const DEFAULT_MIN_SDK_VERSION = "0.0.0";
3766
- function validateSingleSourceWidgetPackage(sourcePackages, options = {}) {
3767
- if (sourcePackages.length === 0) return validationFailure({
3768
- code: "NO_SOURCE_PACKAGE",
3769
- message: "No defineWidgetPackage source package was found. Export widgetPackage, widgetPackages, or a default widget package from src/widgets.config.ts."
3770
- });
3771
- if (sourcePackages.length > 1) return validationFailure({
3772
- code: "MULTIPLE_SOURCE_PACKAGES",
3773
- message: "Publish/deploy supports exactly one defineWidgetPackage source package. Export one package or split packages into separate builds."
3774
- });
3775
- const sourcePackageValue = sourcePackages[0];
3776
- if (!sourcePackageValue) return validationFailure({
3777
- code: "NO_SOURCE_PACKAGE",
3778
- message: "No defineWidgetPackage source package was found."
3779
- });
3780
- const shapeErrors = validateSourcePackageShape(sourcePackageValue);
3781
- if (shapeErrors.length > 0) return {
3782
- success: false,
3783
- errors: shapeErrors
3784
- };
3785
- const sourcePackage = sourcePackageValue;
3786
- const errors = [];
3787
- const owner = options.owner ?? sourcePackage.packageType;
3788
- if (owner !== "company" && owner !== "droplet") errors.push({
3789
- code: "UNSUPPORTED_OWNER",
3790
- path: "packageType",
3791
- message: "Widget package publish/deploy currently supports company and droplet owners. Set packageType to \"company\" or \"droplet\"."
3792
- });
3793
- else if (options.owner !== void 0 && sourcePackage.packageType !== options.owner) errors.push({
3794
- code: "UNSUPPORTED_OWNER",
3795
- path: "packageType",
3796
- message: `Widget package metadata must set packageType to "${options.owner}" for this workflow.`
3797
- });
3798
- const resolvedOwner = owner === "droplet" ? "droplet" : "company";
3799
- if (resolvedOwner === "droplet" && !readNonEmptyTrimmedString(sourcePackage.packageStableId)) errors.push({
3800
- code: "INVALID_PACKAGE_KEY",
3801
- path: "packageStableId",
3802
- message: "Droplet widget packages must set packageStableId so published widget types remain stable across builds."
3803
- });
3804
- const packageKey = resolvePackageKey(sourcePackage, resolvedOwner);
3805
- const packageKeyPath = readNonEmptyTrimmedString(sourcePackage.packageStableId) ? "packageStableId" : "packageId";
3806
- if (!isUrlSafeDottedIdentifier(packageKey)) errors.push({
3807
- code: "INVALID_PACKAGE_KEY",
3808
- path: packageKeyPath,
3809
- message: "Widget package key must be URL-safe dot-separated text (letters, numbers, '.', '_', '~', and '-' only)."
3810
- });
3811
- if (!isUrlSafeSemver(sourcePackage.version)) errors.push({
3812
- code: "INVALID_VERSION",
3813
- path: "version",
3814
- message: "Widget package version must be a URL-safe SemVer version without build metadata (for example 1.2.3 or 1.2.3-beta.1)."
3815
- });
3816
- if (sourcePackage.widgets.length === 0) errors.push({
3817
- code: "NO_WIDGETS",
3818
- path: "widgets",
3819
- message: "Widget package must include at least one widget."
4129
+ spinner.succeed("Built widget runtime artifacts");
4130
+ if (dryRun) {
4131
+ console.log(chalk.yellow("Dry run complete — upload session was not requested."));
4132
+ console.log();
4133
+ if (result.sessionPayload !== void 0) printDryRunSessionPayload(result.sessionPayload);
4134
+ } else console.log(chalk.green("Published widget package version."));
4135
+ if (result.requestedVersion) {
4136
+ console.log(chalk.yellow(`Version ${result.requestedVersion} is already published for this company, so this went out as ${result.version}.`));
4137
+ console.log(chalk.gray("Every portal project in a company shares one widget package, so its versions are shared too."));
4138
+ console.log();
4139
+ }
4140
+ printWidgetPublishSummary({
4141
+ title: dryRun ? "Portal deploy dry run" : "Portal deploy",
4142
+ ownerLabel: "company",
4143
+ environment,
4144
+ outDir,
4145
+ packageId: result.packageId,
4146
+ version: result.version,
4147
+ artifactCount: result.artifactCount
3820
4148
  });
3821
- const packageId = `${resolvedOwner}.${packageKey}`;
3822
- const seenNames = /* @__PURE__ */ new Set();
3823
- const seenTypes = /* @__PURE__ */ new Set();
3824
- const widgets = [];
3825
- sourcePackage.widgets.forEach((widget, index) => {
3826
- const path = `widgets[${index}]`;
3827
- if (!isUrlSafeWidgetName(widget.name)) {
3828
- errors.push({
3829
- code: "INVALID_WIDGET_NAME",
3830
- path: `${path}.name`,
3831
- message: "Widget name must be URL-safe text (letters, numbers, '_', '~', and '-' only) and cannot be empty."
3832
- });
3833
- return;
3834
- }
3835
- if (seenNames.has(widget.name)) {
3836
- errors.push({
3837
- code: "DUPLICATE_WIDGET",
3838
- path: `${path}.name`,
3839
- message: `Duplicate widget name "${widget.name}" found in package ${packageKey}.`
3840
- });
3841
- return;
3842
- }
3843
- seenNames.add(widget.name);
3844
- const type = `${packageId}.${widget.name}`;
3845
- if (!isUrlSafeDottedIdentifier(type)) {
3846
- errors.push({
3847
- code: "INVALID_WIDGET_TYPE",
3848
- path: `${path}.type`,
3849
- message: `Generated widget type "${type}" is not URL-safe.`
3850
- });
3851
- return;
3852
- }
3853
- if (seenTypes.has(type)) {
3854
- errors.push({
3855
- code: "DUPLICATE_WIDGET",
3856
- path: `${path}.type`,
3857
- message: `Duplicate generated widget type "${type}" found.`
3858
- });
3859
- return;
3860
- }
3861
- seenTypes.add(type);
3862
- widgets.push(sourceWidgetToDescriptor(widget, type));
3863
- });
3864
- if (errors.length > 0) return {
3865
- success: false,
3866
- errors
3867
- };
3868
- return {
3869
- success: true,
3870
- value: {
3871
- sourcePackage,
3872
- packageKey,
3873
- packageId,
3874
- owner: resolvedOwner,
3875
- version: sourcePackage.version,
3876
- widgets
3877
- },
3878
- errors: []
3879
- };
3880
- }
3881
- function buildWidgetPackagePublishManifest(validated, options = {}) {
3882
- return {
3883
- manifestVersion: WIDGET_PACKAGE_MANIFEST_VERSION,
3884
- runtime: "remote-dom",
3885
- packageId: validated.packageId,
3886
- source: validated.owner,
3887
- version: validated.version,
3888
- workerEntryUrl: toWidgetPackageArtifactPath("widget.js"),
3889
- cssUrls: mergeCssUrls(validated.sourcePackage.cssUrls, options.cssUrls),
3890
- assetUrls: [],
3891
- widgets: validated.widgets.map(({ name: _name, ...widget }) => widget)
3892
- };
3893
- }
3894
- function sourceWidgetToDescriptor(widget, type) {
3895
- const displayName = widget.displayName ?? widget.name;
3896
- return {
3897
- type,
3898
- name: widget.name,
3899
- displayName,
3900
- description: widget.description ?? `Custom widget ${displayName}`,
3901
- icon: widget.icon ?? DEFAULT_WIDGET_ICON,
3902
- category: widget.category ?? DEFAULT_WIDGET_CATEGORY,
3903
- propertySchema: normalizePropertySchema(widget.propertySchema, type),
3904
- defaultProps: widget.defaultProps ?? {},
3905
- container: normalizeContainer(widget.container),
3906
- minSdkVersion: widget.minSdkVersion ?? DEFAULT_MIN_SDK_VERSION,
3907
- resizable: normalizeResizable(widget.resizable),
3908
- capabilities: normalizeCapabilities(widget.capabilities ?? [])
3909
- };
3910
- }
3911
- function normalizeCapabilities(capabilities) {
3912
- return capabilities.map((capability) => ({
3913
- name: capability.name,
3914
- version: capability.version ?? "1"
3915
- }));
3916
- }
3917
- function validationFailure(error) {
3918
- return {
3919
- success: false,
3920
- errors: [error]
3921
- };
3922
- }
3923
- function validateSourcePackageShape(value) {
3924
- if (!isRecord$4(value)) return [{
3925
- code: "INVALID_SOURCE_PACKAGE",
3926
- message: "Widget source package must be a JSON object."
3927
- }];
3928
- const errors = [];
3929
- requireStringField(value, "packageId", "packageId", errors);
3930
- requireStringField(value, "version", "version", errors);
3931
- validateOptionalStringField(value, "scope", "scope", errors);
3932
- validateOptionalStringField(value, "packageStableId", "packageStableId", errors);
3933
- validateOptionalStringField(value, "packageType", "packageType", errors);
3934
- if (Object.prototype.hasOwnProperty.call(value, "navigationOrigins")) errors.push({
3935
- code: "INVALID_SOURCE_PACKAGE",
3936
- path: "navigationOrigins",
3937
- message: "navigationOrigins has been removed. Declare the externalNavigation capability and call allowAnchorUrl(url) for each external link."
3938
- });
3939
- if (value.cssUrls !== void 0) if (!Array.isArray(value.cssUrls)) errors.push({
3940
- code: "INVALID_SOURCE_PACKAGE",
3941
- path: "cssUrls",
3942
- message: "Widget package cssUrls must be an array of strings when present."
3943
- });
3944
- else value.cssUrls.forEach((cssUrl, index) => {
3945
- validateCssUrl(cssUrl, `cssUrls[${index}]`, errors);
3946
- });
3947
- if (!Array.isArray(value.widgets)) {
3948
- errors.push({
3949
- code: "INVALID_SOURCE_PACKAGE",
3950
- path: "widgets",
3951
- message: "Widget package widgets must be an array."
3952
- });
3953
- return errors;
3954
- }
3955
- value.widgets.forEach((widget, index) => {
3956
- const widgetPath = `widgets[${index}]`;
3957
- if (!isRecord$4(widget)) {
3958
- errors.push({
3959
- code: "INVALID_WIDGET_METADATA",
3960
- path: widgetPath,
3961
- message: "Widget metadata must be a JSON object."
3962
- });
3963
- return;
3964
- }
3965
- requireStringField(widget, "name", `${widgetPath}.name`, errors, {
3966
- code: "INVALID_WIDGET_NAME",
3967
- message: "Widget name must be a string and cannot be empty."
3968
- });
3969
- validateOptionalStringField(widget, "displayName", `${widgetPath}.displayName`, errors, { requireNonEmpty: true });
3970
- validateOptionalStringField(widget, "description", `${widgetPath}.description`, errors, { requireNonEmpty: true });
3971
- validateOptionalStringField(widget, "icon", `${widgetPath}.icon`, errors, { requireNonEmpty: true });
3972
- validateOptionalStringField(widget, "category", `${widgetPath}.category`, errors, { requireNonEmpty: true });
3973
- validateOptionalStringField(widget, "minSdkVersion", `${widgetPath}.minSdkVersion`, errors, { requireNonEmpty: true });
3974
- validateOptionalJsonRecordField(widget, "propertySchema", `${widgetPath}.propertySchema`, errors);
3975
- validateOptionalJsonRecordField(widget, "defaultProps", `${widgetPath}.defaultProps`, errors);
3976
- validateOptionalCapabilities(widget, "capabilities", `${widgetPath}.capabilities`, errors);
3977
- });
3978
- return errors;
3979
- }
3980
- function requireStringField(record, key, path, errors, override) {
3981
- const value = record[key];
3982
- if (typeof value === "string" && value.trim().length > 0) return;
3983
- errors.push({
3984
- code: override?.code ?? "INVALID_SOURCE_PACKAGE",
3985
- path,
3986
- message: override?.message ?? `${path} must be a string and cannot be empty.`
3987
- });
3988
- }
3989
- function validateOptionalStringField(record, key, path, errors, options = {}) {
3990
- const value = record[key];
3991
- if (value === void 0) return;
3992
- if (typeof value === "string") {
3993
- if (!options.requireNonEmpty || value.trim().length > 0) return;
3994
- errors.push({
3995
- code: "INVALID_SOURCE_PACKAGE",
3996
- path,
3997
- message: `${path} must be a non-empty string when present.`
3998
- });
3999
- return;
4000
- }
4001
- errors.push({
4002
- code: "INVALID_SOURCE_PACKAGE",
4003
- path,
4004
- message: `${path} must be a string when present.`
4005
- });
4006
- }
4007
- function validateCssUrl(value, cssUrlPath, errors) {
4008
- if (typeof value !== "string" || value.trim().length === 0) {
4009
- errors.push({
4010
- code: "INVALID_SOURCE_PACKAGE",
4011
- path: cssUrlPath,
4012
- message: "Widget package cssUrls entries must be non-empty strings."
4013
- });
4014
- return;
4015
- }
4016
- if (!isWidgetPackageCssArtifactPath(value)) errors.push({
4017
- code: "INVALID_SOURCE_PACKAGE",
4018
- path: cssUrlPath,
4019
- message: "Widget package cssUrls entries must be top-level generated CSS artifact filenames matching [A-Za-z0-9._~-]+.css. Self-host external CSS or fonts via @font-face in widget CSS so the build can isolate them."
4020
- });
4021
- }
4022
- function validateOptionalJsonRecordField(record, key, path, errors) {
4023
- const value = record[key];
4024
- if (value === void 0) return;
4025
- if (isRecord$4(value) && isJsonValue(value)) return;
4026
- errors.push({
4027
- code: "INVALID_WIDGET_METADATA",
4028
- path,
4029
- message: `${path} must be a JSON object when present.`
4030
- });
4031
- }
4032
- function validateOptionalCapabilities(record, key, path, errors) {
4033
- const value = record[key];
4034
- if (value === void 0) return;
4035
- if (!Array.isArray(value)) {
4036
- errors.push({
4037
- code: "INVALID_WIDGET_METADATA",
4038
- path,
4039
- message: `${path} must be an array when present.`
4040
- });
4041
- return;
4042
- }
4043
- const names = /* @__PURE__ */ new Set();
4044
- value.forEach((candidate, index) => {
4045
- const capabilityPath = `${path}[${index}]`;
4046
- if (!isRecord$4(candidate)) {
4047
- errors.push({
4048
- code: "INVALID_WIDGET_METADATA",
4049
- path: capabilityPath,
4050
- message: `${capabilityPath} must be a JSON object.`
4051
- });
4052
- return;
4053
- }
4054
- const name = candidate.name;
4055
- if (!isCanonicalMetadataString(name)) {
4056
- errors.push({
4057
- code: "INVALID_WIDGET_METADATA",
4058
- path: `${capabilityPath}.name`,
4059
- message: `${capabilityPath}.name must be a non-empty string without surrounding whitespace or control characters.`
4060
- });
4061
- return;
4062
- }
4063
- const version = candidate.version;
4064
- if (version !== void 0 && !isCanonicalMetadataString(version)) errors.push({
4065
- code: "INVALID_WIDGET_METADATA",
4066
- path: `${capabilityPath}.version`,
4067
- message: `${capabilityPath}.version must be a non-empty string without surrounding whitespace or control characters when present.`
4068
- });
4069
- if (names.has(name)) {
4070
- errors.push({
4071
- code: "INVALID_WIDGET_METADATA",
4072
- path: `${capabilityPath}.name`,
4073
- message: `${path} must not contain duplicate capability name ${JSON.stringify(name)}.`
4074
- });
4075
- return;
4076
- }
4077
- names.add(name);
4078
- });
4079
- }
4080
- function isCanonicalMetadataString(value) {
4081
- return typeof value === "string" && value.length > 0 && value === value.trim() && !/\p{Cc}/u.test(value);
4082
- }
4083
- function mergeCssUrls(sourceCssUrls, generatedCssUrls) {
4084
- const generatedCssUrlSet = new Set(generatedCssUrls ?? []);
4085
- generatedCssUrls?.forEach((cssUrl, index) => {
4086
- validateGeneratedCssArtifactUrl(cssUrl, `generated cssUrls[${index}]`);
4087
- });
4088
- sourceCssUrls?.forEach((cssUrl, index) => {
4089
- validateGeneratedCssArtifactUrl(cssUrl, `cssUrls[${index}]`);
4090
- if (generatedCssUrlSet.has(cssUrl)) return;
4091
- throw new Error(`cssUrls[${index}] URL ${JSON.stringify(cssUrl)} must match a generated top-level CSS artifact. Import the CSS from the widget package bundle or remove it from cssUrls.`);
4092
- });
4093
- return Array.from(new Set([...sourceCssUrls ?? [], ...generatedCssUrls ?? []])).map(toWidgetPackageArtifactPath);
4094
- }
4095
- function toWidgetPackageArtifactPath(value) {
4096
- return value;
4097
- }
4098
- function validateGeneratedCssArtifactUrl(value, cssUrlPath) {
4099
- if (value.trim().length === 0) throw new Error(`${cssUrlPath} must be a non-empty string.`);
4100
- if (!isWidgetPackageCssArtifactPath(value)) throw new Error(`${cssUrlPath} must be a top-level generated CSS artifact filename matching [A-Za-z0-9._~-]+.css. Self-host external CSS or fonts via @font-face in widget CSS so the build can isolate them.`);
4101
- }
4102
- function normalizePropertySchema(value, widgetType) {
4103
- return {
4104
- ...value ?? {},
4105
- widgetType
4106
- };
4107
- }
4108
- function normalizeContainer(value) {
4109
- if (value === "inline" || value === "block" || value === "card" || value === "fullscreen") return value;
4110
- return DEFAULT_WIDGET_CONTAINER;
4111
- }
4112
- function normalizeResizable(value) {
4113
- if (typeof value === "boolean") return value;
4114
- if (value === "horizontal" || value === "vertical" || value === "both") return value;
4115
- if (!isRecord$4(value)) return false;
4116
- const horizontal = value.horizontal === true;
4117
- const vertical = value.vertical === true;
4118
- if (horizontal && vertical) return "both";
4119
- if (horizontal) return "horizontal";
4120
- if (vertical) return "vertical";
4121
- return false;
4122
- }
4123
- function resolvePackageKey(sourcePackage, owner) {
4124
- const packageStableId = readNonEmptyTrimmedString(sourcePackage.packageStableId);
4125
- if (packageStableId) return prefixPackageStableIdWithNonOwnerScope(packageStableId, sourcePackage.scope);
4126
- return stripOwnerScopeFromPackageId(sourcePackage.packageId, sourcePackage.scope, owner);
4127
- }
4128
- function prefixPackageStableIdWithNonOwnerScope(packageStableId, sourceScope) {
4129
- const sourceScopeText = readNonEmptyTrimmedString(sourceScope);
4130
- if (!sourceScopeText || isWidgetPackageOwnerKind(sourceScopeText)) return packageStableId;
4131
- const sourceScopePrefix = `${sourceScopeText}.`;
4132
- if (packageStableId.startsWith(sourceScopePrefix)) return packageStableId;
4133
- return `${sourceScopeText}.${packageStableId}`;
4134
- }
4135
- function stripOwnerScopeFromPackageId(packageId, sourceScope, owner) {
4136
- const ownerPrefix = `${owner}.`;
4137
- if (packageId.startsWith(ownerPrefix) && packageId.length > ownerPrefix.length) return packageId.slice(ownerPrefix.length);
4138
- const sourceScopeText = readNonEmptyTrimmedString(sourceScope);
4139
- if (isWidgetPackageOwnerKind(sourceScopeText) && packageId.startsWith(`${sourceScopeText}.`) && packageId.length > sourceScopeText.length + 1) return packageId.slice(sourceScopeText.length + 1);
4140
- return packageId;
4141
- }
4142
- function readNonEmptyTrimmedString(value) {
4143
- const trimmed = value?.trim();
4144
- return trimmed ? trimmed : void 0;
4145
- }
4146
- function isWidgetPackageOwnerKind(value) {
4147
- return value === "company" || value === "droplet";
4148
- }
4149
- function isUrlSafeDottedIdentifier(value) {
4150
- return URL_SAFE_DOTTED_IDENTIFIER_PATTERN.test(value);
4151
- }
4152
- function isUrlSafeWidgetName(value) {
4153
- return URL_SAFE_WIDGET_NAME_PATTERN.test(value);
4154
- }
4155
- function isUrlSafeSemver(value) {
4156
- return SEMVER_URL_SAFE_PATTERN.test(value);
4157
- }
4158
- function isRecord$4(value) {
4159
- return typeof value === "object" && value !== null && !Array.isArray(value);
4160
- }
4161
- //#endregion
4162
- //#region ../widget-tooling/src/internal/widget-package-config.ts
4163
- const WIDGET_CONFIG_PATH = "src/widgets.config.ts";
4164
- const SOURCE_PACKAGE_EXTRACT_FILENAME = "extract-widget-packages.ts";
4165
- const SOURCE_PACKAGE_OUTPUT_FILENAME = "source-widget-packages.json";
4166
- const SOURCE_PACKAGE_OUTPUT_SENTINEL = "fluid-widget-source-packages:v1";
4167
- const TSX_CLI_PATH = createRequire(import.meta.url).resolve("tsx/cli");
4168
- async function resolvePortalWidgetSourceConfig(projectDir) {
4169
- const candidate = path.join(projectDir, WIDGET_CONFIG_PATH);
4170
- return await fs.pathExists(candidate) ? {
4171
- path: candidate,
4172
- relativePath: WIDGET_CONFIG_PATH
4173
- } : void 0;
4174
- }
4175
- async function loadSourceWidgetPackages(projectDir) {
4176
- const config = await resolvePortalWidgetSourceConfig(projectDir);
4177
- if (!config) return success([]);
4178
- let tempDir;
4179
- try {
4180
- tempDir = await createTempDirectory(projectDir);
4181
- const extractFile = path.join(tempDir, SOURCE_PACKAGE_EXTRACT_FILENAME);
4182
- const outputFile = path.join(tempDir, SOURCE_PACKAGE_OUTPUT_FILENAME);
4183
- await fs.writeFile(extractFile, createSourcePackageExtractorScript({
4184
- projectDir,
4185
- configPath: config.path
4186
- }), {
4187
- encoding: "utf-8",
4188
- flag: "wx"
4189
- });
4190
- await execa(process.execPath, [
4191
- TSX_CLI_PATH,
4192
- extractFile,
4193
- outputFile
4194
- ], {
4195
- cwd: projectDir,
4196
- stdio: "pipe",
4197
- env: {
4198
- ...process.env,
4199
- NODE_ENV: "production"
4200
- }
4201
- });
4202
- const outputResult = await readSourcePackageExtractorOutput(outputFile);
4203
- if (!outputResult.success) return outputResult;
4204
- const parsed = outputResult.value;
4205
- if (parsed.length === 0) return success([]);
4206
- return success(parsed);
4207
- } catch (err) {
4208
- const error = err;
4209
- return failure({
4210
- code: "CONFIG_LOAD_FAILED",
4211
- message: `Failed to load widget packages from ${config.relativePath}`,
4212
- details: error.stderr ?? error.message ?? String(err)
4213
- });
4214
- } finally {
4215
- if (tempDir) await fs.remove(tempDir).catch(() => {});
4216
- }
4217
- }
4218
- async function createTempDirectory(projectDir) {
4219
- const projectTmpDir = path.join(projectDir, ".fluid", "tmp");
4220
- try {
4221
- await fs.ensureDir(projectTmpDir);
4222
- return await fs.mkdtemp(path.join(projectTmpDir, "widget-packages-"));
4223
- } catch (err) {
4224
- const message = err instanceof Error ? err.message : String(err);
4225
- throw new Error(`Unable to create project-local temporary directory at ${projectTmpDir}: ${message}`);
4226
- }
4227
- }
4228
- async function readSourcePackageExtractorOutput(outputFile) {
4229
- let output;
4230
- try {
4231
- output = await fs.readFile(outputFile, "utf-8");
4232
- } catch (err) {
4233
- return failure({
4234
- code: "CONFIG_LOAD_FAILED",
4235
- message: "Widget package extractor did not write an output file",
4236
- details: err instanceof Error ? err.message : String(err)
4237
- });
4238
- }
4239
- let parsed;
4240
- try {
4241
- parsed = JSON.parse(output);
4242
- } catch {
4243
- return failure({
4244
- code: "INVALID_FORMAT",
4245
- message: "Failed to parse widget package output file as JSON",
4246
- details: `Output was: ${output.slice(0, 200)}`
4247
- });
4248
- }
4249
- if (!isRecord$3(parsed) || parsed.sentinel !== SOURCE_PACKAGE_OUTPUT_SENTINEL) return failure({
4250
- code: "INVALID_FORMAT",
4251
- message: "Widget package extractor output file had an invalid sentinel"
4252
- });
4253
- if (!Array.isArray(parsed.data)) return failure({
4254
- code: "INVALID_FORMAT",
4255
- message: "Extracted widget package output is not an array",
4256
- details: `Expected an array, got: ${typeof parsed.data}`
4257
- });
4258
- return success(parsed.data);
4259
- }
4260
- function createSourcePackageExtractorScript(options) {
4261
- return `
4262
- import fs from "node:fs/promises";
4263
- import path from "node:path";
4264
- import { createServer, normalizePath } from "vite";
4265
-
4266
- const SOURCE_PACKAGE_MARKER = "__fluidSourceWidgetPackage";
4267
- const OUTPUT_SENTINEL = ${JSON.stringify(SOURCE_PACKAGE_OUTPUT_SENTINEL)};
4268
- const projectRoot = ${JSON.stringify(options.projectDir)};
4269
- const widgetConfigPath = ${JSON.stringify(options.configPath)};
4270
- const outputPath = process.argv[2];
4271
- if (!outputPath) throw new Error("Missing widget package extractor output path.");
4272
-
4273
- function toViteModuleId(modulePath) {
4274
- const relativePath = path.relative(projectRoot, modulePath);
4275
- if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
4276
- return "/" + normalizePath(relativePath);
4277
- }
4278
- return normalizePath(modulePath);
4279
- }
4280
-
4281
- const server = await createServer({
4282
- root: projectRoot,
4283
- mode: "production",
4284
- server: { middlewareMode: true },
4285
- appType: "custom",
4286
- logLevel: "error",
4287
- clearScreen: false,
4288
- resolve: {
4289
- conditions: ["module", "browser", "development|production"],
4290
- },
4291
- ssr: {
4292
- noExternal: ["@fluid-app/portal-sdk"],
4293
- resolve: {
4294
- conditions: ["module", "node", "development|production"],
4295
- },
4296
- },
4297
4149
  });
4298
-
4299
- try {
4300
- const widgetConfig = await server.ssrLoadModule(toViteModuleId(widgetConfigPath));
4301
-
4302
- function isSourceWidgetPackage(value) {
4303
- return Boolean(value && typeof value === "object" && value[SOURCE_PACKAGE_MARKER] === true);
4304
- }
4305
-
4306
- function collectSourceWidgetPackages(mod) {
4307
- const candidates = [
4308
- mod.widgetPackage,
4309
- ...(Array.isArray(mod.widgetPackages) ? mod.widgetPackages : []),
4310
- mod.default,
4311
- ];
4312
- const byPackageId = new Map();
4313
- for (const candidate of candidates) {
4314
- if (!isSourceWidgetPackage(candidate)) continue;
4315
- if (!byPackageId.has(candidate.packageId)) byPackageId.set(candidate.packageId, candidate);
4316
- }
4317
- return Array.from(byPackageId.values());
4318
- }
4319
-
4320
- function serializeWidget(widget) {
4321
- if (!widget || typeof widget !== "object" || Array.isArray(widget)) {
4322
- return widget;
4323
- }
4324
- const { component, ...metadata } = widget;
4325
- return metadata;
4326
- }
4327
-
4328
- function serializePackage(sourcePackage) {
4329
- return {
4330
- manifestVersion: sourcePackage.manifestVersion,
4331
- scope: sourcePackage.scope,
4332
- packageStableId: sourcePackage.packageStableId,
4333
- packageId: sourcePackage.packageId,
4334
- packageType: sourcePackage.packageType,
4335
- version: sourcePackage.version,
4336
- cssUrls: sourcePackage.cssUrls,
4337
- ...(Object.prototype.hasOwnProperty.call(sourcePackage, "navigationOrigins")
4338
- ? { navigationOrigins: sourcePackage.navigationOrigins }
4339
- : {}),
4340
- widgets: Array.isArray(sourcePackage.widgets)
4341
- ? sourcePackage.widgets.map(serializeWidget)
4342
- : sourcePackage.widgets,
4343
- };
4344
- }
4345
-
4346
- await fs.writeFile(
4347
- outputPath,
4348
- JSON.stringify({
4349
- sentinel: OUTPUT_SENTINEL,
4350
- data: collectSourceWidgetPackages(widgetConfig).map(serializePackage),
4351
- }),
4352
- "utf-8",
4353
- );
4354
- } finally {
4355
- await server.close();
4356
- }
4357
- `;
4358
- }
4359
- function isRecord$3(value) {
4360
- return typeof value === "object" && value !== null && !Array.isArray(value);
4361
- }
4362
- //#endregion
4363
- //#region ../widget-tooling/src/internal/widget-package-builder.ts
4364
- const DEFAULT_PUBLISH_DIR = ".fluid/widget-dist";
4365
- const DEFAULT_RUNTIME_VERSION = "1";
4366
- const TEMP_BUILD_DIR = ".fluid/widget-build";
4367
- const TEMP_CLI_DIR = ".fluid/tmp";
4368
- const PROJECT_VITE_CONFIG_CANDIDATES = [
4369
- "vite.config.ts",
4370
- "vite.config.mts",
4371
- "vite.config.js",
4372
- "vite.config.mjs"
4373
- ];
4374
- const BLOCKED_FLUID_PROJECT_PLUGIN_NAMES = ["fluid-portal-dev", "fluid-backend-dev"];
4375
- const require = createRequire(import.meta.url);
4376
- function validateSharedWidgetPackagePublishDir(projectDir, publishDir = DEFAULT_PUBLISH_DIR, options = {}) {
4377
- const label = options.optionName ?? "publishDir";
4378
- if (path.isAbsolute(publishDir)) throw new Error(`${label} must be relative to the project directory.`);
4379
- const normalizedPublishDir = normalizePublishDirForValidation(publishDir);
4380
- if (normalizedPublishDir === ".fluid" || !normalizedPublishDir.startsWith(".fluid/")) throw new Error(`${label} must be a relative path under .fluid/.`);
4381
- if (isPathWithinReservedDir(normalizedPublishDir, TEMP_BUILD_DIR)) throw new Error(`${label} must not be ${TEMP_BUILD_DIR} because it is reserved for temporary builder output.`);
4382
- if (isPathWithinReservedDir(normalizedPublishDir, TEMP_CLI_DIR)) throw new Error(`${label} must not be ${TEMP_CLI_DIR} because it is reserved for Fluid CLI temporary files.`);
4383
- const resolvedProjectDir = path.resolve(projectDir);
4384
- const resolvedPublishDir = path.resolve(resolvedProjectDir, publishDir);
4385
- const relative = path.relative(resolvedProjectDir, resolvedPublishDir);
4386
- if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`${label} must stay inside the project directory.`);
4387
- }
4388
- async function buildSharedWidgetPackage(options) {
4389
- const publishDirInput = options.publishDir ?? DEFAULT_PUBLISH_DIR;
4390
- try {
4391
- await validateWidgetPackageOutputPaths(options.projectDir, publishDirInput);
4392
- } catch (err) {
4393
- return failure({
4394
- code: "INVALID_PUBLISH_DIR",
4395
- message: "Unsafe widget package publish directory",
4396
- details: (err instanceof Error ? err : new Error(String(err))).message
4397
- });
4398
- }
4399
- const sourcePackageResult = await loadSourceWidgetPackages(options.projectDir);
4400
- if (!sourcePackageResult.success) return failure({
4401
- code: "CONFIG_LOAD_FAILED",
4402
- message: sourcePackageResult.error.message,
4403
- details: sourcePackageResult.error.details
4404
- });
4405
- const validation = validateSingleSourceWidgetPackage(sourcePackageResult.value, { owner: options.owner });
4406
- if (!validation.success || !validation.value) return failure({
4407
- code: "VALIDATION_FAILED",
4408
- message: validation.errors.map((error) => error.message).join("\n")
4409
- });
4410
- try {
4411
- await validateWidgetPackageOutputPaths(options.projectDir, publishDirInput);
4412
- const publishDir = path.resolve(options.projectDir, publishDirInput);
4413
- const widgetScriptPath = path.join(publishDir, "widget.js");
4414
- await fs.emptyDir(publishDir);
4415
- const tempDir = path.resolve(options.projectDir, TEMP_BUILD_DIR);
4416
- await fs.emptyDir(tempDir);
4417
- let buildFailure;
4418
- try {
4419
- await buildWidgetScriptBundle({
4420
- projectDir: options.projectDir,
4421
- tempDir,
4422
- publishDir,
4423
- validated: validation.value
4424
- });
4425
- } catch (error) {
4426
- buildFailure = { error };
4427
- }
4428
- try {
4429
- await fs.remove(tempDir);
4430
- } catch {
4431
- const cleanupFailure = /* @__PURE__ */ new Error("Failed to remove reserved temporary build directory.");
4432
- if (buildFailure) {
4433
- const primaryError = buildFailure.error instanceof Error ? buildFailure.error : new Error(String(buildFailure.error));
4434
- throw new AggregateError([primaryError, cleanupFailure], `${primaryError.message}\n${cleanupFailure.message}`);
4435
- }
4436
- throw cleanupFailure;
4437
- }
4438
- if (buildFailure) throw buildFailure.error;
4439
- const cssUrls = await collectCssUrls(publishDir);
4440
- const descriptor = buildWidgetPackagePublishManifest(validation.value, { cssUrls });
4441
- const manifestContent = stableStringify(descriptor);
4442
- const publishPackageId = descriptor.packageId;
4443
- const manifestPath = path.join(publishDir, "manifest.json");
4444
- const publishManifestPath = path.join(publishDir, "publish-manifest.json");
4445
- await fs.writeFile(manifestPath, manifestContent, "utf-8");
4446
- const artifacts = await collectWidgetPackageArtifacts(publishDir);
4447
- const publishManifest = createPublishManifestMetadata({
4448
- packageId: publishPackageId,
4449
- version: validation.value.version,
4450
- cliVersion: options.cliVersion,
4451
- runtimeVersion: DEFAULT_RUNTIME_VERSION,
4452
- manifestContent,
4453
- artifacts
4454
- });
4455
- await fs.writeFile(publishManifestPath, stableStringify(publishManifest), "utf-8");
4456
- return success({
4457
- publishDir,
4458
- packageId: validation.value.packageId,
4459
- version: validation.value.version,
4460
- manifestPath,
4461
- publishManifestPath,
4462
- widgetScriptPath
4463
- });
4464
- } catch (err) {
4465
- const error = err instanceof Error ? err : new Error(String(err));
4466
- const isPublishDirValidationError = /^(publishDir|reserved temporary build directory) /.test(error.message);
4467
- return failure({
4468
- code: isPublishDirValidationError ? "INVALID_PUBLISH_DIR" : "WRITE_FAILED",
4469
- message: isPublishDirValidationError ? "Unsafe widget package publish directory" : "Failed to build shared widget package artifacts",
4470
- details: error.message
4471
- });
4472
- }
4473
- }
4474
- function createWidgetPackageEntrySource(options) {
4475
- return `import { createElement } from "react";
4476
- import { startWidgetPackage } from "@fluid-app/portal-sdk/widgets/worker";
4477
- ${options.cssImportPath ? `import ${JSON.stringify(options.cssImportPath)};` : ""}
4478
-
4479
- const widgetConfig = await import(${JSON.stringify(options.configImportPath)});
4480
- const SOURCE_PACKAGE_MARKER = "__fluidSourceWidgetPackage";
4481
- const descriptors = ${JSON.stringify(options.validated.widgets, null, 2)};
4482
-
4483
- function isSourceWidgetPackage(value) {
4484
- return Boolean(value && typeof value === "object" && value[SOURCE_PACKAGE_MARKER] === true);
4485
- }
4486
-
4487
- function collectSourceWidgetPackages(mod) {
4488
- return [
4489
- mod.widgetPackage,
4490
- ...(Array.isArray(mod.widgetPackages) ? mod.widgetPackages : []),
4491
- mod.default,
4492
- ].filter(isSourceWidgetPackage);
4493
- }
4494
-
4495
- const sourcePackage = collectSourceWidgetPackages(widgetConfig)[0];
4496
- if (!sourcePackage) {
4497
- throw new Error("No Fluid source widget package found while loading widget package worker bundle.");
4498
- }
4499
-
4500
- const componentsByName = new Map(
4501
- sourcePackage.widgets.map((widget) => [widget.name, widget.component]),
4502
- );
4503
-
4504
- const widgets = descriptors.map((descriptor) => {
4505
- const component = componentsByName.get(descriptor.name);
4506
- if (typeof component !== "function") {
4507
- throw new Error("Widget package is missing Remote DOM component for " + descriptor.name + ".");
4508
- }
4509
- return {
4510
- ...descriptor,
4511
- component: (props) => createElement(component, props),
4512
- };
4513
- });
4514
-
4515
- startWidgetPackage({ widgets });
4516
- `;
4517
- }
4518
- async function buildWidgetScriptBundle(options) {
4519
- const sourceConfig = await resolvePortalWidgetSourceConfig(options.projectDir);
4520
- if (!sourceConfig) throw new Error("No portal widget source config found.");
4521
- const entryPath = path.join(options.tempDir, "widget-entry.ts");
4522
- const viteConfigPath = path.join(options.tempDir, "vite.config.mjs");
4523
- const rootStylesPath = path.join(options.projectDir, "styles.css");
4524
- await fs.writeFile(entryPath, createWidgetPackageEntrySource({
4525
- configImportPath: pathToFileURL(sourceConfig.path).href,
4526
- cssImportPath: await fs.pathExists(rootStylesPath) ? pathToFileURL(rootStylesPath).href : void 0,
4527
- validated: options.validated
4528
- }), "utf-8");
4529
- await fs.writeFile(viteConfigPath, createViteConfigSource({
4530
- entryPath,
4531
- publishDir: options.publishDir,
4532
- projectConfigPath: await resolveProjectViteConfigPath(options.projectDir)
4533
- }), "utf-8");
4534
- const viteCliPath = resolveViteCliPath(options.projectDir);
4535
- await execa(process.execPath, [
4536
- viteCliPath,
4537
- "build",
4538
- "--config",
4539
- viteConfigPath
4540
- ], {
4541
- cwd: options.projectDir,
4542
- stdio: "pipe",
4543
- env: {
4544
- ...process.env,
4545
- NODE_ENV: "production"
4546
- }
4547
- });
4548
- }
4549
- function resolveViteCliPath(projectDir) {
4550
- const projectViteCliPath = resolveViteCliPathFromRequire(createRequire(path.join(projectDir, "package.json")));
4551
- if (projectViteCliPath) return projectViteCliPath;
4552
- const cliViteCliPath = resolveViteCliPathFromRequire(require);
4553
- if (cliViteCliPath) return cliViteCliPath;
4554
- throw new Error("Unable to resolve Vite for the portal project. Install the project dependencies and ensure vite is available before building widget packages.");
4555
- }
4556
- async function resolveProjectViteConfigPath(projectDir) {
4557
- for (const fileName of PROJECT_VITE_CONFIG_CANDIDATES) {
4558
- const candidate = path.join(projectDir, fileName);
4559
- if (await fs.pathExists(candidate)) return candidate;
4560
- }
4561
- }
4562
- function createViteConfigSource(options) {
4563
- const widgetConfigSource = createWidgetViteConfigObjectSource({
4564
- ...options,
4565
- disableConfigFileLookup: !options.projectConfigPath
4566
- });
4567
- if (!options.projectConfigPath) return `import { defineConfig } from "vite";
4568
-
4569
- export default defineConfig(${widgetConfigSource});
4570
- `;
4571
- return `import { defineConfig, loadConfigFromFile, mergeConfig } from "vite";
4572
-
4573
- const projectConfigPath = ${JSON.stringify(options.projectConfigPath)};
4574
- const widgetConfig = ${widgetConfigSource};
4575
- const fluidWidgetDefineInvariants = {
4576
- "process.env.NODE_ENV": JSON.stringify("production"),
4577
- };
4578
- const blockedFluidPluginNames = new Set(${JSON.stringify(BLOCKED_FLUID_PROJECT_PLUGIN_NAMES)});
4579
-
4580
- function isAllowedProjectPlugin(plugin) {
4581
- return !plugin || typeof plugin !== "object" || !blockedFluidPluginNames.has(plugin.name);
4582
- }
4583
-
4584
- function sanitizeProjectPlugins(pluginOption) {
4585
- if (!Array.isArray(pluginOption)) {
4586
- return isAllowedProjectPlugin(pluginOption) ? pluginOption : [];
4587
- }
4588
-
4589
- return pluginOption
4590
- .map((plugin) => Array.isArray(plugin) ? sanitizeProjectPlugins(plugin) : plugin)
4591
- .filter(isAllowedProjectPlugin);
4592
- }
4593
-
4594
- function copyProjectConfigValue(source, target, key) {
4595
- if (Object.prototype.hasOwnProperty.call(source, key)) {
4596
- target[key] = source[key];
4597
- }
4598
- }
4599
-
4600
- function sanitizeProjectConfig(config) {
4601
- if (!config || typeof config !== "object") return {};
4602
-
4603
- const sanitized = {};
4604
- if (Object.prototype.hasOwnProperty.call(config, "plugins")) {
4605
- sanitized.plugins = sanitizeProjectPlugins(config.plugins);
4606
- }
4607
- copyProjectConfigValue(config, sanitized, "resolve");
4608
- copyProjectConfigValue(config, sanitized, "define");
4609
- copyProjectConfigValue(config, sanitized, "css");
4610
- copyProjectConfigValue(config, sanitized, "esbuild");
4611
- copyProjectConfigValue(config, sanitized, "json");
4612
- copyProjectConfigValue(config, sanitized, "assetsInclude");
4613
- copyProjectConfigValue(config, sanitized, "envDir");
4614
- copyProjectConfigValue(config, sanitized, "envPrefix");
4615
- return sanitized;
4616
- }
4617
-
4618
- function copyConfigDefineValue(value) {
4619
- return value && typeof value === "object" && !Array.isArray(value)
4620
- ? { ...value }
4621
- : {};
4622
- }
4623
-
4624
- function mergeFluidWidgetDefineInvariants(mergedConfig, safeProjectConfig, widgetConfig) {
4625
- mergedConfig.define = {
4626
- ...copyConfigDefineValue(safeProjectConfig.define),
4627
- ...copyConfigDefineValue(mergedConfig.define),
4628
- ...copyConfigDefineValue(widgetConfig.define),
4629
- ...fluidWidgetDefineInvariants,
4630
- };
4631
- return mergedConfig;
4632
- }
4633
-
4634
- export default defineConfig(async (configEnv) => {
4635
- const projectConfig = await loadConfigFromFile(configEnv, projectConfigPath);
4636
- const safeProjectConfig = sanitizeProjectConfig(projectConfig?.config ?? {});
4637
- return mergeFluidWidgetDefineInvariants(mergeConfig(safeProjectConfig, widgetConfig), safeProjectConfig, widgetConfig);
4638
- });
4639
- `;
4640
- }
4641
- function createWidgetViteConfigObjectSource(options) {
4642
- const configFileSource = options.disableConfigFileLookup ? " configFile: false,\n" : "";
4643
- return `(() => {
4644
- const fluidWidgetDefineInvariants = {
4645
- "process.env.NODE_ENV": JSON.stringify("production"),
4646
- };
4647
-
4648
- function createFluidWidgetBuildInvariants() {
4649
- return {
4650
- copyPublicDir: false,
4651
- emptyOutDir: false,
4652
- outDir: ${JSON.stringify(options.publishDir)},
4653
- sourcemap: false,
4654
- target: "esnext",
4655
- cssCodeSplit: false,
4656
- lib: {
4657
- entry: ${JSON.stringify(options.entryPath)},
4658
- formats: ["es"],
4659
- fileName: () => "widget.js",
4660
- cssFileName: "widget",
4661
- },
4662
- rollupOptions: {
4663
- output: {
4664
- inlineDynamicImports: true,
4665
- hoistTransitiveImports: false,
4666
- },
4667
- },
4668
- };
4669
- }
4670
-
4671
- function fluidWidgetBuildInvariantsPlugin() {
4672
- return {
4673
- name: "fluid-widget-build-invariants",
4674
- enforce: "post",
4675
- config() {
4676
- return { build: createFluidWidgetBuildInvariants() };
4677
- },
4678
- configResolved(config) {
4679
- config.define = {
4680
- ...(config.define ?? {}),
4681
- ...fluidWidgetDefineInvariants,
4682
- };
4683
- Object.assign(config.build, createFluidWidgetBuildInvariants());
4684
- },
4685
- };
4686
- }
4687
-
4688
- return {
4689
- ${configFileSource} publicDir: false,
4690
- define: fluidWidgetDefineInvariants,
4691
- resolve: {
4692
- conditions: [
4693
- "module",
4694
- "browser",
4695
- "development|production",
4696
- ],
4697
- },
4698
- plugins: [fluidWidgetBuildInvariantsPlugin()],
4699
- build: createFluidWidgetBuildInvariants(),
4700
- };
4701
- })()`;
4702
- }
4703
- async function validateWidgetPackageOutputPaths(projectDir, publishDir) {
4704
- validateSharedWidgetPackagePublishDir(projectDir, publishDir);
4705
- await rejectSymlinkedPathSegments({
4706
- projectDir,
4707
- relativePath: publishDir,
4708
- label: "publishDir"
4709
- });
4710
- await rejectSymlinkedPathSegments({
4711
- projectDir,
4712
- relativePath: TEMP_BUILD_DIR,
4713
- label: "reserved temporary build directory"
4714
- });
4715
- }
4716
- async function collectCssUrls(publishDir) {
4717
- return (await fs.readdir(publishDir, { withFileTypes: true })).filter((entry) => entry.isFile() && isWidgetPackageCssArtifactPath(entry.name)).map((entry) => entry.name).sort();
4718
- }
4719
- async function rejectSymlinkedPathSegments(options) {
4720
- const resolvedProjectDir = path.resolve(options.projectDir);
4721
- const resolvedTargetPath = path.resolve(resolvedProjectDir, options.relativePath);
4722
- const segments = path.relative(resolvedProjectDir, resolvedTargetPath).split(path.sep).filter(Boolean);
4723
- let currentPath = resolvedProjectDir;
4724
- for (const segment of segments) {
4725
- currentPath = path.join(currentPath, segment);
4726
- let stat;
4727
- try {
4728
- stat = await fs.lstat(currentPath);
4729
- } catch (err) {
4730
- if (isNodeError(err) && err.code === "ENOENT") return;
4731
- throw err;
4732
- }
4733
- if (stat.isSymbolicLink()) throw new Error(`${options.label} must not include symbolic links.`);
4734
- }
4735
- }
4736
- function normalizePublishDirForValidation(value) {
4737
- return toPosixPath(path.normalize(value)).replace(/\/+$/, "") || ".";
4738
- }
4739
- function isPathWithinReservedDir(value, reservedDir) {
4740
- return value === reservedDir || value.startsWith(`${reservedDir}/`);
4741
- }
4742
- function toPosixPath(value) {
4743
- return value.split(path.sep).join("/");
4744
- }
4745
- function stableStringify(value) {
4746
- return `${JSON.stringify(value, null, 2)}\n`;
4747
- }
4748
- function resolveViteCliPathFromRequire(moduleRequire) {
4749
- try {
4750
- const packageJsonPath = moduleRequire.resolve("vite/package.json");
4751
- const binPath = getViteBinPath(moduleRequire(packageJsonPath).bin);
4752
- if (!binPath) return void 0;
4753
- return path.resolve(path.dirname(packageJsonPath), binPath);
4754
- } catch (err) {
4755
- if (isModuleResolutionError(err)) return void 0;
4756
- throw err;
4757
- }
4758
- }
4759
- function getViteBinPath(bin) {
4760
- if (typeof bin === "string") return bin;
4761
- if (!isRecord$2(bin)) return void 0;
4762
- const viteBin = bin.vite;
4763
- return typeof viteBin === "string" ? viteBin : void 0;
4764
- }
4765
- function isRecord$2(value) {
4766
- return typeof value === "object" && value !== null && !Array.isArray(value);
4767
- }
4768
- function isNodeError(value) {
4769
- return typeof value === "object" && value !== null && "code" in value && typeof value.code === "string";
4770
- }
4771
- function isModuleResolutionError(value) {
4772
- return isNodeError(value) && (value.code === "MODULE_NOT_FOUND" || value.code === "ERR_MODULE_NOT_FOUND");
4773
- }
4774
- //#endregion
4775
- //#region ../widget-tooling/src/internal/widget-package-publish-request.ts
4776
- async function createWidgetPackagePublishRequest(options) {
4777
- const manifestContent = await fs.readFile(options.manifestPath);
4778
- const manifest = parseJsonRecord(manifestContent.toString("utf-8"), options.manifestPath);
4779
- const publishManifest = await readPublishManifest(options.publishManifestPath);
4780
- assertManifestIdentityMatches(manifest, publishManifest, options.manifestPath);
4781
- assertMetadataMatches({
4782
- label: "manifestHash",
4783
- path: "manifest.json",
4784
- expected: publishManifest.manifestHash,
4785
- actual: sha256(manifestContent)
4786
- });
4787
- const collectedArtifacts = await collectWidgetPackageArtifacts(options.publishDir);
4788
- assertArtifactInventoryMatches(publishManifest.artifacts, collectedArtifacts);
4789
- const artifacts = await readUploadArtifacts(options.publishDir, publishManifest.artifacts);
4790
- return {
4791
- version: publishManifest.version,
4792
- packageKey: stripOwnerPrefix(publishManifest.packageId, options.owner),
4793
- builderVersion: publishManifest.builderVersion,
4794
- cliVersion: publishManifest.cliVersion,
4795
- runtimeVersion: publishManifest.runtimeVersion,
4796
- manifestHash: publishManifest.manifestHash,
4797
- manifest,
4798
- artifacts
4799
- };
4800
- }
4801
- async function readJsonRecord(filePath) {
4802
- return parseJsonRecord(await fs.readFile(filePath, "utf-8"), filePath);
4803
- }
4804
- function parseJsonRecord(content, filePath) {
4805
- const value = JSON.parse(content);
4806
- if (!isRecord$1(value) || Array.isArray(value)) throw new Error(`${path.basename(filePath)} must contain a JSON object.`);
4807
- return value;
4808
- }
4809
- async function readPublishManifest(filePath) {
4810
- const record = await readJsonRecord(filePath);
4811
- const artifactsValue = record["artifacts"];
4812
- if (!Array.isArray(artifactsValue)) throw new Error("publish-manifest.json must include an artifacts array.");
4813
- return {
4814
- packageId: readRequiredString(record, "packageId", filePath),
4815
- version: readRequiredString(record, "version", filePath),
4816
- builderVersion: readRequiredString(record, "builderVersion", filePath),
4817
- cliVersion: readRequiredString(record, "cliVersion", filePath),
4818
- runtimeVersion: readRequiredString(record, "runtimeVersion", filePath),
4819
- manifestHash: readRequiredString(record, "manifestHash", filePath),
4820
- artifacts: artifactsValue.map((artifact, index) => readArtifactMetadata(artifact, index))
4821
- };
4822
- }
4823
- function readArtifactMetadata(value, index) {
4824
- if (!isRecord$1(value) || Array.isArray(value)) throw new Error(`publish-manifest.json artifacts[${index}] must be a JSON object.`);
4825
- return {
4826
- path: readRequiredString(value, "path", "publish-manifest.json"),
4827
- sha256: readRequiredString(value, "sha256", "publish-manifest.json"),
4828
- bytes: readRequiredNumber(value, "bytes", "publish-manifest.json"),
4829
- contentType: readRequiredString(value, "contentType", "publish-manifest.json")
4830
- };
4831
- }
4832
- async function readUploadArtifacts(publishDir, artifacts) {
4833
- const resolvedPublishDir = path.resolve(publishDir);
4834
- return Promise.all(artifacts.map(async (artifact) => {
4835
- const artifactPath = resolveArtifactPath(resolvedPublishDir, artifact.path);
4836
- const actual = await computeArtifactMetadata(artifactPath, resolvedPublishDir);
4837
- assertArtifactMetadataMatches(artifact, actual);
4838
- return {
4839
- ...actual,
4840
- body: await fs.readFile(artifactPath)
4841
- };
4842
- }));
4843
- }
4844
- function assertManifestIdentityMatches(manifest, publishManifest, manifestPath) {
4845
- assertMetadataMatches({
4846
- label: "packageId",
4847
- path: "manifest.json",
4848
- expected: publishManifest.packageId,
4849
- actual: readRequiredString(manifest, "packageId", manifestPath)
4850
- });
4851
- assertMetadataMatches({
4852
- label: "version",
4853
- path: "manifest.json",
4854
- expected: publishManifest.version,
4855
- actual: readRequiredString(manifest, "version", manifestPath)
4856
- });
4857
- }
4858
- function assertArtifactInventoryMatches(expected, actual) {
4859
- const expectedByPath = /* @__PURE__ */ new Map();
4860
- for (const artifact of expected) {
4861
- if (expectedByPath.has(artifact.path)) throw new Error(`publish-manifest.json artifacts include duplicate path ${JSON.stringify(artifact.path)}. Rebuild the widget package before publishing.`);
4862
- expectedByPath.set(artifact.path, artifact);
4863
- }
4864
- const actualPaths = /* @__PURE__ */ new Set();
4865
- for (const actualArtifact of actual) {
4866
- actualPaths.add(actualArtifact.path);
4867
- const expectedArtifact = expectedByPath.get(actualArtifact.path);
4868
- if (!expectedArtifact) throw new Error(`publish-manifest.json artifacts omit ${JSON.stringify(actualArtifact.path)}. Rebuild the widget package before publishing.`);
4869
- assertArtifactMetadataMatches(expectedArtifact, actualArtifact);
4870
- }
4871
- for (const expectedArtifact of expected) {
4872
- if (actualPaths.has(expectedArtifact.path)) continue;
4873
- throw new Error(`publish-manifest.json artifacts include ${JSON.stringify(expectedArtifact.path)}, but that file is not present on disk. Rebuild the widget package before publishing.`);
4874
- }
4875
- }
4876
- function assertArtifactMetadataMatches(expected, actual) {
4877
- assertMetadataMatches({
4878
- label: "path",
4879
- path: expected.path,
4880
- expected: expected.path,
4881
- actual: actual.path
4882
- });
4883
- assertMetadataMatches({
4884
- label: "sha256",
4885
- path: expected.path,
4886
- expected: expected.sha256,
4887
- actual: actual.sha256
4888
- });
4889
- assertMetadataMatches({
4890
- label: "bytes",
4891
- path: expected.path,
4892
- expected: expected.bytes,
4893
- actual: actual.bytes
4894
- });
4895
- assertMetadataMatches({
4896
- label: "contentType",
4897
- path: expected.path,
4898
- expected: expected.contentType,
4899
- actual: actual.contentType
4900
- });
4901
- }
4902
- function assertMetadataMatches(options) {
4903
- if (options.expected === options.actual) return;
4904
- throw new Error(`publish-manifest.json ${options.label} for ${JSON.stringify(options.path)} does not match the file on disk. Recorded ${JSON.stringify(options.expected)}, computed ${JSON.stringify(options.actual)}. Rebuild the widget package before publishing.`);
4905
- }
4906
- function resolveArtifactPath(publishDir, artifactPath) {
4907
- const resolved = path.resolve(publishDir, artifactPath);
4908
- const relative = path.relative(publishDir, resolved);
4909
- if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error(`Artifact path ${JSON.stringify(artifactPath)} escapes the publish directory.`);
4910
- return resolved;
4911
- }
4912
- function stripOwnerPrefix(packageId, owner) {
4913
- const prefix = `${owner}.`;
4914
- if (packageId.startsWith(prefix) && packageId.length > prefix.length) return packageId.slice(prefix.length);
4915
- return packageId;
4916
- }
4917
- function readRequiredString(record, key, filePath) {
4918
- const value = record[key];
4919
- if (typeof value === "string" && value.trim().length > 0) return value;
4920
- throw new Error(`${path.basename(filePath)} must include string ${key}.`);
4921
- }
4922
- function readRequiredNumber(record, key, filePath) {
4923
- const value = record[key];
4924
- if (typeof value === "number" && Number.isFinite(value)) return value;
4925
- throw new Error(`${path.basename(filePath)} must include number ${key}.`);
4926
- }
4927
- function isRecord$1(value) {
4928
- return typeof value === "object" && value !== null;
4929
- }
4930
- //#endregion
4931
- //#region ../widget-tooling/src/internal/widget-package-upload.ts
4932
- var WidgetPackageUploadError = class extends Error {
4933
- code;
4934
- constructor(code, message) {
4935
- super(message);
4936
- this.name = "WidgetPackageUploadError";
4937
- this.code = code;
4938
- }
4939
- };
4940
- function buildWidgetPackageArtifactPayload(artifact) {
4941
- return {
4942
- path: artifact.path,
4943
- sha256: artifact.sha256,
4944
- bytes: artifact.bytes,
4945
- contentType: artifact.contentType
4946
- };
4947
- }
4948
- function buildWidgetPackageUploadSessionPayload(request) {
4949
- return {
4950
- version: request.version,
4951
- package_key: request.packageKey,
4952
- builder_version: request.builderVersion,
4953
- cli_version: request.cliVersion,
4954
- runtime_version: request.runtimeVersion,
4955
- manifest_hash: request.manifestHash,
4956
- manifest: request.manifest,
4957
- artifacts: request.artifacts.map(buildWidgetPackageArtifactPayload)
4958
- };
4959
- }
4960
- function buildWidgetPackageCompleteUploadPayload(packageKey, artifacts) {
4961
- return {
4962
- package_key: packageKey,
4963
- artifacts: artifacts.map(buildWidgetPackageArtifactPayload)
4964
- };
4965
- }
4966
- function getWidgetPackageUploadSessionRoute(owner) {
4967
- switch (owner.kind) {
4968
- case "droplet": return `/api/droplets/${encodeURIComponent(owner.uuid)}/widget_package_versions`;
4969
- case "company": return "/api/company/fluid_os/widget_package_versions";
4970
- }
4971
- }
4972
- function getWidgetPackageCompleteUploadRoute(owner, version) {
4973
- switch (owner.kind) {
4974
- case "droplet": return `/api/droplets/${encodeURIComponent(owner.uuid)}/widget_package_versions/${encodeURIComponent(version)}/complete_upload`;
4975
- case "company": return `/api/company/fluid_os/widget_package_versions/${encodeURIComponent(version)}/complete_upload`;
4976
- }
4977
- }
4978
- async function requestWidgetPackageUploadSession(client, owner, request) {
4979
- const payload = buildWidgetPackageUploadSessionPayload(request);
4980
- try {
4981
- return normalizeWidgetPackageUploadSession(owner.kind === "company" ? await fluid_os_v0_create_widget_package_version(client, payload) : await client.post(getWidgetPackageUploadSessionRoute(owner), payload), request.version, request.artifacts);
4982
- } catch (err) {
4983
- if (err instanceof WidgetPackageUploadError) throw err;
4984
- throw new WidgetPackageUploadError("SESSION_REQUEST_FAILED", `Failed to create widget package upload session: ${formatUnknownError(err)}`);
4985
- }
4986
- }
4987
- async function uploadWidgetPackageArtifacts(session, artifacts, fetchImpl = fetch) {
4988
- const uploadsByPath = new Map(session.uploads.map((upload) => [upload.path, upload]));
4989
- const results = [];
4990
- for (const artifact of artifacts) {
4991
- const upload = uploadsByPath.get(artifact.path);
4992
- if (!upload) throw new WidgetPackageUploadError("MISSING_SIGNED_URL", `Upload session did not include a signed URL for artifact "${artifact.path}".`);
4993
- if (upload.contentType !== artifact.contentType) throw new WidgetPackageUploadError("CONTENT_TYPE_MISMATCH", `Upload session expected artifact "${artifact.path}" to use content type "${upload.contentType}", but local artifact uses "${artifact.contentType}".`);
4994
- let response;
4995
- try {
4996
- response = await fetchImpl(upload.url, {
4997
- method: "PUT",
4998
- headers: mergeUploadHeaders(upload.headers, upload.contentType),
4999
- body: artifact.body
5000
- });
5001
- } catch (err) {
5002
- throw new WidgetPackageUploadError("ARTIFACT_UPLOAD_FAILED", `Failed to upload artifact "${artifact.path}": ${formatUnknownError(err)}`);
5003
- }
5004
- if (!response.ok) {
5005
- const body = await response.text().catch(() => "");
5006
- const details = body.trim() ? `: ${body.trim().slice(0, 500)}` : "";
5007
- throw new WidgetPackageUploadError("ARTIFACT_UPLOAD_FAILED", `Failed to upload artifact "${artifact.path}": PUT returned HTTP ${response.status}${details}`);
5008
- }
5009
- results.push({
5010
- path: artifact.path,
5011
- status: response.status,
5012
- contentType: upload.contentType
5013
- });
5014
- }
5015
- return results;
5016
- }
5017
- async function completeWidgetPackageUpload(client, owner, version, packageKey, artifacts) {
5018
- const payload = buildWidgetPackageCompleteUploadPayload(packageKey, artifacts);
4150
+ /**
4151
+ * The whole deploy, as one function returning a result.
4152
+ *
4153
+ * A result rather than a throw because two of its outcomes — a key that belongs
4154
+ * to another package, a version already published — are refusals with a fix to
4155
+ * show, not exceptions.
4156
+ */
4157
+ async function runPortalDeploy(options) {
5019
4158
  try {
5020
- return normalizeWidgetPackageCompleteResponse(owner.kind === "company" ? await fluid_os_v0_complete_widget_package_version_upload(client, version, payload) : await client.post(getWidgetPackageCompleteUploadRoute(owner, version), payload), version);
5021
- } catch (err) {
5022
- if (err instanceof WidgetPackageUploadError) throw err;
5023
- throw new WidgetPackageUploadError("COMPLETE_UPLOAD_FAILED", `Failed to complete widget package upload: ${formatUnknownError(err)}`);
5024
- }
5025
- }
5026
- async function publishWidgetPackageVersion(options) {
5027
- const sessionPayload = buildWidgetPackageUploadSessionPayload(options.request);
5028
- if (options.dryRun) return {
5029
- dryRun: true,
5030
- sessionPayload
5031
- };
5032
- if (!options.client) throw new Error("A FetchClient is required when dryRun is false.");
5033
- const session = await requestWidgetPackageUploadSession(options.client, options.owner, options.request);
5034
- return {
5035
- dryRun: false,
5036
- sessionPayload,
5037
- session,
5038
- uploadedArtifacts: await uploadWidgetPackageArtifacts(session, options.request.artifacts, options.fetchImpl),
5039
- complete: await completeWidgetPackageUpload(options.client, options.owner, session.version, options.request.packageKey, options.request.artifacts)
5040
- };
5041
- }
5042
- function normalizeWidgetPackageUploadSession(response, requestedVersion, requestedArtifacts) {
5043
- const record = requireRecord(response, "INVALID_SESSION_RESPONSE", "Upload session response must be a JSON object.");
5044
- requireFiniteNumberProperty(record, "version_id", "INVALID_SESSION_RESPONSE");
5045
- requireStringProperty(record, "artifact_base_url", "INVALID_SESSION_RESPONSE");
5046
- const uploadValues = record["uploads"];
5047
- if (!Array.isArray(uploadValues)) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", "Upload session response must contain an \"uploads\" array.");
5048
- const requestedArtifactsByPath = new Map(requestedArtifacts.map((artifact) => [artifact.path, artifact]));
5049
- const seenUploadPaths = /* @__PURE__ */ new Set();
5050
- const uploads = uploadValues.map((value, index) => {
5051
- const upload = requireRecord(value, "INVALID_SESSION_RESPONSE", `Upload session uploads[${index}] must be a JSON object.`);
5052
- const path = requireStringProperty(upload, "path", "INVALID_SESSION_RESPONSE");
5053
- const url = requireStringProperty(upload, "upload_url", "INVALID_SESSION_RESPONSE");
5054
- requireStringProperty(upload, "public_url", "INVALID_SESSION_RESPONSE");
5055
- const requestedArtifact = requestedArtifactsByPath.get(path);
5056
- if (!requestedArtifact) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", `Upload session response included unexpected artifact "${path}".`);
5057
- if (seenUploadPaths.has(path)) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", `Upload session response included duplicate artifact "${path}".`);
5058
- seenUploadPaths.add(path);
4159
+ await assertStandalonePortalRemoteActionAllowed(options.projectDir, "portal deploy");
4160
+ } catch (error) {
5059
4161
  return {
5060
- path,
5061
- url,
5062
- contentType: requestedArtifact.contentType,
5063
- headers: {}
4162
+ ok: false,
4163
+ code: "STANDALONE_PORTAL",
4164
+ message: error instanceof Error ? error.message : String(error)
5064
4165
  };
5065
- });
5066
- const missingUploadPaths = requestedArtifacts.map((artifact) => artifact.path).filter((artifactPath) => !seenUploadPaths.has(artifactPath));
5067
- if (missingUploadPaths.length > 0) throw new WidgetPackageUploadError("INVALID_SESSION_RESPONSE", `Upload session response is missing signed URLs for artifact(s): ${missingUploadPaths.join(", ")}.`);
5068
- return {
5069
- version: requestedVersion,
5070
- uploads,
5071
- raw: response
5072
- };
5073
- }
5074
- function normalizeWidgetPackageCompleteResponse(response, requestedVersion) {
5075
- const record = requireRecord(response, "INVALID_COMPLETE_RESPONSE", "Complete upload response must be a JSON object.");
5076
- const responseVersion = requireStringProperty(record, "version", "INVALID_COMPLETE_RESPONSE");
5077
- if (responseVersion !== requestedVersion) throw new WidgetPackageUploadError("INVALID_COMPLETE_RESPONSE", `Complete upload response version "${responseVersion}" did not match requested version "${requestedVersion}".`);
5078
- const packageKey = requireStringProperty(record, "packageId", "INVALID_COMPLETE_RESPONSE");
5079
- const status = requireStringProperty(record, "status", "INVALID_COMPLETE_RESPONSE");
5080
- requireStringProperty(record, "scriptUrl", "INVALID_COMPLETE_RESPONSE");
5081
- requireStringProperty(record, "manifestUrl", "INVALID_COMPLETE_RESPONSE");
5082
- requireStringArrayProperty(record, "cssUrls", "INVALID_COMPLETE_RESPONSE");
5083
- return {
5084
- version: responseVersion,
5085
- status,
5086
- packageKey,
5087
- raw: response
5088
- };
5089
- }
5090
- function normalizeHeaderName(name) {
5091
- return name.toLowerCase();
5092
- }
5093
- function mergeUploadHeaders(headers, contentType) {
5094
- const merged = {};
5095
- for (const [key, value] of Object.entries(headers)) {
5096
- if (normalizeHeaderName(key) === "content-type") continue;
5097
- merged[key] = value;
5098
4166
  }
5099
- merged["Content-Type"] = contentType;
5100
- return merged;
5101
- }
5102
- function requireRecord(value, code, message) {
5103
- if (!isRecord(value) || Array.isArray(value)) throw new WidgetPackageUploadError(code, message);
5104
- return value;
5105
- }
5106
- function isRecord(value) {
5107
- return typeof value === "object" && value !== null;
5108
- }
5109
- function requireStringProperty(record, key, code) {
5110
- const value = record[key];
5111
- if (typeof value === "string" && value.trim().length > 0) return value;
5112
- throw new WidgetPackageUploadError(code, `Response field "${key}" must be a non-empty string.`);
5113
- }
5114
- function requireFiniteNumberProperty(record, key, code) {
5115
- const value = record[key];
5116
- if (typeof value === "number" && Number.isFinite(value)) return value;
5117
- throw new WidgetPackageUploadError(code, `Response field "${key}" must be a finite number.`);
5118
- }
5119
- function requireStringArrayProperty(record, key, code) {
5120
- const value = record[key];
5121
- if (Array.isArray(value) && value.every((entry) => typeof entry === "string" && entry.trim().length > 0)) return value;
5122
- throw new WidgetPackageUploadError(code, `Response field "${key}" must be an array of non-empty strings.`);
5123
- }
5124
- function formatUnknownError(err) {
5125
- const base = err instanceof Error ? err.message : String(err);
5126
- if (isRecord(err) && "data" in err) return `${base} — ${safeStringify(err.data)}`;
5127
- return base;
5128
- }
5129
- function safeStringify(value) {
5130
- const seen = /* @__PURE__ */ new WeakSet();
5131
- try {
5132
- return JSON.stringify(value, (_key, item) => {
5133
- if (typeof item === "bigint") return `${item}n`;
5134
- if (typeof item !== "object" || item === null) return item;
5135
- if (seen.has(item)) return "[Circular]";
5136
- seen.add(item);
5137
- return item;
5138
- }) ?? String(value);
5139
- } catch {
5140
- return "[unserializable error data]";
5141
- }
5142
- }
5143
- //#endregion
5144
- //#region ../widget-tooling/src/workflows.ts
5145
- async function buildWidgetPackage(options) {
5146
- const result = await buildSharedWidgetPackage({
5147
- projectDir: options.projectDir,
5148
- publishDir: options.outDir,
5149
- owner: options.owner,
5150
- cliVersion: options.cliVersion
5151
- });
5152
- if (result.success) return result.value;
5153
- throw new WidgetToolingError(result.error.code === "CONFIG_LOAD_FAILED" ? "CONFIG_LOAD_FAILED" : result.error.code === "VALIDATION_FAILED" ? "VALIDATION_FAILED" : result.error.code === "INVALID_PUBLISH_DIR" ? "INVALID_OUTPUT_DIR" : "BUILD_FAILED", result.error.message, { details: result.error.details });
5154
- }
5155
- async function publishWidgetPackage(options) {
5156
- if (options.owner !== options.uploadOwner.kind) throw new WidgetToolingError("OWNER_MISMATCH", `Widget package build owner "${options.owner}" must match upload owner kind "${options.uploadOwner.kind}".`);
5157
- if (!options.dryRun && !options.client) throw new WidgetToolingError("CLIENT_REQUIRED", "A FetchClient is required when dryRun is false.");
5158
- const build = await buildWidgetPackage(options);
5159
- let request;
5160
- try {
5161
- request = await createWidgetPackagePublishRequest({
5162
- publishDir: build.publishDir,
5163
- manifestPath: build.manifestPath,
5164
- publishManifestPath: build.publishManifestPath,
5165
- owner: options.owner
5166
- });
4167
+ let client;
4168
+ if (!options.dryRun) try {
4169
+ client = createAuthenticatedWidgetPackageClient();
5167
4170
  } catch (error) {
5168
- throw new WidgetToolingError("ARTIFACT_VERIFICATION_FAILED", error instanceof Error ? error.message : "Failed to verify widget package artifacts.", { cause: error });
5169
- }
5170
- try {
5171
- const publish = options.dryRun ? await publishWidgetPackageVersion({
5172
- owner: options.uploadOwner,
5173
- request,
5174
- dryRun: true,
5175
- fetchImpl: options.fetchImpl
5176
- }) : await publishWidgetPackageVersion({
5177
- client: options.client,
5178
- owner: options.uploadOwner,
5179
- request,
5180
- fetchImpl: options.fetchImpl
5181
- });
5182
4171
  return {
5183
- build,
5184
- request,
5185
- publish
4172
+ ok: false,
4173
+ code: "NOT_AUTHENTICATED",
4174
+ message: "Not signed in to Fluid, so the widgets were not published.",
4175
+ ...error instanceof Error ? { hint: error.message } : {}
5186
4176
  };
5187
- } catch (error) {
5188
- if (error instanceof WidgetToolingError) throw error;
5189
- if (error instanceof WidgetPackageUploadError) throw new WidgetToolingError(toPublicUploadErrorCode(error.code), error.message, { cause: error });
5190
- throw new WidgetToolingError("PUBLISH_FAILED", error instanceof Error ? error.message : "Failed to publish widget package.", { cause: error });
5191
- }
5192
- }
5193
- function toPublicUploadErrorCode(code) {
5194
- switch (code) {
5195
- case "SESSION_REQUEST_FAILED":
5196
- case "INVALID_SESSION_RESPONSE":
5197
- case "MISSING_SIGNED_URL":
5198
- case "CONTENT_TYPE_MISMATCH":
5199
- case "ARTIFACT_UPLOAD_FAILED":
5200
- case "COMPLETE_UPLOAD_FAILED":
5201
- case "INVALID_COMPLETE_RESPONSE": return code;
5202
- default: return "PUBLISH_FAILED";
5203
4177
  }
5204
- }
5205
- //#endregion
5206
- //#region src/commands/widget-package-publish.ts
5207
- async function publishWidgetRuntimeArtifacts(options) {
5208
- const isDryRun = options.dryRun === true;
5209
- const client = options.client;
5210
- if (!isDryRun && !client) throw new Error("A FetchClient is required when dryRun is false.");
5211
- const cliVersion = await getPortalCliVersion();
5212
- try {
5213
- if (isDryRun) return await publishWidgetPackage({
5214
- projectDir: options.projectDir,
5215
- outDir: options.outDir,
5216
- owner: options.buildOwner,
5217
- uploadOwner: options.uploadOwner,
5218
- cliVersion,
5219
- dryRun: true
5220
- });
5221
- if (!client) throw new Error("A FetchClient is required when dryRun is false.");
5222
- return await publishWidgetPackage({
4178
+ let preflight = {};
4179
+ if (client) {
4180
+ options.onProgress?.("Checking this company's widget package...");
4181
+ preflight = await preflightCompanyPackage({
5223
4182
  projectDir: options.projectDir,
5224
- outDir: options.outDir,
5225
- owner: options.buildOwner,
5226
- uploadOwner: options.uploadOwner,
5227
- cliVersion,
5228
- client
4183
+ client,
4184
+ readConfiguredPackage: async (dir) => {
4185
+ const { validated } = await loadAndValidateWidgetPackage({
4186
+ projectDir: dir,
4187
+ owner: "company"
4188
+ });
4189
+ return {
4190
+ packageId: validated.packageId,
4191
+ version: validated.version
4192
+ };
4193
+ }
5229
4194
  });
5230
- } catch (error) {
5231
- throwWithDetails(error);
5232
- }
5233
- }
5234
- function throwWithDetails(error) {
5235
- if (!(error instanceof Error)) throw error;
5236
- if (!("details" in error) || typeof error.details !== "string" || error.details.length === 0) throw error;
5237
- throw new Error(`${error.message}\n${error.details}`, { cause: error });
5238
- }
5239
- function createAuthenticatedWidgetPackageClient() {
5240
- const { profile, profileName, token } = resolveAuthenticatedProfile();
5241
- if (!token) {
5242
- if (!profileName) throw new Error("Not logged in. Run " + chalk.cyan("fluid login") + " first.");
5243
- throw new Error("No auth token found for profile " + chalk.cyan(profileName) + ". Run " + chalk.cyan("fluid login") + " to re-authenticate.");
5244
- }
5245
- return createFetchClient({
5246
- baseUrl: profile?.baseUrl ?? process.env["FLUID_API_BASE"] ?? "https://api.fluid.app",
5247
- getAuthToken: () => token
5248
- });
5249
- }
5250
- function resolveAuthenticatedProfile() {
5251
- const config = readConfig();
5252
- const projectConfig = findProjectConfig(process.cwd());
5253
- if (projectConfig?.profile) {
5254
- const profile = config.profiles[projectConfig.profile];
5255
- if (profile) return {
5256
- profile,
5257
- profileName: projectConfig.profile,
5258
- token: profile.token
4195
+ if (preflight.mismatch) return {
4196
+ ok: false,
4197
+ code: "PACKAGE_KEY_MISMATCH",
4198
+ message: "This project publishes under a different package key.",
4199
+ hint: preflight.mismatch
5259
4200
  };
5260
4201
  }
5261
- if (!config.activeProfile) return {};
5262
- const profile = config.profiles[config.activeProfile];
5263
- if (!profile) return {};
5264
- return {
5265
- profile,
5266
- profileName: config.activeProfile,
5267
- token: profile.token
5268
- };
5269
- }
5270
- function printWidgetPublishSummary(options) {
5271
- console.log();
5272
- console.log(chalk.green.bold(`${options.title} complete`));
5273
- console.log();
5274
- console.log(chalk.gray("Owner: ") + chalk.white(options.ownerLabel));
5275
- if (options.environment) console.log(chalk.gray("Environment: ") + chalk.white(options.environment));
5276
- console.log(chalk.gray("Package: ") + chalk.white(options.result.build.packageId));
5277
- console.log(chalk.gray("Version: ") + chalk.white(options.result.build.version));
5278
- console.log(chalk.gray("Output: ") + chalk.cyan(options.outDir));
5279
- console.log(chalk.gray("Artifacts: ") + chalk.white(String(options.result.request.artifacts.length)));
5280
- console.log();
5281
- }
5282
- function printRuntimeOnlyNotice() {
5283
- console.log(chalk.yellow("This publishes widget runtime artifacts only; it does not deploy the hosted portal shell."));
5284
- console.log();
5285
- }
5286
- function printDryRunSessionPayload(result) {
5287
- console.log(chalk.bold("Dry-run upload session payload:"));
5288
- console.log(JSON.stringify(result.publish.sessionPayload, null, 2));
5289
- console.log();
5290
- }
5291
- //#endregion
5292
- //#region src/commands/deploy.ts
5293
- const DEFAULT_OUT_DIR$1 = ".fluid/widget-dist";
5294
- const DEFAULT_ENVIRONMENT = "production";
5295
- const deployCommand = new Command("deploy").description("Publish company-owned portal widget runtime artifacts").option("-e, --environment <name>", "Target environment label for output reporting", DEFAULT_ENVIRONMENT).option("-o, --out-dir <dir>", "Widget artifact output directory", DEFAULT_OUT_DIR$1).option("--dry-run", "Build and validate upload payload without publishing").action(async (options) => {
5296
- const environment = options.environment ?? DEFAULT_ENVIRONMENT;
5297
- const outDir = options.outDir ?? DEFAULT_OUT_DIR$1;
5298
- const dryRun = options.dryRun === true;
5299
- console.log();
5300
- console.log(chalk.blue.bold("Fluid Portal Deploy"));
5301
- console.log();
5302
- printRuntimeOnlyNotice();
5303
- console.log(chalk.gray("Environment: ") + chalk.white(environment));
5304
- console.log(chalk.gray("Output: ") + chalk.cyan(outDir));
5305
- if (dryRun) console.log(chalk.yellow("Dry run: no upload will be created."));
5306
- console.log();
5307
- const spinner = ora("Building company-owned widget package...").start();
4202
+ options.onProgress?.("Building company-owned widget package...");
5308
4203
  try {
5309
4204
  const result = await publishWidgetRuntimeArtifacts({
5310
- projectDir: process.cwd(),
5311
- outDir,
4205
+ projectDir: options.projectDir,
4206
+ outDir: options.outDir,
5312
4207
  buildOwner: "company",
5313
4208
  uploadOwner: { kind: "company" },
5314
- dryRun,
5315
- ...dryRun ? {} : { client: createAuthenticatedWidgetPackageClient() }
5316
- });
5317
- spinner.succeed("Built widget runtime artifacts");
5318
- if (dryRun) {
5319
- console.log(chalk.yellow("Dry run complete — upload session was not requested."));
5320
- console.log();
5321
- printDryRunSessionPayload(result);
5322
- } else console.log(chalk.green("Published widget package version."));
5323
- printWidgetPublishSummary({
5324
- title: dryRun ? "Portal deploy dry run" : "Portal deploy",
5325
- ownerLabel: "company",
5326
- environment,
5327
- outDir,
5328
- result
4209
+ dryRun: options.dryRun,
4210
+ ...preflight.versionOverride ? { version: preflight.versionOverride } : {},
4211
+ ...client ? { client } : {}
5329
4212
  });
5330
- } catch (err) {
5331
- spinner.fail("Portal deploy failed");
5332
- console.error(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
5333
- process.exit(1);
4213
+ return {
4214
+ ok: true,
4215
+ packageId: result.build.packageId,
4216
+ version: result.build.version,
4217
+ artifactCount: result.request.artifacts.length,
4218
+ ...preflight.versionOverride && preflight.configuredVersion ? { requestedVersion: preflight.configuredVersion } : {},
4219
+ sessionPayload: result.publish.sessionPayload
4220
+ };
4221
+ } catch (error) {
4222
+ return {
4223
+ ok: false,
4224
+ code: error instanceof Error && "code" in error ? String(error.code) : "PUBLISH_FAILED",
4225
+ message: error instanceof Error ? error.message : "Publishing the widgets failed.",
4226
+ ...error instanceof Error && "details" in error && typeof error.details === "string" && error.details.length > 0 ? { hint: error.details } : {}
4227
+ };
5334
4228
  }
5335
- });
4229
+ }
5336
4230
  //#endregion
5337
4231
  //#region src/commands/doctor.ts
5338
4232
  /** Files that are managed by the SDK and should match the canonical template. */
@@ -5507,9 +4401,9 @@ function requireToken() {
5507
4401
  return token;
5508
4402
  }
5509
4403
  function createClient(token) {
5510
- return createFetchClient({
4404
+ return createFluidOsClient({
5511
4405
  baseUrl: getApiBase(),
5512
- getAuthToken: () => token
4406
+ token
5513
4407
  });
5514
4408
  }
5515
4409
  async function requireDefinitionId() {
@@ -5522,13 +4416,14 @@ async function requireDefinitionId() {
5522
4416
  return mappings.definition.id;
5523
4417
  }
5524
4418
  const createVersionCommand = new Command("create").description("Create a new version (snapshot) of the portal definition").option("--activate", "Activate the version immediately after creation").action(async (options) => {
4419
+ await assertStandalonePortalRemoteActionAllowed(process.cwd(), "portal version create");
5525
4420
  const client = createClient(requireToken());
5526
4421
  const definitionId = await requireDefinitionId();
5527
4422
  console.log();
5528
4423
  console.log(chalk.bold("Creating version..."));
5529
4424
  let result;
5530
4425
  try {
5531
- result = await fluid_os_v0_create_fluid_osversion(client, definitionId);
4426
+ result = await fluidOsApi.createVersion(client, definitionId);
5532
4427
  } catch (err) {
5533
4428
  console.error(chalk.red("Error:") + " Failed to create version — " + (err instanceof Error ? err.message : String(err)));
5534
4429
  process.exit(1);
@@ -5546,7 +4441,7 @@ const createVersionCommand = new Command("create").description("Create a new ver
5546
4441
  if (options.activate) {
5547
4442
  console.log("Activating version...");
5548
4443
  try {
5549
- await fluid_os_v0_update_fluid_osversion(client, definitionId, version.id, { version: { active: true } });
4444
+ await fluidOsApi.updateVersion(client, definitionId, version.id, { version: { active: true } });
5550
4445
  } catch (err) {
5551
4446
  console.error(chalk.red("Error:") + " Failed to activate version — " + (err instanceof Error ? err.message : String(err)));
5552
4447
  process.exit(1);
@@ -5561,7 +4456,7 @@ const listVersionCommand = new Command("list").description("List all versions of
5561
4456
  const definitionId = await requireDefinitionId();
5562
4457
  let result;
5563
4458
  try {
5564
- result = await fluid_os_v0_list_fluid_osversions(client, definitionId);
4459
+ result = await fluidOsApi.listVersions(client, definitionId);
5565
4460
  } catch (err) {
5566
4461
  console.error(chalk.red("Error:") + " Failed to list versions — " + (err instanceof Error ? err.message : String(err)));
5567
4462
  process.exit(1);
@@ -5587,6 +4482,12 @@ const listVersionCommand = new Command("list").description("List all versions of
5587
4482
  console.log();
5588
4483
  });
5589
4484
  const activateVersionCommand = new Command("activate").description("Activate a specific version, making it the live version").argument("<version-id>", "The version ID to activate").option("-y, --yes", "Skip confirmation prompt").action(async (versionId, options) => {
4485
+ await assertStandalonePortalRemoteActionAllowed(process.cwd(), "portal version activate");
4486
+ const numericVersionId = Number(versionId);
4487
+ if (!Number.isSafeInteger(numericVersionId) || numericVersionId < 1) {
4488
+ console.error(chalk.red("Error:") + " Version ID must be a positive integer.");
4489
+ process.exit(1);
4490
+ }
5590
4491
  const client = createClient(requireToken());
5591
4492
  const definitionId = await requireDefinitionId();
5592
4493
  if (!options.yes) {
@@ -5604,7 +4505,7 @@ const activateVersionCommand = new Command("activate").description("Activate a s
5604
4505
  console.log();
5605
4506
  console.log(chalk.bold("Activating version..."));
5606
4507
  try {
5607
- await fluid_os_v0_update_fluid_osversion(client, definitionId, versionId, { version: { active: true } });
4508
+ await fluidOsApi.updateVersion(client, definitionId, numericVersionId, { version: { active: true } });
5608
4509
  } catch (err) {
5609
4510
  console.error(chalk.red("Error:") + " Failed to activate version — " + (err instanceof Error ? err.message : String(err)));
5610
4511
  process.exit(1);
@@ -5618,6 +4519,7 @@ const versionCommand = new Command("version").description("Manage portal definit
5618
4519
  //#region src/commands/widget-publish.ts
5619
4520
  const DEFAULT_OUT_DIR = ".fluid/widget-dist";
5620
4521
  const publishSubcommand = new Command("publish").description("Publish droplet-owned widget runtime artifacts").requiredOption("--droplet <uuid>", "Droplet UUID that owns the widget package").option("-o, --out-dir <dir>", "Widget artifact output directory", DEFAULT_OUT_DIR).option("--dry-run", "Build and validate upload payload without publishing").action(async (options) => {
4522
+ await assertStandalonePortalRemoteActionAllowed(process.cwd(), "portal widget publish");
5621
4523
  const outDir = options.outDir ?? DEFAULT_OUT_DIR;
5622
4524
  const dryRun = options.dryRun === true;
5623
4525
  console.log();
@@ -5645,17 +4547,19 @@ const publishSubcommand = new Command("publish").description("Publish droplet-ow
5645
4547
  if (dryRun) {
5646
4548
  console.log(chalk.yellow("Dry run complete — upload session was not requested."));
5647
4549
  console.log();
5648
- printDryRunSessionPayload(result);
4550
+ printDryRunSessionPayload(result.publish.sessionPayload);
5649
4551
  } else console.log(chalk.green("Published widget package version."));
5650
4552
  printWidgetPublishSummary({
5651
4553
  title: dryRun ? "Widget publish dry run" : "Widget publish",
5652
4554
  ownerLabel: `droplet ${options.droplet}`,
5653
4555
  outDir,
5654
- result
4556
+ packageId: result.build.packageId,
4557
+ version: result.build.version,
4558
+ artifactCount: result.request.artifacts.length
5655
4559
  });
5656
4560
  } catch (err) {
5657
4561
  spinner.fail("Widget publish failed");
5658
- console.error(chalk.red("Error:") + " " + (err instanceof Error ? err.message : String(err)));
4562
+ printWidgetPublishError(err);
5659
4563
  process.exit(1);
5660
4564
  }
5661
4565
  });
@@ -5687,6 +4591,6 @@ const plugin = {
5687
4591
  }
5688
4592
  };
5689
4593
  //#endregion
5690
- export { CANONICAL_PROJECT_NAME, FILE_SYSTEM_ERRORS, GitError, TEMPLATES, TEMPLATE_REPO_URL, buildIdToSlugMap, buildNavigationIdToSlugMap, buildSnapshot, buildThemeIdToSlugMap, categorizeChanges, cloneCommand, computeFileHash, copyTemplate, copyTemplateSafe, createCommand, createDirectory, createDirectorySafe, createPortalGitCredential, createPortalProject, plugin as default, deployCommand, deriveScreenSlug, deriveSlug, diffAgainstSnapshot, directoryExists, doctorCommand, ensureGitAvailable, fileExists, getInstallCommand, getOriginUrl, getRunCommand, getSdkVersion, getSdkVersionSafe, getTemplatePaths, gitAheadBehindVsFetchHead, gitAutoCommit, gitClone, gitFetch, gitInit, gitMergeFastForward, gitMissingError, gitPush, hasUncommittedChanges, installDependencies, isGitAvailable, isGitRepo, pathExists, portalGitCredentialPath, promptProjectConfig, pullCommand, pushCommand, readFileSafe, readMappings, readSnapshot, removeMapping, resolveIdToSlug, resolveSlugToId, rewriteProjectName, runPackageManager, runPortalGitSync, scaffoldFromGitHub, slugFromPath, topLevelWidgetCommand, transformNavigation, transformNavigationItems, transformProfile, transformScreen, transformTheme, updateMapping, validateCrossReferences, versionCommand, widgetCommand, writeFileSafe, writeFluidrcProfile, writeMappings, writeSnapshot };
4594
+ export { CANONICAL_PROJECT_NAME, FILE_SYSTEM_ERRORS, GitError, TEMPLATES, TEMPLATE_REPO_URL, buildIdToSlugMap, buildNavigationIdToSlugMap, buildSnapshot, buildThemeIdToSlugMap, categorizeChanges, cloneCommand, computeFileHash, copyTemplate, copyTemplateSafe, createCommand, createDirectory, createDirectorySafe, createPortalGitCredential, createPortalProject, plugin as default, deployCommand, deriveScreenSlug, deriveSlug, diffAgainstSnapshot, directoryExists, doctorCommand, ensureGitAvailable, fileExists, formatPackageKeyMismatch, getInstallCommand, getOriginUrl, getRunCommand, getSdkVersion, getSdkVersionSafe, getTemplatePaths, gitAheadBehindVsFetchHead, gitAutoCommit, gitClone, gitFetch, gitInit, gitMergeFastForward, gitMissingError, gitPush, hasUncommittedChanges, installDependencies, isGitAvailable, isGitRepo, pathExists, portalGitCredentialPath, preflightCompanyPackage, promptProjectConfig, publishWidgetRuntimeArtifacts, pullCommand, pushCommand, readCompanyWidgetPackage, readFileSafe, readMappings, readSnapshot, removeMapping, resolveIdToSlug, resolvePublishVersion, resolveSlugToId, rewriteProjectName, runPackageManager, runPortalGitSync, scaffoldFromGitHub, slugFromPath, topLevelWidgetCommand, transformNavigation, transformNavigationItems, transformProfile, transformScreen, transformTheme, updateMapping, validateCrossReferences, versionCommand, widgetCommand, writeFileSafe, writeFluidrcProfile, writeMappings, writeSnapshot };
5691
4595
 
5692
4596
  //# sourceMappingURL=index.mjs.map