@optima-chat/dev-skills 0.7.33 ā 0.7.36
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.
- package/AGENTS.md +3 -0
- package/bin/helpers/billing-http.ts +191 -0
- package/bin/helpers/confirm-prompt.ts +23 -0
- package/bin/helpers/entitlement/grant.ts +70 -0
- package/bin/helpers/entitlement/list.ts +77 -0
- package/bin/helpers/entitlement/revoke.ts +116 -0
- package/bin/helpers/entitlement.ts +32 -0
- package/bin/helpers/infisical-secrets.ts +41 -0
- package/bin/helpers/plugin/set-default.ts +65 -0
- package/bin/helpers/plugin/set-paid.ts +69 -0
- package/bin/helpers/plugin/show.ts +42 -0
- package/bin/helpers/plugin.ts +32 -0
- package/bin/helpers/product/add-channel.ts +78 -0
- package/bin/helpers/product/create.ts +96 -0
- package/bin/helpers/product/show.ts +43 -0
- package/bin/helpers/product/toggle-channel.ts +56 -0
- package/bin/helpers/product/update.ts +72 -0
- package/bin/helpers/product.ts +46 -0
- package/dist/bin/helpers/billing-http.js +153 -0
- package/dist/bin/helpers/confirm-prompt.js +54 -0
- package/dist/bin/helpers/entitlement/grant.js +73 -0
- package/dist/bin/helpers/entitlement/list.js +62 -0
- package/dist/bin/helpers/entitlement/revoke.js +105 -0
- package/dist/bin/helpers/entitlement.js +39 -0
- package/dist/bin/helpers/infisical-secrets.js +33 -0
- package/dist/bin/helpers/plugin/set-default.js +61 -0
- package/dist/bin/helpers/plugin/set-paid.js +65 -0
- package/dist/bin/helpers/plugin/show.js +45 -0
- package/dist/bin/helpers/plugin.js +39 -0
- package/dist/bin/helpers/product/add-channel.js +95 -0
- package/dist/bin/helpers/product/create.js +124 -0
- package/dist/bin/helpers/product/show.js +46 -0
- package/dist/bin/helpers/product/toggle-channel.js +61 -0
- package/dist/bin/helpers/product/update.js +92 -0
- package/dist/bin/helpers/product.js +53 -0
- package/docs/superpowers/plans/2026-05-24-marketplace-admin-cli-impl.md +1973 -0
- package/docs/superpowers/plans/2026-05-25-optima-plugin-cli-impl.md +700 -0
- package/docs/superpowers/specs/2026-05-24-marketplace-admin-cli-design.md +324 -0
- package/docs/superpowers/specs/2026-05-25-optima-plugin-cli-design.md +156 -0
- package/package.json +8 -5
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { callSkills, validateEnv } from '../billing-http';
|
|
2
|
+
import { confirmIfProd } from '../confirm-prompt';
|
|
3
|
+
|
|
4
|
+
interface SetPaidArgs {
|
|
5
|
+
slug: string;
|
|
6
|
+
paid: boolean;
|
|
7
|
+
yes: boolean;
|
|
8
|
+
env: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv: string[]): SetPaidArgs {
|
|
12
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
13
|
+
console.log(`Usage: optima-plugin set-paid --slug <slug> --paid true|false [options]
|
|
14
|
+
|
|
15
|
+
Required:
|
|
16
|
+
--slug <slug>
|
|
17
|
+
--paid true|false Sets Plugin.isPaid (the user-facing paid/free gate)
|
|
18
|
+
|
|
19
|
+
Optional:
|
|
20
|
+
--yes Skip prod confirmation prompt (no-op on stage)
|
|
21
|
+
--env stage|prod (default: stage)
|
|
22
|
+
|
|
23
|
+
Note: salesUrl is NOT settable here (skills PATCH is strict; salesUrl is
|
|
24
|
+
publish-time-only via plugin.json metadata). When isPaid=true and salesUrl is
|
|
25
|
+
null, the 402 falls back to sales.optima.onl.`);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
const out: Partial<SetPaidArgs> = { env: 'stage', yes: false };
|
|
29
|
+
for (let i = 0; i < argv.length; i++) {
|
|
30
|
+
const a = argv[i];
|
|
31
|
+
const next = argv[i + 1];
|
|
32
|
+
switch (a) {
|
|
33
|
+
case '--slug': out.slug = next; i++; break;
|
|
34
|
+
case '--paid':
|
|
35
|
+
if (next !== 'true' && next !== 'false') throw new Error('--paid must be true or false');
|
|
36
|
+
out.paid = next === 'true'; i++; break;
|
|
37
|
+
case '--yes': out.yes = true; break;
|
|
38
|
+
case '--env': out.env = next; i++; break;
|
|
39
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (!out.slug) throw new Error('--slug required');
|
|
43
|
+
if (out.paid === undefined) throw new Error('--paid required (true|false)');
|
|
44
|
+
return out as SetPaidArgs;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runSetPaid(argv: string[]): Promise<void> {
|
|
48
|
+
const args = parseArgs(argv);
|
|
49
|
+
validateEnv(args.env);
|
|
50
|
+
|
|
51
|
+
await confirmIfProd(
|
|
52
|
+
args.env,
|
|
53
|
+
`Action: set isPaid=${args.paid} on plugin '${args.slug}' (${args.env.toUpperCase()})`,
|
|
54
|
+
args.yes,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
console.log(`\nš° Setting isPaid=${args.paid} on ${args.slug} (${args.env.toUpperCase()})...`);
|
|
58
|
+
const res = await callSkills(
|
|
59
|
+
args.env,
|
|
60
|
+
'PATCH',
|
|
61
|
+
`/api/admin/plugins/${encodeURIComponent(args.slug)}`,
|
|
62
|
+
{ isPaid: args.paid },
|
|
63
|
+
);
|
|
64
|
+
console.log(`ā Updated plugin (HTTP ${res.status}):`);
|
|
65
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
66
|
+
if (args.paid) {
|
|
67
|
+
console.log(`\nā¹ļø Reminder: ensure a billing Product + channel exists for '${args.slug}' (optima-product) or users will 402 with no purchase path. salesUrl is publish-time-only (currently shown above).`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { callSkills, validateEnv } from '../billing-http';
|
|
2
|
+
|
|
3
|
+
interface ShowArgs {
|
|
4
|
+
slug: string;
|
|
5
|
+
env: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function parseArgs(argv: string[]): ShowArgs {
|
|
9
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
10
|
+
console.log(`Usage: optima-plugin show --slug <slug> [options]
|
|
11
|
+
|
|
12
|
+
Required:
|
|
13
|
+
--slug <slug>
|
|
14
|
+
|
|
15
|
+
Optional:
|
|
16
|
+
--env stage|prod (default: stage)
|
|
17
|
+
|
|
18
|
+
Note: reads the public GET /api/plugins/:slug ā shows isPaid, salesUrl, and
|
|
19
|
+
descriptive fields, but NOT defaultForUser / status / trustLevel (public
|
|
20
|
+
endpoint omits them). Returns 404 for non-ACTIVE plugins.`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
const out: Partial<ShowArgs> = { env: 'stage' };
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
const next = argv[i + 1];
|
|
27
|
+
switch (a) {
|
|
28
|
+
case '--slug': out.slug = next; i++; break;
|
|
29
|
+
case '--env': out.env = next; i++; break;
|
|
30
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (!out.slug) throw new Error('--slug required');
|
|
34
|
+
return out as ShowArgs;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function runShow(argv: string[]): Promise<void> {
|
|
38
|
+
const args = parseArgs(argv);
|
|
39
|
+
validateEnv(args.env);
|
|
40
|
+
const res = await callSkills(args.env, 'GET', `/api/plugins/${encodeURIComponent(args.slug)}`);
|
|
41
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
42
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runShow } from './plugin/show';
|
|
4
|
+
import { runSetPaid } from './plugin/set-paid';
|
|
5
|
+
import { runSetDefault } from './plugin/set-default';
|
|
6
|
+
|
|
7
|
+
function printHelp() {
|
|
8
|
+
console.log(`Usage: optima-plugin <subcommand> [options]
|
|
9
|
+
|
|
10
|
+
Subcommands:
|
|
11
|
+
show Show a plugin's marketplace state (isPaid, salesUrl, ... ACTIVE plugins only)
|
|
12
|
+
set-paid Flip a plugin's isPaid flag (the user-facing paid/free gate)
|
|
13
|
+
set-default Flip a plugin's defaultForUser flag
|
|
14
|
+
|
|
15
|
+
Run 'optima-plugin <subcommand> --help' for subcommand-specific options.`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
const [, , subcommand, ...rest] = process.argv;
|
|
20
|
+
if (!subcommand || subcommand === '-h' || subcommand === '--help') { printHelp(); process.exit(0); }
|
|
21
|
+
switch (subcommand) {
|
|
22
|
+
case 'show': await runShow(rest); break;
|
|
23
|
+
case 'set-paid': await runSetPaid(rest); break;
|
|
24
|
+
case 'set-default': await runSetDefault(rest); break;
|
|
25
|
+
default:
|
|
26
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
27
|
+
printHelp();
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
main().catch((err) => { console.error(err.message); process.exit(1); });
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { callBilling, validateEnv } from '../billing-http';
|
|
2
|
+
|
|
3
|
+
interface AddChannelArgs {
|
|
4
|
+
key: string;
|
|
5
|
+
provider: string;
|
|
6
|
+
stripePriceId: string;
|
|
7
|
+
priceCents: number;
|
|
8
|
+
currency: string;
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
metadata?: Record<string, unknown>;
|
|
11
|
+
env: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseArgs(argv: string[]): AddChannelArgs {
|
|
15
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
16
|
+
console.log(`Usage: optima-product add-channel --key <productKey> --provider STRIPE --stripe-price-id <price_xxx> --price-cents N --currency USD [options]
|
|
17
|
+
|
|
18
|
+
Required:
|
|
19
|
+
--key <productKey>
|
|
20
|
+
--provider STRIPE v1 CLI accepts only STRIPE (schema supports more)
|
|
21
|
+
--stripe-price-id <price_xxx> Pre-created in Stripe Dashboard; wire-mapped to externalProductId
|
|
22
|
+
--price-cents N MUST be > 0; should match Stripe Price's unit_amount (NOT verified)
|
|
23
|
+
--currency USD Should match Stripe Price's currency (NOT verified)
|
|
24
|
+
|
|
25
|
+
Optional:
|
|
26
|
+
--enabled true|false default: true
|
|
27
|
+
--metadata '<json>'
|
|
28
|
+
--env stage|prod (default: stage)`);
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
const out: Partial<AddChannelArgs> = { env: 'stage' };
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
const a = argv[i];
|
|
34
|
+
const next = argv[i + 1];
|
|
35
|
+
switch (a) {
|
|
36
|
+
case '--key': out.key = next; i++; break;
|
|
37
|
+
case '--provider': out.provider = next; i++; break;
|
|
38
|
+
case '--stripe-price-id': out.stripePriceId = next; i++; break;
|
|
39
|
+
case '--price-cents': out.priceCents = parseInt(next, 10); i++; break;
|
|
40
|
+
case '--currency': out.currency = next; i++; break;
|
|
41
|
+
case '--enabled': out.enabled = next === 'true'; i++; break;
|
|
42
|
+
case '--metadata': out.metadata = JSON.parse(next); i++; break;
|
|
43
|
+
case '--env': out.env = next; i++; break;
|
|
44
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!out.key) throw new Error('--key required');
|
|
48
|
+
if (!out.provider) throw new Error('--provider required');
|
|
49
|
+
if (out.provider !== 'STRIPE') throw new Error(`v1 CLI accepts only --provider STRIPE (got ${out.provider})`);
|
|
50
|
+
if (!out.stripePriceId) throw new Error('--stripe-price-id required');
|
|
51
|
+
if (out.priceCents === undefined || !Number.isFinite(out.priceCents)) throw new Error('--price-cents required (integer)');
|
|
52
|
+
if (out.priceCents <= 0) throw new Error('--price-cents must be > 0');
|
|
53
|
+
if (!out.currency) throw new Error('--currency required');
|
|
54
|
+
return out as AddChannelArgs;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function runAddChannel(argv: string[]): Promise<void> {
|
|
58
|
+
const args = parseArgs(argv);
|
|
59
|
+
validateEnv(args.env);
|
|
60
|
+
const body: Record<string, unknown> = {
|
|
61
|
+
provider: args.provider,
|
|
62
|
+
externalProductId: args.stripePriceId,
|
|
63
|
+
priceCents: args.priceCents,
|
|
64
|
+
currency: args.currency,
|
|
65
|
+
};
|
|
66
|
+
if (args.enabled !== undefined) body.enabled = args.enabled;
|
|
67
|
+
if (args.metadata !== undefined) body.metadata = args.metadata;
|
|
68
|
+
|
|
69
|
+
console.log(`\nš³ Adding ${args.provider} channel to ${args.key} on ${args.env.toUpperCase()}...`);
|
|
70
|
+
const res = await callBilling(
|
|
71
|
+
args.env,
|
|
72
|
+
'POST',
|
|
73
|
+
`/api/billing/admin/products/${encodeURIComponent(args.key)}/channels`,
|
|
74
|
+
body,
|
|
75
|
+
);
|
|
76
|
+
console.log(`ā Created Channel (HTTP ${res.status}):`);
|
|
77
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
78
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { callBilling, validateEnv } from '../billing-http';
|
|
2
|
+
|
|
3
|
+
interface CreateArgs {
|
|
4
|
+
key: string;
|
|
5
|
+
plugins: string[];
|
|
6
|
+
type: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
refundWindowDays?: number;
|
|
10
|
+
refundProrateMaxDays?: number;
|
|
11
|
+
bundledPlanId?: string;
|
|
12
|
+
bundledDurationDays?: number;
|
|
13
|
+
revokeBundledOnRefund?: boolean;
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
15
|
+
env: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseArgs(argv: string[]): CreateArgs {
|
|
19
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
20
|
+
console.log(`Usage: optima-product create --key <productKey> --plugins <slug1,slug2,...> --type <ProductType> [options]
|
|
21
|
+
|
|
22
|
+
Required:
|
|
23
|
+
--key <productKey> Unique slug
|
|
24
|
+
--plugins <slug1,slug2,...> Comma-separated, >=1 plugin slug
|
|
25
|
+
--type <ProductType> Only ONE_SHOT_SKILL accepted by v1 CLI policy
|
|
26
|
+
|
|
27
|
+
Optional:
|
|
28
|
+
--name "..." Convenience flag, folded into metadata.name
|
|
29
|
+
--description "..." Convenience flag, folded into metadata.description
|
|
30
|
+
--refund-window-days N
|
|
31
|
+
--refund-prorate-max-days N
|
|
32
|
+
--bundled-plan-id <planId> (joint with --bundled-duration-days)
|
|
33
|
+
--bundled-duration-days N
|
|
34
|
+
--revoke-bundled-on-refund true|false
|
|
35
|
+
--metadata '<json>' JSON object stored in product.metadata
|
|
36
|
+
--env stage|prod (default: stage)`);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const out: Partial<CreateArgs> = { env: 'stage', revokeBundledOnRefund: undefined };
|
|
41
|
+
for (let i = 0; i < argv.length; i++) {
|
|
42
|
+
const a = argv[i];
|
|
43
|
+
const next = argv[i + 1];
|
|
44
|
+
switch (a) {
|
|
45
|
+
case '--key': out.key = next; i++; break;
|
|
46
|
+
case '--plugins': out.plugins = next.split(',').map((s) => s.trim()).filter(Boolean); i++; break;
|
|
47
|
+
case '--type': out.type = next; i++; break;
|
|
48
|
+
case '--name': out.name = next; i++; break;
|
|
49
|
+
case '--description': out.description = next; i++; break;
|
|
50
|
+
case '--refund-window-days': out.refundWindowDays = parseInt(next, 10); i++; break;
|
|
51
|
+
case '--refund-prorate-max-days': out.refundProrateMaxDays = parseInt(next, 10); i++; break;
|
|
52
|
+
case '--bundled-plan-id': out.bundledPlanId = next; i++; break;
|
|
53
|
+
case '--bundled-duration-days': out.bundledDurationDays = parseInt(next, 10); i++; break;
|
|
54
|
+
case '--revoke-bundled-on-refund': out.revokeBundledOnRefund = next === 'true'; i++; break;
|
|
55
|
+
case '--metadata': out.metadata = JSON.parse(next); i++; break;
|
|
56
|
+
case '--env': out.env = next; i++; break;
|
|
57
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!out.key) throw new Error('--key required');
|
|
62
|
+
if (!out.plugins || out.plugins.length === 0) throw new Error('--plugins required (>=1 slug)');
|
|
63
|
+
if (!out.type) throw new Error('--type required');
|
|
64
|
+
if (out.type !== 'ONE_SHOT_SKILL') {
|
|
65
|
+
throw new Error(`v1 CLI accepts only --type ONE_SHOT_SKILL (got ${out.type}). See spec §5.1.`);
|
|
66
|
+
}
|
|
67
|
+
if ((out.bundledPlanId == null) !== (out.bundledDurationDays == null)) {
|
|
68
|
+
throw new Error('--bundled-plan-id and --bundled-duration-days must be set together');
|
|
69
|
+
}
|
|
70
|
+
return out as CreateArgs;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function runCreate(argv: string[]): Promise<void> {
|
|
74
|
+
const args = parseArgs(argv);
|
|
75
|
+
validateEnv(args.env);
|
|
76
|
+
|
|
77
|
+
const body: Record<string, unknown> = {
|
|
78
|
+
productKey: args.key,
|
|
79
|
+
type: args.type,
|
|
80
|
+
pluginSlugs: args.plugins,
|
|
81
|
+
};
|
|
82
|
+
if (args.name !== undefined) body.name = args.name;
|
|
83
|
+
if (args.description !== undefined) body.description = args.description;
|
|
84
|
+
if (args.refundWindowDays !== undefined) body.refundWindowDays = args.refundWindowDays;
|
|
85
|
+
if (args.refundProrateMaxDays !== undefined) body.refundProrateMaxDays = args.refundProrateMaxDays;
|
|
86
|
+
if (args.bundledPlanId !== undefined) body.bundledPlanId = args.bundledPlanId;
|
|
87
|
+
if (args.bundledDurationDays !== undefined) body.bundledDurationDays = args.bundledDurationDays;
|
|
88
|
+
if (args.revokeBundledOnRefund !== undefined) body.revokeBundledOnRefund = args.revokeBundledOnRefund;
|
|
89
|
+
if (args.metadata !== undefined) body.metadata = args.metadata;
|
|
90
|
+
|
|
91
|
+
console.log(`\nš Creating product ${args.key} (${args.plugins.length} plugin(s)) on ${args.env.toUpperCase()}...`);
|
|
92
|
+
|
|
93
|
+
const res = await callBilling(args.env, 'POST', '/api/billing/admin/products', body);
|
|
94
|
+
console.log(`ā Created Product (HTTP ${res.status}):`);
|
|
95
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
96
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { callBilling, validateEnv } from '../billing-http';
|
|
2
|
+
|
|
3
|
+
interface ShowArgs {
|
|
4
|
+
key: string;
|
|
5
|
+
env: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function parseArgs(argv: string[]): ShowArgs {
|
|
9
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
10
|
+
console.log(`Usage: optima-product show --key <productKey> [options]
|
|
11
|
+
|
|
12
|
+
Required:
|
|
13
|
+
--key <productKey>
|
|
14
|
+
|
|
15
|
+
Optional:
|
|
16
|
+
--env stage|prod (default: stage)
|
|
17
|
+
|
|
18
|
+
Note: returns the bare Product row only ā productPlugins and channels arrays
|
|
19
|
+
are NOT included (billing's GET /api/internal/products/:key is a plain
|
|
20
|
+
findUnique with no include). To inspect channels/plugins today, query the DB
|
|
21
|
+
directly via optima-query-db. Tracked as follow-up in spec §7.`);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
const out: Partial<ShowArgs> = { env: 'stage' };
|
|
25
|
+
for (let i = 0; i < argv.length; i++) {
|
|
26
|
+
const a = argv[i];
|
|
27
|
+
const next = argv[i + 1];
|
|
28
|
+
switch (a) {
|
|
29
|
+
case '--key': out.key = next; i++; break;
|
|
30
|
+
case '--env': out.env = next; i++; break;
|
|
31
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!out.key) throw new Error('--key required');
|
|
35
|
+
return out as ShowArgs;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function runShow(argv: string[]): Promise<void> {
|
|
39
|
+
const args = parseArgs(argv);
|
|
40
|
+
validateEnv(args.env);
|
|
41
|
+
const res = await callBilling(args.env, 'GET', `/api/internal/products/${encodeURIComponent(args.key)}`);
|
|
42
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
43
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { callBilling, validateEnv } from '../billing-http';
|
|
2
|
+
|
|
3
|
+
interface ToggleArgs {
|
|
4
|
+
key: string;
|
|
5
|
+
provider: string;
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
env: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function parseArgs(argv: string[]): ToggleArgs {
|
|
11
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
12
|
+
console.log(`Usage: optima-product toggle-channel --key <productKey> --provider STRIPE --enabled true|false [options]
|
|
13
|
+
|
|
14
|
+
Required:
|
|
15
|
+
--key <productKey>
|
|
16
|
+
--provider STRIPE v1 CLI accepts only STRIPE
|
|
17
|
+
--enabled true|false
|
|
18
|
+
|
|
19
|
+
Optional:
|
|
20
|
+
--env stage|prod (default: stage)`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
const out: Partial<ToggleArgs> = { env: 'stage' };
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
const next = argv[i + 1];
|
|
27
|
+
switch (a) {
|
|
28
|
+
case '--key': out.key = next; i++; break;
|
|
29
|
+
case '--provider': out.provider = next; i++; break;
|
|
30
|
+
case '--enabled':
|
|
31
|
+
if (next !== 'true' && next !== 'false') throw new Error('--enabled must be true or false');
|
|
32
|
+
out.enabled = next === 'true'; i++; break;
|
|
33
|
+
case '--env': out.env = next; i++; break;
|
|
34
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!out.key) throw new Error('--key required');
|
|
38
|
+
if (!out.provider) throw new Error('--provider required');
|
|
39
|
+
if (out.provider !== 'STRIPE') throw new Error(`v1 CLI accepts only --provider STRIPE (got ${out.provider})`);
|
|
40
|
+
if (out.enabled === undefined) throw new Error('--enabled required');
|
|
41
|
+
return out as ToggleArgs;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function runToggleChannel(argv: string[]): Promise<void> {
|
|
45
|
+
const args = parseArgs(argv);
|
|
46
|
+
validateEnv(args.env);
|
|
47
|
+
console.log(`\nš Setting ${args.provider} channel enabled=${args.enabled} on ${args.key} (${args.env.toUpperCase()})...`);
|
|
48
|
+
const res = await callBilling(
|
|
49
|
+
args.env,
|
|
50
|
+
'PATCH',
|
|
51
|
+
`/api/billing/admin/products/${encodeURIComponent(args.key)}/channels/${args.provider}`,
|
|
52
|
+
{ enabled: args.enabled },
|
|
53
|
+
);
|
|
54
|
+
console.log(`ā Channel updated (HTTP ${res.status}):`);
|
|
55
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
56
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { callBilling, validateEnv } from '../billing-http';
|
|
2
|
+
|
|
3
|
+
interface UpdateArgs {
|
|
4
|
+
key: string;
|
|
5
|
+
refundWindowDays?: number | null;
|
|
6
|
+
refundProrateMaxDays?: number | null;
|
|
7
|
+
bundledPlanId?: string | null;
|
|
8
|
+
bundledDurationDays?: number | null;
|
|
9
|
+
revokeBundledOnRefund?: boolean;
|
|
10
|
+
metadata?: Record<string, unknown>;
|
|
11
|
+
env: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseArgs(argv: string[]): UpdateArgs {
|
|
15
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
16
|
+
console.log(`Usage: optima-product update --key <productKey> [options]
|
|
17
|
+
|
|
18
|
+
Required:
|
|
19
|
+
--key <productKey>
|
|
20
|
+
|
|
21
|
+
Optional (PATCH ā only included fields are updated):
|
|
22
|
+
--refund-window-days N | null
|
|
23
|
+
--refund-prorate-max-days N | null
|
|
24
|
+
--bundled-plan-id <planId> | null (joint with --bundled-duration-days)
|
|
25
|
+
--bundled-duration-days N | null
|
|
26
|
+
--revoke-bundled-on-refund true|false
|
|
27
|
+
--metadata '<json>' FULL REPLACE ā Prisma does not deep-merge JSON
|
|
28
|
+
--env stage|prod (default: stage)
|
|
29
|
+
|
|
30
|
+
Note: productKey, type, and pluginSlugs are immutable post-create. To change plugin
|
|
31
|
+
membership, create a new Product with a new key.`);
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
const out: Partial<UpdateArgs> = { env: 'stage' };
|
|
35
|
+
for (let i = 0; i < argv.length; i++) {
|
|
36
|
+
const a = argv[i];
|
|
37
|
+
const next = argv[i + 1];
|
|
38
|
+
const parseNullable = (v: string): number | null => v === 'null' ? null : parseInt(v, 10);
|
|
39
|
+
switch (a) {
|
|
40
|
+
case '--key': out.key = next; i++; break;
|
|
41
|
+
case '--refund-window-days': out.refundWindowDays = parseNullable(next); i++; break;
|
|
42
|
+
case '--refund-prorate-max-days': out.refundProrateMaxDays = parseNullable(next); i++; break;
|
|
43
|
+
case '--bundled-plan-id': out.bundledPlanId = next === 'null' ? null : next; i++; break;
|
|
44
|
+
case '--bundled-duration-days': out.bundledDurationDays = parseNullable(next); i++; break;
|
|
45
|
+
case '--revoke-bundled-on-refund': out.revokeBundledOnRefund = next === 'true'; i++; break;
|
|
46
|
+
case '--metadata': out.metadata = JSON.parse(next); i++; break;
|
|
47
|
+
case '--env': out.env = next; i++; break;
|
|
48
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!out.key) throw new Error('--key required');
|
|
52
|
+
return out as UpdateArgs;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function runUpdate(argv: string[]): Promise<void> {
|
|
56
|
+
const args = parseArgs(argv);
|
|
57
|
+
validateEnv(args.env);
|
|
58
|
+
const body: Record<string, unknown> = {};
|
|
59
|
+
if (args.refundWindowDays !== undefined) body.refundWindowDays = args.refundWindowDays;
|
|
60
|
+
if (args.refundProrateMaxDays !== undefined) body.refundProrateMaxDays = args.refundProrateMaxDays;
|
|
61
|
+
if (args.bundledPlanId !== undefined) body.bundledPlanId = args.bundledPlanId;
|
|
62
|
+
if (args.bundledDurationDays !== undefined) body.bundledDurationDays = args.bundledDurationDays;
|
|
63
|
+
if (args.revokeBundledOnRefund !== undefined) body.revokeBundledOnRefund = args.revokeBundledOnRefund;
|
|
64
|
+
if (args.metadata !== undefined) body.metadata = args.metadata;
|
|
65
|
+
|
|
66
|
+
if (Object.keys(body).length === 0) throw new Error('At least one updatable field must be passed');
|
|
67
|
+
|
|
68
|
+
console.log(`\nāļø Updating product ${args.key} on ${args.env.toUpperCase()}...`);
|
|
69
|
+
const res = await callBilling(args.env, 'PATCH', `/api/billing/admin/products/${encodeURIComponent(args.key)}`, body);
|
|
70
|
+
console.log(`ā Updated Product (HTTP ${res.status}):`);
|
|
71
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
72
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runCreate } from './product/create';
|
|
4
|
+
import { runUpdate } from './product/update';
|
|
5
|
+
import { runAddChannel } from './product/add-channel';
|
|
6
|
+
import { runToggleChannel } from './product/toggle-channel';
|
|
7
|
+
import { runShow } from './product/show';
|
|
8
|
+
|
|
9
|
+
const SUBCOMMANDS = ['create', 'update', 'add-channel', 'toggle-channel', 'show'] as const;
|
|
10
|
+
|
|
11
|
+
function printHelp() {
|
|
12
|
+
console.log(`Usage: optima-product <subcommand> [options]
|
|
13
|
+
|
|
14
|
+
Subcommands:
|
|
15
|
+
create Create a Product bundling 1+ plugin slugs
|
|
16
|
+
update Patch refund policy / metadata on an existing Product
|
|
17
|
+
add-channel Attach a payment channel (Stripe Price ID) to a Product
|
|
18
|
+
toggle-channel Enable/disable an existing channel
|
|
19
|
+
show Show a Product's bare row (note: does NOT include plugins/channels)
|
|
20
|
+
|
|
21
|
+
Run 'optima-product <subcommand> --help' for subcommand-specific options.`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function main() {
|
|
25
|
+
const [, , subcommand, ...rest] = process.argv;
|
|
26
|
+
if (!subcommand || subcommand === '-h' || subcommand === '--help') {
|
|
27
|
+
printHelp();
|
|
28
|
+
process.exit(0);
|
|
29
|
+
}
|
|
30
|
+
switch (subcommand) {
|
|
31
|
+
case 'create': await runCreate(rest); break;
|
|
32
|
+
case 'update': await runUpdate(rest); break;
|
|
33
|
+
case 'add-channel': await runAddChannel(rest); break;
|
|
34
|
+
case 'toggle-channel': await runToggleChannel(rest); break;
|
|
35
|
+
case 'show': await runShow(rest); break;
|
|
36
|
+
default:
|
|
37
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
38
|
+
printHelp();
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main().catch((err) => {
|
|
44
|
+
console.error(err.message);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|