@farthershore/cli 0.3.6 → 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,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
+ }
@@ -4,48 +4,18 @@ import { loadConfig, saveConfig, saveCredentials, clearCredentials, } from "../c
4
4
  import { resolveToken } from "../auth.js";
5
5
  import * as output from "../output.js";
6
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
+ });
7
14
  program
8
15
  .command("set-key [token]")
9
16
  .description("Set your API token (interactive or pass as argument)")
10
17
  .action(async (tokenArg) => {
11
- let token = tokenArg?.trim();
12
- if (!token) {
13
- // Interactive mode
14
- if (!process.stdin.isTTY) {
15
- output.error("No token provided. Pass it as an argument: farthershore set-key <token>");
16
- process.exitCode = 1;
17
- return;
18
- }
19
- console.log("Set your FartherShore API token\n");
20
- console.log(" Create a token at https://farthershore.com/settings/tokens\n");
21
- const rl = readline.createInterface({
22
- input: process.stdin,
23
- output: process.stdout,
24
- });
25
- token = (await rl.question("Token: ")).trim();
26
- rl.close();
27
- }
28
- if (!token) {
29
- output.error("No token provided.");
30
- process.exitCode = 1;
31
- return;
32
- }
33
- const config = loadConfig();
34
- try {
35
- const client = createClient({ apiUrl: config.apiUrl, token });
36
- const ctx = await client.bootstrap();
37
- saveCredentials({
38
- token,
39
- orgId: ctx.activeOrganization.id,
40
- userId: ctx.user.id,
41
- });
42
- output.success("Authenticated");
43
- console.log(` Organization: ${ctx.activeOrganization.name ?? ctx.activeOrganization.id}`);
44
- }
45
- catch {
46
- output.error("Invalid token. Check it and try again.");
47
- process.exitCode = 1;
48
- }
18
+ await setKey(tokenArg);
49
19
  });
50
20
  program
51
21
  .command("logout")
@@ -118,3 +88,42 @@ export function registerAuthCommands(program) {
118
88
  output.success("API URL reset to https://core.farthershore.com");
119
89
  });
120
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 {};