@farthershore/cli 0.3.9 → 0.3.11

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.
Files changed (43) hide show
  1. package/README.md +49 -0
  2. package/dist/index.js +433 -94
  3. package/dist/mcp.js +201 -0
  4. package/package.json +17 -12
  5. package/dist/auth.d.ts +0 -1
  6. package/dist/auth.js +0 -17
  7. package/dist/build-info.d.ts +0 -1
  8. package/dist/build-info.js +0 -10
  9. package/dist/client.d.ts +0 -89
  10. package/dist/client.js +0 -82
  11. package/dist/commands/apply.d.ts +0 -3
  12. package/dist/commands/apply.js +0 -296
  13. package/dist/commands/billing.d.ts +0 -3
  14. package/dist/commands/billing.js +0 -99
  15. package/dist/commands/feature.d.ts +0 -3
  16. package/dist/commands/feature.js +0 -109
  17. package/dist/commands/helpers.d.ts +0 -15
  18. package/dist/commands/helpers.js +0 -93
  19. package/dist/commands/init.d.ts +0 -3
  20. package/dist/commands/init.js +0 -43
  21. package/dist/commands/login.d.ts +0 -2
  22. package/dist/commands/login.js +0 -144
  23. package/dist/commands/meter.d.ts +0 -3
  24. package/dist/commands/meter.js +0 -121
  25. package/dist/commands/plan-transition.d.ts +0 -40
  26. package/dist/commands/plan-transition.js +0 -504
  27. package/dist/commands/plan.d.ts +0 -3
  28. package/dist/commands/plan.js +0 -234
  29. package/dist/commands/product.d.ts +0 -3
  30. package/dist/commands/product.js +0 -137
  31. package/dist/commands/transition.d.ts +0 -3
  32. package/dist/commands/transition.js +0 -80
  33. package/dist/commands/validate.d.ts +0 -28
  34. package/dist/commands/validate.js +0 -216
  35. package/dist/config.d.ts +0 -6
  36. package/dist/config.js +0 -58
  37. package/dist/index.d.ts +0 -2
  38. package/dist/output.d.ts +0 -8
  39. package/dist/output.js +0 -28
  40. package/dist/remediation.d.ts +0 -6
  41. package/dist/remediation.js +0 -53
  42. package/dist/types.d.ts +0 -75
  43. package/dist/types.js +0 -23
@@ -1,216 +0,0 @@
1
- import { readFileSync, existsSync } from "node:fs";
2
- import { execSync } from "node:child_process";
3
- import { resolve } from "node:path";
4
- import YAML from "yaml";
5
- import { productSpecSchema, } from "@farther-shore/shared-types/plans";
6
- import * as output from "../output.js";
7
- const CI = !!process.env.CI || !!process.env.GITHUB_ACTIONS;
8
- /**
9
- * Read product.yaml from the main branch using git, without checking it out.
10
- * Returns null if not in a git repo or main doesn't have the file.
11
- */
12
- function readMainBranchProductName() {
13
- for (const branch of ["main", "master"]) {
14
- try {
15
- const yaml = execSync(`git show ${branch}:product.yaml 2>/dev/null`, {
16
- encoding: "utf-8",
17
- stdio: ["pipe", "pipe", "pipe"],
18
- });
19
- const spec = YAML.parse(yaml);
20
- const product = spec.product;
21
- return product?.name ?? null;
22
- }
23
- catch {
24
- continue;
25
- }
26
- }
27
- return null;
28
- }
29
- /**
30
- * Validate a parsed YAML object against the canonical Zod schema.
31
- *
32
- * Errors are returned in the same `Plan "key": message` shape the previous
33
- * hand-rolled checks produced, so existing CI output stays grep-friendly.
34
- * Warnings cover product-quality nudges (no free plan, missing display name)
35
- * that don't fail the schema parse but are worth flagging.
36
- */
37
- export function validateProductYaml(spec) {
38
- const result = productSpecSchema.safeParse(spec);
39
- if (!result.success) {
40
- return {
41
- valid: false,
42
- errors: result.error.issues.map((issue) => formatIssue(issue, spec)),
43
- warnings: [],
44
- };
45
- }
46
- const warnings = deriveWarnings(result.data);
47
- // Schema doesn't enforce duplicate plan keys (it would conflict with the
48
- // discriminated-union design). Detect here as a separate check so the CLI
49
- // surfaces this footgun before the server rejects the apply.
50
- const dupeKeys = findDuplicatePlanKeys(result.data);
51
- if (dupeKeys.length > 0) {
52
- return {
53
- valid: false,
54
- errors: [`Duplicate plan keys: ${dupeKeys.join(", ")}`],
55
- warnings,
56
- };
57
- }
58
- return { valid: true, errors: [], warnings };
59
- }
60
- function formatIssue(issue, spec) {
61
- const path = issue.path;
62
- // Plans are addressed positionally in path (e.g. ["plans", 0, "key"]). Look
63
- // up the plan key from the spec so the error message reads
64
- // `Plan "starter": pricing is required` instead of `plans.0.pricing: ...`.
65
- if (path.length >= 2 && path[0] === "plans" && typeof path[1] === "number") {
66
- const plans = spec?.plans ?? [];
67
- const plan = plans[path[1]];
68
- const planLabel = plan?.key ?? plan?.name ?? `#${path[1]}`;
69
- const fieldPath = path.slice(2).join(".");
70
- if (fieldPath) {
71
- return `Plan "${planLabel}": ${fieldPath} — ${issue.message}`;
72
- }
73
- return `Plan "${planLabel}": ${issue.message}`;
74
- }
75
- const dotted = path.length > 0 ? path.join(".") : "(root)";
76
- return `${dotted}: ${issue.message}`;
77
- }
78
- function deriveWarnings(parsed) {
79
- const warnings = [];
80
- if (parsed.plans.length === 0) {
81
- // Schema accepts an empty plan list (default), but a product with no
82
- // plans can't go live — surface as a warning so the builder sees the
83
- // requirement before pushing.
84
- warnings.push("No plans declared — product cannot accept signups yet");
85
- }
86
- else {
87
- const hasFree = parsed.plans.some((plan) => {
88
- const monthly = "monthlyPriceCents" in plan.pricing
89
- ? plan.pricing.monthlyPriceCents
90
- : undefined;
91
- return monthly === 0;
92
- });
93
- if (!hasFree) {
94
- warnings.push("No free plan — consider adding one for developer adoption");
95
- }
96
- }
97
- if (!parsed.product.displayName ||
98
- parsed.product.displayName === parsed.product.name) {
99
- warnings.push("product.displayName not set — using product.name on the pricing page");
100
- }
101
- return warnings;
102
- }
103
- function findDuplicatePlanKeys(parsed) {
104
- const seen = new Set();
105
- const dupes = new Set();
106
- for (const plan of parsed.plans) {
107
- if (seen.has(plan.key)) {
108
- dupes.add(plan.key);
109
- }
110
- else {
111
- seen.add(plan.key);
112
- }
113
- }
114
- return [...dupes];
115
- }
116
- /**
117
- * Read + parse a product.yaml file. Returns either the parsed spec or
118
- * a structured error result. Extracted from the validate command so
119
- * the action handler stays under the cognitive-complexity threshold.
120
- */
121
- export function loadProductYaml(filePath) {
122
- if (!existsSync(filePath)) {
123
- return { ok: false, reason: "missing", message: filePath };
124
- }
125
- try {
126
- const content = readFileSync(filePath, "utf-8");
127
- return { ok: true, spec: YAML.parse(content) };
128
- }
129
- catch (err) {
130
- return {
131
- ok: false,
132
- reason: "parse",
133
- message: err instanceof Error ? err.message : String(err),
134
- };
135
- }
136
- }
137
- /**
138
- * Print a ValidationResult to stdout, optionally emitting GitHub
139
- * Actions annotations. Sets `process.exitCode = 1` on validation
140
- * failure so the CLI exits non-zero.
141
- */
142
- function reportValidationResult(result) {
143
- if (CI) {
144
- for (const err of result.errors) {
145
- console.log(`::error file=product.yaml::${err}`);
146
- }
147
- for (const w of result.warnings) {
148
- console.log(`::warning file=product.yaml::${w}`);
149
- }
150
- }
151
- if (result.errors.length === 0) {
152
- output.success("product.yaml is valid");
153
- for (const w of result.warnings) {
154
- output.warn(w);
155
- }
156
- return;
157
- }
158
- output.error(`product.yaml has ${result.errors.length} error(s):\n`);
159
- for (const err of result.errors) {
160
- console.log(` • ${err}`);
161
- }
162
- if (result.warnings.length > 0) {
163
- console.log();
164
- for (const w of result.warnings) {
165
- output.warn(w);
166
- }
167
- }
168
- process.exitCode = 1;
169
- }
170
- export function registerValidateCommand(program) {
171
- program
172
- .command("validate [file]")
173
- .description("Validate a local product.yaml file without API calls")
174
- .addHelpText("after", `
175
- Agent notes:
176
- Use validate before committing product.yaml changes. This is local schema validation only.
177
- Use apply --dry-run for remote compiler validation against pushed branch state.
178
-
179
- Examples:
180
- farthershore validate
181
- farthershore validate product.yaml
182
- farthershore apply --dry-run --format json
183
- `)
184
- // Action body is sync; commander only requires a thenable on
185
- // .action when await is used. Drop `async` keyword.
186
- .action((file) => {
187
- const filePath = resolve(file ?? "product.yaml");
188
- const loaded = loadProductYaml(filePath);
189
- if (!loaded.ok) {
190
- if (loaded.reason === "missing") {
191
- if (CI)
192
- console.log(`::error file=product.yaml::File not found: ${loaded.message}`);
193
- output.error(`File not found: ${loaded.message}`);
194
- }
195
- else {
196
- if (CI)
197
- console.log(`::error file=product.yaml::YAML parse error: ${loaded.message}`);
198
- output.error(`Failed to parse YAML: ${loaded.message}`);
199
- }
200
- process.exitCode = 1;
201
- return;
202
- }
203
- const result = validateProductYaml(loaded.spec);
204
- // Check product.name consistency against main branch
205
- const currentName = loaded.spec
206
- ?.product?.name;
207
- if (currentName) {
208
- const mainName = readMainBranchProductName();
209
- if (mainName && mainName !== currentName) {
210
- result.errors.push(`product.name "${currentName}" differs from main branch "${mainName}" — product name must not change`);
211
- result.valid = false;
212
- }
213
- }
214
- reportValidationResult(result);
215
- });
216
- }
package/dist/config.d.ts DELETED
@@ -1,6 +0,0 @@
1
- import type { CliConfig, Credentials } from "./types.js";
2
- export declare function loadConfig(): CliConfig;
3
- export declare function saveConfig(config: Partial<CliConfig>): void;
4
- export declare function loadCredentials(): Credentials | null;
5
- export declare function saveCredentials(creds: Credentials): void;
6
- export declare function clearCredentials(): void;
package/dist/config.js DELETED
@@ -1,58 +0,0 @@
1
- // ~/.farthershore/ config management
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
5
- import { BUILD_API_URL } from "./build-info.js";
6
- const CONFIG_DIR = join(homedir(), ".farthershore");
7
- const CONFIG_FILE = join(CONFIG_DIR, "config.json");
8
- const CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
9
- function ensureDir(dir) {
10
- if (!existsSync(dir))
11
- mkdirSync(dir, { recursive: true, mode: 0o700 });
12
- }
13
- // --- Config ---
14
- const DEFAULT_CONFIG = {
15
- apiUrl: BUILD_API_URL,
16
- defaultFormat: "table",
17
- };
18
- export function loadConfig() {
19
- ensureDir(CONFIG_DIR);
20
- if (!existsSync(CONFIG_FILE))
21
- return DEFAULT_CONFIG;
22
- try {
23
- // JSON.parse → `any`; cast to a partial of the typed shape so the
24
- // spread doesn't propagate `any` into the returned union.
25
- const parsed = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
26
- return { ...DEFAULT_CONFIG, ...parsed };
27
- }
28
- catch {
29
- return DEFAULT_CONFIG;
30
- }
31
- }
32
- export function saveConfig(config) {
33
- ensureDir(CONFIG_DIR);
34
- const current = loadConfig();
35
- writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...config }, null, 2) + "\n");
36
- }
37
- // --- Credentials ---
38
- export function loadCredentials() {
39
- if (!existsSync(CREDENTIALS_FILE))
40
- return null;
41
- try {
42
- return JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
43
- }
44
- catch {
45
- return null;
46
- }
47
- }
48
- export function saveCredentials(creds) {
49
- ensureDir(CONFIG_DIR);
50
- writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2) + "\n", {
51
- mode: 0o600,
52
- });
53
- }
54
- export function clearCredentials() {
55
- if (existsSync(CREDENTIALS_FILE)) {
56
- writeFileSync(CREDENTIALS_FILE, "{}");
57
- }
58
- }
package/dist/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/dist/output.d.ts DELETED
@@ -1,8 +0,0 @@
1
- export declare function json(data: unknown): string;
2
- export declare function success(msg: string): void;
3
- export declare function error(msg: string): void;
4
- export declare function warn(msg: string): void;
5
- export declare function info(msg: string): void;
6
- export declare function heading(msg: string): void;
7
- export declare function isTTY(): boolean;
8
- export declare function outputFormat(flagFormat: string | undefined): "table" | "json";
package/dist/output.js DELETED
@@ -1,28 +0,0 @@
1
- // Output formatting: tables, JSON, colors
2
- import chalk from "chalk";
3
- export function json(data) {
4
- return JSON.stringify(data, null, 2);
5
- }
6
- export function success(msg) {
7
- console.log(chalk.green(`✓ ${msg}`));
8
- }
9
- export function error(msg) {
10
- console.error(chalk.red(`✗ ${msg}`));
11
- }
12
- export function warn(msg) {
13
- console.warn(chalk.yellow(`⚠ ${msg}`));
14
- }
15
- export function info(msg) {
16
- console.log(chalk.dim(msg));
17
- }
18
- export function heading(msg) {
19
- console.log(chalk.bold(msg));
20
- }
21
- export function isTTY() {
22
- return process.stdout.isTTY === true;
23
- }
24
- export function outputFormat(flagFormat) {
25
- if (flagFormat === "json" || flagFormat === "table")
26
- return flagFormat;
27
- return isTTY() ? "table" : "json";
28
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Look up a one-line remediation hint for a server error code. Returns
3
- * `undefined` if no hint is registered — callers should treat that as
4
- * "show no hint" rather than a fallback string.
5
- */
6
- export declare function getRemediation(code: string | undefined): string | undefined;
@@ -1,53 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Per-error-code remediation hints.
3
- //
4
- // When `core` returns a 4XX/5XX with a known `error.code`, the CLI prints
5
- // the code alongside the message AND a 1-line "next step" so users (and
6
- // agents) don't have to grep docs to figure out what to do.
7
- //
8
- // Codes are kept as raw strings — the CLI doesn't depend on the SDK, and
9
- // pulling in `@farther-shore/sdk` just for a string union would be more
10
- // coupling than this is worth. If a code is missing here, no remediation
11
- // is shown — fail-safe, never wrong.
12
- // ---------------------------------------------------------------------------
13
- const REMEDIATIONS = {
14
- // --- Auth ---
15
- UNAUTHORIZED: "Token is invalid or revoked. Run `farthershore set-key` to update it.",
16
- FORBIDDEN: "Your token doesn't have access to this resource. Check the org / product scope.",
17
- INVALID_ACCESS_KEY: "Token format is wrong. Generate a new one at https://farthershore.com/settings/tokens.",
18
- MAKER_TOKEN_REVOKED: "This maker token was revoked. Mint a new one in the product settings.",
19
- MAKER_TOKEN_NO_PRODUCT: "Maker token is not bound to a product. Re-create it from the product page.",
20
- // --- Stripe ---
21
- STRIPE_NOT_CONFIGURED: "Stripe isn't connected on this product. Connect it in the dashboard before running billing operations.",
22
- STRIPE_BALANCE_OUTSTANDING: "Customer has an outstanding balance. Resolve the invoice in Stripe before retrying.",
23
- CHECKOUT_SESSION_FAILED: "Stripe rejected the checkout request. Check that the plan exists and Stripe credentials are valid.",
24
- // --- Product / config ---
25
- PRODUCT_NOT_FOUND: "Check the product slug. Run `farthershore` (no args) for a list of products you can see.",
26
- PRODUCT_REPO_NOT_LINKED: "Link a GitHub repo to this product before running `apply`.",
27
- PRODUCT_YAML_NOT_FOUND: "No product.yaml on the target branch. Push a config file before running `apply`.",
28
- YAML_PARSE_ERROR: "product.yaml is not valid YAML. Run `farthershore validate` locally to see the parse error.",
29
- GITHUB_NOT_CONNECTED: "Connect GitHub on the org page before running `init` (it provisions the repo).",
30
- BRANCH_NO_MATCHING_ENV: "The current branch isn't mapped to an environment. Add a branch rule in product settings or pass --branch explicitly.",
31
- // --- Plans / pricing ---
32
- PLAN_NOT_FOUND: "Plan key doesn't exist on this product. Check `plans:` in product.yaml.",
33
- PLAN_HAS_ACTIVE_SUBSCRIPTIONS: "Plan has active subscribers and can't be deleted. Migrate them to another plan first.",
34
- PLAN_SLUG_CONFLICT: "Another plan already uses this key. Pick a unique key in product.yaml.",
35
- SLUG_CONFLICT: "This product slug is taken. Pick a different name.",
36
- SLUG_BLOCKED: "This slug is reserved or blocked. Pick a different name.",
37
- SLUG_RESERVED: "This slug is reserved by Farther Shore. Pick a different name.",
38
- SLUG_INVALID_FORMAT: "Slug must be lowercase letters, digits, and hyphens (no leading/trailing hyphen).",
39
- // --- Generic ---
40
- RATE_LIMIT_EXCEEDED: "You've hit the rate limit. Wait a moment and retry.",
41
- VALIDATION_ERROR: "Request is malformed. The `details` field has the field-level errors.",
42
- CONFLICT: "The resource is in a state that conflicts with the request. Inspect `details` to learn more.",
43
- };
44
- /**
45
- * Look up a one-line remediation hint for a server error code. Returns
46
- * `undefined` if no hint is registered — callers should treat that as
47
- * "show no hint" rather than a fallback string.
48
- */
49
- export function getRemediation(code) {
50
- if (!code)
51
- return undefined;
52
- return REMEDIATIONS[code];
53
- }
package/dist/types.d.ts DELETED
@@ -1,75 +0,0 @@
1
- import type { BuilderPlan } from "@farther-shore/shared-types/plans";
2
- /**
3
- * Diagnostic emitted by core's /compile endpoints. The `compiledPlanId` and
4
- * `planKey` fields (added in core v0.14.0) scope a diagnostic to a specific
5
- * plan; both stay optional so diagnostics from older core builds still
6
- * typecheck while the new shape rolls out.
7
- */
8
- export type CompileDiagnostic = {
9
- code?: string;
10
- message: string;
11
- compiledPlanId?: string;
12
- planKey?: string;
13
- };
14
- export type Product = {
15
- id: string;
16
- name: string;
17
- displayName?: string | null;
18
- description: string | null;
19
- baseUrl: string;
20
- status: "DRAFT" | "ACTIVE";
21
- runtimeHostname?: string | null;
22
- portalHostname?: string | null;
23
- createdAt: string;
24
- plans: Plan[];
25
- };
26
- /**
27
- * `Plan` here is the canonical shared-types `BuilderPlan` projection: a
28
- * `PlanSpec` extended with server-owned fields (id, productId, environmentId,
29
- * isActive, createdAt). Adding a field to `planSpecSchema` automatically
30
- * surfaces it on this type — drift between CLI and server is a compile error.
31
- */
32
- export type Plan = BuilderPlan;
33
- export type CliConfig = {
34
- apiUrl: string;
35
- activeOrg?: string;
36
- activeProductId?: string;
37
- activeProductName?: string;
38
- defaultFormat?: "table" | "json" | "yaml";
39
- };
40
- export type Credentials = {
41
- token: string;
42
- orgId?: string;
43
- userId?: string;
44
- };
45
- export type InitProductResponse = {
46
- product: Product;
47
- repo: {
48
- fullName: string;
49
- htmlUrl: string;
50
- cloneUrl: string;
51
- } | null;
52
- agent: {
53
- instructions: string;
54
- agentsMdUrl: string | null;
55
- };
56
- };
57
- /**
58
- * Error thrown by the CLI for any non-success path. Carries the canonical
59
- * `code` from core's `{ error: { code, message, details? } }` envelope so
60
- * the global error handler can print the code alongside the message and
61
- * dispatch a remediation hint when one is registered for that code.
62
- *
63
- * `code`/`details` are optional because the same class also gets thrown for
64
- * local (non-API) failures — e.g. "no token configured" — where there's no
65
- * server-side code to surface.
66
- */
67
- export declare class CliError extends Error {
68
- status?: number | undefined;
69
- readonly code?: string;
70
- readonly details?: Record<string, unknown>;
71
- constructor(message: string, status?: number | undefined, extra?: {
72
- code?: string;
73
- details?: Record<string, unknown>;
74
- });
75
- }
package/dist/types.js DELETED
@@ -1,23 +0,0 @@
1
- // Shared types for the FartherShore CLI
2
- /**
3
- * Error thrown by the CLI for any non-success path. Carries the canonical
4
- * `code` from core's `{ error: { code, message, details? } }` envelope so
5
- * the global error handler can print the code alongside the message and
6
- * dispatch a remediation hint when one is registered for that code.
7
- *
8
- * `code`/`details` are optional because the same class also gets thrown for
9
- * local (non-API) failures — e.g. "no token configured" — where there's no
10
- * server-side code to surface.
11
- */
12
- export class CliError extends Error {
13
- status;
14
- code;
15
- details;
16
- constructor(message, status, extra) {
17
- super(message);
18
- this.status = status;
19
- this.name = "CliError";
20
- this.code = extra?.code;
21
- this.details = extra?.details;
22
- }
23
- }