@fluid-app/fluid-cli-channels 0.0.0-stage → 0.1.1
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 +48 -2
- package/dist/index.d.mts +30 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1125 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +82 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/channels.ts","../src/schemas.ts","../src/api.ts","../src/browser.ts","../src/issues.ts","../src/walk.ts","../src/index.ts"],"sourcesContent":["// The sales channels this plugin connects, and how each one's wire shape\n// maps onto the plugin's own output.\n\nexport class ChannelsError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ChannelsError\";\n }\n}\n\nexport type ChannelId = \"tiktok\" | \"meta\" | \"google\";\n\nexport interface Channel {\n id: ChannelId;\n label: string;\n /** Company admin API base for this channel's connection and listings. */\n base: string;\n /** Listings resource: TikTok and Google call them product listings, Meta\n * calls them catalog listings. The response keys follow the same name. */\n listings: \"product_listings\" | \"catalog_listings\";\n listing: \"product_listing\" | \"catalog_listing\";\n /** Wire field that holds the connected account's id and name. */\n accountIdField: string;\n accountNameField: string;\n /** Wire field that holds the listing's id on the channel. */\n channelProductIdField: string;\n /** Channel-side review fields copied into `review`, when the channel has them. */\n reviewFields: string[];\n /** Connection fields specific to this channel, copied into `details`. */\n detailFields: string[];\n}\n\nexport const CHANNELS: Record<ChannelId, Channel> = {\n tiktok: {\n id: \"tiktok\",\n label: \"TikTok Shop\",\n base: \"/api/v202506/tiktok\",\n listings: \"product_listings\",\n listing: \"product_listing\",\n accountIdField: \"shop_id\",\n accountNameField: \"shop_name\",\n channelProductIdField: \"tiktok_product_id\",\n reviewFields: [],\n detailFields: [\"region\", \"seller_type\", \"tiktok_warehouse_id\"],\n },\n meta: {\n id: \"meta\",\n label: \"Meta (Facebook and Instagram)\",\n base: \"/api/v202506/meta\",\n listings: \"catalog_listings\",\n listing: \"catalog_listing\",\n accountIdField: \"catalog_id\",\n accountNameField: \"catalog_name\",\n channelProductIdField: \"product_retailer_id\",\n reviewFields: [\n \"marketplace_review_status\",\n \"shops_review_status\",\n \"whatsapp_review_status\",\n \"instagram_review_status\",\n ],\n detailFields: [\n \"meta_business_id\",\n \"meta_page_id\",\n \"meta_instagram_business_account_id\",\n \"waba_id\",\n \"phone_number_id\",\n ],\n },\n google: {\n id: \"google\",\n label: \"Google Merchant Center\",\n base: \"/api/v202506/google\",\n listings: \"product_listings\",\n listing: \"product_listing\",\n accountIdField: \"merchant_center_id\",\n accountNameField: \"merchant_center_name\",\n channelProductIdField: \"item_group_id\",\n reviewFields: [\"google_review_status\", \"content_language\", \"feed_label\"],\n detailFields: [\"google_mca_id\"],\n },\n};\n\nexport const CHANNEL_IDS = Object.keys(CHANNELS) as ChannelId[];\n\nexport function channelOf(id: string): Channel {\n const channel = CHANNELS[id.trim().toLowerCase() as ChannelId];\n if (!channel) {\n throw new ChannelsError(\n `\"${id}\" isn't a channel this plugin connects. Use one of: ${CHANNEL_IDS.join(\", \")}.`,\n );\n }\n return channel;\n}\n\nexport type Wire = Record<string, unknown>;\n\nexport interface ConnectionSummary {\n channel: ChannelId;\n connected: boolean;\n id: number;\n status: string;\n account: { id: string | null; name: string | null };\n connectedAt: string | null;\n disconnectedAt: string | null;\n accessTokenExpiresAt: string | null;\n lastError: unknown;\n details: Record<string, unknown>;\n}\n\nconst str = (value: unknown): string | null =>\n value === undefined || value === null ? null : String(value);\n\nexport function summarizeConnection(\n channel: Channel,\n wire: Wire,\n): ConnectionSummary {\n const details: Record<string, unknown> = {};\n for (const field of channel.detailFields)\n details[field] = wire[field] ?? null;\n return {\n channel: channel.id,\n connected: wire[\"status\"] === \"connected\",\n id: Number(wire[\"id\"]),\n status: String(wire[\"status\"] ?? \"unknown\"),\n account: {\n id: str(wire[channel.accountIdField]),\n name: str(wire[channel.accountNameField]),\n },\n connectedAt: str(wire[\"connected_at\"]),\n disconnectedAt: str(wire[\"disconnected_at\"]),\n accessTokenExpiresAt: str(wire[\"access_token_expires_at\"]),\n lastError: wire[\"last_error\"] ?? null,\n details,\n };\n}\n\nexport interface ListingSummary {\n id: number;\n productId: number;\n status: string;\n auditStatus: string | null;\n /** The listing's id on the channel; null until the first publish lands. */\n channelProductId: string | null;\n review: Record<string, unknown>;\n lastPublishedAt: string | null;\n lastSyncedAt: string | null;\n lastError: unknown;\n}\n\nexport function summarizeListing(channel: Channel, wire: Wire): ListingSummary {\n const review: Record<string, unknown> = {};\n for (const field of channel.reviewFields) review[field] = wire[field] ?? null;\n return {\n id: Number(wire[\"id\"]),\n productId: Number(wire[\"product_id\"]),\n status: String(wire[\"status\"] ?? \"unknown\"),\n auditStatus: str(wire[\"audit_status\"]),\n channelProductId: str(wire[channel.channelProductIdField]),\n review,\n lastPublishedAt: str(wire[\"last_published_at\"]),\n lastSyncedAt: str(wire[\"last_synced_at\"]),\n lastError: wire[\"last_error\"] ?? null,\n };\n}\n\n/** A listing that's no longer on the channel: removed by you (`deleted`),\n * or, on TikTok, deactivated by you. Adding its product again re-lists it. */\nexport function isRemoved(listing: ListingSummary): boolean {\n return (\n listing.status === \"deleted\" || listing.status === \"seller_deactivated\"\n );\n}\n\n/** A listing whose product is on the channel now, or has been sent to it. */\nexport function isListed(listing: ListingSummary): boolean {\n return !isRemoved(listing);\n}\n","// Shapes of the requests this plugin sends and the responses it reads. A\n// response that doesn't match fails with a clear error instead of producing\n// output with missing fields.\n\nimport { z } from \"zod\";\n\nimport { ChannelsError } from \"./channels.js\";\n\n/** A connection as the API returns it. Channel-specific fields pass through. */\nexport const connectionWire = z.looseObject({\n id: z.number(),\n status: z.string(),\n});\n\nexport const connectionResponse = z.object({ connection: connectionWire });\n\nexport const authorizeUrlResponse = z.object({ url: z.string().min(1) });\n\n/** A listing as the API returns it. Channel-specific fields pass through. */\nexport const listingWire = z.looseObject({\n id: z.number(),\n product_id: z.number(),\n status: z.string(),\n audit_status: z.string().nullish(),\n});\n\n/** The cursor block of a listings page; the rows are read with listingsResponse. */\nexport const listingsPageMeta = z.object({\n meta: z\n .object({\n pagination: z.object({ next_cursor: z.string().nullish() }).optional(),\n })\n .optional(),\n});\n\nexport const listingResponse = (key: string) =>\n z.object({ [key]: listingWire });\n\nexport const listingsResponse = (key: string) =>\n z.object({ [key]: z.array(listingWire) });\n\nexport const productsPage = z.object({\n products: z.array(\n z.looseObject({ id: z.number(), title: z.string().nullish() }),\n ),\n meta: z\n .object({\n pagination: z.object({ total_pages: z.number().nullish() }).optional(),\n })\n .optional(),\n});\n\nexport const collectionsPage = z.object({\n collections: z.array(\n z.looseObject({ id: z.number(), title: z.string().nullish() }),\n ),\n meta: z\n .object({\n pagination: z.object({ next_cursor: z.string().nullish() }).optional(),\n })\n .optional(),\n});\n\nexport const productIdsInput = z.object({\n product_ids: z.array(z.number().int().positive()).min(1),\n});\n\nexport const warehouseInput = z.object({\n tiktok_warehouse_id: z.string().trim().min(1),\n});\n\nexport const regionInput = z.object({ region: z.enum([\"US\", \"GLOBAL\"]) });\n\n/** Validate a request body before it's sent, naming the bad input. */\nexport function validateInput<T extends z.ZodType>(\n schema: T,\n data: unknown,\n what: string,\n): z.infer<T> {\n const result = schema.safeParse(data);\n if (result.success) return result.data;\n throw new ChannelsError(\n `Invalid ${what}: ${result.error.issues[0]?.message ?? \"invalid value\"}.`,\n );\n}\n\n/** Validate a response against a schema, naming what was being read on failure. */\nexport function parse<T extends z.ZodType>(\n schema: T,\n data: unknown,\n what: string,\n): z.infer<T> {\n const result = schema.safeParse(data);\n if (result.success) return result.data;\n const issue = result.error.issues[0];\n const where =\n issue && issue.path.length > 0 ? ` at ${issue.path.join(\".\")}` : \"\";\n throw new ChannelsError(\n `Fluid returned an unexpected ${what}${where}: ${issue?.message ?? \"invalid shape\"}.`,\n );\n}\n","// Calls to the company admin sales channel API (connection, listings) and\n// the product list the whole-catalog choice reads from.\n\nimport {\n ChannelsError,\n isListed,\n summarizeConnection,\n summarizeListing,\n type Channel,\n type ConnectionSummary,\n type ListingSummary,\n type Wire,\n} from \"./channels.js\";\nimport {\n authorizeUrlResponse,\n connectionResponse,\n listingResponse,\n collectionsPage,\n listingsPageMeta,\n listingsResponse,\n parse,\n productIdsInput,\n productsPage,\n regionInput,\n validateInput,\n warehouseInput,\n} from \"./schemas.js\";\n\n/** The slice of the CLI's API client these calls use. */\nexport interface ChannelsClient {\n get<T>(endpoint: string, params?: Record<string, unknown>): Promise<T>;\n post<T>(endpoint: string, body?: unknown): Promise<T>;\n patch<T>(endpoint: string, body?: unknown): Promise<T>;\n delete<T>(endpoint: string): Promise<T>;\n}\n\nconst LISTING_PAGE_SIZE = 100;\nconst MAX_LISTING_PAGES = 100;\nconst PRODUCT_PAGE_SIZE = 100;\nconst MAX_PRODUCT_PAGES = 100;\n/** Product IDs sent per publish request. Each one queues a publish job. */\nexport const ADD_BATCH_SIZE = 50;\n\nfunction statusOf(err: unknown): number | undefined {\n const status = (err as { status?: unknown } | null)?.status;\n return typeof status === \"number\" ? status : undefined;\n}\n\nexport interface NotConnected {\n channel: Channel[\"id\"];\n connected: false;\n status: \"not_connected\";\n}\n\n/** The channel's connection, or a not-connected marker when the company has\n * none (the API answers 404). */\nexport async function getConnection(\n client: ChannelsClient,\n channel: Channel,\n): Promise<ConnectionSummary | NotConnected> {\n try {\n const data = parse(\n connectionResponse,\n await client.get<unknown>(`${channel.base}/connection`),\n `${channel.label} connection`,\n );\n return summarizeConnection(channel, data.connection);\n } catch (err) {\n if (statusOf(err) === 404) {\n return { channel: channel.id, connected: false, status: \"not_connected\" };\n }\n throw err;\n }\n}\n\n/** Requires a connection; listings and disconnect have nothing to act on\n * without one. */\nexport async function requireConnection(\n client: ChannelsClient,\n channel: Channel,\n): Promise<ConnectionSummary> {\n const connection = await getConnection(client, channel);\n if (!(\"id\" in connection)) {\n throw new ChannelsError(\n `This company has no ${channel.label} connection. Run fluid channels connect ${channel.id} first.`,\n );\n }\n return connection;\n}\n\n/** Ask Fluid for the channel's sign-in URL. A person opens it, approves\n * Fluid's access on the channel, and the channel sends them back to Fluid,\n * which saves the connection. */\nexport async function authorizeUrl(\n client: ChannelsClient,\n channel: Channel,\n options: { region?: string } = {},\n): Promise<string> {\n const body =\n channel.id === \"tiktok\"\n ? validateInput(\n regionInput,\n { region: options.region ?? \"US\" },\n \"--region\",\n )\n : {};\n const data = parse(\n authorizeUrlResponse,\n await client.post<unknown>(\n `${channel.base}/connection/authorize_url`,\n body,\n ),\n \"sign-in URL response\",\n );\n return data.url;\n}\n\nexport async function disconnect(\n client: ChannelsClient,\n channel: Channel,\n): Promise<ConnectionSummary> {\n const data = parse(\n connectionResponse,\n await client.delete<unknown>(`${channel.base}/connection`),\n \"disconnect response\",\n );\n return summarizeConnection(channel, data.connection);\n}\n\nexport async function setTiktokWarehouse(\n client: ChannelsClient,\n channel: Channel,\n tiktokWarehouseId: string,\n): Promise<ConnectionSummary> {\n const body = validateInput(\n warehouseInput,\n { tiktok_warehouse_id: tiktokWarehouseId },\n \"TikTok warehouse ID\",\n );\n const data = parse(\n connectionResponse,\n await client.patch<unknown>(`${channel.base}/connection`, body),\n \"warehouse update response\",\n );\n return summarizeConnection(channel, data.connection);\n}\n\n/** Every listing on the connection, newest first. */\nexport async function listListings(\n client: ChannelsClient,\n channel: Channel,\n): Promise<ListingSummary[]> {\n const all: ListingSummary[] = [];\n let cursor: string | undefined;\n for (let page = 0; page < MAX_LISTING_PAGES; page += 1) {\n const raw = await client.get<unknown>(\n `${channel.base}/${channel.listings}`,\n {\n page: { limit: LISTING_PAGE_SIZE, ...(cursor ? { cursor } : {}) },\n },\n );\n const what = `${channel.label} listings page`;\n const rows = parse(listingsResponse(channel.listings), raw, what)[\n channel.listings\n ] as Wire[];\n all.push(...rows.map((row) => summarizeListing(channel, row)));\n const next = parse(listingsPageMeta, raw, what).meta?.pagination\n ?.next_cursor;\n if (!next || rows.length === 0) return all;\n cursor = next;\n }\n throw new ChannelsError(\n `This connection has more than ${MAX_LISTING_PAGES * LISTING_PAGE_SIZE} listings; refusing to act on a partial list.`,\n );\n}\n\nexport interface ProductRef {\n id: number;\n title: string;\n}\n\n/** The company's active products, optionally only those in the given\n * collections. Draft and archived products are never sent to a channel. */\nexport async function listActiveProducts(\n client: ChannelsClient,\n collectionIds: number[] = [],\n): Promise<ProductRef[]> {\n const byId = new Map<number, ProductRef>();\n for (let page = 1; page <= MAX_PRODUCT_PAGES; page += 1) {\n const data = parse(\n productsPage,\n await client.get<unknown>(\"/api/v2025-06/products\", {\n page,\n per_page: PRODUCT_PAGE_SIZE,\n status: [\"active\"],\n ...(collectionIds.length > 0 ? { collection_ids: collectionIds } : {}),\n }),\n \"product list page\",\n );\n const products = data.products;\n for (const p of products) {\n byId.set(p.id, { id: p.id, title: p.title ?? `Product ${p.id}` });\n }\n const totalPages = data.meta?.pagination?.total_pages;\n const done =\n totalPages != null\n ? page >= totalPages\n : products.length < PRODUCT_PAGE_SIZE;\n if (done) return [...byId.values()];\n }\n throw new ChannelsError(\n `This company has more than ${MAX_PRODUCT_PAGES * PRODUCT_PAGE_SIZE} active products; refusing to add a partial catalog.`,\n );\n}\n\n/** IDs of the company's active products; see listActiveProducts. */\nexport async function listActiveProductIds(\n client: ChannelsClient,\n collectionIds: number[] = [],\n): Promise<number[]> {\n return (await listActiveProducts(client, collectionIds)).map((p) => p.id);\n}\n\nexport interface CollectionRef {\n id: number;\n title: string;\n}\n\n/** Every collection on the company, for picking which to send. */\nexport async function listCollections(\n client: ChannelsClient,\n): Promise<CollectionRef[]> {\n const all: CollectionRef[] = [];\n let cursor: string | undefined;\n for (let page = 0; page < MAX_LISTING_PAGES; page += 1) {\n const data = parse(\n collectionsPage,\n await client.get<unknown>(\"/api/v202604/company/collections\", {\n page: { limit: 100, ...(cursor ? { cursor } : {}) },\n }),\n \"collection list page\",\n );\n all.push(\n ...data.collections.map((c) => ({\n id: c.id,\n title: c.title ?? `Collection ${c.id}`,\n })),\n );\n const next = data.meta?.pagination?.next_cursor;\n if (!next || data.collections.length === 0) return all;\n cursor = next;\n }\n throw new ChannelsError(\"This company has too many collections to list.\");\n}\n\nexport interface AddPlan {\n toAdd: number[];\n alreadyListed: number[];\n previouslyRemoved: number[];\n}\n\n/**\n * Decide which products to send. A product already on the channel is\n * skipped. One you removed earlier is skipped too, unless you ask to bring\n * removed products back, so adding the whole catalog never undoes a\n * deliberate removal.\n */\nexport function planAdd(\n productIds: number[],\n listings: ListingSummary[],\n includeRemoved: boolean,\n): AddPlan {\n const byProduct = new Map(listings.map((l) => [l.productId, l]));\n const plan: AddPlan = { toAdd: [], alreadyListed: [], previouslyRemoved: [] };\n for (const id of new Set(productIds)) {\n const listing = byProduct.get(id);\n if (!listing) plan.toAdd.push(id);\n else if (isListed(listing)) plan.alreadyListed.push(id);\n else if (includeRemoved) plan.toAdd.push(id);\n else plan.previouslyRemoved.push(id);\n }\n return plan;\n}\n\n/** Send products to the channel in batches. Each new listing starts as a\n * draft and publishes in the background. */\nexport async function addProducts(\n client: ChannelsClient,\n channel: Channel,\n productIds: number[],\n): Promise<ListingSummary[]> {\n const added: ListingSummary[] = [];\n for (let start = 0; start < productIds.length; start += ADD_BATCH_SIZE) {\n const batch = productIds.slice(start, start + ADD_BATCH_SIZE);\n try {\n const body = validateInput(\n productIdsInput,\n { product_ids: batch },\n \"product IDs\",\n );\n const data = parse(\n listingsResponse(channel.listings),\n await client.post<unknown>(`${channel.base}/${channel.listings}`, body),\n \"publish response\",\n );\n const rows = data[channel.listings] as Wire[];\n added.push(...rows.map((row) => summarizeListing(channel, row)));\n } catch (err) {\n if (added.length === 0) throw err;\n throw new ChannelsError(\n `Added ${added.length} product(s), then a batch failed: ${err instanceof Error ? err.message : String(err)}. Run the same command again to continue; products already added are skipped.`,\n );\n }\n }\n return added;\n}\n\n/** Find the listing for each product ID, refusing names that have none. */\nexport function listingsForProducts(\n productIds: number[],\n listings: ListingSummary[],\n): { found: ListingSummary[]; notListed: number[] } {\n const byProduct = new Map(listings.map((l) => [l.productId, l]));\n const found: ListingSummary[] = [];\n const notListed: number[] = [];\n for (const id of new Set(productIds)) {\n const listing = byProduct.get(id);\n if (listing && isListed(listing)) found.push(listing);\n else notListed.push(id);\n }\n return { found, notListed };\n}\n\n/** Take a listing off the channel. A published listing is withdrawn from\n * the channel; one that never published is just marked removed. */\nexport async function removeListing(\n client: ChannelsClient,\n channel: Channel,\n listingId: number,\n): Promise<ListingSummary> {\n const data = parse(\n listingResponse(channel.listing),\n await client.delete<unknown>(\n `${channel.base}/${channel.listings}/${encodeURIComponent(String(listingId))}`,\n ),\n \"remove response\",\n );\n return summarizeListing(channel, data[channel.listing] as Wire);\n}\n\n/** Re-run a failed publish. Only listings whose status is publish_failed\n * can be retried. */\nexport async function retryListing(\n client: ChannelsClient,\n channel: Channel,\n listingId: number,\n): Promise<ListingSummary> {\n const data = parse(\n listingResponse(channel.listing),\n await client.post<unknown>(\n `${channel.base}/${channel.listings}/${encodeURIComponent(String(listingId))}/retry`,\n ),\n \"retry response\",\n );\n return summarizeListing(channel, data[channel.listing] as Wire);\n}\n","import { spawn } from \"node:child_process\";\n\n/** Open a URL in the default browser. Returns false when there's no way to. */\nexport function openInBrowser(url: string): boolean {\n const [command, args] =\n process.platform === \"darwin\"\n ? [\"open\", [url]]\n : process.platform === \"win32\"\n ? // Not `cmd /c start`: cmd reads the `&` between query\n // parameters as a command separator and truncates the link.\n [\"rundll32\", [\"url.dll,FileProtocolHandler\", url]]\n : [\"xdg-open\", [url]];\n try {\n const child = spawn(command as string, args as string[], {\n detached: true,\n stdio: \"ignore\",\n });\n child.on(\"error\", () => undefined);\n child.unref();\n return true;\n } catch {\n return false;\n }\n}\n","// Which listings need attention, and what to do about each.\n\nimport { isListed, isRemoved, type ListingSummary } from \"./channels.js\";\n\nexport type IssueAction =\n /** The publish failed; fix the cause, then retry it. */\n | \"retry\"\n /** The channel's review rejected it; fix the product, then remove it and\n * add it again so the corrected product is sent. */\n | \"fix_and_relist\"\n /** The channel took it down; resolve it on the channel's side. */\n | \"resolve_on_channel\";\n\nexport interface Issue {\n listingId: number;\n productId: number;\n status: string;\n auditStatus: string | null;\n action: IssueAction;\n errorType: string | null;\n error: string | null;\n hint: string;\n}\n\n/** Fixes for the failure types Fluid records when it can't send a product. */\nconst HINTS: Record<string, string> = {\n missing_description: \"Give the product a description in Fluid, then retry.\",\n missing_package_dimensions:\n \"Give at least one variant a positive package length, width, height, and weight in Fluid, then retry.\",\n missing_mandatory_attribute:\n \"TikTok's category for this product requires the attributes named in the error. Check the product's category in TikTok Seller Center.\",\n warehouse_not_configured:\n \"Choose the TikTok warehouse first: fluid channels warehouse tiktok <tiktok-warehouse-id> --yes. Then retry.\",\n warehouse_not_mapped:\n \"No Fluid warehouse is linked to this TikTok warehouse ID yet. The CLI can't link one; ask Fluid support to, then run fluid channels warehouse tiktok <tiktok-warehouse-id> --yes again.\",\n missing_sku: \"Give every active variant a SKU in Fluid, then retry.\",\n duplicate_sku:\n \"Another listing on this channel already uses the SKU. Make each variant's SKU unique, then retry.\",\n no_variants:\n \"The product has no active variants. Activate at least one variant, then retry.\",\n connection_inactive:\n \"The connection isn't active. Reconnect with fluid channels connect, then retry.\",\n};\n\nfunction errorOf(listing: ListingSummary): {\n type: string | null;\n message: string | null;\n} {\n const error = listing.lastError;\n if (!error || typeof error !== \"object\") {\n return { type: null, message: typeof error === \"string\" ? error : null };\n }\n const record = error as Record<string, unknown>;\n return {\n type: typeof record[\"type\"] === \"string\" ? record[\"type\"] : null,\n message:\n typeof record[\"description\"] === \"string\"\n ? record[\"description\"]\n : typeof record[\"message\"] === \"string\"\n ? record[\"message\"]\n : null,\n };\n}\n\nfunction actionFor(listing: ListingSummary): IssueAction | null {\n // A removed listing is off the channel, whatever its last review said.\n if (isRemoved(listing)) return null;\n if (listing.status === \"publish_failed\") return \"retry\";\n if (\n listing.status === \"platform_deactivated\" ||\n listing.status === \"frozen\"\n ) {\n return \"resolve_on_channel\";\n }\n if (listing.auditStatus === \"audit_failed\") return \"fix_and_relist\";\n return null;\n}\n\nconst ACTION_HINTS: Record<IssueAction, string> = {\n retry: \"Fix what the error names, then retry.\",\n fix_and_relist:\n \"The channel's review rejected the product. Fix it in Fluid, then remove it and add it again so the corrected product is sent.\",\n resolve_on_channel:\n \"The channel deactivated or froze the listing. Check the reason in the channel's seller tools.\",\n};\n\nexport function findIssues(listings: ListingSummary[]): Issue[] {\n const issues: Issue[] = [];\n for (const listing of listings) {\n const action = actionFor(listing);\n if (!action) continue;\n const { type, message } = errorOf(listing);\n issues.push({\n listingId: listing.id,\n productId: listing.productId,\n status: listing.status,\n auditStatus: listing.auditStatus,\n action,\n errorType: type,\n error: message,\n hint: (action === \"retry\" && type && HINTS[type]) || ACTION_HINTS[action],\n });\n }\n return issues;\n}\n\nexport interface ListingCounts {\n total: number;\n listed: number;\n issues: number;\n byStatus: Record<string, number>;\n byAuditStatus: Record<string, number>;\n}\n\nexport function countListings(listings: ListingSummary[]): ListingCounts {\n const byStatus: Record<string, number> = {};\n const byAuditStatus: Record<string, number> = {};\n let listed = 0;\n for (const listing of listings) {\n byStatus[listing.status] = (byStatus[listing.status] ?? 0) + 1;\n const audit = listing.auditStatus ?? \"none\";\n byAuditStatus[audit] = (byAuditStatus[audit] ?? 0) + 1;\n if (isListed(listing)) listed += 1;\n }\n return {\n total: listings.length,\n listed,\n issues: findIssues(listings).length,\n byStatus,\n byAuditStatus,\n };\n}\n","// `fluid channels walk`: the guided setup as a conversation. It picks the\n// channel, connects it, sets TikTok's warehouse, chooses the products, and\n// works through the issues. A person can go back, skip a stage, or jump to\n// any stage from the menu. Every change goes through the same functions as\n// the non-interactive commands, and asks before it runs.\n\nimport {\n NAV_HELP,\n type Choice,\n type Nav,\n type Prompter,\n type Question,\n type Reply,\n} from \"@fluid-app/fluid-cli\";\n\nimport {\n addProducts,\n authorizeUrl,\n getConnection,\n listActiveProductIds,\n listActiveProducts,\n listCollections,\n listListings,\n planAdd,\n retryListing,\n setTiktokWarehouse,\n type ChannelsClient,\n} from \"./api.js\";\nimport {\n CHANNEL_IDS,\n CHANNELS,\n channelOf,\n type Channel,\n type ChannelId,\n type ConnectionSummary,\n} from \"./channels.js\";\nimport { countListings, findIssues } from \"./issues.js\";\n\nexport type StageId =\n | \"channel\"\n | \"connect\"\n | \"warehouse\"\n | \"catalog\"\n | \"issues\";\n\nconst STAGE_NAMES: Record<StageId, string> = {\n channel: \"Pick the channel\",\n connect: \"Connect\",\n warehouse: \"TikTok warehouse\",\n catalog: \"Choose the products\",\n issues: \"Fix issues\",\n};\n\nexport interface WalkDeps {\n api: ChannelsClient;\n prompter: Prompter;\n /** Start on this channel instead of asking. */\n channel?: string;\n /** Opens the sign-in URL in a browser; false when it couldn't. */\n open?: (url: string) => boolean;\n}\n\nexport interface WalkResult {\n channel: ChannelId | null;\n connected: boolean;\n warehouseSet: boolean;\n added: number;\n retried: number;\n skipped: StageId[];\n finished: boolean;\n}\n\n/** What a stage wants next. */\ntype Outcome =\n | \"next\"\n | \"skip\"\n | \"back\"\n | \"menu\"\n | \"quit\"\n | \"stay\"\n | { jump: StageId };\n\nconst isConnected = (\n c: ConnectionSummary | { connected: false },\n): c is ConnectionSummary => c.connected === true;\n\n/** A reply's navigation as a stage outcome; null when it's an answer. */\nfunction navOutcome(reply: Reply): Outcome | null {\n if (!(\"nav\" in reply)) return null;\n const map: Record<Nav, Outcome> = {\n back: \"back\",\n skip: \"skip\",\n menu: \"menu\",\n quit: \"quit\",\n };\n return map[reply.nav];\n}\n\nexport async function walkChannels(deps: WalkDeps): Promise<WalkResult> {\n const { api, prompter } = deps;\n let channel: Channel | null = deps.channel ? channelOf(deps.channel) : null;\n let connected = false;\n let warehouseSet = false;\n let added = 0;\n let retried = 0;\n const skipped = new Set<StageId>();\n const history: StageId[] = [];\n\n const stages = (): StageId[] => [\n ...(deps.channel ? [] : ([\"channel\"] as StageId[])),\n \"connect\",\n ...(channel?.id === \"tiktok\" ? ([\"warehouse\"] as StageId[]) : []),\n \"catalog\",\n \"issues\",\n ];\n\n const ask = (q: Omit<Question, \"canGoBack\">) =>\n prompter.ask({ ...q, canGoBack: history.length > 0 });\n\n /** Every live change asks first. Navigating away counts as no. */\n const confirm = async (message: string): Promise<boolean> => {\n const reply = await ask({\n section: \"Confirm\",\n message,\n kind: \"select\",\n choices: [\n { title: \"Yes, do it\", value: \"yes\" },\n { title: \"No\", value: \"no\" },\n ],\n });\n return \"value\" in reply && reply.value === \"yes\";\n };\n\n const failed = (error: unknown) =>\n prompter.note(\n `Not done: ${error instanceof Error ? error.message : String(error)}`,\n );\n\n /** The connection, or a note and a jump to Connect when there isn't one. */\n const requireConnected = async (\n current: Channel,\n ): Promise<ConnectionSummary | { jump: StageId }> => {\n const connection = await getConnection(api, current);\n if (isConnected(connection)) return connection;\n prompter.note(`${current.label} isn't connected yet. Connect it first.`);\n return { jump: \"connect\" };\n };\n\n // ------------------------------------------------------------- stages\n\n const pickChannel = async (): Promise<Outcome> => {\n const choices: Choice[] = [];\n for (const id of CHANNEL_IDS) {\n const c = CHANNELS[id];\n const connection = await getConnection(api, c);\n choices.push({\n title: c.label,\n value: id,\n description: isConnected(connection)\n ? `connected${connection.account.name ? ` to ${connection.account.name}` : \"\"}`\n : \"not connected\",\n });\n }\n const reply = await ask({\n section: STAGE_NAMES.channel,\n message: \"Which channel?\",\n kind: \"select\",\n choices,\n ...(channel ? { current: channel.label } : {}),\n });\n const nav = navOutcome(reply);\n if (nav) return nav;\n channel = channelOf((reply as { value: string }).value);\n return \"next\";\n };\n\n const connect = async (current: Channel): Promise<Outcome> => {\n const existing = await getConnection(api, current);\n if (isConnected(existing)) {\n connected = true;\n const reply = await ask({\n section: STAGE_NAMES.connect,\n message: `${current.label} is connected${existing.account.name ? ` to ${existing.account.name}` : \"\"}.`,\n kind: \"select\",\n choices: [\n { title: \"Continue\", value: \"continue\" },\n {\n title: \"Approve again\",\n value: \"reconnect\",\n description: \"for a new account, or after access expired\",\n },\n ],\n });\n const nav = navOutcome(reply);\n if (nav) return nav;\n if ((reply as { value: string }).value === \"continue\") return \"next\";\n }\n\n let url = await authorizeUrl(api, current);\n const show = (link: string) => {\n prompter.note(\n `Open this link in a browser signed in to the ${current.label} account and approve Fluid's access. It works once, for 10 minutes:\\n${link}`,\n );\n if (deps.open?.(link)) prompter.note(\"Opened it in your browser.\");\n };\n show(url);\n for (;;) {\n const reply = await ask({\n section: STAGE_NAMES.connect,\n message: \"Done approving?\",\n kind: \"select\",\n choices: [\n { title: \"Yes, check the connection\", value: \"check\" },\n { title: \"Get a new link\", value: \"link\" },\n ],\n });\n const nav = navOutcome(reply);\n if (nav) return nav;\n if ((reply as { value: string }).value === \"link\") {\n url = await authorizeUrl(api, current);\n show(url);\n continue;\n }\n const after = await getConnection(api, current);\n if (isConnected(after)) {\n connected = true;\n prompter.note(\n `Connected${after.account.name ? ` to ${after.account.name}` : \"\"}.`,\n );\n return \"next\";\n }\n prompter.note(\n `Not connected yet (status: ${after.status}). Finish approving in the browser, or get a new link if it expired.`,\n );\n }\n };\n\n const warehouse = async (current: Channel): Promise<Outcome> => {\n const connection = await requireConnected(current);\n if (\"jump\" in connection) return connection;\n const saved = connection.details[\"tiktok_warehouse_id\"];\n const reply = await ask({\n section: STAGE_NAMES.warehouse,\n message: \"TikTok warehouse ID to publish inventory from\",\n kind: \"text\",\n hint: \"from TikTok Seller Center\",\n ...(typeof saved === \"string\" && saved ? { current: saved } : {}),\n });\n const nav = navOutcome(reply);\n if (nav) return nav;\n const value = (reply as { value: string }).value.trim();\n if (value === \"\") {\n if (typeof saved === \"string\" && saved) return \"next\";\n prompter.note(\"Type the warehouse ID, or > to skip.\");\n return \"stay\";\n }\n if (value === saved) return \"next\";\n if (!(await confirm(`Publish inventory from TikTok warehouse ${value}?`)))\n return \"stay\";\n try {\n await setTiktokWarehouse(api, current, value);\n } catch (error) {\n failed(error);\n prompter.note(\n \"Fluid only accepts a TikTok warehouse ID that a Fluid warehouse is linked to. If it isn't linked, ask Fluid support.\",\n );\n return \"stay\";\n }\n warehouseSet = true;\n prompter.note(`Inventory publishes from TikTok warehouse ${value}.`);\n return \"next\";\n };\n\n /** Pick several items from a searchable list. Returns the picked IDs, or\n * a navigation outcome. `<` with picks undoes the last one. */\n const pickMany = async (\n message: string,\n items: Array<{ id: number; title: string }>,\n ): Promise<number[] | Outcome> => {\n const picked: number[] = [];\n for (;;) {\n const remaining = items.filter((item) => !picked.includes(item.id));\n const reply = await ask({\n section: STAGE_NAMES.catalog,\n message: `${message} (${picked.length} picked)`,\n kind: \"search\",\n hint: \"type to search; < undoes the last pick\",\n choices: [\n { title: `Done picking (${picked.length})`, value: \"done\" },\n ...remaining.map((item) => ({\n title: item.title,\n value: String(item.id),\n })),\n ],\n });\n if (\"nav\" in reply && reply.nav === \"back\" && picked.length > 0) {\n picked.pop();\n continue;\n }\n const nav = navOutcome(reply);\n if (nav) return nav;\n const value = (reply as { value: string }).value;\n if (value === \"done\") return picked;\n picked.push(Number(value));\n }\n };\n\n const sendProducts = async (\n current: Channel,\n productIds: number[],\n { explicit = false }: { explicit?: boolean } = {},\n ): Promise<Outcome> => {\n // Products picked one by one are an explicit re-add, so a product\n // removed earlier goes back; whole-catalog and collection adds leave\n // removed products off.\n const plan = planAdd(\n productIds,\n await listListings(api, current),\n explicit,\n );\n if (plan.previouslyRemoved.length > 0) {\n prompter.note(\n `${plan.previouslyRemoved.length} product(s) you removed earlier stay off. Re-add them with fluid channels catalog ${current.id} --add <ids> --include-removed --yes.`,\n );\n }\n if (plan.toAdd.length === 0) {\n prompter.note(\n `Nothing new to send; ${plan.alreadyListed.length} already listed.`,\n );\n return \"next\";\n }\n if (\n !(await confirm(\n `Send ${plan.toAdd.length} product(s) to ${current.label}?`,\n ))\n )\n return \"stay\";\n try {\n const listings = await addProducts(api, current, plan.toAdd);\n added += listings.length;\n } catch (error) {\n failed(error);\n return \"stay\";\n }\n prompter.note(\n `Sent ${plan.toAdd.length}. Each publishes in the background, and ${current.label} reviews it; check later with fluid channels status ${current.id}.`,\n );\n return \"next\";\n };\n\n const catalog = async (current: Channel): Promise<Outcome> => {\n const connection = await requireConnected(current);\n if (\"jump\" in connection) return connection;\n const counts = countListings(await listListings(api, current));\n const reply = await ask({\n section: STAGE_NAMES.catalog,\n message: `Which products go to ${current.label}? (${counts.listed} listed now)`,\n kind: \"select\",\n choices: [\n {\n title: \"Whole catalog\",\n value: \"all\",\n description:\n \"every active product not listed yet; products you create later need another run\",\n },\n {\n title: \"Whole collections\",\n value: \"collections\",\n description: \"the active products in the collections you pick\",\n },\n {\n title: \"Selected products\",\n value: \"selected\",\n description:\n \"only the products you pick; best for trying a few first\",\n },\n { title: \"Leave them as they are\", value: \"none\" },\n ],\n });\n const nav = navOutcome(reply);\n if (nav) return nav;\n const choice = (reply as { value: string }).value;\n if (choice === \"none\") return \"next\";\n if (choice === \"all\")\n return sendProducts(current, await listActiveProductIds(api));\n if (choice === \"collections\") {\n const collections = await listCollections(api);\n if (collections.length === 0) {\n prompter.note(\"This company has no collections.\");\n return \"stay\";\n }\n const ids = await pickMany(\"Pick collections\", collections);\n if (!Array.isArray(ids)) return ids === \"back\" ? \"stay\" : ids;\n if (ids.length === 0) return \"stay\";\n return sendProducts(current, await listActiveProductIds(api, ids));\n }\n const products = await listActiveProducts(api);\n if (products.length === 0) {\n prompter.note(\"This company has no active products.\");\n return \"stay\";\n }\n const ids = await pickMany(\"Pick products\", products);\n if (!Array.isArray(ids)) return ids === \"back\" ? \"stay\" : ids;\n if (ids.length === 0) return \"stay\";\n return sendProducts(current, ids, { explicit: true });\n };\n\n const issues = async (current: Channel): Promise<Outcome> => {\n const connection = await requireConnected(current);\n if (\"jump\" in connection) return connection;\n const found = findIssues(await listListings(api, current));\n if (found.length === 0) {\n prompter.note(\n `No issues. Products still publishing or in review show in fluid channels status ${current.id}.`,\n );\n return \"next\";\n }\n for (const issue of found.slice(0, 10)) {\n prompter.note(\n `Product ${issue.productId} (${issue.status}${issue.auditStatus ? `, ${issue.auditStatus}` : \"\"}): ${issue.error ? `${issue.error}. ` : \"\"}${issue.hint}`,\n );\n }\n if (found.length > 10)\n prompter.note(\n `…and ${found.length - 10} more: fluid channels issues ${current.id}`,\n );\n const retryable = found.filter((issue) => issue.action === \"retry\");\n const reply = await ask({\n section: STAGE_NAMES.issues,\n message: `${found.length} issue(s). Fix the products in Fluid, then:`,\n kind: \"select\",\n choices: [\n ...(retryable.length > 0\n ? [\n {\n title: `Retry ${retryable.length} failed publish(es)`,\n value: \"retry\",\n },\n ]\n : []),\n { title: \"Check again\", value: \"check\" },\n { title: \"Done for now\", value: \"done\" },\n ],\n });\n const nav = navOutcome(reply);\n if (nav) return nav;\n const choice = (reply as { value: string }).value;\n if (choice === \"done\") return \"next\";\n if (choice === \"check\") return \"stay\";\n if (\n !(await confirm(\n `Retry ${retryable.length} failed publish(es) on ${current.label}?`,\n ))\n )\n return \"stay\";\n for (const issue of retryable) {\n try {\n await retryListing(api, current, issue.listingId);\n retried += 1;\n } catch (error) {\n failed(error);\n }\n }\n prompter.note(`Retried ${retried}. They publish in the background.`);\n return \"stay\";\n };\n\n const runStage = async (stage: StageId): Promise<Outcome> => {\n if (stage === \"channel\") return pickChannel();\n if (!channel) return { jump: \"channel\" };\n if (stage === \"connect\") return connect(channel);\n if (stage === \"warehouse\") return warehouse(channel);\n if (stage === \"catalog\") return catalog(channel);\n return issues(channel);\n };\n\n /** The menu: jump to any stage, or stop. */\n const openMenu = async (): Promise<StageId | \"quit\" | null> => {\n const choice = await prompter.menu<StageId | \"quit\">(\"Where to?\", [\n ...stages().map((stage) => ({\n title: `${skipped.has(stage) ? \"skipped \" : \"\"}${STAGE_NAMES[stage]}`,\n value: stage,\n disabled: stage !== \"channel\" && !channel,\n })),\n { title: \"Stop for now\", value: \"quit\" },\n ]);\n return choice;\n };\n\n prompter.note(NAV_HELP);\n let stage: StageId = stages()[0]!;\n let finished = false;\n for (;;) {\n const outcome = await runStage(stage);\n if (outcome === \"quit\") break;\n if (outcome === \"stay\") continue;\n if (typeof outcome === \"object\") {\n history.push(stage);\n stage = outcome.jump;\n continue;\n }\n if (outcome === \"back\") {\n const previous = history.pop();\n if (previous) stage = previous;\n else prompter.note(\"This is the first step.\");\n continue;\n }\n if (outcome === \"menu\") {\n const choice = await openMenu();\n if (choice === \"quit\") break;\n if (choice) {\n history.push(stage);\n stage = choice;\n }\n continue;\n }\n if (outcome === \"skip\") skipped.add(stage);\n else skipped.delete(stage);\n const order = stages();\n const next = order[order.indexOf(stage) + 1];\n history.push(stage);\n if (!next) {\n finished = true;\n break;\n }\n stage = next;\n }\n\n if (finished && channel) {\n prompter.note(\n `Done. Check how the products sync with fluid channels status ${channel.id}, and fix rejections with fluid channels issues ${channel.id}.`,\n );\n }\n return {\n channel: channel?.id ?? null,\n connected,\n warehouseSet,\n added,\n retried,\n skipped: [...skipped],\n finished,\n };\n}\n","import type { Command } from \"commander\";\nimport prompts from \"prompts\";\nimport type { CommandContext, FluidPlugin } from \"@fluid-app/fluid-cli\";\nimport {\n assertInteractive,\n createDomainCommand,\n terminalPrompter,\n} from \"@fluid-app/fluid-cli\";\n\nimport {\n addProducts,\n authorizeUrl,\n disconnect,\n getConnection,\n listActiveProductIds,\n listingsForProducts,\n listListings,\n planAdd,\n removeListing,\n requireConnection,\n retryListing,\n setTiktokWarehouse,\n type ChannelsClient,\n} from \"./api.js\";\nimport {\n CHANNEL_IDS,\n CHANNELS,\n ChannelsError,\n channelOf,\n isListed,\n type Channel,\n type ListingSummary,\n} from \"./channels.js\";\nimport { openInBrowser } from \"./browser.js\";\nimport { countListings, findIssues } from \"./issues.js\";\nimport { walkChannels } from \"./walk.js\";\n\nasync function client(ctx: CommandContext): Promise<ChannelsClient> {\n return (await ctx.getClient()) as unknown as ChannelsClient;\n}\n\n/**\n * Changes to a live channel need an explicit yes. Scripts and agents pass\n * `--yes`; an interactive terminal is asked. With neither, nothing runs.\n */\nexport async function confirmChange(\n question: string,\n yes: boolean,\n interactive: boolean = Boolean(process.stdin.isTTY),\n): Promise<void> {\n if (yes) return;\n if (!interactive)\n throw new ChannelsError(`${question} Pass --yes to confirm.`);\n const answer = await prompts({\n type: \"confirm\",\n name: \"ok\",\n message: question,\n initial: false,\n });\n if (!answer.ok) throw new ChannelsError(\"Cancelled.\");\n}\n\n/** The error the API sent back, as `{ message, details }`, when there is one. */\nfunction apiError(err: unknown): { message?: string; details?: unknown } {\n const body = (err as { body?: Record<string, unknown> | null } | null)?.body;\n if (!body || typeof body !== \"object\") return {};\n const error = body[\"error\"];\n if (error && typeof error === \"object\") {\n const record = error as Record<string, unknown>;\n const details = record[\"details\"];\n return {\n ...(typeof record[\"message\"] === \"string\"\n ? { message: record[\"message\"] }\n : {}),\n ...(details &&\n typeof details === \"object\" &&\n Object.keys(details).length > 0\n ? { details }\n : {}),\n };\n }\n if (typeof error === \"string\") return { message: error };\n if (typeof body[\"error_message\"] === \"string\")\n return { message: body[\"error_message\"] };\n return {};\n}\n\n/** Print a failure as JSON on stderr and exit non-zero, so scripts and\n * agents can read it the same way as a success. */\nexport function fail(err: unknown): void {\n const rawStatus = (err as { status?: unknown } | null)?.status;\n const status = typeof rawStatus === \"number\" ? rawStatus : undefined;\n const fromApi = apiError(err);\n const message =\n fromApi.message ?? (err instanceof Error ? err.message : String(err));\n process.stderr.write(\n JSON.stringify({\n error: message,\n ...(status ? { status } : {}),\n ...(fromApi.details ? { details: fromApi.details } : {}),\n }) + \"\\n\",\n );\n process.exitCode = 1;\n}\n\nfunction run(action: () => Promise<void>): Promise<void> {\n return action().catch(fail);\n}\n\nconst collect = (value: string, previous: string[] = []): string[] => [\n ...previous,\n value,\n];\n\n/** Product, listing, or collection IDs from repeatable, comma-separated flags. */\nexport function parseIds(values: string[], flag: string): number[] {\n const ids: number[] = [];\n for (const value of values) {\n for (const part of value.split(\",\")) {\n const trimmed = part.trim();\n if (trimmed === \"\") continue;\n if (!/^\\d+$/.test(trimmed) || Number(trimmed) <= 0) {\n throw new ChannelsError(\n `${flag} takes numeric IDs; \"${trimmed}\" isn't one.`,\n );\n }\n ids.push(Number(trimmed));\n }\n }\n return [...new Set(ids)];\n}\n\ninterface CatalogOptions {\n all: boolean;\n collection: string[];\n includeRemoved: boolean;\n add: string[];\n remove: string[];\n removeAll: boolean;\n yes: boolean;\n}\n\n/** Exactly one catalog change per run, or none to list the listings. */\nexport function catalogMode(\n opts: CatalogOptions,\n): \"list\" | \"all\" | \"add\" | \"remove\" | \"remove-all\" {\n const chosen = [\n opts.all && \"all\",\n opts.add.length > 0 && \"add\",\n opts.remove.length > 0 && \"remove\",\n opts.removeAll && \"remove-all\",\n ].filter(Boolean) as Array<\"all\" | \"add\" | \"remove\" | \"remove-all\">;\n if (chosen.length > 1) {\n throw new ChannelsError(\n \"Use one of --all, --add, --remove, or --remove-all per run.\",\n );\n }\n if (opts.collection.length > 0 && !opts.all) {\n throw new ChannelsError(\"--collection goes with --all.\");\n }\n if (opts.includeRemoved && !opts.all && opts.add.length === 0) {\n throw new ChannelsError(\"--include-removed goes with --all or --add.\");\n }\n return chosen[0] ?? \"list\";\n}\n\nasync function removeEach(\n api: ChannelsClient,\n channel: Channel,\n listings: ListingSummary[],\n): Promise<{\n removed: ListingSummary[];\n failed: Array<{ productId: number; error: string }>;\n}> {\n const removed: ListingSummary[] = [];\n const failed: Array<{ productId: number; error: string }> = [];\n for (const listing of listings) {\n try {\n removed.push(await removeListing(api, channel, listing.id));\n } catch (err) {\n failed.push({\n productId: listing.productId,\n error:\n apiError(err).message ??\n (err instanceof Error ? err.message : String(err)),\n });\n }\n }\n return { removed, failed };\n}\n\n/** Print a result that has per-item failures, and exit non-zero when any did. */\nfunction outputWithFailures(\n ctx: CommandContext,\n result: { failed: unknown[] } & Record<string, unknown>,\n): void {\n ctx.output(result);\n if (result.failed.length > 0) {\n process.stderr.write(\n JSON.stringify({\n error: `${result.failed.length} item(s) failed; see failed in the output.`,\n }) + \"\\n\",\n );\n process.exitCode = 1;\n }\n}\n\nexport function registerChannelCommands(\n parent: Command,\n ctx: CommandContext,\n): void {\n parent\n .command(\"list\")\n .description(`Show ${CHANNEL_IDS.join(\", \")} and whether each is connected`)\n .action(() =>\n run(async () => {\n const api = await client(ctx);\n const rows = [];\n for (const id of CHANNEL_IDS) {\n const channel = CHANNELS[id];\n const connection = await getConnection(api, channel);\n rows.push({ label: channel.label, ...connection });\n }\n ctx.output(rows);\n }),\n );\n\n parent\n .command(\"connect <channel>\")\n .description(\n \"Print the URL a person opens to approve Fluid's access on the channel; --open opens it in a browser\",\n )\n .option(\"--region <region>\", \"TikTok Shop only: US or GLOBAL\", \"US\")\n .option(\"--open\", \"Open the URL in the default browser\", false)\n .action((channelId: string, opts: { region: string; open: boolean }) =>\n run(async () => {\n const channel = channelOf(channelId);\n const region = opts.region.trim().toUpperCase();\n if (channel.id === \"tiktok\" && region !== \"US\" && region !== \"GLOBAL\") {\n throw new ChannelsError(\n `--region is US or GLOBAL, not \"${opts.region}\".`,\n );\n }\n const api = await client(ctx);\n const current = await getConnection(api, channel);\n const url = await authorizeUrl(api, channel, { region });\n const opened = opts.open ? openInBrowser(url) : false;\n ctx.output({\n channel: channel.id,\n alreadyConnected: current.connected,\n url,\n opened,\n next: `Open the URL in a browser signed in to the ${channel.label} account, approve the access, then run: fluid channels status ${channel.id}. The link works once and expires; run connect again for a new one.`,\n });\n }),\n );\n\n parent\n .command(\"status <channel>\")\n .description(\"Show the connection and a count of its listings by status\")\n .action((channelId: string) =>\n run(async () => {\n const channel = channelOf(channelId);\n const api = await client(ctx);\n const connection = await getConnection(api, channel);\n if (!(\"id\" in connection)) {\n ctx.output({ connection, listings: null });\n return;\n }\n ctx.output({\n connection,\n listings: countListings(await listListings(api, channel)),\n });\n }),\n );\n\n parent\n .command(\"catalog <channel>\")\n .description(\n \"List the products on the channel, or change them: --all, --add, --remove, or --remove-all (changes live config)\",\n )\n .option(\n \"--all\",\n \"Add every active product (or every active product in --collection) not already listed\",\n false,\n )\n .option(\n \"--collection <ids>\",\n \"With --all, only products in these collections (repeatable, comma-separated)\",\n collect,\n [],\n )\n .option(\n \"--include-removed\",\n \"With --all or --add, also re-add products you removed earlier\",\n false,\n )\n .option(\n \"--add <ids>\",\n \"Add these product IDs (repeatable, comma-separated)\",\n collect,\n [],\n )\n .option(\n \"--remove <ids>\",\n \"Remove these product IDs from the channel (repeatable, comma-separated)\",\n collect,\n [],\n )\n .option(\n \"--remove-all\",\n \"Remove every listed product from the channel\",\n false,\n )\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action((channelId: string, opts: CatalogOptions) =>\n run(async () => {\n const channel = channelOf(channelId);\n const mode = catalogMode(opts);\n const api = await client(ctx);\n await requireConnection(api, channel);\n const listings = await listListings(api, channel);\n\n if (mode === \"list\") {\n ctx.output({\n channel: channel.id,\n counts: countListings(listings),\n listings,\n });\n return;\n }\n\n if (mode === \"all\" || mode === \"add\") {\n const collections = parseIds(opts.collection, \"--collection\");\n const requested =\n mode === \"all\"\n ? await listActiveProductIds(api, collections)\n : parseIds(opts.add, \"--add\");\n const plan = planAdd(requested, listings, opts.includeRemoved);\n const summary = {\n channel: channel.id,\n mode,\n ...(mode === \"all\"\n ? { collections, activeProducts: requested.length }\n : {}),\n alreadyListed: plan.alreadyListed.length,\n previouslyRemoved: plan.previouslyRemoved,\n };\n if (plan.toAdd.length === 0) {\n ctx.output({ ...summary, added: 0, listings: [] });\n return;\n }\n await confirmChange(\n `Send ${plan.toAdd.length} product(s) to ${channel.label}?`,\n opts.yes,\n );\n const added = await addProducts(api, channel, plan.toAdd);\n ctx.output({\n ...summary,\n added: added.length,\n listings: added,\n next: `Each product publishes in the background. Check progress with: fluid channels status ${channel.id}`,\n });\n return;\n }\n\n const targets =\n mode === \"remove-all\"\n ? { found: listings.filter(isListed), notListed: [] as number[] }\n : listingsForProducts(parseIds(opts.remove, \"--remove\"), listings);\n if (targets.found.length === 0) {\n ctx.output({\n channel: channel.id,\n mode,\n removed: [],\n notListed: targets.notListed,\n failed: [],\n });\n return;\n }\n await confirmChange(\n `Remove ${targets.found.length} product(s) from ${channel.label}?`,\n opts.yes,\n );\n const result = await removeEach(api, channel, targets.found);\n outputWithFailures(ctx, {\n channel: channel.id,\n mode,\n removed: result.removed,\n notListed: targets.notListed,\n failed: result.failed,\n });\n }),\n );\n\n parent\n .command(\"issues <channel>\")\n .description(\n \"List listings that failed to publish or were rejected, with what to do; --retry retries failed publishes (changes live config)\",\n )\n .option(\"--retry\", \"Retry every listing whose publish failed\", false)\n .option(\"--yes\", \"Confirm --retry without a prompt\", false)\n .action((channelId: string, opts: { retry: boolean; yes: boolean }) =>\n run(async () => {\n const channel = channelOf(channelId);\n const api = await client(ctx);\n await requireConnection(api, channel);\n const issues = findIssues(await listListings(api, channel));\n if (!opts.retry) {\n ctx.output({ channel: channel.id, count: issues.length, issues });\n return;\n }\n const retryable = issues.filter((issue) => issue.action === \"retry\");\n if (retryable.length === 0) {\n ctx.output({ channel: channel.id, retried: [], failed: [], issues });\n return;\n }\n await confirmChange(\n `Retry ${retryable.length} failed publish(es) on ${channel.label}?`,\n opts.yes,\n );\n const retried: ListingSummary[] = [];\n const failed: Array<{ productId: number; error: string }> = [];\n for (const issue of retryable) {\n try {\n retried.push(await retryListing(api, channel, issue.listingId));\n } catch (err) {\n failed.push({\n productId: issue.productId,\n error:\n apiError(err).message ??\n (err instanceof Error ? err.message : String(err)),\n });\n }\n }\n outputWithFailures(ctx, { channel: channel.id, retried, failed });\n }),\n );\n\n parent\n .command(\"warehouse <channel> <tiktokWarehouseId>\")\n .description(\n \"TikTok Shop only: choose the TikTok warehouse whose inventory Fluid publishes (changes live config)\",\n )\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action(\n (channelId: string, tiktokWarehouseId: string, opts: { yes: boolean }) =>\n run(async () => {\n const channel = channelOf(channelId);\n if (channel.id !== \"tiktok\") {\n throw new ChannelsError(\n \"Only TikTok Shop uses a warehouse setting.\",\n );\n }\n const id = tiktokWarehouseId.trim();\n if (id === \"\")\n throw new ChannelsError(\"Pass the TikTok warehouse ID.\");\n const api = await client(ctx);\n await requireConnection(api, channel);\n await confirmChange(\n `Publish inventory from TikTok warehouse ${id}?`,\n opts.yes,\n );\n ctx.output(await setTiktokWarehouse(api, channel, id));\n }),\n );\n\n parent\n .command(\"walk [channel]\")\n .description(\n \"Set a channel up question by question in the terminal: connect it, choose the products, fix issues; asks before each change (changes live config)\",\n )\n .action((channelId: string | undefined) =>\n run(async () => {\n assertInteractive(\n \"`connect`, `status`, `catalog`, and `issues` with --yes\",\n );\n if (channelId) channelOf(channelId);\n ctx.output(\n await walkChannels({\n api: await client(ctx),\n prompter: terminalPrompter(),\n ...(channelId ? { channel: channelId } : {}),\n open: openInBrowser,\n }),\n );\n }),\n );\n\n parent\n .command(\"disconnect <channel>\")\n .description(\n \"Disconnect the channel; products already on it stay there (changes live config)\",\n )\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action((channelId: string, opts: { yes: boolean }) =>\n run(async () => {\n const channel = channelOf(channelId);\n const api = await client(ctx);\n await requireConnection(api, channel);\n const stillListed = (await listListings(api, channel)).filter(\n isListed,\n ).length;\n const warning =\n stillListed > 0\n ? ` ${stillListed} product(s) stay on ${channel.label} and stop syncing; remove them first with fluid channels catalog ${channel.id} --remove-all.`\n : \"\";\n await confirmChange(`Disconnect ${channel.label}?${warning}`, opts.yes);\n ctx.output({ connection: await disconnect(api, channel), stillListed });\n }),\n );\n}\n\nconst plugin: FluidPlugin = {\n name: \"@fluid-app/fluid-cli-channels\",\n version: \"0.1.0\",\n register(ctx) {\n ctx.program.addCommand(\n createDomainCommand(\n \"channels\",\n \"Sales channels: connect TikTok Shop, Meta, or Google Merchant Center, choose the products, check sync, disconnect\",\n registerChannelCommands,\n ),\n );\n },\n};\n\nexport default plugin;\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,85 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluid-app/fluid-cli-channels",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Fluid CLI plugin for sales channels — connect TikTok Shop, Meta, or Google Merchant Center, choose which products to list, check sync status, and disconnect",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist",
|
|
7
|
+
"README.md"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.mjs",
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": {
|
|
15
|
+
"types": "./dist/index.d.mts",
|
|
16
|
+
"default": "./dist/index.mjs"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@fluid-app/fluid-cli": "0.1.36",
|
|
25
|
+
"commander": "^12.0.0",
|
|
26
|
+
"prompts": "^2.4.2",
|
|
27
|
+
"zod": "4.6.5"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@fluid-app/typescript-config": "0.0.0",
|
|
31
|
+
"@swc/core": "^1.15.18",
|
|
32
|
+
"@swc/jest": "^0.2.39",
|
|
33
|
+
"@types/jest": "^29.5.14",
|
|
34
|
+
"@types/node": "24.10.12",
|
|
35
|
+
"@types/prompts": "^2.4.9",
|
|
36
|
+
"jest": "^29.7.0",
|
|
37
|
+
"tsdown": "^0.21.0",
|
|
38
|
+
"typescript": "^5"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=24.0.0"
|
|
42
|
+
},
|
|
43
|
+
"nx": {
|
|
44
|
+
"targets": {
|
|
45
|
+
"typecheck": {
|
|
46
|
+
"dependsOn": [
|
|
47
|
+
{
|
|
48
|
+
"target": "build",
|
|
49
|
+
"projects": [
|
|
50
|
+
"@fluid-app/fluid-cli"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"test": {
|
|
56
|
+
"dependsOn": [
|
|
57
|
+
{
|
|
58
|
+
"target": "build",
|
|
59
|
+
"projects": [
|
|
60
|
+
"@fluid-app/fluid-cli"
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
"build": {
|
|
66
|
+
"dependsOn": [
|
|
67
|
+
"typecheck",
|
|
68
|
+
{
|
|
69
|
+
"target": "build",
|
|
70
|
+
"projects": [
|
|
71
|
+
"@fluid-app/fluid-cli"
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"scripts": {
|
|
79
|
+
"build": "tsdown",
|
|
80
|
+
"dev": "tsdown --watch",
|
|
81
|
+
"typecheck": "tsgo --noEmit",
|
|
82
|
+
"lint": "oxlint",
|
|
83
|
+
"test": "jest"
|
|
84
|
+
}
|
|
6
85
|
}
|