@fluid-app/fluid-cli 0.1.33 → 0.1.35

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/README.md CHANGED
@@ -60,7 +60,7 @@ Plugins are auto-discovered from:
60
60
 
61
61
  ## Development
62
62
 
63
- For contributors working in `fluid-mono`:
63
+ For contributors working in this monorepo:
64
64
 
65
65
  ```bash
66
66
  pnpm --filter @fluid-app/fluid-cli build
@@ -590,7 +590,7 @@ function resolveLoadPath(importSpecifier, resolve = defaultResolver) {
590
590
  /**
591
591
  * Decide whether to print the per-plugin "Loaded plugin …" diagnostics.
592
592
  *
593
- * A fluid-mono checkout resolves ~a dozen plugin packages, so emitting a line
593
+ * A monorepo checkout resolves ~a dozen plugin packages, so emitting a line
594
594
  * for each on every command (including `--help` and tab-completion, and into
595
595
  * Mist's captured stderr dialog) is noise. The success diagnostics are gated
596
596
  * behind an explicit opt-in; load *failures* are always surfaced by the loader
@@ -1 +1 @@
1
- {"version":3,"file":"fluid.mjs","names":[],"sources":["../../src/auth/profiles.ts","../../src/commands/login.ts","../../src/commands/logout.ts","../../src/commands/whoami.ts","../../src/commands/switch.ts","../../src/plugins/discovery.ts","../../src/plugins/format-load-line.ts","../../src/plugins/resolve-load-path.ts","../../src/plugins/should-log-plugin-loads.ts","../../src/plugins/loader.ts","../../src/bin/fluid.ts"],"sourcesContent":["/**\n * Helpers for constructing stored auth profiles.\n */\n\nimport type { FluidProfile } from \"../config/types.js\";\n\ninterface CreateFluidProfileInput {\n readonly name: string;\n readonly token: string;\n readonly companyName: string;\n readonly baseUrl: string;\n readonly storedAt?: string;\n}\n\nexport function createFluidProfile({\n name,\n token,\n companyName,\n baseUrl,\n storedAt,\n}: CreateFluidProfileInput): FluidProfile {\n return {\n name,\n token,\n companyName,\n storedAt: storedAt ?? new Date().toISOString(),\n baseUrl,\n };\n}\n","/**\n * fluid login — authenticate via email MFA or API token\n */\n\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport prompts from \"prompts\";\nimport ora from \"ora\";\nimport {\n validateToken,\n sendMfa,\n confirmMfa,\n getFluidApiBase,\n type CompanyChoice,\n} from \"../auth/fluid-api.js\";\nimport { createFluidProfile } from \"../auth/profiles.js\";\nimport { updateConfig, readConfig } from \"../config/config.js\";\n\nasync function confirmOverwrite(profileName: string): Promise<boolean> {\n const existing = readConfig().profiles[profileName];\n if (!existing) return true;\n\n const response = await prompts({\n type: \"confirm\",\n name: \"overwrite\",\n message: `Profile \"${profileName}\" already exists (${existing.companyName}). Overwrite?`,\n initial: false,\n });\n return Boolean(response[\"overwrite\"]);\n}\n\nfunction storeProfile(\n profileName: string,\n token: string,\n companyName: string,\n): void {\n updateConfig((config) => ({\n ...config,\n activeProfile: profileName,\n profiles: {\n ...config.profiles,\n [profileName]: createFluidProfile({\n name: profileName,\n token,\n companyName,\n baseUrl: getFluidApiBase(),\n }),\n },\n }));\n}\n\nasync function loginWithToken(\n token: string,\n profileName?: string,\n): Promise<void> {\n const spinner = ora(\"Validating token...\").start();\n const result = await validateToken(token);\n\n if (!result.success) {\n spinner.fail(chalk.red(result.error.message));\n if (result.error.details) {\n console.log(chalk.dim(result.error.details));\n }\n process.exitCode = 1;\n return;\n }\n\n const name = profileName ?? result.value.name;\n\n // Stop spinner before potential interactive prompt to avoid terminal corruption\n spinner.succeed(\n chalk.green(`Token valid — ${chalk.bold(result.value.name)}`),\n );\n\n if (!(await confirmOverwrite(name))) {\n console.log(\n chalk.yellow(\"Login cancelled — existing profile not overwritten.\"),\n );\n return;\n }\n\n storeProfile(name, token, result.value.name);\n console.log(\n chalk.green(\n `Logged in as ${chalk.bold(result.value.name)} (profile: ${chalk.bold(name)})`,\n ),\n );\n}\n\nasync function loginWithEmail(\n initialEmail?: string,\n profileName?: string,\n): Promise<void> {\n // 1. Prompt for email\n let email = initialEmail;\n if (!email) {\n const response = await prompts({\n type: \"text\",\n name: \"email\",\n message: \"Enter your email\",\n });\n email = response[\"email\"] as string | undefined;\n if (!email) {\n console.log(chalk.red(\"No email provided. Aborting.\"));\n process.exitCode = 1;\n return;\n }\n }\n\n // 2. Send MFA code\n const sendSpinner = ora(\"Sending verification code...\").start();\n const sendResult = await sendMfa(email);\n\n if (!sendResult.success) {\n sendSpinner.fail(chalk.red(sendResult.error.message));\n if (sendResult.error.details) {\n console.log(chalk.dim(sendResult.error.details));\n }\n process.exitCode = 1;\n return;\n }\n\n const expiresAt = new Date(sendResult.value.expiresAt);\n const expiresInMs = expiresAt.getTime() - Date.now();\n const expiresInMin = Math.round(expiresInMs / 1000 / 60);\n const expiryNote =\n expiresInMin > 0\n ? `expires in ~${expiresInMin} min`\n : \"code may expire soon — check your email quickly\";\n sendSpinner.succeed(\n `Verification code sent — check your email (${expiryNote})`,\n );\n\n // 3. Prompt for verification code (retry up to 3 times on invalid code)\n const MAX_CODE_ATTEMPTS = 3;\n let confirmResult;\n for (let attempt = 1; attempt <= MAX_CODE_ATTEMPTS; attempt++) {\n const codeResponse = await prompts({\n type: \"text\",\n name: \"code\",\n message: \"Enter the 6-digit code\",\n });\n const code = codeResponse[\"code\"] as string | undefined;\n if (!code) {\n console.log(chalk.red(\"No code provided. Aborting.\"));\n process.exitCode = 1;\n return;\n }\n if (!/^\\d{6}$/.test(code)) {\n console.log(chalk.red(\"Code must be exactly 6 digits.\"));\n if (attempt < MAX_CODE_ATTEMPTS) continue;\n console.log(chalk.red(\"Too many invalid attempts. Aborting.\"));\n process.exitCode = 1;\n return;\n }\n\n const confirmSpinner = ora(\"Verifying code...\").start();\n confirmResult = await confirmMfa(sendResult.value.uuid, code);\n\n if (confirmResult.success) {\n confirmSpinner.succeed(\"Verified\");\n break;\n }\n\n // Retryable: wrong code, still have attempts left\n if (\n confirmResult.error.code === \"INVALID_CODE\" &&\n attempt < MAX_CODE_ATTEMPTS\n ) {\n confirmSpinner.fail(\n chalk.red(\n `${confirmResult.error.message} (attempt ${attempt}/${MAX_CODE_ATTEMPTS})`,\n ),\n );\n continue;\n }\n\n // Non-retryable (expired, unreachable, etc.) or final attempt\n confirmSpinner.fail(chalk.red(confirmResult.error.message));\n if (confirmResult.error.details) {\n console.log(chalk.dim(confirmResult.error.details));\n }\n process.exitCode = 1;\n return;\n }\n\n if (!confirmResult?.success) return;\n\n const { companies } = confirmResult.value;\n\n if (companies.length === 0) {\n console.log(chalk.red(\"No companies found for this account.\"));\n process.exitCode = 1;\n return;\n }\n\n // 4. Select company (auto-select if only one)\n let selected: CompanyChoice;\n if (companies.length === 1) {\n const first = companies[0];\n if (!first) {\n console.log(chalk.red(\"No companies found for this account.\"));\n process.exitCode = 1;\n return;\n }\n selected = first;\n } else {\n // Key on `c.id`, not the array index, and resolve via `.find` —\n // `prompts` autocomplete returns the raw typed string on Enter when\n // the user hasn't arrow-key-highlighted a filtered choice, so an\n // array-index lookup ends up asking for `companies[\"Fluid\"]` and\n // failing with \"Invalid selection\". Same shape as `switch.ts`.\n const companyChoices = companies.map((c) => ({\n title: `${c.name} (${c.shopName})`,\n value: c.id,\n }));\n\n const selectResponse = await prompts({\n type: \"autocomplete\",\n name: \"companyId\",\n message: \"Select a company (type to search)\",\n choices: companyChoices,\n suggest: (input, choices) =>\n Promise.resolve(\n input\n ? choices.filter((c) =>\n c.title.toLowerCase().includes(input.toLowerCase()),\n )\n : choices,\n ),\n });\n\n const companyId = selectResponse[\"companyId\"] as number | undefined;\n\n if (companyId === undefined) {\n console.log(chalk.red(\"No company selected. Aborting.\"));\n process.exitCode = 1;\n return;\n }\n const choice = companies.find((c) => c.id === companyId);\n if (!choice) {\n console.log(chalk.red(\"Invalid selection. Aborting.\"));\n process.exitCode = 1;\n return;\n }\n selected = choice;\n }\n\n // 5. Store profile\n const name = profileName ?? selected.name;\n\n if (!(await confirmOverwrite(name))) {\n console.log(\n chalk.yellow(\"Login cancelled — existing profile not overwritten.\"),\n );\n return;\n }\n\n storeProfile(name, selected.jwt, selected.name);\n console.log(\n chalk.green(\n `Logged in as ${chalk.bold(selected.name)} (profile: ${chalk.bold(name)})`,\n ),\n );\n}\n\nexport const loginCommand = new Command(\"login\")\n .description(\"Authenticate with the Fluid API\")\n .option(\n \"-t, --token <token>\",\n \"API token (skips email flow). Prefer FLUID_TOKEN env var to avoid shell history exposure\",\n )\n .option(\"-e, --email <email>\", \"Email address for MFA login\")\n .option(\"-n, --name <name>\", \"Profile name (defaults to company name)\")\n .action(async (opts: { token?: string; email?: string; name?: string }) => {\n // Accept token from env var to avoid shell history / process listing exposure\n const token = opts.token ?? process.env[\"FLUID_TOKEN\"];\n if (token) {\n if (opts.token) {\n console.log(\n chalk.yellow(\n \"Warning: token passed via --token flag is visible in shell history and process listings. \" +\n \"Prefer the FLUID_TOKEN environment variable.\",\n ),\n );\n }\n await loginWithToken(token, opts.name);\n } else {\n await loginWithEmail(opts.email, opts.name);\n }\n });\n","/**\n * fluid logout — remove stored auth profile(s)\n */\n\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport { readConfig, writeConfig } from \"../config/config.js\";\n\nexport const logoutCommand = new Command(\"logout\")\n .description(\"Remove stored authentication\")\n .option(\"-a, --all\", \"Remove all profiles\")\n .action((opts: { all?: boolean }) => {\n const config = readConfig();\n\n if (opts.all) {\n writeConfig({\n ...config,\n activeProfile: null,\n profiles: {},\n });\n console.log(chalk.green(\"All profiles removed.\"));\n return;\n }\n\n if (!config.activeProfile) {\n console.log(chalk.yellow(\"Not currently logged in.\"));\n return;\n }\n\n const profileName = config.activeProfile;\n const { [profileName]: _, ...remainingProfiles } = config.profiles;\n\n // Pick the first remaining profile as active, or null\n const nextActive = Object.keys(remainingProfiles)[0] ?? null;\n\n writeConfig({\n ...config,\n activeProfile: nextActive,\n profiles: remainingProfiles,\n });\n\n console.log(\n chalk.green(`Logged out of profile ${chalk.bold(profileName)}.`),\n );\n if (nextActive) {\n console.log(chalk.dim(`Switched to profile ${nextActive}.`));\n }\n });\n","/**\n * fluid whoami — show current auth profile and validate against API\n */\n\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport ora from \"ora\";\nimport { getActiveProfile } from \"../auth/token.js\";\nimport { validateToken } from \"../auth/fluid-api.js\";\n\nexport const whoamiCommand = new Command(\"whoami\")\n .description(\"Show the current authenticated profile\")\n .action(async () => {\n const profile = getActiveProfile();\n\n if (!profile) {\n console.log(\n chalk.yellow(\"Not logged in. Run `fluid login` to authenticate.\"),\n );\n process.exitCode = 1;\n return;\n }\n\n const spinner = ora(\"Verifying token...\").start();\n const result = await validateToken(profile.token);\n\n if (!result.success) {\n spinner.fail(chalk.red(\"Token is no longer valid.\"));\n console.log(chalk.dim(`Profile: ${profile.name}`));\n console.log(chalk.dim(\"Run `fluid login` to re-authenticate.\"));\n process.exitCode = 1;\n return;\n }\n\n spinner.succeed(chalk.green(\"Authenticated\"));\n console.log(` Profile: ${chalk.bold(profile.name)}`);\n console.log(` Company: ${chalk.bold(result.value.name)}`);\n console.log(` Stored: ${chalk.dim(profile.storedAt)}`);\n });\n","/**\n * fluid switch — switch between companies (fetched from API, with local fallback)\n */\n\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport prompts from \"prompts\";\nimport ora from \"ora\";\nimport {\n fetchUserCompanies,\n switchCompany,\n FLUID_API_ERROR,\n getFluidApiBase,\n type UserCompany,\n} from \"../auth/fluid-api.js\";\nimport { createFluidProfile } from \"../auth/profiles.js\";\nimport { getActiveProfile } from \"../auth/token.js\";\nimport { readConfig, updateConfig } from \"../config/config.js\";\n\nasync function localSwitch(profileArg?: string): Promise<void> {\n const config = readConfig();\n const profileNames = Object.keys(config.profiles);\n\n if (profileNames.length === 0) {\n console.log(chalk.yellow(\"No profiles stored. Run `fluid login` first.\"));\n process.exitCode = 1;\n return;\n }\n\n let targetProfile = profileArg;\n\n if (!targetProfile) {\n const profileChoices = profileNames.map((name) => ({\n title: name === config.activeProfile ? `${name} (active)` : name,\n value: name,\n }));\n\n const response = await prompts({\n type: \"autocomplete\",\n name: \"profile\",\n message: \"Select a profile (type to search)\",\n choices: profileChoices,\n suggest: (input, choices) =>\n Promise.resolve(\n input\n ? choices.filter((c) =>\n c.value.toLowerCase().includes(input.toLowerCase()),\n )\n : choices,\n ),\n });\n targetProfile = response[\"profile\"] as string | undefined;\n\n if (!targetProfile) {\n console.log(chalk.dim(\"Cancelled.\"));\n return;\n }\n }\n\n if (!config.profiles[targetProfile]) {\n console.log(chalk.red(`Profile \"${targetProfile}\" not found.`));\n console.log(chalk.dim(`Available: ${profileNames.join(\", \")}`));\n process.exitCode = 1;\n return;\n }\n\n updateConfig((c) => ({ ...c, activeProfile: targetProfile! }));\n console.log(chalk.green(`Switched to profile ${chalk.bold(targetProfile)}.`));\n}\n\nasync function selectCompany(\n companies: UserCompany[],\n activeCompanyName: string | undefined,\n): Promise<UserCompany | undefined> {\n const companyChoices = companies.map((c) => ({\n title:\n c.name === activeCompanyName\n ? `${c.name} ${chalk.dim(\"(active)\")}`\n : c.name,\n value: c.id,\n }));\n\n const response = await prompts({\n type: \"autocomplete\",\n name: \"companyId\",\n message: \"Select a company (type to search)\",\n choices: companyChoices,\n suggest: (input, choices) =>\n Promise.resolve(\n input\n ? choices.filter((c) =>\n c.title.toLowerCase().includes(input.toLowerCase()),\n )\n : choices,\n ),\n });\n\n const companyId = response[\"companyId\"] as number | undefined;\n if (companyId === undefined) return undefined;\n return companies.find((c) => c.id === companyId);\n}\n\nasync function performSwitch(\n token: string,\n company: UserCompany,\n baseUrl?: string,\n): Promise<void> {\n const spinner = ora(`Switching to ${chalk.bold(company.name)}...`).start();\n const result = await switchCompany(token, company.id, baseUrl);\n\n if (!result.success) {\n if (result.error.code === FLUID_API_ERROR.INVALID_TOKEN.code) {\n spinner.fail(\n chalk.red(\n \"Your session has expired. Please run `fluid login` to re-authenticate.\",\n ),\n );\n } else {\n spinner.fail(chalk.red(result.error.message));\n if (result.error.details) {\n console.log(chalk.dim(result.error.details));\n }\n }\n process.exitCode = 1;\n return;\n }\n\n const { companyName, jwt } = result.value;\n\n updateConfig((config) => ({\n ...config,\n activeProfile: companyName,\n profiles: {\n ...config.profiles,\n [companyName]: createFluidProfile({\n name: companyName,\n token: jwt,\n companyName,\n baseUrl: baseUrl ?? getFluidApiBase(),\n }),\n },\n }));\n\n spinner.succeed(\n chalk.green(\n `Switched to ${chalk.bold(companyName)} (profile: ${chalk.bold(companyName)})`,\n ),\n );\n}\n\nexport const switchCommand = new Command(\"switch\")\n .description(\"Switch between companies\")\n .argument(\"[profile]\", \"Company or profile name to switch to\")\n .action(async (profileArg?: string) => {\n const activeProfile = getActiveProfile();\n\n if (!activeProfile) {\n console.log(chalk.yellow(\"Not logged in. Run `fluid login` first.\"));\n process.exitCode = 1;\n return;\n }\n\n const token = activeProfile.token;\n const activeCompanyName = activeProfile.companyName;\n const activeBaseUrl = activeProfile.baseUrl ?? getFluidApiBase();\n\n // Fetch companies from API\n const spinner = ora(\"Fetching companies...\").start();\n const result = await fetchUserCompanies(token, activeBaseUrl);\n\n if (!result.success) {\n if (result.error.code === FLUID_API_ERROR.INVALID_TOKEN.code) {\n spinner.fail(\n chalk.red(\n \"Your session has expired. Please run `fluid login` to re-authenticate.\",\n ),\n );\n process.exitCode = 1;\n return;\n }\n spinner.warn(\n chalk.yellow(\n `Could not fetch companies from API: ${result.error.message}`,\n ),\n );\n console.log(chalk.dim(\"Falling back to locally stored profiles.\"));\n await localSwitch(profileArg);\n return;\n }\n\n const companies = result.value;\n spinner.succeed(\n `Found ${companies.length} company${companies.length === 1 ? \"\" : \"ies\"}`,\n );\n\n if (companies.length === 0) {\n console.log(chalk.yellow(\"No companies found for this account.\"));\n process.exitCode = 1;\n return;\n }\n\n // If a profile argument was passed, match against API companies first\n if (profileArg) {\n const match = companies.find(\n (c) => c.name.toLowerCase() === profileArg.toLowerCase(),\n );\n\n if (match) {\n await performSwitch(token, match, activeBaseUrl);\n return;\n }\n\n // Fall back to local profile matching\n const config = readConfig();\n if (config.profiles[profileArg]) {\n updateConfig((c) => ({ ...c, activeProfile: profileArg! }));\n console.log(\n chalk.green(`Switched to profile ${chalk.bold(profileArg)}.`),\n );\n return;\n }\n\n console.log(chalk.red(`Company or profile \"${profileArg}\" not found.`));\n process.exitCode = 1;\n return;\n }\n\n // Interactive selection\n const selected = await selectCompany(companies, activeCompanyName);\n if (!selected) {\n console.log(chalk.dim(\"Cancelled.\"));\n return;\n }\n\n await performSwitch(token, selected, activeBaseUrl);\n });\n","/**\n * Auto-discover @fluid-app CLI plugins.\n *\n * Three discovery strategies run in order:\n *\n * 1a. **node_modules scan (cwd)** — look in `<cwd>/node_modules/@fluid-app/`\n * for directories whose names match the plugin naming convention.\n * This is the primary mechanism for project-local plugin installs.\n *\n * 1b. **node_modules scan (CLI install location)** — look in the\n * `node_modules/@fluid-app/` directory containing the CLI core package\n * itself. This covers global installs where plugins are sibling packages\n * under the same global `node_modules/`.\n *\n * 2. **Workspace scan** — walk upward from the CLI core package root to the\n * monorepo workspace root, then scan `packages/` for sibling plugin\n * packages. This covers the pnpm-workspace development case where\n * plugins are not symlinked into `node_modules`.\n *\n * Only first-party @fluid-app scoped packages are loaded.\n */\n\nimport { existsSync, readFileSync, readdirSync, realpathSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\n\nconst PLUGIN_SCOPE = \"@fluid-app\";\n\n/**\n * A plugin package name must match one of these patterns:\n * - @fluid-app/fluid-cli-* (standard)\n * - @fluid-app/*-cli-commands (v2025-06 style)\n */\nfunction isPluginName(packageName: string): boolean {\n if (!packageName.startsWith(`${PLUGIN_SCOPE}/`)) return false;\n const bare = packageName.slice(`${PLUGIN_SCOPE}/`.length);\n return bare.startsWith(\"fluid-cli-\") || bare.endsWith(\"-cli-commands\");\n}\n\n// ---------------------------------------------------------------------------\n// Strategy 1: node_modules scan (production / published installs)\n// ---------------------------------------------------------------------------\n\nfunction discoverFromNodeModules(basePath: string): DiscoveredPlugin[] {\n const scopeDir = join(basePath, \"node_modules\", PLUGIN_SCOPE);\n\n if (!existsSync(scopeDir)) return [];\n\n return readdirSync(scopeDir, { withFileTypes: true })\n .filter(\n (entry) =>\n (entry.isDirectory() || entry.isSymbolicLink()) &&\n isPluginName(`${PLUGIN_SCOPE}/${entry.name}`),\n )\n .map((entry) => {\n const name = `${PLUGIN_SCOPE}/${entry.name}`;\n const entryDir = join(scopeDir, entry.name);\n // Resolve symlinks (pnpm workspace links) to file:// URLs so the\n // dynamic import works regardless of pnpm's strict module resolution.\n try {\n const realDir = realpathSync(entryDir);\n const distEntry = join(realDir, \"dist\", \"index.mjs\");\n if (existsSync(distEntry)) {\n return { name, importSpecifier: pathToFileURL(distEntry).href };\n }\n } catch {\n // Broken symlink or other fs error — fall through to bare specifier\n }\n // Fallback to bare specifier for non-workspace (published) installs\n return { name, importSpecifier: name };\n });\n}\n\n// ---------------------------------------------------------------------------\n// Strategy 2: workspace scan (pnpm monorepo development)\n// ---------------------------------------------------------------------------\n\nfunction findWorkspaceRoot(startDir: string): string | null {\n let dir = startDir;\n while (true) {\n if (\n existsSync(join(dir, \"pnpm-workspace.yaml\")) ||\n existsSync(join(dir, \"pnpm-workspace.yml\"))\n ) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\nfunction readPackageName(dir: string): string | null {\n const pkgPath = join(dir, \"package.json\");\n if (!existsSync(pkgPath)) return null;\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\")) as {\n name?: string;\n };\n return pkg.name ?? null;\n } catch {\n return null;\n }\n}\n\nexport interface DiscoveredPlugin {\n name: string;\n importSpecifier: string;\n}\n\nexport interface PluginDiscoveryOptions {\n /**\n * Prefer plugins installed beside this CLI over same-named packages in the\n * target project's node_modules. Desktop callers use this so an older\n * project-local plugin cannot override the command contract bundled with\n * the app. Normal project-local CLI invocations keep the existing order.\n */\n preferInstallation?: boolean;\n /** Test seam for the directory containing the CLI installation. */\n installationBasePath?: string;\n}\n\nfunction discoverFromWorkspace(startDir: string): DiscoveredPlugin[] {\n const workspaceRoot = findWorkspaceRoot(startDir);\n if (!workspaceRoot) return [];\n\n const results: DiscoveredPlugin[] = [];\n const packagesDir = join(workspaceRoot, \"packages\");\n if (!existsSync(packagesDir)) return [];\n\n // Scan exactly two levels deep under packages/ (e.g. packages/cli/themes,\n // packages/cli/v2025-06). This matches the monorepo convention where all\n // packages live at packages/<domain>/<package>/.\n // If a plugin is added at a different depth, update this scan accordingly.\n let domainDirs: string[];\n try {\n domainDirs = readdirSync(packagesDir, { withFileTypes: true })\n .filter((e) => e.isDirectory())\n .map((e) => join(packagesDir, e.name));\n } catch {\n return [];\n }\n\n for (const domainDir of domainDirs) {\n let subDirs: string[];\n try {\n subDirs = readdirSync(domainDir, { withFileTypes: true })\n .filter((e) => e.isDirectory())\n .map((e) => join(domainDir, e.name));\n } catch {\n continue;\n }\n\n for (const subDir of subDirs) {\n const name = readPackageName(subDir);\n if (!name || !name.startsWith(`${PLUGIN_SCOPE}/`) || !isPluginName(name))\n continue;\n\n const distEntry = join(subDir, \"dist\", \"index.mjs\");\n if (!existsSync(distEntry)) {\n continue;\n }\n\n results.push({\n name,\n importSpecifier: pathToFileURL(distEntry).href,\n });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Exported API\n// ---------------------------------------------------------------------------\n\n/** Resolved once — the CLI core package root (`packages/cli/core/`). */\nexport const corePackageDir = dirname(\n dirname(dirname(fileURLToPath(import.meta.url))),\n);\n\n/**\n * Discover installed plugin packages. Returns a de-duplicated, sorted list\n * of `{ name, importSpecifier }` objects.\n *\n * @param basePath - Directory to scan for `node_modules/@fluid-app/`\n * @param searchFrom - Starting directory for workspace root detection\n * (walked upward via `findWorkspaceRoot`). Pass `null` to skip workspace\n * scanning (useful in tests). Defaults to `corePackageDir`.\n */\nexport function discoverPlugins(\n basePath: string,\n searchFrom?: string | null,\n options: PluginDiscoveryOptions = {},\n): DiscoveredPlugin[] {\n const seen = new Set<string>();\n const results: DiscoveredPlugin[] = [];\n\n const addPlugins = (plugins: DiscoveredPlugin[]) => {\n for (const plugin of plugins) {\n if (seen.has(plugin.name)) continue;\n seen.add(plugin.name);\n results.push(plugin);\n }\n };\n\n const cliParent =\n options.installationBasePath ?? dirname(dirname(dirname(corePackageDir)));\n const projectPlugins = () => discoverFromNodeModules(basePath);\n const installationPlugins = () =>\n cliParent === basePath ? [] : discoverFromNodeModules(cliParent);\n const workspacePlugins = () => {\n if (searchFrom === null) return [];\n return discoverFromWorkspace(searchFrom ?? corePackageDir);\n };\n\n if (options.preferInstallation) {\n addPlugins(installationPlugins());\n // In a monorepo development build, resolveFluidBinary() deliberately\n // realpaths the CLI into packages/cli/core. Its sibling plugins are then\n // discovered from the workspace rather than an installation-level\n // node_modules, so they are part of the same trusted CLI installation.\n addPlugins(workspacePlugins());\n addPlugins(projectPlugins());\n } else {\n addPlugins(projectPlugins());\n addPlugins(installationPlugins());\n addPlugins(workspacePlugins());\n }\n\n return results.sort((a, b) => a.name.localeCompare(b.name));\n}\n\n/**\n * Dynamically import a plugin module and return its default export.\n *\n * @internal Not intended for external callers. Only called with specifiers\n * produced by {@link discoverPlugins}:\n * - bare package names (e.g. `@fluid-app/fluid-cli-portal`) from the\n * node_modules scan, validated by {@link isPluginName}.\n * - `file://` URLs from {@link discoverFromWorkspace}, which constructs\n * them internally from validated workspace paths (`packages/` tree,\n * `dist/index.mjs`). These are trusted internal specifiers.\n */\nexport async function importPlugin(specifier: string): Promise<unknown> {\n // file:// URLs are trusted — they originate from discoverFromWorkspace\n // which builds them from validated workspace paths under packages/.\n // For bare package names, verify they match the plugin naming convention.\n if (!specifier.startsWith(\"file://\") && !isPluginName(specifier)) {\n throw new Error(`Refusing to import non-plugin package: ${specifier}`);\n }\n const mod = (await import(specifier)) as Record<string, unknown>;\n return mod[\"default\"] ?? mod;\n}\n","/**\n * Format one-line diagnostics naming a CLI plugin build and where it was\n * resolved from. The resolved path is the load-bearing detail: a workspace\n * build and a published copy share the same name and version, so the path is\n * the only thing that distinguishes them. See CURRENT-3218.\n */\n\nimport { fileURLToPath } from \"node:url\";\n\nexport interface PluginLoadInfo {\n readonly name: string;\n readonly version: string;\n /**\n * Where the plugin was resolved from — an absolute `file://` URL (workspace\n * or published install, resolved upstream) or, only when resolution failed,\n * a bare package name.\n */\n readonly importSpecifier: string;\n /**\n * Whether installation-bundled plugins were preferred over project-local\n * ones (`FLUID_CLI_PREFER_INSTALLATION_PLUGINS=1`, set by Mist).\n */\n readonly preferInstallation: boolean;\n}\n\nexport interface PluginLoadFailureInfo {\n readonly name: string;\n readonly importSpecifier: string;\n readonly preferInstallation: boolean;\n readonly message: string;\n}\n\nexport function modeLabel(preferInstallation: boolean): string {\n return preferInstallation ? \"installation-preferred\" : \"project-first\";\n}\n\n/**\n * Render a specifier for display: `file://` URLs become absolute filesystem\n * paths; anything else is shown verbatim. Never throws — a malformed `file://`\n * specifier falls back to its raw form so a diagnostic can never turn a\n * successful load into a reported failure.\n */\nexport function displaySource(importSpecifier: string): string {\n if (!importSpecifier.startsWith(\"file://\")) return importSpecifier;\n try {\n return fileURLToPath(importSpecifier);\n } catch {\n return importSpecifier;\n }\n}\n\nexport function formatPluginLoadLine(info: PluginLoadInfo): string {\n return `Loaded plugin ${info.name}@${info.version} from ${displaySource(\n info.importSpecifier,\n )} [${modeLabel(info.preferInstallation)}]`;\n}\n\nexport function formatPluginLoadFailure(info: PluginLoadFailureInfo): string {\n return `Failed to load plugin ${info.name} from ${displaySource(\n info.importSpecifier,\n )} [${modeLabel(info.preferInstallation)}]: ${info.message}`;\n}\n","/**\n * Resolve a plugin import specifier to a concrete path for diagnostics.\n *\n * The node_modules discovery strategy falls back to a bare package name when it\n * cannot realpath a workspace symlink — precisely the published-install case\n * where the path is the only thing that would reveal a stale plugin winning.\n * Resolving the bare specifier recovers that path. See CURRENT-3218.\n */\n\nexport type Resolver = (specifier: string) => string;\n\nconst defaultResolver: Resolver = (specifier) => import.meta.resolve(specifier);\n\n/**\n * Return a `file://` URL for `importSpecifier`. `file://` specifiers (already\n * resolved by discovery) pass through untouched; bare specifiers are resolved\n * via `resolve`. If resolution throws, the bare specifier is returned so a\n * diagnostic degrades gracefully rather than failing the load.\n */\nexport function resolveLoadPath(\n importSpecifier: string,\n resolve: Resolver = defaultResolver,\n): string {\n if (importSpecifier.startsWith(\"file://\")) return importSpecifier;\n try {\n return resolve(importSpecifier);\n } catch {\n return importSpecifier;\n }\n}\n","/**\n * Decide whether to print the per-plugin \"Loaded plugin …\" diagnostics.\n *\n * A fluid-mono checkout resolves ~a dozen plugin packages, so emitting a line\n * for each on every command (including `--help` and tab-completion, and into\n * Mist's captured stderr dialog) is noise. The success diagnostics are gated\n * behind an explicit opt-in; load *failures* are always surfaced by the loader\n * regardless of this flag. See CURRENT-3218.\n */\n\nfunction isTruthyFlag(value: string | undefined): boolean {\n return value === \"1\" || value === \"true\";\n}\n\nexport function shouldLogPluginLoads(\n argv: readonly string[] = process.argv,\n env: NodeJS.ProcessEnv = process.env,\n): boolean {\n if (argv.includes(\"--verbose\")) return true;\n return isTruthyFlag(env[\"FLUID_CLI_DEBUG\"] ?? env[\"FLUID_CLI_VERBOSE\"]);\n}\n","/**\n * Plugin loader — orchestrates discovery and registration\n */\n\nimport type { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport { getConfigDir } from \"../config/paths.js\";\nimport { getAuthToken } from \"../auth/token.js\";\nimport { readConfig } from \"../config/config.js\";\nimport type { FluidPlugin, PluginContext } from \"./types.js\";\nimport { discoverPlugins, importPlugin } from \"./discovery.js\";\nimport {\n formatPluginLoadLine,\n formatPluginLoadFailure,\n} from \"./format-load-line.js\";\nimport { resolveLoadPath } from \"./resolve-load-path.js\";\nimport { shouldLogPluginLoads } from \"./should-log-plugin-loads.js\";\n\nfunction isFluidPlugin(value: unknown): value is FluidPlugin {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return (\n typeof obj[\"name\"] === \"string\" &&\n typeof obj[\"version\"] === \"string\" &&\n typeof obj[\"register\"] === \"function\"\n );\n}\n\n/**\n * Discover, import, and register all installed plugins\n */\nexport async function loadPlugins(\n program: Command,\n basePath: string,\n): Promise<void> {\n const preferInstallation =\n process.env[\"FLUID_CLI_PREFER_INSTALLATION_PLUGINS\"] === \"1\";\n const discovered = discoverPlugins(basePath, undefined, {\n preferInstallation,\n });\n const { enabledPlugins } = readConfig();\n\n // If enabledPlugins is set, only load explicitly allowed plugins.\n // This acts as an allow-list to prevent accidental supply-chain execution.\n const allowedSet = enabledPlugins !== null ? new Set(enabledPlugins) : null;\n const plugins = allowedSet\n ? discovered.filter((p) => allowedSet.has(p.name))\n : discovered;\n\n if (plugins.length === 0) return;\n\n const ctx: PluginContext = {\n program,\n getAuthToken,\n configDir: getConfigDir(),\n };\n\n // Success diagnostics are opt-in (a checkout resolves ~a dozen plugins);\n // load failures below are always surfaced. See CURRENT-3218.\n const verbose = shouldLogPluginLoads();\n\n for (const { name, importSpecifier } of plugins) {\n // Resolve to a concrete path so a published install is distinguishable\n // from a workspace build at a glance.\n const source = resolveLoadPath(importSpecifier);\n try {\n const exported = await importPlugin(importSpecifier);\n\n if (!isFluidPlugin(exported)) {\n console.warn(\n chalk.yellow(`Warning: ${name} does not export a valid FluidPlugin`),\n );\n continue;\n }\n\n await exported.register(ctx);\n\n if (verbose) {\n console.error(\n chalk.dim(\n formatPluginLoadLine({\n name: exported.name,\n version: exported.version,\n importSpecifier: source,\n preferInstallation,\n }),\n ),\n );\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n // Always name what failed to load and from where — this is exactly the\n // invisible-artifact failure the diagnostic exists to catch.\n console.warn(\n chalk.yellow(\n `Warning: ${formatPluginLoadFailure({\n name,\n importSpecifier: source,\n preferInstallation,\n message,\n })}`,\n ),\n );\n }\n }\n}\n","#!/usr/bin/env node\n\nimport { createRequire } from \"node:module\";\nimport { Command } from \"commander\";\nimport { loginCommand } from \"../commands/login.js\";\nimport { logoutCommand } from \"../commands/logout.js\";\nimport { whoamiCommand } from \"../commands/whoami.js\";\nimport { switchCommand } from \"../commands/switch.js\";\nimport { loadPlugins } from \"../plugins/loader.js\";\n\nconst require = createRequire(import.meta.url);\nconst { version } = require(\"../../package.json\") as { version: string };\n\nconst packageRoot = process.cwd();\n\nconst program = new Command();\n\nprogram.name(\"fluid\").description(\"Fluid Commerce CLI\").version(version);\n\n// Built-in auth commands\nprogram.addCommand(loginCommand);\nprogram.addCommand(logoutCommand);\nprogram.addCommand(whoamiCommand);\nprogram.addCommand(switchCommand);\n\n// Discover and load all plugins (auto-discovered from node_modules and workspace)\nawait loadPlugins(program, packageRoot);\n\nprogram.parse(process.argv, { from: \"node\" });\n"],"mappings":"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"}
1
+ {"version":3,"file":"fluid.mjs","names":[],"sources":["../../src/auth/profiles.ts","../../src/commands/login.ts","../../src/commands/logout.ts","../../src/commands/whoami.ts","../../src/commands/switch.ts","../../src/plugins/discovery.ts","../../src/plugins/format-load-line.ts","../../src/plugins/resolve-load-path.ts","../../src/plugins/should-log-plugin-loads.ts","../../src/plugins/loader.ts","../../src/bin/fluid.ts"],"sourcesContent":["/**\n * Helpers for constructing stored auth profiles.\n */\n\nimport type { FluidProfile } from \"../config/types.js\";\n\ninterface CreateFluidProfileInput {\n readonly name: string;\n readonly token: string;\n readonly companyName: string;\n readonly baseUrl: string;\n readonly storedAt?: string;\n}\n\nexport function createFluidProfile({\n name,\n token,\n companyName,\n baseUrl,\n storedAt,\n}: CreateFluidProfileInput): FluidProfile {\n return {\n name,\n token,\n companyName,\n storedAt: storedAt ?? new Date().toISOString(),\n baseUrl,\n };\n}\n","/**\n * fluid login — authenticate via email MFA or API token\n */\n\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport prompts from \"prompts\";\nimport ora from \"ora\";\nimport {\n validateToken,\n sendMfa,\n confirmMfa,\n getFluidApiBase,\n type CompanyChoice,\n} from \"../auth/fluid-api.js\";\nimport { createFluidProfile } from \"../auth/profiles.js\";\nimport { updateConfig, readConfig } from \"../config/config.js\";\n\nasync function confirmOverwrite(profileName: string): Promise<boolean> {\n const existing = readConfig().profiles[profileName];\n if (!existing) return true;\n\n const response = await prompts({\n type: \"confirm\",\n name: \"overwrite\",\n message: `Profile \"${profileName}\" already exists (${existing.companyName}). Overwrite?`,\n initial: false,\n });\n return Boolean(response[\"overwrite\"]);\n}\n\nfunction storeProfile(\n profileName: string,\n token: string,\n companyName: string,\n): void {\n updateConfig((config) => ({\n ...config,\n activeProfile: profileName,\n profiles: {\n ...config.profiles,\n [profileName]: createFluidProfile({\n name: profileName,\n token,\n companyName,\n baseUrl: getFluidApiBase(),\n }),\n },\n }));\n}\n\nasync function loginWithToken(\n token: string,\n profileName?: string,\n): Promise<void> {\n const spinner = ora(\"Validating token...\").start();\n const result = await validateToken(token);\n\n if (!result.success) {\n spinner.fail(chalk.red(result.error.message));\n if (result.error.details) {\n console.log(chalk.dim(result.error.details));\n }\n process.exitCode = 1;\n return;\n }\n\n const name = profileName ?? result.value.name;\n\n // Stop spinner before potential interactive prompt to avoid terminal corruption\n spinner.succeed(\n chalk.green(`Token valid — ${chalk.bold(result.value.name)}`),\n );\n\n if (!(await confirmOverwrite(name))) {\n console.log(\n chalk.yellow(\"Login cancelled — existing profile not overwritten.\"),\n );\n return;\n }\n\n storeProfile(name, token, result.value.name);\n console.log(\n chalk.green(\n `Logged in as ${chalk.bold(result.value.name)} (profile: ${chalk.bold(name)})`,\n ),\n );\n}\n\nasync function loginWithEmail(\n initialEmail?: string,\n profileName?: string,\n): Promise<void> {\n // 1. Prompt for email\n let email = initialEmail;\n if (!email) {\n const response = await prompts({\n type: \"text\",\n name: \"email\",\n message: \"Enter your email\",\n });\n email = response[\"email\"] as string | undefined;\n if (!email) {\n console.log(chalk.red(\"No email provided. Aborting.\"));\n process.exitCode = 1;\n return;\n }\n }\n\n // 2. Send MFA code\n const sendSpinner = ora(\"Sending verification code...\").start();\n const sendResult = await sendMfa(email);\n\n if (!sendResult.success) {\n sendSpinner.fail(chalk.red(sendResult.error.message));\n if (sendResult.error.details) {\n console.log(chalk.dim(sendResult.error.details));\n }\n process.exitCode = 1;\n return;\n }\n\n const expiresAt = new Date(sendResult.value.expiresAt);\n const expiresInMs = expiresAt.getTime() - Date.now();\n const expiresInMin = Math.round(expiresInMs / 1000 / 60);\n const expiryNote =\n expiresInMin > 0\n ? `expires in ~${expiresInMin} min`\n : \"code may expire soon — check your email quickly\";\n sendSpinner.succeed(\n `Verification code sent — check your email (${expiryNote})`,\n );\n\n // 3. Prompt for verification code (retry up to 3 times on invalid code)\n const MAX_CODE_ATTEMPTS = 3;\n let confirmResult;\n for (let attempt = 1; attempt <= MAX_CODE_ATTEMPTS; attempt++) {\n const codeResponse = await prompts({\n type: \"text\",\n name: \"code\",\n message: \"Enter the 6-digit code\",\n });\n const code = codeResponse[\"code\"] as string | undefined;\n if (!code) {\n console.log(chalk.red(\"No code provided. Aborting.\"));\n process.exitCode = 1;\n return;\n }\n if (!/^\\d{6}$/.test(code)) {\n console.log(chalk.red(\"Code must be exactly 6 digits.\"));\n if (attempt < MAX_CODE_ATTEMPTS) continue;\n console.log(chalk.red(\"Too many invalid attempts. Aborting.\"));\n process.exitCode = 1;\n return;\n }\n\n const confirmSpinner = ora(\"Verifying code...\").start();\n confirmResult = await confirmMfa(sendResult.value.uuid, code);\n\n if (confirmResult.success) {\n confirmSpinner.succeed(\"Verified\");\n break;\n }\n\n // Retryable: wrong code, still have attempts left\n if (\n confirmResult.error.code === \"INVALID_CODE\" &&\n attempt < MAX_CODE_ATTEMPTS\n ) {\n confirmSpinner.fail(\n chalk.red(\n `${confirmResult.error.message} (attempt ${attempt}/${MAX_CODE_ATTEMPTS})`,\n ),\n );\n continue;\n }\n\n // Non-retryable (expired, unreachable, etc.) or final attempt\n confirmSpinner.fail(chalk.red(confirmResult.error.message));\n if (confirmResult.error.details) {\n console.log(chalk.dim(confirmResult.error.details));\n }\n process.exitCode = 1;\n return;\n }\n\n if (!confirmResult?.success) return;\n\n const { companies } = confirmResult.value;\n\n if (companies.length === 0) {\n console.log(chalk.red(\"No companies found for this account.\"));\n process.exitCode = 1;\n return;\n }\n\n // 4. Select company (auto-select if only one)\n let selected: CompanyChoice;\n if (companies.length === 1) {\n const first = companies[0];\n if (!first) {\n console.log(chalk.red(\"No companies found for this account.\"));\n process.exitCode = 1;\n return;\n }\n selected = first;\n } else {\n // Key on `c.id`, not the array index, and resolve via `.find` —\n // `prompts` autocomplete returns the raw typed string on Enter when\n // the user hasn't arrow-key-highlighted a filtered choice, so an\n // array-index lookup ends up asking for `companies[\"Fluid\"]` and\n // failing with \"Invalid selection\". Same shape as `switch.ts`.\n const companyChoices = companies.map((c) => ({\n title: `${c.name} (${c.shopName})`,\n value: c.id,\n }));\n\n const selectResponse = await prompts({\n type: \"autocomplete\",\n name: \"companyId\",\n message: \"Select a company (type to search)\",\n choices: companyChoices,\n suggest: (input, choices) =>\n Promise.resolve(\n input\n ? choices.filter((c) =>\n c.title.toLowerCase().includes(input.toLowerCase()),\n )\n : choices,\n ),\n });\n\n const companyId = selectResponse[\"companyId\"] as number | undefined;\n\n if (companyId === undefined) {\n console.log(chalk.red(\"No company selected. Aborting.\"));\n process.exitCode = 1;\n return;\n }\n const choice = companies.find((c) => c.id === companyId);\n if (!choice) {\n console.log(chalk.red(\"Invalid selection. Aborting.\"));\n process.exitCode = 1;\n return;\n }\n selected = choice;\n }\n\n // 5. Store profile\n const name = profileName ?? selected.name;\n\n if (!(await confirmOverwrite(name))) {\n console.log(\n chalk.yellow(\"Login cancelled — existing profile not overwritten.\"),\n );\n return;\n }\n\n storeProfile(name, selected.jwt, selected.name);\n console.log(\n chalk.green(\n `Logged in as ${chalk.bold(selected.name)} (profile: ${chalk.bold(name)})`,\n ),\n );\n}\n\nexport const loginCommand = new Command(\"login\")\n .description(\"Authenticate with the Fluid API\")\n .option(\n \"-t, --token <token>\",\n \"API token (skips email flow). Prefer FLUID_TOKEN env var to avoid shell history exposure\",\n )\n .option(\"-e, --email <email>\", \"Email address for MFA login\")\n .option(\"-n, --name <name>\", \"Profile name (defaults to company name)\")\n .action(async (opts: { token?: string; email?: string; name?: string }) => {\n // Accept token from env var to avoid shell history / process listing exposure\n const token = opts.token ?? process.env[\"FLUID_TOKEN\"];\n if (token) {\n if (opts.token) {\n console.log(\n chalk.yellow(\n \"Warning: token passed via --token flag is visible in shell history and process listings. \" +\n \"Prefer the FLUID_TOKEN environment variable.\",\n ),\n );\n }\n await loginWithToken(token, opts.name);\n } else {\n await loginWithEmail(opts.email, opts.name);\n }\n });\n","/**\n * fluid logout — remove stored auth profile(s)\n */\n\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport { readConfig, writeConfig } from \"../config/config.js\";\n\nexport const logoutCommand = new Command(\"logout\")\n .description(\"Remove stored authentication\")\n .option(\"-a, --all\", \"Remove all profiles\")\n .action((opts: { all?: boolean }) => {\n const config = readConfig();\n\n if (opts.all) {\n writeConfig({\n ...config,\n activeProfile: null,\n profiles: {},\n });\n console.log(chalk.green(\"All profiles removed.\"));\n return;\n }\n\n if (!config.activeProfile) {\n console.log(chalk.yellow(\"Not currently logged in.\"));\n return;\n }\n\n const profileName = config.activeProfile;\n const { [profileName]: _, ...remainingProfiles } = config.profiles;\n\n // Pick the first remaining profile as active, or null\n const nextActive = Object.keys(remainingProfiles)[0] ?? null;\n\n writeConfig({\n ...config,\n activeProfile: nextActive,\n profiles: remainingProfiles,\n });\n\n console.log(\n chalk.green(`Logged out of profile ${chalk.bold(profileName)}.`),\n );\n if (nextActive) {\n console.log(chalk.dim(`Switched to profile ${nextActive}.`));\n }\n });\n","/**\n * fluid whoami — show current auth profile and validate against API\n */\n\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport ora from \"ora\";\nimport { getActiveProfile } from \"../auth/token.js\";\nimport { validateToken } from \"../auth/fluid-api.js\";\n\nexport const whoamiCommand = new Command(\"whoami\")\n .description(\"Show the current authenticated profile\")\n .action(async () => {\n const profile = getActiveProfile();\n\n if (!profile) {\n console.log(\n chalk.yellow(\"Not logged in. Run `fluid login` to authenticate.\"),\n );\n process.exitCode = 1;\n return;\n }\n\n const spinner = ora(\"Verifying token...\").start();\n const result = await validateToken(profile.token);\n\n if (!result.success) {\n spinner.fail(chalk.red(\"Token is no longer valid.\"));\n console.log(chalk.dim(`Profile: ${profile.name}`));\n console.log(chalk.dim(\"Run `fluid login` to re-authenticate.\"));\n process.exitCode = 1;\n return;\n }\n\n spinner.succeed(chalk.green(\"Authenticated\"));\n console.log(` Profile: ${chalk.bold(profile.name)}`);\n console.log(` Company: ${chalk.bold(result.value.name)}`);\n console.log(` Stored: ${chalk.dim(profile.storedAt)}`);\n });\n","/**\n * fluid switch — switch between companies (fetched from API, with local fallback)\n */\n\nimport { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport prompts from \"prompts\";\nimport ora from \"ora\";\nimport {\n fetchUserCompanies,\n switchCompany,\n FLUID_API_ERROR,\n getFluidApiBase,\n type UserCompany,\n} from \"../auth/fluid-api.js\";\nimport { createFluidProfile } from \"../auth/profiles.js\";\nimport { getActiveProfile } from \"../auth/token.js\";\nimport { readConfig, updateConfig } from \"../config/config.js\";\n\nasync function localSwitch(profileArg?: string): Promise<void> {\n const config = readConfig();\n const profileNames = Object.keys(config.profiles);\n\n if (profileNames.length === 0) {\n console.log(chalk.yellow(\"No profiles stored. Run `fluid login` first.\"));\n process.exitCode = 1;\n return;\n }\n\n let targetProfile = profileArg;\n\n if (!targetProfile) {\n const profileChoices = profileNames.map((name) => ({\n title: name === config.activeProfile ? `${name} (active)` : name,\n value: name,\n }));\n\n const response = await prompts({\n type: \"autocomplete\",\n name: \"profile\",\n message: \"Select a profile (type to search)\",\n choices: profileChoices,\n suggest: (input, choices) =>\n Promise.resolve(\n input\n ? choices.filter((c) =>\n c.value.toLowerCase().includes(input.toLowerCase()),\n )\n : choices,\n ),\n });\n targetProfile = response[\"profile\"] as string | undefined;\n\n if (!targetProfile) {\n console.log(chalk.dim(\"Cancelled.\"));\n return;\n }\n }\n\n if (!config.profiles[targetProfile]) {\n console.log(chalk.red(`Profile \"${targetProfile}\" not found.`));\n console.log(chalk.dim(`Available: ${profileNames.join(\", \")}`));\n process.exitCode = 1;\n return;\n }\n\n updateConfig((c) => ({ ...c, activeProfile: targetProfile! }));\n console.log(chalk.green(`Switched to profile ${chalk.bold(targetProfile)}.`));\n}\n\nasync function selectCompany(\n companies: UserCompany[],\n activeCompanyName: string | undefined,\n): Promise<UserCompany | undefined> {\n const companyChoices = companies.map((c) => ({\n title:\n c.name === activeCompanyName\n ? `${c.name} ${chalk.dim(\"(active)\")}`\n : c.name,\n value: c.id,\n }));\n\n const response = await prompts({\n type: \"autocomplete\",\n name: \"companyId\",\n message: \"Select a company (type to search)\",\n choices: companyChoices,\n suggest: (input, choices) =>\n Promise.resolve(\n input\n ? choices.filter((c) =>\n c.title.toLowerCase().includes(input.toLowerCase()),\n )\n : choices,\n ),\n });\n\n const companyId = response[\"companyId\"] as number | undefined;\n if (companyId === undefined) return undefined;\n return companies.find((c) => c.id === companyId);\n}\n\nasync function performSwitch(\n token: string,\n company: UserCompany,\n baseUrl?: string,\n): Promise<void> {\n const spinner = ora(`Switching to ${chalk.bold(company.name)}...`).start();\n const result = await switchCompany(token, company.id, baseUrl);\n\n if (!result.success) {\n if (result.error.code === FLUID_API_ERROR.INVALID_TOKEN.code) {\n spinner.fail(\n chalk.red(\n \"Your session has expired. Please run `fluid login` to re-authenticate.\",\n ),\n );\n } else {\n spinner.fail(chalk.red(result.error.message));\n if (result.error.details) {\n console.log(chalk.dim(result.error.details));\n }\n }\n process.exitCode = 1;\n return;\n }\n\n const { companyName, jwt } = result.value;\n\n updateConfig((config) => ({\n ...config,\n activeProfile: companyName,\n profiles: {\n ...config.profiles,\n [companyName]: createFluidProfile({\n name: companyName,\n token: jwt,\n companyName,\n baseUrl: baseUrl ?? getFluidApiBase(),\n }),\n },\n }));\n\n spinner.succeed(\n chalk.green(\n `Switched to ${chalk.bold(companyName)} (profile: ${chalk.bold(companyName)})`,\n ),\n );\n}\n\nexport const switchCommand = new Command(\"switch\")\n .description(\"Switch between companies\")\n .argument(\"[profile]\", \"Company or profile name to switch to\")\n .action(async (profileArg?: string) => {\n const activeProfile = getActiveProfile();\n\n if (!activeProfile) {\n console.log(chalk.yellow(\"Not logged in. Run `fluid login` first.\"));\n process.exitCode = 1;\n return;\n }\n\n const token = activeProfile.token;\n const activeCompanyName = activeProfile.companyName;\n const activeBaseUrl = activeProfile.baseUrl ?? getFluidApiBase();\n\n // Fetch companies from API\n const spinner = ora(\"Fetching companies...\").start();\n const result = await fetchUserCompanies(token, activeBaseUrl);\n\n if (!result.success) {\n if (result.error.code === FLUID_API_ERROR.INVALID_TOKEN.code) {\n spinner.fail(\n chalk.red(\n \"Your session has expired. Please run `fluid login` to re-authenticate.\",\n ),\n );\n process.exitCode = 1;\n return;\n }\n spinner.warn(\n chalk.yellow(\n `Could not fetch companies from API: ${result.error.message}`,\n ),\n );\n console.log(chalk.dim(\"Falling back to locally stored profiles.\"));\n await localSwitch(profileArg);\n return;\n }\n\n const companies = result.value;\n spinner.succeed(\n `Found ${companies.length} company${companies.length === 1 ? \"\" : \"ies\"}`,\n );\n\n if (companies.length === 0) {\n console.log(chalk.yellow(\"No companies found for this account.\"));\n process.exitCode = 1;\n return;\n }\n\n // If a profile argument was passed, match against API companies first\n if (profileArg) {\n const match = companies.find(\n (c) => c.name.toLowerCase() === profileArg.toLowerCase(),\n );\n\n if (match) {\n await performSwitch(token, match, activeBaseUrl);\n return;\n }\n\n // Fall back to local profile matching\n const config = readConfig();\n if (config.profiles[profileArg]) {\n updateConfig((c) => ({ ...c, activeProfile: profileArg! }));\n console.log(\n chalk.green(`Switched to profile ${chalk.bold(profileArg)}.`),\n );\n return;\n }\n\n console.log(chalk.red(`Company or profile \"${profileArg}\" not found.`));\n process.exitCode = 1;\n return;\n }\n\n // Interactive selection\n const selected = await selectCompany(companies, activeCompanyName);\n if (!selected) {\n console.log(chalk.dim(\"Cancelled.\"));\n return;\n }\n\n await performSwitch(token, selected, activeBaseUrl);\n });\n","/**\n * Auto-discover @fluid-app CLI plugins.\n *\n * Three discovery strategies run in order:\n *\n * 1a. **node_modules scan (cwd)** — look in `<cwd>/node_modules/@fluid-app/`\n * for directories whose names match the plugin naming convention.\n * This is the primary mechanism for project-local plugin installs.\n *\n * 1b. **node_modules scan (CLI install location)** — look in the\n * `node_modules/@fluid-app/` directory containing the CLI core package\n * itself. This covers global installs where plugins are sibling packages\n * under the same global `node_modules/`.\n *\n * 2. **Workspace scan** — walk upward from the CLI core package root to the\n * monorepo workspace root, then scan `packages/` for sibling plugin\n * packages. This covers the pnpm-workspace development case where\n * plugins are not symlinked into `node_modules`.\n *\n * Only first-party @fluid-app scoped packages are loaded.\n */\n\nimport { existsSync, readFileSync, readdirSync, realpathSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\n\nconst PLUGIN_SCOPE = \"@fluid-app\";\n\n/**\n * A plugin package name must match one of these patterns:\n * - @fluid-app/fluid-cli-* (standard)\n * - @fluid-app/*-cli-commands (v2025-06 style)\n */\nfunction isPluginName(packageName: string): boolean {\n if (!packageName.startsWith(`${PLUGIN_SCOPE}/`)) return false;\n const bare = packageName.slice(`${PLUGIN_SCOPE}/`.length);\n return bare.startsWith(\"fluid-cli-\") || bare.endsWith(\"-cli-commands\");\n}\n\n// ---------------------------------------------------------------------------\n// Strategy 1: node_modules scan (production / published installs)\n// ---------------------------------------------------------------------------\n\nfunction discoverFromNodeModules(basePath: string): DiscoveredPlugin[] {\n const scopeDir = join(basePath, \"node_modules\", PLUGIN_SCOPE);\n\n if (!existsSync(scopeDir)) return [];\n\n return readdirSync(scopeDir, { withFileTypes: true })\n .filter(\n (entry) =>\n (entry.isDirectory() || entry.isSymbolicLink()) &&\n isPluginName(`${PLUGIN_SCOPE}/${entry.name}`),\n )\n .map((entry) => {\n const name = `${PLUGIN_SCOPE}/${entry.name}`;\n const entryDir = join(scopeDir, entry.name);\n // Resolve symlinks (pnpm workspace links) to file:// URLs so the\n // dynamic import works regardless of pnpm's strict module resolution.\n try {\n const realDir = realpathSync(entryDir);\n const distEntry = join(realDir, \"dist\", \"index.mjs\");\n if (existsSync(distEntry)) {\n return { name, importSpecifier: pathToFileURL(distEntry).href };\n }\n } catch {\n // Broken symlink or other fs error — fall through to bare specifier\n }\n // Fallback to bare specifier for non-workspace (published) installs\n return { name, importSpecifier: name };\n });\n}\n\n// ---------------------------------------------------------------------------\n// Strategy 2: workspace scan (pnpm monorepo development)\n// ---------------------------------------------------------------------------\n\nfunction findWorkspaceRoot(startDir: string): string | null {\n let dir = startDir;\n while (true) {\n if (\n existsSync(join(dir, \"pnpm-workspace.yaml\")) ||\n existsSync(join(dir, \"pnpm-workspace.yml\"))\n ) {\n return dir;\n }\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\nfunction readPackageName(dir: string): string | null {\n const pkgPath = join(dir, \"package.json\");\n if (!existsSync(pkgPath)) return null;\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\")) as {\n name?: string;\n };\n return pkg.name ?? null;\n } catch {\n return null;\n }\n}\n\nexport interface DiscoveredPlugin {\n name: string;\n importSpecifier: string;\n}\n\nexport interface PluginDiscoveryOptions {\n /**\n * Prefer plugins installed beside this CLI over same-named packages in the\n * target project's node_modules. Desktop callers use this so an older\n * project-local plugin cannot override the command contract bundled with\n * the app. Normal project-local CLI invocations keep the existing order.\n */\n preferInstallation?: boolean;\n /** Test seam for the directory containing the CLI installation. */\n installationBasePath?: string;\n}\n\nfunction discoverFromWorkspace(startDir: string): DiscoveredPlugin[] {\n const workspaceRoot = findWorkspaceRoot(startDir);\n if (!workspaceRoot) return [];\n\n const results: DiscoveredPlugin[] = [];\n const packagesDir = join(workspaceRoot, \"packages\");\n if (!existsSync(packagesDir)) return [];\n\n // Scan exactly two levels deep under packages/ (e.g. packages/cli/themes,\n // packages/cli/v2025-06). This matches the monorepo convention where all\n // packages live at packages/<domain>/<package>/.\n // If a plugin is added at a different depth, update this scan accordingly.\n let domainDirs: string[];\n try {\n domainDirs = readdirSync(packagesDir, { withFileTypes: true })\n .filter((e) => e.isDirectory())\n .map((e) => join(packagesDir, e.name));\n } catch {\n return [];\n }\n\n for (const domainDir of domainDirs) {\n let subDirs: string[];\n try {\n subDirs = readdirSync(domainDir, { withFileTypes: true })\n .filter((e) => e.isDirectory())\n .map((e) => join(domainDir, e.name));\n } catch {\n continue;\n }\n\n for (const subDir of subDirs) {\n const name = readPackageName(subDir);\n if (!name || !name.startsWith(`${PLUGIN_SCOPE}/`) || !isPluginName(name))\n continue;\n\n const distEntry = join(subDir, \"dist\", \"index.mjs\");\n if (!existsSync(distEntry)) {\n continue;\n }\n\n results.push({\n name,\n importSpecifier: pathToFileURL(distEntry).href,\n });\n }\n }\n\n return results;\n}\n\n// ---------------------------------------------------------------------------\n// Exported API\n// ---------------------------------------------------------------------------\n\n/** Resolved once — the CLI core package root (`packages/cli/core/`). */\nexport const corePackageDir = dirname(\n dirname(dirname(fileURLToPath(import.meta.url))),\n);\n\n/**\n * Discover installed plugin packages. Returns a de-duplicated, sorted list\n * of `{ name, importSpecifier }` objects.\n *\n * @param basePath - Directory to scan for `node_modules/@fluid-app/`\n * @param searchFrom - Starting directory for workspace root detection\n * (walked upward via `findWorkspaceRoot`). Pass `null` to skip workspace\n * scanning (useful in tests). Defaults to `corePackageDir`.\n */\nexport function discoverPlugins(\n basePath: string,\n searchFrom?: string | null,\n options: PluginDiscoveryOptions = {},\n): DiscoveredPlugin[] {\n const seen = new Set<string>();\n const results: DiscoveredPlugin[] = [];\n\n const addPlugins = (plugins: DiscoveredPlugin[]) => {\n for (const plugin of plugins) {\n if (seen.has(plugin.name)) continue;\n seen.add(plugin.name);\n results.push(plugin);\n }\n };\n\n const cliParent =\n options.installationBasePath ?? dirname(dirname(dirname(corePackageDir)));\n const projectPlugins = () => discoverFromNodeModules(basePath);\n const installationPlugins = () =>\n cliParent === basePath ? [] : discoverFromNodeModules(cliParent);\n const workspacePlugins = () => {\n if (searchFrom === null) return [];\n return discoverFromWorkspace(searchFrom ?? corePackageDir);\n };\n\n if (options.preferInstallation) {\n addPlugins(installationPlugins());\n // In a monorepo development build, resolveFluidBinary() deliberately\n // realpaths the CLI into packages/cli/core. Its sibling plugins are then\n // discovered from the workspace rather than an installation-level\n // node_modules, so they are part of the same trusted CLI installation.\n addPlugins(workspacePlugins());\n addPlugins(projectPlugins());\n } else {\n addPlugins(projectPlugins());\n addPlugins(installationPlugins());\n addPlugins(workspacePlugins());\n }\n\n return results.sort((a, b) => a.name.localeCompare(b.name));\n}\n\n/**\n * Dynamically import a plugin module and return its default export.\n *\n * @internal Not intended for external callers. Only called with specifiers\n * produced by {@link discoverPlugins}:\n * - bare package names (e.g. `@fluid-app/fluid-cli-portal`) from the\n * node_modules scan, validated by {@link isPluginName}.\n * - `file://` URLs from {@link discoverFromWorkspace}, which constructs\n * them internally from validated workspace paths (`packages/` tree,\n * `dist/index.mjs`). These are trusted internal specifiers.\n */\nexport async function importPlugin(specifier: string): Promise<unknown> {\n // file:// URLs are trusted — they originate from discoverFromWorkspace\n // which builds them from validated workspace paths under packages/.\n // For bare package names, verify they match the plugin naming convention.\n if (!specifier.startsWith(\"file://\") && !isPluginName(specifier)) {\n throw new Error(`Refusing to import non-plugin package: ${specifier}`);\n }\n const mod = (await import(specifier)) as Record<string, unknown>;\n return mod[\"default\"] ?? mod;\n}\n","/**\n * Format one-line diagnostics naming a CLI plugin build and where it was\n * resolved from. The resolved path is the load-bearing detail: a workspace\n * build and a published copy share the same name and version, so the path is\n * the only thing that distinguishes them. See CURRENT-3218.\n */\n\nimport { fileURLToPath } from \"node:url\";\n\nexport interface PluginLoadInfo {\n readonly name: string;\n readonly version: string;\n /**\n * Where the plugin was resolved from — an absolute `file://` URL (workspace\n * or published install, resolved upstream) or, only when resolution failed,\n * a bare package name.\n */\n readonly importSpecifier: string;\n /**\n * Whether installation-bundled plugins were preferred over project-local\n * ones (`FLUID_CLI_PREFER_INSTALLATION_PLUGINS=1`, set by Mist).\n */\n readonly preferInstallation: boolean;\n}\n\nexport interface PluginLoadFailureInfo {\n readonly name: string;\n readonly importSpecifier: string;\n readonly preferInstallation: boolean;\n readonly message: string;\n}\n\nexport function modeLabel(preferInstallation: boolean): string {\n return preferInstallation ? \"installation-preferred\" : \"project-first\";\n}\n\n/**\n * Render a specifier for display: `file://` URLs become absolute filesystem\n * paths; anything else is shown verbatim. Never throws — a malformed `file://`\n * specifier falls back to its raw form so a diagnostic can never turn a\n * successful load into a reported failure.\n */\nexport function displaySource(importSpecifier: string): string {\n if (!importSpecifier.startsWith(\"file://\")) return importSpecifier;\n try {\n return fileURLToPath(importSpecifier);\n } catch {\n return importSpecifier;\n }\n}\n\nexport function formatPluginLoadLine(info: PluginLoadInfo): string {\n return `Loaded plugin ${info.name}@${info.version} from ${displaySource(\n info.importSpecifier,\n )} [${modeLabel(info.preferInstallation)}]`;\n}\n\nexport function formatPluginLoadFailure(info: PluginLoadFailureInfo): string {\n return `Failed to load plugin ${info.name} from ${displaySource(\n info.importSpecifier,\n )} [${modeLabel(info.preferInstallation)}]: ${info.message}`;\n}\n","/**\n * Resolve a plugin import specifier to a concrete path for diagnostics.\n *\n * The node_modules discovery strategy falls back to a bare package name when it\n * cannot realpath a workspace symlink — precisely the published-install case\n * where the path is the only thing that would reveal a stale plugin winning.\n * Resolving the bare specifier recovers that path. See CURRENT-3218.\n */\n\nexport type Resolver = (specifier: string) => string;\n\nconst defaultResolver: Resolver = (specifier) => import.meta.resolve(specifier);\n\n/**\n * Return a `file://` URL for `importSpecifier`. `file://` specifiers (already\n * resolved by discovery) pass through untouched; bare specifiers are resolved\n * via `resolve`. If resolution throws, the bare specifier is returned so a\n * diagnostic degrades gracefully rather than failing the load.\n */\nexport function resolveLoadPath(\n importSpecifier: string,\n resolve: Resolver = defaultResolver,\n): string {\n if (importSpecifier.startsWith(\"file://\")) return importSpecifier;\n try {\n return resolve(importSpecifier);\n } catch {\n return importSpecifier;\n }\n}\n","/**\n * Decide whether to print the per-plugin \"Loaded plugin …\" diagnostics.\n *\n * A monorepo checkout resolves ~a dozen plugin packages, so emitting a line\n * for each on every command (including `--help` and tab-completion, and into\n * Mist's captured stderr dialog) is noise. The success diagnostics are gated\n * behind an explicit opt-in; load *failures* are always surfaced by the loader\n * regardless of this flag. See CURRENT-3218.\n */\n\nfunction isTruthyFlag(value: string | undefined): boolean {\n return value === \"1\" || value === \"true\";\n}\n\nexport function shouldLogPluginLoads(\n argv: readonly string[] = process.argv,\n env: NodeJS.ProcessEnv = process.env,\n): boolean {\n if (argv.includes(\"--verbose\")) return true;\n return isTruthyFlag(env[\"FLUID_CLI_DEBUG\"] ?? env[\"FLUID_CLI_VERBOSE\"]);\n}\n","/**\n * Plugin loader — orchestrates discovery and registration\n */\n\nimport type { Command } from \"commander\";\nimport chalk from \"chalk\";\nimport { getConfigDir } from \"../config/paths.js\";\nimport { getAuthToken } from \"../auth/token.js\";\nimport { readConfig } from \"../config/config.js\";\nimport type { FluidPlugin, PluginContext } from \"./types.js\";\nimport { discoverPlugins, importPlugin } from \"./discovery.js\";\nimport {\n formatPluginLoadLine,\n formatPluginLoadFailure,\n} from \"./format-load-line.js\";\nimport { resolveLoadPath } from \"./resolve-load-path.js\";\nimport { shouldLogPluginLoads } from \"./should-log-plugin-loads.js\";\n\nfunction isFluidPlugin(value: unknown): value is FluidPlugin {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return (\n typeof obj[\"name\"] === \"string\" &&\n typeof obj[\"version\"] === \"string\" &&\n typeof obj[\"register\"] === \"function\"\n );\n}\n\n/**\n * Discover, import, and register all installed plugins\n */\nexport async function loadPlugins(\n program: Command,\n basePath: string,\n): Promise<void> {\n const preferInstallation =\n process.env[\"FLUID_CLI_PREFER_INSTALLATION_PLUGINS\"] === \"1\";\n const discovered = discoverPlugins(basePath, undefined, {\n preferInstallation,\n });\n const { enabledPlugins } = readConfig();\n\n // If enabledPlugins is set, only load explicitly allowed plugins.\n // This acts as an allow-list to prevent accidental supply-chain execution.\n const allowedSet = enabledPlugins !== null ? new Set(enabledPlugins) : null;\n const plugins = allowedSet\n ? discovered.filter((p) => allowedSet.has(p.name))\n : discovered;\n\n if (plugins.length === 0) return;\n\n const ctx: PluginContext = {\n program,\n getAuthToken,\n configDir: getConfigDir(),\n };\n\n // Success diagnostics are opt-in (a checkout resolves ~a dozen plugins);\n // load failures below are always surfaced. See CURRENT-3218.\n const verbose = shouldLogPluginLoads();\n\n for (const { name, importSpecifier } of plugins) {\n // Resolve to a concrete path so a published install is distinguishable\n // from a workspace build at a glance.\n const source = resolveLoadPath(importSpecifier);\n try {\n const exported = await importPlugin(importSpecifier);\n\n if (!isFluidPlugin(exported)) {\n console.warn(\n chalk.yellow(`Warning: ${name} does not export a valid FluidPlugin`),\n );\n continue;\n }\n\n await exported.register(ctx);\n\n if (verbose) {\n console.error(\n chalk.dim(\n formatPluginLoadLine({\n name: exported.name,\n version: exported.version,\n importSpecifier: source,\n preferInstallation,\n }),\n ),\n );\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n // Always name what failed to load and from where — this is exactly the\n // invisible-artifact failure the diagnostic exists to catch.\n console.warn(\n chalk.yellow(\n `Warning: ${formatPluginLoadFailure({\n name,\n importSpecifier: source,\n preferInstallation,\n message,\n })}`,\n ),\n );\n }\n }\n}\n","#!/usr/bin/env node\n\nimport { createRequire } from \"node:module\";\nimport { Command } from \"commander\";\nimport { loginCommand } from \"../commands/login.js\";\nimport { logoutCommand } from \"../commands/logout.js\";\nimport { whoamiCommand } from \"../commands/whoami.js\";\nimport { switchCommand } from \"../commands/switch.js\";\nimport { loadPlugins } from \"../plugins/loader.js\";\n\nconst require = createRequire(import.meta.url);\nconst { version } = require(\"../../package.json\") as { version: string };\n\nconst packageRoot = process.cwd();\n\nconst program = new Command();\n\nprogram.name(\"fluid\").description(\"Fluid Commerce CLI\").version(version);\n\n// Built-in auth commands\nprogram.addCommand(loginCommand);\nprogram.addCommand(logoutCommand);\nprogram.addCommand(whoamiCommand);\nprogram.addCommand(switchCommand);\n\n// Discover and load all plugins (auto-discovered from node_modules and workspace)\nawait loadPlugins(program, packageRoot);\n\nprogram.parse(process.argv, { from: \"node\" });\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -260,6 +260,9 @@ interface OutputOptions {
260
260
  }
261
261
  interface CommandContext {
262
262
  getClient(): Promise<FetchClient>;
263
+ /** A client for another Fluid service (e.g. fluid-integrations) that
264
+ * sends the same resolved token as getClient(). */
265
+ getClientFor(baseUrl: string): Promise<FetchClient>;
263
266
  output(data: unknown): void;
264
267
  parseBody(raw: string): unknown;
265
268
  verbose: boolean;
@@ -283,6 +286,53 @@ declare function createCommandContext(opts: {
283
286
  //#region src/domain/output.d.ts
284
287
  declare function formatOutput(data: unknown, options: OutputOptions): string;
285
288
  //#endregion
289
+ //#region src/walk/prompter.d.ts
290
+ type Nav = "back" | "skip" | "menu" | "quit";
291
+ /** An answer, or a request to move: back a question, skip it, open the
292
+ * menu, or stop (Ctrl-C / Esc). */
293
+ type Reply = {
294
+ value: string;
295
+ } | {
296
+ nav: Nav;
297
+ };
298
+ interface Choice {
299
+ title: string;
300
+ value: string;
301
+ description?: string;
302
+ }
303
+ interface Question {
304
+ /** A line above the prompt, e.g. "Business Info · 3 left". */
305
+ section?: string;
306
+ message: string;
307
+ /** `secret` hides the typing; `search` filters a long list as you type. */
308
+ kind: "text" | "secret" | "select" | "search";
309
+ choices?: Choice[];
310
+ /** The saved answer, already masked if sensitive. Enter keeps it. */
311
+ current?: string;
312
+ hint?: string;
313
+ canGoBack: boolean;
314
+ }
315
+ interface MenuItem<T extends string> {
316
+ title: string;
317
+ value: T;
318
+ description?: string;
319
+ disabled?: boolean;
320
+ }
321
+ interface Prompter {
322
+ ask(question: Question): Promise<Reply>;
323
+ /** Null when the person cancels the menu. */
324
+ menu<T extends string>(message: string, items: ReadonlyArray<MenuItem<T>>): Promise<T | null>;
325
+ note(text: string): void;
326
+ }
327
+ declare const NAV_HELP = "Type < to go back, > to skip, ? for the menu. Every answer is saved as you go; Ctrl-C stops.";
328
+ /** A typed `<`, `>` or `?` moves instead of answering. */
329
+ declare function parseTyped(raw: string): Reply;
330
+ /** A prompter on the terminal, built on `prompts`. */
331
+ declare function terminalPrompter(): Prompter;
332
+ /** A walk needs a person at a terminal; scripts and agents use the
333
+ * non-interactive commands it names. */
334
+ declare function assertInteractive(alternative: string, isTTY?: boolean): void;
335
+ //#endregion
286
336
  //#region src/git-sync/commit-subject.d.ts
287
337
  /**
288
338
  * The canonical GitSync commit-subject format, shared by every kind of
@@ -509,5 +559,5 @@ declare function writeCachedActor(actor: GitSyncActor, now?: Date): void;
509
559
  */
510
560
  declare function resolveGitSyncActor(fetchActor: () => Promise<GitSyncActor | null>): Promise<GitSyncActor | null>;
511
561
  //#endregion
512
- export { ACTOR_CACHE_TTL_MS, type CachedGitSyncActor, type ChangeEntry, type ChangeStatus, type CliError, type CommandContext, type CommitMessage, type CompanyChoice, type CompanyInfo, type ConfirmMfaResponse, type Failure, type FetchClient, type FluidApiError, type FluidConfig, type FluidPlugin, type FluidProfile, type GitSyncActor, type MfaResponse, type OutputOptions, type ParsedGitSyncSubject, type PluginContext, type ProjectRcConfig, type Result, SYSTEM_ACTOR_NAME, type Success, type SwitchCompanyResult, type UserCompany, changeStatusFromCode, confirmMfa, createCommandContext, createDefaultConfig, createDomainCommand, failure, fetchUserCompanies, findProjectConfig, formatCommitTimestamp, formatOutput, getActiveProfile, getAuthToken, getConfigDir, getConfigFilePath, getErrorMessage, getFluidApiBase, gitSyncActorFromMe, gitSyncCommitSubject, gitSyncSubjectAuthor, isError, isFailure, isNodeError, isSuccess, listProfileNames, mapError, mapResult, parseGitSyncSubject, parseNameStatusZ, readCachedActor, readConfig, resolveGitSyncActor, sendMfa, subjectFragment, success, summarizeChanges, switchCompany, tryCatch, tryCatchAsync, unwrap, unwrapOr, updateConfig, validateToken, writeCachedActor, writeConfig };
562
+ export { ACTOR_CACHE_TTL_MS, type CachedGitSyncActor, type ChangeEntry, type ChangeStatus, type Choice, type CliError, type CommandContext, type CommitMessage, type CompanyChoice, type CompanyInfo, type ConfirmMfaResponse, type Failure, type FetchClient, type FluidApiError, type FluidConfig, type FluidPlugin, type FluidProfile, type GitSyncActor, type MenuItem, type MfaResponse, NAV_HELP, type Nav, type OutputOptions, type ParsedGitSyncSubject, type PluginContext, type ProjectRcConfig, type Prompter, type Question, type Reply, type Result, SYSTEM_ACTOR_NAME, type Success, type SwitchCompanyResult, type UserCompany, assertInteractive, changeStatusFromCode, confirmMfa, createCommandContext, createDefaultConfig, createDomainCommand, failure, fetchUserCompanies, findProjectConfig, formatCommitTimestamp, formatOutput, getActiveProfile, getAuthToken, getConfigDir, getConfigFilePath, getErrorMessage, getFluidApiBase, gitSyncActorFromMe, gitSyncCommitSubject, gitSyncSubjectAuthor, isError, isFailure, isNodeError, isSuccess, listProfileNames, mapError, mapResult, parseGitSyncSubject, parseNameStatusZ, parseTyped, readCachedActor, readConfig, resolveGitSyncActor, sendMfa, subjectFragment, success, summarizeChanges, switchCompany, terminalPrompter, tryCatch, tryCatchAsync, unwrap, unwrapOr, updateConfig, validateToken, writeCachedActor, writeConfig };
513
563
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/config/types.ts","../src/config/paths.ts","../src/config/config.ts","../src/config/project-config.ts","../src/utils/errors.ts","../src/utils/result.ts","../src/auth/fluid-api.ts","../src/auth/token.ts","../src/plugins/types.ts","../../../platform/api-client-core/src/fetch-client.ts","../src/domain/types.ts","../src/domain/command.ts","../src/domain/context.ts","../src/domain/output.ts","../src/git-sync/commit-subject.ts","../src/git-sync/change-summary.ts","../src/git-sync/actor-cache.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/config/types.ts","../src/config/paths.ts","../src/config/config.ts","../src/config/project-config.ts","../src/utils/errors.ts","../src/utils/result.ts","../src/auth/fluid-api.ts","../src/auth/token.ts","../src/plugins/types.ts","../../../platform/api-client-core/src/fetch-client.ts","../src/domain/types.ts","../src/domain/command.ts","../src/domain/context.ts","../src/domain/output.ts","../src/walk/prompter.ts","../src/git-sync/commit-subject.ts","../src/git-sync/change-summary.ts","../src/git-sync/actor-cache.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { A as unwrapOr, C as isSuccess, D as tryCatch, E as success, O as tryCatchAsync, S as isNodeError, T as mapResult, _ as validateToken, a as readConfig, b as isError, c as getConfigDir, f as confirmMfa, g as switchCompany, h as sendMfa, i as findProjectConfig, k as unwrap, l as getConfigFilePath, m as getFluidApiBase, n as getAuthToken, o as updateConfig, p as fetchUserCompanies, r as listProfileNames, s as writeConfig, t as getActiveProfile, u as createDefaultConfig, v as failure, w as mapError, x as isFailure, y as getErrorMessage } from "./token--hQqx3DU.mjs";
2
2
  import { Command } from "commander";
3
+ import prompts from "prompts";
3
4
  //#region ../../platform/api-client-core/src/api-error-shape.ts
4
5
  /**
5
6
  * Narrows a parsed JSON error payload to the envelope contract. Arrays and
@@ -113,8 +114,10 @@ function createFetchClient(config) {
113
114
  const queryString = new URLSearchParams();
114
115
  Object.entries(params).forEach(([key, value]) => {
115
116
  if (value === void 0 || value === null) return;
116
- if (Array.isArray(value)) value.forEach((item) => queryString.append(`${key}[]`, String(item)));
117
- else if (typeof value === "object") Object.entries(value).forEach(([subKey, subValue]) => {
117
+ if (Array.isArray(value)) {
118
+ const arrayKey = key.endsWith("[]") ? key : `${key}[]`;
119
+ value.forEach((item) => queryString.append(arrayKey, String(item)));
120
+ } else if (typeof value === "object") Object.entries(value).forEach(([subKey, subValue]) => {
118
121
  if (subValue === void 0 || subValue === null) return;
119
122
  if (Array.isArray(subValue)) subValue.forEach((item) => queryString.append(`${key}[${subKey}][]`, String(item)));
120
123
  else queryString.append(`${key}[${subKey}]`, String(subValue));
@@ -308,21 +311,30 @@ function createCommandContext(opts) {
308
311
  compact: opts.compact,
309
312
  jq: opts.jq
310
313
  };
314
+ function requireToken() {
315
+ const token = resolveToken(opts);
316
+ if (token) return token;
317
+ if (opts.profile) throw new Error(`No API token found for profile "${opts.profile}". Run \`fluid login\` for that profile or provide --token <token>`);
318
+ throw new Error("No API token found. Run `fluid login` or provide --token <token>");
319
+ }
311
320
  return {
312
321
  verbose: opts.verbose,
313
322
  async getClient() {
314
323
  if (clientInstance) return clientInstance;
315
- const token = resolveToken(opts);
316
- if (!token) {
317
- if (opts.profile) throw new Error(`No API token found for profile "${opts.profile}". Run \`fluid login\` for that profile or provide --token <token>`);
318
- throw new Error("No API token found. Run `fluid login` or provide --token <token>");
319
- }
324
+ const token = requireToken();
320
325
  clientInstance = createFetchClient({
321
326
  baseUrl: opts.baseUrl ?? process.env["FLUID_API_BASE"] ?? "https://api.fluid.app",
322
327
  getAuthToken: () => token
323
328
  });
324
329
  return clientInstance;
325
330
  },
331
+ async getClientFor(serviceBaseUrl) {
332
+ const token = requireToken();
333
+ return createFetchClient({
334
+ baseUrl: serviceBaseUrl.replace(/\/$/, ""),
335
+ getAuthToken: () => token
336
+ });
337
+ },
326
338
  output(data) {
327
339
  const formatted = formatOutput(data, outputOptions);
328
340
  process.stdout.write(formatted + "\n");
@@ -375,6 +387,9 @@ function createDomainCommand(name, description, register) {
375
387
  getClient() {
376
388
  return getCtx().getClient();
377
389
  },
390
+ getClientFor(baseUrl) {
391
+ return getCtx().getClientFor(baseUrl);
392
+ },
378
393
  output(data) {
379
394
  return getCtx().output(data);
380
395
  },
@@ -385,6 +400,105 @@ function createDomainCommand(name, description, register) {
385
400
  return cmd;
386
401
  }
387
402
  //#endregion
403
+ //#region src/walk/prompter.ts
404
+ const NAV_HELP = "Type < to go back, > to skip, ? for the menu. Every answer is saved as you go; Ctrl-C stops.";
405
+ const TYPED_NAV = {
406
+ "<": "back",
407
+ ">": "skip",
408
+ "?": "menu"
409
+ };
410
+ /** A typed `<`, `>` or `?` moves instead of answering. */
411
+ function parseTyped(raw) {
412
+ const nav = TYPED_NAV[raw.trim()];
413
+ return nav ? { nav } : { value: raw };
414
+ }
415
+ const NAV_PREFIX = "\0";
416
+ function navChoices(canGoBack) {
417
+ return [
418
+ ...canGoBack ? [{
419
+ title: "← Back",
420
+ value: `${NAV_PREFIX}back`
421
+ }] : [],
422
+ {
423
+ title: "Skip →",
424
+ value: `${NAV_PREFIX}skip`
425
+ },
426
+ {
427
+ title: "Menu…",
428
+ value: `${NAV_PREFIX}menu`
429
+ }
430
+ ];
431
+ }
432
+ function fromChoice(value) {
433
+ return value.startsWith(NAV_PREFIX) ? { nav: value.slice(1) } : { value };
434
+ }
435
+ /** A prompter on the terminal, built on `prompts`. */
436
+ function terminalPrompter() {
437
+ let cancelled = false;
438
+ const onCancel = () => {
439
+ cancelled = true;
440
+ return false;
441
+ };
442
+ return {
443
+ async ask(q) {
444
+ cancelled = false;
445
+ if (q.section) process.stdout.write(`\n${q.section}\n`);
446
+ const message = q.current ? `${q.message} (saved: ${q.current})` : q.message;
447
+ if (q.kind === "select" || q.kind === "search") {
448
+ const choices = [...q.choices ?? [], ...navChoices(q.canGoBack)];
449
+ const initial = Math.max(0, choices.findIndex((c) => c.title === q.current || c.value === q.current));
450
+ const { answer } = await prompts({
451
+ type: q.kind === "search" ? "autocomplete" : "select",
452
+ name: "answer",
453
+ message,
454
+ choices,
455
+ initial,
456
+ hint: q.hint,
457
+ ...q.kind === "search" ? { suggest: async (input, all) => {
458
+ const needle = input.trim().toLowerCase();
459
+ return all.filter((c) => c.value.startsWith(NAV_PREFIX) || c.title.toLowerCase().includes(needle) || c.value.toLowerCase().includes(needle));
460
+ } } : {}
461
+ }, { onCancel });
462
+ if (cancelled || answer === void 0) return { nav: "quit" };
463
+ return fromChoice(answer);
464
+ }
465
+ const { answer } = await prompts({
466
+ type: q.kind === "secret" ? "password" : "text",
467
+ name: "answer",
468
+ message: q.hint ? `${message} — ${q.hint}` : message,
469
+ ...q.kind === "text" && q.current ? { initial: q.current } : {}
470
+ }, { onCancel });
471
+ if (cancelled || answer === void 0) return { nav: "quit" };
472
+ if (q.kind === "secret" && q.current && answer === "") return { nav: "skip" };
473
+ return parseTyped(answer);
474
+ },
475
+ async menu(message, items) {
476
+ cancelled = false;
477
+ const { choice } = await prompts({
478
+ type: "select",
479
+ name: "choice",
480
+ message,
481
+ choices: items.map((item) => ({
482
+ title: item.title,
483
+ value: item.value,
484
+ description: item.description,
485
+ disabled: item.disabled
486
+ }))
487
+ }, { onCancel });
488
+ if (cancelled || choice === void 0) return null;
489
+ return choice;
490
+ },
491
+ note(text) {
492
+ process.stdout.write(`${text}\n`);
493
+ }
494
+ };
495
+ }
496
+ /** A walk needs a person at a terminal; scripts and agents use the
497
+ * non-interactive commands it names. */
498
+ function assertInteractive(alternative, isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
499
+ if (!isTTY) throw new Error(`walk asks questions in a terminal. In a script, use ${alternative}.`);
500
+ }
501
+ //#endregion
388
502
  //#region src/git-sync/commit-subject.ts
389
503
  /** Actor name used when no human triggered the commit. */
390
504
  const SYSTEM_ACTOR_NAME = "Fluid";
@@ -678,6 +792,6 @@ async function resolveGitSyncActor(fetchActor) {
678
792
  return fetched;
679
793
  }
680
794
  //#endregion
681
- export { ACTOR_CACHE_TTL_MS, SYSTEM_ACTOR_NAME, changeStatusFromCode, confirmMfa, createCommandContext, createDefaultConfig, createDomainCommand, failure, fetchUserCompanies, findProjectConfig, formatCommitTimestamp, formatOutput, getActiveProfile, getAuthToken, getConfigDir, getConfigFilePath, getErrorMessage, getFluidApiBase, gitSyncActorFromMe, gitSyncCommitSubject, gitSyncSubjectAuthor, isError, isFailure, isNodeError, isSuccess, listProfileNames, mapError, mapResult, parseGitSyncSubject, parseNameStatusZ, readCachedActor, readConfig, resolveGitSyncActor, sendMfa, subjectFragment, success, summarizeChanges, switchCompany, tryCatch, tryCatchAsync, unwrap, unwrapOr, updateConfig, validateToken, writeCachedActor, writeConfig };
795
+ export { ACTOR_CACHE_TTL_MS, NAV_HELP, SYSTEM_ACTOR_NAME, assertInteractive, changeStatusFromCode, confirmMfa, createCommandContext, createDefaultConfig, createDomainCommand, failure, fetchUserCompanies, findProjectConfig, formatCommitTimestamp, formatOutput, getActiveProfile, getAuthToken, getConfigDir, getConfigFilePath, getErrorMessage, getFluidApiBase, gitSyncActorFromMe, gitSyncCommitSubject, gitSyncSubjectAuthor, isError, isFailure, isNodeError, isSuccess, listProfileNames, mapError, mapResult, parseGitSyncSubject, parseNameStatusZ, parseTyped, readCachedActor, readConfig, resolveGitSyncActor, sendMfa, subjectFragment, success, summarizeChanges, switchCompany, terminalPrompter, tryCatch, tryCatchAsync, unwrap, unwrapOr, updateConfig, validateToken, writeCachedActor, writeConfig };
682
796
 
683
797
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../platform/api-client-core/src/api-error-shape.ts","../../../platform/api-client-core/src/fetch-client.ts","../src/domain/output.ts","../src/domain/context.ts","../src/domain/command.ts","../src/git-sync/commit-subject.ts","../src/git-sync/change-summary.ts","../src/git-sync/actor-cache.ts"],"sourcesContent":["/**\n * The shape contract for `ApiError.body` and `ApiError.data`.\n *\n * One decision, in one place: `body` is the response envelope exactly as the\n * server sent it, `data` is the unwrapped field-error bag. Rationale and the\n * alternatives considered live in `docs/api-error-data-contract.md`.\n */\n\n/**\n * The parsed JSON error response body, exactly as the server sent it.\n *\n * This is the envelope: `errors`, `error_message`, `error`, `status`, `meta`\n * and `request_id` all live here. `null` when the response carried no JSON\n * object — HTML, an empty body, a JSON primitive, or unparseable text.\n */\nexport type ApiErrorBody = Record<string, unknown> | null;\n\n/**\n * The field-level error bag: the envelope's `errors` value when the body has\n * one, otherwise the body itself.\n *\n * `errors` is declared `never` because unwrapping already consumed it — read\n * the envelope from `ApiError.body`, not from here. That makes the historical\n * mistake (`error.data.errors.some_field`, which silently resolved to\n * `undefined`) a typecheck failure instead of a degraded error message.\n */\nexport type ApiErrorFieldErrors = Record<string, unknown> & {\n errors?: never;\n};\n\n/**\n * Every value `ApiError.data` can hold. Fluid endpoints return an object bag,\n * but `errors` is occasionally an array or a string, and non-JSON responses\n * carry no data at all.\n */\nexport type ApiErrorData =\n | ApiErrorFieldErrors\n | readonly unknown[]\n | string\n | number\n | boolean\n | null;\n\n/**\n * Narrows a parsed JSON error payload to the envelope contract. Arrays and\n * primitives are not envelopes, so they resolve to `null` — their content is\n * still reachable through `ApiError.data`.\n */\nexport function toApiErrorBody(value: unknown): ApiErrorBody {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return null;\n }\n\n return value as Record<string, unknown>;\n}\n\n/**\n * The single unwrap rule, shared by every producer of `ApiError`.\n *\n * `??` rather than `||` so an explicitly empty `errors` (`\"\"`, `0`, `false`)\n * is preserved instead of silently falling back to the whole envelope.\n */\nexport function toApiErrorData(body: ApiErrorBody): ApiErrorData {\n if (!body) {\n return null;\n }\n\n return (body.errors ?? body) as ApiErrorData;\n}\n","/**\n * Minimal, framework-agnostic fetch client for Fluid APIs\n * Compatible with fluid-admin patterns but usable standalone\n */\n\nimport type { ApiErrorBody, ApiErrorData } from \"./api-error-shape\";\nimport { toApiErrorBody, toApiErrorData } from \"./api-error-shape\";\n\nexport interface FetchClientConfig {\n /**\n * Base URL for all requests (e.g., \"https://api.fluid.app/api\")\n */\n baseUrl: string;\n\n /**\n * Optional function to get auth token\n * Return null/undefined if no token available\n */\n getAuthToken?: () => string | null | Promise<string | null>;\n\n /**\n * Optional callback when 401 auth error occurs\n */\n onAuthError?: () => void;\n\n /**\n * Default headers to include in all requests\n * Example: { \"x-fluid-client\": \"admin\" }\n */\n defaultHeaders?: Record<string, string>;\n\n /**\n * Credentials mode for fetch requests.\n * Set to `\"include\"` for cookie-based (same-origin BFF) authentication.\n * @default undefined (browser default: \"same-origin\")\n */\n credentials?: RequestCredentials;\n\n /**\n * Request cache mode for fetch requests.\n * @default undefined (browser default)\n */\n cache?: RequestCache;\n\n /**\n * Retry configuration for thrown network errors from fetch.\n * Does not retry HTTP error responses or aborted requests.\n * @default undefined (no retries)\n */\n networkRetry?: {\n maxRetries?: number;\n baseDelayMs?: number;\n };\n\n /**\n * Throw ApiError when a successful response declares JSON but cannot be parsed.\n * Defaults to false to preserve the legacy generated-client behavior.\n * @default false\n */\n throwOnInvalidJson?: boolean;\n}\n\nexport interface RequestOptions {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\";\n headers?: Record<string, string>;\n params?: Record<string, unknown>;\n body?: unknown;\n signal?: AbortSignal;\n priority?: RequestInit[\"priority\"];\n}\n\n/**\n * API Error class compatible with fluid-admin's ApiError\n */\nexport class ApiError extends Error {\n public readonly status: number;\n\n /**\n * The unwrapped field-error bag. See `ApiErrorData`; the contract is\n * documented in `docs/api-error-data-contract.md`.\n */\n public readonly data: ApiErrorData;\n\n /**\n * The full error response envelope. Read `error_message`, `error`, `status`\n * and nested `errors` from here — `data` has already unwrapped one level.\n */\n public readonly body: ApiErrorBody;\n\n public readonly requestId?: string;\n\n constructor(\n message: string,\n status: number,\n data?: ApiErrorData,\n requestId?: string,\n body?: ApiErrorBody,\n ) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.data = data ?? null;\n this.body = body ?? null;\n this.requestId = requestId;\n\n if (\"captureStackTrace\" in Error) {\n (\n Error as {\n captureStackTrace: (\n target: Error,\n constructor: NewableFunction,\n ) => void;\n }\n ).captureStackTrace(this, ApiError);\n }\n }\n\n toJSON(): {\n name: string;\n message: string;\n status: number;\n data: ApiErrorData;\n body: ApiErrorBody;\n requestId?: string;\n } {\n return {\n name: this.name,\n message: this.message,\n status: this.status,\n data: this.data,\n body: this.body,\n requestId: this.requestId,\n };\n }\n}\n\nfunction getStringRequestId(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction getRequestIdFromHeaders(headers: Headers): string | undefined {\n return (\n getStringRequestId(headers.get(\"x-request-id\")) ??\n getStringRequestId(headers.get(\"request-id\")) ??\n getStringRequestId(headers.get(\"X-Request-ID\"))\n );\n}\n\nfunction getRequestIdFromJsonBody(body: unknown): string | undefined {\n if (!body || typeof body !== \"object\" || Array.isArray(body)) {\n return undefined;\n }\n\n const record = body as Record<string, unknown>;\n const meta = record.meta;\n\n return (\n getStringRequestId(record.request_id) ??\n getStringRequestId(record.requestId) ??\n (meta && typeof meta === \"object\" && !Array.isArray(meta)\n ? (getStringRequestId((meta as Record<string, unknown>).request_id) ??\n getStringRequestId((meta as Record<string, unknown>).requestId))\n : undefined)\n );\n}\n\n/**\n * Type guard for ApiError\n */\nexport function isApiError(error: unknown): error is ApiError {\n return error instanceof ApiError;\n}\n\nexport interface FetchClientInstance {\n request: <TResponse = unknown>(\n endpoint: string,\n options?: RequestOptions,\n ) => Promise<TResponse>;\n requestWithFormData: <TResponse = unknown>(\n endpoint: string,\n formData: FormData,\n options?: Omit<RequestOptions, \"body\" | \"params\"> & {\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n },\n ) => Promise<TResponse>;\n get: <TResponse = unknown>(\n endpoint: string,\n params?: Record<string, unknown>,\n options?: Omit<RequestOptions, \"method\" | \"params\">,\n ) => Promise<TResponse>;\n post: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n put: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n patch: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n delete: <TResponse = unknown>(\n endpoint: string,\n options?: Omit<RequestOptions, \"method\">,\n ) => Promise<TResponse>;\n}\n\n/**\n * Creates a configured fetch client instance\n */\nexport function createFetchClient(\n config: FetchClientConfig,\n): FetchClientInstance {\n const {\n baseUrl,\n getAuthToken,\n onAuthError,\n defaultHeaders = {},\n credentials,\n cache,\n networkRetry,\n throwOnInvalidJson = false,\n } = config;\n const maxNetworkRetries = Math.max(0, networkRetry?.maxRetries ?? 0);\n const baseNetworkRetryDelayMs = Math.max(0, networkRetry?.baseDelayMs ?? 0);\n\n /**\n * Build headers for a request\n */\n async function buildHeaders(\n customHeaders?: Record<string, string>,\n ): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...defaultHeaders,\n ...customHeaders,\n };\n\n // Add auth token if available\n if (getAuthToken) {\n const token = await getAuthToken();\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n }\n\n return headers;\n }\n\n /**\n * Join baseUrl + endpoint via string concatenation (matches fetchApi).\n * Using `new URL(endpoint, baseUrl)` would strip any path prefix from\n * baseUrl (e.g. \"/api\") when the endpoint starts with \"/\".\n */\n function joinUrl(endpoint: string): string {\n return `${baseUrl}${endpoint}`;\n }\n\n /**\n * Build URL with query parameters for GET requests\n * Compatible with fluid-admin's query param handling\n */\n function buildUrl(\n endpoint: string,\n params?: Record<string, unknown>,\n ): string {\n const fullUrl = joinUrl(endpoint);\n\n if (!params || Object.keys(params).length === 0) {\n return fullUrl;\n }\n\n const queryString = new URLSearchParams();\n\n Object.entries(params).forEach(([key, value]) => {\n if (value === undefined || value === null) {\n return; // Skip undefined/null values\n }\n\n if (Array.isArray(value)) {\n // Handle arrays like Rails expects: key[]\n value.forEach((item) => queryString.append(`${key}[]`, String(item)));\n } else if (typeof value === \"object\") {\n // Handle nested objects: key[subkey]\n Object.entries(value).forEach(([subKey, subValue]) => {\n if (subValue === undefined || subValue === null) {\n return;\n }\n\n if (Array.isArray(subValue)) {\n subValue.forEach((item) =>\n queryString.append(`${key}[${subKey}][]`, String(item)),\n );\n } else {\n queryString.append(`${key}[${subKey}]`, String(subValue));\n }\n });\n } else {\n queryString.append(key, String(value));\n }\n });\n\n const qs = queryString.toString();\n return qs ? `${fullUrl}?${qs}` : fullUrl;\n }\n\n /**\n * Shared response handler for both JSON and FormData requests.\n * Handles auth errors, non-OK responses, 204 No Content, and JSON parsing.\n */\n async function handleResponse<TResponse>(\n response: Response,\n method: string,\n _url: string,\n ): Promise<TResponse> {\n const headerRequestId = getRequestIdFromHeaders(response.headers);\n\n if (response.status === 401 && onAuthError) {\n onAuthError();\n }\n\n if (!response.ok) {\n // Read body as text first to avoid SyntaxError from response.json()\n // when server returns non-JSON bodies with application/json content-type.\n const errorText = await response.text().catch(() => \"\");\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType?.includes(\"application/json\")) {\n // Parsed as `unknown` and narrowed before any property read: a body of\n // `null` (or a bare string/number/array) is valid JSON, and reading\n // `.error` off it directly threw a TypeError instead of an ApiError.\n let parsed: unknown;\n try {\n parsed = JSON.parse(errorText);\n } catch {\n throw new ApiError(\n errorText.slice(0, 200) ||\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\n headerRequestId,\n );\n }\n\n const body = toApiErrorBody(parsed);\n // Only an object envelope can carry a message. Some Rails BFF endpoints\n // return `{ error: { message, details } }` instead of `{ message }` or\n // `{ error_message }`, so that shape is tried last.\n const msg = body\n ? (() => {\n const nestedError =\n typeof body.error === \"object\" && body.error !== null\n ? (body.error as { message?: unknown }).message\n : undefined;\n const directError =\n typeof body.error === \"string\" ? body.error : undefined;\n const message =\n typeof body.message === \"string\" ? body.message : undefined;\n const errorMessage =\n typeof body.error_message === \"string\"\n ? body.error_message\n : undefined;\n return (\n message ||\n errorMessage ||\n directError ||\n (typeof nestedError === \"string\" ? nestedError : undefined)\n );\n })()\n : undefined;\n\n throw new ApiError(\n msg || `${method} request failed with status ${response.status}`,\n response.status,\n body ? toApiErrorData(body) : (parsed as ApiErrorData),\n headerRequestId ?? getRequestIdFromJsonBody(parsed),\n body,\n );\n } else {\n throw new ApiError(\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\n headerRequestId,\n );\n }\n }\n\n if (\n response.status === 204 ||\n response.headers.get(\"content-length\") === \"0\"\n ) {\n return null as TResponse;\n }\n\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType?.includes(\"application/json\")) {\n const responseText = await response.text();\n\n try {\n const data = JSON.parse(responseText);\n return data as TResponse;\n } catch {\n if (throwOnInvalidJson) {\n throw new ApiError(\n \"Failed to parse response as JSON\",\n response.status,\n null,\n headerRequestId,\n );\n }\n\n // API declared JSON content-type but body isn't valid JSON.\n // Return the raw payload to preserve the legacy non-strict path.\n return responseText ? (responseText as TResponse) : (null as TResponse);\n }\n }\n\n // Non-JSON response (text/plain, text/html, etc.)\n return null as TResponse;\n }\n\n function getNetworkRetryDelayMs(retryAttempt: number): number {\n return baseNetworkRetryDelayMs * 2 ** (retryAttempt - 1);\n }\n\n async function waitForNetworkRetry(retryAttempt: number): Promise<void> {\n const delayMs = getNetworkRetryDelayMs(retryAttempt);\n if (delayMs <= 0) {\n return;\n }\n\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n\n async function fetchWithNetworkRetry(\n url: string,\n fetchOptions: RequestInit,\n signal?: AbortSignal,\n ): Promise<Response> {\n let retryCount = 0;\n\n while (true) {\n try {\n return await fetch(url, fetchOptions);\n } catch (networkError) {\n if (signal?.aborted || retryCount >= maxNetworkRetries) {\n throw networkError;\n }\n\n retryCount += 1;\n await waitForNetworkRetry(retryCount);\n\n if (signal?.aborted) {\n throw networkError;\n }\n }\n }\n }\n\n /**\n * Main request function\n */\n async function request<TResponse = unknown>(\n endpoint: string,\n options: RequestOptions = {},\n ): Promise<TResponse> {\n const {\n method = \"GET\",\n headers: customHeaders,\n params,\n body,\n signal,\n priority,\n } = options;\n\n const url = params ? buildUrl(endpoint, params) : joinUrl(endpoint);\n\n const headers = await buildHeaders(customHeaders);\n\n let response: Response;\n\n try {\n const fetchOptions: RequestInit = { method, headers };\n if (credentials) fetchOptions.credentials = credentials;\n if (cache) fetchOptions.cache = cache;\n if (priority) fetchOptions.priority = priority;\n const serializedBody =\n body && method !== \"GET\" ? JSON.stringify(body) : null;\n if (serializedBody) fetchOptions.body = serializedBody;\n if (signal) fetchOptions.signal = signal;\n response = await fetchWithNetworkRetry(url, fetchOptions, signal);\n } catch (networkError) {\n throw new ApiError(\n `Network error: ${networkError instanceof Error ? networkError.message : \"Unknown network error\"}`,\n 0,\n null,\n );\n }\n\n return handleResponse<TResponse>(response, method, url);\n }\n\n /**\n * Request with FormData (for file uploads)\n */\n async function requestWithFormData<TResponse = unknown>(\n endpoint: string,\n formData: FormData,\n options: Omit<RequestOptions, \"body\" | \"params\"> & {\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n } = {},\n ): Promise<TResponse> {\n const {\n method = \"POST\",\n headers: customHeaders,\n signal,\n priority,\n } = options;\n\n const url = joinUrl(endpoint);\n const headers = await buildHeaders(customHeaders);\n\n // Remove Content-Type to let browser set it with boundary\n delete headers[\"Content-Type\"];\n\n let response: Response;\n\n try {\n const fetchOptions: RequestInit = { method, headers, body: formData };\n if (credentials) fetchOptions.credentials = credentials;\n if (cache) fetchOptions.cache = cache;\n if (priority) fetchOptions.priority = priority;\n if (signal) fetchOptions.signal = signal;\n response = await fetchWithNetworkRetry(url, fetchOptions, signal);\n } catch (networkError) {\n throw new ApiError(\n `Network error: ${networkError instanceof Error ? networkError.message : \"Unknown network error\"}`,\n 0,\n null,\n );\n }\n\n return handleResponse<TResponse>(response, method, url);\n }\n\n // Return client with convenience methods\n return {\n request: request,\n requestWithFormData: requestWithFormData,\n\n // Convenience methods for common HTTP verbs\n get: <TResponse = unknown>(\n endpoint: string,\n params?: Record<string, unknown>,\n options?: Omit<RequestOptions, \"method\" | \"params\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"GET\" as const,\n ...(params && { params }),\n }),\n\n post: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"POST\",\n body,\n }),\n\n put: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"PUT\",\n body,\n }),\n\n patch: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"PATCH\",\n body,\n }),\n\n delete: <TResponse = unknown>(\n endpoint: string,\n options?: Omit<RequestOptions, \"method\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"DELETE\",\n }),\n };\n}\n\nexport type FetchClient = FetchClientInstance;\n","import type { OutputOptions } from \"./types.js\";\n\nexport function formatOutput(data: unknown, options: OutputOptions): string {\n let result = data;\n\n if (options.jq) {\n result = extractPath(result, options.jq);\n }\n\n if (options.format === \"table\") {\n return formatTable(result);\n }\n\n if (options.compact) {\n return JSON.stringify(result);\n }\n return JSON.stringify(result, null, 2);\n}\n\nfunction extractPath(data: unknown, path: string): unknown {\n const parts = path\n .replace(/^\\.*/, \"\")\n .split(/\\.|\\[/)\n .filter(Boolean)\n .map((p) => p.replace(/\\]$/, \"\"));\n\n let current: any = data;\n for (const part of parts) {\n if (current == null) return undefined;\n current = current[part];\n }\n return current;\n}\n\nfunction formatTable(data: unknown): string {\n if (Array.isArray(data)) {\n if (data.length === 0) return \"(empty)\";\n\n if (typeof data[0] === \"object\" && data[0] !== null) {\n const keys = Object.keys(data[0]);\n const widths = keys.map((k) =>\n Math.max(k.length, ...data.map((row) => String(row[k] ?? \"\").length)),\n );\n\n const header = keys\n .map((k, i) => k.toUpperCase().padEnd(widths[i]!))\n .join(\" \");\n const separator = widths.map((w) => \"-\".repeat(w)).join(\" \");\n const rows = data.map((row) =>\n keys.map((k, i) => String(row[k] ?? \"\").padEnd(widths[i]!)).join(\" \"),\n );\n\n return [header, separator, ...rows].join(\"\\n\");\n }\n\n return data.map(String).join(\"\\n\");\n }\n\n if (typeof data === \"object\" && data !== null) {\n const entries = Object.entries(data);\n const maxKeyLen = Math.max(...entries.map(([k]) => k.length));\n return entries\n .map(([k, v]) => `${k.padEnd(maxKeyLen)} ${JSON.stringify(v)}`)\n .join(\"\\n\");\n }\n\n return String(data);\n}\n","import { createFetchClient } from \"@fluid-app/api-client-core\";\nimport type { FetchClient } from \"@fluid-app/api-client-core\";\n\nimport { getAuthToken } from \"../auth/token.js\";\nimport { findProjectConfig } from \"../config/project-config.js\";\nimport { formatOutput } from \"./output.js\";\nimport type { CommandContext, OutputOptions } from \"./types.js\";\n\nexport function createCommandContext(opts: {\n token?: string;\n baseUrl?: string;\n profile?: string;\n format: \"json\" | \"table\";\n compact: boolean;\n jq?: string;\n verbose: boolean;\n}): CommandContext {\n let clientInstance: FetchClient | null = null;\n\n const outputOptions: OutputOptions = {\n format: opts.format,\n compact: opts.compact,\n jq: opts.jq,\n };\n\n return {\n verbose: opts.verbose,\n\n async getClient(): Promise<FetchClient> {\n if (clientInstance) return clientInstance;\n\n const token = resolveToken(opts);\n if (!token) {\n if (opts.profile) {\n throw new Error(\n `No API token found for profile \"${opts.profile}\". Run \\`fluid login\\` for that profile or provide --token <token>`,\n );\n }\n\n throw new Error(\n \"No API token found. Run `fluid login` or provide --token <token>\",\n );\n }\n\n const baseUrl =\n opts.baseUrl ??\n process.env[\"FLUID_API_BASE\"] ??\n \"https://api.fluid.app\";\n\n clientInstance = createFetchClient({\n baseUrl,\n getAuthToken: () => token,\n });\n\n return clientInstance;\n },\n\n output(data: unknown): void {\n const formatted = formatOutput(data, outputOptions);\n process.stdout.write(formatted + \"\\n\");\n },\n\n parseBody(raw: string): unknown {\n try {\n return JSON.parse(raw);\n } catch {\n throw new Error(`Invalid JSON body: ${raw}`);\n }\n },\n };\n}\n\nfunction resolveToken(opts: {\n token?: string;\n profile?: string;\n}): string | null {\n if (opts.token) return opts.token;\n\n if (opts.profile) return getAuthToken(opts.profile);\n\n const envToken = process.env[\"FLUID_TOKEN\"] ?? process.env[\"FLUID_API_TOKEN\"];\n if (envToken) return envToken;\n\n // Project-level .fluidrc profile (before global active profile)\n const projectConfig = findProjectConfig(process.cwd());\n if (projectConfig?.profile) {\n const rcToken = getAuthToken(projectConfig.profile);\n if (rcToken) return rcToken;\n }\n\n return getAuthToken();\n}\n","import { Command } from \"commander\";\n\nimport { createCommandContext } from \"./context.js\";\nimport type { CommandContext } from \"./types.js\";\n\ntype RegisterFn = (parent: Command, ctx: CommandContext) => void;\n\nexport function createDomainCommand(\n name: string,\n description: string,\n register: RegisterFn,\n): Command {\n const cmd = new Command(name)\n .description(description)\n // Keep global domain flags usable before or after nested subcommands.\n .enablePositionalOptions(false)\n .option(\"--token <token>\", \"API authentication token\")\n .option(\"--base-url <url>\", \"API base URL\")\n .option(\"--profile <name>\", \"Config profile name\")\n .option(\"--format <format>\", \"Output format (json|table)\", \"json\")\n .option(\"--compact\", \"Compact JSON output\", false)\n .option(\"--jq <path>\", \"Extract value at JSON path\")\n .option(\"--verbose\", \"Print request details to stderr\", false);\n\n let resolvedCtx: CommandContext | null = null;\n\n function getCtx(): CommandContext {\n if (!resolvedCtx) {\n const opts = cmd.opts();\n resolvedCtx = createCommandContext({\n token: opts.token,\n baseUrl: opts.baseUrl,\n profile: opts.profile,\n format: opts.format as \"json\" | \"table\",\n compact: opts.compact,\n jq: opts.jq,\n verbose: opts.verbose,\n });\n }\n return resolvedCtx;\n }\n\n const lazyCtx: CommandContext = {\n get verbose() {\n return getCtx().verbose;\n },\n getClient() {\n return getCtx().getClient();\n },\n output(data: unknown) {\n return getCtx().output(data);\n },\n parseBody(raw: string) {\n return getCtx().parseBody(raw);\n },\n };\n\n register(cmd, lazyCtx);\n\n return cmd;\n}\n","/**\n * The canonical GitSync commit-subject format, shared by every kind of\n * Fluid-managed repository — Mist apps, Themes, Portals and Widgets.\n *\n * Each kind used to invent its own wording, so `git log` across a\n * company's repos read as four unrelated systems and only some commits\n * said who made them. One shape now covers all of them:\n *\n * <Actor Name>: <Action> · <timestamp> (<public_id>)\n * Mike Tingey: Publish portal Acme Portal · 2026-08-06 12:00:00 (0182d21e-…)\n *\n * Name and action come first so `git log --oneline` stays scannable in a\n * narrow terminal; the long UUID sits at the end where truncation eats\n * it instead of the prose. The timestamp is redundant with git's own\n * committer date but is kept inline because it survives cherry-picks,\n * rebases and the hosting providers' deployment feeds, which surface the\n * subject and nothing else.\n *\n * System-authored commits (auto-provisioning, sweeper retries, webhook\n * fan-out, an unreachable `/api/me`) use the literal actor name `Fluid`\n * and carry no UUID, so they parse with the same grammar rather than\n * needing a second one:\n *\n * Fluid: Publish theme Aurora · 2026-08-06 12:00:00\n *\n * The Ruby side mirrors this in `GitSync::CommitSubject`\n * (fluid/app/models/git_sync/commit_subject.rb) and Mist Desktop mirrors\n * the parser in `apps/mist-desktop/src/shared/git-commit-subject.ts`.\n * Change all three together.\n */\n\n/** The person a commit is attributed to. */\nexport interface GitSyncActor {\n /** Display name: full_name → first+last → email → `user-<id>`. */\n readonly name: string;\n /** The user's stable public UUID, rendered as a ` (uuid)` suffix. */\n readonly publicId: string | null;\n}\n\n/** Actor name used when no human triggered the commit. */\nexport const SYSTEM_ACTOR_NAME = \"Fluid\";\n\n/** Parseable fallback when a caller supplies only grammar delimiters or\n * whitespace as the action (for example, `-m \"·\"`). */\nconst DEFAULT_ACTION = \"Sync changes\";\n\n/**\n * A subject broken back into its parts. `publicId` is null for\n * system-authored commits and for actors whose public id was unknown at\n * commit time.\n */\nexport interface ParsedGitSyncSubject {\n readonly actorName: string;\n readonly action: string;\n /** The inline `YYYY-MM-DD HH:MM:SS` stamp, in UTC. */\n readonly timestamp: string;\n readonly publicId: string | null;\n}\n\n/**\n * Matches the canonical shape. The actor group is greedy so a display\n * name containing `\": \"` (e.g. `Tingey: Mike`) splits at the LAST colon\n * before the ` · ` separator — safe because {@link subjectFragment}\n * strips both delimiters out of every value we interpolate into an\n * action.\n */\nconst SUBJECT_RE =\n /^(.+): (.+?) · (\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})(?: \\(([^()]+)\\))?$/;\n\n/** `2026-08-06T12:00:00.000Z` → `2026-08-06 12:00:00`. */\nexport function formatCommitTimestamp(when: Date): string {\n return when.toISOString().slice(0, 19).replace(\"T\", \" \");\n}\n\n/**\n * Neutralize the two sequences the grammar splits on, so no value can\n * shift the actor / action / uuid boundaries:\n *\n * `\": \"` separates the actor from the action\n * `\"·\"` separates the action from the timestamp\n *\n * Only those exact sequences are replaced, not every colon — `12:30`\n * and `fix:handle-nulls` survive intact, because neither can be\n * mistaken for a field boundary. A trailing colon is likewise safe: the\n * parser backtracks past it.\n *\n * {@link gitSyncCommitSubject} applies this to every action, so callers\n * don't have to remember. It's idempotent, so calling it yourself on an\n * interpolated resource name (as the CLIs do) costs nothing.\n */\nexport function subjectFragment(value: string): string {\n return value\n .replace(/:\\s/g, \" \")\n .replace(/·/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\n/**\n * Build a canonical commit subject.\n *\n * @param action - What happened, e.g. `Publish theme Aurora`. Run any\n * interpolated resource name through {@link subjectFragment} first.\n * @param actor - The signed-in user, or null for system-authored\n * commits (which are attributed to `Fluid`).\n * @param when - Injectable clock, so callers and tests get a stable\n * stamp.\n */\nexport function gitSyncCommitSubject(\n action: string,\n actor: GitSyncActor | null,\n when: Date = new Date(),\n): string {\n // Sanitized HERE rather than at each call site. The action is the one\n // field a user fully controls (`-m`), and a `\": \"` in it would be read\n // back as part of the actor — `-m \"Publish: Acme\"` attributing the\n // commit to \"Mike Tingey: Publish\". Doing it at the choke point means\n // no current or future caller can reintroduce that.\n //\n // The actor name is deliberately NOT sanitized: a real name may\n // contain `\": \"` (`Tingey: Mike`), and the greedy actor group parses\n // that correctly precisely because the action can no longer contain\n // the delimiter.\n const safeAction = subjectFragment(action) || DEFAULT_ACTION;\n const stamp = formatCommitTimestamp(when);\n if (!actor) return `${SYSTEM_ACTOR_NAME}: ${safeAction} · ${stamp}`;\n const idSuffix = actor.publicId ? ` (${actor.publicId})` : \"\";\n return `${actor.name}: ${safeAction} · ${stamp}${idSuffix}`;\n}\n\n/**\n * Split a canonical subject back into its parts, or null when the\n * subject predates the format (legacy CLI wording such as `push @ <iso>`\n * or a hand-written commit). Callers fall back to rendering the raw\n * subject.\n */\nexport function parseGitSyncSubject(\n subject: string | null | undefined,\n): ParsedGitSyncSubject | null {\n if (!subject) return null;\n const match = subject.trim().match(SUBJECT_RE);\n if (!match) return null;\n const [, actorName, action, timestamp, publicId] = match;\n // The regex guarantees these groups, but noUncheckedIndexedAccess\n // types them as possibly-undefined.\n if (!actorName || !action || !timestamp) return null;\n return {\n actorName: actorName.trim(),\n action: action.trim(),\n timestamp,\n publicId: publicId?.trim() || null,\n };\n}\n\n/**\n * The person a canonical subject is attributed to, or null when the\n * subject isn't canonical or was system-authored. Use this for \"who\n * shipped this\" UI — a `Fluid:` commit has no human to name.\n */\nexport function gitSyncSubjectAuthor(\n subject: string | null | undefined,\n): string | null {\n const parsed = parseGitSyncSubject(subject);\n if (!parsed || parsed.actorName === SYSTEM_ACTOR_NAME) return null;\n return parsed.actorName;\n}\n\n/** The `/api/me` fields the actor is derived from. */\ninterface MeUser {\n readonly id?: unknown;\n readonly public_id?: unknown;\n readonly full_name?: unknown;\n readonly first_name?: unknown;\n readonly last_name?: unknown;\n readonly email?: unknown;\n}\n\n/** Some `/api/me` deployments nest the user, others return it flat. */\ninterface MeResponse extends MeUser {\n readonly user?: MeUser;\n}\n\nfunction str(value: unknown): string | null {\n if (typeof value === \"string\" && value.trim().length > 0) return value.trim();\n return null;\n}\n\n/**\n * Resolve an `/api/me` body to a commit actor, with the name fallback\n * chain `full_name → first+last → email → user-<id>`. Returns null when\n * the body carries no usable identity at all, which callers treat the\n * same as an unreachable `/api/me`: a system-authored commit.\n */\nexport function gitSyncActorFromMe(raw: MeResponse): GitSyncActor | null {\n const user = raw.user ?? raw;\n const composed = [str(user.first_name), str(user.last_name)]\n .filter((part): part is string => part !== null)\n .join(\" \")\n .trim();\n const name =\n str(user.full_name) ??\n (composed.length > 0 ? composed : null) ??\n str(user.email) ??\n (typeof user.id === \"number\" && Number.isFinite(user.id)\n ? `user-${user.id}`\n : null);\n if (!name) return null;\n const publicId =\n str(user.public_id) ??\n (typeof user.id === \"number\" && Number.isFinite(user.id)\n ? String(user.id)\n : null);\n return { name, publicId };\n}\n","/**\n * Turn a set of changed paths into the `<Action>` half of a canonical\n * GitSync commit subject (see commit-subject.ts).\n *\n * Publishes used to be stamped with a constant — \"Publish from Mist\n * Desktop\", \"Publish theme Aurora\" — so `git log` told you a publish\n * happened but never what it carried. Every entry in the history looked\n * identical, which made the log useless for finding the commit that\n * introduced something.\n *\n * This derives a short, honest phrase from the staged diff:\n *\n * Update templates/product.liquid\n * Update 3 files in templates\n * Add 2 files, update 3, delete 1\n *\n * It is deliberately mechanical. A caller that knows the INTENT behind a\n * change (Mist Desktop, or a human passing `-m`) should pass that\n * instead — an intent describes why, and no diff can recover that. This\n * is the fallback for when nobody supplied one.\n *\n * Pure: callers collect the diff (each CLI has its own git layer) and\n * hand the entries here, so the wording can't drift between kinds.\n */\n\n/** Git's porcelain status letters, narrowed to what we act on. */\nexport type ChangeStatus = \"added\" | \"modified\" | \"deleted\" | \"renamed\";\n\n/**\n * What an auto-commit helper accepts as its subject: either a literal\n * (the caller already knows the intent — a `-m` message), or a resolver\n * handed the staged changes so it can describe them.\n *\n * The resolver form exists because staging is decided INSIDE the commit\n * helpers — they `git add -A` and then unstage private paths — so only\n * they know the final file set. A summary built by the caller beforehand\n * would describe a diff the commit doesn't carry.\n *\n * It may return a promise so a caller can start slow work (an `/api/me`\n * identity lookup, say) BEFORE the git reconcile and await it here, at\n * the last possible moment — overlapping it with the fetch/merge instead\n * of paying for it serially up front. Anything awaited here is on the\n * critical path of the commit, so it must be bounded and best-effort:\n * a publish must never hang waiting to describe itself.\n */\nexport type CommitMessage =\n | string\n | ((changes: readonly ChangeEntry[]) => string | Promise<string>);\n\nexport interface ChangeEntry {\n readonly status: ChangeStatus;\n /** Repo-relative path, forward-slashed. */\n readonly path: string;\n}\n\n/** Map a `git diff --name-status` letter to a status. `R`/`C` carry a\n * similarity score (`R100`), so only the leading letter is read.\n * Unknown letters (`U` unmerged, `T` typechange) count as modified —\n * the file did change, and mislabeling the verb is better than\n * dropping it from the count. */\nexport function changeStatusFromCode(code: string): ChangeStatus {\n switch (code.trim().charAt(0).toUpperCase()) {\n case \"A\":\n return \"added\";\n case \"D\":\n return \"deleted\";\n case \"R\":\n return \"renamed\";\n default:\n return \"modified\";\n }\n}\n\n/**\n * Parse `git diff --cached --name-status -z` output.\n *\n * NUL-delimited on purpose: paths containing spaces, quotes or newlines\n * are legal in git and common in themes (`assets/hero image.png`), and\n * the newline-delimited form quotes and escapes them, which would corrupt\n * both the count and the path. With `-z`, records are\n * `status\\0path\\0` — except renames and copies, which are\n * `status\\0from\\0to\\0` and consume an extra field.\n */\nexport function parseNameStatusZ(stdout: string): ChangeEntry[] {\n const fields = stdout.split(\"\\0\").filter((f) => f.length > 0);\n const entries: ChangeEntry[] = [];\n for (let i = 0; i < fields.length; ) {\n const code = fields[i];\n if (code === undefined) break;\n const status = changeStatusFromCode(code);\n if (status === \"renamed\") {\n // status, from, to — the destination is the path we report.\n const to = fields[i + 2];\n if (to === undefined) break;\n entries.push({ status, path: to });\n i += 3;\n continue;\n }\n const path = fields[i + 1];\n if (path === undefined) break;\n entries.push({ status, path });\n i += 2;\n }\n return entries;\n}\n\n/** Top-level directory of a path, or null for a repo-root file. */\nfunction topLevelDir(path: string): string | null {\n const slash = path.indexOf(\"/\");\n return slash > 0 ? path.slice(0, slash) : null;\n}\n\nfunction plural(count: number, noun: string): string {\n return `${count} ${noun}${count === 1 ? \"\" : \"s\"}`;\n}\n\nconst VERB_FOR: Record<ChangeStatus, string> = {\n added: \"Add\",\n modified: \"Update\",\n deleted: \"Delete\",\n renamed: \"Rename\",\n};\n\n/**\n * Build the action phrase for a set of changes, or null when there is\n * nothing to describe — the caller then falls back to its own constant\n * (`Publish theme Aurora`), so an empty diff never produces a subject\n * like \"Update 0 files\".\n *\n * @param entries - The staged changes.\n * @param maxLength - Clip the result to keep `git log --oneline`\n * readable. The subject also carries a name, timestamp and UUID, so\n * the action gets a modest budget.\n */\nexport function summarizeChanges(\n entries: readonly ChangeEntry[],\n maxLength = 72,\n): string | null {\n if (entries.length === 0) return null;\n\n // A single change is the most useful case and the most common one:\n // name the actual file rather than counting to one.\n if (entries.length === 1) {\n const only = entries[0]!;\n return clip(`${VERB_FOR[only.status]} ${only.path}`, maxLength);\n }\n\n // Group by verb, ordered so the dominant kind of change leads.\n const byStatus = new Map<ChangeStatus, number>();\n for (const entry of entries) {\n byStatus.set(entry.status, (byStatus.get(entry.status) ?? 0) + 1);\n }\n\n // All one verb → \"Update 3 files\", plus a location when they share one.\n if (byStatus.size === 1) {\n const status = entries[0]!.status;\n const dirs = new Set(entries.map((e) => topLevelDir(e.path)));\n const only = dirs.size === 1 ? [...dirs][0] : null;\n const phrase = `${VERB_FOR[status]} ${plural(entries.length, \"file\")}`;\n return clip(only ? `${phrase} in ${only}` : phrase, maxLength);\n }\n\n // Mixed → lead with the largest group, then the rest as bare counts:\n // \"Update 3 files, add 2, delete 1\".\n const ordered = [...byStatus.entries()].sort(\n ([aStatus, aCount], [bStatus, bCount]) =>\n bCount - aCount || VERB_FOR[aStatus].localeCompare(VERB_FOR[bStatus]),\n );\n const [leadStatus, leadCount] = ordered[0]!;\n const rest = ordered\n .slice(1)\n .map(([status, count]) => `${VERB_FOR[status].toLowerCase()} ${count}`);\n const head = `${VERB_FOR[leadStatus]} ${plural(leadCount, \"file\")}`;\n return clip([head, ...rest].join(\", \"), maxLength);\n}\n\nfunction clip(text: string, maxLength: number): string {\n return text.length > maxLength\n ? `${text.slice(0, maxLength - 1).trimEnd()}…`\n : text;\n}\n","/**\n * Cache the signed-in user's commit identity on the active profile.\n *\n * Commit subjects are stamped `<name>: <action> · <ts> (<public_id>)`,\n * which needs the user's display name and public id. `/api/me` is the\n * only source, and it can be slow — so paying for it on EVERY push, to\n * learn a name that changes approximately never, is the wrong trade.\n *\n * The identity is stored per profile (profiles are per token + company,\n * so the cache can't leak across accounts) and refreshed only when\n * missing or stale. Login and company-switch write a fresh profile,\n * which drops the cached identity with it.\n *\n * Every failure mode here is non-fatal: an unreadable or unwritable\n * config just means we fall back to the network, and a failed network\n * lookup means a `Fluid:` system commit. A publish must never fail\n * because we couldn't work out whose name to put on it.\n */\n\nimport { readConfig, updateConfig } from \"../config/config.js\";\nimport type { GitSyncActor } from \"./commit-subject.js\";\n\n/**\n * How long a cached identity is trusted. Long, because a display name\n * changing is rare — but not forever, so a rename does eventually reach\n * the commit log without the user having to re-login.\n */\nexport const ACTOR_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days\n\n/** Cached identity as persisted on a profile. */\nexport interface CachedGitSyncActor {\n readonly name: string;\n readonly publicId: string | null;\n /** ISO-8601. */\n readonly cachedAt: string;\n}\n\nfunction isFresh(cached: CachedGitSyncActor, now: number): boolean {\n const at = Date.parse(cached.cachedAt);\n // An unparseable timestamp is treated as stale rather than trusted\n // forever — a corrupt field shouldn't pin a wrong name permanently.\n if (Number.isNaN(at)) return false;\n return now - at < ACTOR_CACHE_TTL_MS;\n}\n\n/** The cached identity for the active profile, or null. */\nexport function readCachedActor(now = Date.now()): GitSyncActor | null {\n try {\n const config = readConfig();\n const profileName = config.activeProfile;\n if (!profileName) return null;\n const cached = config.profiles[profileName]?.gitSyncActor;\n if (!cached || !cached.name || !isFresh(cached, now)) return null;\n return { name: cached.name, publicId: cached.publicId ?? null };\n } catch {\n return null;\n }\n}\n\n/** Persist an identity against the active profile. Best-effort. */\nexport function writeCachedActor(actor: GitSyncActor, now = new Date()): void {\n try {\n updateConfig((config) => {\n const profileName = config.activeProfile;\n if (!profileName) return config;\n const profile = config.profiles[profileName];\n if (!profile) return config;\n return {\n ...config,\n profiles: {\n ...config.profiles,\n [profileName]: {\n ...profile,\n gitSyncActor: {\n name: actor.name,\n publicId: actor.publicId,\n cachedAt: now.toISOString(),\n },\n },\n },\n };\n });\n } catch {\n // A read-only or malformed config just means we look it up again\n // next time — never a reason to fail a publish.\n }\n}\n\n/**\n * The commit identity, from cache when possible.\n *\n * @param fetchActor - Network lookup, used only on a cache miss. Each\n * CLI has its own client, so the caller supplies it. It must already\n * be bounded — this function adds no timeout of its own.\n *\n * Returns null when there's no cache and the lookup fails or yields no\n * usable identity; callers then write a `Fluid:` system commit.\n */\nexport async function resolveGitSyncActor(\n fetchActor: () => Promise<GitSyncActor | null>,\n): Promise<GitSyncActor | null> {\n const cached = readCachedActor();\n if (cached) return cached;\n\n const fetched = await fetchActor().catch(() => null);\n if (fetched) writeCachedActor(fetched);\n return fetched;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../platform/api-client-core/src/api-error-shape.ts","../../../platform/api-client-core/src/fetch-client.ts","../src/domain/output.ts","../src/domain/context.ts","../src/domain/command.ts","../src/walk/prompter.ts","../src/git-sync/commit-subject.ts","../src/git-sync/change-summary.ts","../src/git-sync/actor-cache.ts"],"sourcesContent":["/**\n * The shape contract for `ApiError.body` and `ApiError.data`.\n *\n * One decision, in one place: `body` is the response envelope exactly as the\n * server sent it, `data` is the unwrapped field-error bag. Rationale and the\n * alternatives considered live in `docs/api-error-data-contract.md`.\n */\n\n/**\n * The parsed JSON error response body, exactly as the server sent it.\n *\n * This is the envelope: `errors`, `error_message`, `error`, `status`, `meta`\n * and `request_id` all live here. `null` when the response carried no JSON\n * object — HTML, an empty body, a JSON primitive, or unparseable text.\n */\nexport type ApiErrorBody = Record<string, unknown> | null;\n\n/**\n * The field-level error bag: the envelope's `errors` value when the body has\n * one, otherwise the body itself.\n *\n * `errors` is declared `never` because unwrapping already consumed it — read\n * the envelope from `ApiError.body`, not from here. That makes the historical\n * mistake (`error.data.errors.some_field`, which silently resolved to\n * `undefined`) a typecheck failure instead of a degraded error message.\n */\nexport type ApiErrorFieldErrors = Record<string, unknown> & {\n errors?: never;\n};\n\n/**\n * Every value `ApiError.data` can hold. Fluid endpoints return an object bag,\n * but `errors` is occasionally an array or a string, and non-JSON responses\n * carry no data at all.\n */\nexport type ApiErrorData =\n | ApiErrorFieldErrors\n | readonly unknown[]\n | string\n | number\n | boolean\n | null;\n\n/**\n * Narrows a parsed JSON error payload to the envelope contract. Arrays and\n * primitives are not envelopes, so they resolve to `null` — their content is\n * still reachable through `ApiError.data`.\n */\nexport function toApiErrorBody(value: unknown): ApiErrorBody {\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return null;\n }\n\n return value as Record<string, unknown>;\n}\n\n/**\n * The single unwrap rule, shared by every producer of `ApiError`.\n *\n * `??` rather than `||` so an explicitly empty `errors` (`\"\"`, `0`, `false`)\n * is preserved instead of silently falling back to the whole envelope.\n */\nexport function toApiErrorData(body: ApiErrorBody): ApiErrorData {\n if (!body) {\n return null;\n }\n\n return (body.errors ?? body) as ApiErrorData;\n}\n","/**\n * Minimal, framework-agnostic fetch client for Fluid APIs\n * Compatible with fluid-admin patterns but usable standalone\n */\n\nimport type { ApiErrorBody, ApiErrorData } from \"./api-error-shape\";\nimport { toApiErrorBody, toApiErrorData } from \"./api-error-shape\";\n\nexport interface FetchClientConfig {\n /**\n * Base URL for all requests (e.g., \"https://api.fluid.app/api\")\n */\n baseUrl: string;\n\n /**\n * Optional function to get auth token\n * Return null/undefined if no token available\n */\n getAuthToken?: () => string | null | Promise<string | null>;\n\n /**\n * Optional callback when 401 auth error occurs\n */\n onAuthError?: () => void;\n\n /**\n * Default headers to include in all requests\n * Example: { \"x-fluid-client\": \"admin\" }\n */\n defaultHeaders?: Record<string, string>;\n\n /**\n * Credentials mode for fetch requests.\n * Set to `\"include\"` for cookie-based (same-origin BFF) authentication.\n * @default undefined (browser default: \"same-origin\")\n */\n credentials?: RequestCredentials;\n\n /**\n * Request cache mode for fetch requests.\n * @default undefined (browser default)\n */\n cache?: RequestCache;\n\n /**\n * Retry configuration for thrown network errors from fetch.\n * Does not retry HTTP error responses or aborted requests.\n * @default undefined (no retries)\n */\n networkRetry?: {\n maxRetries?: number;\n baseDelayMs?: number;\n };\n\n /**\n * Throw ApiError when a successful response declares JSON but cannot be parsed.\n * Defaults to false to preserve the legacy generated-client behavior.\n * @default false\n */\n throwOnInvalidJson?: boolean;\n}\n\nexport interface RequestOptions {\n method?: \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\";\n headers?: Record<string, string>;\n params?: Record<string, unknown>;\n body?: unknown;\n signal?: AbortSignal;\n priority?: RequestInit[\"priority\"];\n}\n\n/**\n * API Error class compatible with fluid-admin's ApiError\n */\nexport class ApiError extends Error {\n public readonly status: number;\n\n /**\n * The unwrapped field-error bag. See `ApiErrorData`; the contract is\n * documented in `docs/api-error-data-contract.md`.\n */\n public readonly data: ApiErrorData;\n\n /**\n * The full error response envelope. Read `error_message`, `error`, `status`\n * and nested `errors` from here — `data` has already unwrapped one level.\n */\n public readonly body: ApiErrorBody;\n\n public readonly requestId?: string;\n\n constructor(\n message: string,\n status: number,\n data?: ApiErrorData,\n requestId?: string,\n body?: ApiErrorBody,\n ) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.data = data ?? null;\n this.body = body ?? null;\n this.requestId = requestId;\n\n if (\"captureStackTrace\" in Error) {\n (\n Error as {\n captureStackTrace: (\n target: Error,\n constructor: NewableFunction,\n ) => void;\n }\n ).captureStackTrace(this, ApiError);\n }\n }\n\n toJSON(): {\n name: string;\n message: string;\n status: number;\n data: ApiErrorData;\n body: ApiErrorBody;\n requestId?: string;\n } {\n return {\n name: this.name,\n message: this.message,\n status: this.status,\n data: this.data,\n body: this.body,\n requestId: this.requestId,\n };\n }\n}\n\nfunction getStringRequestId(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction getRequestIdFromHeaders(headers: Headers): string | undefined {\n return (\n getStringRequestId(headers.get(\"x-request-id\")) ??\n getStringRequestId(headers.get(\"request-id\")) ??\n getStringRequestId(headers.get(\"X-Request-ID\"))\n );\n}\n\nfunction getRequestIdFromJsonBody(body: unknown): string | undefined {\n if (!body || typeof body !== \"object\" || Array.isArray(body)) {\n return undefined;\n }\n\n const record = body as Record<string, unknown>;\n const meta = record.meta;\n\n return (\n getStringRequestId(record.request_id) ??\n getStringRequestId(record.requestId) ??\n (meta && typeof meta === \"object\" && !Array.isArray(meta)\n ? (getStringRequestId((meta as Record<string, unknown>).request_id) ??\n getStringRequestId((meta as Record<string, unknown>).requestId))\n : undefined)\n );\n}\n\n/**\n * Type guard for ApiError\n */\nexport function isApiError(error: unknown): error is ApiError {\n return error instanceof ApiError;\n}\n\nexport interface FetchClientInstance {\n request: <TResponse = unknown>(\n endpoint: string,\n options?: RequestOptions,\n ) => Promise<TResponse>;\n requestWithFormData: <TResponse = unknown>(\n endpoint: string,\n formData: FormData,\n options?: Omit<RequestOptions, \"body\" | \"params\"> & {\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n },\n ) => Promise<TResponse>;\n get: <TResponse = unknown>(\n endpoint: string,\n params?: Record<string, unknown>,\n options?: Omit<RequestOptions, \"method\" | \"params\">,\n ) => Promise<TResponse>;\n post: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n put: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n patch: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ) => Promise<TResponse>;\n delete: <TResponse = unknown>(\n endpoint: string,\n options?: Omit<RequestOptions, \"method\">,\n ) => Promise<TResponse>;\n}\n\n/**\n * Creates a configured fetch client instance\n */\nexport function createFetchClient(\n config: FetchClientConfig,\n): FetchClientInstance {\n const {\n baseUrl,\n getAuthToken,\n onAuthError,\n defaultHeaders = {},\n credentials,\n cache,\n networkRetry,\n throwOnInvalidJson = false,\n } = config;\n const maxNetworkRetries = Math.max(0, networkRetry?.maxRetries ?? 0);\n const baseNetworkRetryDelayMs = Math.max(0, networkRetry?.baseDelayMs ?? 0);\n\n /**\n * Build headers for a request\n */\n async function buildHeaders(\n customHeaders?: Record<string, string>,\n ): Promise<Record<string, string>> {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n ...defaultHeaders,\n ...customHeaders,\n };\n\n // Add auth token if available\n if (getAuthToken) {\n const token = await getAuthToken();\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n }\n\n return headers;\n }\n\n /**\n * Join baseUrl + endpoint via string concatenation (matches fetchApi).\n * Using `new URL(endpoint, baseUrl)` would strip any path prefix from\n * baseUrl (e.g. \"/api\") when the endpoint starts with \"/\".\n */\n function joinUrl(endpoint: string): string {\n return `${baseUrl}${endpoint}`;\n }\n\n /**\n * Build URL with query parameters for GET requests\n * Compatible with fluid-admin's query param handling\n */\n function buildUrl(\n endpoint: string,\n params?: Record<string, unknown>,\n ): string {\n const fullUrl = joinUrl(endpoint);\n\n if (!params || Object.keys(params).length === 0) {\n return fullUrl;\n }\n\n const queryString = new URLSearchParams();\n\n Object.entries(params).forEach(([key, value]) => {\n if (value === undefined || value === null) {\n return; // Skip undefined/null values\n }\n\n if (Array.isArray(value)) {\n // Handle arrays like Rails expects: key[]. The OpenAPI-declared\n // param name is sometimes already bracketed (e.g. \"product_ids[]\",\n // \"filters[integration_class][]\") to match Rails' own convention;\n // appending another \"[]\" in that case would double-bracket the wire\n // format (key[][]=1), which Rails does not parse as the array.\n const arrayKey = key.endsWith(\"[]\") ? key : `${key}[]`;\n value.forEach((item) => queryString.append(arrayKey, String(item)));\n } else if (typeof value === \"object\") {\n // Handle nested objects: key[subkey]\n Object.entries(value).forEach(([subKey, subValue]) => {\n if (subValue === undefined || subValue === null) {\n return;\n }\n\n if (Array.isArray(subValue)) {\n subValue.forEach((item) =>\n queryString.append(`${key}[${subKey}][]`, String(item)),\n );\n } else {\n queryString.append(`${key}[${subKey}]`, String(subValue));\n }\n });\n } else {\n queryString.append(key, String(value));\n }\n });\n\n const qs = queryString.toString();\n return qs ? `${fullUrl}?${qs}` : fullUrl;\n }\n\n /**\n * Shared response handler for both JSON and FormData requests.\n * Handles auth errors, non-OK responses, 204 No Content, and JSON parsing.\n */\n async function handleResponse<TResponse>(\n response: Response,\n method: string,\n _url: string,\n ): Promise<TResponse> {\n const headerRequestId = getRequestIdFromHeaders(response.headers);\n\n if (response.status === 401 && onAuthError) {\n onAuthError();\n }\n\n if (!response.ok) {\n // Read body as text first to avoid SyntaxError from response.json()\n // when server returns non-JSON bodies with application/json content-type.\n const errorText = await response.text().catch(() => \"\");\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType?.includes(\"application/json\")) {\n // Parsed as `unknown` and narrowed before any property read: a body of\n // `null` (or a bare string/number/array) is valid JSON, and reading\n // `.error` off it directly threw a TypeError instead of an ApiError.\n let parsed: unknown;\n try {\n parsed = JSON.parse(errorText);\n } catch {\n throw new ApiError(\n errorText.slice(0, 200) ||\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\n headerRequestId,\n );\n }\n\n const body = toApiErrorBody(parsed);\n // Only an object envelope can carry a message. Some Rails BFF endpoints\n // return `{ error: { message, details } }` instead of `{ message }` or\n // `{ error_message }`, so that shape is tried last.\n const msg = body\n ? (() => {\n const nestedError =\n typeof body.error === \"object\" && body.error !== null\n ? (body.error as { message?: unknown }).message\n : undefined;\n const directError =\n typeof body.error === \"string\" ? body.error : undefined;\n const message =\n typeof body.message === \"string\" ? body.message : undefined;\n const errorMessage =\n typeof body.error_message === \"string\"\n ? body.error_message\n : undefined;\n return (\n message ||\n errorMessage ||\n directError ||\n (typeof nestedError === \"string\" ? nestedError : undefined)\n );\n })()\n : undefined;\n\n throw new ApiError(\n msg || `${method} request failed with status ${response.status}`,\n response.status,\n body ? toApiErrorData(body) : (parsed as ApiErrorData),\n headerRequestId ?? getRequestIdFromJsonBody(parsed),\n body,\n );\n } else {\n throw new ApiError(\n `${method} request failed with status ${response.status}`,\n response.status,\n null,\n headerRequestId,\n );\n }\n }\n\n if (\n response.status === 204 ||\n response.headers.get(\"content-length\") === \"0\"\n ) {\n return null as TResponse;\n }\n\n const contentType = response.headers.get(\"content-type\");\n\n if (contentType?.includes(\"application/json\")) {\n const responseText = await response.text();\n\n try {\n const data = JSON.parse(responseText);\n return data as TResponse;\n } catch {\n if (throwOnInvalidJson) {\n throw new ApiError(\n \"Failed to parse response as JSON\",\n response.status,\n null,\n headerRequestId,\n );\n }\n\n // API declared JSON content-type but body isn't valid JSON.\n // Return the raw payload to preserve the legacy non-strict path.\n return responseText ? (responseText as TResponse) : (null as TResponse);\n }\n }\n\n // Non-JSON response (text/plain, text/html, etc.)\n return null as TResponse;\n }\n\n function getNetworkRetryDelayMs(retryAttempt: number): number {\n return baseNetworkRetryDelayMs * 2 ** (retryAttempt - 1);\n }\n\n async function waitForNetworkRetry(retryAttempt: number): Promise<void> {\n const delayMs = getNetworkRetryDelayMs(retryAttempt);\n if (delayMs <= 0) {\n return;\n }\n\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n\n async function fetchWithNetworkRetry(\n url: string,\n fetchOptions: RequestInit,\n signal?: AbortSignal,\n ): Promise<Response> {\n let retryCount = 0;\n\n while (true) {\n try {\n return await fetch(url, fetchOptions);\n } catch (networkError) {\n if (signal?.aborted || retryCount >= maxNetworkRetries) {\n throw networkError;\n }\n\n retryCount += 1;\n await waitForNetworkRetry(retryCount);\n\n if (signal?.aborted) {\n throw networkError;\n }\n }\n }\n }\n\n /**\n * Main request function\n */\n async function request<TResponse = unknown>(\n endpoint: string,\n options: RequestOptions = {},\n ): Promise<TResponse> {\n const {\n method = \"GET\",\n headers: customHeaders,\n params,\n body,\n signal,\n priority,\n } = options;\n\n const url = params ? buildUrl(endpoint, params) : joinUrl(endpoint);\n\n const headers = await buildHeaders(customHeaders);\n\n let response: Response;\n\n try {\n const fetchOptions: RequestInit = { method, headers };\n if (credentials) fetchOptions.credentials = credentials;\n if (cache) fetchOptions.cache = cache;\n if (priority) fetchOptions.priority = priority;\n const serializedBody =\n body && method !== \"GET\" ? JSON.stringify(body) : null;\n if (serializedBody) fetchOptions.body = serializedBody;\n if (signal) fetchOptions.signal = signal;\n response = await fetchWithNetworkRetry(url, fetchOptions, signal);\n } catch (networkError) {\n throw new ApiError(\n `Network error: ${networkError instanceof Error ? networkError.message : \"Unknown network error\"}`,\n 0,\n null,\n );\n }\n\n return handleResponse<TResponse>(response, method, url);\n }\n\n /**\n * Request with FormData (for file uploads)\n */\n async function requestWithFormData<TResponse = unknown>(\n endpoint: string,\n formData: FormData,\n options: Omit<RequestOptions, \"body\" | \"params\"> & {\n method?: \"POST\" | \"PUT\" | \"PATCH\";\n } = {},\n ): Promise<TResponse> {\n const {\n method = \"POST\",\n headers: customHeaders,\n signal,\n priority,\n } = options;\n\n const url = joinUrl(endpoint);\n const headers = await buildHeaders(customHeaders);\n\n // Remove Content-Type to let browser set it with boundary\n delete headers[\"Content-Type\"];\n\n let response: Response;\n\n try {\n const fetchOptions: RequestInit = { method, headers, body: formData };\n if (credentials) fetchOptions.credentials = credentials;\n if (cache) fetchOptions.cache = cache;\n if (priority) fetchOptions.priority = priority;\n if (signal) fetchOptions.signal = signal;\n response = await fetchWithNetworkRetry(url, fetchOptions, signal);\n } catch (networkError) {\n throw new ApiError(\n `Network error: ${networkError instanceof Error ? networkError.message : \"Unknown network error\"}`,\n 0,\n null,\n );\n }\n\n return handleResponse<TResponse>(response, method, url);\n }\n\n // Return client with convenience methods\n return {\n request: request,\n requestWithFormData: requestWithFormData,\n\n // Convenience methods for common HTTP verbs\n get: <TResponse = unknown>(\n endpoint: string,\n params?: Record<string, unknown>,\n options?: Omit<RequestOptions, \"method\" | \"params\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"GET\" as const,\n ...(params && { params }),\n }),\n\n post: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"POST\",\n body,\n }),\n\n put: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"PUT\",\n body,\n }),\n\n patch: <TResponse = unknown>(\n endpoint: string,\n body?: unknown,\n options?: Omit<RequestOptions, \"method\" | \"body\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"PATCH\",\n body,\n }),\n\n delete: <TResponse = unknown>(\n endpoint: string,\n options?: Omit<RequestOptions, \"method\">,\n ): Promise<TResponse> =>\n request<TResponse>(endpoint, {\n ...options,\n method: \"DELETE\",\n }),\n };\n}\n\nexport type FetchClient = FetchClientInstance;\n","import type { OutputOptions } from \"./types.js\";\n\nexport function formatOutput(data: unknown, options: OutputOptions): string {\n let result = data;\n\n if (options.jq) {\n result = extractPath(result, options.jq);\n }\n\n if (options.format === \"table\") {\n return formatTable(result);\n }\n\n if (options.compact) {\n return JSON.stringify(result);\n }\n return JSON.stringify(result, null, 2);\n}\n\nfunction extractPath(data: unknown, path: string): unknown {\n const parts = path\n .replace(/^\\.*/, \"\")\n .split(/\\.|\\[/)\n .filter(Boolean)\n .map((p) => p.replace(/\\]$/, \"\"));\n\n let current: any = data;\n for (const part of parts) {\n if (current == null) return undefined;\n current = current[part];\n }\n return current;\n}\n\nfunction formatTable(data: unknown): string {\n if (Array.isArray(data)) {\n if (data.length === 0) return \"(empty)\";\n\n if (typeof data[0] === \"object\" && data[0] !== null) {\n const keys = Object.keys(data[0]);\n const widths = keys.map((k) =>\n Math.max(k.length, ...data.map((row) => String(row[k] ?? \"\").length)),\n );\n\n const header = keys\n .map((k, i) => k.toUpperCase().padEnd(widths[i]!))\n .join(\" \");\n const separator = widths.map((w) => \"-\".repeat(w)).join(\" \");\n const rows = data.map((row) =>\n keys.map((k, i) => String(row[k] ?? \"\").padEnd(widths[i]!)).join(\" \"),\n );\n\n return [header, separator, ...rows].join(\"\\n\");\n }\n\n return data.map(String).join(\"\\n\");\n }\n\n if (typeof data === \"object\" && data !== null) {\n const entries = Object.entries(data);\n const maxKeyLen = Math.max(...entries.map(([k]) => k.length));\n return entries\n .map(([k, v]) => `${k.padEnd(maxKeyLen)} ${JSON.stringify(v)}`)\n .join(\"\\n\");\n }\n\n return String(data);\n}\n","import { createFetchClient } from \"@fluid-app/api-client-core\";\nimport type { FetchClient } from \"@fluid-app/api-client-core\";\n\nimport { getAuthToken } from \"../auth/token.js\";\nimport { findProjectConfig } from \"../config/project-config.js\";\nimport { formatOutput } from \"./output.js\";\nimport type { CommandContext, OutputOptions } from \"./types.js\";\n\nexport function createCommandContext(opts: {\n token?: string;\n baseUrl?: string;\n profile?: string;\n format: \"json\" | \"table\";\n compact: boolean;\n jq?: string;\n verbose: boolean;\n}): CommandContext {\n let clientInstance: FetchClient | null = null;\n\n const outputOptions: OutputOptions = {\n format: opts.format,\n compact: opts.compact,\n jq: opts.jq,\n };\n\n function requireToken(): string {\n const token = resolveToken(opts);\n if (token) return token;\n if (opts.profile) {\n throw new Error(\n `No API token found for profile \"${opts.profile}\". Run \\`fluid login\\` for that profile or provide --token <token>`,\n );\n }\n throw new Error(\n \"No API token found. Run `fluid login` or provide --token <token>\",\n );\n }\n\n return {\n verbose: opts.verbose,\n\n async getClient(): Promise<FetchClient> {\n if (clientInstance) return clientInstance;\n\n const token = requireToken();\n const baseUrl =\n opts.baseUrl ??\n process.env[\"FLUID_API_BASE\"] ??\n \"https://api.fluid.app\";\n\n clientInstance = createFetchClient({\n baseUrl,\n getAuthToken: () => token,\n });\n\n return clientInstance;\n },\n\n async getClientFor(serviceBaseUrl: string): Promise<FetchClient> {\n const token = requireToken();\n return createFetchClient({\n baseUrl: serviceBaseUrl.replace(/\\/$/, \"\"),\n getAuthToken: () => token,\n });\n },\n\n output(data: unknown): void {\n const formatted = formatOutput(data, outputOptions);\n process.stdout.write(formatted + \"\\n\");\n },\n\n parseBody(raw: string): unknown {\n try {\n return JSON.parse(raw);\n } catch {\n throw new Error(`Invalid JSON body: ${raw}`);\n }\n },\n };\n}\n\nfunction resolveToken(opts: {\n token?: string;\n profile?: string;\n}): string | null {\n if (opts.token) return opts.token;\n\n if (opts.profile) return getAuthToken(opts.profile);\n\n const envToken = process.env[\"FLUID_TOKEN\"] ?? process.env[\"FLUID_API_TOKEN\"];\n if (envToken) return envToken;\n\n // Project-level .fluidrc profile (before global active profile)\n const projectConfig = findProjectConfig(process.cwd());\n if (projectConfig?.profile) {\n const rcToken = getAuthToken(projectConfig.profile);\n if (rcToken) return rcToken;\n }\n\n return getAuthToken();\n}\n","import { Command } from \"commander\";\n\nimport { createCommandContext } from \"./context.js\";\nimport type { CommandContext } from \"./types.js\";\n\ntype RegisterFn = (parent: Command, ctx: CommandContext) => void;\n\nexport function createDomainCommand(\n name: string,\n description: string,\n register: RegisterFn,\n): Command {\n const cmd = new Command(name)\n .description(description)\n // Keep global domain flags usable before or after nested subcommands.\n .enablePositionalOptions(false)\n .option(\"--token <token>\", \"API authentication token\")\n .option(\"--base-url <url>\", \"API base URL\")\n .option(\"--profile <name>\", \"Config profile name\")\n .option(\"--format <format>\", \"Output format (json|table)\", \"json\")\n .option(\"--compact\", \"Compact JSON output\", false)\n .option(\"--jq <path>\", \"Extract value at JSON path\")\n .option(\"--verbose\", \"Print request details to stderr\", false);\n\n let resolvedCtx: CommandContext | null = null;\n\n function getCtx(): CommandContext {\n if (!resolvedCtx) {\n const opts = cmd.opts();\n resolvedCtx = createCommandContext({\n token: opts.token,\n baseUrl: opts.baseUrl,\n profile: opts.profile,\n format: opts.format as \"json\" | \"table\",\n compact: opts.compact,\n jq: opts.jq,\n verbose: opts.verbose,\n });\n }\n return resolvedCtx;\n }\n\n const lazyCtx: CommandContext = {\n get verbose() {\n return getCtx().verbose;\n },\n getClient() {\n return getCtx().getClient();\n },\n getClientFor(baseUrl: string) {\n return getCtx().getClientFor(baseUrl);\n },\n output(data: unknown) {\n return getCtx().output(data);\n },\n parseBody(raw: string) {\n return getCtx().parseBody(raw);\n },\n };\n\n register(cmd, lazyCtx);\n\n return cmd;\n}\n","// The input side of a plugin's interactive `walk`: ask one question, or show\n// a menu, and report whether the person answered or wants to move around.\n// Plugins drive the walk; this only talks to the terminal.\n\nimport prompts from \"prompts\";\n\nexport type Nav = \"back\" | \"skip\" | \"menu\" | \"quit\";\n\n/** An answer, or a request to move: back a question, skip it, open the\n * menu, or stop (Ctrl-C / Esc). */\nexport type Reply = { value: string } | { nav: Nav };\n\nexport interface Choice {\n title: string;\n value: string;\n description?: string;\n}\n\nexport interface Question {\n /** A line above the prompt, e.g. \"Business Info · 3 left\". */\n section?: string;\n message: string;\n /** `secret` hides the typing; `search` filters a long list as you type. */\n kind: \"text\" | \"secret\" | \"select\" | \"search\";\n choices?: Choice[];\n /** The saved answer, already masked if sensitive. Enter keeps it. */\n current?: string;\n hint?: string;\n canGoBack: boolean;\n}\n\nexport interface MenuItem<T extends string> {\n title: string;\n value: T;\n description?: string;\n disabled?: boolean;\n}\n\nexport interface Prompter {\n ask(question: Question): Promise<Reply>;\n /** Null when the person cancels the menu. */\n menu<T extends string>(\n message: string,\n items: ReadonlyArray<MenuItem<T>>,\n ): Promise<T | null>;\n note(text: string): void;\n}\n\nexport const NAV_HELP =\n \"Type < to go back, > to skip, ? for the menu. Every answer is saved as you go; Ctrl-C stops.\";\n\nconst TYPED_NAV: Record<string, Nav> = { \"<\": \"back\", \">\": \"skip\", \"?\": \"menu\" };\n\n/** A typed `<`, `>` or `?` moves instead of answering. */\nexport function parseTyped(raw: string): Reply {\n const nav = TYPED_NAV[raw.trim()];\n return nav ? { nav } : { value: raw };\n}\n\n// Navigation entries appended to a pick list. The NUL prefix keeps them\n// from colliding with a real option value.\nconst NAV_PREFIX = \"\\u0000\";\n\nfunction navChoices(canGoBack: boolean): Choice[] {\n return [\n ...(canGoBack ? [{ title: \"← Back\", value: `${NAV_PREFIX}back` }] : []),\n { title: \"Skip →\", value: `${NAV_PREFIX}skip` },\n { title: \"Menu…\", value: `${NAV_PREFIX}menu` },\n ];\n}\n\nfunction fromChoice(value: string): Reply {\n return value.startsWith(NAV_PREFIX)\n ? { nav: value.slice(NAV_PREFIX.length) as Nav }\n : { value };\n}\n\n/** A prompter on the terminal, built on `prompts`. */\nexport function terminalPrompter(): Prompter {\n let cancelled = false;\n const onCancel = () => {\n cancelled = true;\n return false;\n };\n\n return {\n async ask(q) {\n cancelled = false;\n if (q.section) process.stdout.write(`\\n${q.section}\\n`);\n const message = q.current\n ? `${q.message} (saved: ${q.current})`\n : q.message;\n\n if (q.kind === \"select\" || q.kind === \"search\") {\n const choices = [...(q.choices ?? []), ...navChoices(q.canGoBack)];\n const initial = Math.max(\n 0,\n choices.findIndex((c) => c.title === q.current || c.value === q.current),\n );\n const { answer } = (await prompts(\n {\n type: q.kind === \"search\" ? \"autocomplete\" : \"select\",\n name: \"answer\",\n message,\n choices,\n initial,\n hint: q.hint,\n ...(q.kind === \"search\"\n ? {\n suggest: async (input: string, all: Choice[]) => {\n const needle = input.trim().toLowerCase();\n return all.filter(\n (c) =>\n c.value.startsWith(NAV_PREFIX) ||\n c.title.toLowerCase().includes(needle) ||\n c.value.toLowerCase().includes(needle),\n );\n },\n }\n : {}),\n } as prompts.PromptObject<\"answer\">,\n { onCancel },\n )) as { answer?: string };\n if (cancelled || answer === undefined) return { nav: \"quit\" };\n return fromChoice(answer);\n }\n\n const { answer } = (await prompts(\n {\n type: q.kind === \"secret\" ? \"password\" : \"text\",\n name: \"answer\",\n message: q.hint ? `${message} — ${q.hint}` : message,\n // A secret's saved value is masked, so it can't pre-fill; Enter on\n // an empty secret keeps what's saved.\n ...(q.kind === \"text\" && q.current ? { initial: q.current } : {}),\n },\n { onCancel },\n )) as { answer?: string };\n if (cancelled || answer === undefined) return { nav: \"quit\" };\n // Enter on a secret that's already saved keeps it: moving on without\n // an answer is a skip, so a walk never saves an empty value over it.\n if (q.kind === \"secret\" && q.current && answer === \"\") return { nav: \"skip\" };\n return parseTyped(answer);\n },\n\n async menu(message, items) {\n cancelled = false;\n const { choice } = (await prompts(\n {\n type: \"select\",\n name: \"choice\",\n message,\n choices: items.map((item) => ({\n title: item.title,\n value: item.value,\n description: item.description,\n disabled: item.disabled,\n })),\n },\n { onCancel },\n )) as { choice?: string };\n if (cancelled || choice === undefined) return null;\n return choice as (typeof items)[number][\"value\"];\n },\n\n note(text) {\n process.stdout.write(`${text}\\n`);\n },\n };\n}\n\n/** A walk needs a person at a terminal; scripts and agents use the\n * non-interactive commands it names. */\nexport function assertInteractive(\n alternative: string,\n isTTY: boolean = Boolean(process.stdin.isTTY && process.stdout.isTTY),\n): void {\n if (!isTTY) {\n throw new Error(\n `walk asks questions in a terminal. In a script, use ${alternative}.`,\n );\n }\n}\n","/**\n * The canonical GitSync commit-subject format, shared by every kind of\n * Fluid-managed repository — Mist apps, Themes, Portals and Widgets.\n *\n * Each kind used to invent its own wording, so `git log` across a\n * company's repos read as four unrelated systems and only some commits\n * said who made them. One shape now covers all of them:\n *\n * <Actor Name>: <Action> · <timestamp> (<public_id>)\n * Mike Tingey: Publish portal Acme Portal · 2026-08-06 12:00:00 (0182d21e-…)\n *\n * Name and action come first so `git log --oneline` stays scannable in a\n * narrow terminal; the long UUID sits at the end where truncation eats\n * it instead of the prose. The timestamp is redundant with git's own\n * committer date but is kept inline because it survives cherry-picks,\n * rebases and the hosting providers' deployment feeds, which surface the\n * subject and nothing else.\n *\n * System-authored commits (auto-provisioning, sweeper retries, webhook\n * fan-out, an unreachable `/api/me`) use the literal actor name `Fluid`\n * and carry no UUID, so they parse with the same grammar rather than\n * needing a second one:\n *\n * Fluid: Publish theme Aurora · 2026-08-06 12:00:00\n *\n * The Ruby side mirrors this in `GitSync::CommitSubject`\n * (fluid/app/models/git_sync/commit_subject.rb) and Mist Desktop mirrors\n * the parser in `apps/mist-desktop/src/shared/git-commit-subject.ts`.\n * Change all three together.\n */\n\n/** The person a commit is attributed to. */\nexport interface GitSyncActor {\n /** Display name: full_name → first+last → email → `user-<id>`. */\n readonly name: string;\n /** The user's stable public UUID, rendered as a ` (uuid)` suffix. */\n readonly publicId: string | null;\n}\n\n/** Actor name used when no human triggered the commit. */\nexport const SYSTEM_ACTOR_NAME = \"Fluid\";\n\n/** Parseable fallback when a caller supplies only grammar delimiters or\n * whitespace as the action (for example, `-m \"·\"`). */\nconst DEFAULT_ACTION = \"Sync changes\";\n\n/**\n * A subject broken back into its parts. `publicId` is null for\n * system-authored commits and for actors whose public id was unknown at\n * commit time.\n */\nexport interface ParsedGitSyncSubject {\n readonly actorName: string;\n readonly action: string;\n /** The inline `YYYY-MM-DD HH:MM:SS` stamp, in UTC. */\n readonly timestamp: string;\n readonly publicId: string | null;\n}\n\n/**\n * Matches the canonical shape. The actor group is greedy so a display\n * name containing `\": \"` (e.g. `Tingey: Mike`) splits at the LAST colon\n * before the ` · ` separator — safe because {@link subjectFragment}\n * strips both delimiters out of every value we interpolate into an\n * action.\n */\nconst SUBJECT_RE =\n /^(.+): (.+?) · (\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})(?: \\(([^()]+)\\))?$/;\n\n/** `2026-08-06T12:00:00.000Z` → `2026-08-06 12:00:00`. */\nexport function formatCommitTimestamp(when: Date): string {\n return when.toISOString().slice(0, 19).replace(\"T\", \" \");\n}\n\n/**\n * Neutralize the two sequences the grammar splits on, so no value can\n * shift the actor / action / uuid boundaries:\n *\n * `\": \"` separates the actor from the action\n * `\"·\"` separates the action from the timestamp\n *\n * Only those exact sequences are replaced, not every colon — `12:30`\n * and `fix:handle-nulls` survive intact, because neither can be\n * mistaken for a field boundary. A trailing colon is likewise safe: the\n * parser backtracks past it.\n *\n * {@link gitSyncCommitSubject} applies this to every action, so callers\n * don't have to remember. It's idempotent, so calling it yourself on an\n * interpolated resource name (as the CLIs do) costs nothing.\n */\nexport function subjectFragment(value: string): string {\n return value\n .replace(/:\\s/g, \" \")\n .replace(/·/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\n/**\n * Build a canonical commit subject.\n *\n * @param action - What happened, e.g. `Publish theme Aurora`. Run any\n * interpolated resource name through {@link subjectFragment} first.\n * @param actor - The signed-in user, or null for system-authored\n * commits (which are attributed to `Fluid`).\n * @param when - Injectable clock, so callers and tests get a stable\n * stamp.\n */\nexport function gitSyncCommitSubject(\n action: string,\n actor: GitSyncActor | null,\n when: Date = new Date(),\n): string {\n // Sanitized HERE rather than at each call site. The action is the one\n // field a user fully controls (`-m`), and a `\": \"` in it would be read\n // back as part of the actor — `-m \"Publish: Acme\"` attributing the\n // commit to \"Mike Tingey: Publish\". Doing it at the choke point means\n // no current or future caller can reintroduce that.\n //\n // The actor name is deliberately NOT sanitized: a real name may\n // contain `\": \"` (`Tingey: Mike`), and the greedy actor group parses\n // that correctly precisely because the action can no longer contain\n // the delimiter.\n const safeAction = subjectFragment(action) || DEFAULT_ACTION;\n const stamp = formatCommitTimestamp(when);\n if (!actor) return `${SYSTEM_ACTOR_NAME}: ${safeAction} · ${stamp}`;\n const idSuffix = actor.publicId ? ` (${actor.publicId})` : \"\";\n return `${actor.name}: ${safeAction} · ${stamp}${idSuffix}`;\n}\n\n/**\n * Split a canonical subject back into its parts, or null when the\n * subject predates the format (legacy CLI wording such as `push @ <iso>`\n * or a hand-written commit). Callers fall back to rendering the raw\n * subject.\n */\nexport function parseGitSyncSubject(\n subject: string | null | undefined,\n): ParsedGitSyncSubject | null {\n if (!subject) return null;\n const match = subject.trim().match(SUBJECT_RE);\n if (!match) return null;\n const [, actorName, action, timestamp, publicId] = match;\n // The regex guarantees these groups, but noUncheckedIndexedAccess\n // types them as possibly-undefined.\n if (!actorName || !action || !timestamp) return null;\n return {\n actorName: actorName.trim(),\n action: action.trim(),\n timestamp,\n publicId: publicId?.trim() || null,\n };\n}\n\n/**\n * The person a canonical subject is attributed to, or null when the\n * subject isn't canonical or was system-authored. Use this for \"who\n * shipped this\" UI — a `Fluid:` commit has no human to name.\n */\nexport function gitSyncSubjectAuthor(\n subject: string | null | undefined,\n): string | null {\n const parsed = parseGitSyncSubject(subject);\n if (!parsed || parsed.actorName === SYSTEM_ACTOR_NAME) return null;\n return parsed.actorName;\n}\n\n/** The `/api/me` fields the actor is derived from. */\ninterface MeUser {\n readonly id?: unknown;\n readonly public_id?: unknown;\n readonly full_name?: unknown;\n readonly first_name?: unknown;\n readonly last_name?: unknown;\n readonly email?: unknown;\n}\n\n/** Some `/api/me` deployments nest the user, others return it flat. */\ninterface MeResponse extends MeUser {\n readonly user?: MeUser;\n}\n\nfunction str(value: unknown): string | null {\n if (typeof value === \"string\" && value.trim().length > 0) return value.trim();\n return null;\n}\n\n/**\n * Resolve an `/api/me` body to a commit actor, with the name fallback\n * chain `full_name → first+last → email → user-<id>`. Returns null when\n * the body carries no usable identity at all, which callers treat the\n * same as an unreachable `/api/me`: a system-authored commit.\n */\nexport function gitSyncActorFromMe(raw: MeResponse): GitSyncActor | null {\n const user = raw.user ?? raw;\n const composed = [str(user.first_name), str(user.last_name)]\n .filter((part): part is string => part !== null)\n .join(\" \")\n .trim();\n const name =\n str(user.full_name) ??\n (composed.length > 0 ? composed : null) ??\n str(user.email) ??\n (typeof user.id === \"number\" && Number.isFinite(user.id)\n ? `user-${user.id}`\n : null);\n if (!name) return null;\n const publicId =\n str(user.public_id) ??\n (typeof user.id === \"number\" && Number.isFinite(user.id)\n ? String(user.id)\n : null);\n return { name, publicId };\n}\n","/**\n * Turn a set of changed paths into the `<Action>` half of a canonical\n * GitSync commit subject (see commit-subject.ts).\n *\n * Publishes used to be stamped with a constant — \"Publish from Mist\n * Desktop\", \"Publish theme Aurora\" — so `git log` told you a publish\n * happened but never what it carried. Every entry in the history looked\n * identical, which made the log useless for finding the commit that\n * introduced something.\n *\n * This derives a short, honest phrase from the staged diff:\n *\n * Update templates/product.liquid\n * Update 3 files in templates\n * Add 2 files, update 3, delete 1\n *\n * It is deliberately mechanical. A caller that knows the INTENT behind a\n * change (Mist Desktop, or a human passing `-m`) should pass that\n * instead — an intent describes why, and no diff can recover that. This\n * is the fallback for when nobody supplied one.\n *\n * Pure: callers collect the diff (each CLI has its own git layer) and\n * hand the entries here, so the wording can't drift between kinds.\n */\n\n/** Git's porcelain status letters, narrowed to what we act on. */\nexport type ChangeStatus = \"added\" | \"modified\" | \"deleted\" | \"renamed\";\n\n/**\n * What an auto-commit helper accepts as its subject: either a literal\n * (the caller already knows the intent — a `-m` message), or a resolver\n * handed the staged changes so it can describe them.\n *\n * The resolver form exists because staging is decided INSIDE the commit\n * helpers — they `git add -A` and then unstage private paths — so only\n * they know the final file set. A summary built by the caller beforehand\n * would describe a diff the commit doesn't carry.\n *\n * It may return a promise so a caller can start slow work (an `/api/me`\n * identity lookup, say) BEFORE the git reconcile and await it here, at\n * the last possible moment — overlapping it with the fetch/merge instead\n * of paying for it serially up front. Anything awaited here is on the\n * critical path of the commit, so it must be bounded and best-effort:\n * a publish must never hang waiting to describe itself.\n */\nexport type CommitMessage =\n | string\n | ((changes: readonly ChangeEntry[]) => string | Promise<string>);\n\nexport interface ChangeEntry {\n readonly status: ChangeStatus;\n /** Repo-relative path, forward-slashed. */\n readonly path: string;\n}\n\n/** Map a `git diff --name-status` letter to a status. `R`/`C` carry a\n * similarity score (`R100`), so only the leading letter is read.\n * Unknown letters (`U` unmerged, `T` typechange) count as modified —\n * the file did change, and mislabeling the verb is better than\n * dropping it from the count. */\nexport function changeStatusFromCode(code: string): ChangeStatus {\n switch (code.trim().charAt(0).toUpperCase()) {\n case \"A\":\n return \"added\";\n case \"D\":\n return \"deleted\";\n case \"R\":\n return \"renamed\";\n default:\n return \"modified\";\n }\n}\n\n/**\n * Parse `git diff --cached --name-status -z` output.\n *\n * NUL-delimited on purpose: paths containing spaces, quotes or newlines\n * are legal in git and common in themes (`assets/hero image.png`), and\n * the newline-delimited form quotes and escapes them, which would corrupt\n * both the count and the path. With `-z`, records are\n * `status\\0path\\0` — except renames and copies, which are\n * `status\\0from\\0to\\0` and consume an extra field.\n */\nexport function parseNameStatusZ(stdout: string): ChangeEntry[] {\n const fields = stdout.split(\"\\0\").filter((f) => f.length > 0);\n const entries: ChangeEntry[] = [];\n for (let i = 0; i < fields.length; ) {\n const code = fields[i];\n if (code === undefined) break;\n const status = changeStatusFromCode(code);\n if (status === \"renamed\") {\n // status, from, to — the destination is the path we report.\n const to = fields[i + 2];\n if (to === undefined) break;\n entries.push({ status, path: to });\n i += 3;\n continue;\n }\n const path = fields[i + 1];\n if (path === undefined) break;\n entries.push({ status, path });\n i += 2;\n }\n return entries;\n}\n\n/** Top-level directory of a path, or null for a repo-root file. */\nfunction topLevelDir(path: string): string | null {\n const slash = path.indexOf(\"/\");\n return slash > 0 ? path.slice(0, slash) : null;\n}\n\nfunction plural(count: number, noun: string): string {\n return `${count} ${noun}${count === 1 ? \"\" : \"s\"}`;\n}\n\nconst VERB_FOR: Record<ChangeStatus, string> = {\n added: \"Add\",\n modified: \"Update\",\n deleted: \"Delete\",\n renamed: \"Rename\",\n};\n\n/**\n * Build the action phrase for a set of changes, or null when there is\n * nothing to describe — the caller then falls back to its own constant\n * (`Publish theme Aurora`), so an empty diff never produces a subject\n * like \"Update 0 files\".\n *\n * @param entries - The staged changes.\n * @param maxLength - Clip the result to keep `git log --oneline`\n * readable. The subject also carries a name, timestamp and UUID, so\n * the action gets a modest budget.\n */\nexport function summarizeChanges(\n entries: readonly ChangeEntry[],\n maxLength = 72,\n): string | null {\n if (entries.length === 0) return null;\n\n // A single change is the most useful case and the most common one:\n // name the actual file rather than counting to one.\n if (entries.length === 1) {\n const only = entries[0]!;\n return clip(`${VERB_FOR[only.status]} ${only.path}`, maxLength);\n }\n\n // Group by verb, ordered so the dominant kind of change leads.\n const byStatus = new Map<ChangeStatus, number>();\n for (const entry of entries) {\n byStatus.set(entry.status, (byStatus.get(entry.status) ?? 0) + 1);\n }\n\n // All one verb → \"Update 3 files\", plus a location when they share one.\n if (byStatus.size === 1) {\n const status = entries[0]!.status;\n const dirs = new Set(entries.map((e) => topLevelDir(e.path)));\n const only = dirs.size === 1 ? [...dirs][0] : null;\n const phrase = `${VERB_FOR[status]} ${plural(entries.length, \"file\")}`;\n return clip(only ? `${phrase} in ${only}` : phrase, maxLength);\n }\n\n // Mixed → lead with the largest group, then the rest as bare counts:\n // \"Update 3 files, add 2, delete 1\".\n const ordered = [...byStatus.entries()].sort(\n ([aStatus, aCount], [bStatus, bCount]) =>\n bCount - aCount || VERB_FOR[aStatus].localeCompare(VERB_FOR[bStatus]),\n );\n const [leadStatus, leadCount] = ordered[0]!;\n const rest = ordered\n .slice(1)\n .map(([status, count]) => `${VERB_FOR[status].toLowerCase()} ${count}`);\n const head = `${VERB_FOR[leadStatus]} ${plural(leadCount, \"file\")}`;\n return clip([head, ...rest].join(\", \"), maxLength);\n}\n\nfunction clip(text: string, maxLength: number): string {\n return text.length > maxLength\n ? `${text.slice(0, maxLength - 1).trimEnd()}…`\n : text;\n}\n","/**\n * Cache the signed-in user's commit identity on the active profile.\n *\n * Commit subjects are stamped `<name>: <action> · <ts> (<public_id>)`,\n * which needs the user's display name and public id. `/api/me` is the\n * only source, and it can be slow — so paying for it on EVERY push, to\n * learn a name that changes approximately never, is the wrong trade.\n *\n * The identity is stored per profile (profiles are per token + company,\n * so the cache can't leak across accounts) and refreshed only when\n * missing or stale. Login and company-switch write a fresh profile,\n * which drops the cached identity with it.\n *\n * Every failure mode here is non-fatal: an unreadable or unwritable\n * config just means we fall back to the network, and a failed network\n * lookup means a `Fluid:` system commit. A publish must never fail\n * because we couldn't work out whose name to put on it.\n */\n\nimport { readConfig, updateConfig } from \"../config/config.js\";\nimport type { GitSyncActor } from \"./commit-subject.js\";\n\n/**\n * How long a cached identity is trusted. Long, because a display name\n * changing is rare — but not forever, so a rename does eventually reach\n * the commit log without the user having to re-login.\n */\nexport const ACTOR_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days\n\n/** Cached identity as persisted on a profile. */\nexport interface CachedGitSyncActor {\n readonly name: string;\n readonly publicId: string | null;\n /** ISO-8601. */\n readonly cachedAt: string;\n}\n\nfunction isFresh(cached: CachedGitSyncActor, now: number): boolean {\n const at = Date.parse(cached.cachedAt);\n // An unparseable timestamp is treated as stale rather than trusted\n // forever — a corrupt field shouldn't pin a wrong name permanently.\n if (Number.isNaN(at)) return false;\n return now - at < ACTOR_CACHE_TTL_MS;\n}\n\n/** The cached identity for the active profile, or null. */\nexport function readCachedActor(now = Date.now()): GitSyncActor | null {\n try {\n const config = readConfig();\n const profileName = config.activeProfile;\n if (!profileName) return null;\n const cached = config.profiles[profileName]?.gitSyncActor;\n if (!cached || !cached.name || !isFresh(cached, now)) return null;\n return { name: cached.name, publicId: cached.publicId ?? null };\n } catch {\n return null;\n }\n}\n\n/** Persist an identity against the active profile. Best-effort. */\nexport function writeCachedActor(actor: GitSyncActor, now = new Date()): void {\n try {\n updateConfig((config) => {\n const profileName = config.activeProfile;\n if (!profileName) return config;\n const profile = config.profiles[profileName];\n if (!profile) return config;\n return {\n ...config,\n profiles: {\n ...config.profiles,\n [profileName]: {\n ...profile,\n gitSyncActor: {\n name: actor.name,\n publicId: actor.publicId,\n cachedAt: now.toISOString(),\n },\n },\n },\n };\n });\n } catch {\n // A read-only or malformed config just means we look it up again\n // next time — never a reason to fail a publish.\n }\n}\n\n/**\n * The commit identity, from cache when possible.\n *\n * @param fetchActor - Network lookup, used only on a cache miss. Each\n * CLI has its own client, so the caller supplies it. It must already\n * be bounded — this function adds no timeout of its own.\n *\n * Returns null when there's no cache and the lookup fails or yields no\n * usable identity; callers then write a `Fluid:` system commit.\n */\nexport async function resolveGitSyncActor(\n fetchActor: () => Promise<GitSyncActor | null>,\n): Promise<GitSyncActor | null> {\n const cached = readCachedActor();\n if (cached) return cached;\n\n const fetched = await fetchActor().catch(() => null);\n if (fetched) writeCachedActor(fetched);\n return fetched;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluid-app/fluid-cli",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "Core CLI for Fluid Commerce — auth, config, and plugin system",
5
5
  "bin": {
6
6
  "fluid": "./dist/bin/fluid.mjs"