@farthershore/cli 0.3.7 → 0.3.8

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,59 @@
1
+ import { loadAcceptedSpec, objectAt, printResult, resolveProductId, updateSpec, } from "./helpers.js";
2
+ export function registerBillingCommands(program, getClient) {
3
+ const billing = program.command("billing").description("Manage billing");
4
+ const strategy = billing.command("strategy").description("Billing strategy");
5
+ strategy
6
+ .command("get")
7
+ .option("--product <product>", "Product id or name")
8
+ .action(async (opts) => {
9
+ const client = getClient();
10
+ const productId = await resolveProductId(client, opts.product);
11
+ const config = (await client.getProductConfig(productId));
12
+ printResult(program, "Billing strategy loaded", {
13
+ ok: true,
14
+ productId,
15
+ strategy: config.acceptedConfig?.billing?.strategy ?? null,
16
+ });
17
+ });
18
+ strategy
19
+ .command("set <strategy>")
20
+ .option("--product <product>", "Product id or name")
21
+ .option("--transition <action>", "Default subscriber transition action", "preserve_current_period")
22
+ .option("--proration <mode>", "Proration mode", "none")
23
+ .option("--dry-run", "Validate without mutating")
24
+ .action(async (nextStrategy, opts) => {
25
+ const client = getClient();
26
+ const productId = await resolveProductId(client, opts.product);
27
+ const spec = await loadAcceptedSpec(client, productId);
28
+ const billing = objectAt(spec, "billing");
29
+ billing.strategy = nextStrategy;
30
+ billing.subscriberChangePolicy = {
31
+ default: opts.transition,
32
+ proration: opts.proration,
33
+ };
34
+ await updateSpec(program, client, productId, spec, {
35
+ dryRun: opts.dryRun,
36
+ message: "Billing strategy updated",
37
+ });
38
+ });
39
+ billing
40
+ .command("policy set")
41
+ .option("--product <product>", "Product id or name")
42
+ .option("--default <action>", "Default subscriber action", "preserve_current_period")
43
+ .option("--proration <mode>", "Proration mode", "none")
44
+ .option("--dry-run", "Validate without mutating")
45
+ .action(async (opts) => {
46
+ const client = getClient();
47
+ const productId = await resolveProductId(client, opts.product);
48
+ const spec = await loadAcceptedSpec(client, productId);
49
+ const billing = objectAt(spec, "billing");
50
+ billing.subscriberChangePolicy = {
51
+ default: opts.default,
52
+ proration: opts.proration,
53
+ };
54
+ await updateSpec(program, client, productId, spec, {
55
+ dryRun: opts.dryRun,
56
+ message: "Billing policy updated",
57
+ });
58
+ });
59
+ }
@@ -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,80 @@
1
+ import { findPlan, loadAcceptedSpec, objectAt, printResult, resolveProductId, stringArrayAt, updateSpec, } from "./helpers.js";
2
+ export function registerFeatureCommands(program, getClient) {
3
+ const feature = program.command("feature").description("Manage features");
4
+ feature
5
+ .command("list")
6
+ .option("--product <product>", "Product id or name")
7
+ .action(async (opts) => {
8
+ const client = getClient();
9
+ const productId = await resolveProductId(client, opts.product);
10
+ const spec = await loadAcceptedSpec(client, productId);
11
+ printResult(program, "Features loaded", {
12
+ ok: true,
13
+ productId,
14
+ features: objectAt(spec, "features"),
15
+ });
16
+ });
17
+ feature
18
+ .command("add <key>")
19
+ .option("--product <product>", "Product id or name")
20
+ .option("--route <route>", "Route binding METHOD:/path; repeatable", collect, [])
21
+ .option("--dry-run", "Validate without mutating")
22
+ .action(async (key, opts) => {
23
+ const client = getClient();
24
+ const productId = await resolveProductId(client, opts.product);
25
+ const spec = await loadAcceptedSpec(client, productId);
26
+ const features = objectAt(spec, "features");
27
+ features[key] = { routes: opts.route.map(parseRoute) };
28
+ await updateSpec(program, client, productId, spec, {
29
+ dryRun: opts.dryRun,
30
+ message: `Feature "${key}" saved`,
31
+ });
32
+ });
33
+ feature
34
+ .command("bind <key>")
35
+ .requiredOption("--plan <plan>", "Plan key")
36
+ .option("--product <product>", "Product id or name")
37
+ .option("--dry-run", "Validate without mutating")
38
+ .action(async (key, opts) => {
39
+ const client = getClient();
40
+ const productId = await resolveProductId(client, opts.product);
41
+ const spec = await loadAcceptedSpec(client, productId);
42
+ const plan = findPlan(spec, opts.plan);
43
+ const features = stringArrayAt(plan, "features");
44
+ if (!features.includes(key))
45
+ features.push(key);
46
+ await updateSpec(program, client, productId, spec, {
47
+ dryRun: opts.dryRun,
48
+ message: `Feature "${key}" bound to "${opts.plan}"`,
49
+ });
50
+ });
51
+ feature
52
+ .command("unbind <key>")
53
+ .requiredOption("--plan <plan>", "Plan key")
54
+ .option("--product <product>", "Product id or name")
55
+ .option("--dry-run", "Validate without mutating")
56
+ .action(async (key, opts) => {
57
+ const client = getClient();
58
+ const productId = await resolveProductId(client, opts.product);
59
+ const spec = await loadAcceptedSpec(client, productId);
60
+ const plan = findPlan(spec, opts.plan);
61
+ plan.features = stringArrayAt(plan, "features").filter((featureKey) => featureKey !== key);
62
+ await updateSpec(program, client, productId, spec, {
63
+ dryRun: opts.dryRun,
64
+ message: `Feature "${key}" unbound from "${opts.plan}"`,
65
+ });
66
+ });
67
+ }
68
+ function collect(value, previous) {
69
+ previous.push(value);
70
+ return previous;
71
+ }
72
+ function parseRoute(value) {
73
+ const separator = value.indexOf(":");
74
+ if (separator === -1)
75
+ return { method: "*", path: value };
76
+ return {
77
+ method: value.slice(0, separator).toUpperCase(),
78
+ path: value.slice(separator + 1),
79
+ };
80
+ }
@@ -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,129 @@
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.command("auth").description("Manage authentication");
8
+ auth
9
+ .command("set-key [token]")
10
+ .description("Set your API token")
11
+ .action(async (tokenArg) => {
12
+ await setKey(tokenArg);
13
+ });
14
+ program
15
+ .command("set-key [token]")
16
+ .description("Set your API token (interactive or pass as argument)")
17
+ .action(async (tokenArg) => {
18
+ await setKey(tokenArg);
19
+ });
20
+ program
21
+ .command("logout")
22
+ .description("Clear stored credentials")
23
+ .action(() => {
24
+ clearCredentials();
25
+ output.success("Credentials cleared.");
26
+ });
27
+ program
28
+ .command("whoami")
29
+ .description("Show current authentication context")
30
+ .action(async () => {
31
+ const config = loadConfig();
32
+ const formatOpt = program.opts().format;
33
+ const format = output.outputFormat(formatOpt);
34
+ try {
35
+ const token = resolveToken();
36
+ const client = createClient({ apiUrl: config.apiUrl, token });
37
+ const ctx = await client.bootstrap();
38
+ const authSource = process.env.FARTHERSHORE_TOKEN
39
+ ? "env:FARTHERSHORE_TOKEN"
40
+ : "credentials-file";
41
+ if (format === "json") {
42
+ // Machine-readable shape — stable contract for CI scripts
43
+ // ("am I logged in?"). Don't include the token itself.
44
+ console.log(output.json({
45
+ organization: {
46
+ id: ctx.activeOrganization.id,
47
+ name: ctx.activeOrganization.name ?? null,
48
+ slug: ctx.activeOrganization.slug ?? null,
49
+ },
50
+ user: { id: ctx.user.id },
51
+ apiUrl: config.apiUrl,
52
+ authSource,
53
+ }));
54
+ return;
55
+ }
56
+ output.heading("Current Context");
57
+ console.log(` Organization: ${ctx.activeOrganization.name ?? ctx.activeOrganization.id}`);
58
+ console.log(` API URL: ${config.apiUrl}`);
59
+ if (authSource === "env:FARTHERSHORE_TOKEN") {
60
+ output.info(" Auth: FARTHERSHORE_TOKEN env var");
61
+ }
62
+ else {
63
+ output.info(" Auth: ~/.farthershore/credentials.json");
64
+ }
65
+ }
66
+ catch {
67
+ if (format === "json") {
68
+ console.log(output.json({ authenticated: false }));
69
+ }
70
+ else {
71
+ output.error("Not authenticated. Run `farthershore set-key` or set FARTHERSHORE_TOKEN.");
72
+ }
73
+ process.exitCode = 1;
74
+ }
75
+ });
76
+ program
77
+ .command("set-url <url>")
78
+ .description("Override the API base URL (for staging/testing)")
79
+ .action((url) => {
80
+ saveConfig({ apiUrl: url });
81
+ output.success(`API URL set to ${url}`);
82
+ });
83
+ program
84
+ .command("reset-url")
85
+ .description("Reset the API URL to production default")
86
+ .action(() => {
87
+ saveConfig({ apiUrl: "https://core.farthershore.com" });
88
+ output.success("API URL reset to https://core.farthershore.com");
89
+ });
90
+ }
91
+ async function setKey(tokenArg) {
92
+ let token = tokenArg?.trim();
93
+ if (!token) {
94
+ if (!process.stdin.isTTY) {
95
+ output.error("No token provided. Pass it as an argument: farthershore auth set-key <token>");
96
+ process.exitCode = 1;
97
+ return;
98
+ }
99
+ console.log("Set your FartherShore API token\n");
100
+ console.log(" Create a token at https://farthershore.com/settings/tokens\n");
101
+ const rl = readline.createInterface({
102
+ input: process.stdin,
103
+ output: process.stdout,
104
+ });
105
+ token = (await rl.question("Token: ")).trim();
106
+ rl.close();
107
+ }
108
+ if (!token) {
109
+ output.error("No token provided.");
110
+ process.exitCode = 1;
111
+ return;
112
+ }
113
+ const config = loadConfig();
114
+ try {
115
+ const client = createClient({ apiUrl: config.apiUrl, token });
116
+ const ctx = await client.bootstrap();
117
+ saveCredentials({
118
+ token,
119
+ orgId: ctx.activeOrganization.id,
120
+ userId: ctx.user.id,
121
+ });
122
+ output.success("Authenticated");
123
+ console.log(` Organization: ${ctx.activeOrganization.name ?? ctx.activeOrganization.id}`);
124
+ }
125
+ catch {
126
+ output.error("Invalid token. Check it and try again.");
127
+ process.exitCode = 1;
128
+ }
129
+ }
@@ -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,94 @@
1
+ import { loadAcceptedSpec, objectAt, printResult, resolveProductId, updateSpec, } from "./helpers.js";
2
+ export function registerMeterCommands(program, getClient) {
3
+ const meter = program.command("meter").description("Manage usage meters");
4
+ meter
5
+ .command("list")
6
+ .option("--product <product>", "Product id or name")
7
+ .action(async (opts) => {
8
+ const client = getClient();
9
+ const productId = await resolveProductId(client, opts.product);
10
+ const spec = await loadAcceptedSpec(client, productId);
11
+ const usage = objectAt(spec, "usage");
12
+ printResult(program, "Meters loaded", {
13
+ ok: true,
14
+ productId,
15
+ meters: objectAt(usage, "meters"),
16
+ });
17
+ });
18
+ meter
19
+ .command("add <key>")
20
+ .option("--product <product>", "Product id or name")
21
+ .option("--selector <field>", "Selector field, for example model")
22
+ .option("--measure <measure>", "Measure name; repeatable", collect, [])
23
+ .option("--rating <source>", "Rating source", "fixed")
24
+ .option("--catalog <catalog>", "Catalog key for provider_catalog rating")
25
+ .option("--resolver <resolver>", "Resolver id for external/custom rating")
26
+ .option("--amount-field <path>", "Amount field for upstream_reported rating")
27
+ .option("--currency-field <path>", "Currency field for upstream_reported rating")
28
+ .option("--dry-run", "Validate without mutating")
29
+ .action(async (key, opts) => {
30
+ const client = getClient();
31
+ const productId = await resolveProductId(client, opts.product);
32
+ const spec = await loadAcceptedSpec(client, productId);
33
+ const usage = objectAt(spec, "usage");
34
+ const meters = objectAt(usage, "meters");
35
+ meters[key] = {
36
+ ...(opts.selector ? { selector: opts.selector } : {}),
37
+ measures: opts.measure.length > 0 ? opts.measure : ["count"],
38
+ rating: ratingFromOptions(opts),
39
+ };
40
+ await updateSpec(program, client, productId, spec, {
41
+ dryRun: opts.dryRun,
42
+ message: `Meter "${key}" saved`,
43
+ });
44
+ });
45
+ meter
46
+ .command("rating set <key>")
47
+ .option("--product <product>", "Product id or name")
48
+ .requiredOption("--rating <source>", "Rating source")
49
+ .option("--catalog <catalog>", "Catalog key for provider_catalog rating")
50
+ .option("--resolver <resolver>", "Resolver id for external/custom rating")
51
+ .option("--amount-field <path>", "Amount field for upstream_reported rating")
52
+ .option("--currency-field <path>", "Currency field for upstream_reported rating")
53
+ .option("--dry-run", "Validate without mutating")
54
+ .action(async (key, opts) => {
55
+ const client = getClient();
56
+ const productId = await resolveProductId(client, opts.product);
57
+ const spec = await loadAcceptedSpec(client, productId);
58
+ const usage = objectAt(spec, "usage");
59
+ const meters = objectAt(usage, "meters");
60
+ const current = meters[key];
61
+ if (!current || typeof current !== "object" || Array.isArray(current)) {
62
+ throw new Error(`Meter "${key}" not found`);
63
+ }
64
+ current.rating = ratingFromOptions(opts);
65
+ await updateSpec(program, client, productId, spec, {
66
+ dryRun: opts.dryRun,
67
+ message: `Meter "${key}" rating saved`,
68
+ });
69
+ });
70
+ }
71
+ function collect(value, previous) {
72
+ previous.push(value);
73
+ return previous;
74
+ }
75
+ function ratingFromOptions(opts) {
76
+ if (opts.rating === "provider_catalog") {
77
+ return {
78
+ source: opts.rating,
79
+ catalog: opts.catalog ?? "default",
80
+ pricePolicy: "pass_through",
81
+ };
82
+ }
83
+ if (opts.rating === "upstream_reported") {
84
+ return {
85
+ source: opts.rating,
86
+ amountField: opts.amountField ?? "usage.cost_micros",
87
+ ...(opts.currencyField ? { currencyField: opts.currencyField } : {}),
88
+ };
89
+ }
90
+ if (opts.rating === "external_rate_api" || opts.rating === "custom") {
91
+ return { source: opts.rating, resolver: opts.resolver ?? "default" };
92
+ }
93
+ return { source: "fixed", rates: {} };
94
+ }
@@ -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 {};