@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,101 @@
1
+ import { saveConfig } from "../config.js";
2
+ import * as output from "../output.js";
3
+ import { commandFormat, printResult, resolveProductId } from "./helpers.js";
4
+ export function registerProductCommands(program, getClient) {
5
+ const product = program.command("product").description("Manage products");
6
+ product
7
+ .command("create <name>")
8
+ .description("Create a product and select it for subsequent CLI commands")
9
+ .requiredOption("--base-url <url>", "Backend API base URL")
10
+ .option("--strategy <strategy>", "Product billing strategy", "flat_subscription")
11
+ .option("--description <desc>", "Product description")
12
+ .option("--display-name <name>", "Display name")
13
+ .option("--dry-run", "Print the create request without mutating")
14
+ .action(async (name, opts) => {
15
+ if (opts.dryRun) {
16
+ printResult(program, "Product create dry run", {
17
+ ok: true,
18
+ dryRun: true,
19
+ proposal: {
20
+ name,
21
+ baseUrl: opts.baseUrl,
22
+ description: opts.description,
23
+ displayName: opts.displayName,
24
+ billingStrategy: opts.strategy,
25
+ subscriberChangePolicy: {
26
+ default: "preserve_current_period",
27
+ proration: "none",
28
+ },
29
+ },
30
+ nextActions: ["Run without --dry-run to create the product"],
31
+ });
32
+ return;
33
+ }
34
+ const client = getClient();
35
+ const result = await client.initProduct({
36
+ name,
37
+ baseUrl: opts.baseUrl,
38
+ description: opts.description,
39
+ displayName: opts.displayName,
40
+ billingStrategy: opts.strategy,
41
+ subscriberChangePolicy: {
42
+ default: "preserve_current_period",
43
+ proration: "none",
44
+ },
45
+ });
46
+ saveConfig({
47
+ activeProductId: result.product.id,
48
+ activeProductName: result.product.name,
49
+ });
50
+ printResult(program, `Created product "${result.product.name}"`, {
51
+ ok: true,
52
+ productId: result.product.id,
53
+ product: result.product,
54
+ repo: result.repo,
55
+ githubSyncStatus: result.product
56
+ .acceptedProductSpecGithubSyncStatus ?? null,
57
+ nextActions: [
58
+ "Add meters, features, and plans",
59
+ "Run farthershore apply --dry-run --format json",
60
+ ],
61
+ });
62
+ });
63
+ product
64
+ .command("status")
65
+ .description("Show selected product status")
66
+ .option("--product <product>", "Product id or name")
67
+ .action(async (opts) => {
68
+ const client = getClient();
69
+ const productId = await resolveProductId(client, opts.product);
70
+ const config = await client.getProductConfig(productId);
71
+ printResult(program, "Product status loaded", {
72
+ ok: true,
73
+ productId,
74
+ ...(typeof config === "object" && config ? config : {}),
75
+ });
76
+ });
77
+ product
78
+ .command("config")
79
+ .description("Show latest accepted internal product config")
80
+ .option("--product <product>", "Product id or name")
81
+ .option("--format <format>", "Output format: json or yaml")
82
+ .action(async (opts) => {
83
+ const client = getClient();
84
+ const productId = await resolveProductId(client, opts.product);
85
+ const format = opts.format ?? commandFormat(program);
86
+ if (format === "yaml") {
87
+ console.log(await client.getProductConfigYaml(productId));
88
+ return;
89
+ }
90
+ console.log(output.json(await client.getProductConfig(productId)));
91
+ });
92
+ product
93
+ .command("attempts")
94
+ .description("Show rejected product config attempts")
95
+ .option("--product <product>", "Product id or name")
96
+ .action(async (opts) => {
97
+ const client = getClient();
98
+ const productId = await resolveProductId(client, opts.product);
99
+ console.log(output.json(await client.getProductAttempts(productId)));
100
+ });
101
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { ApiClient } from "../client.js";
3
+ export declare function registerTransitionCommands(program: Command, getClient: () => ApiClient): void;
@@ -0,0 +1,63 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import YAML from "yaml";
4
+ import * as output from "../output.js";
5
+ import { loadAcceptedSpec, resolveProductId } from "./helpers.js";
6
+ import { analyzePlanTransition } from "./plan-transition.js";
7
+ export function registerTransitionCommands(program, getClient) {
8
+ const transition = program
9
+ .command("transition")
10
+ .description("Preview subscriber impact for product config changes");
11
+ transition
12
+ .command("preview")
13
+ .description("Compare the latest accepted config to a proposed config and show subscriber transition impact")
14
+ .option("--product <product>", "Product id or name")
15
+ .option("--to <file>", "Proposed product YAML file")
16
+ .option("--format <format>", "Output format: table or json")
17
+ .action(async (opts) => {
18
+ const client = getClient();
19
+ const productId = await resolveProductId(client, opts.product);
20
+ const accepted = await loadAcceptedSpec(client, productId);
21
+ const proposed = loadProposedSpec(opts.to) ?? accepted;
22
+ const analysis = analyzePlanTransition({
23
+ fromSpec: accepted,
24
+ toSpec: proposed,
25
+ fromLabel: "acceptedProductSpec",
26
+ toLabel: opts.to ?? "acceptedProductSpec",
27
+ });
28
+ const globalOpts = program.opts();
29
+ const format = opts.format ?? output.outputFormat(globalOpts.format);
30
+ if (format === "json") {
31
+ console.log(output.json({
32
+ ok: analysis.valid,
33
+ productId,
34
+ transitionPreview: analysis,
35
+ errors: analysis.errors,
36
+ warnings: analysis.warnings,
37
+ nextActions: analysis.agentHints,
38
+ }));
39
+ if (!analysis.valid)
40
+ process.exitCode = 1;
41
+ return;
42
+ }
43
+ if (analysis.valid)
44
+ output.success("Transition preview passed");
45
+ else
46
+ output.error("Transition preview failed");
47
+ for (const change of analysis.changes) {
48
+ console.log(` • ${change.message} -> ${change.subscriberAction}`);
49
+ }
50
+ for (const warning of analysis.warnings)
51
+ output.warn(warning);
52
+ for (const error of analysis.errors)
53
+ output.error(error);
54
+ if (!analysis.valid)
55
+ process.exitCode = 1;
56
+ });
57
+ }
58
+ function loadProposedSpec(file) {
59
+ const path = resolve(file ?? "product.yaml");
60
+ if (!existsSync(path))
61
+ return null;
62
+ return YAML.parse(readFileSync(path, "utf-8"));
63
+ }
package/dist/index.js CHANGED
@@ -10,6 +10,13 @@ import { registerAuthCommands } from "./commands/login.js";
10
10
  import { registerInitCommand } from "./commands/init.js";
11
11
  import { registerValidateCommand } from "./commands/validate.js";
12
12
  import { registerApplyCommand } from "./commands/apply.js";
13
+ import { registerBillingCommands } from "./commands/billing.js";
14
+ import { registerFeatureCommands } from "./commands/feature.js";
15
+ import { registerMeterCommands } from "./commands/meter.js";
16
+ import { registerPlanTransitionCommand } from "./commands/plan-transition.js";
17
+ import { registerPlanCommands } from "./commands/plan.js";
18
+ import { registerProductCommands } from "./commands/product.js";
19
+ import { registerTransitionCommands } from "./commands/transition.js";
13
20
  import { CliError } from "./types.js";
14
21
  import { getRemediation } from "./remediation.js";
15
22
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -33,7 +40,14 @@ function getClient() {
33
40
  // Register commands
34
41
  registerAuthCommands(program);
35
42
  registerInitCommand(program, getClient);
43
+ registerProductCommands(program, getClient);
44
+ registerBillingCommands(program, getClient);
45
+ registerMeterCommands(program, getClient);
46
+ registerFeatureCommands(program, getClient);
47
+ registerPlanCommands(program, getClient);
48
+ registerTransitionCommands(program, getClient);
36
49
  registerValidateCommand(program);
50
+ registerPlanTransitionCommand(program);
37
51
  registerApplyCommand(program, getClient);
38
52
  // Global error handler
39
53
  program.exitOverride();
package/dist/types.d.ts CHANGED
@@ -33,6 +33,8 @@ export type Plan = BuilderPlan;
33
33
  export type CliConfig = {
34
34
  apiUrl: string;
35
35
  activeOrg?: string;
36
+ activeProductId?: string;
37
+ activeProductName?: string;
36
38
  defaultFormat?: "table" | "json" | "yaml";
37
39
  };
38
40
  export type Credentials = {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@farthershore/cli",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "FartherShore CLI — create and configure API products",
5
5
  "type": "module",
6
6
  "bin": {
7
- "farthershore": "./dist/index.js"
7
+ "farthershore": "dist/index.js"
8
8
  },
9
9
  "files": [
10
10
  "dist"
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "https://github.com/farther-shore/farthershore.git"
20
+ "url": "git+https://github.com/farther-shore/farthershore.git"
21
21
  },
22
22
  "scripts": {
23
23
  "dev": "tsx src/index.ts",