@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,700 @@
|
|
|
1
|
+
# optima-plugin CLI — Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add `optima-plugin` (show / set-paid / set-default) to `@optima-chat/dev-skills` — the skills-side admin command the marketplace-admin-cli missed, flipping `Plugin.isPaid` / `defaultForUser` (the actual user-facing paid/free gate).
|
|
6
|
+
|
|
7
|
+
**Architecture:** New bin mirroring `optima-product`'s dispatcher + subcommand-handler structure. Hits optima-skills `PATCH /api/admin/plugins/:slug` (writes) + public `GET /api/plugins/:slug` (show). Reuses the dev-skills M2M token (same client works against skills). Refactors `billing-http.ts` into a generic `callService` core shared by `callBilling` + a new `callSkills`.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Node 18+ `fetch`, existing helpers (`getServiceToken`, `fetchInfisicalSecret`, `confirmIfProd`, `validateEnv`).
|
|
10
|
+
|
|
11
|
+
**Spec:** [`docs/superpowers/specs/2026-05-25-optima-plugin-cli-design.md`](../specs/2026-05-25-optima-plugin-cli-design.md)
|
|
12
|
+
|
|
13
|
+
**Testing convention:** smoke-only, no unit tests (matches all existing dev-skills helpers). Each task has a stage smoke step. Billing regression smoke after the refactor (T2).
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## File Structure
|
|
18
|
+
|
|
19
|
+
| Path | Change |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `bin/helpers/billing-http.ts` | **Modify** — extract `callService(baseUrl, env, method, path, body?)` private core (token-mint + 5xx-retry + non-JSON guard move in); `callBilling` delegates (signature/behavior unchanged); add `getSkillsUrl(env)` + `callSkills`; rename `formatBillingError`→`formatServiceError` + genericize its non-JSON string; export `callSkills`. |
|
|
22
|
+
| `bin/helpers/plugin.ts` | **Create** — `optima-plugin` dispatcher (show/set-paid/set-default). |
|
|
23
|
+
| `bin/helpers/plugin/show.ts` | **Create** — `show` handler (public GET). |
|
|
24
|
+
| `bin/helpers/plugin/set-paid.ts` | **Create** — `set-paid` handler (PATCH isPaid). |
|
|
25
|
+
| `bin/helpers/plugin/set-default.ts` | **Create** — `set-default` handler (PATCH defaultForUser). |
|
|
26
|
+
| `package.json` | **Modify** — add `optima-plugin` bin entry; version bump. |
|
|
27
|
+
| `AGENTS.md` | **Modify** — add `optima-plugin` to Primary Entry Points. |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Task 1: Infra verification (no code)
|
|
32
|
+
|
|
33
|
+
**Goal:** Confirm the one remaining open question (spec §10): prod skills URL + prod dev-skills client on prod skills allowlist. Stage is already fully verified (spec §2).
|
|
34
|
+
|
|
35
|
+
- [ ] **Step 1: Verify prod SKILLS_REGISTRY_URL + stage (sanity)**
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
cd /mnt/d/work/projects/optima/optima-dev-skills
|
|
39
|
+
node -e "const {fetchInfisicalSecret}=require('./dist/bin/helpers/infisical-secrets'); console.log('stage:', fetchInfisicalSecret('stage','/shared-secrets/domain-urls','SKILLS_REGISTRY_URL')); console.log('prod:', fetchInfisicalSecret('prod','/shared-secrets/domain-urls','SKILLS_REGISTRY_URL'));"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Expected: stage `https://skills.stage.optima.onl`; prod some `https://skills.optima.onl` (or NOT FOUND → record; prod plugin commands then unavailable until added, non-blocking since the immediate goal is stage scout).
|
|
43
|
+
|
|
44
|
+
- [ ] **Step 2: Record findings inline, commit**
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
RESOLVED: stage SKILLS_REGISTRY_URL=<...>, prod=<... or NOT FOUND>
|
|
48
|
+
(prod dev-skills allowlist: skills config default 'sales-page,dev-skills' applies unless overridden — verify only if prod use is needed now.)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git add docs/superpowers/plans/2026-05-25-optima-plugin-cli-impl.md
|
|
53
|
+
git commit -m "plan T1: verify prod SKILLS_REGISTRY_URL"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Stage is sufficient to proceed regardless of prod result (immediate goal is stage scout/skillify).
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Task 2: Refactor billing-http → callService core + callSkills
|
|
61
|
+
|
|
62
|
+
**Files:**
|
|
63
|
+
- Modify: `bin/helpers/billing-http.ts`
|
|
64
|
+
|
|
65
|
+
- [ ] **Step 1: Add getSkillsUrl + refactor call core**
|
|
66
|
+
|
|
67
|
+
In `bin/helpers/billing-http.ts`:
|
|
68
|
+
|
|
69
|
+
(a) Add a skills URL resolver next to `getBillingUrl` (which is private, after the cache decls):
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
function getSkillsUrl(env: string): string {
|
|
73
|
+
if (skillsUrlCache[env]) return skillsUrlCache[env];
|
|
74
|
+
const url = fetchInfisicalSecret(env, '/shared-secrets/domain-urls', 'SKILLS_REGISTRY_URL');
|
|
75
|
+
skillsUrlCache[env] = url;
|
|
76
|
+
return url;
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
(b) Add the cache decl alongside the existing `billingUrlCache`:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const skillsUrlCache: Record<string, string> = {};
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
(c) Rename `formatBillingError` → `formatServiceError` (declaration at `billing-http.ts:92` + its one call site, now inside `callService` per step (d)). The `"Billing returned non-JSON 2xx body"` string lives in the call core and is genericized to `"Service returned..."` in step (d)'s code block.
|
|
87
|
+
|
|
88
|
+
(d) Replace the `callBilling` function (lines ~117-162) with a generic core + two thin wrappers:
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// ───── Public: callService / callBilling / callSkills ───────────────────────
|
|
92
|
+
export interface ServiceResponse<T> {
|
|
93
|
+
status: number;
|
|
94
|
+
body: T;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Authenticated call to an Optima service (billing or skills — same dev-skills
|
|
99
|
+
* M2M token works for both). Returns `{status, body}` on 2xx; throws Error with
|
|
100
|
+
* formatted message on non-2xx. Single retry on 5xx (no backoff — admin CLI).
|
|
101
|
+
*/
|
|
102
|
+
async function callService<T>(
|
|
103
|
+
baseUrl: string,
|
|
104
|
+
env: string,
|
|
105
|
+
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',
|
|
106
|
+
path: string,
|
|
107
|
+
body?: object,
|
|
108
|
+
): Promise<ServiceResponse<T>> {
|
|
109
|
+
const url = `${baseUrl}${path}`;
|
|
110
|
+
const token = getServiceToken(env);
|
|
111
|
+
|
|
112
|
+
const doFetch = async () => fetch(url, {
|
|
113
|
+
method,
|
|
114
|
+
headers: {
|
|
115
|
+
Authorization: `Bearer ${token}`,
|
|
116
|
+
'Content-Type': 'application/json',
|
|
117
|
+
},
|
|
118
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
let res = await doFetch();
|
|
122
|
+
if (res.status >= 500) {
|
|
123
|
+
res = await doFetch();
|
|
124
|
+
}
|
|
125
|
+
const text = await res.text();
|
|
126
|
+
if (!res.ok) {
|
|
127
|
+
throw new Error(formatServiceError(res.status, res.statusText, text));
|
|
128
|
+
}
|
|
129
|
+
let parsed: T;
|
|
130
|
+
try {
|
|
131
|
+
parsed = text ? (JSON.parse(text) as T) : (undefined as unknown as T);
|
|
132
|
+
} catch {
|
|
133
|
+
throw new Error(`Service returned non-JSON 2xx body: ${text.slice(0, 200)}`);
|
|
134
|
+
}
|
|
135
|
+
return { status: res.status, body: parsed };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @deprecated name retained for the billing-side callers. */
|
|
139
|
+
export interface BillingResponse<T> extends ServiceResponse<T> {}
|
|
140
|
+
|
|
141
|
+
export async function callBilling<T = unknown>(
|
|
142
|
+
env: string,
|
|
143
|
+
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',
|
|
144
|
+
path: string,
|
|
145
|
+
body?: object,
|
|
146
|
+
): Promise<ServiceResponse<T>> {
|
|
147
|
+
return callService<T>(getBillingUrl(env), env, method, path, body);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function callSkills<T = unknown>(
|
|
151
|
+
env: string,
|
|
152
|
+
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',
|
|
153
|
+
path: string,
|
|
154
|
+
body?: object,
|
|
155
|
+
): Promise<ServiceResponse<T>> {
|
|
156
|
+
return callService<T>(getSkillsUrl(env), env, method, path, body);
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
(e) Rename the function declaration `function formatBillingError(` → `function formatServiceError(`. Leave its internal envelope-handling comments as-is (they accurately describe billing's shapes; skills' nested shape is handled by the same nested branch).
|
|
161
|
+
|
|
162
|
+
- [ ] **Step 2: Build**
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
npm run build
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Expected: no errors. (`callBilling` callers in product/* + entitlement/* unchanged — they import `callBilling` which still exists with identical signature. `BillingResponse<T>` kept as alias so any type references still resolve.)
|
|
169
|
+
|
|
170
|
+
- [ ] **Step 3: Billing regression smoke (prove the refactor didn't break billing)**
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
node dist/bin/helpers/entitlement.js list --email admin@optima.chat --env stage
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Expected: same behavior as before (prints `(no entitlements ...)` or a table) — proves callBilling still works through callService.
|
|
177
|
+
|
|
178
|
+
- [ ] **Step 4: Skills reachability smoke (prove callSkills works)**
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
node -e "
|
|
182
|
+
const { callSkills } = require('./dist/bin/helpers/billing-http');
|
|
183
|
+
callSkills('stage','GET','/api/plugins/scout')
|
|
184
|
+
.then(r => console.log('STATUS', r.status, 'isPaid', r.body.isPaid))
|
|
185
|
+
.catch(e => { console.error(e.message); process.exit(1); });
|
|
186
|
+
"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Expected: `STATUS 200 isPaid false` (scout current state).
|
|
190
|
+
|
|
191
|
+
- [ ] **Step 5: Commit**
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
git add bin/helpers/billing-http.ts
|
|
195
|
+
git commit -m "refactor(billing-http): extract callService core + add callSkills/getSkillsUrl"
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Task 3: optima-plugin dispatcher + show subcommand
|
|
201
|
+
|
|
202
|
+
**Files:**
|
|
203
|
+
- Create: `bin/helpers/plugin.ts`
|
|
204
|
+
- Create: `bin/helpers/plugin/show.ts`
|
|
205
|
+
|
|
206
|
+
- [ ] **Step 1: Write the dispatcher**
|
|
207
|
+
|
|
208
|
+
Create `bin/helpers/plugin.ts`:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
#!/usr/bin/env node
|
|
212
|
+
|
|
213
|
+
import { runShow } from './plugin/show';
|
|
214
|
+
|
|
215
|
+
function printHelp() {
|
|
216
|
+
console.log(`Usage: optima-plugin <subcommand> [options]
|
|
217
|
+
|
|
218
|
+
Subcommands:
|
|
219
|
+
show Show a plugin's marketplace state (isPaid, salesUrl, ... ACTIVE plugins only)
|
|
220
|
+
set-paid Flip a plugin's isPaid flag (the user-facing paid/free gate)
|
|
221
|
+
set-default Flip a plugin's defaultForUser flag
|
|
222
|
+
|
|
223
|
+
Run 'optima-plugin <subcommand> --help' for subcommand-specific options.`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function main() {
|
|
227
|
+
const [, , subcommand, ...rest] = process.argv;
|
|
228
|
+
if (!subcommand || subcommand === '-h' || subcommand === '--help') { printHelp(); process.exit(0); }
|
|
229
|
+
switch (subcommand) {
|
|
230
|
+
case 'show': await runShow(rest); break;
|
|
231
|
+
case 'set-paid':
|
|
232
|
+
case 'set-default':
|
|
233
|
+
console.error(`Subcommand '${subcommand}' not yet implemented (added in a later task).`);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
default:
|
|
236
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
237
|
+
printHelp();
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
main().catch((err) => { console.error(err.message); process.exit(1); });
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
- [ ] **Step 2: Write the show handler**
|
|
246
|
+
|
|
247
|
+
Create `bin/helpers/plugin/show.ts`:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import { callSkills, validateEnv } from '../billing-http';
|
|
251
|
+
|
|
252
|
+
interface ShowArgs {
|
|
253
|
+
slug: string;
|
|
254
|
+
env: string;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function parseArgs(argv: string[]): ShowArgs {
|
|
258
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
259
|
+
console.log(`Usage: optima-plugin show --slug <slug> [options]
|
|
260
|
+
|
|
261
|
+
Required:
|
|
262
|
+
--slug <slug>
|
|
263
|
+
|
|
264
|
+
Optional:
|
|
265
|
+
--env stage|prod (default: stage)
|
|
266
|
+
|
|
267
|
+
Note: reads the public GET /api/plugins/:slug — shows isPaid, salesUrl, and
|
|
268
|
+
descriptive fields, but NOT defaultForUser / status / trustLevel (public
|
|
269
|
+
endpoint omits them). Returns 404 for non-ACTIVE plugins.`);
|
|
270
|
+
process.exit(0);
|
|
271
|
+
}
|
|
272
|
+
const out: Partial<ShowArgs> = { env: 'stage' };
|
|
273
|
+
for (let i = 0; i < argv.length; i++) {
|
|
274
|
+
const a = argv[i];
|
|
275
|
+
const next = argv[i + 1];
|
|
276
|
+
switch (a) {
|
|
277
|
+
case '--slug': out.slug = next; i++; break;
|
|
278
|
+
case '--env': out.env = next; i++; break;
|
|
279
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!out.slug) throw new Error('--slug required');
|
|
283
|
+
return out as ShowArgs;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export async function runShow(argv: string[]): Promise<void> {
|
|
287
|
+
const args = parseArgs(argv);
|
|
288
|
+
validateEnv(args.env);
|
|
289
|
+
const res = await callSkills(args.env, 'GET', `/api/plugins/${encodeURIComponent(args.slug)}`);
|
|
290
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
- [ ] **Step 3: Build + smoke**
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
npm run build
|
|
298
|
+
node dist/bin/helpers/plugin.js --help
|
|
299
|
+
node dist/bin/helpers/plugin.js show --help
|
|
300
|
+
node dist/bin/helpers/plugin.js show --slug scout --env stage
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Expected: help texts exit 0; `show --slug scout` prints scout JSON with `isPaid: false`, `salesUrl: null`.
|
|
304
|
+
|
|
305
|
+
- [ ] **Step 4: Commit**
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
git add bin/helpers/plugin.ts bin/helpers/plugin/show.ts
|
|
309
|
+
git commit -m "feat(plugin): add optima-plugin dispatcher + show subcommand"
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Task 4: set-paid subcommand
|
|
315
|
+
|
|
316
|
+
**Files:**
|
|
317
|
+
- Create: `bin/helpers/plugin/set-paid.ts`
|
|
318
|
+
- Modify: `bin/helpers/plugin.ts`
|
|
319
|
+
|
|
320
|
+
- [ ] **Step 1: Write the handler**
|
|
321
|
+
|
|
322
|
+
Create `bin/helpers/plugin/set-paid.ts`:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
import { callSkills, validateEnv } from '../billing-http';
|
|
326
|
+
import { confirmIfProd } from '../confirm-prompt';
|
|
327
|
+
|
|
328
|
+
interface SetPaidArgs {
|
|
329
|
+
slug: string;
|
|
330
|
+
paid: boolean;
|
|
331
|
+
yes: boolean;
|
|
332
|
+
env: string;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function parseArgs(argv: string[]): SetPaidArgs {
|
|
336
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
337
|
+
console.log(`Usage: optima-plugin set-paid --slug <slug> --paid true|false [options]
|
|
338
|
+
|
|
339
|
+
Required:
|
|
340
|
+
--slug <slug>
|
|
341
|
+
--paid true|false Sets Plugin.isPaid (the user-facing paid/free gate)
|
|
342
|
+
|
|
343
|
+
Optional:
|
|
344
|
+
--yes Skip prod confirmation prompt (no-op on stage)
|
|
345
|
+
--env stage|prod (default: stage)
|
|
346
|
+
|
|
347
|
+
Note: salesUrl is NOT settable here (skills PATCH is strict; salesUrl is
|
|
348
|
+
publish-time-only via plugin.json metadata). When isPaid=true and salesUrl is
|
|
349
|
+
null, the 402 falls back to sales.optima.onl.`);
|
|
350
|
+
process.exit(0);
|
|
351
|
+
}
|
|
352
|
+
const out: Partial<SetPaidArgs> = { env: 'stage', yes: false };
|
|
353
|
+
for (let i = 0; i < argv.length; i++) {
|
|
354
|
+
const a = argv[i];
|
|
355
|
+
const next = argv[i + 1];
|
|
356
|
+
switch (a) {
|
|
357
|
+
case '--slug': out.slug = next; i++; break;
|
|
358
|
+
case '--paid':
|
|
359
|
+
if (next !== 'true' && next !== 'false') throw new Error('--paid must be true or false');
|
|
360
|
+
out.paid = next === 'true'; i++; break;
|
|
361
|
+
case '--yes': out.yes = true; break;
|
|
362
|
+
case '--env': out.env = next; i++; break;
|
|
363
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (!out.slug) throw new Error('--slug required');
|
|
367
|
+
if (out.paid === undefined) throw new Error('--paid required (true|false)');
|
|
368
|
+
return out as SetPaidArgs;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export async function runSetPaid(argv: string[]): Promise<void> {
|
|
372
|
+
const args = parseArgs(argv);
|
|
373
|
+
validateEnv(args.env);
|
|
374
|
+
|
|
375
|
+
await confirmIfProd(
|
|
376
|
+
args.env,
|
|
377
|
+
`Action: set isPaid=${args.paid} on plugin '${args.slug}' (${args.env.toUpperCase()})`,
|
|
378
|
+
args.yes,
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
console.log(`\n💰 Setting isPaid=${args.paid} on ${args.slug} (${args.env.toUpperCase()})...`);
|
|
382
|
+
const res = await callSkills(
|
|
383
|
+
args.env,
|
|
384
|
+
'PATCH',
|
|
385
|
+
`/api/admin/plugins/${encodeURIComponent(args.slug)}`,
|
|
386
|
+
{ isPaid: args.paid },
|
|
387
|
+
);
|
|
388
|
+
console.log(`✓ Updated plugin (HTTP ${res.status}):`);
|
|
389
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
390
|
+
if (args.paid) {
|
|
391
|
+
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).`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
- [ ] **Step 2: Wire into dispatcher**
|
|
397
|
+
|
|
398
|
+
In `bin/helpers/plugin.ts` add import:
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { runSetPaid } from './plugin/set-paid';
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Replace the `case 'set-paid':` line in the stub block:
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
case 'set-paid':
|
|
408
|
+
case 'set-default':
|
|
409
|
+
console.error(`Subcommand '${subcommand}' not yet implemented (added in a later task).`);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
with:
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
case 'set-paid': await runSetPaid(rest); break;
|
|
417
|
+
case 'set-default':
|
|
418
|
+
console.error(`Subcommand '${subcommand}' not yet implemented (added in a later task).`);
|
|
419
|
+
process.exit(1);
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
- [ ] **Step 3: Build + smoke**
|
|
423
|
+
|
|
424
|
+
```bash
|
|
425
|
+
npm run build
|
|
426
|
+
node dist/bin/helpers/plugin.js set-paid --help
|
|
427
|
+
# Smoke: skillify is meant to be free — set-paid false is a safe no-op-ish smoke.
|
|
428
|
+
node dist/bin/helpers/plugin.js set-paid --slug skillify --paid false --env stage
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Expected: HTTP 200, returned row `isPaid: false`. (Does NOT make scout paid yet — that's the operator's deliberate action, kept out of the build smoke.)
|
|
432
|
+
|
|
433
|
+
- [ ] **Step 4: Commit**
|
|
434
|
+
|
|
435
|
+
```bash
|
|
436
|
+
git add bin/helpers/plugin.ts bin/helpers/plugin/set-paid.ts
|
|
437
|
+
git commit -m "feat(plugin): add set-paid subcommand"
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## Task 5: set-default subcommand
|
|
443
|
+
|
|
444
|
+
**Files:**
|
|
445
|
+
- Create: `bin/helpers/plugin/set-default.ts`
|
|
446
|
+
- Modify: `bin/helpers/plugin.ts`
|
|
447
|
+
|
|
448
|
+
- [ ] **Step 1: Write the handler**
|
|
449
|
+
|
|
450
|
+
Create `bin/helpers/plugin/set-default.ts`:
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
import { callSkills, validateEnv } from '../billing-http';
|
|
454
|
+
import { confirmIfProd } from '../confirm-prompt';
|
|
455
|
+
|
|
456
|
+
interface SetDefaultArgs {
|
|
457
|
+
slug: string;
|
|
458
|
+
default: boolean;
|
|
459
|
+
yes: boolean;
|
|
460
|
+
env: string;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function parseArgs(argv: string[]): SetDefaultArgs {
|
|
464
|
+
if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
|
|
465
|
+
console.log(`Usage: optima-plugin set-default --slug <slug> --default true|false [options]
|
|
466
|
+
|
|
467
|
+
Required:
|
|
468
|
+
--slug <slug>
|
|
469
|
+
--default true|false Sets Plugin.defaultForUser
|
|
470
|
+
|
|
471
|
+
Optional:
|
|
472
|
+
--yes Skip prod confirmation prompt (no-op on stage)
|
|
473
|
+
--env stage|prod (default: stage)
|
|
474
|
+
|
|
475
|
+
Note: no skill-sync broadcast — changes what NEW user syncs receive; does not
|
|
476
|
+
retroactively add/remove the plugin for existing users until their next sync.`);
|
|
477
|
+
process.exit(0);
|
|
478
|
+
}
|
|
479
|
+
const out: Partial<SetDefaultArgs> = { env: 'stage', yes: false };
|
|
480
|
+
for (let i = 0; i < argv.length; i++) {
|
|
481
|
+
const a = argv[i];
|
|
482
|
+
const next = argv[i + 1];
|
|
483
|
+
switch (a) {
|
|
484
|
+
case '--slug': out.slug = next; i++; break;
|
|
485
|
+
case '--default':
|
|
486
|
+
if (next !== 'true' && next !== 'false') throw new Error('--default must be true or false');
|
|
487
|
+
out.default = next === 'true'; i++; break;
|
|
488
|
+
case '--yes': out.yes = true; break;
|
|
489
|
+
case '--env': out.env = next; i++; break;
|
|
490
|
+
default: throw new Error(`Unknown arg: ${a}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (!out.slug) throw new Error('--slug required');
|
|
494
|
+
if (out.default === undefined) throw new Error('--default required (true|false)');
|
|
495
|
+
return out as SetDefaultArgs;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export async function runSetDefault(argv: string[]): Promise<void> {
|
|
499
|
+
const args = parseArgs(argv);
|
|
500
|
+
validateEnv(args.env);
|
|
501
|
+
|
|
502
|
+
await confirmIfProd(
|
|
503
|
+
args.env,
|
|
504
|
+
`Action: set defaultForUser=${args.default} on plugin '${args.slug}' (${args.env.toUpperCase()})`,
|
|
505
|
+
args.yes,
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
console.log(`\n🔧 Setting defaultForUser=${args.default} on ${args.slug} (${args.env.toUpperCase()})...`);
|
|
509
|
+
const res = await callSkills(
|
|
510
|
+
args.env,
|
|
511
|
+
'PATCH',
|
|
512
|
+
`/api/admin/plugins/${encodeURIComponent(args.slug)}`,
|
|
513
|
+
{ defaultForUser: args.default },
|
|
514
|
+
);
|
|
515
|
+
console.log(`✓ Updated plugin (HTTP ${res.status}):`);
|
|
516
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
- [ ] **Step 2: Finalize dispatcher**
|
|
521
|
+
|
|
522
|
+
In `bin/helpers/plugin.ts` add import:
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
import { runSetDefault } from './plugin/set-default';
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
Replace the remaining stub block:
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
case 'set-default':
|
|
532
|
+
console.error(`Subcommand '${subcommand}' not yet implemented (added in a later task).`);
|
|
533
|
+
process.exit(1);
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
with:
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
case 'set-default': await runSetDefault(rest); break;
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Final switch should read:
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
switch (subcommand) {
|
|
546
|
+
case 'show': await runShow(rest); break;
|
|
547
|
+
case 'set-paid': await runSetPaid(rest); break;
|
|
548
|
+
case 'set-default': await runSetDefault(rest); break;
|
|
549
|
+
default:
|
|
550
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
551
|
+
printHelp();
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
- [ ] **Step 3: Build + smoke (read-back via returned row)**
|
|
557
|
+
|
|
558
|
+
```bash
|
|
559
|
+
npm run build
|
|
560
|
+
node dist/bin/helpers/plugin.js set-default --help
|
|
561
|
+
# Read scout's current defaultForUser by setting it to its own value is risky (unknown current);
|
|
562
|
+
# instead smoke on skillify: set true then confirm returned row, then leave as-is.
|
|
563
|
+
node dist/bin/helpers/plugin.js set-default --slug skillify --default true --env stage
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Expected: HTTP 200, returned row shows `defaultForUser: true`. (Records skillify's prior value from the returned row first if you want to restore; skillify as a free default plugin should be defaultForUser=true anyway.)
|
|
567
|
+
|
|
568
|
+
- [ ] **Step 4: Commit**
|
|
569
|
+
|
|
570
|
+
```bash
|
|
571
|
+
git add bin/helpers/plugin.ts bin/helpers/plugin/set-default.ts
|
|
572
|
+
git commit -m "feat(plugin): add set-default subcommand"
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
## Task 6: Register bin + AGENTS.md + version bump
|
|
578
|
+
|
|
579
|
+
**Files:**
|
|
580
|
+
- Modify: `package.json`
|
|
581
|
+
- Modify: `AGENTS.md`
|
|
582
|
+
|
|
583
|
+
- [ ] **Step 1: Add bin entry**
|
|
584
|
+
|
|
585
|
+
In `package.json` `bin`, add (alphabetical):
|
|
586
|
+
|
|
587
|
+
```json
|
|
588
|
+
"optima-plugin": "dist/bin/helpers/plugin.js",
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
(between `optima-grant-subscription` and `optima-product`.)
|
|
592
|
+
|
|
593
|
+
- [ ] **Step 2: Version bump**
|
|
594
|
+
|
|
595
|
+
```bash
|
|
596
|
+
npm version patch --no-git-tag-version
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
(0.7.35 → 0.7.36.)
|
|
600
|
+
|
|
601
|
+
- [ ] **Step 3: Update AGENTS.md**
|
|
602
|
+
|
|
603
|
+
In the "Primary Entry Points" list add:
|
|
604
|
+
|
|
605
|
+
```markdown
|
|
606
|
+
- `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)
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
- [ ] **Step 4: Build + commit**
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
npm run build
|
|
613
|
+
git add package.json AGENTS.md
|
|
614
|
+
git commit -m "chore: register optima-plugin bin entry + AGENTS.md (v0.7.36)"
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
## Task 7: End-to-end stage smoke (spec §8)
|
|
620
|
+
|
|
621
|
+
**Files:** none.
|
|
622
|
+
|
|
623
|
+
- [ ] **Step 1: Install locally + run the spec §8 sequence**
|
|
624
|
+
|
|
625
|
+
```bash
|
|
626
|
+
npm run build
|
|
627
|
+
node dist/bin/helpers/plugin.js show --slug scout --env stage # isPaid=false baseline
|
|
628
|
+
node dist/bin/helpers/plugin.js set-paid --slug scout --paid true --env stage # → 200 isPaid=true
|
|
629
|
+
node dist/bin/helpers/plugin.js show --slug scout --env stage # isPaid=true reflected
|
|
630
|
+
node dist/bin/helpers/plugin.js set-paid --slug skillify --paid false --env stage # skillify stays free
|
|
631
|
+
node dist/bin/helpers/plugin.js set-default --slug nonexistent-xyz --default true --env stage # → exit 1, 404
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
Expected: each as annotated. **Note**: step 2 deliberately makes scout paid on stage — that is the intended end state (the operator's goal), not throwaway data. Leave scout `isPaid=true` after smoke. (skillify set-paid false is a no-op — already false.)
|
|
635
|
+
|
|
636
|
+
- [ ] **Step 2: Record results / any divergence**
|
|
637
|
+
|
|
638
|
+
If any response shape, error envelope, or field differs from spec, append `~/.claude/projects/-mnt-d-work-projects-optima/memory/marketplace_admin_cli_smoke_notes.md`. Else skip (silent green).
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
## Task 8: PR
|
|
643
|
+
|
|
644
|
+
**Files:** none.
|
|
645
|
+
|
|
646
|
+
- [ ] **Step 1: Push + PR**
|
|
647
|
+
|
|
648
|
+
```bash
|
|
649
|
+
git push -u origin spec/optima-plugin-cli
|
|
650
|
+
gh pr create --base main --title "feat: optima-plugin CLI (skills-side isPaid/defaultForUser)" --body "$(cat <<'EOF'
|
|
651
|
+
## Summary
|
|
652
|
+
Adds `optima-plugin` (show / set-paid / set-default) — the skills-side admin command the marketplace-admin-cli (#11) missed. Flips `Plugin.isPaid` / `defaultForUser` (the actual user-facing paid/free gate) via skills `PATCH /api/admin/plugins/:slug`, reusing the dev-skills M2M token. Refactors billing-http into a shared `callService` core (`callBilling` unchanged + new `callSkills`).
|
|
653
|
+
|
|
654
|
+
## Spec + plan
|
|
655
|
+
- Spec: `docs/superpowers/specs/2026-05-25-optima-plugin-cli-design.md` (2 review rounds)
|
|
656
|
+
- Plan: `docs/superpowers/plans/2026-05-25-optima-plugin-cli-impl.md`
|
|
657
|
+
|
|
658
|
+
## Smoke (stage)
|
|
659
|
+
show/set-paid/set-default all green; billing regression smoke (`optima-entitlement list`) green post-refactor; scout flipped isPaid=true (intended).
|
|
660
|
+
|
|
661
|
+
## Follow-ups (spec §7)
|
|
662
|
+
- skills `GET /api/admin/plugins/:slug` (admin read for defaultForUser/status/trustLevel + non-ACTIVE)
|
|
663
|
+
- skills patchSchema: add salesUrl (so set-paid could set a custom sales page without re-publish)
|
|
664
|
+
|
|
665
|
+
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
|
666
|
+
EOF
|
|
667
|
+
)"
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
- [ ] **Step 2: (branch note)** This plan lives on `spec/optima-plugin-cli` (spec + plan + impl on one branch, like the main CLI). PR base = main.
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## Self-review
|
|
675
|
+
|
|
676
|
+
| Spec section | Task |
|
|
677
|
+
|---|---|
|
|
678
|
+
| §5.1 show | T3 |
|
|
679
|
+
| §5.2 set-paid (no --sales-url, reminder on paid=true) | T4 |
|
|
680
|
+
| §5.3 set-default (sync note) | T5 |
|
|
681
|
+
| §4 callService refactor (token-mint moves in, generic error string, callBilling byte-identical, callSkills added) | T2 |
|
|
682
|
+
| §6 token reuse / error handling / exit 1 | T2 (callService) + handlers |
|
|
683
|
+
| §8 smoke (incl negative 404) | T7 |
|
|
684
|
+
| §10 prod URL open Q | T1 |
|
|
685
|
+
| §7 follow-ups | PR body T8 |
|
|
686
|
+
|
|
687
|
+
**Type/signature consistency:**
|
|
688
|
+
- `callSkills(env, method, path, body?)` — used in show/set-paid/set-default identically. ✓
|
|
689
|
+
- `callBilling` signature unchanged → product/* + entitlement/* unaffected. ✓
|
|
690
|
+
- `validateEnv(env)` first line of every runX. ✓
|
|
691
|
+
- `confirmIfProd(env, action, yes)` in set-paid + set-default. ✓
|
|
692
|
+
- PATCH bodies: `{isPaid: bool}` (T4), `{defaultForUser: bool}` (T5) — both within skills `.strict()` patchSchema. ✓
|
|
693
|
+
|
|
694
|
+
**Placeholder scan:** T1 records prod URL inline (resolved at exec). No TBD/TODO. Smoke uses real slugs (scout/skillify) + a deliberate nonexistent slug for the negative case. ✓
|
|
695
|
+
|
|
696
|
+
---
|
|
697
|
+
|
|
698
|
+
## Execution handoff
|
|
699
|
+
|
|
700
|
+
Plan saved. Subagent-driven execution (per main-CLI precedent): fresh subagent per task + per-task review where substantive, combined review where plan-pinned. T2 (refactor) gets careful review since it touches shipped billing code.
|