diff --git a/packages/cli/src/commands.ts b/packages/cli/src/commands.ts index 8e51c94..a6ece34 100644 --- a/packages/cli/src/commands.ts +++ b/packages/cli/src/commands.ts @@ -74,6 +74,7 @@ import { tokenPlanCreateKey, tokenPlanAssignSeats, tokenPlanAddMember, + bootstrap, } from "bailian-cli-commands"; // Full bailian-cli product: every command, exposed under the `bl` binary. @@ -156,4 +157,5 @@ export const commands: Record = { "token-plan create-key": tokenPlanCreateKey, "token-plan assign-seats": tokenPlanAssignSeats, "token-plan add-member": tokenPlanAddMember, + bootstrap: bootstrap, }; diff --git a/packages/commands/src/commands/bootstrap/index.ts b/packages/commands/src/commands/bootstrap/index.ts new file mode 100644 index 0000000..3bd9b96 --- /dev/null +++ b/packages/commands/src/commands/bootstrap/index.ts @@ -0,0 +1,154 @@ +import { defineCommand, detectOutputFormat, BailianError, ExitCode } from "bailian-cli-core"; +import { emitResult, emitBare } from "bailian-cli-runtime"; + +const API = { + loginInfo: "zeldaEasy.cornerstone-portal.cs-console.loginInfo", + initSpace: "zeldaEasy.bailian-dash-workspace.space.initSpace", + queryBuyResult: "zeldaEasy.broadscope-bailian.bill.queryBuyPostpaidResult", + commodityOrderInfo: "zeldaEasy.broadscope-bailian.bill.postpaidCommodityOrderInfo", + buyCommodity: "zeldaEasy.broadscope-bailian.bill.buyPostpaidCommodity", +} as const; + +const POLL_INTERVAL_MS = 1000; +const MAX_POLL_ATTEMPTS = 120; + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +interface CommodityItem { + commodity?: string; + status?: number; + startDate?: string; +} + +export default defineCommand({ + description: "Initialize Bailian workspace and activate postpaid services", + auth: "console", + usageArgs: "", + flags: {}, + exampleArgs: [], + async run(ctx) { + const { settings } = ctx; + const format = detectOutputFormat(settings.output); + + if (settings.dryRun) { + emitResult( + { + apis: [ + { step: 1, api: API.loginInfo, description: "Check login & workspace status" }, + { step: 2, api: API.initSpace, description: "Initialize workspace (if needed)" }, + { step: 3, api: API.queryBuyResult, description: "Query postpaid order status" }, + { + step: 4, + api: API.commodityOrderInfo, + description: "Query commodity activation status", + }, + { + step: 5, + api: API.buyCommodity, + description: "Activate postpaid commodities (if needed)", + }, + ], + }, + format, + ); + return; + } + + const verbose = settings.verbose; + const callApi = async (api: string) => { + if (verbose) process.stderr.write(`> ${api}\n`); + const resp = await ctx.client.console(api, {}); + if (verbose) process.stderr.write(`< ${JSON.stringify(resp)}\n`); + return resp; + }; + + // Step 1: Check login info + emitBare("Checking workspace status..."); + const loginInfo = await callApi(API.loginInfo); + const spaceInited = loginInfo?.spaceInited === true; + + // Step 2: Init space if needed + if (!spaceInited) { + emitBare("Initializing workspace..."); + await callApi(API.initSpace); + emitBare("Workspace initialized."); + } else { + emitBare("Workspace already initialized."); + } + + // Step 3-5: Order & commodity flow + await ensureCommoditiesActive(callApi, format); + }, +}); + +type ApiCall = (api: string) => Promise; + +async function ensureCommoditiesActive(call: ApiCall, format: "text" | "json"): Promise { + emitBare("Checking service activation status..."); + let buyResult: string | undefined; + for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) { + const resp = await call(API.queryBuyResult); + buyResult = resp?.result; + if (buyResult !== "buying") break; + if (i === 0) emitBare("Service activation in progress, polling..."); + await sleep(POLL_INTERVAL_MS); + } + + if (buyResult === "fail") { + throw new BailianError("Service activation failed.", ExitCode.GENERAL); + } + + if (buyResult === "success") { + await pollCommoditiesUntilActive(call, format); + return; + } + + await checkAndActivateCommodities(call, format); +} + +async function checkAndActivateCommodities(call: ApiCall, format: "text" | "json"): Promise { + const resp = await call(API.commodityOrderInfo); + const items: CommodityItem[] = resp?.result ?? []; + + const overdue = items.filter((c) => c.status === 11); + if (overdue.length > 0) { + emitBare("Warning: Some services are overdue:"); + for (const c of overdue) emitBare(` - ${c.commodity}`); + } + + const notActivated = items.filter((c) => c.status === 1); + if (notActivated.length > 0) { + emitBare("Activating postpaid services..."); + await call(API.buyCommodity); + await pollCommoditiesUntilActive(call, format); + return; + } + + const active = items.filter((c) => c.status === 10); + emitResult({ status: "ready", activeServices: active.map((c) => c.commodity) }, format); +} + +async function pollCommoditiesUntilActive(call: ApiCall, format: "text" | "json"): Promise { + emitBare("Waiting for services to activate..."); + for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) { + const resp = await call(API.commodityOrderInfo); + const items: CommodityItem[] = resp?.result ?? []; + + const pending = items.filter((c) => c.status !== 10 && c.status !== 11); + if (pending.length === 0) { + const overdue = items.filter((c) => c.status === 11); + if (overdue.length > 0) { + emitBare("Warning: Some services are overdue:"); + for (const c of overdue) emitBare(` - ${c.commodity}`); + } + const active = items.filter((c) => c.status === 10); + emitResult({ status: "ready", activeServices: active.map((c) => c.commodity) }, format); + return; + } + await sleep(POLL_INTERVAL_MS); + } + + throw new BailianError("Timed out waiting for services to activate.", ExitCode.TIMEOUT); +} diff --git a/packages/commands/src/index.ts b/packages/commands/src/index.ts index 7a46e39..cdc78ed 100644 --- a/packages/commands/src/index.ts +++ b/packages/commands/src/index.ts @@ -77,3 +77,4 @@ export { default as tokenPlanListSeats } from "./commands/token-plan/list-seats. export { default as tokenPlanCreateKey } from "./commands/token-plan/create-key.ts"; export { default as tokenPlanAssignSeats } from "./commands/token-plan/assign-seats.ts"; export { default as tokenPlanAddMember } from "./commands/token-plan/add-member.ts"; +export { default as bootstrap } from "./commands/bootstrap/index.ts"; diff --git a/skills/bailian-cli/reference/bootstrap.md b/skills/bailian-cli/reference/bootstrap.md new file mode 100644 index 0000000..97fd869 --- /dev/null +++ b/skills/bailian-cli/reference/bootstrap.md @@ -0,0 +1,35 @@ +# `bl bootstrap` commands + +> Auto-generated from `packages/cli/src/commands.ts`. Do not edit by hand. +> Regenerate: `pnpm --filter bailian-cli run generate:reference`. + +Index: [index.md](index.md) + +## Commands in this group + +| Command | Description | +| -------------- | ----------------------------------------------------------- | +| `bl bootstrap` | Initialize Bailian workspace and activate postpaid services | + +## Command details + +### `bl bootstrap` + +| Field | Value | +| --------------- | ----------------------------------------------------------- | +| **Name** | `bootstrap` | +| **Description** | Initialize Bailian workspace and activate postpaid services | +| **Usage** | `bl bootstrap` | + +#### Flags + +| Flag | Type | Required | Description | +| ------------------------------ | ------ | -------- | -------------------------------------------------------- | +| `--console-region ` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) | +| `--console-site ` | string | no | Console site: domestic, international | +| `--console-switch-agent ` | number | no | Switch agent UID for delegated access | +| `--workspace-id ` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) | + +#### Examples + +_No examples._ diff --git a/skills/bailian-cli/reference/index.md b/skills/bailian-cli/reference/index.md index cf2f0aa..d1eb127 100644 --- a/skills/bailian-cli/reference/index.md +++ b/skills/bailian-cli/reference/index.md @@ -17,6 +17,7 @@ Use this index for the full quick index and global flags. | `bl auth login` | Authenticate with API key, console browser login, or OpenAPI AK/SK (credentials can coexist) | [auth.md](auth.md) | | `bl auth logout` | Clear stored credentials | [auth.md](auth.md) | | `bl auth status` | Show current authentication state | [auth.md](auth.md) | +| `bl bootstrap` | Initialize Bailian workspace and activate postpaid services | [bootstrap.md](bootstrap.md) | | `bl config set` | Set a config value | [config.md](config.md) | | `bl config show` | Display current configuration | [config.md](config.md) | | `bl console call` | Call a Bailian console API via the CLI gateway | [console.md](console.md) | @@ -92,6 +93,7 @@ Use this index for the full quick index and global flags. | `advisor` | `recommend` | [advisor.md](advisor.md) | | `app` | `call`, `list` | [app.md](app.md) | | `auth` | `generate-access-token`, `login`, `logout`, `status` | [auth.md](auth.md) | +| `bootstrap` | `(root)` | [bootstrap.md](bootstrap.md) | | `config` | `set`, `show` | [config.md](config.md) | | `console` | `call` | [console.md](console.md) | | `dataset` | `delete`, `get`, `list`, `upload`, `validate` | [dataset.md](dataset.md) |