@shipeasy/openapi 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/index.d.ts +24 -0
  2. package/dist/index.js +1924 -3
  3. package/dist/node-context.d.ts +37 -0
  4. package/dist/node-context.js +56 -0
  5. package/dist/operations/attributes.d.ts +33 -0
  6. package/dist/operations/attributes.js +16 -0
  7. package/dist/operations/cli-adapter.d.ts +35 -0
  8. package/dist/operations/cli-adapter.js +91 -0
  9. package/dist/operations/coerce.d.ts +32 -0
  10. package/dist/operations/coerce.js +53 -0
  11. package/dist/operations/configs.d.ts +28 -0
  12. package/dist/operations/configs.js +119 -0
  13. package/dist/operations/docs.d.ts +37 -0
  14. package/dist/operations/docs.js +39 -0
  15. package/dist/operations/events.d.ts +28 -0
  16. package/dist/operations/events.js +121 -0
  17. package/dist/operations/experiments.d.ts +28 -0
  18. package/dist/operations/experiments.js +280 -0
  19. package/dist/operations/gates.d.ts +28 -0
  20. package/dist/operations/gates.js +132 -0
  21. package/dist/operations/i18n.d.ts +28 -0
  22. package/dist/operations/i18n.js +121 -0
  23. package/dist/operations/index.d.ts +63 -0
  24. package/dist/operations/index.js +1751 -0
  25. package/dist/operations/killswitches.d.ts +28 -0
  26. package/dist/operations/killswitches.js +126 -0
  27. package/dist/operations/mcp-adapter.d.ts +35 -0
  28. package/dist/operations/mcp-adapter.js +94 -0
  29. package/dist/operations/metrics.d.ts +30 -0
  30. package/dist/operations/metrics.js +114 -0
  31. package/dist/operations/ops.d.ts +28 -0
  32. package/dist/operations/ops.js +313 -0
  33. package/dist/operations/projects.d.ts +28 -0
  34. package/dist/operations/projects.js +36 -0
  35. package/dist/operations/sdk-docs.d.ts +28 -0
  36. package/dist/operations/sdk-docs.js +160 -0
  37. package/dist/operations/types.d.ts +138 -0
  38. package/dist/operations/types.js +9 -0
  39. package/dist/operations/universes.d.ts +28 -0
  40. package/dist/operations/universes.js +93 -0
  41. package/dist/operations/util.d.ts +33 -0
  42. package/dist/operations/util.js +9 -0
  43. package/dist/resources/alert-rules.d.ts +3 -1
  44. package/dist/resources/alert-rules.js +11 -2
  45. package/dist/resources/attributes.d.ts +19 -0
  46. package/dist/resources/attributes.js +10 -0
  47. package/dist/resources/events.d.ts +61 -0
  48. package/dist/resources/events.js +42 -0
  49. package/dist/resources/i18n.d.ts +84 -0
  50. package/dist/resources/i18n.js +60 -0
  51. package/dist/resources/index.d.ts +22 -4
  52. package/dist/resources/index.js +184 -3
  53. package/dist/resources/metrics.d.ts +54 -0
  54. package/dist/resources/metrics.js +40 -0
  55. package/dist/resources/ops.d.ts +88 -0
  56. package/dist/resources/ops.js +36 -0
  57. package/dist/resources/projects.d.ts +54 -0
  58. package/dist/resources/projects.js +14 -0
  59. package/dist/schemas/alert-rules.d.ts +23 -1
  60. package/dist/schemas/alert-rules.js +13 -3
  61. package/dist/schemas/index.d.ts +1 -1
  62. package/dist/schemas/index.js +12 -2
  63. package/package.json +10 -1
@@ -0,0 +1,1751 @@
1
+ // src/operations/types.ts
2
+ var opId = (op) => [...op.group, op.name].join(".");
3
+ var opMcpName = (op) => [...op.group, op.name].join("_");
4
+ var opCli = (op) => [...op.group, op.name].join(" ");
5
+
6
+ // src/transport.ts
7
+ var ApiError = class extends Error {
8
+ constructor(message, status) {
9
+ super(message);
10
+ this.status = status;
11
+ this.name = "ApiError";
12
+ }
13
+ status;
14
+ };
15
+
16
+ // src/operations/coerce.ts
17
+ function coerceValue(param, raw) {
18
+ if (raw === void 0 || raw === null) return void 0;
19
+ switch (param.type) {
20
+ case "number": {
21
+ const n = typeof raw === "number" ? raw : Number(raw);
22
+ if (!Number.isFinite(n)) throw new ApiError(`--${param.name} must be a number`, 400);
23
+ return n;
24
+ }
25
+ case "boolean":
26
+ return raw === true || raw === "true" || raw === "1";
27
+ case "json":
28
+ if (typeof raw !== "string") return raw;
29
+ try {
30
+ return JSON.parse(raw);
31
+ } catch {
32
+ throw new ApiError(`--${param.name} must be valid JSON`, 400);
33
+ }
34
+ case "string":
35
+ default: {
36
+ const s = String(raw);
37
+ if (param.enum && !param.enum.includes(s)) {
38
+ throw new ApiError(`--${param.name} must be one of: ${param.enum.join(", ")}`, 400);
39
+ }
40
+ return s;
41
+ }
42
+ }
43
+ }
44
+ function coerceInput(op, raw) {
45
+ const out = {};
46
+ for (const param of op.params) {
47
+ const value = coerceValue(param, raw[param.name]);
48
+ if (value === void 0) {
49
+ if (param.required) throw new ApiError(`${param.name} is required`, 400);
50
+ continue;
51
+ }
52
+ out[param.name] = value;
53
+ }
54
+ return out;
55
+ }
56
+
57
+ // src/operations/cli-adapter.ts
58
+ var toKebab = (s) => s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
59
+ function commandSignature(op) {
60
+ const positionals = op.params.filter((p) => p.positional).map((p) => p.required === false ? `[${p.name}]` : `<${p.name}>`);
61
+ return [op.name, ...positionals].join(" ");
62
+ }
63
+ function mountOperations(group, ops, ctx) {
64
+ for (const op of ops) {
65
+ const positionals = op.params.filter((p) => p.positional);
66
+ const flags = op.params.filter((p) => !p.positional);
67
+ const cmd = group.command(commandSignature(op)).description(op.summary);
68
+ for (const p of flags) {
69
+ cmd.option(`--${toKebab(p.name)} <value>`, p.description);
70
+ }
71
+ cmd.option("--json", "Output as JSON");
72
+ cmd.option("--project <id>", "Project ID override");
73
+ cmd.action(async (...argv) => {
74
+ try {
75
+ const opts = argv.find((a) => a && typeof a === "object" && !Array.isArray(a)) ?? {};
76
+ const raw = { ...opts };
77
+ positionals.forEach((p, i) => {
78
+ raw[p.name] = argv[i];
79
+ });
80
+ const input = coerceInput(op, raw);
81
+ const client = ctx.getClient({
82
+ mutates: op.mutates,
83
+ projectOverride: opts.project
84
+ });
85
+ const data = await op.run(client, input);
86
+ if (opts.json) ctx.printJson(data);
87
+ else ctx.printResult(op, data, input);
88
+ } catch (e) {
89
+ ctx.onError(e);
90
+ }
91
+ });
92
+ }
93
+ }
94
+
95
+ // src/operations/mcp-adapter.ts
96
+ function jsonType(t) {
97
+ switch (t) {
98
+ case "number":
99
+ return "number";
100
+ case "boolean":
101
+ return "boolean";
102
+ case "json":
103
+ case "string":
104
+ default:
105
+ return "string";
106
+ }
107
+ }
108
+ function operationsToMcpTools(ops) {
109
+ return ops.map((op) => {
110
+ const properties = {};
111
+ const required = [];
112
+ for (const p of op.params) {
113
+ properties[p.name] = { type: jsonType(p.type), description: p.description };
114
+ if (p.enum) properties[p.name].enum = p.enum;
115
+ if (p.required) required.push(p.name);
116
+ }
117
+ return {
118
+ name: opMcpName(op),
119
+ description: op.description,
120
+ inputSchema: { type: "object", ...required.length ? { required } : {}, properties }
121
+ };
122
+ });
123
+ }
124
+ function operationsToDispatch(ops) {
125
+ const map = {};
126
+ for (const op of ops) {
127
+ map[opMcpName(op)] = async (client, args) => op.run(client, coerceInput(op, args ?? {}));
128
+ }
129
+ return map;
130
+ }
131
+
132
+ // src/operations/docs.ts
133
+ function renderOperationsMarkdown(ops, opts = {}) {
134
+ const lines = [];
135
+ if (opts.title) lines.push(`# ${opts.title}`, "");
136
+ for (const op of ops) {
137
+ lines.push(`## \`${opId(op)}\``, "");
138
+ lines.push(op.summary, "");
139
+ if (op.description && op.description !== op.summary) lines.push(op.description, "");
140
+ lines.push(
141
+ `- **CLI:** \`shipeasy ${opCli(op)}\` \xB7 **MCP tool:** \`${opMcpName(op)}\` \xB7 ${op.mutates ? "write (needs project binding)" : "read"}`,
142
+ ""
143
+ );
144
+ if (op.params.length) {
145
+ lines.push("| Param | Type | Required | Description |", "| --- | --- | --- | --- |");
146
+ for (const p of op.params) {
147
+ const kind = p.positional ? `${p.type} (positional)` : p.type;
148
+ lines.push(`| \`${p.name}\` | ${kind} | ${p.required ? "yes" : "no"} | ${p.description} |`);
149
+ }
150
+ lines.push("");
151
+ }
152
+ if (op.examples?.length) {
153
+ lines.push("**Examples**", "");
154
+ for (const ex of op.examples) {
155
+ if (ex.note) lines.push(`- ${ex.note}:`);
156
+ lines.push("```sh", ex.run, "```");
157
+ }
158
+ lines.push("");
159
+ }
160
+ }
161
+ return lines.join("\n");
162
+ }
163
+
164
+ // src/operations/util.ts
165
+ var num = (i, k) => i[k] === void 0 ? void 0 : i[k];
166
+ var str = (i, k) => i[k] === void 0 ? void 0 : i[k];
167
+ var bool = (i, k) => i[k] === void 0 ? void 0 : i[k];
168
+
169
+ // src/operations/gates.ts
170
+ var ROLLOUT_DESC = "Rollout percentage, 0\u2013100 (converted to basis points internally).";
171
+ var RULES_DESC = `Targeting rules as a JSON array, e.g. '[{"attr":"plan","op":"eq","value":"pro"}]'. AND-combined.`;
172
+ var STACK_DESC = "Gatekeeper stack as a JSON array of ordered first-match-wins tiers (condition/rollout steps, rolloutPct in basis points). Takes precedence over flat rules/rollout. Pass to express internal \u222A beta \u222A public in one gate.";
173
+ var gateOperations = [
174
+ {
175
+ group: ["release", "flags"],
176
+ name: "list",
177
+ mutates: false,
178
+ summary: "List all feature gates",
179
+ description: "Return every feature gate in the project (name, enabled, rollout %), ordered by most recently updated.",
180
+ params: [],
181
+ examples: [{ run: "shipeasy release flags list" }],
182
+ run: (client) => client.gates.listAll()
183
+ },
184
+ {
185
+ group: ["release", "flags"],
186
+ name: "create",
187
+ mutates: true,
188
+ summary: "Create a feature gate",
189
+ description: "Create a feature gate. A flat gate is `rules` + a single `rollout` %. For tiered rollouts (e.g. 'US @ 50% \u2192 bots @ 30% \u2192 everyone else @ 10%') pass `stack` \u2014 an ordered list of steps evaluated first-match-wins; flat `rules`/`rollout` are then ignored.",
190
+ params: [
191
+ { name: "name", type: "string", description: "Gate key (lowercase, digits, _/-).", required: true, positional: true },
192
+ { name: "rollout", type: "number", description: ROLLOUT_DESC },
193
+ { name: "rules", type: "json", description: RULES_DESC },
194
+ { name: "salt", type: "string", description: "Override hash salt (immutable after create)." },
195
+ { name: "stack", type: "json", description: STACK_DESC }
196
+ ],
197
+ examples: [
198
+ { note: "Off by default; flip it on later", run: "shipeasy release flags create checkout-v2" },
199
+ {
200
+ note: "Roll out to 25% of pro-plan users",
201
+ run: `shipeasy release flags create new-ui --rollout 25 --rules '[{"attr":"plan","op":"eq","value":"pro"}]'`
202
+ }
203
+ ],
204
+ run: (client, i) => client.gates.create({
205
+ name: i.name,
206
+ rollout_pct: Math.round((num(i, "rollout") ?? 0) * 100),
207
+ // Validated at runtime by gateCreateSchema.parse inside gates.create().
208
+ rules: i.rules ?? [],
209
+ ...i.salt ? { salt: i.salt } : {},
210
+ ...i.stack ? { stack: i.stack } : {}
211
+ })
212
+ },
213
+ {
214
+ group: ["release", "flags"],
215
+ name: "update",
216
+ mutates: true,
217
+ summary: "Update a gate's rollout, rules, stack, or enabled flag",
218
+ description: "Update a feature gate's rollout, rules, gatekeeper stack, or enabled flag. Pass `stack` to replace the tiered rollout wholesale; array fields replace, they do not merge.",
219
+ params: [
220
+ { name: "name", type: "string", description: "Gate name to update.", required: true, positional: true },
221
+ { name: "rollout", type: "number", description: ROLLOUT_DESC },
222
+ { name: "rules", type: "json", description: RULES_DESC + " Replaces the existing rules wholesale." },
223
+ { name: "stack", type: "json", description: STACK_DESC + " Pass null to revert to flat evaluation." },
224
+ { name: "enabled", type: "boolean", description: "Master switch \u2014 false forces the gate off for every caller." }
225
+ ],
226
+ examples: [{ note: "Ramp to 50%", run: "shipeasy release flags update checkout-v2 --rollout 50" }],
227
+ run: async (client, i) => {
228
+ const gate = await client.gates.resolve(i.name);
229
+ const patch = {};
230
+ const rollout = num(i, "rollout");
231
+ if (rollout !== void 0) patch.rollout_pct = Math.round(rollout * 100);
232
+ if (i.rules !== void 0) patch.rules = i.rules;
233
+ if (i.stack !== void 0) patch.stack = i.stack;
234
+ if (i.enabled !== void 0) patch.enabled = i.enabled;
235
+ return client.gates.update(gate.id, patch);
236
+ }
237
+ },
238
+ {
239
+ group: ["release", "flags"],
240
+ name: "enable",
241
+ mutates: true,
242
+ summary: "Enable a feature gate",
243
+ description: "Set `enabled: true`, preserving the current rollout %.",
244
+ params: [{ name: "name", type: "string", description: "Gate name.", required: true, positional: true }],
245
+ examples: [{ run: "shipeasy release flags enable checkout-v2" }],
246
+ run: async (client, i) => {
247
+ const gate = await client.gates.resolve(i.name);
248
+ return client.gates.enable(gate.id);
249
+ }
250
+ },
251
+ {
252
+ group: ["release", "flags"],
253
+ name: "disable",
254
+ mutates: true,
255
+ summary: "Disable a feature gate (kill switch)",
256
+ description: "Set `enabled: false` so the gate evaluates to `false` for every caller regardless of rules/rollout.",
257
+ params: [{ name: "name", type: "string", description: "Gate name.", required: true, positional: true }],
258
+ examples: [{ run: "shipeasy release flags disable checkout-v2" }],
259
+ run: async (client, i) => {
260
+ const gate = await client.gates.resolve(i.name);
261
+ return client.gates.disable(gate.id);
262
+ }
263
+ },
264
+ {
265
+ group: ["release", "flags"],
266
+ name: "rollout",
267
+ mutates: true,
268
+ summary: "Set rollout percentage (0\u2013100)",
269
+ description: "Resolve the gate by name and set its rollout percentage (0\u2013100).",
270
+ params: [
271
+ { name: "name", type: "string", description: "Gate name.", required: true, positional: true },
272
+ { name: "pct", type: "number", description: "Rollout percentage, 0\u2013100.", required: true, positional: true }
273
+ ],
274
+ examples: [
275
+ { note: "Ramp to 50%", run: "shipeasy release flags rollout checkout-v2 50" },
276
+ { note: "Instant kill \u2014 set to 0", run: "shipeasy release flags rollout checkout-v2 0" }
277
+ ],
278
+ run: (client, i) => client.gates.setRollout(i.name, num(i, "pct"))
279
+ },
280
+ {
281
+ group: ["release", "flags"],
282
+ name: "archive",
283
+ mutates: true,
284
+ summary: "Archive a feature gate",
285
+ description: "Archive (soft-delete) a feature gate by name. Refuses if a running experiment references it as a targeting gate.",
286
+ params: [{ name: "name", type: "string", description: "Gate name.", required: true, positional: true }],
287
+ examples: [{ run: "shipeasy release flags archive checkout-v2" }],
288
+ run: async (client, i) => {
289
+ const gate = await client.gates.resolve(i.name);
290
+ await client.gates.delete(gate.id);
291
+ return { ok: true, archived: i.name };
292
+ }
293
+ }
294
+ ];
295
+
296
+ // src/operations/killswitches.ts
297
+ var GROUP = ["release", "killswitch"];
298
+ var ENVS = ["dev", "staging", "prod"];
299
+ var killswitchOperations = [
300
+ {
301
+ group: GROUP,
302
+ name: "list",
303
+ mutates: false,
304
+ summary: "List all kill switches",
305
+ description: "Return every kill switch with its per-env default value and switch count.",
306
+ params: [],
307
+ examples: [{ run: "shipeasy release killswitch list" }],
308
+ run: (client) => client.killswitches.listAll()
309
+ },
310
+ {
311
+ group: GROUP,
312
+ name: "create",
313
+ mutates: true,
314
+ summary: "Create a kill switch",
315
+ description: "Create a kill switch (a static `{ value, switches }` config shipped in the flags KV blob). Name must be `folder.name`. Switches take precedence over `value` for that switch_key.",
316
+ params: [
317
+ { name: "name", type: "string", description: "`folder.name` \u2014 two lowercase segments.", required: true, positional: true },
318
+ { name: "description", type: "string", description: "Optional description." },
319
+ { name: "value", type: "boolean", description: "Default value (true|false). Defaults to false." },
320
+ { name: "switches", type: "json", description: "JSON object of { switch_key: bool }; takes precedence over value." }
321
+ ],
322
+ examples: [
323
+ { note: "Default OFF", run: "shipeasy release killswitch create payments.stripe-gateway" },
324
+ { note: "Pre-seed per-key switches", run: `shipeasy release killswitch create payments.stripe-gateway --switches '{"refunds":true,"payouts":false}'` }
325
+ ],
326
+ run: (client, i) => client.killswitches.create({
327
+ name: i.name,
328
+ description: str(i, "description"),
329
+ value: bool(i, "value") ?? false,
330
+ switches: i.switches
331
+ })
332
+ },
333
+ {
334
+ group: GROUP,
335
+ name: "update",
336
+ mutates: true,
337
+ summary: "Update a kill switch's default value, switches map, or description",
338
+ description: "Patch a kill switch. `switches` replaces the map wholesale; pass an empty description to clear it.",
339
+ params: [
340
+ { name: "name", type: "string", description: "Kill switch name.", required: true, positional: true },
341
+ { name: "value", type: "boolean", description: "Default value (true|false)." },
342
+ { name: "switches", type: "json", description: "JSON { switch_key: bool } \u2014 replaces wholesale." },
343
+ { name: "description", type: "string", description: "Description (empty string clears it)." }
344
+ ],
345
+ examples: [{ note: "Flip the default ON", run: "shipeasy release killswitch update payments.stripe-gateway --value true" }],
346
+ run: async (client, i) => {
347
+ const k = await client.killswitches.resolve(i.name);
348
+ const patch = {};
349
+ if (i.value !== void 0) patch.value = i.value;
350
+ if (i.switches !== void 0) patch.switches = i.switches;
351
+ if (i.description !== void 0) patch.description = i.description === "" ? null : i.description;
352
+ return client.killswitches.update(k.id, patch);
353
+ }
354
+ },
355
+ {
356
+ group: GROUP,
357
+ name: "set",
358
+ mutates: true,
359
+ summary: "Set one switch entry on one env",
360
+ description: "Set or update a single named switch on one environment (default `prod`), leaving the rest untouched.",
361
+ params: [
362
+ { name: "name", type: "string", description: "Kill switch name.", required: true, positional: true },
363
+ { name: "switchKey", type: "string", description: "Switch key to set.", required: true, positional: true },
364
+ { name: "value", type: "boolean", description: "true|false.", required: true, positional: true },
365
+ { name: "env", type: "string", description: `Env: ${ENVS.join(" | ")} (default prod).`, enum: ENVS }
366
+ ],
367
+ examples: [
368
+ { note: "Kill refunds in prod", run: "shipeasy release killswitch set payments.stripe-gateway refunds true" },
369
+ { note: "Target staging instead", run: "shipeasy release killswitch set payments.stripe-gateway refunds true --env staging" }
370
+ ],
371
+ run: async (client, i) => {
372
+ const k = await client.killswitches.resolve(i.name);
373
+ return client.killswitches.setSwitch(k.id, {
374
+ env: str(i, "env") ?? "prod",
375
+ switchKey: i.switchKey,
376
+ value: i.value
377
+ });
378
+ }
379
+ },
380
+ {
381
+ group: GROUP,
382
+ name: "unset",
383
+ mutates: true,
384
+ summary: "Remove one switch entry from one env",
385
+ description: "Remove a single named switch from one environment (default `prod`), reverting it to the default.",
386
+ params: [
387
+ { name: "name", type: "string", description: "Kill switch name.", required: true, positional: true },
388
+ { name: "switchKey", type: "string", description: "Switch key to remove.", required: true, positional: true },
389
+ { name: "env", type: "string", description: `Env: ${ENVS.join(" | ")} (default prod).`, enum: ENVS }
390
+ ],
391
+ examples: [{ run: "shipeasy release killswitch unset payments.stripe-gateway refunds" }],
392
+ run: async (client, i) => {
393
+ const k = await client.killswitches.resolve(i.name);
394
+ return client.killswitches.unsetSwitch(k.id, {
395
+ env: str(i, "env") ?? "prod",
396
+ switchKey: i.switchKey
397
+ });
398
+ }
399
+ },
400
+ {
401
+ group: GROUP,
402
+ name: "archive",
403
+ mutates: true,
404
+ summary: "Archive a kill switch",
405
+ description: "Archive (soft-delete) a kill switch by name.",
406
+ params: [{ name: "name", type: "string", description: "Kill switch name.", required: true, positional: true }],
407
+ examples: [{ run: "shipeasy release killswitch archive payments.stripe-gateway" }],
408
+ run: async (client, i) => {
409
+ const k = await client.killswitches.resolve(i.name);
410
+ await client.killswitches.delete(k.id);
411
+ return { ok: true, archived: i.name };
412
+ }
413
+ }
414
+ ];
415
+
416
+ // src/operations/configs.ts
417
+ var GROUP2 = ["release", "configs"];
418
+ var configOperations = [
419
+ {
420
+ group: GROUP2,
421
+ name: "list",
422
+ mutates: false,
423
+ summary: "List all configs",
424
+ description: "Return every dynamic config (name, schema, last updated).",
425
+ params: [],
426
+ examples: [{ run: "shipeasy release configs list" }],
427
+ run: (client) => client.configs.listAll()
428
+ },
429
+ {
430
+ group: GROUP2,
431
+ name: "get",
432
+ mutates: false,
433
+ summary: "Show a config by name",
434
+ description: "Resolve a config by name and return its full detail (schema + per-env values + drafts).",
435
+ params: [{ name: "name", type: "string", description: "Config name.", required: true, positional: true }],
436
+ examples: [{ run: "shipeasy release configs get pricing" }],
437
+ run: async (client, i) => {
438
+ const c = await client.configs.resolve(i.name);
439
+ return client.configs.get(c.id);
440
+ }
441
+ },
442
+ {
443
+ group: GROUP2,
444
+ name: "create",
445
+ mutates: true,
446
+ summary: "Create a config",
447
+ description: "Create a dynamic config. Configs are JSON objects validated against a JSON Schema (object-only).",
448
+ params: [
449
+ { name: "name", type: "string", description: "Config name.", required: true, positional: true },
450
+ { name: "schema", type: "json", description: "JSON Schema (top-level type=object). Defaults to a permissive object schema." },
451
+ { name: "value", type: "json", description: "Initial value as a JSON object. Defaults to {}." },
452
+ { name: "description", type: "string", description: "Human-readable description." }
453
+ ],
454
+ examples: [
455
+ { note: "Permissive schema, empty value", run: "shipeasy release configs create pricing" },
456
+ { note: "Seed an initial value", run: `shipeasy release configs create pricing --value '{"free_trial_days":14}'` }
457
+ ],
458
+ run: (client, i) => client.configs.create({
459
+ name: i.name,
460
+ schema: i.schema ?? { type: "object", properties: {}, additionalProperties: true },
461
+ value: i.value ?? {},
462
+ ...i.description ? { description: i.description } : {}
463
+ })
464
+ },
465
+ {
466
+ group: GROUP2,
467
+ name: "update",
468
+ mutates: true,
469
+ summary: "Update a config's value (flat, all envs)",
470
+ description: "Legacy flat value update across all envs. Use draft + publish for per-env staging.",
471
+ params: [
472
+ { name: "name", type: "string", description: "Config name.", required: true, positional: true },
473
+ { name: "value", type: "json", description: "New value (JSON object).", required: true }
474
+ ],
475
+ examples: [{ note: "Overwrite the value (all envs)", run: `shipeasy release configs update pricing --value '{"free_trial_days":30}'` }],
476
+ run: async (client, i) => {
477
+ const c = await client.configs.resolve(i.name);
478
+ return client.configs.update(c.id, { value: i.value });
479
+ }
480
+ },
481
+ {
482
+ group: GROUP2,
483
+ name: "draft",
484
+ mutates: true,
485
+ summary: "Save a draft value for one env",
486
+ description: "Stage a draft value for the given env, to publish later.",
487
+ params: [
488
+ { name: "name", type: "string", description: "Config name.", required: true, positional: true },
489
+ { name: "env", type: "string", description: "Target env (e.g. prod, staging, dev).", required: true },
490
+ { name: "value", type: "json", description: "Draft value (JSON object).", required: true }
491
+ ],
492
+ examples: [{ note: "Stage a prod value", run: `shipeasy release configs draft pricing --env prod --value '{"free_trial_days":30}'` }],
493
+ run: async (client, i) => {
494
+ const c = await client.configs.resolve(i.name);
495
+ return client.configs.saveDraft(c.id, { env: i.env, value: i.value });
496
+ }
497
+ },
498
+ {
499
+ group: GROUP2,
500
+ name: "publish",
501
+ mutates: true,
502
+ summary: "Publish the saved draft for one env",
503
+ description: "Promote the staged draft for the given env to live.",
504
+ params: [
505
+ { name: "name", type: "string", description: "Config name.", required: true, positional: true },
506
+ { name: "env", type: "string", description: "Target env to publish.", required: true }
507
+ ],
508
+ examples: [{ note: "Promote staged draft to prod", run: "shipeasy release configs publish pricing --env prod" }],
509
+ run: async (client, i) => {
510
+ const c = await client.configs.resolve(i.name);
511
+ return client.configs.publish(c.id, { env: str(i, "env") });
512
+ }
513
+ },
514
+ {
515
+ group: GROUP2,
516
+ name: "archive",
517
+ mutates: true,
518
+ summary: "Archive a config",
519
+ description: "Archive (soft-delete) a config by name.",
520
+ params: [{ name: "name", type: "string", description: "Config name.", required: true, positional: true }],
521
+ examples: [{ run: "shipeasy release configs archive pricing" }],
522
+ run: async (client, i) => {
523
+ const c = await client.configs.resolve(i.name);
524
+ await client.configs.delete(c.id);
525
+ return { ok: true, archived: i.name };
526
+ }
527
+ }
528
+ ];
529
+
530
+ // src/operations/experiments.ts
531
+ var GROUP3 = ["release", "experiments"];
532
+ var DEFAULT_GROUPS = [
533
+ { name: "control", weight: 5e3, params: {} },
534
+ { name: "test", weight: 5e3, params: {} }
535
+ ];
536
+ var AGGREGATIONS = [
537
+ "count_users",
538
+ "count_events",
539
+ "retention_7d",
540
+ "retention_30d",
541
+ "sum",
542
+ "avg"
543
+ ];
544
+ function buildGoalMetric(i) {
545
+ const event = str(i, "successEvent");
546
+ if (!event) return void 0;
547
+ const agg = str(i, "successAggregation") ?? "count_users";
548
+ const value = str(i, "successValue");
549
+ switch (agg) {
550
+ case "count_users":
551
+ return { query: `count_users(${event})` };
552
+ case "count_events":
553
+ return { query: `count(${event})` };
554
+ case "retention_7d":
555
+ return { query: `retention_7d(${event})` };
556
+ case "retention_30d":
557
+ return { query: `retention_30d(${event})` };
558
+ case "sum":
559
+ case "avg":
560
+ if (!value)
561
+ throw new Error(
562
+ `successAggregation '${agg}' requires successValue \u2014 the numeric event property to ${agg} (e.g. "amount")`
563
+ );
564
+ return { query: `${agg}(${event}, ${value})` };
565
+ default:
566
+ throw new Error(`Unknown successAggregation '${agg}'`);
567
+ }
568
+ }
569
+ function parseGuardrails(raw) {
570
+ if (!Array.isArray(raw)) return [];
571
+ const out = [];
572
+ for (const entry of raw) {
573
+ if (typeof entry === "string") {
574
+ const query = entry.trim();
575
+ if (query) out.push({ query });
576
+ } else if (entry && typeof entry === "object") {
577
+ const o = entry;
578
+ const query = typeof o.query === "string" ? o.query.trim() : "";
579
+ if (query)
580
+ out.push(typeof o.name === "string" && o.name ? { query, name: o.name } : { query });
581
+ }
582
+ }
583
+ return out;
584
+ }
585
+ function computeVerdict(detail, results) {
586
+ if (results.some((r) => r.srm_detected === 1))
587
+ return { verdict: "invalid_srm", reason: "sample-ratio mismatch detected" };
588
+ if (detail.status !== "running") return { verdict: "not_running", reason: `status=${detail.status}` };
589
+ if (!results.length) return { verdict: "wait", reason: "no data yet" };
590
+ const withP = results.filter((r) => r.p_value !== null);
591
+ if (!withP.length) return { verdict: "wait", reason: "no p-values yet" };
592
+ const best = withP.reduce((a, b) => a.p_value < b.p_value ? a : b);
593
+ const threshold = detail.significance_threshold ?? 0.05;
594
+ if (best.p_value < threshold) return { verdict: "ship" };
595
+ const startedAt = detail.startedAt ?? detail.started_at;
596
+ const daysRunning = startedAt ? (Date.now() - new Date(startedAt).getTime()) / (1e3 * 60 * 60 * 24) : 0;
597
+ return daysRunning < (detail.min_runtime_days ?? 0) ? { verdict: "wait", reason: "min runtime not reached" } : { verdict: "hold" };
598
+ }
599
+ var experimentOperations = [
600
+ {
601
+ group: GROUP3,
602
+ name: "list",
603
+ mutates: false,
604
+ summary: "List experiments",
605
+ description: "Return every experiment (status, universe, allocation).",
606
+ params: [],
607
+ examples: [{ run: "shipeasy release experiments list" }],
608
+ run: (client) => client.experiments.listAll()
609
+ },
610
+ {
611
+ group: GROUP3,
612
+ name: "create",
613
+ mutates: true,
614
+ summary: "Create an experiment",
615
+ description: "Create an experiment draft with groups, params, optional targeting gate, a success (goal) metric, optional guardrail metrics, and full statistical config. Pass `successEvent` (+ `successAggregation`) to attach the goal metric inline \u2014 required before the experiment can be started. Defaults to a 50/50 control-vs-test split at 100% allocation in the `default` universe. Does NOT start \u2014 call `start`.",
616
+ params: [
617
+ { name: "name", type: "string", description: "Experiment name.", required: true, positional: true },
618
+ { name: "universe", type: "string", description: "Universe name. Defaults to 'default'." },
619
+ { name: "allocation", type: "number", description: "Allocation percentage, 0\u2013100. Defaults to 100." },
620
+ { name: "groups", type: "json", description: "Groups as JSON [{name,weight,params}]. weight is basis points." },
621
+ { name: "params", type: "json", description: "Parameter schema {name: type}." },
622
+ { name: "targetingGate", type: "string", description: "Targeting gate name." },
623
+ { name: "bucketBy", type: "string", description: "Attribute to bucket by (defaults to the universe unit)." },
624
+ { name: "salt", type: "string", description: "Override hash salt." },
625
+ { name: "successEvent", type: "string", description: "Event name for the inline goal metric. Attaching one makes the draft startable." },
626
+ { name: "successAggregation", type: "string", description: `Goal-metric reducer. Defaults to count_users.`, enum: [...AGGREGATIONS] },
627
+ { name: "successValue", type: "string", description: "Numeric event property for sum/avg goal metrics." },
628
+ { name: "guardrailMetrics", type: "json", description: "Up to 10 guardrail metrics: JSON array of DSL query strings or {query,name}." },
629
+ { name: "significance", type: "number", description: "Significance threshold (0.0001\u20130.5). Defaults to 0.05." },
630
+ { name: "minRuntimeDays", type: "number", description: "Minimum runtime in days." },
631
+ { name: "minSampleSize", type: "number", description: "Minimum sample size." },
632
+ { name: "sequentialTesting", type: "boolean", description: "Enable sequential testing (always-valid p-values; Premium+)." },
633
+ { name: "description", type: "string", description: "Human-readable description." }
634
+ ],
635
+ examples: [
636
+ { note: "Default 50/50, full traffic", run: "shipeasy release experiments create pricing-page" },
637
+ {
638
+ note: "Attach a goal metric so it's startable",
639
+ run: "shipeasy release experiments create checkout-cta --universe web --success-event checkout_completed"
640
+ }
641
+ ],
642
+ run: (client, i) => {
643
+ const goal_metric = buildGoalMetric(i);
644
+ const guardrail_metrics = parseGuardrails(i.guardrailMetrics);
645
+ return client.experiments.create({
646
+ name: i.name,
647
+ universe: str(i, "universe") ?? "default",
648
+ allocation_pct: Math.round((num(i, "allocation") ?? 100) * 100),
649
+ groups: i.groups ?? DEFAULT_GROUPS,
650
+ params: i.params ?? {},
651
+ targeting_gate: str(i, "targetingGate") ?? null,
652
+ ...str(i, "bucketBy") ? { bucket_by: str(i, "bucketBy") } : {},
653
+ ...str(i, "description") !== void 0 ? { description: str(i, "description") } : {},
654
+ ...i.salt ? { salt: i.salt } : {},
655
+ ...num(i, "significance") !== void 0 ? { significance_threshold: num(i, "significance") } : {},
656
+ ...num(i, "minRuntimeDays") !== void 0 ? { min_runtime_days: num(i, "minRuntimeDays") } : {},
657
+ ...num(i, "minSampleSize") !== void 0 ? { min_sample_size: num(i, "minSampleSize") } : {},
658
+ ...bool(i, "sequentialTesting") !== void 0 ? { sequential_testing: bool(i, "sequentialTesting") } : {},
659
+ ...goal_metric ? { goal_metric } : {},
660
+ ...guardrail_metrics.length ? { guardrail_metrics } : {}
661
+ });
662
+ }
663
+ },
664
+ {
665
+ group: GROUP3,
666
+ name: "update",
667
+ mutates: true,
668
+ summary: "Update an experiment",
669
+ description: "Patch allocation, groups, params, targeting gate, or stats thresholds. Pass `targetingGate` = 'null' to clear it.",
670
+ params: [
671
+ { name: "name", type: "string", description: "Experiment name.", required: true, positional: true },
672
+ { name: "allocation", type: "number", description: "Allocation percentage, 0\u2013100." },
673
+ { name: "groups", type: "json", description: "Groups as JSON [{name,weight,params}]." },
674
+ { name: "params", type: "json", description: "Parameter schema {name: type}." },
675
+ { name: "targetingGate", type: "string", description: "Targeting gate name ('null' to clear)." },
676
+ { name: "bucketBy", type: "string", description: "Attribute to bucket by (empty string clears)." },
677
+ { name: "successEvent", type: "string", description: "Attach/replace the goal metric from this event." },
678
+ { name: "successAggregation", type: "string", description: "Goal-metric reducer (with successEvent).", enum: [...AGGREGATIONS] },
679
+ { name: "successValue", type: "string", description: "Numeric event property for sum/avg goal metrics." },
680
+ { name: "guardrailMetrics", type: "json", description: "Replace the guardrail set: JSON array of DSL queries or {query,name}." },
681
+ { name: "significance", type: "number", description: "Significance threshold (0.0001\u20130.5)." },
682
+ { name: "minRuntimeDays", type: "number", description: "Minimum runtime in days." },
683
+ { name: "minSampleSize", type: "number", description: "Minimum sample size." },
684
+ { name: "sequentialTesting", type: "boolean", description: "Enable sequential testing (always-valid p-values)." }
685
+ ],
686
+ examples: [
687
+ { note: "Dial allocation to 50%", run: "shipeasy release experiments update pricing-page --allocation 50" },
688
+ { note: "Clear targeting + tighten significance", run: "shipeasy release experiments update checkout-cta --targeting-gate null --significance 0.01" }
689
+ ],
690
+ run: async (client, i) => {
691
+ const e = await client.experiments.resolve(i.name);
692
+ const patch = {};
693
+ const allocation = num(i, "allocation");
694
+ if (allocation !== void 0) patch.allocation_pct = Math.round(allocation * 100);
695
+ if (i.groups !== void 0) patch.groups = i.groups;
696
+ if (i.params !== void 0) patch.params = i.params;
697
+ if (i.targetingGate !== void 0)
698
+ patch.targeting_gate = i.targetingGate === "null" ? null : i.targetingGate;
699
+ if (str(i, "bucketBy") !== void 0) patch.bucket_by = str(i, "bucketBy") || null;
700
+ if (i.significance !== void 0) patch.significance_threshold = i.significance;
701
+ if (i.minRuntimeDays !== void 0) patch.min_runtime_days = i.minRuntimeDays;
702
+ if (i.minSampleSize !== void 0) patch.min_sample_size = i.minSampleSize;
703
+ if (bool(i, "sequentialTesting") !== void 0) patch.sequential_testing = bool(i, "sequentialTesting");
704
+ if (i.guardrailMetrics !== void 0) patch.guardrail_metrics = parseGuardrails(i.guardrailMetrics);
705
+ const goal_metric = buildGoalMetric(i);
706
+ if (goal_metric) patch.goal_metric = goal_metric;
707
+ return client.experiments.update(e.id, patch);
708
+ }
709
+ },
710
+ {
711
+ group: GROUP3,
712
+ name: "start",
713
+ mutates: true,
714
+ summary: "Start an experiment",
715
+ description: "Move an experiment from draft to running.",
716
+ params: [{ name: "name", type: "string", description: "Experiment name.", required: true, positional: true }],
717
+ examples: [{ run: "shipeasy release experiments start pricing-page" }],
718
+ run: async (client, i) => {
719
+ const e = await client.experiments.resolve(i.name);
720
+ return client.experiments.start(e.id);
721
+ }
722
+ },
723
+ {
724
+ group: GROUP3,
725
+ name: "stop",
726
+ mutates: true,
727
+ summary: "Stop a running experiment",
728
+ description: "Stop a running experiment. Optionally name a `promoteGroup` to record an intent note (actual promotion happens in the dashboard).",
729
+ params: [
730
+ { name: "name", type: "string", description: "Experiment name.", required: true, positional: true },
731
+ { name: "promoteGroup", type: "string", description: "Group to note as the winner to promote." }
732
+ ],
733
+ examples: [{ run: "shipeasy release experiments stop pricing-page" }],
734
+ run: async (client, i) => {
735
+ const e = await client.experiments.resolve(i.name);
736
+ const result = await client.experiments.stop(e.id);
737
+ const promote = str(i, "promoteGroup");
738
+ return {
739
+ ...result,
740
+ ...promote ? {
741
+ promote_group_note: `Manual promotion needed: promote group '${promote}' in the dashboard or via PATCH /api/admin/experiments/${e.id}.`
742
+ } : {}
743
+ };
744
+ }
745
+ },
746
+ {
747
+ group: GROUP3,
748
+ name: "archive",
749
+ mutates: true,
750
+ summary: "Archive a stopped experiment",
751
+ description: "Archive (soft-delete) a stopped experiment, hiding it from the default list while preserving results.",
752
+ params: [{ name: "name", type: "string", description: "Experiment name.", required: true, positional: true }],
753
+ examples: [{ run: "shipeasy release experiments archive pricing-page" }],
754
+ run: async (client, i) => {
755
+ const e = await client.experiments.resolve(i.name);
756
+ return client.experiments.archive(e.id);
757
+ }
758
+ },
759
+ {
760
+ group: GROUP3,
761
+ name: "restore",
762
+ mutates: true,
763
+ summary: "Restore an archived experiment to draft",
764
+ description: "Restore a soft-deleted (archived) experiment back to `draft`. Allowed only if it never started; one that already ran must be cloned instead. Preserves the goal metric.",
765
+ params: [{ name: "name", type: "string", description: "Experiment name.", required: true, positional: true }],
766
+ examples: [{ run: "shipeasy release experiments restore pricing-page" }],
767
+ run: async (client, i) => {
768
+ const e = await client.experiments.resolve(i.name);
769
+ return client.experiments.restore(e.id);
770
+ }
771
+ },
772
+ {
773
+ group: GROUP3,
774
+ name: "reanalyze",
775
+ mutates: true,
776
+ summary: "Re-run the analysis pass",
777
+ description: "Trigger a fresh analysis pass for an experiment.",
778
+ params: [{ name: "name", type: "string", description: "Experiment name.", required: true, positional: true }],
779
+ examples: [{ run: "shipeasy release experiments reanalyze pricing-page" }],
780
+ run: async (client, i) => {
781
+ const e = await client.experiments.resolve(i.name);
782
+ return client.experiments.reanalyze(e.id);
783
+ }
784
+ },
785
+ {
786
+ group: GROUP3,
787
+ name: "status",
788
+ mutates: false,
789
+ summary: "Show experiment status, verdict, and latest results",
790
+ description: "Return the experiment, its latest per-metric results (enrolment, deltas, p-values), and a ship/hold/wait/not_running/invalid_srm verdict computed from the goal metric vs the significance threshold and minimum runtime.",
791
+ params: [{ name: "name", type: "string", description: "Experiment name.", required: true, positional: true }],
792
+ examples: [{ run: "shipeasy release experiments status pricing-page" }],
793
+ run: async (client, i) => {
794
+ const e = await client.experiments.resolve(i.name);
795
+ const detail = await client.experiments.get(e.id).catch(() => e);
796
+ const results = await client.experiments.results(e.id).catch(() => []);
797
+ const { verdict, reason } = computeVerdict(detail, results);
798
+ return { verdict, ...reason ? { reason } : {}, experiment: detail, results };
799
+ }
800
+ }
801
+ ];
802
+
803
+ // src/operations/universes.ts
804
+ var GROUP4 = ["release", "experiments", "universes"];
805
+ function parseHoldout(raw) {
806
+ if (raw === void 0 || raw === "" || raw === "null") return null;
807
+ const [lo, hi] = raw.split(",").map((s) => Number(s.trim()));
808
+ if (!Number.isFinite(lo) || !Number.isFinite(hi)) {
809
+ throw new ApiError("holdout must be 'lo,hi' integers in [0,9999]", 400);
810
+ }
811
+ return [lo, hi];
812
+ }
813
+ var universeOperations = [
814
+ {
815
+ group: GROUP4,
816
+ name: "list",
817
+ mutates: false,
818
+ summary: "List universes",
819
+ description: "Return every experiment universe (allocation %, holdout %).",
820
+ params: [],
821
+ examples: [{ run: "shipeasy release experiments universes list" }],
822
+ run: (client) => client.universes.listAll()
823
+ },
824
+ {
825
+ group: GROUP4,
826
+ name: "create",
827
+ mutates: true,
828
+ summary: "Create a universe",
829
+ description: "Create an experiment universe (container + optional holdout).",
830
+ params: [
831
+ { name: "name", type: "string", description: "Universe name.", required: true, positional: true },
832
+ { name: "unitType", type: "string", description: "Unit type (e.g. user_id, account_id). Defaults to user_id." },
833
+ { name: "holdout", type: "string", description: "Holdout range as 'lo,hi' (0\u20139999); omit for no holdout." }
834
+ ],
835
+ examples: [
836
+ { note: "Default user_id unit, no holdout", run: "shipeasy release experiments universes create web" },
837
+ { note: "Holdout 0\u2013999, keyed on account_id", run: "shipeasy release experiments universes create web --unit-type account_id --holdout 0,999" }
838
+ ],
839
+ run: (client, i) => client.universes.create({
840
+ name: i.name,
841
+ unit_type: str(i, "unitType") ?? "user_id",
842
+ holdout_range: parseHoldout(str(i, "holdout"))
843
+ })
844
+ },
845
+ {
846
+ group: GROUP4,
847
+ name: "update",
848
+ mutates: true,
849
+ summary: "Update a universe's holdout range",
850
+ description: "Update the holdout range. Pass 'null' to clear the holdout.",
851
+ params: [
852
+ { name: "name", type: "string", description: "Universe name.", required: true, positional: true },
853
+ { name: "holdout", type: "string", description: "Holdout range as 'lo,hi' (0\u20139999), or 'null' to clear." }
854
+ ],
855
+ examples: [
856
+ { note: "Set a 5% holdout", run: "shipeasy release experiments universes update web --holdout 0,499" },
857
+ { note: "Clear the holdout", run: "shipeasy release experiments universes update web --holdout null" }
858
+ ],
859
+ run: async (client, i) => {
860
+ const x = await client.universes.resolve(i.name);
861
+ const patch = i.holdout === void 0 ? {} : { holdout_range: parseHoldout(str(i, "holdout")) };
862
+ return client.universes.update(x.id, patch);
863
+ }
864
+ },
865
+ {
866
+ group: GROUP4,
867
+ name: "archive",
868
+ mutates: true,
869
+ summary: "Archive a universe",
870
+ description: "Archive (soft-delete) a universe by name.",
871
+ params: [{ name: "name", type: "string", description: "Universe name.", required: true, positional: true }],
872
+ examples: [{ run: "shipeasy release experiments universes archive web" }],
873
+ run: async (client, i) => {
874
+ const x = await client.universes.resolve(i.name);
875
+ await client.universes.delete(x.id);
876
+ return { ok: true, archived: i.name };
877
+ }
878
+ }
879
+ ];
880
+
881
+ // src/operations/metrics.ts
882
+ var GROUP5 = ["metrics"];
883
+ var METRIC_GRAMMAR = `Shipeasy metric query DSL
884
+ =========================
885
+
886
+ Query := AggFunc "(" Selector ("," Identifier)? ")" GroupBy?
887
+ AggFunc := count_users | count | sum | avg | min | max | unique |
888
+ p50 | p75 | p90 | p95 | p99 | p999 |
889
+ retention_<N>d (N in 1..90)
890
+ Selector := Identifier ("{" Filter ("," Filter)* ","? "}")?
891
+ Filter := Identifier MatchOp StringLiteral
892
+ MatchOp := "=" | "!=" | "=~" | "!~"
893
+ StringLiteral := double-quoted, supports \\" and \\\\
894
+ GroupBy := ("by" | "without") "(" Identifier ("," Identifier)* ","? ")"
895
+
896
+ Examples
897
+ --------
898
+ count_users(checkout_completed)
899
+ sum(purchase{country="US"}, amount)
900
+ p99(req_dur{route=~"/api/.*"}, ms) by (route, status)
901
+ retention_7d(session_start)
902
+ avg(req_dur{tier!="free"}, ms) without (region)
903
+
904
+ Notes
905
+ -----
906
+ - One event selector per query (no formulas).
907
+ - "count" maps to count_events (per-row count); "count_users" is uniq-by-user.
908
+ - For experiments, "by (label)" is ignored \u2014 per-user reducer is applied.
909
+ - Filter values must be quoted strings, even for numeric labels (they are coerced).
910
+ - Filter labels and value labels must be declared on the source event.
911
+ `;
912
+ var metricOperations = [
913
+ {
914
+ group: GROUP5,
915
+ name: "grammar",
916
+ mutates: false,
917
+ summary: "Print the metric query DSL grammar",
918
+ description: "Return the full metric query DSL grammar \u2014 aggregation functions, event selectors, label filters, and group-by. A pure op: it ignores the client and never hits the API.",
919
+ params: [],
920
+ examples: [{ run: "shipeasy metrics grammar" }],
921
+ run: async () => ({ grammar: METRIC_GRAMMAR })
922
+ },
923
+ {
924
+ group: GROUP5,
925
+ name: "list",
926
+ mutates: false,
927
+ summary: "List all metrics",
928
+ description: "Return every metric in the project (name, folder, source event, aggregation).",
929
+ params: [],
930
+ examples: [{ run: "shipeasy metrics list" }],
931
+ run: (client) => client.metrics.list()
932
+ },
933
+ {
934
+ group: GROUP5,
935
+ name: "show",
936
+ mutates: false,
937
+ summary: "Show one metric",
938
+ description: "Resolve a metric by id, id-prefix, or name and return its full detail.",
939
+ params: [{ name: "metric", type: "string", description: "Metric id or name.", required: true, positional: true }],
940
+ examples: [{ run: "shipeasy metrics show checkouts" }],
941
+ run: (client, i) => client.metrics.resolve(i.metric)
942
+ },
943
+ {
944
+ group: GROUP5,
945
+ name: "create",
946
+ mutates: true,
947
+ summary: "Create a metric",
948
+ description: "Create an event-backed metric. Pass the query as the DSL string (`query`) or the typed IR form (`queryIr`). `event` must match the event the query references. Run `metrics grammar` for the DSL syntax.",
949
+ params: [
950
+ { name: "name", type: "string", description: "Metric name.", required: true, positional: true },
951
+ { name: "event", type: "string", description: "Source event name (must equal the query's event).", required: true },
952
+ { name: "query", type: "string", description: "Query DSL string, e.g. 'sum(purchase, amount)'." },
953
+ { name: "queryIr", type: "json", description: "Typed IR JSON (alternative to query)." },
954
+ { name: "folder", type: "string", description: "Folder to file the metric under." },
955
+ { name: "winsorize", type: "number", description: "Winsorize percentile (1\u201399). Defaults to 99." },
956
+ { name: "mde", type: "number", description: "Min detectable effect (0\u20131)." }
957
+ ],
958
+ examples: [
959
+ { note: "Unique users who completed checkout", run: "shipeasy metrics create checkouts --event checkout_completed --query 'count_users(checkout_completed)'" },
960
+ { note: "p99 latency grouped by route", run: `shipeasy metrics create api-latency --event req_dur --query 'p99(req_dur{route=~"/api/.*"}, ms) by (route)'` }
961
+ ],
962
+ run: (client, i) => client.metrics.create({
963
+ name: i.name,
964
+ event_name: i.event,
965
+ folder: str(i, "folder") ?? null,
966
+ winsorize_pct: num(i, "winsorize") ?? 99,
967
+ min_detectable_effect: num(i, "mde") ?? null,
968
+ ...i.query ? { query: i.query } : {},
969
+ ...i.queryIr ? { query_ir: i.queryIr } : {}
970
+ })
971
+ },
972
+ {
973
+ group: GROUP5,
974
+ name: "archive",
975
+ mutates: true,
976
+ summary: "Archive a metric",
977
+ description: "Archive (soft-delete) a metric by id or name.",
978
+ params: [{ name: "metric", type: "string", description: "Metric id or name.", required: true, positional: true }],
979
+ examples: [{ run: "shipeasy metrics archive checkouts" }],
980
+ run: async (client, i) => {
981
+ const m = await client.metrics.resolve(i.metric);
982
+ await client.metrics.delete(m.id);
983
+ return { ok: true, archived: m.name };
984
+ }
985
+ }
986
+ ];
987
+
988
+ // src/operations/events.ts
989
+ var GROUP6 = ["events"];
990
+ var PROPS_DESC = `Event properties as a JSON array, e.g. '[{"name":"amount","type":"number","required":true}]'. type is one of string|number|boolean. Replaces the full set on update/approve.`;
991
+ function props(i) {
992
+ return i.props === void 0 ? void 0 : i.props;
993
+ }
994
+ var eventOperations = [
995
+ {
996
+ group: GROUP6,
997
+ name: "list",
998
+ mutates: false,
999
+ summary: "List catalogued events",
1000
+ description: "List every catalogued event (including pending auto-discovered names). Pass `pending` to show only the unapproved queue.",
1001
+ params: [{ name: "pending", type: "boolean", description: "Show only pending (unapproved) events." }],
1002
+ examples: [
1003
+ { run: "shipeasy metrics events list" },
1004
+ { note: "Review the auto-discovered queue", run: "shipeasy metrics events list --pending true" }
1005
+ ],
1006
+ run: async (client, i) => {
1007
+ const rows = await client.events.list();
1008
+ return bool(i, "pending") ? rows.filter((e) => e.pending === 1) : rows;
1009
+ }
1010
+ },
1011
+ {
1012
+ group: GROUP6,
1013
+ name: "get",
1014
+ mutates: false,
1015
+ summary: "Show one catalogued event",
1016
+ description: "Resolve an event by id, id-prefix, or name and return its full detail.",
1017
+ params: [{ name: "event", type: "string", description: "Event id or name.", required: true, positional: true }],
1018
+ examples: [{ run: "shipeasy metrics events get checkout_completed" }],
1019
+ run: (client, i) => client.events.resolve(i.event)
1020
+ },
1021
+ {
1022
+ group: GROUP6,
1023
+ name: "create",
1024
+ mutates: true,
1025
+ summary: "Register a new event",
1026
+ description: "Register a new event name and (optionally) its typed properties.",
1027
+ params: [
1028
+ { name: "name", type: "string", description: "Event name.", required: true, positional: true },
1029
+ { name: "folder", type: "string", description: "Folder to file it under." },
1030
+ { name: "description", type: "string", description: "Human-readable description." },
1031
+ { name: "props", type: "json", description: PROPS_DESC }
1032
+ ],
1033
+ examples: [
1034
+ { run: "shipeasy metrics events create checkout_completed" },
1035
+ { note: "With a typed, required property", run: `shipeasy metrics events create purchase --props '[{"name":"amount","type":"number","required":true}]'` }
1036
+ ],
1037
+ run: (client, i) => client.events.create({
1038
+ name: i.name,
1039
+ folder: str(i, "folder") ?? null,
1040
+ ...i.description ? { description: i.description } : {},
1041
+ properties: props(i) ?? []
1042
+ })
1043
+ },
1044
+ {
1045
+ group: GROUP6,
1046
+ name: "update",
1047
+ mutates: true,
1048
+ summary: "Update an event",
1049
+ description: "Update an event's folder, description, or properties (props replace the full set).",
1050
+ params: [
1051
+ { name: "event", type: "string", description: "Event id.", required: true, positional: true },
1052
+ { name: "folder", type: "string", description: "New folder." },
1053
+ { name: "description", type: "string", description: "New description." },
1054
+ { name: "props", type: "json", description: PROPS_DESC }
1055
+ ],
1056
+ examples: [{ run: "shipeasy metrics events update evt_3f9a --folder checkout" }],
1057
+ run: (client, i) => {
1058
+ const patch = {};
1059
+ if (i.folder !== void 0) patch.folder = i.folder;
1060
+ if (i.description !== void 0) patch.description = i.description;
1061
+ const p = props(i);
1062
+ if (p) patch.properties = p;
1063
+ return client.events.update(i.event, patch);
1064
+ }
1065
+ },
1066
+ {
1067
+ group: GROUP6,
1068
+ name: "approve",
1069
+ mutates: true,
1070
+ summary: "Approve a pending event",
1071
+ description: "Promote a pending (auto-discovered) event so metrics can query it. Optionally declare its folder/props on approval.",
1072
+ params: [
1073
+ { name: "event", type: "string", description: "Event id.", required: true, positional: true },
1074
+ { name: "folder", type: "string", description: "Folder to file it under." },
1075
+ { name: "description", type: "string", description: "Description." },
1076
+ { name: "props", type: "json", description: PROPS_DESC }
1077
+ ],
1078
+ examples: [{ note: "Promote an auto-discovered event", run: "shipeasy metrics events approve evt_3f9a" }],
1079
+ run: (client, i) => {
1080
+ const body = {};
1081
+ if (i.folder !== void 0) body.folder = i.folder;
1082
+ if (i.description !== void 0) body.description = i.description;
1083
+ const p = props(i);
1084
+ if (p) body.properties = p;
1085
+ return client.events.approve(i.event, body);
1086
+ }
1087
+ },
1088
+ {
1089
+ group: GROUP6,
1090
+ name: "archive",
1091
+ mutates: true,
1092
+ summary: "Archive an event",
1093
+ description: "Archive (remove) an event from the catalog by id.",
1094
+ params: [{ name: "event", type: "string", description: "Event id.", required: true, positional: true }],
1095
+ examples: [{ run: "shipeasy metrics events archive evt_3f9a" }],
1096
+ run: async (client, i) => {
1097
+ await client.events.delete(i.event);
1098
+ return { ok: true, archived: i.event };
1099
+ }
1100
+ }
1101
+ ];
1102
+
1103
+ // src/operations/ops.ts
1104
+ var QUEUE = ["ops"];
1105
+ var ALERTS = ["ops", "alerts"];
1106
+ var TYPES = ["bug", "feature_request", "error", "alert"];
1107
+ var STATUSES = [
1108
+ "open",
1109
+ "triaged",
1110
+ "in_progress",
1111
+ "ready_for_qa",
1112
+ "resolved",
1113
+ "wont_fix"
1114
+ ];
1115
+ var PRIORITIES = ["nice_to_have", "medium", "high", "critical"];
1116
+ var COMPARATORS = ["gt", "gte", "lt", "lte"];
1117
+ var SEVERITIES = ["danger", "warn", "info"];
1118
+ async function resolveSlackChannel(client, idOrName) {
1119
+ const res = await client.ops.channels();
1120
+ if (!res.connected)
1121
+ throw new ApiError("No Slack connector \u2014 connect Slack in Settings \u2192 Notifications first.", 400);
1122
+ const stripped = idOrName.replace(/^#/, "");
1123
+ const byId = res.channels.find((c) => c.id === idOrName);
1124
+ if (byId) return { id: byId.id, name: byId.name };
1125
+ const byName = res.channels.filter((c) => c.name === stripped);
1126
+ if (byName.length === 1) return { id: byName[0].id, name: byName[0].name };
1127
+ if (byName.length > 1)
1128
+ throw new ApiError(`Slack channel name '${stripped}' is ambiguous \u2014 pass a channel id`, 400);
1129
+ throw new ApiError(`Slack channel '${idOrName}' not found (run 'ops alerts channels')`, 404);
1130
+ }
1131
+ var opsOperations = [
1132
+ // ── unified queue ──────────────────────────────────────────────────────────
1133
+ {
1134
+ group: QUEUE,
1135
+ name: "list",
1136
+ mutates: false,
1137
+ summary: "List the operational queue",
1138
+ description: "List the unified queue \u2014 bugs, feature requests, and auto-filed error/alert tickets. Filter by `type`, `status`, or `priority`.",
1139
+ params: [
1140
+ { name: "type", type: "string", description: "Filter by type.", enum: [...TYPES, "all"] },
1141
+ { name: "status", type: "string", description: "Filter by status.", enum: [...STATUSES, "all"] },
1142
+ { name: "priority", type: "string", description: "Filter by priority.", enum: PRIORITIES },
1143
+ { name: "limit", type: "number", description: "Max rows (default 200)." }
1144
+ ],
1145
+ examples: [
1146
+ { note: "Open critical items only", run: "shipeasy ops list --priority critical" },
1147
+ { note: "Just error tickets", run: "shipeasy ops list --type error" }
1148
+ ],
1149
+ run: async (client, i) => {
1150
+ let items = await client.ops.list({
1151
+ type: str(i, "type") ?? "all",
1152
+ status: str(i, "status") ?? "open",
1153
+ limit: num(i, "limit") ?? 200
1154
+ });
1155
+ const priority = str(i, "priority");
1156
+ if (priority) items = items.filter((x) => x.priority === priority);
1157
+ return items;
1158
+ }
1159
+ },
1160
+ {
1161
+ group: QUEUE,
1162
+ name: "get",
1163
+ mutates: false,
1164
+ summary: "Show one queue item",
1165
+ description: "Show one queue item by number (#7 \u2192 7) or id \u2014 any type.",
1166
+ params: [{ name: "handle", type: "string", description: "Item number or id.", required: true, positional: true }],
1167
+ examples: [{ note: "By queue number", run: "shipeasy ops get 7" }],
1168
+ run: (client, i) => client.ops.get(i.handle)
1169
+ },
1170
+ {
1171
+ group: QUEUE,
1172
+ name: "create",
1173
+ mutates: true,
1174
+ summary: "File a bug or feature request",
1175
+ description: "File an operational item \u2014 a bug report or feature request \u2014 against the bound project. Fires the project's connectors (GitHub issue, Slack). error/alert tickets are auto-filed and cannot be created here.",
1176
+ params: [
1177
+ { name: "type", type: "string", description: "Item type.", required: true, enum: ["bug", "feature_request"] },
1178
+ { name: "title", type: "string", description: "One-line title.", required: true, positional: true },
1179
+ { name: "body", type: "string", description: "Detailed description." },
1180
+ { name: "priority", type: "string", description: "Priority.", enum: PRIORITIES },
1181
+ { name: "stepsToReproduce", type: "string", description: "Repro steps (bugs)." },
1182
+ { name: "pageUrl", type: "string", description: "Page URL the item relates to." }
1183
+ ],
1184
+ examples: [
1185
+ { note: "File a bug", run: 'shipeasy ops create "Checkout 500s on Safari" --type bug' },
1186
+ { note: "File a feature request", run: 'shipeasy ops create "Dark mode" --type feature_request --priority medium' }
1187
+ ],
1188
+ run: (client, i) => client.ops.create({
1189
+ type: i.type,
1190
+ title: i.title,
1191
+ ...i.body ? { body: i.body } : {},
1192
+ ...i.priority ? { priority: i.priority } : {},
1193
+ ...i.stepsToReproduce ? { stepsToReproduce: i.stepsToReproduce } : {},
1194
+ ...i.pageUrl ? { pageUrl: i.pageUrl } : {}
1195
+ })
1196
+ },
1197
+ {
1198
+ group: QUEUE,
1199
+ name: "update",
1200
+ mutates: true,
1201
+ summary: "Update a queue item's status/priority",
1202
+ description: "Flip a queue item's status (any type) and optionally its priority.",
1203
+ params: [
1204
+ { name: "handle", type: "string", description: "Item number or id.", required: true, positional: true },
1205
+ { name: "status", type: "string", description: "New status.", enum: STATUSES },
1206
+ { name: "priority", type: "string", description: "New priority.", enum: PRIORITIES }
1207
+ ],
1208
+ examples: [
1209
+ { note: "Resolve item #7", run: "shipeasy ops update 7 --status resolved" },
1210
+ { note: "Bump priority", run: "shipeasy ops update 7 --priority high" }
1211
+ ],
1212
+ run: (client, i) => {
1213
+ const patch = {};
1214
+ if (i.status !== void 0) patch.status = i.status;
1215
+ if (i.priority !== void 0) patch.priority = i.priority;
1216
+ if (!Object.keys(patch).length)
1217
+ throw new ApiError("Nothing to update \u2014 pass status and/or priority.", 400);
1218
+ return client.ops.update(i.handle, patch);
1219
+ }
1220
+ },
1221
+ {
1222
+ group: QUEUE,
1223
+ name: "link-pr",
1224
+ mutates: true,
1225
+ summary: "Link the PR that fixed an item",
1226
+ description: "Link the PR that fixed a queue item (any type). Records connector_data.github.pr. Pass `remove` to unlink.",
1227
+ params: [
1228
+ { name: "handle", type: "string", description: "Item number or id.", required: true, positional: true },
1229
+ { name: "prNumber", type: "number", description: "PR number.", positional: true },
1230
+ { name: "url", type: "string", description: "Explicit PR URL (required for error/alert tickets)." },
1231
+ { name: "remove", type: "boolean", description: "Unlink the PR instead of setting one." }
1232
+ ],
1233
+ examples: [
1234
+ { note: "Link the fixing PR", run: "shipeasy ops link-pr 7 42 --url https://github.com/acme/app/pull/42" }
1235
+ ],
1236
+ run: (client, i) => {
1237
+ if (i.remove) return client.ops.linkPr(i.handle, { prNumber: null });
1238
+ const prNumber = num(i, "prNumber");
1239
+ if (prNumber === void 0 || !Number.isInteger(prNumber) || prNumber <= 0)
1240
+ throw new ApiError("Pass a positive PR number (or --remove to unlink).", 400);
1241
+ return client.ops.linkPr(i.handle, {
1242
+ prNumber,
1243
+ ...i.url ? { prUrl: i.url } : {}
1244
+ });
1245
+ }
1246
+ },
1247
+ {
1248
+ group: QUEUE,
1249
+ name: "notify",
1250
+ mutates: true,
1251
+ summary: "Raise a 'needs your attention' bell notification",
1252
+ description: "Raise an attention bell notification (agent escalation, create-only). Idempotent on the dedupe key \u2014 pass `item` to derive `feedback:<n>`, or an explicit `key`.",
1253
+ params: [
1254
+ { name: "title", type: "string", description: "One-line headline of what's blocked.", required: true },
1255
+ { name: "summary", type: "string", description: "One sentence: why it can't be fixed in code.", required: true },
1256
+ { name: "steps", type: "json", description: "Steps the human should take, as a JSON array of strings." },
1257
+ { name: "href", type: "string", description: "Dashboard-relative deep link." },
1258
+ { name: "item", type: "string", description: "Queue item number this is about (sets dedupe key feedback:<n>)." },
1259
+ { name: "key", type: "string", description: "Explicit dedupe key (overrides item)." }
1260
+ ],
1261
+ examples: [
1262
+ { note: "Escalate a blocked item", run: `shipeasy ops notify --item 7 --title "Needs a DB migration" --summary "Fix requires a schema change I can't apply in code"` }
1263
+ ],
1264
+ run: (client, i) => {
1265
+ const item = str(i, "item");
1266
+ const dedupeKey = str(i, "key") ?? (item ? `feedback:${item.replace(/^#/, "")}` : void 0);
1267
+ return client.ops.notify({
1268
+ title: i.title,
1269
+ summary: i.summary,
1270
+ steps: i.steps ?? [],
1271
+ ...i.href ? { href: i.href } : {},
1272
+ ...dedupeKey ? { dedupeKey } : {}
1273
+ });
1274
+ }
1275
+ },
1276
+ // ── alert rules (ops alerts …) ───────────────────────────────────────────────
1277
+ {
1278
+ group: ALERTS,
1279
+ name: "list",
1280
+ mutates: false,
1281
+ summary: "List alert rules",
1282
+ description: "List the project's metric-threshold alert rules the analysis cron evaluates.",
1283
+ params: [],
1284
+ examples: [{ run: "shipeasy ops alerts list" }],
1285
+ run: (client) => client.alertRules.list()
1286
+ },
1287
+ {
1288
+ group: ALERTS,
1289
+ name: "channels",
1290
+ mutates: false,
1291
+ summary: "List the project's Slack channels",
1292
+ description: "List the project's Slack channels (for the `slackChannel` alert target).",
1293
+ params: [],
1294
+ examples: [{ run: "shipeasy ops alerts channels" }],
1295
+ run: (client) => client.ops.channels()
1296
+ },
1297
+ {
1298
+ group: ALERTS,
1299
+ name: "create",
1300
+ mutates: true,
1301
+ summary: "Create an alert rule",
1302
+ description: "Create a metric-threshold alert rule. The metric (and its aggregation) is fixed for the rule's life \u2014 delete + recreate to repoint it. Optionally target a Slack channel/email.",
1303
+ params: [
1304
+ { name: "name", type: "string", description: "Rule name.", required: true, positional: true },
1305
+ { name: "metric", type: "string", description: "Metric to evaluate (id or name).", required: true },
1306
+ { name: "comparator", type: "string", description: "Comparison operator.", required: true, enum: COMPARATORS },
1307
+ { name: "threshold", type: "number", description: "Threshold the metric value is compared against.", required: true },
1308
+ { name: "window", type: "number", description: "Lookback window in whole hours (1\u2013720). Defaults to 24." },
1309
+ { name: "severity", type: "string", description: "Severity. Defaults to warn.", enum: SEVERITIES },
1310
+ { name: "disabled", type: "boolean", description: "Create the rule disabled." },
1311
+ { name: "slackChannel", type: "string", description: "Slack channel to post to (id or name)." },
1312
+ { name: "email", type: "string", description: "Email address to notify." }
1313
+ ],
1314
+ examples: [
1315
+ { note: "Alert when error rate exceeds 50 / 24h", run: "shipeasy ops alerts create high-error-rate --metric api-errors --comparator gt --threshold 50" },
1316
+ { note: "Post to a Slack channel + email on-call", run: "shipeasy ops alerts create high-error-rate --metric api-errors --comparator gt --threshold 50 --slack-channel '#incidents' --email oncall@acme.com" }
1317
+ ],
1318
+ run: async (client, i) => {
1319
+ const metric = await client.metrics.resolve(i.metric);
1320
+ const slackChannel = str(i, "slackChannel");
1321
+ const email = str(i, "email");
1322
+ const notify = slackChannel || email ? {
1323
+ slackChannel: slackChannel ? await resolveSlackChannel(client, slackChannel) : null,
1324
+ email: email ?? null
1325
+ } : void 0;
1326
+ return client.alertRules.create({
1327
+ name: i.name,
1328
+ metricId: metric.id,
1329
+ comparator: i.comparator,
1330
+ threshold: num(i, "threshold"),
1331
+ windowHours: num(i, "window") ?? 24,
1332
+ severity: str(i, "severity") ?? "warn",
1333
+ enabled: !i.disabled,
1334
+ ...notify ? { notify } : {}
1335
+ });
1336
+ }
1337
+ },
1338
+ {
1339
+ group: ALERTS,
1340
+ name: "update",
1341
+ mutates: true,
1342
+ summary: "Update an alert rule",
1343
+ description: "Update an alert rule's tunable knobs (by id, id-prefix, or unique name). The metric is immutable. Pass `clearTarget` to revert the rule's target to the project default.",
1344
+ params: [
1345
+ { name: "rule", type: "string", description: "Rule id, id-prefix, or name.", required: true, positional: true },
1346
+ { name: "name", type: "string", description: "New human label." },
1347
+ { name: "comparator", type: "string", description: "Comparison operator.", enum: COMPARATORS },
1348
+ { name: "threshold", type: "number", description: "New threshold." },
1349
+ { name: "window", type: "number", description: "New lookback window in whole hours (1\u2013720)." },
1350
+ { name: "severity", type: "string", description: "New severity.", enum: SEVERITIES },
1351
+ { name: "enabled", type: "boolean", description: "Enable/disable the rule." },
1352
+ { name: "slackChannel", type: "string", description: "Set the Slack channel target (id or name)." },
1353
+ { name: "email", type: "string", description: "Set the email target." },
1354
+ { name: "clearTarget", type: "boolean", description: "Clear the target back to the project default." }
1355
+ ],
1356
+ examples: [
1357
+ { note: "Raise the threshold", run: "shipeasy ops alerts update high-error-rate --threshold 100" },
1358
+ { note: "Disable a rule", run: "shipeasy ops alerts update high-error-rate --enabled false" }
1359
+ ],
1360
+ run: async (client, i) => {
1361
+ const rule = await client.alertRules.resolve(i.rule);
1362
+ const patch = {};
1363
+ if (i.name !== void 0) patch.name = i.name;
1364
+ if (i.comparator !== void 0) patch.comparator = i.comparator;
1365
+ if (i.threshold !== void 0) patch.threshold = num(i, "threshold");
1366
+ if (i.window !== void 0) patch.windowHours = num(i, "window");
1367
+ if (i.severity !== void 0) patch.severity = i.severity;
1368
+ if (i.enabled !== void 0) patch.enabled = i.enabled;
1369
+ const slackChannel = str(i, "slackChannel");
1370
+ const email = str(i, "email");
1371
+ if (i.clearTarget) {
1372
+ patch.notify = { slackChannel: null, email: null };
1373
+ } else if (slackChannel || email) {
1374
+ patch.notify = {
1375
+ slackChannel: slackChannel ? await resolveSlackChannel(client, slackChannel) : rule.notify?.slackChannel ?? null,
1376
+ email: email ?? rule.notify?.email ?? null
1377
+ };
1378
+ }
1379
+ if (!Object.keys(patch).length)
1380
+ throw new ApiError("Nothing to update \u2014 pass at least one field to change.", 400);
1381
+ return client.alertRules.update(rule.id, patch);
1382
+ }
1383
+ },
1384
+ {
1385
+ group: ALERTS,
1386
+ name: "archive",
1387
+ mutates: true,
1388
+ summary: "Archive an alert rule",
1389
+ description: "Archive (delete) an alert rule by id, id-prefix, or unique name.",
1390
+ params: [{ name: "rule", type: "string", description: "Rule id, id-prefix, or name.", required: true, positional: true }],
1391
+ examples: [{ run: "shipeasy ops alerts archive high-error-rate" }],
1392
+ run: async (client, i) => {
1393
+ const rule = await client.alertRules.resolve(i.rule);
1394
+ await client.alertRules.delete(rule.id);
1395
+ return { ok: true, archived: rule.name };
1396
+ }
1397
+ }
1398
+ ];
1399
+
1400
+ // src/operations/projects.ts
1401
+ var GROUP7 = ["projects"];
1402
+ var projectOperations = [
1403
+ {
1404
+ group: GROUP7,
1405
+ name: "current",
1406
+ mutates: false,
1407
+ summary: "Show the current project",
1408
+ description: "Return the project the session resolves to (plan, status, enabled modules, billing). The server reads the project from the auth header \u2014 no id needed. Powers `whoami`.",
1409
+ params: [],
1410
+ examples: [{ run: "shipeasy projects current" }],
1411
+ run: (client) => client.projects.current()
1412
+ },
1413
+ {
1414
+ group: GROUP7,
1415
+ name: "upsert",
1416
+ mutates: false,
1417
+ summary: "Find-or-create a project by domain",
1418
+ description: "Find-or-create a project by domain (idempotent), under the session's owner. Returns the project. The `.shipeasy` bind that records the result locally is a consumer side-effect layered on top of this op \u2014 it is not performed here.",
1419
+ params: [
1420
+ { name: "domain", type: "string", description: "Hostname-like project identifier (e.g. acme.com).", required: true },
1421
+ { name: "name", type: "string", description: "Human-readable name (defaults to the domain on first create)." }
1422
+ ],
1423
+ examples: [
1424
+ { note: "Find-or-create by domain", run: "shipeasy projects upsert --domain acme.com" },
1425
+ { note: "Name it explicitly", run: 'shipeasy projects upsert --domain shouks.app --name "Shouks"' }
1426
+ ],
1427
+ run: (client, i) => client.projects.upsert({
1428
+ domain: i.domain,
1429
+ ...i.name ? { name: i.name } : {}
1430
+ })
1431
+ }
1432
+ ];
1433
+
1434
+ // src/operations/i18n.ts
1435
+ var I18N = ["i18n"];
1436
+ var PROFILES = ["i18n", "profiles"];
1437
+ var DRAFTS = ["i18n", "drafts"];
1438
+ var i18nOperations = [
1439
+ {
1440
+ group: PROFILES,
1441
+ name: "list",
1442
+ mutates: false,
1443
+ summary: "List i18n locale profiles",
1444
+ description: "List the project's i18n locale profiles (name, default locale, locales).",
1445
+ params: [],
1446
+ examples: [{ run: "shipeasy i18n profiles list" }],
1447
+ run: (client) => client.i18n.listProfiles()
1448
+ },
1449
+ {
1450
+ group: PROFILES,
1451
+ name: "create",
1452
+ mutates: true,
1453
+ summary: "Create an i18n locale profile",
1454
+ description: "Create an i18n profile (e.g. 'en:prod', 'fr:prod') with one or more locales.",
1455
+ params: [
1456
+ { name: "name", type: "string", description: "Profile name.", required: true, positional: true },
1457
+ { name: "locales", type: "json", description: `Locales as a JSON array, e.g. '["fr","fr-CA"]'. Defaults to ["en"].` },
1458
+ { name: "defaultLocale", type: "string", description: "Default locale (defaults to the first locale)." }
1459
+ ],
1460
+ examples: [
1461
+ { run: "shipeasy i18n profiles create en:prod" },
1462
+ { note: "Multiple locales with a default", run: `shipeasy i18n profiles create fr:prod --locales '["fr","fr-CA"]' --default-locale fr` }
1463
+ ],
1464
+ run: (client, i) => {
1465
+ const locales = i.locales ?? ["en"];
1466
+ return client.i18n.createProfile({
1467
+ name: i.name,
1468
+ locales,
1469
+ default_locale: str(i, "defaultLocale") ?? locales[0]
1470
+ });
1471
+ }
1472
+ },
1473
+ {
1474
+ group: I18N,
1475
+ name: "push",
1476
+ mutates: true,
1477
+ summary: "Add new i18n keys (insert-only)",
1478
+ description: "Add NEW keys to a profile. Insert-only \u2014 existing keys are never overwritten (use `i18n update` to change a value). Keys are a JSON array of {key,value}.",
1479
+ params: [
1480
+ { name: "profile", type: "string", description: "Profile name (e.g. 'en:prod').", required: true },
1481
+ { name: "keys", type: "json", description: `Keys as a JSON array, e.g. '[{"key":"home.cta","value":"Get started"}]'.`, required: true },
1482
+ { name: "chunk", type: "string", description: "Logical grouping for the keys. Defaults to 'default'." }
1483
+ ],
1484
+ examples: [
1485
+ { note: "Add keys to a profile", run: `shipeasy i18n push --profile en:prod --keys '[{"key":"home.cta","value":"Get started"}]'` }
1486
+ ],
1487
+ run: async (client, i) => {
1488
+ const profile = await client.i18n.resolveProfile(i.profile);
1489
+ return client.i18n.pushKeys({
1490
+ profile_id: profile.id,
1491
+ chunk: str(i, "chunk") ?? "default",
1492
+ keys: i.keys
1493
+ });
1494
+ }
1495
+ },
1496
+ {
1497
+ group: I18N,
1498
+ name: "update",
1499
+ mutates: true,
1500
+ summary: "Update one i18n key's value",
1501
+ description: "Change the value of a single existing key (the only overwrite path; `push` only adds). One key per call.",
1502
+ params: [
1503
+ { name: "key", type: "string", description: "Key name.", required: true, positional: true },
1504
+ { name: "value", type: "string", description: "New value.", required: true, positional: true },
1505
+ { name: "profile", type: "string", description: "Profile name (e.g. 'en:prod').", required: true },
1506
+ { name: "description", type: "string", description: "Optional description to store with the key." }
1507
+ ],
1508
+ examples: [
1509
+ { note: "Change one key's value", run: "shipeasy i18n update home.cta 'Get started' --profile en:prod" }
1510
+ ],
1511
+ run: async (client, i) => {
1512
+ const profile = await client.i18n.resolveProfile(i.profile);
1513
+ return client.i18n.updateKeyByName(profile.id, i.key, {
1514
+ value: i.value,
1515
+ ...i.description !== void 0 ? { description: i.description } : {}
1516
+ });
1517
+ }
1518
+ },
1519
+ {
1520
+ group: I18N,
1521
+ name: "publish",
1522
+ mutates: true,
1523
+ summary: "Publish a profile chunk to the CDN",
1524
+ description: "Publish a profile chunk (rebuilds the KV manifest and purges the CDN).",
1525
+ params: [
1526
+ { name: "profile", type: "string", description: "Profile name (e.g. 'en:prod').", required: true },
1527
+ { name: "chunk", type: "string", description: "Chunk to publish. Defaults to 'default'." }
1528
+ ],
1529
+ examples: [
1530
+ { note: "Publish the default chunk", run: "shipeasy i18n publish --profile en:prod" },
1531
+ { note: "Publish a specific chunk", run: "shipeasy i18n publish --profile fr:prod --chunk marketing" }
1532
+ ],
1533
+ run: async (client, i) => {
1534
+ const profile = await client.i18n.resolveProfile(i.profile);
1535
+ return client.i18n.publish(profile.id, { chunk: str(i, "chunk") ?? "default" });
1536
+ }
1537
+ },
1538
+ {
1539
+ group: DRAFTS,
1540
+ name: "list",
1541
+ mutates: false,
1542
+ summary: "List staged i18n drafts",
1543
+ description: "List staged translation drafts. Replaces the `drafts` kind of the deleted MCP generic read.",
1544
+ params: [],
1545
+ examples: [{ run: "shipeasy i18n drafts list" }],
1546
+ run: (client) => client.i18n.listDrafts()
1547
+ }
1548
+ ];
1549
+
1550
+ // src/operations/attributes.ts
1551
+ var attributeOperations = [
1552
+ {
1553
+ group: ["attributes"],
1554
+ name: "list",
1555
+ mutates: false,
1556
+ summary: "List targeting attributes",
1557
+ description: "List the auto-inferred user-attribute schema available for gate/experiment targeting.",
1558
+ params: [],
1559
+ examples: [{ run: "shipeasy attributes list" }],
1560
+ run: (client) => client.attributes.list()
1561
+ }
1562
+ ];
1563
+
1564
+ // src/operations/sdk-docs.ts
1565
+ var SDK_REPOS = {
1566
+ typescript: { owner: "shipeasy-ai", repo: "sdk" },
1567
+ javascript: { owner: "shipeasy-ai", repo: "sdk" },
1568
+ node: { owner: "shipeasy-ai", repo: "sdk" },
1569
+ ts: { owner: "shipeasy-ai", repo: "sdk" },
1570
+ python: { owner: "shipeasy-ai", repo: "sdk-python" },
1571
+ go: { owner: "shipeasy-ai", repo: "sdk-go" },
1572
+ java: { owner: "shipeasy-ai", repo: "sdk-java" },
1573
+ kotlin: { owner: "shipeasy-ai", repo: "sdk-kotlin" },
1574
+ php: { owner: "shipeasy-ai", repo: "sdk-php" },
1575
+ swift: { owner: "shipeasy-ai", repo: "sdk-swift" },
1576
+ ruby: { owner: "shipeasy-ai", repo: "sdk-ruby" }
1577
+ };
1578
+ var SDK_ENUM = Object.keys(SDK_REPOS);
1579
+ var DEFAULT_MANIFEST = {
1580
+ schemaVersion: 2,
1581
+ sdk: "default",
1582
+ placeholders: ["RESOURCE_NAME"],
1583
+ skill: "skill/SKILL.md",
1584
+ pages: { overview: "pages/overview.md", installation: "pages/installation.md" },
1585
+ snippets: { release: { flags: "snippets/release/flags.md" } }
1586
+ };
1587
+ var DEFAULT_CONTENT = {
1588
+ "pages/overview.md": "# Shipeasy SDK\n\nThis SDK has not published its `/docs/` folder yet. See https://docs.shipeasy.ai for the full reference.",
1589
+ "pages/installation.md": "# Installation\n\nInstall the Shipeasy SDK for your language, then call `shipeasy({ serverKey })` (server) or `shipeasy({ clientKey })` (browser).",
1590
+ "snippets/release/flags.md": "```\nconst on = await client.checkGate(user, '{{RESOURCE_NAME}}');\n```",
1591
+ "skill/SKILL.md": "---\nname: shipeasy-sdk\ndescription: Use the Shipeasy SDK for flags, experiments, configs, and i18n.\n---\n\nThis SDK has not published a skill yet. See https://docs.shipeasy.ai."
1592
+ };
1593
+ function repoFor(sdk) {
1594
+ const r = SDK_REPOS[sdk.toLowerCase()];
1595
+ if (!r) throw new ApiError(`Unknown SDK '${sdk}'. One of: ${SDK_ENUM.join(", ")}`, 400);
1596
+ return r;
1597
+ }
1598
+ function pagesBase(sdk) {
1599
+ const { owner, repo } = repoFor(sdk);
1600
+ return `https://${owner}.github.io/${repo}`;
1601
+ }
1602
+ async function fetchText(url) {
1603
+ const res = await fetch(url);
1604
+ if (!res.ok) return null;
1605
+ return res.text();
1606
+ }
1607
+ async function loadManifest(sdk) {
1608
+ const text = await fetchText(`${pagesBase(sdk)}/manifest.json`);
1609
+ if (!text) return { manifest: DEFAULT_MANIFEST, fallback: true };
1610
+ try {
1611
+ return { manifest: JSON.parse(text), fallback: false };
1612
+ } catch {
1613
+ return { manifest: DEFAULT_MANIFEST, fallback: true };
1614
+ }
1615
+ }
1616
+ function resolvePath(manifest, handle) {
1617
+ if (handle.endsWith(".md")) return handle;
1618
+ if (manifest.pages?.[handle]) return manifest.pages[handle];
1619
+ if (handle.includes("/")) {
1620
+ const [group, resource] = handle.split("/", 2);
1621
+ const p = manifest.snippets?.[group]?.[resource];
1622
+ if (p) return p;
1623
+ }
1624
+ throw new ApiError(
1625
+ `Doc '${handle}' not found. Run 'docs list --sdk <lang>' to see available pages and snippets.`,
1626
+ 404
1627
+ );
1628
+ }
1629
+ function substitute(body, manifest, i) {
1630
+ let out = body;
1631
+ for (const ph of manifest.placeholders ?? []) {
1632
+ const value = str(i, ph) ?? (ph === "RESOURCE_NAME" ? str(i, "name") : void 0) ?? (ph === "FRAMEWORK" ? str(i, "framework") : void 0);
1633
+ if (value !== void 0) out = out.replaceAll(`{{${ph}}}`, value);
1634
+ }
1635
+ return out;
1636
+ }
1637
+ var docsOperations = [
1638
+ {
1639
+ group: ["docs"],
1640
+ name: "list",
1641
+ mutates: false,
1642
+ summary: "List an SDK's documentation tree",
1643
+ description: "Fetch an SDK's `/docs/manifest.json` and return the doc tree \u2014 the feature-reference pages, the nested snippet groups, and whether an installable skill exists. The runtime 'explore' step before fetching a specific page or snippet.",
1644
+ params: [{ name: "sdk", type: "string", description: "SDK language.", required: true, enum: SDK_ENUM }],
1645
+ examples: [{ run: "shipeasy docs list --sdk python" }],
1646
+ run: async (_client, i) => {
1647
+ const { manifest, fallback } = await loadManifest(i.sdk);
1648
+ return {
1649
+ sdk: i.sdk,
1650
+ fallback,
1651
+ pages: Object.keys(manifest.pages ?? {}),
1652
+ snippets: Object.fromEntries(
1653
+ Object.entries(manifest.snippets ?? {}).map(([g, r]) => [g, Object.keys(r)])
1654
+ ),
1655
+ skill: Boolean(manifest.skill)
1656
+ };
1657
+ }
1658
+ },
1659
+ {
1660
+ group: ["docs"],
1661
+ name: "get",
1662
+ mutates: false,
1663
+ summary: "Fetch one SDK doc page or snippet",
1664
+ description: "Fetch one feature-reference page (`flags`, `experiments`, \u2026) or nested snippet (`release/experiments`, `i18n/setup`, \u2026) from the SDK's published docs, substituting declared {{placeholders}} from caller args. Replaces the old `get_sdk_snippet`.",
1665
+ params: [
1666
+ { name: "sdk", type: "string", description: "SDK language.", required: true, enum: SDK_ENUM },
1667
+ { name: "path", type: "string", description: "Page key or snippet 'group/resource'.", required: true, positional: true },
1668
+ { name: "framework", type: "string", description: "Framework hint (substitutes {{FRAMEWORK}})." },
1669
+ { name: "name", type: "string", description: "Resource name (substitutes {{RESOURCE_NAME}})." }
1670
+ ],
1671
+ examples: [
1672
+ { note: "A feature page", run: "shipeasy docs get --sdk python experiments" },
1673
+ { note: "A nested snippet, with a resource name", run: "shipeasy docs get --sdk typescript release/flags --name checkout_v2" }
1674
+ ],
1675
+ run: async (_client, i) => {
1676
+ const sdk = i.sdk;
1677
+ const { manifest, fallback } = await loadManifest(sdk);
1678
+ const path = resolvePath(manifest, i.path);
1679
+ const body = fallback ? DEFAULT_CONTENT[path] : await fetchText(`${pagesBase(sdk)}/${path}`) ?? void 0;
1680
+ if (body === void 0) throw new ApiError(`Doc '${path}' could not be fetched for '${sdk}'.`, 404);
1681
+ return { sdk, path, fallback, content: substitute(body, manifest, i) };
1682
+ }
1683
+ },
1684
+ {
1685
+ group: ["docs"],
1686
+ name: "skill",
1687
+ mutates: false,
1688
+ summary: "Fetch an SDK's installable LLM skill",
1689
+ description: "Fetch the SDK's `skill/SKILL.md` (YAML frontmatter intact) so an agent can install it verbatim into its skills directory. The CLI's `--install` writes it locally (a consumer fs side-effect layered on the fetch); the op itself just returns the bytes.",
1690
+ params: [
1691
+ { name: "sdk", type: "string", description: "SDK language.", required: true, enum: SDK_ENUM },
1692
+ { name: "install", type: "boolean", description: "CLI only: write the skill to the local agent skills dir." }
1693
+ ],
1694
+ examples: [
1695
+ { run: "shipeasy docs skill --sdk python" },
1696
+ { note: "Install into the local skills dir", run: "shipeasy docs skill --sdk python --install true" }
1697
+ ],
1698
+ run: async (_client, i) => {
1699
+ const sdk = i.sdk;
1700
+ const { manifest, fallback } = await loadManifest(sdk);
1701
+ const skillPath = manifest.skill ?? "skill/SKILL.md";
1702
+ const content = fallback ? DEFAULT_CONTENT[skillPath] : await fetchText(`${pagesBase(sdk)}/${skillPath}`) ?? void 0;
1703
+ if (content === void 0) throw new ApiError(`No skill published for '${sdk}'.`, 404);
1704
+ return { sdk, skillPath, fallback, content };
1705
+ }
1706
+ }
1707
+ ];
1708
+
1709
+ // src/operations/index.ts
1710
+ var RELEASE_OPERATIONS = [
1711
+ ...gateOperations,
1712
+ ...killswitchOperations,
1713
+ ...configOperations,
1714
+ ...experimentOperations,
1715
+ ...universeOperations
1716
+ ];
1717
+ var ALL_OPERATIONS = [
1718
+ ...RELEASE_OPERATIONS,
1719
+ ...metricOperations,
1720
+ ...eventOperations,
1721
+ ...opsOperations,
1722
+ ...projectOperations,
1723
+ ...i18nOperations,
1724
+ ...attributeOperations,
1725
+ ...docsOperations
1726
+ ];
1727
+ export {
1728
+ ALL_OPERATIONS,
1729
+ METRIC_GRAMMAR,
1730
+ RELEASE_OPERATIONS,
1731
+ attributeOperations,
1732
+ coerceInput,
1733
+ configOperations,
1734
+ docsOperations,
1735
+ eventOperations,
1736
+ experimentOperations,
1737
+ gateOperations,
1738
+ i18nOperations,
1739
+ killswitchOperations,
1740
+ metricOperations,
1741
+ mountOperations,
1742
+ opCli,
1743
+ opId,
1744
+ opMcpName,
1745
+ operationsToDispatch,
1746
+ operationsToMcpTools,
1747
+ opsOperations,
1748
+ projectOperations,
1749
+ renderOperationsMarkdown,
1750
+ universeOperations
1751
+ };