feat(bootstrap): add command to initialize Bailian workspace and activate postpaid services

This commit is contained in:
lisheng.lisheng
2026-07-10 10:06:32 +08:00
parent 2907ad2625
commit 049eecd991
5 changed files with 194 additions and 0 deletions
+2
View File
@@ -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<string, AnyCommand> = {
"token-plan create-key": tokenPlanCreateKey,
"token-plan assign-seats": tokenPlanAssignSeats,
"token-plan add-member": tokenPlanAddMember,
bootstrap: bootstrap,
};
@@ -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<void> {
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<any>(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<any>;
async function ensureCommoditiesActive(call: ApiCall, format: "text" | "json"): Promise<void> {
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<void> {
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<void> {
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);
}
+1
View File
@@ -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";
+35
View File
@@ -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 <region>` | string | no | Console gateway region (e.g. cn-beijing, ap-southeast-1) |
| `--console-site <site>` | string | no | Console site: domestic, international |
| `--console-switch-agent <uid>` | number | no | Switch agent UID for delegated access |
| `--workspace-id <id>` | string | no | Workspace ID (env: BAILIAN_WORKSPACE_ID) |
#### Examples
_No examples._
+2
View File
@@ -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) |