@farthershore/cli 0.3.7 → 0.3.9

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.
@@ -0,0 +1,99 @@
1
+ import { loadAcceptedSpec, objectAt, printResult, resolveProductId, updateSpec, } from "./helpers.js";
2
+ export function registerBillingCommands(program, getClient) {
3
+ const billing = program
4
+ .command("billing")
5
+ .description("Manage product-level billing strategy and transition policy")
6
+ .addHelpText("after", `
7
+ Agent notes:
8
+ Billing strategy is product-level for paid plans. Paid plans should not mix strategies.
9
+ Strategies: free_trial, flat_subscription, included_usage, subscription_overage, pay_as_you_go, prepaid_credits.
10
+ Subscriber actions: preserve_current_period, switch_immediately, switch_immediately_prorate, new_subscribers_only.
11
+
12
+ Examples:
13
+ farthershore billing strategy get --format json
14
+ farthershore billing strategy set prepaid_credits --transition preserve_current_period --format json
15
+ farthershore billing policy set --default preserve_current_period --proration none --format json
16
+ `);
17
+ const strategy = billing
18
+ .command("strategy")
19
+ .description("Get or set the product-level billing strategy")
20
+ .addHelpText("after", `
21
+ Examples:
22
+ farthershore billing strategy get --format json
23
+ farthershore billing strategy set subscription_overage --format json
24
+ `);
25
+ strategy
26
+ .command("get")
27
+ .option("--product <product>", "Product id or name")
28
+ .addHelpText("after", `
29
+ Example:
30
+ farthershore billing strategy get --format json
31
+ `)
32
+ .action(async (opts) => {
33
+ const client = getClient();
34
+ const productId = await resolveProductId(client, opts.product);
35
+ const config = (await client.getProductConfig(productId));
36
+ printResult(program, "Billing strategy loaded", {
37
+ ok: true,
38
+ productId,
39
+ strategy: config.acceptedConfig?.billing?.strategy ?? null,
40
+ });
41
+ });
42
+ strategy
43
+ .command("set <strategy>")
44
+ .option("--product <product>", "Product id or name")
45
+ .option("--transition <action>", "Default subscriber transition action: preserve_current_period, switch_immediately, switch_immediately_prorate, new_subscribers_only", "preserve_current_period")
46
+ .option("--proration <mode>", "Proration mode", "none")
47
+ .option("--dry-run", "Validate without mutating")
48
+ .addHelpText("after", `
49
+ Examples:
50
+ farthershore billing strategy set prepaid_credits --transition preserve_current_period --format json
51
+ farthershore billing strategy set pay_as_you_go --dry-run --format json
52
+ `)
53
+ .action(async (nextStrategy, opts) => {
54
+ const client = getClient();
55
+ const productId = await resolveProductId(client, opts.product);
56
+ const spec = await loadAcceptedSpec(client, productId);
57
+ const billing = objectAt(spec, "billing");
58
+ billing.strategy = nextStrategy;
59
+ billing.subscriberChangePolicy = {
60
+ default: opts.transition,
61
+ proration: opts.proration,
62
+ };
63
+ await updateSpec(program, client, productId, spec, {
64
+ dryRun: opts.dryRun,
65
+ message: "Billing strategy updated",
66
+ });
67
+ });
68
+ const policy = billing
69
+ .command("policy")
70
+ .description("Set subscriber change policy defaults")
71
+ .addHelpText("after", `
72
+ Example:
73
+ farthershore billing policy set --default preserve_current_period --proration none --format json
74
+ `);
75
+ policy
76
+ .command("set")
77
+ .option("--product <product>", "Product id or name")
78
+ .option("--default <action>", "Default subscriber action: preserve_current_period, switch_immediately, switch_immediately_prorate, new_subscribers_only", "preserve_current_period")
79
+ .option("--proration <mode>", "Proration mode", "none")
80
+ .option("--dry-run", "Validate without mutating")
81
+ .addHelpText("after", `
82
+ Example:
83
+ farthershore billing policy set --default preserve_current_period --proration none --format json
84
+ `)
85
+ .action(async (opts) => {
86
+ const client = getClient();
87
+ const productId = await resolveProductId(client, opts.product);
88
+ const spec = await loadAcceptedSpec(client, productId);
89
+ const billing = objectAt(spec, "billing");
90
+ billing.subscriberChangePolicy = {
91
+ default: opts.default,
92
+ proration: opts.proration,
93
+ };
94
+ await updateSpec(program, client, productId, spec, {
95
+ dryRun: opts.dryRun,
96
+ message: "Billing policy updated",
97
+ });
98
+ });
99
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { ApiClient } from "../client.js";
3
+ export declare function registerFeatureCommands(program: Command, getClient: () => ApiClient): void;
@@ -0,0 +1,109 @@
1
+ import { findPlan, loadAcceptedSpec, objectAt, printResult, resolveProductId, stringArrayAt, updateSpec, } from "./helpers.js";
2
+ export function registerFeatureCommands(program, getClient) {
3
+ const feature = program
4
+ .command("feature")
5
+ .description("Manage route-bound product features")
6
+ .addHelpText("after", `
7
+ Agent notes:
8
+ Features gate routes. Add a feature with one or more METHOD:/path routes, then bind it to plans.
9
+ Use METHOD:* only when the feature should cover all methods for a path.
10
+
11
+ Examples:
12
+ farthershore feature add chat_completions --route POST:/v1/chat/completions --format json
13
+ farthershore feature bind chat_completions --plan pro --format json
14
+ farthershore feature list --format json
15
+ `);
16
+ feature
17
+ .command("list")
18
+ .option("--product <product>", "Product id or name")
19
+ .addHelpText("after", `
20
+ Example:
21
+ farthershore feature list --format json
22
+ `)
23
+ .action(async (opts) => {
24
+ const client = getClient();
25
+ const productId = await resolveProductId(client, opts.product);
26
+ const spec = await loadAcceptedSpec(client, productId);
27
+ printResult(program, "Features loaded", {
28
+ ok: true,
29
+ productId,
30
+ features: objectAt(spec, "features"),
31
+ });
32
+ });
33
+ feature
34
+ .command("add <key>")
35
+ .option("--product <product>", "Product id or name")
36
+ .option("--route <route>", "Route binding METHOD:/path; repeatable", collect, [])
37
+ .option("--dry-run", "Validate without mutating")
38
+ .addHelpText("after", `
39
+ Examples:
40
+ farthershore feature add chat_completions --route POST:/v1/chat/completions --format json
41
+ farthershore feature add weather_read --route GET:/v1/weather --route GET:/v1/forecast --dry-run --format json
42
+ `)
43
+ .action(async (key, opts) => {
44
+ const client = getClient();
45
+ const productId = await resolveProductId(client, opts.product);
46
+ const spec = await loadAcceptedSpec(client, productId);
47
+ const features = objectAt(spec, "features");
48
+ features[key] = { routes: opts.route.map(parseRoute) };
49
+ await updateSpec(program, client, productId, spec, {
50
+ dryRun: opts.dryRun,
51
+ message: `Feature "${key}" saved`,
52
+ });
53
+ });
54
+ feature
55
+ .command("bind <key>")
56
+ .requiredOption("--plan <plan>", "Plan key")
57
+ .option("--product <product>", "Product id or name")
58
+ .option("--dry-run", "Validate without mutating")
59
+ .addHelpText("after", `
60
+ Example:
61
+ farthershore feature bind chat_completions --plan pro --format json
62
+ `)
63
+ .action(async (key, opts) => {
64
+ const client = getClient();
65
+ const productId = await resolveProductId(client, opts.product);
66
+ const spec = await loadAcceptedSpec(client, productId);
67
+ const plan = findPlan(spec, opts.plan);
68
+ const features = stringArrayAt(plan, "features");
69
+ if (!features.includes(key))
70
+ features.push(key);
71
+ await updateSpec(program, client, productId, spec, {
72
+ dryRun: opts.dryRun,
73
+ message: `Feature "${key}" bound to "${opts.plan}"`,
74
+ });
75
+ });
76
+ feature
77
+ .command("unbind <key>")
78
+ .requiredOption("--plan <plan>", "Plan key")
79
+ .option("--product <product>", "Product id or name")
80
+ .option("--dry-run", "Validate without mutating")
81
+ .addHelpText("after", `
82
+ Example:
83
+ farthershore feature unbind chat_completions --plan starter --format json
84
+ `)
85
+ .action(async (key, opts) => {
86
+ const client = getClient();
87
+ const productId = await resolveProductId(client, opts.product);
88
+ const spec = await loadAcceptedSpec(client, productId);
89
+ const plan = findPlan(spec, opts.plan);
90
+ plan.features = stringArrayAt(plan, "features").filter((featureKey) => featureKey !== key);
91
+ await updateSpec(program, client, productId, spec, {
92
+ dryRun: opts.dryRun,
93
+ message: `Feature "${key}" unbound from "${opts.plan}"`,
94
+ });
95
+ });
96
+ }
97
+ function collect(value, previous) {
98
+ previous.push(value);
99
+ return previous;
100
+ }
101
+ function parseRoute(value) {
102
+ const separator = value.indexOf(":");
103
+ if (separator === -1)
104
+ return { method: "*", path: value };
105
+ return {
106
+ method: value.slice(0, separator).toUpperCase(),
107
+ path: value.slice(separator + 1),
108
+ };
109
+ }
@@ -0,0 +1,15 @@
1
+ import type { Command } from "commander";
2
+ import type { ApiClient } from "../client.js";
3
+ export declare function commandFormat(program: Command): "table" | "json";
4
+ export declare function printResult(program: Command, humanMessage: string, payload: Record<string, unknown>): void;
5
+ export declare function resolveProductId(client: ApiClient, productArg?: string): Promise<string>;
6
+ export declare function resolvePlanId(client: ApiClient, productId: string, planArg: string): Promise<string>;
7
+ export declare function loadAcceptedSpec(client: ApiClient, productId: string): Promise<Record<string, unknown>>;
8
+ export declare function plans(spec: Record<string, unknown>): Array<Record<string, unknown>>;
9
+ export declare function objectAt(parent: Record<string, unknown>, key: string): Record<string, unknown>;
10
+ export declare function stringArrayAt(parent: Record<string, unknown>, key: string): string[];
11
+ export declare function findPlan(spec: Record<string, unknown>, planKey: string): Record<string, unknown>;
12
+ export declare function updateSpec(program: Command, client: ApiClient, productId: string, spec: Record<string, unknown>, opts?: {
13
+ dryRun?: boolean;
14
+ message?: string;
15
+ }): Promise<unknown>;
@@ -0,0 +1,93 @@
1
+ import { loadConfig } from "../config.js";
2
+ import * as output from "../output.js";
3
+ export function commandFormat(program) {
4
+ return output.outputFormat(program.opts().format);
5
+ }
6
+ export function printResult(program, humanMessage, payload) {
7
+ if (commandFormat(program) === "json") {
8
+ console.log(output.json(payload));
9
+ return;
10
+ }
11
+ output.success(humanMessage);
12
+ }
13
+ export async function resolveProductId(client, productArg) {
14
+ const config = loadConfig();
15
+ if (productArg) {
16
+ if (productArg.length === 36)
17
+ return productArg;
18
+ const products = client.isMakerToken()
19
+ ? await client.managementListProducts()
20
+ : await client.listProducts();
21
+ const product = products.find((p) => p.id === productArg ||
22
+ p.name.toLowerCase() === productArg.toLowerCase());
23
+ if (product)
24
+ return product.id;
25
+ }
26
+ if (config.activeProductId)
27
+ return config.activeProductId;
28
+ throw new Error("No product selected. Pass --product <id-or-name> or run `farthershore product create` first.");
29
+ }
30
+ export async function resolvePlanId(client, productId, planArg) {
31
+ if (planArg.length === 36)
32
+ return planArg;
33
+ const products = client.isMakerToken()
34
+ ? await client.managementListProducts()
35
+ : await client.listProducts();
36
+ const product = products.find((p) => p.id === productId);
37
+ const plan = product?.plans?.find((p) => p.id === planArg ||
38
+ p.key === planArg ||
39
+ p.name.toLowerCase() === planArg.toLowerCase());
40
+ if (!plan)
41
+ throw new Error(`Plan "${planArg}" not found`);
42
+ return plan.id;
43
+ }
44
+ export async function loadAcceptedSpec(client, productId) {
45
+ const config = (await client.getProductConfig(productId));
46
+ if (!config.acceptedConfig ||
47
+ typeof config.acceptedConfig !== "object" ||
48
+ Array.isArray(config.acceptedConfig)) {
49
+ throw new Error("Product has no accepted internal config yet.");
50
+ }
51
+ return config.acceptedConfig;
52
+ }
53
+ export function plans(spec) {
54
+ if (!Array.isArray(spec.plans))
55
+ spec.plans = [];
56
+ return spec.plans;
57
+ }
58
+ export function objectAt(parent, key) {
59
+ const current = parent[key];
60
+ if (current && typeof current === "object" && !Array.isArray(current)) {
61
+ return current;
62
+ }
63
+ const next = {};
64
+ parent[key] = next;
65
+ return next;
66
+ }
67
+ export function stringArrayAt(parent, key) {
68
+ if (!Array.isArray(parent[key]))
69
+ parent[key] = [];
70
+ const values = parent[key];
71
+ const strings = values.filter((value) => typeof value === "string");
72
+ if (strings.length !== values.length)
73
+ parent[key] = strings;
74
+ return parent[key];
75
+ }
76
+ export function findPlan(spec, planKey) {
77
+ const plan = plans(spec).find((candidate) => candidate.key === planKey);
78
+ if (!plan)
79
+ throw new Error(`Plan "${planKey}" not found`);
80
+ return plan;
81
+ }
82
+ export async function updateSpec(program, client, productId, spec, opts) {
83
+ const result = await client.updateProductConfig(productId, spec, {
84
+ dryRun: opts?.dryRun,
85
+ });
86
+ printResult(program, opts?.message ?? "Product config updated", {
87
+ ok: true,
88
+ productId,
89
+ dryRun: opts?.dryRun === true,
90
+ ...(typeof result === "object" && result ? result : {}),
91
+ });
92
+ return result;
93
+ }
@@ -0,0 +1,43 @@
1
+ import * as output from "../output.js";
2
+ export function registerInitCommand(program, getClient) {
3
+ program
4
+ .command("init <name>")
5
+ .description("Create a new product with a GitHub repo for agent-first configuration")
6
+ .option("--base-url <url>", "Backend API base URL")
7
+ .option("--description <desc>", "Product description")
8
+ .option("--display-name <name>", "Display name")
9
+ .action(async (name, opts) => {
10
+ const client = getClient();
11
+ const result = await client.initProduct({
12
+ name,
13
+ baseUrl: opts.baseUrl,
14
+ description: opts.description,
15
+ displayName: opts.displayName,
16
+ });
17
+ // `program.opts()` is typed `OptionValues` (string-indexed any).
18
+ // Narrow at the boundary so `outputFormat`'s `string | undefined`
19
+ // parameter type is satisfied without an unsafe-arg lint.
20
+ const formatOpt = program.opts().format;
21
+ const format = output.outputFormat(formatOpt);
22
+ if (format === "json") {
23
+ console.log(output.json(result));
24
+ return;
25
+ }
26
+ output.success(`Created product "${result.product.name}" (DRAFT)`);
27
+ console.log();
28
+ if (result.repo) {
29
+ console.log(` Repository: ${result.repo.htmlUrl}`);
30
+ console.log(` Clone: git clone ${result.repo.cloneUrl}`);
31
+ console.log();
32
+ }
33
+ console.log(" Next steps:");
34
+ console.log(" 1. Clone the repository");
35
+ console.log(" 2. Read AGENTS.md for the full configuration reference");
36
+ console.log(" 3. Edit product.yaml — add your base URL, plans, and meters");
37
+ console.log(" 4. Push to main — a valid config goes live automatically");
38
+ console.log();
39
+ if (result.agent.agentsMdUrl) {
40
+ console.log(` Docs: ${result.agent.agentsMdUrl}`);
41
+ }
42
+ });
43
+ }
@@ -0,0 +1,144 @@
1
+ import * as readline from "node:readline/promises";
2
+ import { createClient } from "../client.js";
3
+ import { loadConfig, saveConfig, saveCredentials, clearCredentials, } from "../config.js";
4
+ import { resolveToken } from "../auth.js";
5
+ import * as output from "../output.js";
6
+ export function registerAuthCommands(program) {
7
+ const auth = program
8
+ .command("auth")
9
+ .description("Manage authentication")
10
+ .addHelpText("after", `
11
+ Agent notes:
12
+ Prefer FARTHERSHORE_TOKEN in CI/agent environments. Use auth set-key for local persistent credentials.
13
+
14
+ Examples:
15
+ farthershore auth set-key "$FARTHERSHORE_TOKEN"
16
+ farthershore whoami --format json
17
+ `);
18
+ auth
19
+ .command("set-key [token]")
20
+ .description("Set your API token")
21
+ .addHelpText("after", `
22
+ Examples:
23
+ farthershore auth set-key mk_xxx
24
+ farthershore auth set-key "$FARTHERSHORE_TOKEN"
25
+ `)
26
+ .action(async (tokenArg) => {
27
+ await setKey(tokenArg);
28
+ });
29
+ program
30
+ .command("set-key [token]")
31
+ .description("Set your API token (interactive or pass as argument)")
32
+ .action(async (tokenArg) => {
33
+ await setKey(tokenArg);
34
+ });
35
+ program
36
+ .command("logout")
37
+ .description("Clear stored credentials")
38
+ .action(() => {
39
+ clearCredentials();
40
+ output.success("Credentials cleared.");
41
+ });
42
+ program
43
+ .command("whoami")
44
+ .description("Show current authentication context")
45
+ .action(async () => {
46
+ const config = loadConfig();
47
+ const formatOpt = program.opts().format;
48
+ const format = output.outputFormat(formatOpt);
49
+ try {
50
+ const token = resolveToken();
51
+ const client = createClient({ apiUrl: config.apiUrl, token });
52
+ const ctx = await client.bootstrap();
53
+ const authSource = process.env.FARTHERSHORE_TOKEN
54
+ ? "env:FARTHERSHORE_TOKEN"
55
+ : "credentials-file";
56
+ if (format === "json") {
57
+ // Machine-readable shape — stable contract for CI scripts
58
+ // ("am I logged in?"). Don't include the token itself.
59
+ console.log(output.json({
60
+ organization: {
61
+ id: ctx.activeOrganization.id,
62
+ name: ctx.activeOrganization.name ?? null,
63
+ slug: ctx.activeOrganization.slug ?? null,
64
+ },
65
+ user: { id: ctx.user.id },
66
+ apiUrl: config.apiUrl,
67
+ authSource,
68
+ }));
69
+ return;
70
+ }
71
+ output.heading("Current Context");
72
+ console.log(` Organization: ${ctx.activeOrganization.name ?? ctx.activeOrganization.id}`);
73
+ console.log(` API URL: ${config.apiUrl}`);
74
+ if (authSource === "env:FARTHERSHORE_TOKEN") {
75
+ output.info(" Auth: FARTHERSHORE_TOKEN env var");
76
+ }
77
+ else {
78
+ output.info(" Auth: ~/.farthershore/credentials.json");
79
+ }
80
+ }
81
+ catch {
82
+ if (format === "json") {
83
+ console.log(output.json({ authenticated: false }));
84
+ }
85
+ else {
86
+ output.error("Not authenticated. Run `farthershore set-key` or set FARTHERSHORE_TOKEN.");
87
+ }
88
+ process.exitCode = 1;
89
+ }
90
+ });
91
+ program
92
+ .command("set-url <url>")
93
+ .description("Override the API base URL (for staging/testing)")
94
+ .action((url) => {
95
+ saveConfig({ apiUrl: url });
96
+ output.success(`API URL set to ${url}`);
97
+ });
98
+ program
99
+ .command("reset-url")
100
+ .description("Reset the API URL to production default")
101
+ .action(() => {
102
+ saveConfig({ apiUrl: "https://core.farthershore.com" });
103
+ output.success("API URL reset to https://core.farthershore.com");
104
+ });
105
+ }
106
+ async function setKey(tokenArg) {
107
+ let token = tokenArg?.trim();
108
+ if (!token) {
109
+ if (!process.stdin.isTTY) {
110
+ output.error("No token provided. Pass it as an argument: farthershore auth set-key <token>");
111
+ process.exitCode = 1;
112
+ return;
113
+ }
114
+ console.log("Set your FartherShore API token\n");
115
+ console.log(" Create a token at https://farthershore.com/settings/tokens\n");
116
+ const rl = readline.createInterface({
117
+ input: process.stdin,
118
+ output: process.stdout,
119
+ });
120
+ token = (await rl.question("Token: ")).trim();
121
+ rl.close();
122
+ }
123
+ if (!token) {
124
+ output.error("No token provided.");
125
+ process.exitCode = 1;
126
+ return;
127
+ }
128
+ const config = loadConfig();
129
+ try {
130
+ const client = createClient({ apiUrl: config.apiUrl, token });
131
+ const ctx = await client.bootstrap();
132
+ saveCredentials({
133
+ token,
134
+ orgId: ctx.activeOrganization.id,
135
+ userId: ctx.user.id,
136
+ });
137
+ output.success("Authenticated");
138
+ console.log(` Organization: ${ctx.activeOrganization.name ?? ctx.activeOrganization.id}`);
139
+ }
140
+ catch {
141
+ output.error("Invalid token. Check it and try again.");
142
+ process.exitCode = 1;
143
+ }
144
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { ApiClient } from "../client.js";
3
+ export declare function registerMeterCommands(program: Command, getClient: () => ApiClient): void;
@@ -0,0 +1,121 @@
1
+ import { loadAcceptedSpec, objectAt, printResult, resolveProductId, updateSpec, } from "./helpers.js";
2
+ export function registerMeterCommands(program, getClient) {
3
+ const meter = program
4
+ .command("meter")
5
+ .description("Manage product usage meters and rating sources")
6
+ .addHelpText("after", `
7
+ Agent notes:
8
+ Meters define what usage is measured. Plans reference meters; plans do not define provider-specific rates.
9
+ Rating sources: fixed, provider_catalog, upstream_reported, external_rate_api, custom.
10
+
11
+ Examples:
12
+ farthershore meter add ai_tokens --selector model --measure input_tokens --measure output_tokens --rating provider_catalog --catalog llm_models --format json
13
+ farthershore meter add requests --measure count --rating fixed --format json
14
+ farthershore meter list --format json
15
+ `);
16
+ meter
17
+ .command("list")
18
+ .option("--product <product>", "Product id or name")
19
+ .addHelpText("after", `
20
+ Example:
21
+ farthershore meter list --format json
22
+ `)
23
+ .action(async (opts) => {
24
+ const client = getClient();
25
+ const productId = await resolveProductId(client, opts.product);
26
+ const spec = await loadAcceptedSpec(client, productId);
27
+ const usage = objectAt(spec, "usage");
28
+ printResult(program, "Meters loaded", {
29
+ ok: true,
30
+ productId,
31
+ meters: objectAt(usage, "meters"),
32
+ });
33
+ });
34
+ meter
35
+ .command("add <key>")
36
+ .option("--product <product>", "Product id or name")
37
+ .option("--selector <field>", "Selector field, for example model")
38
+ .option("--measure <measure>", "Measure name; repeatable", collect, [])
39
+ .option("--rating <source>", "Rating source: fixed, provider_catalog, upstream_reported, external_rate_api, custom", "fixed")
40
+ .option("--catalog <catalog>", "Catalog key for provider_catalog rating")
41
+ .option("--resolver <resolver>", "Resolver id for external/custom rating")
42
+ .option("--amount-field <path>", "Amount field for upstream_reported rating")
43
+ .option("--currency-field <path>", "Currency field for upstream_reported rating")
44
+ .option("--dry-run", "Validate without mutating")
45
+ .addHelpText("after", `
46
+ Examples:
47
+ farthershore meter add ai_tokens --selector model --measure input_tokens --measure output_tokens --rating provider_catalog --catalog llm_models --format json
48
+ farthershore meter add requests --measure count --rating fixed --dry-run --format json
49
+ farthershore meter add upstream_cost --measure cost_micros --rating upstream_reported --amount-field usage.cost_micros --format json
50
+ `)
51
+ .action(async (key, opts) => {
52
+ const client = getClient();
53
+ const productId = await resolveProductId(client, opts.product);
54
+ const spec = await loadAcceptedSpec(client, productId);
55
+ const usage = objectAt(spec, "usage");
56
+ const meters = objectAt(usage, "meters");
57
+ meters[key] = {
58
+ ...(opts.selector ? { selector: opts.selector } : {}),
59
+ measures: opts.measure.length > 0 ? opts.measure : ["count"],
60
+ rating: ratingFromOptions(opts),
61
+ };
62
+ await updateSpec(program, client, productId, spec, {
63
+ dryRun: opts.dryRun,
64
+ message: `Meter "${key}" saved`,
65
+ });
66
+ });
67
+ meter
68
+ .command("rating set <key>")
69
+ .option("--product <product>", "Product id or name")
70
+ .requiredOption("--rating <source>", "Rating source: fixed, provider_catalog, upstream_reported, external_rate_api, custom")
71
+ .option("--catalog <catalog>", "Catalog key for provider_catalog rating")
72
+ .option("--resolver <resolver>", "Resolver id for external/custom rating")
73
+ .option("--amount-field <path>", "Amount field for upstream_reported rating")
74
+ .option("--currency-field <path>", "Currency field for upstream_reported rating")
75
+ .option("--dry-run", "Validate without mutating")
76
+ .addHelpText("after", `
77
+ Examples:
78
+ farthershore meter rating set ai_tokens --rating provider_catalog --catalog llm_models --format json
79
+ farthershore meter rating set upstream_cost --rating upstream_reported --amount-field usage.cost_micros --dry-run --format json
80
+ `)
81
+ .action(async (key, opts) => {
82
+ const client = getClient();
83
+ const productId = await resolveProductId(client, opts.product);
84
+ const spec = await loadAcceptedSpec(client, productId);
85
+ const usage = objectAt(spec, "usage");
86
+ const meters = objectAt(usage, "meters");
87
+ const current = meters[key];
88
+ if (!current || typeof current !== "object" || Array.isArray(current)) {
89
+ throw new Error(`Meter "${key}" not found`);
90
+ }
91
+ current.rating = ratingFromOptions(opts);
92
+ await updateSpec(program, client, productId, spec, {
93
+ dryRun: opts.dryRun,
94
+ message: `Meter "${key}" rating saved`,
95
+ });
96
+ });
97
+ }
98
+ function collect(value, previous) {
99
+ previous.push(value);
100
+ return previous;
101
+ }
102
+ function ratingFromOptions(opts) {
103
+ if (opts.rating === "provider_catalog") {
104
+ return {
105
+ source: opts.rating,
106
+ catalog: opts.catalog ?? "default",
107
+ pricePolicy: "pass_through",
108
+ };
109
+ }
110
+ if (opts.rating === "upstream_reported") {
111
+ return {
112
+ source: opts.rating,
113
+ amountField: opts.amountField ?? "usage.cost_micros",
114
+ ...(opts.currencyField ? { currencyField: opts.currencyField } : {}),
115
+ };
116
+ }
117
+ if (opts.rating === "external_rate_api" || opts.rating === "custom") {
118
+ return { source: opts.rating, resolver: opts.resolver ?? "default" };
119
+ }
120
+ return { source: "fixed", rates: {} };
121
+ }
@@ -0,0 +1,40 @@
1
+ import { Command } from "commander";
2
+ type ChangeKind = "strategy_changed" | "price_increase" | "price_decrease" | "feature_added" | "feature_removed" | "limit_increased" | "limit_reduced" | "credit_increased" | "credit_reduced" | "rating_changed" | "plan_added" | "plan_removed";
3
+ type SubscriberAction = "preserve_current_period" | "switch_immediately" | "switch_immediately_prorate" | "new_subscribers_only";
4
+ type TransitionChange = {
5
+ kind: ChangeKind;
6
+ planKey?: string;
7
+ meter?: string;
8
+ feature?: string;
9
+ from?: unknown;
10
+ to?: unknown;
11
+ subscriberAction: SubscriberAction;
12
+ requiresAcknowledgment: boolean;
13
+ message: string;
14
+ };
15
+ type TransitionAnalysis = {
16
+ valid: boolean;
17
+ from: string;
18
+ to: string;
19
+ strategy: {
20
+ from?: string;
21
+ to?: string;
22
+ };
23
+ policy: {
24
+ default: SubscriberAction;
25
+ proration: string;
26
+ when: Record<string, SubscriberAction>;
27
+ };
28
+ changes: TransitionChange[];
29
+ errors: string[];
30
+ warnings: string[];
31
+ agentHints: string[];
32
+ };
33
+ export declare function analyzePlanTransition(options: {
34
+ fromSpec: unknown;
35
+ toSpec: unknown;
36
+ fromLabel?: string;
37
+ toLabel?: string;
38
+ }): TransitionAnalysis;
39
+ export declare function registerPlanTransitionCommand(program: Command): void;
40
+ export {};