@optima-chat/dev-skills 0.7.32 โ 0.7.35
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 +2 -0
- package/bin/helpers/billing-http.ts +162 -0
- package/bin/helpers/confirm-prompt.ts +23 -0
- package/bin/helpers/db-utils.ts +38 -2
- 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/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 +139 -0
- package/dist/bin/helpers/confirm-prompt.js +54 -0
- package/dist/bin/helpers/db-utils.js +36 -3
- 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/generate-test-token.js +0 -0
- package/dist/bin/helpers/grant-balance.js +0 -0
- package/dist/bin/helpers/grant-credits.js +71 -0
- package/dist/bin/helpers/grant-subscription.js +0 -0
- package/dist/bin/helpers/infisical-secrets.js +33 -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/dist/bin/helpers/query-db.js +0 -0
- package/dist/bin/helpers/show-env.js +0 -0
- package/docs/superpowers/plans/2026-05-24-marketplace-admin-cli-impl.md +1973 -0
- package/docs/superpowers/specs/2026-05-24-marketplace-admin-cli-design.md +324 -0
- package/package.json +7 -5
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runList = runList;
|
|
4
|
+
const billing_http_1 = require("../billing-http");
|
|
5
|
+
const db_utils_1 = require("../db-utils");
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
8
|
+
console.log(`Usage: optima-entitlement list --email <user-email> [options]
|
|
9
|
+
|
|
10
|
+
Required:
|
|
11
|
+
--email <user-email> Resolved to userId via user-auth DB
|
|
12
|
+
|
|
13
|
+
Optional:
|
|
14
|
+
--env stage|prod (default: stage)`);
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
const out = { env: 'stage' };
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
const a = argv[i];
|
|
20
|
+
const next = argv[i + 1];
|
|
21
|
+
switch (a) {
|
|
22
|
+
case '--email':
|
|
23
|
+
out.email = next;
|
|
24
|
+
i++;
|
|
25
|
+
break;
|
|
26
|
+
case '--env':
|
|
27
|
+
out.env = next;
|
|
28
|
+
i++;
|
|
29
|
+
break;
|
|
30
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (!out.email)
|
|
34
|
+
throw new Error('--email required');
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
async function runList(argv) {
|
|
38
|
+
const args = parseArgs(argv);
|
|
39
|
+
(0, billing_http_1.validateEnv)(args.env);
|
|
40
|
+
const cfg = (0, db_utils_1.getInfisicalConfig)();
|
|
41
|
+
const token = (0, db_utils_1.getInfisicalToken)(cfg);
|
|
42
|
+
const userId = await (0, db_utils_1.resolveUserId)(args.email, args.env, cfg, token);
|
|
43
|
+
const res = await (0, billing_http_1.callBilling)(args.env, 'GET', `/api/billing/admin/entitlements?userId=${encodeURIComponent(userId)}`);
|
|
44
|
+
const rows = res.body.entitlements ?? [];
|
|
45
|
+
if (rows.length === 0) {
|
|
46
|
+
console.log(`(no entitlements for ${args.email} on ${args.env})`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Newest first per spec
|
|
50
|
+
rows.sort((a, b) => b.purchasedAt.localeCompare(a.purchasedAt));
|
|
51
|
+
console.log(`${rows.length} entitlement(s) for ${args.email}:\n`);
|
|
52
|
+
console.log('id'.padEnd(38) + ' | ' + 'productKey'.padEnd(32) + ' | ' + 'status'.padEnd(9) + ' | ' + 'source'.padEnd(12) + ' | purchasedAt | refundedAt');
|
|
53
|
+
console.log('-'.repeat(140));
|
|
54
|
+
for (const r of rows) {
|
|
55
|
+
console.log(r.id.padEnd(38) + ' | ' +
|
|
56
|
+
r.productKey.padEnd(32) + ' | ' +
|
|
57
|
+
r.status.padEnd(9) + ' | ' +
|
|
58
|
+
r.source.padEnd(12) + ' | ' +
|
|
59
|
+
r.purchasedAt.padEnd(24) + ' | ' +
|
|
60
|
+
(r.refundedAt ?? ''));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runRevoke = runRevoke;
|
|
4
|
+
const billing_http_1 = require("../billing-http");
|
|
5
|
+
const confirm_prompt_1 = require("../confirm-prompt");
|
|
6
|
+
const db_utils_1 = require("../db-utils");
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
9
|
+
console.log(`Usage: optima-entitlement revoke --email <user> --product-key <slug> --reason "..." [options]
|
|
10
|
+
|
|
11
|
+
Required:
|
|
12
|
+
--email <user-email> Resolved to userId via user-auth DB
|
|
13
|
+
--product-key <productKey>
|
|
14
|
+
--reason "..." Required by billing (400 otherwise); stored on entitlement.refundReason
|
|
15
|
+
|
|
16
|
+
Optional:
|
|
17
|
+
--yes Skip prod confirmation prompt (no-op on stage)
|
|
18
|
+
--env stage|prod (default: stage)
|
|
19
|
+
|
|
20
|
+
Refuses non-ADMIN_GRANT sources (PAYMENT, PARTNER) with source-specific
|
|
21
|
+
error pointing to the right reversal flow.`);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
const out = { env: 'stage', yes: false };
|
|
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 '--email':
|
|
30
|
+
out.email = next;
|
|
31
|
+
i++;
|
|
32
|
+
break;
|
|
33
|
+
case '--product-key':
|
|
34
|
+
out.productKey = next;
|
|
35
|
+
i++;
|
|
36
|
+
break;
|
|
37
|
+
case '--reason':
|
|
38
|
+
out.reason = next;
|
|
39
|
+
i++;
|
|
40
|
+
break;
|
|
41
|
+
case '--yes':
|
|
42
|
+
out.yes = true;
|
|
43
|
+
break;
|
|
44
|
+
case '--env':
|
|
45
|
+
out.env = next;
|
|
46
|
+
i++;
|
|
47
|
+
break;
|
|
48
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!out.email)
|
|
52
|
+
throw new Error('--email required');
|
|
53
|
+
if (!out.productKey)
|
|
54
|
+
throw new Error('--product-key required');
|
|
55
|
+
if (!out.reason)
|
|
56
|
+
throw new Error('--reason required (billing returns 400 otherwise)');
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
const PAYMENT_REFUSAL = `refusing to revoke a PAYMENT-source entitlement via CLI; this would leave the customer charged but unentitled. Use the Stripe refund flow which calls Stripe refund API + records refundedAmountCents + emits webhook. Manual psql is the escape hatch if absolutely necessary.`;
|
|
60
|
+
const PARTNER_REFUSAL = `refusing to revoke a PARTNER-source entitlement via CLI; PARTNER grants are issued out-of-band and must be reversed via the partner contract / process that issued them. Manual psql is the escape hatch if absolutely necessary.`;
|
|
61
|
+
async function runRevoke(argv) {
|
|
62
|
+
const args = parseArgs(argv);
|
|
63
|
+
(0, billing_http_1.validateEnv)(args.env);
|
|
64
|
+
const cfg = (0, db_utils_1.getInfisicalConfig)();
|
|
65
|
+
const token = (0, db_utils_1.getInfisicalToken)(cfg);
|
|
66
|
+
const userId = await (0, db_utils_1.resolveUserId)(args.email, args.env, cfg, token);
|
|
67
|
+
// Step 1: Fetch user's entitlements
|
|
68
|
+
const listRes = await (0, billing_http_1.callBilling)(args.env, 'GET', `/api/billing/admin/entitlements?userId=${encodeURIComponent(userId)}`);
|
|
69
|
+
const all = listRes.body.entitlements ?? [];
|
|
70
|
+
// Step 2: Filter for ACTIVE + matching productKey
|
|
71
|
+
const matches = all.filter((e) => e.status === 'ACTIVE' && e.productKey === args.productKey);
|
|
72
|
+
// Step 3: Validate count (partial unique index enforces โค1 ACTIVE)
|
|
73
|
+
if (matches.length === 0) {
|
|
74
|
+
throw new Error(`no active entitlement for (user=${args.email}, product=${args.productKey}) on ${args.env}`);
|
|
75
|
+
}
|
|
76
|
+
if (matches.length > 1) {
|
|
77
|
+
throw new Error(`unexpected: ${matches.length} ACTIVE entitlements for (user, product) โ partial unique constraint should prevent this. Inspect with: optima-entitlement list --email ${args.email}`);
|
|
78
|
+
}
|
|
79
|
+
const target = matches[0];
|
|
80
|
+
// Step 4: Validate source
|
|
81
|
+
if (target.source === 'PAYMENT')
|
|
82
|
+
throw new Error(PAYMENT_REFUSAL);
|
|
83
|
+
if (target.source === 'PARTNER')
|
|
84
|
+
throw new Error(PARTNER_REFUSAL);
|
|
85
|
+
if (target.source !== 'ADMIN_GRANT')
|
|
86
|
+
throw new Error(`unknown entitlement source: ${target.source}`);
|
|
87
|
+
await (0, confirm_prompt_1.confirmIfProd)(args.env, `Action: REVOKE entitlement ${target.id} (productKey=${target.productKey}) for user ${args.email} (userId=${userId}) on ${args.env.toUpperCase()}\nReason: ${args.reason}`, args.yes);
|
|
88
|
+
// Step 5: Refund
|
|
89
|
+
// refundAmountCents=0 is always correct for ADMIN_GRANT (priceCents=0,
|
|
90
|
+
// no upstream charge). Pass explicitly so billing's auto-compute
|
|
91
|
+
// (which requires refundWindowDays on the Product) doesn't 400 with
|
|
92
|
+
// MANUAL_REFUND_AMOUNT_REQUIRED for products that lack a refund policy.
|
|
93
|
+
// PAYMENT/PARTNER sources are already refused in step 4 above, so
|
|
94
|
+
// this branch only ever runs for ADMIN_GRANT (priceCents=0) โ Stripe
|
|
95
|
+
// refund path in billing (admin-products.ts:296-307) is gated on
|
|
96
|
+
// source=PAYMENT and won't trigger here.
|
|
97
|
+
console.log(`\nโป๏ธ Revoking entitlement ${target.id} for ${args.email}...`);
|
|
98
|
+
const res = await (0, billing_http_1.callBilling)(args.env, 'POST', '/api/billing/admin/refund-entitlement', {
|
|
99
|
+
entitlementId: target.id,
|
|
100
|
+
refundReason: args.reason,
|
|
101
|
+
refundAmountCents: 0,
|
|
102
|
+
});
|
|
103
|
+
console.log(`โ Revoked entitlement (HTTP ${res.status}):`);
|
|
104
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
105
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const grant_1 = require("./entitlement/grant");
|
|
5
|
+
const list_1 = require("./entitlement/list");
|
|
6
|
+
const revoke_1 = require("./entitlement/revoke");
|
|
7
|
+
function printHelp() {
|
|
8
|
+
console.log(`Usage: optima-entitlement <subcommand> [options]
|
|
9
|
+
|
|
10
|
+
Subcommands:
|
|
11
|
+
grant Admin-grant a product entitlement to a user
|
|
12
|
+
revoke Revoke an admin-granted entitlement (refuses PAYMENT / PARTNER sources)
|
|
13
|
+
list List a user's entitlements, newest first
|
|
14
|
+
|
|
15
|
+
Run 'optima-entitlement <subcommand> --help' for subcommand-specific options.`);
|
|
16
|
+
}
|
|
17
|
+
async function main() {
|
|
18
|
+
const [, , subcommand, ...rest] = process.argv;
|
|
19
|
+
if (!subcommand || subcommand === '-h' || subcommand === '--help') {
|
|
20
|
+
printHelp();
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
switch (subcommand) {
|
|
24
|
+
case 'list':
|
|
25
|
+
await (0, list_1.runList)(rest);
|
|
26
|
+
break;
|
|
27
|
+
case 'grant':
|
|
28
|
+
await (0, grant_1.runGrant)(rest);
|
|
29
|
+
break;
|
|
30
|
+
case 'revoke':
|
|
31
|
+
await (0, revoke_1.runRevoke)(rest);
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
35
|
+
printHelp();
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
main().catch((err) => { console.error(err.message); process.exit(1); });
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const db_utils_1 = require("./db-utils");
|
|
5
|
+
function parseArgs(args) {
|
|
6
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
7
|
+
console.log(`Usage: optima-grant-credits <email> --amount <n> [options]
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
--amount <n> Credits to grant (required)
|
|
11
|
+
--type <type> Credit type: bonus, referral (default: bonus)
|
|
12
|
+
--description <text> Description (optional)
|
|
13
|
+
--env <env> Environment: stage, prod (default: stage)
|
|
14
|
+
-h, --help Show this help`);
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
const email = args[0];
|
|
18
|
+
let amount = 0;
|
|
19
|
+
let type = 'bonus';
|
|
20
|
+
let description = null;
|
|
21
|
+
let env = 'stage';
|
|
22
|
+
for (let i = 1; i < args.length; i++) {
|
|
23
|
+
if (args[i] === '--amount' && args[i + 1]) {
|
|
24
|
+
amount = parseInt(args[++i], 10);
|
|
25
|
+
}
|
|
26
|
+
else if (args[i] === '--type' && args[i + 1]) {
|
|
27
|
+
type = args[++i];
|
|
28
|
+
}
|
|
29
|
+
else if (args[i] === '--description' && args[i + 1]) {
|
|
30
|
+
description = args[++i];
|
|
31
|
+
}
|
|
32
|
+
else if (args[i] === '--env' && args[i + 1]) {
|
|
33
|
+
env = args[++i];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (amount < 1) {
|
|
37
|
+
console.error('--amount is required and must be >= 1');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
if (!['bonus', 'referral'].includes(type)) {
|
|
41
|
+
console.error(`Unknown type: ${type}. Available: bonus, referral`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (!['stage', 'prod'].includes(env)) {
|
|
45
|
+
console.error('Env must be stage or prod (billing DB not available in CI)');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
return { email, amount, type, description, env };
|
|
49
|
+
}
|
|
50
|
+
async function main() {
|
|
51
|
+
const { email, amount, type, description, env } = parseArgs(process.argv.slice(2));
|
|
52
|
+
const infisicalConfig = (0, db_utils_1.getInfisicalConfig)();
|
|
53
|
+
const token = (0, db_utils_1.getInfisicalToken)(infisicalConfig);
|
|
54
|
+
console.log(`\n๐ Granting ${amount} ${type} credits to ${email} [${env.toUpperCase()}]\n`);
|
|
55
|
+
const userId = await (0, db_utils_1.resolveUserId)(email, env, infisicalConfig, token);
|
|
56
|
+
const billing = await (0, db_utils_1.connectBillingDB)(env, infisicalConfig, token);
|
|
57
|
+
const bq = billing.query;
|
|
58
|
+
const now = new Date().toISOString();
|
|
59
|
+
const safeUserId = (0, db_utils_1.escapeSQL)(userId);
|
|
60
|
+
const safeType = (0, db_utils_1.escapeSQL)(type);
|
|
61
|
+
const safeDesc = (0, db_utils_1.escapeSQL)(description || `Admin ${type} credit grant`);
|
|
62
|
+
console.log(`Inserting ${amount} ${type} credits...`);
|
|
63
|
+
const ledgerId = bq(`INSERT INTO credit_ledger (id, user_id, type, description, initial_amount, remaining, created_at) VALUES (concat('crd_${safeType}_', substr(md5(random()::text), 1, 16)), '${safeUserId}', '${safeType}', '${safeDesc}', ${amount}, ${amount}, '${now}') RETURNING id`);
|
|
64
|
+
console.log(`โ Credits granted (ledger ID: ${ledgerId})`);
|
|
65
|
+
const balance = bq(`SELECT COALESCE(SUM(remaining), 0) FROM credit_ledger WHERE user_id='${safeUserId}' AND remaining > 0 AND (expires_at IS NULL OR expires_at > NOW())`);
|
|
66
|
+
console.log(`\nโ
Done! ${email} now has ${balance} total credits\n`);
|
|
67
|
+
}
|
|
68
|
+
main().catch(error => {
|
|
69
|
+
console.error('\nโ Error:', error.message);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchInfisicalSecret = fetchInfisicalSecret;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const db_utils_1 = require("./db-utils");
|
|
6
|
+
/**
|
|
7
|
+
* Fetch a single secret value from Infisical given env + path + name.
|
|
8
|
+
*
|
|
9
|
+
* env mapping: 'stage' โ Infisical env slug 'staging'; 'prod' โ 'prod'
|
|
10
|
+
* (matches dev-skills convention documented at
|
|
11
|
+
* ~/.claude/projects/-mnt-d-work-projects-optima/memory/optima_infisical_env_naming.md).
|
|
12
|
+
*
|
|
13
|
+
* Returns the raw secretValue string. Throws if the secret is missing.
|
|
14
|
+
*/
|
|
15
|
+
function fetchInfisicalSecret(env, secretPath, secretName, config, token) {
|
|
16
|
+
const cfg = config ?? (0, db_utils_1.getInfisicalConfig)();
|
|
17
|
+
const tok = token ?? (0, db_utils_1.getInfisicalToken)(cfg);
|
|
18
|
+
const envSlug = env === 'stage' ? 'staging' : env;
|
|
19
|
+
const encodedPath = encodeURIComponent(secretPath);
|
|
20
|
+
const encodedName = encodeURIComponent(secretName);
|
|
21
|
+
const response = (0, child_process_1.execSync)(`curl -s "${cfg.url}/api/v3/secrets/raw/${encodedName}?workspaceId=${cfg.projectId}&environment=${envSlug}&secretPath=${encodedPath}" -H "Authorization: Bearer ${tok}"`, { encoding: 'utf-8' });
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = JSON.parse(response);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
throw new Error(`Infisical raw secret fetch returned non-JSON for ${secretPath}/${secretName} (${envSlug}): ${response.slice(0, 200)}`);
|
|
28
|
+
}
|
|
29
|
+
if (!parsed.secret?.secretValue) {
|
|
30
|
+
throw new Error(`Infisical secret not found: env=${envSlug} path=${secretPath} name=${secretName} (response: ${response.slice(0, 200)})`);
|
|
31
|
+
}
|
|
32
|
+
return parsed.secret.secretValue;
|
|
33
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runAddChannel = runAddChannel;
|
|
4
|
+
const billing_http_1 = require("../billing-http");
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
7
|
+
console.log(`Usage: optima-product add-channel --key <productKey> --provider STRIPE --stripe-price-id <price_xxx> --price-cents N --currency USD [options]
|
|
8
|
+
|
|
9
|
+
Required:
|
|
10
|
+
--key <productKey>
|
|
11
|
+
--provider STRIPE v1 CLI accepts only STRIPE (schema supports more)
|
|
12
|
+
--stripe-price-id <price_xxx> Pre-created in Stripe Dashboard; wire-mapped to externalProductId
|
|
13
|
+
--price-cents N MUST be > 0; should match Stripe Price's unit_amount (NOT verified)
|
|
14
|
+
--currency USD Should match Stripe Price's currency (NOT verified)
|
|
15
|
+
|
|
16
|
+
Optional:
|
|
17
|
+
--enabled true|false default: true
|
|
18
|
+
--metadata '<json>'
|
|
19
|
+
--env stage|prod (default: stage)`);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
const out = { env: 'stage' };
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
const a = argv[i];
|
|
25
|
+
const next = argv[i + 1];
|
|
26
|
+
switch (a) {
|
|
27
|
+
case '--key':
|
|
28
|
+
out.key = next;
|
|
29
|
+
i++;
|
|
30
|
+
break;
|
|
31
|
+
case '--provider':
|
|
32
|
+
out.provider = next;
|
|
33
|
+
i++;
|
|
34
|
+
break;
|
|
35
|
+
case '--stripe-price-id':
|
|
36
|
+
out.stripePriceId = next;
|
|
37
|
+
i++;
|
|
38
|
+
break;
|
|
39
|
+
case '--price-cents':
|
|
40
|
+
out.priceCents = parseInt(next, 10);
|
|
41
|
+
i++;
|
|
42
|
+
break;
|
|
43
|
+
case '--currency':
|
|
44
|
+
out.currency = next;
|
|
45
|
+
i++;
|
|
46
|
+
break;
|
|
47
|
+
case '--enabled':
|
|
48
|
+
out.enabled = next === 'true';
|
|
49
|
+
i++;
|
|
50
|
+
break;
|
|
51
|
+
case '--metadata':
|
|
52
|
+
out.metadata = JSON.parse(next);
|
|
53
|
+
i++;
|
|
54
|
+
break;
|
|
55
|
+
case '--env':
|
|
56
|
+
out.env = next;
|
|
57
|
+
i++;
|
|
58
|
+
break;
|
|
59
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!out.key)
|
|
63
|
+
throw new Error('--key required');
|
|
64
|
+
if (!out.provider)
|
|
65
|
+
throw new Error('--provider required');
|
|
66
|
+
if (out.provider !== 'STRIPE')
|
|
67
|
+
throw new Error(`v1 CLI accepts only --provider STRIPE (got ${out.provider})`);
|
|
68
|
+
if (!out.stripePriceId)
|
|
69
|
+
throw new Error('--stripe-price-id required');
|
|
70
|
+
if (out.priceCents === undefined || !Number.isFinite(out.priceCents))
|
|
71
|
+
throw new Error('--price-cents required (integer)');
|
|
72
|
+
if (out.priceCents <= 0)
|
|
73
|
+
throw new Error('--price-cents must be > 0');
|
|
74
|
+
if (!out.currency)
|
|
75
|
+
throw new Error('--currency required');
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
async function runAddChannel(argv) {
|
|
79
|
+
const args = parseArgs(argv);
|
|
80
|
+
(0, billing_http_1.validateEnv)(args.env);
|
|
81
|
+
const body = {
|
|
82
|
+
provider: args.provider,
|
|
83
|
+
externalProductId: args.stripePriceId,
|
|
84
|
+
priceCents: args.priceCents,
|
|
85
|
+
currency: args.currency,
|
|
86
|
+
};
|
|
87
|
+
if (args.enabled !== undefined)
|
|
88
|
+
body.enabled = args.enabled;
|
|
89
|
+
if (args.metadata !== undefined)
|
|
90
|
+
body.metadata = args.metadata;
|
|
91
|
+
console.log(`\n๐ณ Adding ${args.provider} channel to ${args.key} on ${args.env.toUpperCase()}...`);
|
|
92
|
+
const res = await (0, billing_http_1.callBilling)(args.env, 'POST', `/api/billing/admin/products/${encodeURIComponent(args.key)}/channels`, body);
|
|
93
|
+
console.log(`โ Created Channel (HTTP ${res.status}):`);
|
|
94
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
95
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runCreate = runCreate;
|
|
4
|
+
const billing_http_1 = require("../billing-http");
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
7
|
+
console.log(`Usage: optima-product create --key <productKey> --plugins <slug1,slug2,...> --type <ProductType> [options]
|
|
8
|
+
|
|
9
|
+
Required:
|
|
10
|
+
--key <productKey> Unique slug
|
|
11
|
+
--plugins <slug1,slug2,...> Comma-separated, >=1 plugin slug
|
|
12
|
+
--type <ProductType> Only ONE_SHOT_SKILL accepted by v1 CLI policy
|
|
13
|
+
|
|
14
|
+
Optional:
|
|
15
|
+
--name "..." Convenience flag, folded into metadata.name
|
|
16
|
+
--description "..." Convenience flag, folded into metadata.description
|
|
17
|
+
--refund-window-days N
|
|
18
|
+
--refund-prorate-max-days N
|
|
19
|
+
--bundled-plan-id <planId> (joint with --bundled-duration-days)
|
|
20
|
+
--bundled-duration-days N
|
|
21
|
+
--revoke-bundled-on-refund true|false
|
|
22
|
+
--metadata '<json>' JSON object stored in product.metadata
|
|
23
|
+
--env stage|prod (default: stage)`);
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
const out = { env: 'stage', revokeBundledOnRefund: undefined };
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
const a = argv[i];
|
|
29
|
+
const next = argv[i + 1];
|
|
30
|
+
switch (a) {
|
|
31
|
+
case '--key':
|
|
32
|
+
out.key = next;
|
|
33
|
+
i++;
|
|
34
|
+
break;
|
|
35
|
+
case '--plugins':
|
|
36
|
+
out.plugins = next.split(',').map((s) => s.trim()).filter(Boolean);
|
|
37
|
+
i++;
|
|
38
|
+
break;
|
|
39
|
+
case '--type':
|
|
40
|
+
out.type = next;
|
|
41
|
+
i++;
|
|
42
|
+
break;
|
|
43
|
+
case '--name':
|
|
44
|
+
out.name = next;
|
|
45
|
+
i++;
|
|
46
|
+
break;
|
|
47
|
+
case '--description':
|
|
48
|
+
out.description = next;
|
|
49
|
+
i++;
|
|
50
|
+
break;
|
|
51
|
+
case '--refund-window-days':
|
|
52
|
+
out.refundWindowDays = parseInt(next, 10);
|
|
53
|
+
i++;
|
|
54
|
+
break;
|
|
55
|
+
case '--refund-prorate-max-days':
|
|
56
|
+
out.refundProrateMaxDays = parseInt(next, 10);
|
|
57
|
+
i++;
|
|
58
|
+
break;
|
|
59
|
+
case '--bundled-plan-id':
|
|
60
|
+
out.bundledPlanId = next;
|
|
61
|
+
i++;
|
|
62
|
+
break;
|
|
63
|
+
case '--bundled-duration-days':
|
|
64
|
+
out.bundledDurationDays = parseInt(next, 10);
|
|
65
|
+
i++;
|
|
66
|
+
break;
|
|
67
|
+
case '--revoke-bundled-on-refund':
|
|
68
|
+
out.revokeBundledOnRefund = next === 'true';
|
|
69
|
+
i++;
|
|
70
|
+
break;
|
|
71
|
+
case '--metadata':
|
|
72
|
+
out.metadata = JSON.parse(next);
|
|
73
|
+
i++;
|
|
74
|
+
break;
|
|
75
|
+
case '--env':
|
|
76
|
+
out.env = next;
|
|
77
|
+
i++;
|
|
78
|
+
break;
|
|
79
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (!out.key)
|
|
83
|
+
throw new Error('--key required');
|
|
84
|
+
if (!out.plugins || out.plugins.length === 0)
|
|
85
|
+
throw new Error('--plugins required (>=1 slug)');
|
|
86
|
+
if (!out.type)
|
|
87
|
+
throw new Error('--type required');
|
|
88
|
+
if (out.type !== 'ONE_SHOT_SKILL') {
|
|
89
|
+
throw new Error(`v1 CLI accepts only --type ONE_SHOT_SKILL (got ${out.type}). See spec ยง5.1.`);
|
|
90
|
+
}
|
|
91
|
+
if ((out.bundledPlanId == null) !== (out.bundledDurationDays == null)) {
|
|
92
|
+
throw new Error('--bundled-plan-id and --bundled-duration-days must be set together');
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
async function runCreate(argv) {
|
|
97
|
+
const args = parseArgs(argv);
|
|
98
|
+
(0, billing_http_1.validateEnv)(args.env);
|
|
99
|
+
const body = {
|
|
100
|
+
productKey: args.key,
|
|
101
|
+
type: args.type,
|
|
102
|
+
pluginSlugs: args.plugins,
|
|
103
|
+
};
|
|
104
|
+
if (args.name !== undefined)
|
|
105
|
+
body.name = args.name;
|
|
106
|
+
if (args.description !== undefined)
|
|
107
|
+
body.description = args.description;
|
|
108
|
+
if (args.refundWindowDays !== undefined)
|
|
109
|
+
body.refundWindowDays = args.refundWindowDays;
|
|
110
|
+
if (args.refundProrateMaxDays !== undefined)
|
|
111
|
+
body.refundProrateMaxDays = args.refundProrateMaxDays;
|
|
112
|
+
if (args.bundledPlanId !== undefined)
|
|
113
|
+
body.bundledPlanId = args.bundledPlanId;
|
|
114
|
+
if (args.bundledDurationDays !== undefined)
|
|
115
|
+
body.bundledDurationDays = args.bundledDurationDays;
|
|
116
|
+
if (args.revokeBundledOnRefund !== undefined)
|
|
117
|
+
body.revokeBundledOnRefund = args.revokeBundledOnRefund;
|
|
118
|
+
if (args.metadata !== undefined)
|
|
119
|
+
body.metadata = args.metadata;
|
|
120
|
+
console.log(`\n๐ Creating product ${args.key} (${args.plugins.length} plugin(s)) on ${args.env.toUpperCase()}...`);
|
|
121
|
+
const res = await (0, billing_http_1.callBilling)(args.env, 'POST', '/api/billing/admin/products', body);
|
|
122
|
+
console.log(`โ Created Product (HTTP ${res.status}):`);
|
|
123
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
124
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runShow = runShow;
|
|
4
|
+
const billing_http_1 = require("../billing-http");
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
7
|
+
console.log(`Usage: optima-product show --key <productKey> [options]
|
|
8
|
+
|
|
9
|
+
Required:
|
|
10
|
+
--key <productKey>
|
|
11
|
+
|
|
12
|
+
Optional:
|
|
13
|
+
--env stage|prod (default: stage)
|
|
14
|
+
|
|
15
|
+
Note: returns the bare Product row only โ productPlugins and channels arrays
|
|
16
|
+
are NOT included (billing's GET /api/internal/products/:key is a plain
|
|
17
|
+
findUnique with no include). To inspect channels/plugins today, query the DB
|
|
18
|
+
directly via optima-query-db. Tracked as follow-up in spec ยง7.`);
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
const out = { env: 'stage' };
|
|
22
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23
|
+
const a = argv[i];
|
|
24
|
+
const next = argv[i + 1];
|
|
25
|
+
switch (a) {
|
|
26
|
+
case '--key':
|
|
27
|
+
out.key = next;
|
|
28
|
+
i++;
|
|
29
|
+
break;
|
|
30
|
+
case '--env':
|
|
31
|
+
out.env = next;
|
|
32
|
+
i++;
|
|
33
|
+
break;
|
|
34
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (!out.key)
|
|
38
|
+
throw new Error('--key required');
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
async function runShow(argv) {
|
|
42
|
+
const args = parseArgs(argv);
|
|
43
|
+
(0, billing_http_1.validateEnv)(args.env);
|
|
44
|
+
const res = await (0, billing_http_1.callBilling)(args.env, 'GET', `/api/internal/products/${encodeURIComponent(args.key)}`);
|
|
45
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
46
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runToggleChannel = runToggleChannel;
|
|
4
|
+
const billing_http_1 = require("../billing-http");
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
7
|
+
console.log(`Usage: optima-product toggle-channel --key <productKey> --provider STRIPE --enabled true|false [options]
|
|
8
|
+
|
|
9
|
+
Required:
|
|
10
|
+
--key <productKey>
|
|
11
|
+
--provider STRIPE v1 CLI accepts only STRIPE
|
|
12
|
+
--enabled true|false
|
|
13
|
+
|
|
14
|
+
Optional:
|
|
15
|
+
--env stage|prod (default: stage)`);
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
18
|
+
const out = { env: 'stage' };
|
|
19
|
+
for (let i = 0; i < argv.length; i++) {
|
|
20
|
+
const a = argv[i];
|
|
21
|
+
const next = argv[i + 1];
|
|
22
|
+
switch (a) {
|
|
23
|
+
case '--key':
|
|
24
|
+
out.key = next;
|
|
25
|
+
i++;
|
|
26
|
+
break;
|
|
27
|
+
case '--provider':
|
|
28
|
+
out.provider = next;
|
|
29
|
+
i++;
|
|
30
|
+
break;
|
|
31
|
+
case '--enabled':
|
|
32
|
+
if (next !== 'true' && next !== 'false')
|
|
33
|
+
throw new Error('--enabled must be true or false');
|
|
34
|
+
out.enabled = next === 'true';
|
|
35
|
+
i++;
|
|
36
|
+
break;
|
|
37
|
+
case '--env':
|
|
38
|
+
out.env = next;
|
|
39
|
+
i++;
|
|
40
|
+
break;
|
|
41
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!out.key)
|
|
45
|
+
throw new Error('--key required');
|
|
46
|
+
if (!out.provider)
|
|
47
|
+
throw new Error('--provider required');
|
|
48
|
+
if (out.provider !== 'STRIPE')
|
|
49
|
+
throw new Error(`v1 CLI accepts only --provider STRIPE (got ${out.provider})`);
|
|
50
|
+
if (out.enabled === undefined)
|
|
51
|
+
throw new Error('--enabled required');
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
async function runToggleChannel(argv) {
|
|
55
|
+
const args = parseArgs(argv);
|
|
56
|
+
(0, billing_http_1.validateEnv)(args.env);
|
|
57
|
+
console.log(`\n๐ Setting ${args.provider} channel enabled=${args.enabled} on ${args.key} (${args.env.toUpperCase()})...`);
|
|
58
|
+
const res = await (0, billing_http_1.callBilling)(args.env, 'PATCH', `/api/billing/admin/products/${encodeURIComponent(args.key)}/channels/${args.provider}`, { enabled: args.enabled });
|
|
59
|
+
console.log(`โ Channel updated (HTTP ${res.status}):`);
|
|
60
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
61
|
+
}
|