@optima-chat/dev-skills 0.7.35 → 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 +1 -0
- package/bin/helpers/billing-http.ts +41 -12
- 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/dist/bin/helpers/billing-http.js +23 -9
- 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/docs/superpowers/plans/2026-05-25-optima-plugin-cli-impl.md +700 -0
- package/docs/superpowers/specs/2026-05-25-optima-plugin-cli-design.md +156 -0
- package/package.json +2 -1
package/AGENTS.md
CHANGED
|
@@ -13,6 +13,7 @@ Prefer the installed CLI tools over reimplementing long shell workflows:
|
|
|
13
13
|
- `optima-grant-balance <email> --amount <usd> [options]`
|
|
14
14
|
- `optima-product <create|update|add-channel|toggle-channel|show> [options]` — manage paid-plugin marketplace Products + Stripe channels (Wave 1.5 admin endpoints; stage default)
|
|
15
15
|
- `optima-entitlement <grant|revoke|list> [options]` — admin-grant / revoke / list paid-plugin entitlements (refuses revoke of PAYMENT / PARTNER source)
|
|
16
|
+
- `optima-plugin <show|set-paid|set-default> [options]` — flip a plugin's skills-side paid/free state (isPaid) + defaultForUser (the user-facing gate; pairs with optima-product for the billing side)
|
|
16
17
|
|
|
17
18
|
For code-reading tasks across Optima repositories, use `gh` commands against `Optima-Chat/<repo>`.
|
|
18
19
|
|
|
@@ -43,6 +43,7 @@ const DEV_SKILLS_CLIENT_SECRET_KEY = 'DEV_SKILLS_OAUTH_CLIENT_SECRET';
|
|
|
43
43
|
// retry; any transient failure surfaces immediately. Re-run the CLI.
|
|
44
44
|
const tokenCache: Record<string, string> = {};
|
|
45
45
|
const billingUrlCache: Record<string, string> = {};
|
|
46
|
+
const skillsUrlCache: Record<string, string> = {};
|
|
46
47
|
|
|
47
48
|
function getBillingUrl(env: string): string {
|
|
48
49
|
if (billingUrlCache[env]) return billingUrlCache[env];
|
|
@@ -51,6 +52,13 @@ function getBillingUrl(env: string): string {
|
|
|
51
52
|
return url;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
function getSkillsUrl(env: string): string {
|
|
56
|
+
if (skillsUrlCache[env]) return skillsUrlCache[env];
|
|
57
|
+
const url = fetchInfisicalSecret(env, '/shared-secrets/domain-urls', 'SKILLS_REGISTRY_URL');
|
|
58
|
+
skillsUrlCache[env] = url;
|
|
59
|
+
return url;
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
export function getServiceToken(env: string): string {
|
|
55
63
|
if (tokenCache[env]) return tokenCache[env];
|
|
56
64
|
|
|
@@ -89,7 +97,7 @@ interface BillingErrorEnvelope {
|
|
|
89
97
|
code?: string;
|
|
90
98
|
}
|
|
91
99
|
|
|
92
|
-
function
|
|
100
|
+
function formatServiceError(status: number, statusText: string, body: string): string {
|
|
93
101
|
let parsed: BillingErrorEnvelope | null = null;
|
|
94
102
|
try { parsed = JSON.parse(body); } catch { /* non-JSON */ }
|
|
95
103
|
|
|
@@ -114,24 +122,25 @@ function formatBillingError(status: number, statusText: string, body: string): s
|
|
|
114
122
|
return `❌ Error [${status}] ${statusText}\n Response body (first 500 bytes): ${body.slice(0, 500)}`;
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
// ───── Public: callBilling
|
|
118
|
-
export interface
|
|
125
|
+
// ───── Public: callService / callBilling / callSkills ───────────────────────
|
|
126
|
+
export interface ServiceResponse<T> {
|
|
119
127
|
status: number;
|
|
120
128
|
body: T;
|
|
121
129
|
}
|
|
122
130
|
|
|
123
131
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
132
|
+
* Authenticated call to an Optima service (billing or skills — same dev-skills
|
|
133
|
+
* M2M token works for both). Returns `{status, body}` on 2xx; throws Error with
|
|
134
|
+
* formatted message on non-2xx. Single retry on 5xx (no backoff — admin CLI).
|
|
127
135
|
*/
|
|
128
|
-
|
|
136
|
+
async function callService<T>(
|
|
137
|
+
baseUrl: string,
|
|
129
138
|
env: string,
|
|
130
139
|
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',
|
|
131
140
|
path: string,
|
|
132
141
|
body?: object,
|
|
133
|
-
): Promise<
|
|
134
|
-
const url = `${
|
|
142
|
+
): Promise<ServiceResponse<T>> {
|
|
143
|
+
const url = `${baseUrl}${path}`;
|
|
135
144
|
const token = getServiceToken(env);
|
|
136
145
|
|
|
137
146
|
const doFetch = async () => fetch(url, {
|
|
@@ -145,18 +154,38 @@ export async function callBilling<T = unknown>(
|
|
|
145
154
|
|
|
146
155
|
let res = await doFetch();
|
|
147
156
|
if (res.status >= 500) {
|
|
148
|
-
// One retry on 5xx
|
|
149
157
|
res = await doFetch();
|
|
150
158
|
}
|
|
151
159
|
const text = await res.text();
|
|
152
160
|
if (!res.ok) {
|
|
153
|
-
throw new Error(
|
|
161
|
+
throw new Error(formatServiceError(res.status, res.statusText, text));
|
|
154
162
|
}
|
|
155
163
|
let parsed: T;
|
|
156
164
|
try {
|
|
157
165
|
parsed = text ? (JSON.parse(text) as T) : (undefined as unknown as T);
|
|
158
166
|
} catch {
|
|
159
|
-
throw new Error(`
|
|
167
|
+
throw new Error(`Service returned non-JSON 2xx body: ${text.slice(0, 200)}`);
|
|
160
168
|
}
|
|
161
169
|
return { status: res.status, body: parsed };
|
|
162
170
|
}
|
|
171
|
+
|
|
172
|
+
/** Kept as an alias for billing-side callers that reference the response type. */
|
|
173
|
+
export interface BillingResponse<T> extends ServiceResponse<T> {}
|
|
174
|
+
|
|
175
|
+
export async function callBilling<T = unknown>(
|
|
176
|
+
env: string,
|
|
177
|
+
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',
|
|
178
|
+
path: string,
|
|
179
|
+
body?: object,
|
|
180
|
+
): Promise<ServiceResponse<T>> {
|
|
181
|
+
return callService<T>(getBillingUrl(env), env, method, path, body);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function callSkills<T = unknown>(
|
|
185
|
+
env: string,
|
|
186
|
+
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',
|
|
187
|
+
path: string,
|
|
188
|
+
body?: object,
|
|
189
|
+
): Promise<ServiceResponse<T>> {
|
|
190
|
+
return callService<T>(getSkillsUrl(env), env, method, path, body);
|
|
191
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { callSkills, validateEnv } from '../billing-http';
|
|
2
|
+
import { confirmIfProd } from '../confirm-prompt';
|
|
3
|
+
|
|
4
|
+
interface SetDefaultArgs {
|
|
5
|
+
slug: string;
|
|
6
|
+
default: boolean;
|
|
7
|
+
yes: boolean;
|
|
8
|
+
env: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv: string[]): SetDefaultArgs {
|
|
12
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
13
|
+
console.log(`Usage: optima-plugin set-default --slug <slug> --default true|false [options]
|
|
14
|
+
|
|
15
|
+
Required:
|
|
16
|
+
--slug <slug>
|
|
17
|
+
--default true|false Sets Plugin.defaultForUser
|
|
18
|
+
|
|
19
|
+
Optional:
|
|
20
|
+
--yes Skip prod confirmation prompt (no-op on stage)
|
|
21
|
+
--env stage|prod (default: stage)
|
|
22
|
+
|
|
23
|
+
Note: no skill-sync broadcast — changes what NEW user syncs receive; does not
|
|
24
|
+
retroactively add/remove the plugin for existing users until their next sync.`);
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
const out: Partial<SetDefaultArgs> = { env: 'stage', yes: false };
|
|
28
|
+
for (let i = 0; i < argv.length; i++) {
|
|
29
|
+
const a = argv[i];
|
|
30
|
+
const next = argv[i + 1];
|
|
31
|
+
switch (a) {
|
|
32
|
+
case '--slug': out.slug = next; i++; break;
|
|
33
|
+
case '--default':
|
|
34
|
+
if (next !== 'true' && next !== 'false') throw new Error('--default must be true or false');
|
|
35
|
+
out.default = next === 'true'; i++; break;
|
|
36
|
+
case '--yes': out.yes = true; break;
|
|
37
|
+
case '--env': out.env = next; i++; break;
|
|
38
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!out.slug) throw new Error('--slug required');
|
|
42
|
+
if (out.default === undefined) throw new Error('--default required (true|false)');
|
|
43
|
+
return out as SetDefaultArgs;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function runSetDefault(argv: string[]): Promise<void> {
|
|
47
|
+
const args = parseArgs(argv);
|
|
48
|
+
validateEnv(args.env);
|
|
49
|
+
|
|
50
|
+
await confirmIfProd(
|
|
51
|
+
args.env,
|
|
52
|
+
`Action: set defaultForUser=${args.default} on plugin '${args.slug}' (${args.env.toUpperCase()})`,
|
|
53
|
+
args.yes,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
console.log(`\n🔧 Setting defaultForUser=${args.default} on ${args.slug} (${args.env.toUpperCase()})...`);
|
|
57
|
+
const res = await callSkills(
|
|
58
|
+
args.env,
|
|
59
|
+
'PATCH',
|
|
60
|
+
`/api/admin/plugins/${encodeURIComponent(args.slug)}`,
|
|
61
|
+
{ defaultForUser: args.default },
|
|
62
|
+
);
|
|
63
|
+
console.log(`✓ Updated plugin (HTTP ${res.status}):`);
|
|
64
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
65
|
+
}
|
|
@@ -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); });
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.validateEnv = validateEnv;
|
|
4
4
|
exports.getServiceToken = getServiceToken;
|
|
5
5
|
exports.callBilling = callBilling;
|
|
6
|
+
exports.callSkills = callSkills;
|
|
6
7
|
const child_process_1 = require("child_process");
|
|
7
8
|
const infisical_secrets_1 = require("./infisical-secrets");
|
|
8
9
|
const db_utils_1 = require("./db-utils");
|
|
@@ -44,6 +45,7 @@ const DEV_SKILLS_CLIENT_SECRET_KEY = 'DEV_SKILLS_OAUTH_CLIENT_SECRET';
|
|
|
44
45
|
// retry; any transient failure surfaces immediately. Re-run the CLI.
|
|
45
46
|
const tokenCache = {};
|
|
46
47
|
const billingUrlCache = {};
|
|
48
|
+
const skillsUrlCache = {};
|
|
47
49
|
function getBillingUrl(env) {
|
|
48
50
|
if (billingUrlCache[env])
|
|
49
51
|
return billingUrlCache[env];
|
|
@@ -51,6 +53,13 @@ function getBillingUrl(env) {
|
|
|
51
53
|
billingUrlCache[env] = url;
|
|
52
54
|
return url;
|
|
53
55
|
}
|
|
56
|
+
function getSkillsUrl(env) {
|
|
57
|
+
if (skillsUrlCache[env])
|
|
58
|
+
return skillsUrlCache[env];
|
|
59
|
+
const url = (0, infisical_secrets_1.fetchInfisicalSecret)(env, '/shared-secrets/domain-urls', 'SKILLS_REGISTRY_URL');
|
|
60
|
+
skillsUrlCache[env] = url;
|
|
61
|
+
return url;
|
|
62
|
+
}
|
|
54
63
|
function getServiceToken(env) {
|
|
55
64
|
if (tokenCache[env])
|
|
56
65
|
return tokenCache[env];
|
|
@@ -77,7 +86,7 @@ function getServiceToken(env) {
|
|
|
77
86
|
tokenCache[env] = parsed.access_token;
|
|
78
87
|
return parsed.access_token;
|
|
79
88
|
}
|
|
80
|
-
function
|
|
89
|
+
function formatServiceError(status, statusText, body) {
|
|
81
90
|
let parsed = null;
|
|
82
91
|
try {
|
|
83
92
|
parsed = JSON.parse(body);
|
|
@@ -104,12 +113,12 @@ function formatBillingError(status, statusText, body) {
|
|
|
104
113
|
return `❌ Error [${status}] ${statusText}\n Response body (first 500 bytes): ${body.slice(0, 500)}`;
|
|
105
114
|
}
|
|
106
115
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
116
|
+
* Authenticated call to an Optima service (billing or skills — same dev-skills
|
|
117
|
+
* M2M token works for both). Returns `{status, body}` on 2xx; throws Error with
|
|
118
|
+
* formatted message on non-2xx. Single retry on 5xx (no backoff — admin CLI).
|
|
110
119
|
*/
|
|
111
|
-
async function
|
|
112
|
-
const url = `${
|
|
120
|
+
async function callService(baseUrl, env, method, path, body) {
|
|
121
|
+
const url = `${baseUrl}${path}`;
|
|
113
122
|
const token = getServiceToken(env);
|
|
114
123
|
const doFetch = async () => fetch(url, {
|
|
115
124
|
method,
|
|
@@ -121,19 +130,24 @@ async function callBilling(env, method, path, body) {
|
|
|
121
130
|
});
|
|
122
131
|
let res = await doFetch();
|
|
123
132
|
if (res.status >= 500) {
|
|
124
|
-
// One retry on 5xx
|
|
125
133
|
res = await doFetch();
|
|
126
134
|
}
|
|
127
135
|
const text = await res.text();
|
|
128
136
|
if (!res.ok) {
|
|
129
|
-
throw new Error(
|
|
137
|
+
throw new Error(formatServiceError(res.status, res.statusText, text));
|
|
130
138
|
}
|
|
131
139
|
let parsed;
|
|
132
140
|
try {
|
|
133
141
|
parsed = text ? JSON.parse(text) : undefined;
|
|
134
142
|
}
|
|
135
143
|
catch {
|
|
136
|
-
throw new Error(`
|
|
144
|
+
throw new Error(`Service returned non-JSON 2xx body: ${text.slice(0, 200)}`);
|
|
137
145
|
}
|
|
138
146
|
return { status: res.status, body: parsed };
|
|
139
147
|
}
|
|
148
|
+
async function callBilling(env, method, path, body) {
|
|
149
|
+
return callService(getBillingUrl(env), env, method, path, body);
|
|
150
|
+
}
|
|
151
|
+
async function callSkills(env, method, path, body) {
|
|
152
|
+
return callService(getSkillsUrl(env), env, method, path, body);
|
|
153
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runSetDefault = runSetDefault;
|
|
4
|
+
const billing_http_1 = require("../billing-http");
|
|
5
|
+
const confirm_prompt_1 = require("../confirm-prompt");
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
8
|
+
console.log(`Usage: optima-plugin set-default --slug <slug> --default true|false [options]
|
|
9
|
+
|
|
10
|
+
Required:
|
|
11
|
+
--slug <slug>
|
|
12
|
+
--default true|false Sets Plugin.defaultForUser
|
|
13
|
+
|
|
14
|
+
Optional:
|
|
15
|
+
--yes Skip prod confirmation prompt (no-op on stage)
|
|
16
|
+
--env stage|prod (default: stage)
|
|
17
|
+
|
|
18
|
+
Note: no skill-sync broadcast — changes what NEW user syncs receive; does not
|
|
19
|
+
retroactively add/remove the plugin for existing users until their next sync.`);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
const out = { env: 'stage', yes: false };
|
|
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 '--slug':
|
|
28
|
+
out.slug = next;
|
|
29
|
+
i++;
|
|
30
|
+
break;
|
|
31
|
+
case '--default':
|
|
32
|
+
if (next !== 'true' && next !== 'false')
|
|
33
|
+
throw new Error('--default must be true or false');
|
|
34
|
+
out.default = next === 'true';
|
|
35
|
+
i++;
|
|
36
|
+
break;
|
|
37
|
+
case '--yes':
|
|
38
|
+
out.yes = true;
|
|
39
|
+
break;
|
|
40
|
+
case '--env':
|
|
41
|
+
out.env = next;
|
|
42
|
+
i++;
|
|
43
|
+
break;
|
|
44
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!out.slug)
|
|
48
|
+
throw new Error('--slug required');
|
|
49
|
+
if (out.default === undefined)
|
|
50
|
+
throw new Error('--default required (true|false)');
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
async function runSetDefault(argv) {
|
|
54
|
+
const args = parseArgs(argv);
|
|
55
|
+
(0, billing_http_1.validateEnv)(args.env);
|
|
56
|
+
await (0, confirm_prompt_1.confirmIfProd)(args.env, `Action: set defaultForUser=${args.default} on plugin '${args.slug}' (${args.env.toUpperCase()})`, args.yes);
|
|
57
|
+
console.log(`\n🔧 Setting defaultForUser=${args.default} on ${args.slug} (${args.env.toUpperCase()})...`);
|
|
58
|
+
const res = await (0, billing_http_1.callSkills)(args.env, 'PATCH', `/api/admin/plugins/${encodeURIComponent(args.slug)}`, { defaultForUser: args.default });
|
|
59
|
+
console.log(`✓ Updated plugin (HTTP ${res.status}):`);
|
|
60
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
61
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runSetPaid = runSetPaid;
|
|
4
|
+
const billing_http_1 = require("../billing-http");
|
|
5
|
+
const confirm_prompt_1 = require("../confirm-prompt");
|
|
6
|
+
function parseArgs(argv) {
|
|
7
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
8
|
+
console.log(`Usage: optima-plugin set-paid --slug <slug> --paid true|false [options]
|
|
9
|
+
|
|
10
|
+
Required:
|
|
11
|
+
--slug <slug>
|
|
12
|
+
--paid true|false Sets Plugin.isPaid (the user-facing paid/free gate)
|
|
13
|
+
|
|
14
|
+
Optional:
|
|
15
|
+
--yes Skip prod confirmation prompt (no-op on stage)
|
|
16
|
+
--env stage|prod (default: stage)
|
|
17
|
+
|
|
18
|
+
Note: salesUrl is NOT settable here (skills PATCH is strict; salesUrl is
|
|
19
|
+
publish-time-only via plugin.json metadata). When isPaid=true and salesUrl is
|
|
20
|
+
null, the 402 falls back to sales.optima.onl.`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
const out = { env: 'stage', yes: false };
|
|
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':
|
|
29
|
+
out.slug = next;
|
|
30
|
+
i++;
|
|
31
|
+
break;
|
|
32
|
+
case '--paid':
|
|
33
|
+
if (next !== 'true' && next !== 'false')
|
|
34
|
+
throw new Error('--paid must be true or false');
|
|
35
|
+
out.paid = next === 'true';
|
|
36
|
+
i++;
|
|
37
|
+
break;
|
|
38
|
+
case '--yes':
|
|
39
|
+
out.yes = true;
|
|
40
|
+
break;
|
|
41
|
+
case '--env':
|
|
42
|
+
out.env = next;
|
|
43
|
+
i++;
|
|
44
|
+
break;
|
|
45
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!out.slug)
|
|
49
|
+
throw new Error('--slug required');
|
|
50
|
+
if (out.paid === undefined)
|
|
51
|
+
throw new Error('--paid required (true|false)');
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
async function runSetPaid(argv) {
|
|
55
|
+
const args = parseArgs(argv);
|
|
56
|
+
(0, billing_http_1.validateEnv)(args.env);
|
|
57
|
+
await (0, confirm_prompt_1.confirmIfProd)(args.env, `Action: set isPaid=${args.paid} on plugin '${args.slug}' (${args.env.toUpperCase()})`, args.yes);
|
|
58
|
+
console.log(`\n💰 Setting isPaid=${args.paid} on ${args.slug} (${args.env.toUpperCase()})...`);
|
|
59
|
+
const res = await (0, billing_http_1.callSkills)(args.env, 'PATCH', `/api/admin/plugins/${encodeURIComponent(args.slug)}`, { isPaid: args.paid });
|
|
60
|
+
console.log(`✓ Updated plugin (HTTP ${res.status}):`);
|
|
61
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
62
|
+
if (args.paid) {
|
|
63
|
+
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).`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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-plugin show --slug <slug> [options]
|
|
8
|
+
|
|
9
|
+
Required:
|
|
10
|
+
--slug <slug>
|
|
11
|
+
|
|
12
|
+
Optional:
|
|
13
|
+
--env stage|prod (default: stage)
|
|
14
|
+
|
|
15
|
+
Note: reads the public GET /api/plugins/:slug — shows isPaid, salesUrl, and
|
|
16
|
+
descriptive fields, but NOT defaultForUser / status / trustLevel (public
|
|
17
|
+
endpoint omits them). Returns 404 for non-ACTIVE plugins.`);
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
const out = { env: 'stage' };
|
|
21
|
+
for (let i = 0; i < argv.length; i++) {
|
|
22
|
+
const a = argv[i];
|
|
23
|
+
const next = argv[i + 1];
|
|
24
|
+
switch (a) {
|
|
25
|
+
case '--slug':
|
|
26
|
+
out.slug = next;
|
|
27
|
+
i++;
|
|
28
|
+
break;
|
|
29
|
+
case '--env':
|
|
30
|
+
out.env = next;
|
|
31
|
+
i++;
|
|
32
|
+
break;
|
|
33
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!out.slug)
|
|
37
|
+
throw new Error('--slug required');
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
async function runShow(argv) {
|
|
41
|
+
const args = parseArgs(argv);
|
|
42
|
+
(0, billing_http_1.validateEnv)(args.env);
|
|
43
|
+
const res = await (0, billing_http_1.callSkills)(args.env, 'GET', `/api/plugins/${encodeURIComponent(args.slug)}`);
|
|
44
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
45
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const show_1 = require("./plugin/show");
|
|
5
|
+
const set_paid_1 = require("./plugin/set-paid");
|
|
6
|
+
const set_default_1 = require("./plugin/set-default");
|
|
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
|
+
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 'show':
|
|
25
|
+
await (0, show_1.runShow)(rest);
|
|
26
|
+
break;
|
|
27
|
+
case 'set-paid':
|
|
28
|
+
await (0, set_paid_1.runSetPaid)(rest);
|
|
29
|
+
break;
|
|
30
|
+
case 'set-default':
|
|
31
|
+
await (0, set_default_1.runSetDefault)(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); });
|