@shipeasy/openapi 1.6.0 → 2.2.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 (151) hide show
  1. package/README.md +37 -0
  2. package/dist/chunk-643YSMLU.js +1616 -0
  3. package/dist/chunk-643YSMLU.js.map +1 -0
  4. package/dist/chunk-MRW3DJ57.js +3385 -0
  5. package/dist/chunk-MRW3DJ57.js.map +1 -0
  6. package/dist/client-K0ShmsC7.d.ts +11284 -0
  7. package/dist/client.d.ts +1 -0
  8. package/dist/client.js +215 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/custom.d.ts +86 -0
  11. package/dist/custom.js +300 -0
  12. package/dist/custom.js.map +1 -0
  13. package/dist/index.d.ts +29 -41
  14. package/dist/index.js +258 -4375
  15. package/dist/index.js.map +1 -0
  16. package/dist/node-context.d.ts +81 -33
  17. package/dist/node-context.js +98 -44
  18. package/dist/node-context.js.map +1 -0
  19. package/dist/schemas.d.ts +7973 -0
  20. package/dist/schemas.js +1099 -0
  21. package/dist/schemas.js.map +1 -0
  22. package/openapi.json +15922 -3232
  23. package/openapi.yaml +14277 -0
  24. package/package.json +33 -26
  25. package/spec/components/errors.yaml +53 -0
  26. package/spec/components/parameters.yaml +32 -0
  27. package/spec/components/responses.yaml +122 -0
  28. package/spec/components/schemas/alert-rules.yaml +175 -0
  29. package/spec/components/schemas/attributes.yaml +157 -0
  30. package/spec/components/schemas/common.yaml +92 -0
  31. package/spec/components/schemas/configs.yaml +371 -0
  32. package/spec/components/schemas/connectors.yaml +587 -0
  33. package/spec/components/schemas/errors-api.yaml +209 -0
  34. package/spec/components/schemas/events.yaml +321 -0
  35. package/spec/components/schemas/experiments.yaml +865 -0
  36. package/spec/components/schemas/gate-templates.yaml +237 -0
  37. package/spec/components/schemas/gates.yaml +890 -0
  38. package/spec/components/schemas/i18n.yaml +478 -0
  39. package/spec/components/schemas/keys.yaml +186 -0
  40. package/spec/components/schemas/killswitches.yaml +309 -0
  41. package/spec/components/schemas/metrics.yaml +547 -0
  42. package/spec/components/schemas/ops.yaml +1013 -0
  43. package/spec/components/schemas/projects.yaml +222 -0
  44. package/spec/components/schemas/search.yaml +46 -0
  45. package/spec/components/schemas/universes.yaml +127 -0
  46. package/spec/openapi.yaml +367 -0
  47. package/spec/paths/alert-rules.yaml +258 -0
  48. package/spec/paths/attributes.yaml +272 -0
  49. package/spec/paths/configs.yaml +758 -0
  50. package/spec/paths/connectors.yaml +782 -0
  51. package/spec/paths/errors.yaml +382 -0
  52. package/spec/paths/events.yaml +380 -0
  53. package/spec/paths/experiments.yaml +803 -0
  54. package/spec/paths/gate-templates.yaml +267 -0
  55. package/spec/paths/gates.yaml +465 -0
  56. package/spec/paths/i18n.yaml +557 -0
  57. package/spec/paths/keys.yaml +182 -0
  58. package/spec/paths/killswitches.yaml +574 -0
  59. package/spec/paths/metrics.yaml +313 -0
  60. package/spec/paths/ops.yaml +446 -0
  61. package/spec/paths/projects.yaml +210 -0
  62. package/spec/paths/search.yaml +62 -0
  63. package/spec/paths/universes.yaml +255 -0
  64. package/dist/operations/attributes.d.ts +0 -33
  65. package/dist/operations/attributes.js +0 -16
  66. package/dist/operations/cli-adapter.d.ts +0 -35
  67. package/dist/operations/cli-adapter.js +0 -91
  68. package/dist/operations/coerce.d.ts +0 -32
  69. package/dist/operations/coerce.js +0 -53
  70. package/dist/operations/configs.d.ts +0 -28
  71. package/dist/operations/configs.js +0 -119
  72. package/dist/operations/docs.d.ts +0 -37
  73. package/dist/operations/docs.js +0 -39
  74. package/dist/operations/events.d.ts +0 -28
  75. package/dist/operations/events.js +0 -121
  76. package/dist/operations/experiments.d.ts +0 -28
  77. package/dist/operations/experiments.js +0 -280
  78. package/dist/operations/gates.d.ts +0 -28
  79. package/dist/operations/gates.js +0 -132
  80. package/dist/operations/i18n.d.ts +0 -28
  81. package/dist/operations/i18n.js +0 -121
  82. package/dist/operations/index.d.ts +0 -63
  83. package/dist/operations/index.js +0 -1751
  84. package/dist/operations/killswitches.d.ts +0 -28
  85. package/dist/operations/killswitches.js +0 -126
  86. package/dist/operations/mcp-adapter.d.ts +0 -35
  87. package/dist/operations/mcp-adapter.js +0 -94
  88. package/dist/operations/metrics.d.ts +0 -30
  89. package/dist/operations/metrics.js +0 -114
  90. package/dist/operations/ops.d.ts +0 -28
  91. package/dist/operations/ops.js +0 -313
  92. package/dist/operations/projects.d.ts +0 -28
  93. package/dist/operations/projects.js +0 -36
  94. package/dist/operations/sdk-docs.d.ts +0 -28
  95. package/dist/operations/sdk-docs.js +0 -160
  96. package/dist/operations/types.d.ts +0 -138
  97. package/dist/operations/types.js +0 -9
  98. package/dist/operations/universes.d.ts +0 -28
  99. package/dist/operations/universes.js +0 -93
  100. package/dist/operations/util.d.ts +0 -33
  101. package/dist/operations/util.js +0 -9
  102. package/dist/resources/alert-rules.d.ts +0 -50
  103. package/dist/resources/alert-rules.js +0 -75
  104. package/dist/resources/attributes.d.ts +0 -19
  105. package/dist/resources/attributes.js +0 -10
  106. package/dist/resources/configs.d.ts +0 -681
  107. package/dist/resources/configs.js +0 -509
  108. package/dist/resources/events.d.ts +0 -61
  109. package/dist/resources/events.js +0 -42
  110. package/dist/resources/experiments.d.ts +0 -849
  111. package/dist/resources/experiments.js +0 -707
  112. package/dist/resources/gates.d.ts +0 -808
  113. package/dist/resources/gates.js +0 -522
  114. package/dist/resources/i18n.d.ts +0 -84
  115. package/dist/resources/i18n.js +0 -60
  116. package/dist/resources/index.d.ts +0 -2767
  117. package/dist/resources/index.js +0 -2563
  118. package/dist/resources/killswitches.d.ts +0 -436
  119. package/dist/resources/killswitches.js +0 -514
  120. package/dist/resources/metrics.d.ts +0 -54
  121. package/dist/resources/metrics.js +0 -40
  122. package/dist/resources/ops.d.ts +0 -88
  123. package/dist/resources/ops.js +0 -36
  124. package/dist/resources/projects.d.ts +0 -54
  125. package/dist/resources/projects.js +0 -14
  126. package/dist/resources/universes.d.ts +0 -185
  127. package/dist/resources/universes.js +0 -234
  128. package/dist/schemas/alert-rules.d.ts +0 -65
  129. package/dist/schemas/alert-rules.js +0 -37
  130. package/dist/schemas/configs.d.ts +0 -154
  131. package/dist/schemas/configs.js +0 -134
  132. package/dist/schemas/constants.d.ts +0 -6
  133. package/dist/schemas/constants.js +0 -7
  134. package/dist/schemas/experiments.d.ts +0 -229
  135. package/dist/schemas/experiments.js +0 -221
  136. package/dist/schemas/folder.d.ts +0 -10
  137. package/dist/schemas/folder.js +0 -8
  138. package/dist/schemas/gates.d.ts +0 -386
  139. package/dist/schemas/gates.js +0 -190
  140. package/dist/schemas/index.d.ts +0 -11
  141. package/dist/schemas/index.js +0 -762
  142. package/dist/schemas/killswitches.d.ts +0 -110
  143. package/dist/schemas/killswitches.js +0 -226
  144. package/dist/schemas/metric-name.d.ts +0 -5
  145. package/dist/schemas/metric-name.js +0 -6
  146. package/dist/schemas/pagination.d.ts +0 -37
  147. package/dist/schemas/pagination.js +0 -44
  148. package/dist/schemas/universes.d.ts +0 -35
  149. package/dist/schemas/universes.js +0 -55
  150. package/dist/transport.d.ts +0 -30
  151. package/dist/transport.js +0 -44
package/dist/index.js CHANGED
@@ -1,4379 +1,262 @@
1
- // src/transport.ts
2
- var ApiError = class extends Error {
3
- constructor(message, status) {
4
- super(message);
5
- this.status = status;
6
- this.name = "ApiError";
7
- }
8
- status;
1
+ import {
2
+ approveEvent,
3
+ client,
4
+ configure,
5
+ createAlertRule,
6
+ createAttribute,
7
+ createClient,
8
+ createConfig,
9
+ createConfig2,
10
+ createConnector,
11
+ createEvent,
12
+ createExperiment,
13
+ createGate,
14
+ createGateTemplate,
15
+ createI18nDraft,
16
+ createI18nProfile,
17
+ createKey,
18
+ createKillswitch,
19
+ createMetric,
20
+ createOpsItem,
21
+ createTriggerConnector,
22
+ createUniverse,
23
+ deleteAlertRule,
24
+ deleteAttribute,
25
+ deleteConfig,
26
+ deleteConnector,
27
+ deleteEvent,
28
+ deleteExperiment,
29
+ deleteGate,
30
+ deleteGateTemplate,
31
+ deleteKillswitch,
32
+ deleteMetric,
33
+ deleteUniverse,
34
+ disableGate,
35
+ discardConfigDraft,
36
+ enableGate,
37
+ fileErrorTicket,
38
+ fireConnector,
39
+ getAttribute,
40
+ getConfig,
41
+ getConnector,
42
+ getCurrentProject,
43
+ getError,
44
+ getErrorSeries,
45
+ getEvent,
46
+ getExperiment,
47
+ getExperimentResults,
48
+ getExperimentTimeseries,
49
+ getGateTemplate,
50
+ getKillswitch,
51
+ getMetric,
52
+ getOpsItem,
53
+ linkPrToOpsItem,
54
+ listAlertRules,
55
+ listAttributes,
56
+ listConfigActivity,
57
+ listConfigs,
58
+ listConnectors,
59
+ listErrors,
60
+ listEvents,
61
+ listExperiments,
62
+ listGateTemplates,
63
+ listGates,
64
+ listI18nDrafts,
65
+ listI18nKeys,
66
+ listI18nProfiles,
67
+ listKeys,
68
+ listKillswitches,
69
+ listMetrics,
70
+ listOpsItems,
71
+ listSlackChannels,
72
+ listUniverses,
73
+ notifyOps,
74
+ publishConfigDraft,
75
+ publishI18nProfile,
76
+ pushI18nKeys,
77
+ reanalyzeExperiment,
78
+ revokeKey,
79
+ saveConfigDraft,
80
+ searchResources,
81
+ setExperimentMetrics,
82
+ setExperimentStatus,
83
+ setI18nLabel,
84
+ setKillswitchSwitch,
85
+ setKillswitchValue,
86
+ testConnector,
87
+ unsetKillswitchSwitch,
88
+ updateAlertRule,
89
+ updateAttribute,
90
+ updateConfig,
91
+ updateConfigSchema,
92
+ updateConnector,
93
+ updateErrorStatus,
94
+ updateEvent,
95
+ updateExperiment,
96
+ updateGate,
97
+ updateGateTemplate,
98
+ updateI18nDraft,
99
+ updateI18nKey,
100
+ updateKillswitch,
101
+ updateMetric,
102
+ updateOpsItem,
103
+ updateProject,
104
+ updateTriggerConnector,
105
+ updateUniverse,
106
+ upsertProject
107
+ } from "./chunk-643YSMLU.js";
108
+ import {
109
+ zErrorCode
110
+ } from "./chunk-MRW3DJ57.js";
111
+
112
+ // src/errors.ts
113
+ var ERROR_CODES = zErrorCode.options;
114
+ function isApiErrorBody(value) {
115
+ return typeof value === "object" && value !== null && "error" in value && typeof value.error === "string";
116
+ }
117
+ function errorCodeOf(value) {
118
+ if (!isApiErrorBody(value)) return void 0;
119
+ const code = value.code;
120
+ return code && ERROR_CODES.includes(code) ? code : void 0;
121
+ }
122
+ var REAUTH_COMMAND = {
123
+ cli: "shipeasy login --force",
124
+ mcp: "shipeasy-mcp install --force"
9
125
  };
10
- function createHttpTransport(opts) {
11
- const baseUrl = opts.baseUrl.replace(/\/$/, "");
12
- return {
13
- async request(method, path, body, query) {
14
- if (method !== "GET" && opts.beforeMutation) {
15
- await opts.beforeMutation(method, path);
16
- }
17
- const auth = opts.getAuth();
18
- const url = new URL(`${baseUrl}${path}`);
19
- if (query) {
20
- for (const [k, v] of Object.entries(query)) url.searchParams.set(k, v);
21
- }
22
- const res = await fetch(url.toString(), {
23
- method,
24
- headers: {
25
- "X-SDK-Key": auth.token,
26
- "X-Project-Id": auth.projectId,
27
- "Content-Type": "application/json"
28
- },
29
- ...body !== void 0 ? { body: JSON.stringify(body) } : {}
30
- });
31
- if (res.status === 204 || res.status === 202) return void 0;
32
- const json = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
33
- if (!res.ok) {
34
- const msg = json.error ?? `HTTP ${res.status}`;
35
- throw new ApiError(msg, res.status);
36
- }
37
- return json;
38
- }
39
- };
40
- }
41
-
42
- // src/schemas/pagination.ts
43
- import { z } from "zod";
44
- var DEFAULT_PAGE_SIZE = 100;
45
- var MAX_PAGE_SIZE = 500;
46
- var pageQuerySchema = z.object({
47
- limit: z.coerce.number().int().min(1).max(MAX_PAGE_SIZE).default(DEFAULT_PAGE_SIZE),
48
- cursor: z.string().min(1).optional()
49
- });
50
- var SEP = "|";
51
- function encodeCursor(parts) {
52
- if (parts.ts.includes(SEP)) {
53
- throw new Error(`pagination: ts must not contain '${SEP}'`);
54
- }
55
- return base64UrlEncode(`${parts.ts}${SEP}${parts.id}`);
56
- }
57
- function decodeCursor(cursor) {
58
- let raw;
59
- try {
60
- raw = base64UrlDecode(cursor);
61
- } catch {
62
- throw new Error("pagination: cursor is not valid base64url");
63
- }
64
- const sep = raw.indexOf(SEP);
65
- if (sep < 0) throw new Error("pagination: cursor missing separator");
66
- const ts = raw.slice(0, sep);
67
- const id = raw.slice(sep + 1);
68
- if (!ts || !id) throw new Error("pagination: cursor parts empty");
69
- return { ts, id };
126
+ function formatAuthFailure(surface, status, message) {
127
+ const reauth = REAUTH_COMMAND[surface];
128
+ if (status === 401) {
129
+ return [
130
+ `AUTH_REQUIRED: ${message} (401)`,
131
+ ``,
132
+ `Cause: the request's Shipeasy credentials are missing, invalid, or expired.`,
133
+ `Fix:`,
134
+ ` 1. Run: ${reauth}`,
135
+ ` 2. Retry the command that failed.`
136
+ ].join("\n");
137
+ }
138
+ if (status === 403) {
139
+ return [
140
+ `AUTH_FORBIDDEN: ${message} (403)`,
141
+ ``,
142
+ `Cause: the credentials are valid but don't have access to this project or resource.`,
143
+ `Fix:`,
144
+ ` 1. Confirm the bound project is correct (check the .shipeasy file, or run: shipeasy root --json).`,
145
+ ` 2. If it's the wrong project, run: shipeasy bind <project_id>.`,
146
+ ` 3. If it's the right project, ask a workspace admin to grant access \u2014 retrying will not help.`
147
+ ].join("\n");
148
+ }
149
+ return void 0;
70
150
  }
71
- function base64UrlEncode(s) {
72
- const b64 = btoa(s);
73
- return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
74
- }
75
- function base64UrlDecode(s) {
76
- const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
77
- return atob(s.replace(/-/g, "+").replace(/_/g, "/") + pad);
78
- }
79
-
80
- // src/resources/gates.ts
81
- import { z as z4 } from "zod";
82
-
83
- // src/schemas/gates.ts
84
- import { z as z3 } from "zod";
85
-
86
- // src/schemas/folder.ts
87
- import { z as z2 } from "zod";
88
- var folderSchema = z2.string().max(256).regex(/^[a-zA-Z0-9_-]+$/, "Folder must be alphanumeric, `_` or `-` (no `/`).").nullable().optional().describe(
89
- "Optional folder name used to group items in the dashboard. Part of the SDK lookup key: an item in folder `checkout` named `new-cart` is referenced as `checkout/new-cart` from the SDK."
90
- );
91
-
92
- // src/schemas/gates.ts
93
- var gateRuleSchema = z3.object({
94
- attr: z3.string().min(1).describe(
95
- "Attribute key on the evaluation context (e.g. `country`, `plan`, `email`). Matched case-sensitively against `Shipeasy.checkGate(user, name)` input."
96
- ),
97
- op: z3.enum([
98
- "eq",
99
- "neq",
100
- "in",
101
- "not_in",
102
- "gt",
103
- "gte",
104
- "lt",
105
- "lte",
106
- "contains",
107
- "regex",
108
- "semver_gt",
109
- "semver_gte",
110
- "semver_lt",
111
- "semver_lte",
112
- "gate_pass",
113
- "exp_in"
114
- ]).describe(
115
- 'Comparison operator. Equality: `eq`/`neq`. Set membership: `in`/`not_in` (value is an array). Numeric order: `gt`/`gte`/`lt`/`lte`. Semver order: `semver_gt`/`semver_gte`/`semver_lt`/`semver_lte` (value is a dotted version like `4.2.0`). String: `contains` (substring), `regex` (JS-flavour pattern, value is the pattern string). Cross-resource references (evaluated only on the worker hot path, where both KV blobs load): `gate_pass` (`value` is another gate\'s name \u2014 true when that gate passes for the same caller); `exp_in` (`attr` is a running experiment\'s name, `value` is a group name, `"$holdout"`, or `"$any"` \u2014 true when the caller is in that variant / the universe holdout / any variant).'
116
- ),
117
- value: z3.unknown().describe(
118
- 'Operand. Shape depends on `op`: scalar for `eq/neq/gt/gte/lt/lte/contains/semver_*`; array of scalars for `in/not_in`; string pattern for `regex`; referenced gate name for `gate_pass`; group name / `"$holdout"` / `"$any"` for `exp_in`.'
119
- )
120
- }).describe(
121
- "Single targeting predicate. A gate's `rules` array is evaluated with AND semantics; use the gatekeeper `stack` for OR / fall-through logic."
122
- );
123
- var gateNameSchema = z3.string().regex(
124
- /^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?)?$/,
125
- "lowercase, digits, _/-; optional folder.name shape"
126
- ).max(128).describe(
127
- "Stable gate key used by SDKs (`Shipeasy.checkGate(user, '<name>')`). Single segment or `folder.name`. Lowercase letters, digits, `_` or `-`; max 128 chars. Immutable after create \u2014 rename = delete + recreate."
128
- );
129
- var rampSchema = z3.object({
130
- from: z3.number().int().min(0).max(1e4).describe("Start of the ramp in basis points (0\u201310000)."),
131
- to: z3.number().int().min(0).max(1e4).describe("End of the ramp in basis points (0\u201310000)."),
132
- startAt: z3.number().int().nonnegative().describe("Ramp start, epoch milliseconds (UTC)."),
133
- durationMs: z3.number().int().positive().describe("Ramp length in milliseconds.")
134
- }).describe(
135
- "Linear rollout ramp. The effective rollout % interpolates `from`\u2192`to` over `[startAt, startAt+durationMs]`, clamped outside that window, evaluated against the wall-clock at eval time. Overrides the entry's static `rolloutPct` while present."
136
- );
137
- var stackedConditionSchema = z3.object({
138
- id: z3.string().min(1).describe(
139
- "Client-supplied stable id for the stack entry (used for React keys, audit diffs, and as the default per-condition rollout salt)."
140
- ),
141
- type: z3.literal("condition").describe("Discriminator. `condition` = rule-based predicate sub-gate."),
142
- name: z3.string().optional().describe("Human label shown in the dashboard."),
143
- fromTemplate: z3.string().nullable().optional().describe(
144
- "Id of the project template this entry was seeded from, or `null` if hand-authored."
145
- ),
146
- pass: z3.enum(["all", "any"]).optional().describe("Combinator across `rules`. `all` = AND (default), `any` = OR."),
147
- rules: z3.array(gateRuleSchema).default([]).describe("Predicates evaluated under `pass`. Empty array = match nothing."),
148
- rolloutPct: z3.number().int().min(0).max(1e4).optional().describe(
149
- "Per-condition rollout in basis points. After the rules match, the caller is bucketed at this %; in-bucket \u2192 gate `true`, otherwise fall through to the next entry. Absent \u21D2 10000 (100%, every matching caller passes)."
150
- ),
151
- bucketBy: z3.string().optional().describe("Attribute used to hash the caller into the per-condition bucket. Default `userID`."),
152
- salt: z3.string().optional().describe("Per-condition hash salt. Defaults to the entry `id`, then the gate salt."),
153
- ramp: rampSchema.optional(),
154
- locked: z3.boolean().optional().describe("If `true`, dashboard hides edit controls (e.g. the trailing public-floor entry).")
155
- }).describe(
156
- "Rule-based sub-gate. When the rules match and the caller falls in the per-condition rollout bucket, evaluation short-circuits and the gate returns `true`; otherwise it falls through to the next entry."
157
- );
158
- var stackedRolloutSchema = z3.object({
159
- id: z3.string().min(1).describe("Client-supplied stable id for the stack entry."),
160
- type: z3.literal("rollout").describe("Discriminator. `rollout` = percentage-bucket sub-gate."),
161
- name: z3.string().optional().describe("Human label shown in the dashboard."),
162
- fromTemplate: z3.string().nullable().optional().describe("Source template id, or `null`."),
163
- rolloutPct: z3.number().int().min(0).max(1e4).describe(
164
- "Bucket size in basis points (0\u201310000 = 0%\u2013100%). `5000` = 50%, `100` = 1%, allows sub-1% precision."
165
- ),
166
- bucketBy: z3.string().optional().describe(
167
- "Attribute used to hash the caller into a bucket. Defaults to `user.userID`. Use e.g. `accountID` to bucket whole accounts together."
168
- ),
169
- salt: z3.string().optional().describe(
170
- "Per-entry hash salt. Defaults to the gate's salt. Set explicitly to keep two rollouts independent or correlated across gates."
171
- ),
172
- ramp: rampSchema.optional(),
173
- locked: z3.boolean().optional().describe("If `true`, dashboard hides edit controls.")
174
- }).describe(
175
- "Percentage-rollout sub-gate. Hashes `bucketBy` (default `userID`) with `salt` and returns `true` for `rolloutPct/10000` fraction of callers (or the ramp's effective % when `ramp` is set)."
176
- );
177
- var stackedGateEntrySchema = z3.discriminatedUnion("type", [stackedConditionSchema, stackedRolloutSchema]).describe(
178
- "One entry in the ordered gatekeeper stack. Evaluated top-to-bottom; first matching entry decides. Mix `condition` and `rollout` entries to express `internal-only` + `1% beta` + `50% public` in a single gate."
179
- );
180
- var gateMetadataFields = {
181
- title: z3.string().max(140).optional().describe("Human-readable title shown in the dashboard. Free-form, no key format constraint."),
182
- description: z3.string().max(2e3).optional().describe("Long-form description / runbook. Markdown is rendered in the dashboard."),
183
- folder: folderSchema,
184
- group: z3.string().max(64).optional().describe("Group label for dashboard organisation (e.g. team or product area)."),
185
- owner_email: z3.string().max(190).optional().describe("Owner contact. Displayed verbatim; not used for auth.")
186
- };
187
- var gateMetadataPatchSchema = z3.object(gateMetadataFields);
188
- var gateCreateSchema = z3.object({
189
- name: gateNameSchema,
190
- enabled: z3.boolean().default(true).describe(
191
- "Master switch. Defaults to `true`. Set `false` to create the gate disabled (evaluates to `false` regardless of rules/rollout); flip on via `POST /{id}/enable` or PATCH."
192
- ),
193
- rollout_pct: z3.number().int().min(0).max(1e4).default(0).describe(
194
- "Initial rollout in **basis points** (0\u201310000 = 0%\u2013100%) \u2014 `100` here means **1%**, not 100%. Use `rollout_percent` (0\u2013100) below if you'd rather think in percent. Use `0` to create the gate dark and ramp via PATCH after deploy validation."
195
- ),
196
- rollout_percent: z3.number().min(0).max(100).optional().describe(
197
- "Initial rollout as a **percentage** (0\u2013100, fractional ok). Friendlier alias for `rollout_pct`; converted internally to basis points (e.g. `100` here = 10000 bp = 100%). If both `rollout_pct` and `rollout_percent` are set, `rollout_percent` wins."
198
- ),
199
- rules: z3.array(gateRuleSchema).default([]).describe(
200
- "Targeting predicates. AND-combined. If non-empty, the gate returns `true` only for callers that satisfy every rule **and** fall under `rollout_pct`."
201
- ),
202
- salt: z3.string().min(1).max(64).optional().describe(
203
- "Hash salt for percentage bucketing. Auto-generated if omitted. Provide explicitly to keep a gate's buckets stable across delete/recreate. **Immutable after create** \u2014 there is no PATCH for `salt` because changing it would re-bucket every caller."
204
- ),
205
- stack: z3.array(stackedGateEntrySchema).nullable().optional().describe(
206
- "Optional gatekeeper stack. When provided, takes precedence over `rules` + `rollout_pct` at evaluation time. Omit (or pass `null`) for a flat gate."
207
- ),
208
- ...gateMetadataFields
209
- }).describe(
210
- "Body for `POST /api/admin/gates`. At minimum supply `name`; everything else has sensible defaults."
211
- );
212
- var gateUpdateSchema = z3.object({
213
- rollout_pct: z3.number().int().min(0).max(1e4).optional().describe(
214
- "New rollout in **basis points** (0\u201310000 = 0%\u2013100%) \u2014 `100` here means **1%**. Use `rollout_percent` (0\u2013100) below for percent. Omit both to leave unchanged."
215
- ),
216
- rollout_percent: z3.number().min(0).max(100).optional().describe(
217
- "New rollout as a **percentage** (0\u2013100). Friendlier alias for `rollout_pct`; converted internally. Wins over `rollout_pct` if both are supplied. Omit both to leave unchanged."
218
- ),
219
- rules: z3.array(gateRuleSchema).optional().describe(
220
- "Replaces the rule list wholesale. To add a value to an `in` rule, send the full new `rules` array with the augmented `value` (e.g. previous `['US','CA']` \u2192 `['US','CA','GB']`)."
221
- ),
222
- enabled: z3.boolean().optional().describe(
223
- "Master switch. `false` makes the gate evaluate to `false` for every caller regardless of `rollout_pct`, `rules`, or `stack` \u2014 use as kill switch."
224
- ),
225
- stack: z3.array(stackedGateEntrySchema).nullable().optional().describe(
226
- "Replaces the gatekeeper stack wholesale. Send `null` to revert to flat `rules` + `rollout_pct` evaluation."
227
- ),
228
- ...gateMetadataFields
229
- }).describe(
230
- "Body for `PATCH /api/admin/gates/{id}`. Partial \u2014 only supplied fields change. Array fields (`rules`, `stack`) replace, not merge."
231
- );
232
- var gateResponseSchema = z3.object({
233
- id: z3.string().describe("Stable opaque gate id (`gat_\u2026`)."),
234
- name: gateNameSchema,
235
- /** D1 stores enabled as 0/1; clients see either form. */
236
- enabled: z3.union([z3.boolean(), z3.number().int().min(0).max(1)]).describe("Master switch. Returned as `1`/`0` from D1; treat truthy as enabled."),
237
- /** Rollout percentage in basis points (0–10000 = 0%–100%). */
238
- rolloutPct: z3.number().int().min(0).max(1e4).describe("Current rollout in basis points (0\u201310000)."),
239
- rules: z3.array(gateRuleSchema).optional(),
240
- salt: z3.string().optional().describe("Hash salt used for percentage bucketing."),
241
- title: z3.string().nullable().optional(),
242
- description: z3.string().nullable().optional(),
243
- folder: z3.string().nullable().optional(),
244
- groupName: z3.string().nullable().optional().describe("Group label (note: `groupName` in response, `group` in request)."),
245
- ownerEmail: z3.string().nullable().optional().describe("Owner email (note: `ownerEmail` in response, `owner_email` in request)."),
246
- stack: z3.array(stackedGateEntrySchema).nullable().optional(),
247
- updatedAt: z3.string().describe("ISO-8601 timestamp of last mutation.")
248
- });
249
- var gateCreateResponseSchema = z3.object({
250
- id: z3.string().describe("Newly assigned gate id (`gat_\u2026`)."),
251
- name: gateNameSchema
252
- });
253
- var gateUpdateResponseSchema = z3.object({ id: z3.string().describe("Gate id that was updated.") }).describe("Update returns only the id \u2014 re-fetch via `GET /api/admin/gates` for the full row.");
254
- var gateDeleteResponseSchema = z3.object({ ok: z3.literal(true) });
255
- var gateToggleResponseSchema = z3.object({
256
- id: z3.string(),
257
- enabled: z3.boolean()
258
- });
259
-
260
- // src/resources/gates.ts
261
- var gateListResponseSchema = z4.object({
262
- data: z4.array(gateResponseSchema),
263
- next_cursor: z4.string().nullable()
264
- });
265
- var BASE = "/api/admin/gates";
266
- function gatesClient(t) {
267
- async function list(opts = {}) {
268
- const query = {};
269
- if (opts.limit !== void 0) query.limit = String(opts.limit);
270
- if (opts.cursor) query.cursor = opts.cursor;
271
- return t.request("GET", BASE, void 0, query);
272
- }
273
- async function listAll() {
274
- const out = [];
275
- let cursor;
276
- do {
277
- const page = await list({ limit: 500, cursor });
278
- out.push(...page.data);
279
- cursor = page.next_cursor ?? void 0;
280
- } while (cursor);
281
- return out;
282
- }
283
- async function resolve(idOrName) {
284
- const all = await listAll();
285
- const found = all.find((g) => g.id === idOrName) ?? all.find((g) => g.name === idOrName);
286
- if (!found) throw new ApiError(`Gate '${idOrName}' not found`, 404);
287
- return found;
288
- }
289
- return {
290
- list,
291
- listAll,
292
- resolve,
293
- getByName: async (name) => {
294
- const found = (await listAll()).find((g) => g.name === name);
295
- if (!found) throw new ApiError(`Gate '${name}' not found`, 404);
296
- return found;
297
- },
298
- create: (input) => t.request("POST", BASE, gateCreateSchema.parse(input)),
299
- update: (id, input) => t.request("PATCH", `${BASE}/${id}`, gateUpdateSchema.parse(input)),
300
- delete: (id) => t.request("DELETE", `${BASE}/${id}`),
301
- enable: (id) => t.request("POST", `${BASE}/${id}/enable`),
302
- disable: (id) => t.request("POST", `${BASE}/${id}/disable`),
303
- setRollout: async (name, pct) => {
304
- if (!Number.isFinite(pct) || pct < 0 || pct > 100) {
305
- throw new ApiError("rollout must be a number between 0 and 100", 400);
306
- }
307
- const gate = await resolve(name);
308
- return t.request("PATCH", `${BASE}/${gate.id}`, {
309
- rollout_pct: Math.round(pct * 100)
310
- });
311
- }
312
- };
313
- }
314
- var gatesResource = {
315
- name: "gates",
316
- basePath: BASE,
317
- describeOne: "feature gate",
318
- describeMany: "feature gates",
319
- tag: {
320
- name: "Gates",
321
- description: [
322
- "Feature gates: boolean flags evaluated at runtime against project rules + a percentage rollout.",
323
- "",
324
- "**Identity.** Each gate is keyed by a stable `name` (a-z, 0-9, `_`/`-`, max 64 chars) which is what SDKs pass to `Shipeasy.checkGate(user, '<name>')`. The `name` is immutable \u2014 rename means delete + recreate.",
325
- "",
326
- "**Evaluation model.** A gate returns `true` when (a) `enabled` is true, and (b) the caller satisfies the gate's rules. There are two evaluation shapes:",
327
- "- **Flat** \u2014 `rules` (AND-combined predicates) gate the caller, then `rollout_pct` (basis points, 0\u201310000) hashes them into a bucket. Used for simple `is in X% rollout` gates.",
328
- "- **Gatekeeper stack** \u2014 an ordered array of `condition` and `rollout` sub-gates, evaluated top-to-bottom; first match wins. Used to express `internal-only \u222A 1% beta \u222A 50% public` in one gate. When `stack` is present it takes precedence over the flat fields.",
329
- "",
330
- "**Rules.** Each rule is `{ attr, op, value }`. Supported ops: `eq`, `neq`, `in`, `not_in`, `gt`, `gte`, `lt`, `lte`, `contains`, `regex`. Attribute names match the keys on the SDK evaluation context (e.g. `country`, `plan`, `email`).",
331
- "",
332
- "**Rollout basis points.** `rollout_pct` is in **basis points**, not percent. `0` = 0%, `100` = 1%, `5000` = 50%, `10000` = 100%. This allows sub-1% precision (e.g. `7` = 0.07%).",
333
- "",
334
- "**Lifecycle.** Create dark (`rollout_pct: 0`) \u2192 attach rules \u2192 ramp via PATCH \u2192 flip kill-switch via `disable`/`enable` \u2192 delete once retired. Deletion is blocked while a running experiment references the gate as a targeting gate."
335
- ].join("\n")
336
- },
337
- schemas: {
338
- create: gateCreateSchema,
339
- update: gateUpdateSchema
340
- },
341
- /** Custom action endpoints beyond CRUD (POST <basePath>/:id/<action>). */
342
- actions: [
343
- { name: "enable", description: "Enable a gate" },
344
- { name: "disable", description: "Disable a gate" }
345
- ],
346
- /**
347
- * Full operation list for the OpenAPI doc. Order here is the order shown in
348
- * the docs sidebar. The typed client (`gatesClient`) is the runtime contract;
349
- * this is the documented surface.
350
- */
351
- endpoints: [
352
- {
353
- operationId: "listGates",
354
- method: "GET",
355
- path: "",
356
- summary: "List feature gates",
357
- description: "Returns a single page of gates ordered by `updated_at desc, id desc`. Use the `cursor` query parameter to paginate.",
358
- response: gateListResponseSchema,
359
- examples: {
360
- response: {
361
- data: [
362
- {
363
- id: "gat_01j7w7m9q4hxbf6npe6s9zr3vc",
364
- name: "checkout_v2",
365
- enabled: 1,
366
- rolloutPct: 5e3,
367
- rules: [
368
- { attr: "country", op: "in", value: ["US", "CA", "GB"] },
369
- { attr: "plan", op: "neq", value: "free" }
370
- ],
371
- salt: "9c1f4f1f2c0c4a5fa1c2b6d3e7c8e3a1",
372
- title: "Checkout v2",
373
- description: "New checkout flow. Pro users in US/CA/GB only.",
374
- folder: "checkout",
375
- groupName: "growth",
376
- ownerEmail: "ana@example.com",
377
- stack: null,
378
- updatedAt: "2026-05-09T16:01:22.000Z"
379
- }
380
- ],
381
- next_cursor: null
382
- }
383
- },
384
- useCase: "Snapshot every gate in the project \u2014 for example to render an admin overview or to drive a CI check that asserts no gate is left at 100% in staging."
385
- },
386
- {
387
- operationId: "createGate",
388
- method: "POST",
389
- path: "",
390
- summary: "Create a feature gate",
391
- description: [
392
- "Creates a new gate. Default `enabled: true` at the supplied `rollout_pct` (`0` = fully dark).",
393
- "",
394
- "Only `name` is required. Request fields use snake_case (`owner_email`); the GET response returns camelCase (`ownerEmail`, `groupName`).",
395
- "",
396
- "Returns `409` if `name` already exists in the project (case-sensitive)."
397
- ].join("\n"),
398
- successStatus: 201,
399
- request: gateCreateSchema,
400
- response: gateCreateResponseSchema,
401
- examples: {
402
- requestExamples: {
403
- minimal: {
404
- summary: "Minimal \u2014 dark gate, name only",
405
- description: "Smallest valid body. The gate is created `enabled: true` at `rollout_pct: 0`, with no rules. Ramp it later via PATCH.",
406
- value: { name: "checkout_v2" }
407
- },
408
- targeted: {
409
- summary: "Targeted rollout \u2014 rules + percentage",
410
- description: "Gate Pro users in US/CA/GB, then hash 10% of that audience into the on-bucket (`rollout_pct: 1000` = 10.00%). Adds dashboard metadata so the gate is self-documenting.",
411
- value: {
412
- name: "checkout_v2",
413
- rollout_pct: 1e3,
414
- rules: [
415
- { attr: "country", op: "in", value: ["US", "CA", "GB"] },
416
- { attr: "plan", op: "neq", value: "free" }
417
- ],
418
- title: "Checkout v2",
419
- description: "New checkout flow. Pro users in US/CA/GB only.",
420
- folder: "checkout",
421
- group: "growth",
422
- owner_email: "ana@example.com"
423
- }
424
- },
425
- gatekeeperStack: {
426
- summary: "Gatekeeper stack \u2014 internal \u222A beta \u222A public",
427
- description: "Three-layer fall-through: employees always on, opted-in beta accounts always on, then 25% of everyone else. First match wins; evaluation stops as soon as one entry returns `true`.",
428
- value: {
429
- name: "checkout_v2",
430
- title: "Checkout v2",
431
- owner_email: "ana@example.com",
432
- stack: [
433
- {
434
- id: "01HKJ0EMP",
435
- type: "condition",
436
- name: "Employees",
437
- pass: "any",
438
- rules: [{ attr: "email", op: "regex", value: "@acme\\.com$" }]
439
- },
440
- {
441
- id: "01HKJ0BETA",
442
- type: "condition",
443
- name: "Beta opt-in",
444
- pass: "all",
445
- rules: [{ attr: "beta_opt_in", op: "eq", value: true }]
446
- },
447
- {
448
- id: "01HKJ0PUB",
449
- type: "rollout",
450
- name: "Public",
451
- rolloutPct: 2500,
452
- bucketBy: "userID",
453
- locked: true
454
- }
455
- ]
456
- }
457
- },
458
- disabledOnCreate: {
459
- summary: "Disabled on create \u2014 provision then enable later",
460
- description: "Create with `enabled: false` so the gate evaluates to `false` for every caller until a follow-up `POST /{id}/enable`. Useful when provisioning ahead of a launch.",
461
- value: { name: "checkout_v2", enabled: false, rollout_pct: 0 }
462
- }
463
- },
464
- response: { id: "gat_01j7w7m9q4hxbf6npe6s9zr3vc", name: "checkout_v2" }
465
- },
466
- useCase: [
467
- '- **Dark create + ramp later** \u2014 `{ "name": "checkout_v2" }` at 0% rollout. Ramp via PATCH after deploy validation.',
468
- "- **Targeted rollout** \u2014 supply `rules` to gate the caller (e.g. only `plan = pro` users) plus a `rollout_pct` to bucket within that audience.",
469
- "- **Gatekeeper stack** \u2014 supply `stack` instead of `rules`/`rollout_pct` for `internal \u222A beta \u222A public` fall-through. Stack entries evaluated top-to-bottom; first match wins.",
470
- "- **Dashboard metadata** \u2014 populate `title`, `description`, `folder`, `group`, `owner_email` so the admin UI is self-documenting from day one.",
471
- "- **Disabled on create** \u2014 pre-provision with `enabled: false` for a future launch; flip on with `POST /{id}/enable` at go-live."
472
- ].join("\n")
473
- },
474
- {
475
- operationId: "updateGate",
476
- method: "PATCH",
477
- path: "/{id}",
478
- summary: "Update a feature gate",
479
- description: [
480
- "Partial update \u2014 only supplied fields change. Array fields (`rules`, `stack`) **replace** wholesale; there is no merge or append.",
481
- "",
482
- "`name` and the gate id are immutable. The response carries only `{ id }` \u2014 re-fetch via `GET /api/admin/gates` for the new row."
483
- ].join("\n"),
484
- pathParams: { id: "Stable opaque gate id (`gat_\u2026`)." },
485
- request: gateUpdateSchema,
486
- response: gateUpdateResponseSchema,
487
- examples: {
488
- requestExamples: {
489
- rampRollout: {
490
- summary: "Ramp rollout \u2014 0 \u2192 50%",
491
- description: "Single-field patch. `5000` basis points = 50%.",
492
- value: { rollout_pct: 5e3 }
493
- },
494
- killSwitch: {
495
- summary: "Kill switch \u2014 disable the gate",
496
- description: "Forces evaluation to `false` for every caller, regardless of rules or rollout. Same effect as `POST /{id}/disable` but composable with other patches.",
497
- value: { enabled: false }
498
- },
499
- extendInRule: {
500
- summary: "Add a value to an `in` rule",
501
- description: "Array fields replace wholesale \u2014 there is no per-rule patch. To add `'AU'` to a country whitelist that previously held `['US','CA','GB']`, send the full new `rules` array. Same pattern applies to `not_in`.",
502
- value: {
503
- rules: [
504
- { attr: "country", op: "in", value: ["US", "CA", "GB", "AU"] },
505
- { attr: "plan", op: "neq", value: "free" }
506
- ]
507
- }
508
- },
509
- replaceStack: {
510
- summary: "Replace gatekeeper stack",
511
- description: 'Stack is replace-wholesale. Send the full new ordered list. Pass `"stack": null` to revert the gate to flat `rules` + `rollout_pct` evaluation.',
512
- value: {
513
- stack: [
514
- {
515
- id: "01HKJ0EMP",
516
- type: "condition",
517
- name: "Employees",
518
- pass: "any",
519
- rules: [{ attr: "email", op: "regex", value: "@acme\\.com$" }]
520
- },
521
- {
522
- id: "01HKJ0PUB",
523
- type: "rollout",
524
- name: "Public",
525
- rolloutPct: 5e3,
526
- locked: true
527
- }
528
- ]
529
- }
530
- },
531
- updateMetadata: {
532
- summary: "Update dashboard metadata",
533
- description: "Any subset of `title`, `description`, `folder`, `group`, `owner_email`. Snake-case in the request; the GET response returns camelCase (`groupName`, `ownerEmail`).",
534
- value: {
535
- title: "Checkout v2 (50%)",
536
- description: "Bumped to 50% after passing canary. Added AU.",
537
- folder: "checkout",
538
- owner_email: "ana@example.com"
539
- }
540
- }
541
- },
542
- response: { id: "gat_01j7w7m9q4hxbf6npe6s9zr3vc" }
543
- },
544
- useCase: [
545
- '- **Ramp rollout** \u2014 `{ "rollout_pct": 5000 }` for 50%. Basis points (0\u201310000); `100` = 1%.',
546
- '- **Kill switch** \u2014 `{ "enabled": false }`. Forces evaluation to `false` for every caller regardless of rules/rollout. Re-enable with `POST /{id}/enable` or `{ "enabled": true }`.',
547
- '- **Modify a rule\'s `in` set** \u2014 send the full new `rules` array. To add `\'GB\'` to `[\'US\',\'CA\']`: `{ "rules": [{ "attr": "country", "op": "in", "value": ["US","CA","GB"] }] }`. No per-rule patch endpoint.',
548
- '- **Add targeting from scratch** \u2014 `{ "rules": [{ "attr": "email", "op": "regex", "value": "@acme\\\\.com$" }] }`.',
549
- '- **Switch to gatekeeper stack** \u2014 send a non-null `stack`. To revert to flat eval, send `{ "stack": null }`.',
550
- "- **Update metadata** \u2014 any subset of `title`, `description`, `folder`, `group`, `owner_email`."
551
- ].join("\n")
552
- },
553
- {
554
- operationId: "deleteGate",
555
- method: "DELETE",
556
- path: "/{id}",
557
- summary: "Delete a feature gate",
558
- description: "Soft-deletes the gate. Returns 409 if the gate is still referenced by a running experiment as a targeting gate \u2014 stop the experiment first.",
559
- pathParams: { id: "Stable opaque gate id (`gat_\u2026`)." },
560
- response: gateDeleteResponseSchema,
561
- examples: { response: { ok: true } },
562
- useCase: "Tear down a gate after a feature has fully shipped and the rollout flag is no longer needed."
563
- },
564
- {
565
- operationId: "enableGate",
566
- method: "POST",
567
- path: "/{id}/enable",
568
- summary: "Enable a gate",
569
- description: "Sets `enabled: true`. The current `rollout_pct` is preserved.",
570
- pathParams: { id: "Stable opaque gate id." },
571
- response: gateToggleResponseSchema,
572
- examples: { response: { id: "gat_01j7w7m9q4hxbf6npe6s9zr3vc", enabled: true } },
573
- useCase: "Re-enable a previously disabled gate without re-issuing a full update."
574
- },
575
- {
576
- operationId: "disableGate",
577
- method: "POST",
578
- path: "/{id}/disable",
579
- summary: "Disable a gate",
580
- description: "Sets `enabled: false` so the gate evaluates to `false` for every caller, regardless of `rollout_pct` or `rules`. Use as a quick kill switch.",
581
- pathParams: { id: "Stable opaque gate id." },
582
- response: gateToggleResponseSchema,
583
- examples: { response: { id: "gat_01j7w7m9q4hxbf6npe6s9zr3vc", enabled: false } },
584
- useCase: "Flip a gate off in production without redeploying \u2014 the canonical kill-switch flow."
585
- }
586
- ]
587
- };
588
-
589
- // src/resources/experiments.ts
590
- import { z as z7 } from "zod";
591
-
592
- // src/schemas/experiments.ts
593
- import { z as z6 } from "zod";
594
-
595
- // src/schemas/metric-name.ts
596
- import { z as z5 } from "zod";
597
- var metricNameSchema = z5.string().regex(/^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?)?$/).max(128);
598
-
599
- // src/schemas/experiments.ts
600
- var experimentInlineMetricSchema = z6.object({
601
- name: metricNameSchema.optional(),
602
- query: z6.string().min(1).max(4096).describe("Metric DSL string, e.g. `count_users(checkout_completed)`.")
603
- });
604
- var experimentNameSchema = z6.string().regex(/^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?)?$/).max(128).describe(
605
- "Stable experiment key. Single segment or `folder.name` (a-z, 0-9, `_`/`-`; max 128 chars). Used by SDKs as `Shipeasy.getExperiment(user, '<name>')`. Immutable after create."
606
- );
607
- var experimentGroupSchema = z6.object({
608
- name: z6.string().min(1).max(64).describe("Group label (e.g. `control`, `treatment_a`). Max 64 chars."),
609
- weight: z6.number().int().min(0).max(1e4).describe(
610
- "Allocation weight in basis points (0\u201310000). The sum across all groups must equal exactly **10000** (100%)."
611
- ),
612
- params: z6.record(z6.string(), z6.unknown()).default({}).describe(
613
- "Per-group parameter values delivered to the SDK when a caller is hashed into this group. Keys must match the experiment's `params` schema."
614
- )
615
- }).describe("One variant in the experiment. Weights across all groups must sum to 10000.");
616
- var paramSchemaSchema = z6.record(z6.string(), z6.enum(["string", "bool", "number"])).describe(
617
- "Map of param-name \u2192 scalar type. Defines the shape of `groups[].params`. Example: `{ headline: 'string', show_cta: 'bool' }`."
618
- );
619
- var bucketBySchema = z6.string().min(1).max(128).regex(
620
- /^[a-zA-Z_][a-zA-Z0-9_]*$/,
621
- "Must be a user-attribute name (letters, digits, underscore; not starting with a digit)."
622
- ).describe(
623
- "User-attribute name used as the bucketing key \u2014 e.g. `user_id` (default), `session_id`, `device_id`, or a custom attribute like `company_id` to keep a whole org on one variant. Defaults to `user_id` when omitted."
624
- );
625
- var experimentCreateSchema = z6.object({
626
- name: experimentNameSchema,
627
- description: z6.string().max(2e3).nullable().default(null).describe("Free-form description. Max 2000 chars, markdown rendered in the dashboard."),
628
- hypothesis: z6.string().max(4e3).nullable().default(null).describe("Hypothesis statement shown in the editor. Display-only."),
629
- tag: z6.string().max(64).nullable().default(null).describe("Short tag chip rendered next to the name. Display-only."),
630
- owner_email: z6.string().max(254).nullable().default(null).describe("Owner email. Display-only."),
631
- audience: z6.string().max(256).nullable().default(null).describe("Audience label shown in the editor. Display-only."),
632
- bucket_by: bucketBySchema.nullable().default(null),
633
- folder: folderSchema,
634
- universe: z6.string().min(1).describe(
635
- "Name of an existing universe in the project. Returns `422` if the universe doesn't exist."
636
- ),
637
- targeting_gate: z6.string().nullable().default(null).describe(
638
- "Optional gate name. Only callers that pass the gate are enrolled in the experiment."
639
- ),
640
- allocation_pct: z6.number().int().min(0).max(1e4).default(0).describe(
641
- "Share of the (gated) audience allocated to the experiment, in basis points (0\u201310000 = 0%\u2013100%). `0` = unallocated. Immutable while the experiment is running."
642
- ),
643
- salt: z6.string().min(1).max(64).optional().describe("Hash salt for bucketing. Auto-generated if omitted. Immutable while running."),
644
- params: paramSchemaSchema.default({}),
645
- groups: z6.array(experimentGroupSchema).min(2).describe(
646
- "Two or more variants. Weights must sum to exactly 10000 (100%). Immutable while running."
647
- ),
648
- significance_threshold: z6.number().min(1e-4).max(0.5).default(0.05).describe(
649
- "p-value cutoff used by the analysis pass. Defaults to `0.05`. Values other than 0.05 require Pro plan or higher."
650
- ),
651
- min_runtime_days: z6.number().int().min(0).max(365).default(0).describe("Minimum days the experiment must run before results are considered conclusive."),
652
- min_sample_size: z6.number().int().min(1).default(100).describe("Minimum exposures per group before results are considered conclusive."),
653
- sequential_testing: z6.boolean().default(false).describe(
654
- "Enable sequential testing (always-valid p-values). Requires Premium plan or higher."
655
- ),
656
- goal_metric: experimentInlineMetricSchema.optional().describe(
657
- "Single goal metric defined inline as a DSL query. The underlying event is auto-created if missing."
658
- ),
659
- guardrail_metrics: z6.array(experimentInlineMetricSchema).max(10).default([]).describe(
660
- "Up to 10 guardrail metrics defined inline. Each is upserted (event + metric) and attached with role=guardrail."
661
- )
662
- }).refine(
663
- (e) => e.groups.reduce((s, g) => s + g.weight, 0) === 1e4,
664
- "groups weights must sum to exactly 10000"
665
- ).describe(
666
- "Body for `POST /api/admin/experiments`. `name`, `universe`, and `groups` (\u22652, weights sum to 10000) required."
667
- );
668
- var experimentUpdateSchema = z6.object({
669
- name: experimentNameSchema.optional(),
670
- description: z6.string().max(2e3).nullable().optional(),
671
- hypothesis: z6.string().max(4e3).nullable().optional(),
672
- tag: z6.string().max(64).nullable().optional(),
673
- owner_email: z6.string().max(254).nullable().optional(),
674
- audience: z6.string().max(256).nullable().optional(),
675
- bucket_by: bucketBySchema.nullable().optional(),
676
- folder: folderSchema,
677
- targeting_gate: z6.string().nullable().optional(),
678
- allocation_pct: z6.number().int().min(0).max(1e4).optional().describe("Basis-points allocation (0\u201310000). Immutable while the experiment is running."),
679
- salt: z6.string().min(1).max(64).optional().describe("Hash salt. Immutable while running."),
680
- universe: z6.string().optional().describe(
681
- "New universe name. Immutable while running. Returns `422` if the universe doesn't exist."
682
- ),
683
- params: paramSchemaSchema.optional(),
684
- groups: z6.array(experimentGroupSchema).min(2).optional().describe("Replacement groups. Weights must sum to 10000. Immutable while running."),
685
- significance_threshold: z6.number().min(1e-4).max(0.5).optional(),
686
- min_runtime_days: z6.number().int().min(0).max(365).optional(),
687
- min_sample_size: z6.number().int().min(1).optional(),
688
- sequential_testing: z6.boolean().optional(),
689
- goal_metric: experimentInlineMetricSchema.optional().describe("Replaces the goal metric (event auto-upserted)."),
690
- guardrail_metrics: z6.array(experimentInlineMetricSchema).max(10).optional().describe("Replaces the guardrail set wholesale (event auto-upserted per entry).")
691
- }).describe(
692
- "Body for `PATCH /api/admin/experiments/{id}`. Partial \u2014 only supplied fields change. `allocation_pct`, `groups`, `salt`, `universe`, `params` are immutable while the experiment is running (stop first)."
693
- );
694
- var experimentStatusUpdateSchema = z6.object({
695
- status: z6.enum(["draft", "running", "stopped", "archived"]).describe(
696
- "Target status. Allowed transitions: `draft \u2192 running`, `running \u2192 stopped`, `stopped \u2192 archived`, `draft \u2192 archived`, and `archived \u2192 draft` (restore \u2014 only if the experiment never started). Restarting an archived experiment directly is not allowed; restore it to draft first, then start."
697
- )
698
- }).describe("Body for `POST /api/admin/experiments/{id}/status`.");
699
- var experimentMetricsUpdateSchema = z6.object({
700
- metrics: z6.array(
701
- z6.object({
702
- metric_id: z6.string().describe("Existing metric id in the project."),
703
- role: z6.enum(["goal", "guardrail", "secondary"]).describe(
704
- "Metric role. `goal` drives the decision, `guardrail` blocks ship if degraded, `secondary` is informational."
705
- )
706
- })
707
- ).describe("Replacement metrics list \u2014 replaces the current attachments wholesale.")
708
- }).describe(
709
- "Body for `POST /api/admin/experiments/{id}/metrics`. Replaces the experiment's metric attachments wholesale. Returns `422` if any `metric_id` is unknown."
710
- );
711
- var experimentResponseSchema = z6.object({
712
- id: z6.string().describe("Stable opaque experiment id (`exp_\u2026`)."),
713
- name: experimentNameSchema,
714
- description: z6.string().nullable(),
715
- hypothesis: z6.string().nullable(),
716
- tag: z6.string().nullable(),
717
- ownerEmail: z6.string().nullable(),
718
- audience: z6.string().nullable(),
719
- bucketBy: bucketBySchema.nullable(),
720
- folder: z6.string().nullable(),
721
- status: z6.enum(["draft", "running", "stopped", "archived"]),
722
- universe: z6.string().describe("Universe name this experiment draws from."),
723
- targetingGate: z6.string().nullable(),
724
- allocationPct: z6.number().int().describe("Allocation in basis points (0\u201310000)."),
725
- salt: z6.string(),
726
- params: z6.record(z6.string(), z6.enum(["string", "bool", "number"])),
727
- groups: z6.array(experimentGroupSchema),
728
- significanceThreshold: z6.number(),
729
- minRuntimeDays: z6.number().int(),
730
- minSampleSize: z6.number().int(),
731
- sequentialTesting: z6.boolean(),
732
- startedAt: z6.string().nullable().describe("ISO-8601 timestamp the experiment last transitioned to `running`, or `null`."),
733
- stoppedAt: z6.string().nullable().optional(),
734
- updatedAt: z6.string()
735
- });
736
- var experimentCreateResponseSchema = z6.object({
737
- id: z6.string().describe("Newly assigned experiment id."),
738
- name: experimentNameSchema
739
- });
740
- var experimentUpdateResponseSchema = z6.object({
741
- id: z6.string().describe("Experiment id that was updated.")
742
- });
743
- var experimentDeleteResponseSchema = z6.object({ ok: z6.literal(true) });
744
- var experimentStatusResponseSchema = z6.object({
745
- id: z6.string(),
746
- status: z6.enum(["draft", "running", "stopped", "archived"])
747
- });
748
- var experimentMetricsResponseSchema = z6.object({
749
- id: z6.string(),
750
- metrics: z6.array(
751
- z6.object({
752
- metric_id: z6.string(),
753
- role: z6.enum(["goal", "guardrail", "secondary"])
754
- })
755
- )
756
- });
757
- var experimentResultRowSchema = z6.object({
758
- metric: z6.string(),
759
- group_name: z6.string(),
760
- ds: z6.string().describe("Date slice (`YYYY-MM-DD`)."),
761
- n: z6.number().nullable(),
762
- mean: z6.number().nullable(),
763
- delta_pct: z6.number().nullable(),
764
- p_value: z6.number().nullable(),
765
- srm_detected: z6.number().nullable().describe("`1` if sample-ratio mismatch detected, else `0`.")
766
- });
767
- var experimentResultsResponseSchema = z6.object({
768
- experiment: z6.object({
769
- id: z6.string(),
770
- name: experimentNameSchema,
771
- status: z6.enum(["draft", "running", "stopped", "archived"])
772
- }),
773
- results: z6.array(experimentResultRowSchema)
774
- });
775
- var experimentTimeseriesResponseSchema = z6.object({
776
- experiment: z6.object({
777
- id: z6.string(),
778
- name: experimentNameSchema,
779
- status: z6.enum(["draft", "running", "stopped", "archived"])
780
- }),
781
- series: z6.array(experimentResultRowSchema)
782
- });
783
- var experimentReanalyzeResponseSchema = z6.object({
784
- id: z6.string(),
785
- queued: z6.literal(true)
786
- });
787
-
788
- // src/resources/experiments.ts
789
- var experimentListResponseSchema = z7.object({
790
- data: z7.array(experimentResponseSchema),
791
- next_cursor: z7.string().nullable()
792
- });
793
- var BASE2 = "/api/admin/experiments";
794
- function experimentsClient(t) {
795
- async function list(opts = {}) {
796
- const query = {};
797
- if (opts.limit !== void 0) query.limit = String(opts.limit);
798
- if (opts.cursor) query.cursor = opts.cursor;
799
- return t.request("GET", BASE2, void 0, query);
800
- }
801
- async function listAllWith(status) {
802
- const out = [];
803
- let cursor;
804
- do {
805
- const query = { limit: "500" };
806
- if (cursor) query.cursor = cursor;
807
- if (status) query.status = status;
808
- const page = await t.request("GET", BASE2, void 0, query);
809
- out.push(...page.data);
810
- cursor = page.next_cursor ?? void 0;
811
- } while (cursor);
812
- return out;
813
- }
814
- async function listAll() {
815
- return listAllWith();
816
- }
817
- async function resolve(idOrName) {
818
- try {
819
- return await t.request("GET", `${BASE2}/${idOrName}`);
820
- } catch (err) {
821
- if (!(err instanceof ApiError) || err.status !== 404) throw err;
822
- }
823
- const found = (await listAllWith()).find((e) => e.name === idOrName) ?? (await listAllWith("archived")).find((e) => e.name === idOrName);
824
- if (!found) throw new ApiError(`Experiment '${idOrName}' not found`, 404);
825
- return found;
826
- }
827
- async function setStatus(id, status) {
828
- return t.request(
829
- "POST",
830
- `${BASE2}/${id}/status`,
831
- experimentStatusUpdateSchema.parse({ status })
832
- );
833
- }
834
- return {
835
- list,
836
- listAll,
837
- resolve,
838
- get: (id) => t.request("GET", `${BASE2}/${id}`),
839
- create: (input) => t.request("POST", BASE2, experimentCreateSchema.parse(input)),
840
- update: (id, input) => t.request("PATCH", `${BASE2}/${id}`, experimentUpdateSchema.parse(input)),
841
- delete: (id) => t.request("DELETE", `${BASE2}/${id}`),
842
- setStatus,
843
- start: (id) => setStatus(id, "running"),
844
- stop: (id) => setStatus(id, "stopped"),
845
- archive: (id) => setStatus(id, "archived"),
846
- restore: (id) => setStatus(id, "draft"),
847
- setMetrics: (id, input) => t.request("POST", `${BASE2}/${id}/metrics`, experimentMetricsUpdateSchema.parse(input)),
848
- results: (id) => t.request("GET", `${BASE2}/${id}/results`),
849
- timeseries: (id, metric) => t.request(
850
- "GET",
851
- `${BASE2}/${id}/timeseries`,
852
- void 0,
853
- metric ? { metric } : void 0
854
- ),
855
- reanalyze: (id) => t.request("POST", `${BASE2}/${id}/reanalyze`)
856
- };
857
- }
858
- var SAMPLE_EXP = {
859
- id: "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
860
- name: "checkout_button_color",
861
- description: "Test green vs. blue CTA on the checkout page.",
862
- tag: "checkout",
863
- status: "running",
864
- universe: "primary_users",
865
- targetingGate: null,
866
- allocationPct: 5e3,
867
- salt: "8d3e9a1f6b7c4a5fa1c2b6d3e7c8e3a1",
868
- params: { cta_color: "string" },
869
- groups: [
870
- { name: "control", weight: 5e3, params: { cta_color: "blue" } },
871
- { name: "treatment", weight: 5e3, params: { cta_color: "green" } }
872
- ],
873
- significanceThreshold: 0.05,
874
- minRuntimeDays: 7,
875
- minSampleSize: 1e3,
876
- sequentialTesting: false,
877
- startedAt: "2026-05-01T12:00:00.000Z",
878
- stoppedAt: null,
879
- updatedAt: "2026-05-09T18:22:11.000Z"
880
- };
881
- var experimentsResource = {
882
- name: "experiments",
883
- basePath: BASE2,
884
- describeOne: "experiment",
885
- describeMany: "experiments",
886
- tag: {
887
- name: "Experiments",
888
- description: [
889
- "A/B/n experiments: randomised group assignment plus the analysis pipeline (t-test, sequential testing, SRM detection) on top of a universe.",
890
- "",
891
- "**Identity.** Stable `name` (a-z, 0-9, `_`/`-`, max 64 chars). Immutable after create.",
892
- "",
893
- "**Lifecycle.** `draft \u2192 running \u2192 stopped \u2192 archived`. Transition via `POST /{id}/status`. An archived experiment that never started can be restored with `archived \u2192 draft`; restarting an archived experiment directly is not allowed.",
894
- "",
895
- "**Allocation.** `allocation_pct` (basis points, 0\u201310000) is the share of the targeted audience enrolled; `groups[].weight` (must sum to 10000) splits the enrolled audience. `targeting_gate` narrows the eligible audience before allocation.",
896
- "",
897
- "**Immutable while running.** `allocation_pct`, `groups`, `salt`, `universe`, `params` cannot be edited on a running experiment \u2014 stop it first.",
898
- "",
899
- "**Metrics.** Attach via `POST /{id}/metrics`. Each metric has a role: `goal` drives the decision, `guardrail` blocks ship on regression, `secondary` is informational.",
900
- "",
901
- "**Analysis.** Daily cron writes results to D1. Read via `GET /{id}/results` (latest per metric/group/day) or `GET /{id}/timeseries` (full history). `POST /{id}/reanalyze` requeues the analysis pass."
902
- ].join("\n")
903
- },
904
- schemas: {
905
- create: experimentCreateSchema,
906
- update: experimentUpdateSchema
907
- },
908
- actions: [
909
- { name: "start", description: "Transition draft experiment to running" },
910
- { name: "stop", description: "Stop a running experiment" },
911
- { name: "archive", description: "Archive a stopped experiment" },
912
- { name: "restore", description: "Restore an archived (never-started) experiment to draft" },
913
- { name: "reanalyze", description: "Re-run analysis pass for the experiment" }
914
- ],
915
- endpoints: [
916
- {
917
- operationId: "listExperiments",
918
- method: "GET",
919
- path: "",
920
- summary: "List experiments",
921
- description: "Returns a single page of non-archived experiments ordered by `updated_at desc, id desc`. Use the `cursor` query parameter to paginate.",
922
- response: experimentListResponseSchema,
923
- examples: { response: { data: [SAMPLE_EXP], next_cursor: null } },
924
- useCase: "Snapshot every active experiment in the project \u2014 e.g. render an overview dashboard or drive a CI check that no experiment has been running past its `min_runtime_days`."
925
- },
926
- {
927
- operationId: "getExperiment",
928
- method: "GET",
929
- path: "/{id}",
930
- summary: "Get one experiment",
931
- description: "Returns the full experiment row including groups, params, allocation, and lifecycle timestamps.",
932
- pathParams: { id: "Stable opaque experiment id (`exp_\u2026`) or the experiment's `name`." },
933
- response: experimentResponseSchema,
934
- examples: { response: SAMPLE_EXP },
935
- useCase: "Fetch one experiment to render the detail page or to inspect its current allocation and group weights."
936
- },
937
- {
938
- operationId: "createExperiment",
939
- method: "POST",
940
- path: "",
941
- summary: "Create an experiment",
942
- description: [
943
- "Creates a new experiment in `draft` status. `name`, `universe`, and `groups` are required; everything else has sensible defaults.",
944
- "",
945
- "Returns `409` if `name` already exists, `422` if the named universe doesn't exist, `403` if a plan-gated option is set (`sequential_testing`, custom `significance_threshold`) on a plan that doesn't include it."
946
- ].join("\n"),
947
- successStatus: 201,
948
- request: experimentCreateSchema,
949
- response: experimentCreateResponseSchema,
950
- examples: {
951
- requestExamples: {
952
- minimal: {
953
- summary: "Minimal \u2014 two-group 50/50",
954
- description: "Smallest valid body. Two groups at 50/50, no allocation, no targeting.",
955
- value: {
956
- name: "checkout_button_color",
957
- universe: "primary_users",
958
- groups: [
959
- { name: "control", weight: 5e3 },
960
- { name: "treatment", weight: 5e3 }
961
- ]
962
- }
963
- },
964
- targeted: {
965
- summary: "Targeted \u2014 gate + allocation + params",
966
- description: "Gate the audience with a `targeting_gate`, allocate 50% of it, and parameterise the variants.",
967
- value: {
968
- name: "checkout_button_color",
969
- description: "Green CTA on checkout for Pro users.",
970
- universe: "primary_users",
971
- targeting_gate: "pro_plan_only",
972
- allocation_pct: 5e3,
973
- params: { cta_color: "string" },
974
- groups: [
975
- { name: "control", weight: 5e3, params: { cta_color: "blue" } },
976
- { name: "treatment", weight: 5e3, params: { cta_color: "green" } }
977
- ]
978
- }
979
- },
980
- multivariant: {
981
- summary: "Multivariant \u2014 three groups",
982
- description: "Three variants splitting 40/30/30. Requires the sum to equal 10000.",
983
- value: {
984
- name: "headline_test",
985
- universe: "primary_users",
986
- groups: [
987
- { name: "control", weight: 4e3 },
988
- { name: "alt_a", weight: 3e3 },
989
- { name: "alt_b", weight: 3e3 }
990
- ]
991
- }
992
- },
993
- sequential: {
994
- summary: "Sequential testing \u2014 Premium plan",
995
- description: "Enable always-valid sequential tests. Requires Premium plan or higher.",
996
- value: {
997
- name: "checkout_button_color",
998
- universe: "primary_users",
999
- groups: [
1000
- { name: "control", weight: 5e3 },
1001
- { name: "treatment", weight: 5e3 }
1002
- ],
1003
- sequential_testing: true,
1004
- min_runtime_days: 14
1005
- }
1006
- }
1007
- },
1008
- response: { id: "exp_01j7wb12c3d4e5f6g7h8j9k0l1", name: "checkout_button_color" }
1009
- },
1010
- useCase: [
1011
- "- **Minimal 50/50** \u2014 `name` + `universe` + two equal-weight groups.",
1012
- "- **Targeted rollout** \u2014 supply `targeting_gate` to restrict the eligible audience and `allocation_pct` to enrol a slice of it.",
1013
- "- **Multivariant** \u2014 three or more groups with weights summing to 10000.",
1014
- "- **Sequential testing** \u2014 `sequential_testing: true` for Premium plans."
1015
- ].join("\n")
1016
- },
1017
- {
1018
- operationId: "updateExperiment",
1019
- method: "PATCH",
1020
- path: "/{id}",
1021
- summary: "Update an experiment",
1022
- description: [
1023
- "Partial update. `allocation_pct`, `groups`, `salt`, `universe`, `params` are **immutable while running** \u2014 returns `409` if you try. Stop the experiment first.",
1024
- "",
1025
- "Editing `groups` while in `draft` is fine; weights must still sum to 10000."
1026
- ].join("\n"),
1027
- pathParams: { id: "Stable opaque experiment id." },
1028
- request: experimentUpdateSchema,
1029
- response: experimentUpdateResponseSchema,
1030
- examples: {
1031
- requestExamples: {
1032
- rampAllocation: {
1033
- summary: "Ramp allocation \u2014 0 \u2192 50%",
1034
- description: "Bumps the share of the targeted audience enrolled. Allowed only when the experiment is in `draft`.",
1035
- value: { allocation_pct: 5e3 }
1036
- },
1037
- changeWeights: {
1038
- summary: "Change group weights",
1039
- description: "Replace the groups array \u2014 weights must still sum to 10000.",
1040
- value: {
1041
- groups: [
1042
- { name: "control", weight: 2500 },
1043
- { name: "treatment", weight: 7500 }
1044
- ]
1045
- }
1046
- },
1047
- tighterSignificance: {
1048
- summary: "Tighter significance threshold",
1049
- description: "Drop the p-value cutoff from 0.05 \u2192 0.01. Requires Pro plan or higher.",
1050
- value: { significance_threshold: 0.01 }
1051
- },
1052
- updateMetadata: {
1053
- summary: "Update metadata",
1054
- description: "Change description/tag/targeting_gate freely on running experiments.",
1055
- value: {
1056
- description: "Bumped to 50% after passing canary on 25%.",
1057
- tag: "checkout"
1058
- }
1059
- }
1060
- },
1061
- response: { id: "exp_01j7wb12c3d4e5f6g7h8j9k0l1" }
1062
- },
1063
- useCase: [
1064
- "- **Update metadata** \u2014 `description`, `tag`, `targeting_gate` editable any time.",
1065
- "- **Ramp before launch** \u2014 set `allocation_pct` while still in `draft`.",
1066
- "- **Tighten significance** \u2014 `significance_threshold` (Pro+).",
1067
- "- **Rewire groups** \u2014 replace `groups` wholesale while in `draft`; immutable once running."
1068
- ].join("\n")
1069
- },
1070
- {
1071
- operationId: "deleteExperiment",
1072
- method: "DELETE",
1073
- path: "/{id}",
1074
- summary: "Delete an experiment",
1075
- description: "Archives the experiment (soft-delete via status transition). Returns `409` if the experiment is still `running` \u2014 stop it first.",
1076
- pathParams: { id: "Stable opaque experiment id." },
1077
- response: experimentDeleteResponseSchema,
1078
- examples: { response: { ok: true } },
1079
- useCase: "Tear down an experiment after the analysis is signed off."
1080
- },
1081
- {
1082
- operationId: "setExperimentStatus",
1083
- method: "POST",
1084
- path: "/{id}/status",
1085
- summary: "Transition experiment status",
1086
- description: [
1087
- "Drives the experiment lifecycle. Allowed transitions:",
1088
- "",
1089
- "- `draft \u2192 running` \u2014 starts allocation. Bumps the `startedAt` timestamp.",
1090
- "- `running \u2192 stopped` \u2014 halts allocation. Existing exposures stay in the dataset.",
1091
- "- `stopped \u2192 archived` \u2014 soft-delete.",
1092
- "- `draft \u2192 archived` \u2014 discard an unstarted experiment.",
1093
- "- `archived \u2192 draft` \u2014 restore a soft-deleted experiment so it can be re-completed and started. Allowed only if it never started; one that already ran must be cloned instead.",
1094
- "",
1095
- "Restarting an `archived` experiment directly is not allowed \u2014 restore it to `draft` first. Returns `409` on illegal transitions and `429` if the plan's `experiments_running` limit is exceeded on `\u2192 running`."
1096
- ].join("\n"),
1097
- pathParams: { id: "Stable opaque experiment id." },
1098
- request: experimentStatusUpdateSchema,
1099
- response: experimentStatusResponseSchema,
1100
- examples: {
1101
- requestExamples: {
1102
- start: {
1103
- summary: "Start \u2014 draft \u2192 running",
1104
- description: "Begins allocation. The experiment counts against the plan's `experiments_running` limit.",
1105
- value: { status: "running" }
1106
- },
1107
- stop: {
1108
- summary: "Stop \u2014 running \u2192 stopped",
1109
- description: "Halts allocation. Existing exposures remain analysable.",
1110
- value: { status: "stopped" }
1111
- },
1112
- archive: {
1113
- summary: "Archive \u2014 stopped \u2192 archived",
1114
- description: "Soft-deletes the experiment. List endpoints exclude archived rows by default.",
1115
- value: { status: "archived" }
1116
- },
1117
- restore: {
1118
- summary: "Restore \u2014 archived \u2192 draft",
1119
- description: "Un-deletes a soft-deleted experiment so it can be re-completed and started. Allowed only if it never ran.",
1120
- value: { status: "draft" }
1121
- }
1122
- },
1123
- response: { id: "exp_01j7wb12c3d4e5f6g7h8j9k0l1", status: "running" }
1124
- },
1125
- useCase: [
1126
- '- **Start** \u2014 `{ "status": "running" }` after wiring up the SDK and verifying targeting on staging.',
1127
- '- **Stop** \u2014 `{ "status": "stopped" }` once the experiment hits its `min_runtime_days` and conclusive results land.',
1128
- '- **Archive** \u2014 `{ "status": "archived" }` to soft-delete after sign-off.'
1129
- ].join("\n")
1130
- },
1131
- {
1132
- operationId: "setExperimentMetrics",
1133
- method: "POST",
1134
- path: "/{id}/metrics",
1135
- summary: "Attach metrics",
1136
- description: [
1137
- "Replaces the experiment's metric attachments wholesale. Each entry pairs an existing `metric_id` with a `role` (`goal` / `guardrail` / `secondary`).",
1138
- "",
1139
- "Returns `422` if any `metric_id` doesn't exist in the project. Pass `{ metrics: [] }` to detach everything."
1140
- ].join("\n"),
1141
- pathParams: { id: "Stable opaque experiment id." },
1142
- request: experimentMetricsUpdateSchema,
1143
- response: experimentMetricsResponseSchema,
1144
- examples: {
1145
- requestExamples: {
1146
- standard: {
1147
- summary: "Standard \u2014 one goal + one guardrail",
1148
- description: "Attaches one decision metric and one safety metric.",
1149
- value: {
1150
- metrics: [
1151
- { metric_id: "met_checkout_completed", role: "goal" },
1152
- { metric_id: "met_page_errors", role: "guardrail" }
1153
- ]
1154
- }
1155
- },
1156
- detach: {
1157
- summary: "Detach all",
1158
- description: "Send an empty array to clear every metric.",
1159
- value: { metrics: [] }
1160
- }
1161
- },
1162
- response: {
1163
- id: "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
1164
- metrics: [
1165
- { metric_id: "met_checkout_completed", role: "goal" },
1166
- { metric_id: "met_page_errors", role: "guardrail" }
1167
- ]
1168
- }
1169
- },
1170
- useCase: [
1171
- "- **Standard setup** \u2014 one `goal`, one or two `guardrail`, optional `secondary` metrics for diagnostics.",
1172
- '- **Detach all** \u2014 send `{ "metrics": [] }` before archiving.'
1173
- ].join("\n")
1174
- },
1175
- {
1176
- operationId: "getExperimentResults",
1177
- method: "GET",
1178
- path: "/{id}/results",
1179
- summary: "Get analysis results",
1180
- description: "Returns the latest analysis output for the experiment \u2014 one row per metric/group/day, including sample size, mean, % delta vs. control, p-value, and a sample-ratio mismatch flag.",
1181
- pathParams: { id: "Stable opaque experiment id." },
1182
- response: experimentResultsResponseSchema,
1183
- examples: {
1184
- response: {
1185
- experiment: {
1186
- id: "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
1187
- name: "checkout_button_color",
1188
- status: "running"
1189
- },
1190
- results: [
1191
- {
1192
- metric: "checkout_completed",
1193
- group_name: "control",
1194
- ds: "2026-05-09",
1195
- n: 12421,
1196
- mean: 0.1834,
1197
- delta_pct: null,
1198
- p_value: null,
1199
- srm_detected: 0
1200
- },
1201
- {
1202
- metric: "checkout_completed",
1203
- group_name: "treatment",
1204
- ds: "2026-05-09",
1205
- n: 12519,
1206
- mean: 0.1922,
1207
- delta_pct: 4.8,
1208
- p_value: 0.018,
1209
- srm_detected: 0
1210
- }
1211
- ]
1212
- }
1213
- },
1214
- useCase: "Render the results table on the experiment detail page or drive an automated decision once a `goal` metric reaches significance."
1215
- },
1216
- {
1217
- operationId: "getExperimentTimeseries",
1218
- method: "GET",
1219
- path: "/{id}/timeseries",
1220
- summary: "Get analysis timeseries",
1221
- description: "Same row shape as `/results`, but returns every daily slice rather than the latest. Filter to a single metric with the `metric` query parameter.",
1222
- pathParams: { id: "Stable opaque experiment id." },
1223
- queryParams: {
1224
- metric: {
1225
- schema: z7.string().optional(),
1226
- description: "Optional metric name to filter the series."
1227
- }
1228
- },
1229
- response: experimentTimeseriesResponseSchema,
1230
- examples: {
1231
- response: {
1232
- experiment: {
1233
- id: "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
1234
- name: "checkout_button_color",
1235
- status: "running"
1236
- },
1237
- series: [
1238
- {
1239
- metric: "checkout_completed",
1240
- group_name: "treatment",
1241
- ds: "2026-05-08",
1242
- n: 11200,
1243
- mean: 0.1903,
1244
- delta_pct: 3.9,
1245
- p_value: 0.034,
1246
- srm_detected: 0
1247
- },
1248
- {
1249
- metric: "checkout_completed",
1250
- group_name: "treatment",
1251
- ds: "2026-05-09",
1252
- n: 12519,
1253
- mean: 0.1922,
1254
- delta_pct: 4.8,
1255
- p_value: 0.018,
1256
- srm_detected: 0
1257
- }
1258
- ]
1259
- }
1260
- },
1261
- useCase: "Drive a chart of metric movement over the experiment runtime, or sanity-check the lift is monotonic before deciding."
1262
- },
1263
- {
1264
- operationId: "reanalyzeExperiment",
1265
- method: "POST",
1266
- path: "/{id}/reanalyze",
1267
- summary: "Re-queue analysis",
1268
- description: "Requeues the daily analysis pass for this experiment outside the normal cron cadence. Useful after attaching a new metric or correcting an event taxonomy. The job runs asynchronously.",
1269
- pathParams: { id: "Stable opaque experiment id." },
1270
- response: experimentReanalyzeResponseSchema,
1271
- examples: { response: { id: "exp_01j7wb12c3d4e5f6g7h8j9k0l1", queued: true } },
1272
- useCase: "Force-refresh results after wiring up a new metric without waiting for the next nightly cron tick."
1273
- }
1274
- ]
1275
- };
1276
-
1277
- // src/resources/configs.ts
1278
- import { z as z9 } from "zod";
1279
-
1280
- // src/schemas/configs.ts
1281
- import { z as z8 } from "zod";
1282
-
1283
- // src/schemas/constants.ts
1284
- var CONFIG_ENVS = ["dev", "staging", "prod"];
1285
- var CONFIG_KINDS = ["config", "killswitch"];
1286
-
1287
- // src/schemas/configs.ts
1288
- var SEGMENT = /^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/;
1289
- var RESERVED_FOLDER = "_default";
1290
- var configNameSchema = z8.string().max(128).refine(
1291
- (s) => {
1292
- const parts = s.split(".");
1293
- if (parts.length !== 2) return false;
1294
- return parts.every((p, i) => p === RESERVED_FOLDER && i === 0 ? true : SEGMENT.test(p));
1295
- },
1296
- {
1297
- message: "Must be `folder.name` \u2014 exactly two lowercase segments separated by a dot (e.g. `pricing.tiers`)."
1298
- }
1299
- ).describe(
1300
- "Stable config key in `folder.name` form (two lowercase segments separated by a dot, e.g. `pricing.tiers`). Used by SDKs as `Shipeasy.getConfig('<name>')`. Immutable after create."
1301
- );
1302
- var configKindSchema = z8.enum(CONFIG_KINDS);
1303
- var configEnvSchema = z8.enum(CONFIG_ENVS).describe("Target environment. One of the project's configured envs (`dev`, `stage`, `prod`).");
1304
- var jsonSchemaSchema = z8.record(z8.string(), z8.unknown()).refine((s) => s.type === "object", { message: "Top-level schema type must be 'object'" }).describe(
1305
- "JSON Schema (draft 2020-12) describing the shape of the config value. Top-level `type` must be `'object'`; every published value is validated against this schema."
1306
- );
1307
- var initialValuesSchema = z8.union([z8.unknown(), z8.record(configEnvSchema, z8.unknown())]).describe(
1308
- "Initial config value. Either a single JSON object applied to every env, or a `{ env: value }` map seeding per-env values. Must match `schema`. Defaults to `{}` on every env when omitted."
1309
- );
1310
- var configCreateSchema = z8.object({
1311
- name: configNameSchema,
1312
- description: z8.string().max(512).optional().describe("Optional free-form description shown in the dashboard. Max 512 chars."),
1313
- folder: folderSchema,
1314
- schema: jsonSchemaSchema,
1315
- value: initialValuesSchema.optional()
1316
- }).describe("Body for `POST /api/admin/configs`. `name` + `schema` required.");
1317
- var configUpdateSchema = z8.object({
1318
- schema: jsonSchemaSchema.optional().describe(
1319
- "Replacement schema. When supplied, the new schema is validated against every published value before it lands."
1320
- ),
1321
- value: z8.unknown().optional().describe(
1322
- "Flat value applied to **every** env. Publishes a new version per env. To target one env, use `PUT /{id}/drafts` then `POST /{id}/publish`."
1323
- ),
1324
- folder: folderSchema
1325
- }).describe(
1326
- "Body for `PATCH /api/admin/configs/{id}`. Partial \u2014 only supplied fields change. `value` republishes on every env."
1327
- );
1328
- var configSchemaUpdateSchema = z8.object({ schema: jsonSchemaSchema }).describe("Schema-only update. Does not bump value versions or rebuild KV.");
1329
- var configDraftUpsertSchema = z8.object({
1330
- env: configEnvSchema,
1331
- value: z8.unknown().describe("Draft value to stage on `env`. Validated against the config's current schema.")
1332
- }).describe(
1333
- "Body for `PUT /api/admin/configs/{id}/drafts`. Stages a value for a single env without publishing."
1334
- );
1335
- var configPublishSchema = z8.object({ env: configEnvSchema }).describe(
1336
- "Body for `POST /api/admin/configs/{id}/publish` and `DELETE /api/admin/configs/{id}/drafts`. Names the target env."
1337
- );
1338
- var configEnvPublishedSchema = z8.object({
1339
- version: z8.number().int().describe("Monotonically increasing version per env."),
1340
- publishedAt: z8.string().describe("ISO-8601 publish timestamp."),
1341
- publishedBy: z8.string().describe("Email/id of the actor who published this version.")
1342
- });
1343
- var configDraftSummarySchema = z8.object({
1344
- updatedAt: z8.string().describe("ISO-8601 last-edit timestamp on the draft."),
1345
- authorEmail: z8.string().describe("Email/id of the actor who last edited the draft."),
1346
- baseVersion: z8.number().int().describe("Published version the draft was forked from.")
1347
- });
1348
- var configResponseSchema = z8.object({
1349
- id: z8.string().describe("Stable opaque config id (`cfg_\u2026`)."),
1350
- name: configNameSchema,
1351
- description: z8.string().nullable(),
1352
- schema: jsonSchemaSchema,
1353
- updatedAt: z8.string().describe("ISO-8601 timestamp of last mutation."),
1354
- envs: z8.record(configEnvSchema, configEnvPublishedSchema).describe("Per-env latest published version metadata."),
1355
- drafts: z8.record(configEnvSchema, configDraftSummarySchema).describe("Per-env active drafts (if any)."),
1356
- values: z8.record(configEnvSchema, z8.unknown()).optional().describe("Per-env latest published values (only returned by `GET /{id}`, not list)."),
1357
- draftValues: z8.record(configEnvSchema, z8.unknown()).optional().describe("Per-env draft values (only returned by `GET /{id}`).")
1358
- });
1359
- var configCreateResponseSchema = z8.object({
1360
- id: z8.string().describe("Newly assigned config id."),
1361
- name: configNameSchema
1362
- });
1363
- var configUpdateResponseSchema = z8.object({
1364
- id: z8.string().describe("Config id that was updated.")
1365
- });
1366
- var configDeleteResponseSchema = z8.object({ ok: z8.literal(true) });
1367
- var configDraftSaveResponseSchema = z8.object({
1368
- id: z8.string(),
1369
- env: configEnvSchema,
1370
- baseVersion: z8.number().int().describe("Published version the draft is based on."),
1371
- updatedAt: z8.string()
1372
- });
1373
- var configDraftDiscardResponseSchema = z8.object({ ok: z8.literal(true) });
1374
- var configPublishResponseSchema = z8.object({
1375
- id: z8.string(),
1376
- env: configEnvSchema,
1377
- version: z8.number().int().describe("Newly published version on `env`.")
1378
- });
1379
- var configActivityEntrySchema = z8.object({
1380
- id: z8.string(),
1381
- action: z8.string().describe("Audit action (e.g. `config.create`, `config.publish`)."),
1382
- actorEmail: z8.string(),
1383
- actorType: z8.enum(["user", "cli", "system"]),
1384
- payload: z8.unknown().nullable(),
1385
- createdAt: z8.string().describe("ISO-8601 timestamp.")
1386
- });
1387
- var configActivityResponseSchema = z8.array(configActivityEntrySchema).describe("Recent audit rows for one config, newest first.");
1388
-
1389
- // src/resources/configs.ts
1390
- var configListResponseSchema = z9.object({
1391
- data: z9.array(configResponseSchema),
1392
- next_cursor: z9.string().nullable()
1393
- });
1394
- var BASE3 = "/api/admin/configs";
1395
- function configsClient(t) {
1396
- async function list(opts = {}) {
1397
- const query = {};
1398
- if (opts.limit !== void 0) query.limit = String(opts.limit);
1399
- if (opts.cursor) query.cursor = opts.cursor;
1400
- return t.request("GET", BASE3, void 0, query);
1401
- }
1402
- async function listAll() {
1403
- const out = [];
1404
- let cursor;
1405
- do {
1406
- const page = await list({ limit: 500, cursor });
1407
- out.push(...page.data);
1408
- cursor = page.next_cursor ?? void 0;
1409
- } while (cursor);
1410
- return out;
1411
- }
1412
- async function resolve(idOrName) {
1413
- try {
1414
- return await t.request("GET", `${BASE3}/${idOrName}`);
1415
- } catch (err) {
1416
- if (!(err instanceof ApiError) || err.status !== 404) throw err;
1417
- }
1418
- const all = await listAll();
1419
- const found = all.find((c) => c.name === idOrName);
1420
- if (!found) throw new ApiError(`Config '${idOrName}' not found`, 404);
1421
- return found;
1422
- }
1423
- return {
1424
- list,
1425
- listAll,
1426
- resolve,
1427
- get: (id) => t.request("GET", `${BASE3}/${id}`),
1428
- create: (input) => t.request("POST", BASE3, configCreateSchema.parse(input)),
1429
- update: (id, input) => t.request("PATCH", `${BASE3}/${id}`, configUpdateSchema.parse(input)),
1430
- delete: (id) => t.request("DELETE", `${BASE3}/${id}`),
1431
- saveDraft: (id, input) => t.request("PUT", `${BASE3}/${id}/drafts`, configDraftUpsertSchema.parse(input)),
1432
- discardDraft: (id, input) => t.request("DELETE", `${BASE3}/${id}/drafts`, configPublishSchema.parse(input)),
1433
- publish: (id, input) => t.request("POST", `${BASE3}/${id}/publish`, configPublishSchema.parse(input)),
1434
- activity: (id, limit) => t.request(
1435
- "GET",
1436
- `${BASE3}/${id}/activity`,
1437
- void 0,
1438
- limit ? { limit: String(limit) } : void 0
1439
- )
1440
- };
1441
- }
1442
- var SAMPLE_CFG = {
1443
- id: "cfg_01j7wae5h6j7k8l9m0n1p2q3r4",
1444
- name: "pricing.tiers",
1445
- description: "Pricing tier definitions consumed by the checkout flow.",
1446
- schema: {
1447
- type: "object",
1448
- properties: {
1449
- tiers: { type: "array", items: { type: "object" } }
1450
- },
1451
- required: ["tiers"]
1452
- },
1453
- updatedAt: "2026-05-09T18:22:11.000Z",
1454
- envs: {
1455
- dev: { version: 5, publishedAt: "2026-05-09T18:22:11.000Z", publishedBy: "ana@example.com" },
1456
- stage: { version: 4, publishedAt: "2026-05-08T11:05:22.000Z", publishedBy: "ana@example.com" },
1457
- prod: { version: 4, publishedAt: "2026-05-08T11:05:22.000Z", publishedBy: "ana@example.com" }
1458
- },
1459
- drafts: {
1460
- dev: {
1461
- updatedAt: "2026-05-10T09:31:00.000Z",
1462
- authorEmail: "bo@example.com",
1463
- baseVersion: 5
1464
- }
1465
- }
1466
- };
1467
- var configsResource = {
1468
- name: "configs",
1469
- basePath: BASE3,
1470
- describeOne: "config",
1471
- describeMany: "configs",
1472
- tag: {
1473
- name: "Configs",
1474
- description: [
1475
- "Dynamic configs: JSON-Schema-validated structured values delivered to SDKs and editable per environment with a draft/publish workflow.",
1476
- "",
1477
- "**Identity.** Each config is keyed by `name` in `folder.name` form (e.g. `pricing.tiers`). Immutable after create.",
1478
- "",
1479
- "**Schema-first.** Every config carries a JSON Schema (draft 2020-12, top-level `type: 'object'`). Every published value is validated against it.",
1480
- "",
1481
- "**Drafts \u2192 publish.** Per-env edits go through `PUT /{id}/drafts` (stages a value) then `POST /{id}/publish` (promotes to a new version). The flat `PATCH /{id}` republishes on **every** env in one shot \u2014 bypassing drafts.",
1482
- "",
1483
- "**Versioning.** Each publish bumps the per-env `version` monotonically. SDKs deliver the latest published version for each env."
1484
- ].join("\n")
1485
- },
1486
- schemas: {
1487
- create: configCreateSchema,
1488
- update: configUpdateSchema,
1489
- saveDraft: configDraftUpsertSchema,
1490
- publish: configPublishSchema
1491
- },
1492
- actions: [
1493
- { name: "publish", description: "Publish a config draft for an env" },
1494
- { name: "saveDraft", description: "Save a draft value for an env" },
1495
- { name: "discardDraft", description: "Discard a draft for an env" }
1496
- ],
1497
- endpoints: [
1498
- {
1499
- operationId: "listConfigs",
1500
- method: "GET",
1501
- path: "",
1502
- summary: "List dynamic configs",
1503
- description: "Returns a single page of configs ordered by `updated_at desc, id desc`. Each row includes the latest published `version` per env and any active drafts.",
1504
- response: configListResponseSchema,
1505
- examples: { response: { data: [SAMPLE_CFG], next_cursor: null } },
1506
- useCase: "Snapshot every config in the project \u2014 e.g. CI check that asserts no env is stuck on a stale default or that every config has a published value on prod."
1507
- },
1508
- {
1509
- operationId: "getConfig",
1510
- method: "GET",
1511
- path: "/{id}",
1512
- summary: "Get one config",
1513
- description: "Returns config metadata plus the latest published values per env and any active draft values. Use this to fetch the JSON the editor renders.",
1514
- pathParams: { id: "Stable opaque config id (`cfg_\u2026`) or the config's `name`." },
1515
- response: configResponseSchema,
1516
- examples: {
1517
- response: {
1518
- ...SAMPLE_CFG,
1519
- values: {
1520
- dev: { tiers: [{ name: "free" }, { name: "pro" }] },
1521
- stage: { tiers: [{ name: "free" }, { name: "pro" }] },
1522
- prod: { tiers: [{ name: "free" }, { name: "pro" }] }
1523
- },
1524
- draftValues: {
1525
- dev: { tiers: [{ name: "free" }, { name: "pro" }, { name: "enterprise" }] }
1526
- }
1527
- }
1528
- },
1529
- useCase: "Fetch one config's current published values and any in-flight drafts."
1530
- },
1531
- {
1532
- operationId: "createConfig",
1533
- method: "POST",
1534
- path: "",
1535
- summary: "Create a dynamic config",
1536
- description: [
1537
- "Creates a new config with the given `schema`. The initial `value` (or an empty object) is published as version 1 on **every** env.",
1538
- "",
1539
- "Returns `409` if `name` already exists in the project, `400` if `value` doesn't validate against `schema`."
1540
- ].join("\n"),
1541
- successStatus: 201,
1542
- request: configCreateSchema,
1543
- response: configCreateResponseSchema,
1544
- examples: {
1545
- requestExamples: {
1546
- minimal: {
1547
- summary: "Minimal \u2014 name + schema",
1548
- description: "Smallest valid body. Initial value defaults to `{}` on every env.",
1549
- value: {
1550
- name: "pricing.tiers",
1551
- schema: {
1552
- type: "object",
1553
- properties: { tiers: { type: "array" } },
1554
- required: ["tiers"]
1555
- }
1556
- }
1557
- },
1558
- seededValue: {
1559
- summary: "Seeded \u2014 schema + initial value",
1560
- description: "Seed the same JSON object on every env at version 1.",
1561
- value: {
1562
- name: "pricing.tiers",
1563
- description: "Pricing tier definitions consumed by checkout.",
1564
- schema: {
1565
- type: "object",
1566
- properties: { tiers: { type: "array" } },
1567
- required: ["tiers"]
1568
- },
1569
- value: { tiers: [{ name: "free" }, { name: "pro" }] }
1570
- }
1571
- },
1572
- perEnvSeed: {
1573
- summary: "Per-env seed",
1574
- description: "Pass a `{ env: value }` map to seed different initial values per env.",
1575
- value: {
1576
- name: "pricing.tiers",
1577
- schema: {
1578
- type: "object",
1579
- properties: { tiers: { type: "array" } },
1580
- required: ["tiers"]
1581
- },
1582
- value: {
1583
- dev: { tiers: [{ name: "free" }, { name: "pro" }, { name: "enterprise" }] },
1584
- stage: { tiers: [{ name: "free" }, { name: "pro" }] },
1585
- prod: { tiers: [{ name: "free" }, { name: "pro" }] }
1586
- }
1587
- }
1588
- }
1589
- },
1590
- response: { id: "cfg_01j7wae5h6j7k8l9m0n1p2q3r4", name: "pricing.tiers" }
1591
- },
1592
- useCase: [
1593
- "- **Minimal create** \u2014 `name` + `schema`. Initial value defaults to `{}`.",
1594
- "- **Seeded create** \u2014 supply a flat `value` to publish the same object on every env.",
1595
- "- **Per-env seed** \u2014 supply a `{ env: value }` map for different per-env starting values."
1596
- ].join("\n")
1597
- },
1598
- {
1599
- operationId: "updateConfig",
1600
- method: "PATCH",
1601
- path: "/{id}",
1602
- summary: "Update a dynamic config",
1603
- description: [
1604
- "Partial update. When `value` is supplied it is **republished on every env** (new version per env). When `schema` is supplied it replaces the current schema; every existing value is re-validated.",
1605
- "",
1606
- "For env-scoped edits, use the draft/publish flow (`PUT /{id}/drafts` then `POST /{id}/publish`) instead."
1607
- ].join("\n"),
1608
- pathParams: { id: "Stable opaque config id." },
1609
- request: configUpdateSchema,
1610
- response: configUpdateResponseSchema,
1611
- examples: {
1612
- requestExamples: {
1613
- flatValue: {
1614
- summary: "Flat value \u2014 republish on every env",
1615
- description: "Sets the same JSON object as a new version on dev/stage/prod.",
1616
- value: { value: { tiers: [{ name: "free" }, { name: "pro" }] } }
1617
- },
1618
- schemaOnly: {
1619
- summary: "Schema-only update",
1620
- description: "Replaces the schema. Existing values must still validate or the update fails.",
1621
- value: {
1622
- schema: {
1623
- type: "object",
1624
- properties: {
1625
- tiers: { type: "array" },
1626
- trial_days: { type: "integer" }
1627
- },
1628
- required: ["tiers"]
1629
- }
1630
- }
1631
- }
1632
- },
1633
- response: { id: "cfg_01j7wae5h6j7k8l9m0n1p2q3r4" }
1634
- },
1635
- useCase: [
1636
- '- **Republish flat value** \u2014 `{ "value": {\u2026} }` sets the same value on every env.',
1637
- '- **Schema migration** \u2014 `{ "schema": {\u2026} }` replaces the schema; existing values are re-validated.',
1638
- "- **Env-scoped edits** \u2014 use `PUT /{id}/drafts` + `POST /{id}/publish` instead of PATCH."
1639
- ].join("\n")
1640
- },
1641
- {
1642
- operationId: "deleteConfig",
1643
- method: "DELETE",
1644
- path: "/{id}",
1645
- summary: "Delete a dynamic config",
1646
- description: "Soft-deletes the config and rebuilds the project's flags KV blob.",
1647
- pathParams: { id: "Stable opaque config id." },
1648
- response: configDeleteResponseSchema,
1649
- examples: { response: { ok: true } },
1650
- useCase: "Tear down a config after its consumers have stopped reading it."
1651
- },
1652
- {
1653
- operationId: "saveConfigDraft",
1654
- method: "PUT",
1655
- path: "/{id}/drafts",
1656
- summary: "Save a draft value",
1657
- description: [
1658
- "Stages a value for one env without publishing. The draft is validated against the config's current schema and stored alongside the `baseVersion` it was forked from.",
1659
- "",
1660
- "Saving over an existing draft overwrites it. Use `POST /{id}/publish` to promote it to a new published version."
1661
- ].join("\n"),
1662
- pathParams: { id: "Stable opaque config id." },
1663
- request: configDraftUpsertSchema,
1664
- response: configDraftSaveResponseSchema,
1665
- examples: {
1666
- requestExamples: {
1667
- stageDev: {
1668
- summary: "Stage a dev draft",
1669
- description: "Stages a new value on dev without touching stage/prod.",
1670
- value: {
1671
- env: "dev",
1672
- value: { tiers: [{ name: "free" }, { name: "pro" }, { name: "enterprise" }] }
1673
- }
1674
- }
1675
- },
1676
- response: {
1677
- id: "cfg_01j7wae5h6j7k8l9m0n1p2q3r4",
1678
- env: "dev",
1679
- baseVersion: 5,
1680
- updatedAt: "2026-05-10T09:31:00.000Z"
1681
- }
1682
- },
1683
- useCase: "Iterate on a config value on dev without affecting prod \u2014 preview in staging, then publish."
1684
- },
1685
- {
1686
- operationId: "discardConfigDraft",
1687
- method: "DELETE",
1688
- path: "/{id}/drafts",
1689
- summary: "Discard a draft",
1690
- description: "Drops the in-flight draft on one env. Published values are unaffected.",
1691
- pathParams: { id: "Stable opaque config id." },
1692
- request: configPublishSchema,
1693
- response: configDraftDiscardResponseSchema,
1694
- examples: {
1695
- requestExamples: {
1696
- discardDev: {
1697
- summary: "Discard the dev draft",
1698
- description: "Drops the staged dev draft without publishing.",
1699
- value: { env: "dev" }
1700
- }
1701
- },
1702
- response: { ok: true }
1703
- },
1704
- useCase: "Abandon an in-progress draft after deciding not to ship it."
1705
- },
1706
- {
1707
- operationId: "publishConfigDraft",
1708
- method: "POST",
1709
- path: "/{id}/publish",
1710
- summary: "Publish a draft",
1711
- description: [
1712
- "Promotes the staged draft on one env to a new published version. The draft must still validate against the current schema.",
1713
- "",
1714
- "Returns `404` if there is no draft for the given env."
1715
- ].join("\n"),
1716
- pathParams: { id: "Stable opaque config id." },
1717
- request: configPublishSchema,
1718
- response: configPublishResponseSchema,
1719
- examples: {
1720
- requestExamples: {
1721
- publishDev: {
1722
- summary: "Publish the dev draft",
1723
- description: "Promotes the dev draft to a new published version on dev.",
1724
- value: { env: "dev" }
1725
- },
1726
- publishProd: {
1727
- summary: "Publish to prod",
1728
- description: "Promotes the prod draft after staging signed off.",
1729
- value: { env: "prod" }
1730
- }
1731
- },
1732
- response: { id: "cfg_01j7wae5h6j7k8l9m0n1p2q3r4", env: "dev", version: 6 }
1733
- },
1734
- useCase: "Ship a staged change once you've validated it on a lower env."
1735
- },
1736
- {
1737
- operationId: "listConfigActivity",
1738
- method: "GET",
1739
- path: "/{id}/activity",
1740
- summary: "List config activity",
1741
- description: "Returns recent audit rows for one config (create, update, draft.save, publish, delete) ordered newest first. Use the `limit` query parameter to cap the result (1\u2013100, default 20).",
1742
- pathParams: { id: "Stable opaque config id." },
1743
- queryParams: {
1744
- limit: {
1745
- schema: z9.coerce.number().int().min(1).max(100).default(20),
1746
- description: "Max rows to return (1\u2013100). Defaults to 20."
1747
- }
1748
- },
1749
- response: configActivityResponseSchema,
1750
- examples: {
1751
- response: [
1752
- {
1753
- id: "act_01j7waf01a2b3c4d5e6f7g8h9i",
1754
- action: "config.publish",
1755
- actorEmail: "ana@example.com",
1756
- actorType: "user",
1757
- payload: { env: "dev", version: 6 },
1758
- createdAt: "2026-05-10T09:31:42.000Z"
1759
- }
1760
- ]
1761
- },
1762
- useCase: "Render the activity panel in the config editor or drive a slack notification on publish events."
1763
- }
1764
- ]
1765
- };
1766
-
1767
- // src/resources/universes.ts
1768
- import { z as z11 } from "zod";
1769
-
1770
- // src/schemas/universes.ts
1771
- import { z as z10 } from "zod";
1772
- var universeNameSchema = z10.string().regex(/^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?)?$/).max(128).describe(
1773
- "Stable universe key. Single segment or `folder.name`. Lowercase letters, digits, `_` or `-`; max 128 chars. Immutable after create."
1774
- );
1775
- var holdoutRangeSchema = z10.tuple([z10.number().int().min(0).max(9999), z10.number().int().min(0).max(9999)]).refine(([lo, hi]) => lo <= hi, "lo must be <= hi").nullable().describe(
1776
- "Inclusive `[lo, hi]` bucket range (0\u20139999) reserved as the **holdout** \u2014 callers hashed into this slice are excluded from every experiment in the universe. `null` disables the holdout. Pro plan or higher required."
1777
- );
1778
- var universeCreateSchema = z10.object({
1779
- name: universeNameSchema,
1780
- folder: folderSchema,
1781
- unit_type: z10.string().default("user_id").describe(
1782
- "Unit of randomisation. Typically `user_id`. Use `account_id` to keep whole accounts in the same group across an experiment."
1783
- ),
1784
- holdout_range: holdoutRangeSchema.default(null)
1785
- }).describe("Body for `POST /api/admin/universes`. Only `name` is required.");
1786
- var universeUpdateSchema = z10.object({
1787
- folder: folderSchema,
1788
- holdout_range: holdoutRangeSchema.optional()
1789
- }).describe(
1790
- "Body for `PATCH /api/admin/universes/{id}`. Only `holdout_range` is mutable \u2014 name and unit_type are immutable after create."
1791
- );
1792
- var universeResponseSchema = z10.object({
1793
- id: z10.string().describe("Stable opaque universe id."),
1794
- name: universeNameSchema,
1795
- unitType: z10.string().describe("Unit of randomisation. Snake-case in request (`unit_type`), camelCase in response."),
1796
- holdoutRange: z10.tuple([z10.number(), z10.number()]).nullable().describe("Reserved holdout bucket range, or `null` if none."),
1797
- createdAt: z10.string().describe("ISO-8601 timestamp of creation.")
1798
- });
1799
- var universeCreateResponseSchema = z10.object({
1800
- id: z10.string().describe("Newly assigned universe id."),
1801
- name: universeNameSchema
1802
- });
1803
- var universeUpdateResponseSchema = z10.object({
1804
- id: z10.string().describe("Universe id that was updated.")
1805
- });
1806
- var universeDeleteResponseSchema = z10.object({ ok: z10.literal(true) });
1807
-
1808
- // src/resources/universes.ts
1809
- var universeListResponseSchema = z11.object({
1810
- data: z11.array(universeResponseSchema),
1811
- next_cursor: z11.string().nullable()
1812
- });
1813
- var BASE4 = "/api/admin/universes";
1814
- function universesClient(t) {
1815
- async function list(opts = {}) {
1816
- const query = {};
1817
- if (opts.limit !== void 0) query.limit = String(opts.limit);
1818
- if (opts.cursor) query.cursor = opts.cursor;
1819
- return t.request("GET", BASE4, void 0, query);
1820
- }
1821
- async function listAll() {
1822
- const out = [];
1823
- let cursor;
1824
- do {
1825
- const page = await list({ limit: 500, cursor });
1826
- out.push(...page.data);
1827
- cursor = page.next_cursor ?? void 0;
1828
- } while (cursor);
1829
- return out;
1830
- }
1831
- async function resolve(idOrName) {
1832
- const all = await listAll();
1833
- const found = all.find((u) => u.id === idOrName) ?? all.find((u) => u.name === idOrName);
1834
- if (!found) throw new ApiError(`Universe '${idOrName}' not found`, 404);
1835
- return found;
1836
- }
1837
- return {
1838
- list,
1839
- listAll,
1840
- resolve,
1841
- create: (input) => t.request("POST", BASE4, universeCreateSchema.parse(input)),
1842
- update: (id, input) => t.request("PATCH", `${BASE4}/${id}`, universeUpdateSchema.parse(input)),
1843
- delete: (id) => t.request("DELETE", `${BASE4}/${id}`)
1844
- };
1845
- }
1846
- var universesResource = {
1847
- name: "universes",
1848
- basePath: BASE4,
1849
- describeOne: "universe",
1850
- describeMany: "universes",
1851
- tag: {
1852
- name: "Universes",
1853
- description: [
1854
- "Universes: the shared bucketing space all experiments draw from.",
1855
- "",
1856
- "**Identity.** Each universe is keyed by a stable `name` (a-z, 0-9, `_`/`-`, max 64 chars). Experiments reference it via `universe: '<name>'`. The name is immutable.",
1857
- "",
1858
- "**Unit of randomisation.** `unit_type` selects the attribute hashed into a 0\u20139999 bucket \u2014 default `user_id` for per-user randomisation, `account_id` to keep a whole account in the same group.",
1859
- "",
1860
- "**Holdout.** `holdout_range` is an inclusive `[lo, hi]` bucket range (0\u20139999) reserved for measurement \u2014 callers hashed into the holdout are excluded from every experiment in the universe. Pro plan or higher.",
1861
- "",
1862
- "**Deletion.** Blocked while any non-archived experiment references the universe \u2014 archive those first."
1863
- ].join("\n")
1864
- },
1865
- schemas: {
1866
- create: universeCreateSchema,
1867
- update: universeUpdateSchema
1868
- },
1869
- actions: [],
1870
- endpoints: [
1871
- {
1872
- operationId: "listUniverses",
1873
- method: "GET",
1874
- path: "",
1875
- summary: "List universes",
1876
- description: "Returns a single page of universes ordered by `created_at desc, id desc`. The universes table has no `updated_at`, so this list is keyed on creation time.",
1877
- response: universeListResponseSchema,
1878
- examples: {
1879
- response: {
1880
- data: [
1881
- {
1882
- id: "uni_01j7w8a1b2c3d4e5f6g7h8i9j0",
1883
- name: "primary_users",
1884
- unitType: "user_id",
1885
- holdoutRange: [9500, 9999],
1886
- createdAt: "2026-04-12T10:14:08.000Z"
1887
- }
1888
- ],
1889
- next_cursor: null
1890
- }
1891
- },
1892
- useCase: "Snapshot every universe in the project \u2014 for example to audit which `unit_type` and `holdout_range` are in use before launching a new experiment."
1893
- },
1894
- {
1895
- operationId: "createUniverse",
1896
- method: "POST",
1897
- path: "",
1898
- summary: "Create a universe",
1899
- description: [
1900
- "Creates a new universe. Only `name` is required \u2014 `unit_type` defaults to `user_id` and `holdout_range` defaults to `null` (no holdout).",
1901
- "",
1902
- "Returns `409` if `name` already exists in the project. Returns `403` if you supply `holdout_range` on a plan below Pro."
1903
- ].join("\n"),
1904
- successStatus: 201,
1905
- request: universeCreateSchema,
1906
- response: universeCreateResponseSchema,
1907
- examples: {
1908
- requestExamples: {
1909
- minimal: {
1910
- summary: "Minimal \u2014 name only",
1911
- description: "Smallest valid body. Creates a `user_id`-keyed universe with no holdout.",
1912
- value: { name: "primary_users" }
1913
- },
1914
- holdout: {
1915
- summary: "Holdout \u2014 reserve 5% for measurement",
1916
- description: "Reserves buckets 9500\u20139999 (5% of callers) as the holdout. Pro plan or higher.",
1917
- value: { name: "primary_users", holdout_range: [9500, 9999] }
1918
- },
1919
- accountLevel: {
1920
- summary: "Account-level randomisation",
1921
- description: "Bucket whole accounts together via `unit_type: 'account_id'`.",
1922
- value: { name: "saas_accounts", unit_type: "account_id" }
1923
- }
1924
- },
1925
- response: { id: "uni_01j7w8a1b2c3d4e5f6g7h8i9j0", name: "primary_users" }
1926
- },
1927
- useCase: [
1928
- '- **Default universe** \u2014 `{ "name": "primary_users" }`. Per-user randomisation, no holdout.',
1929
- "- **Reserved holdout** \u2014 supply `holdout_range` to carve out a measurement slice excluded from all experiments.",
1930
- "- **Account-level** \u2014 `unit_type: 'account_id'` so multi-seat accounts see one consistent variant."
1931
- ].join("\n")
1932
- },
1933
- {
1934
- operationId: "updateUniverse",
1935
- method: "PATCH",
1936
- path: "/{id}",
1937
- summary: "Update a universe",
1938
- description: [
1939
- "Partial update. Only `holdout_range` is mutable \u2014 `name` and `unit_type` are immutable after create.",
1940
- "",
1941
- 'Pass `"holdout_range": null` to remove an existing holdout.'
1942
- ].join("\n"),
1943
- pathParams: { id: "Stable opaque universe id (`uni_\u2026`)." },
1944
- request: universeUpdateSchema,
1945
- response: universeUpdateResponseSchema,
1946
- examples: {
1947
- requestExamples: {
1948
- setHoldout: {
1949
- summary: "Set holdout \u2014 reserve 5%",
1950
- description: "Reserve buckets 9500\u20139999 as the holdout. Pro plan or higher.",
1951
- value: { holdout_range: [9500, 9999] }
1952
- },
1953
- removeHoldout: {
1954
- summary: "Remove holdout",
1955
- description: "Disable the holdout entirely.",
1956
- value: { holdout_range: null }
1957
- }
1958
- },
1959
- response: { id: "uni_01j7w8a1b2c3d4e5f6g7h8i9j0" }
1960
- },
1961
- useCase: [
1962
- "- **Adjust holdout** \u2014 change the reserved measurement slice without recreating experiments.",
1963
- '- **Remove holdout** \u2014 `{ "holdout_range": null }`.'
1964
- ].join("\n")
1965
- },
1966
- {
1967
- operationId: "deleteUniverse",
1968
- method: "DELETE",
1969
- path: "/{id}",
1970
- summary: "Delete a universe",
1971
- description: "Soft-deletes the universe. Returns `409` if any non-archived experiment still references it \u2014 archive those experiments first.",
1972
- pathParams: { id: "Stable opaque universe id." },
1973
- response: universeDeleteResponseSchema,
1974
- examples: { response: { ok: true } },
1975
- useCase: "Tear down a universe after every experiment that used it has been archived."
1976
- }
1977
- ]
1978
- };
1979
-
1980
- // src/resources/killswitches.ts
1981
- import { z as z13 } from "zod";
1982
-
1983
- // src/schemas/killswitches.ts
1984
- import { z as z12 } from "zod";
1985
- var SWITCH_KEY_RE = /^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/;
1986
- var switchKeySchema = z12.string().max(64).regex(SWITCH_KEY_RE, {
1987
- message: "Switch keys must be lowercase, no dots."
1988
- }).describe(
1989
- "Single-segment switch key (lowercase letters, digits, `_`/`-`; no dots). Used as the nested switch entry inside a killswitch's `switches` map."
1990
- );
1991
- var killswitchValueSchema = z12.object({
1992
- value: z12.boolean().describe(
1993
- "Default boolean delivered when no `switches` entry matches the caller's switch key."
1994
- ),
1995
- switches: z12.record(switchKeySchema, z12.boolean()).optional().describe(
1996
- "Per-switch overrides. When present, a matching key takes precedence over `value` on the client."
1997
- )
1998
- }).describe("Runtime killswitch payload returned to SDKs.");
1999
- var killswitchCreateSchema = z12.object({
2000
- name: configNameSchema.describe(
2001
- "Stable killswitch key in `folder.name` form (two lowercase segments separated by a dot \u2014 e.g. `payments.checkout`). Immutable after create."
2002
- ),
2003
- description: z12.string().max(512).optional().describe("Optional free-form description shown in the dashboard. Max 512 chars."),
2004
- folder: folderSchema,
2005
- value: z12.boolean().optional().describe(
2006
- "Default value applied to every env at creation. Defaults to `false`. Use `true` to ship the killswitch pre-tripped."
2007
- ),
2008
- switches: z12.record(switchKeySchema, z12.boolean()).optional().describe(
2009
- "Initial per-switch overrides applied to every env. Empty/omitted leaves the killswitch with only the flat `value`."
2010
- )
2011
- }).describe("Body for `POST /api/admin/killswitches`. Only `name` is required.");
2012
- var killswitchUpdateSchema = z12.object({
2013
- description: z12.string().max(512).nullish().describe("New description, or `null` to clear it. Max 512 chars."),
2014
- folder: folderSchema,
2015
- value: z12.boolean().optional().describe(
2016
- "Flat value applied to every env. Publishes a new version per env when set. Omit to leave values unchanged."
2017
- ),
2018
- switches: z12.record(switchKeySchema, z12.boolean()).optional().describe(
2019
- "Replace the switches map wholesale on every env. To edit a single entry on a single env use `PUT /{id}/switch` instead."
2020
- )
2021
- }).describe(
2022
- "Body for `PATCH /api/admin/killswitches/{id}`. Partial \u2014 only supplied fields change. `switches` replaces wholesale (no merge)."
2023
- );
2024
- var killswitchSwitchSetSchema = z12.object({
2025
- env: configEnvSchema.describe("Target environment (`dev`/`stage`/`prod`)."),
2026
- switchKey: switchKeySchema.describe("Switch key to set."),
2027
- value: z12.boolean().describe("New boolean value for this `switchKey` on this `env`.")
2028
- }).describe(
2029
- "Body for `PUT /api/admin/killswitches/{id}/switch`. Sets or updates one switch entry on one env."
2030
- );
2031
- var killswitchValueSetSchema = z12.object({
2032
- env: configEnvSchema.describe("Target environment whose flat default flips."),
2033
- value: z12.boolean().describe("New flat default for this env. Existing switches carry forward unchanged.")
2034
- }).describe(
2035
- "Body for the killswitch quick-toggle. Sets the flat `value` on one env without touching its switches."
2036
- );
2037
- var killswitchSwitchUnsetSchema = z12.object({
2038
- env: configEnvSchema.describe("Target environment."),
2039
- switchKey: switchKeySchema.describe("Switch key to remove.")
2040
- }).describe(
2041
- "Body for `DELETE /api/admin/killswitches/{id}/switch`. Removes one switch entry from one env."
2042
- );
2043
- var killswitchEnvEntrySchema = z12.object({
2044
- value: z12.boolean().describe("Flat boolean published for this env."),
2045
- switches: z12.record(switchKeySchema, z12.boolean()).optional(),
2046
- version: z12.number().int().describe("Monotonically increasing version per env."),
2047
- publishedAt: z12.string().describe("ISO-8601 publish timestamp for this version.")
2048
- });
2049
- var killswitchResponseSchema = z12.object({
2050
- id: z12.string().describe("Stable opaque killswitch id."),
2051
- name: configNameSchema,
2052
- description: z12.string().nullable().describe("Free-form description or `null`."),
2053
- updatedAt: z12.string().describe("ISO-8601 timestamp of last mutation."),
2054
- envs: z12.record(configEnvSchema, killswitchEnvEntrySchema).describe("Per-env latest value, switches, version, and publish timestamp.")
2055
- });
2056
- var killswitchCreateResponseSchema = z12.object({
2057
- id: z12.string().describe("Newly assigned killswitch id."),
2058
- name: configNameSchema
2059
- });
2060
- var killswitchUpdateResponseSchema = z12.object({
2061
- id: z12.string().describe("Killswitch id that was updated.")
2062
- });
2063
- var killswitchDeleteResponseSchema = z12.object({ ok: z12.literal(true) });
2064
- var killswitchSwitchSetResponseSchema = z12.object({
2065
- id: z12.string(),
2066
- env: configEnvSchema,
2067
- switchKey: switchKeySchema,
2068
- value: z12.boolean()
2069
- });
2070
- var killswitchSwitchUnsetResponseSchema = z12.object({
2071
- id: z12.string(),
2072
- env: configEnvSchema,
2073
- switchKey: switchKeySchema,
2074
- removed: z12.boolean().describe("`true` if the entry existed and was removed, `false` if no-op.")
2075
- });
2076
-
2077
- // src/resources/killswitches.ts
2078
- var killswitchListResponseSchema = z13.object({
2079
- data: z13.array(killswitchResponseSchema),
2080
- next_cursor: z13.string().nullable()
2081
- });
2082
- var BASE5 = "/api/admin/killswitches";
2083
- function killswitchesClient(t) {
2084
- async function list(opts = {}) {
2085
- const query = {};
2086
- if (opts.limit !== void 0) query.limit = String(opts.limit);
2087
- if (opts.cursor) query.cursor = opts.cursor;
2088
- return t.request("GET", BASE5, void 0, query);
2089
- }
2090
- async function listAll() {
2091
- const out = [];
2092
- let cursor;
2093
- do {
2094
- const page = await list({ limit: 500, cursor });
2095
- out.push(...page.data);
2096
- cursor = page.next_cursor ?? void 0;
2097
- } while (cursor);
2098
- return out;
2099
- }
2100
- async function resolve(idOrName) {
2101
- try {
2102
- return await t.request("GET", `${BASE5}/${idOrName}`);
2103
- } catch (err) {
2104
- if (!(err instanceof ApiError) || err.status !== 404) throw err;
2105
- }
2106
- const all = await listAll();
2107
- const found = all.find((k) => k.name === idOrName);
2108
- if (!found) throw new ApiError(`Killswitch '${idOrName}' not found`, 404);
2109
- return found;
2110
- }
2111
- return {
2112
- list,
2113
- listAll,
2114
- resolve,
2115
- get: (id) => t.request("GET", `${BASE5}/${id}`),
2116
- create: (input) => t.request("POST", BASE5, killswitchCreateSchema.parse(input)),
2117
- update: (id, input) => t.request("PATCH", `${BASE5}/${id}`, killswitchUpdateSchema.parse(input)),
2118
- delete: (id) => t.request("DELETE", `${BASE5}/${id}`),
2119
- setSwitch: (id, input) => t.request("PUT", `${BASE5}/${id}/switch`, killswitchSwitchSetSchema.parse(input)),
2120
- unsetSwitch: (id, input) => t.request("DELETE", `${BASE5}/${id}/switch`, killswitchSwitchUnsetSchema.parse(input))
2121
- };
2122
- }
2123
- var SAMPLE_KS = {
2124
- id: "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8",
2125
- name: "payments.checkout",
2126
- description: "Master kill for the checkout flow. Trip to fall back to the legacy provider.",
2127
- updatedAt: "2026-05-09T18:22:11.000Z",
2128
- envs: {
2129
- dev: { value: false, version: 3, publishedAt: "2026-05-09T18:22:11.000Z" },
2130
- stage: { value: false, version: 3, publishedAt: "2026-05-09T18:22:11.000Z" },
2131
- prod: {
2132
- value: false,
2133
- switches: { eu_region: true },
2134
- version: 5,
2135
- publishedAt: "2026-05-09T18:22:11.000Z"
2136
- }
2137
- }
2138
- };
2139
- var killswitchesResource = {
2140
- name: "killswitches",
2141
- basePath: BASE5,
2142
- describeOne: "killswitch",
2143
- describeMany: "killswitches",
2144
- tag: {
2145
- name: "Killswitches",
2146
- description: [
2147
- "Killswitches: per-env boolean overrides for kill-style operational toggles. Optimised for incident response \u2014 no rules, no rollout, just a flat boolean (plus optional per-key overrides) versioned per environment.",
2148
- "",
2149
- "**Identity.** Each killswitch is keyed by `name` in `folder.name` form (e.g. `payments.checkout`). Immutable after create.",
2150
- "",
2151
- "**Per-env values.** Every killswitch stores one version stream per env (`dev`, `stage`, `prod`). A `PATCH` with `value`/`switches` applies to **every** env in one shot (publishes a new version per env). To touch a single env, use `PUT /{id}/switch` and friends.",
2152
- "",
2153
- "**Switches map.** Optional `switches: { key: bool }` overrides the flat `value` for specific named call sites \u2014 useful for region/feature-scoped kills.",
2154
- "",
2155
- "**Versioning.** Each publish (create, update, set-switch, unset-switch) bumps the per-env `version` monotonically. SDKs deliver the latest published version."
2156
- ].join("\n")
2157
- },
2158
- schemas: {
2159
- create: killswitchCreateSchema,
2160
- update: killswitchUpdateSchema,
2161
- setSwitch: killswitchSwitchSetSchema,
2162
- unsetSwitch: killswitchSwitchUnsetSchema
2163
- },
2164
- actions: [
2165
- { name: "setSwitch", description: "Set or update one switch entry on one env" },
2166
- { name: "unsetSwitch", description: "Remove one switch entry from one env" }
2167
- ],
2168
- endpoints: [
2169
- {
2170
- operationId: "listKillswitches",
2171
- method: "GET",
2172
- path: "",
2173
- summary: "List killswitches",
2174
- description: "Returns a single page of killswitches ordered by `updated_at desc, id desc`. Each row includes the latest published `value`/`switches`/`version` per env.",
2175
- response: killswitchListResponseSchema,
2176
- examples: { response: { data: [SAMPLE_KS], next_cursor: null } },
2177
- useCase: "Snapshot every killswitch in the project \u2014 e.g. to render an incident-response runbook listing every kill and its current trip state."
2178
- },
2179
- {
2180
- operationId: "getKillswitch",
2181
- method: "GET",
2182
- path: "/{id}",
2183
- summary: "Get one killswitch",
2184
- description: "Returns the killswitch metadata plus the latest published `value`/`switches`/`version` per env.",
2185
- pathParams: { id: "Stable opaque killswitch id (`ksw_\u2026`)." },
2186
- response: killswitchResponseSchema,
2187
- examples: { response: SAMPLE_KS },
2188
- useCase: "Fetch the current state of one killswitch \u2014 e.g. to verify a trip propagated before declaring an incident mitigated."
2189
- },
2190
- {
2191
- operationId: "createKillswitch",
2192
- method: "POST",
2193
- path: "",
2194
- summary: "Create a killswitch",
2195
- description: [
2196
- "Creates a new killswitch with `value` (default `false`) applied to **every** env at version 1.",
2197
- "",
2198
- "Returns `409` if `name` already exists in the project."
2199
- ].join("\n"),
2200
- successStatus: 201,
2201
- request: killswitchCreateSchema,
2202
- response: killswitchCreateResponseSchema,
2203
- examples: {
2204
- requestExamples: {
2205
- minimal: {
2206
- summary: "Minimal \u2014 name only, untripped",
2207
- description: "Smallest valid body. Creates a killswitch with `value: false` on every env.",
2208
- value: { name: "payments.checkout" }
2209
- },
2210
- preTripped: {
2211
- summary: "Pre-tripped on create",
2212
- description: "Ship the killswitch already tripped (`value: true`) \u2014 useful when provisioning a kill ahead of a deploy you want to ship with the feature off.",
2213
- value: {
2214
- name: "payments.checkout",
2215
- description: "Master kill for the new checkout flow.",
2216
- value: true
2217
- }
2218
- },
2219
- withSwitches: {
2220
- summary: "With per-region switches",
2221
- description: "Seed a `switches` map so specific named call sites can be tripped independently of the flat `value`.",
2222
- value: {
2223
- name: "payments.checkout",
2224
- value: false,
2225
- switches: { eu_region: true, apac_region: false }
2226
- }
2227
- }
2228
- },
2229
- response: { id: "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8", name: "payments.checkout" }
2230
- },
2231
- useCase: [
2232
- '- **Untripped create** \u2014 `{ "name": "payments.checkout" }`. Provision the kill ahead of an incident.',
2233
- '- **Pre-tripped** \u2014 `{ "value": true }` to ship the killswitch already engaged.',
2234
- "- **With switches** \u2014 seed `switches` to carve out per-region/per-tenant kills from day one."
2235
- ].join("\n")
2236
- },
2237
- {
2238
- operationId: "updateKillswitch",
2239
- method: "PATCH",
2240
- path: "/{id}",
2241
- summary: "Update a killswitch",
2242
- description: [
2243
- "Partial update applied to **every** env. Setting `value`/`switches` publishes a new version per env. Description-only patches don't bump versions.",
2244
- "",
2245
- "To change a single switch on a single env, use `PUT /{id}/switch` instead."
2246
- ].join("\n"),
2247
- pathParams: { id: "Stable opaque killswitch id." },
2248
- request: killswitchUpdateSchema,
2249
- response: killswitchUpdateResponseSchema,
2250
- examples: {
2251
- requestExamples: {
2252
- trip: {
2253
- summary: "Trip the killswitch on every env",
2254
- description: "Sets `value: true` on dev/stage/prod, bumping each env's version by one.",
2255
- value: { value: true }
2256
- },
2257
- untrip: {
2258
- summary: "Untrip on every env",
2259
- description: "Sets `value: false` on every env.",
2260
- value: { value: false }
2261
- },
2262
- replaceSwitches: {
2263
- summary: "Replace switches map",
2264
- description: "Replaces the per-key overrides wholesale on every env. Pass `{}` to clear.",
2265
- value: { switches: { eu_region: true, apac_region: true } }
2266
- },
2267
- updateDescription: {
2268
- summary: "Description-only update",
2269
- description: "Doesn't bump versions or trigger a republish \u2014 only metadata changes.",
2270
- value: { description: "Master kill \u2014 owner: payments-platform" }
2271
- }
2272
- },
2273
- response: { id: "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8" }
2274
- },
2275
- useCase: [
2276
- '- **Trip everywhere** \u2014 `{ "value": true }`. Kills the feature across dev/stage/prod in one call.',
2277
- '- **Untrip everywhere** \u2014 `{ "value": false }`.',
2278
- "- **Replace switches** \u2014 send the full new map; per-key edits use `PUT /{id}/switch`.",
2279
- "- **Update description** \u2014 metadata-only patches don't bump versions."
2280
- ].join("\n")
2281
- },
2282
- {
2283
- operationId: "deleteKillswitch",
2284
- method: "DELETE",
2285
- path: "/{id}",
2286
- summary: "Delete a killswitch",
2287
- description: "Soft-deletes the killswitch and rebuilds the project's flags KV blob so SDKs stop seeing it.",
2288
- pathParams: { id: "Stable opaque killswitch id." },
2289
- response: killswitchDeleteResponseSchema,
2290
- examples: { response: { ok: true } },
2291
- useCase: "Tear down a killswitch after the feature it protected has been removed."
2292
- },
2293
- {
2294
- operationId: "setKillswitchSwitch",
2295
- method: "PUT",
2296
- path: "/{id}/switch",
2297
- summary: "Set one switch entry",
2298
- description: [
2299
- "Sets or updates a single `switchKey` on a single `env`. Publishes one new version on that env only \u2014 other envs untouched.",
2300
- "",
2301
- "Use this for surgical per-env, per-key flips during incident response (e.g. trip `eu_region` on prod without touching the flat `value` or other envs)."
2302
- ].join("\n"),
2303
- pathParams: { id: "Stable opaque killswitch id." },
2304
- request: killswitchSwitchSetSchema,
2305
- response: killswitchSwitchSetResponseSchema,
2306
- examples: {
2307
- requestExamples: {
2308
- tripRegion: {
2309
- summary: "Trip eu_region on prod",
2310
- description: "Sets `switches.eu_region = true` on prod only. Other envs and the flat `value` are unchanged.",
2311
- value: { env: "prod", switchKey: "eu_region", value: true }
2312
- },
2313
- untripRegion: {
2314
- summary: "Untrip eu_region on prod",
2315
- description: "Flip the same entry back off without removing it from the map.",
2316
- value: { env: "prod", switchKey: "eu_region", value: false }
2317
- }
2318
- },
2319
- response: {
2320
- id: "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8",
2321
- env: "prod",
2322
- switchKey: "eu_region",
2323
- value: true
2324
- }
2325
- },
2326
- useCase: [
2327
- '- **Trip a region** \u2014 `{ "env": "prod", "switchKey": "eu_region", "value": true }`.',
2328
- "- **Untrip without removing** \u2014 same payload with `value: false`. To remove the entry entirely use `DELETE /{id}/switch`."
2329
- ].join("\n")
2330
- },
2331
- {
2332
- operationId: "unsetKillswitchSwitch",
2333
- method: "DELETE",
2334
- path: "/{id}/switch",
2335
- summary: "Remove one switch entry",
2336
- description: [
2337
- "Removes a single `switchKey` from the `switches` map on a single `env`. Publishes a new version on that env.",
2338
- "",
2339
- "Returns `{ removed: false }` if the entry didn't exist (idempotent no-op)."
2340
- ].join("\n"),
2341
- pathParams: { id: "Stable opaque killswitch id." },
2342
- request: killswitchSwitchUnsetSchema,
2343
- response: killswitchSwitchUnsetResponseSchema,
2344
- examples: {
2345
- requestExamples: {
2346
- removeRegion: {
2347
- summary: "Remove eu_region from prod",
2348
- description: "Drops the `eu_region` override on prod so the flat `value` applies again.",
2349
- value: { env: "prod", switchKey: "eu_region" }
2350
- }
2351
- },
2352
- response: {
2353
- id: "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8",
2354
- env: "prod",
2355
- switchKey: "eu_region",
2356
- removed: true
2357
- }
2358
- },
2359
- useCase: "Clean up a per-region override after the incident is resolved so the flat `value` governs again."
2360
- }
2361
- ]
2362
- };
2363
-
2364
- // src/schemas/alert-rules.ts
2365
- import { z as z14 } from "zod";
2366
- var ALERT_COMPARATORS = ["gt", "gte", "lt", "lte"];
2367
- var ALERT_SEVERITIES = ["danger", "warn", "info"];
2368
- var notificationTargetSchema = z14.object({
2369
- slackChannel: z14.object({
2370
- id: z14.string().min(1).describe("Slack channel id (e.g. C0123ABCD)."),
2371
- name: z14.string().min(1).describe("Slack channel name (without the leading #).")
2372
- }).nullish().describe("Slack channel to post this rule's alert to. Requires a Slack connector."),
2373
- email: z14.string().email().nullish().describe("Email address to notify for this rule (overrides the default recipient).")
2374
- }).strict();
2375
- var alertRuleCreateSchema = z14.object({
2376
- name: z14.string().min(1).max(120).describe("Human label for the rule, shown on the alert and the rules list."),
2377
- metricId: z14.string().min(1).describe("Id of the metric to evaluate."),
2378
- comparator: z14.enum(ALERT_COMPARATORS).describe("How the metric value is compared to the threshold (gt/gte/lt/lte)."),
2379
- threshold: z14.number().finite().describe("Threshold the metric value is compared against."),
2380
- windowHours: z14.number().int().min(1).max(720).default(24).describe("Lookback window (hours) the metric is aggregated over. 1\u2013720."),
2381
- severity: z14.enum(ALERT_SEVERITIES).default("warn").describe("Severity of the raised alert."),
2382
- enabled: z14.boolean().default(true).describe("Whether the rule is evaluated by the cron."),
2383
- notify: notificationTargetSchema.nullish().describe("Where to deliver this rule's alert (Slack channel and/or email target).")
2384
- }).strict();
2385
- var alertRuleUpdateSchema = z14.object({
2386
- name: z14.string().min(1).max(120),
2387
- comparator: z14.enum(ALERT_COMPARATORS),
2388
- threshold: z14.number().finite(),
2389
- windowHours: z14.number().int().min(1).max(720),
2390
- severity: z14.enum(ALERT_SEVERITIES),
2391
- enabled: z14.boolean(),
2392
- notify: notificationTargetSchema.nullish()
2393
- }).partial().strict();
2394
-
2395
- // src/resources/alert-rules.ts
2396
- var BASE6 = "/api/admin/alert-rules";
2397
- function alertRulesClient(t) {
2398
- async function list() {
2399
- return t.request("GET", BASE6);
2400
- }
2401
- async function resolve(idOrName) {
2402
- const all = await list();
2403
- const byId = all.find((r) => r.id === idOrName);
2404
- if (byId) return byId;
2405
- const byPrefix = all.filter((r) => r.id.startsWith(idOrName));
2406
- if (byPrefix.length === 1) return byPrefix[0];
2407
- if (byPrefix.length > 1)
2408
- throw new ApiError(`Alert rule id prefix '${idOrName}' is ambiguous`, 400);
2409
- const byName = all.filter((r) => r.name === idOrName);
2410
- if (byName.length === 1) return byName[0];
2411
- if (byName.length > 1)
2412
- throw new ApiError(
2413
- `Alert rule name '${idOrName}' is ambiguous \u2014 pass an id instead`,
2414
- 400
2415
- );
2416
- throw new ApiError(`Alert rule '${idOrName}' not found`, 404);
2417
- }
2418
- return {
2419
- list,
2420
- resolve,
2421
- create: (input) => t.request("POST", BASE6, alertRuleCreateSchema.parse(input)),
2422
- update: (id, input) => t.request("PATCH", `${BASE6}/${id}`, alertRuleUpdateSchema.parse(input)),
2423
- delete: (id) => t.request("DELETE", `${BASE6}/${id}`)
2424
- };
2425
- }
2426
-
2427
- // src/resources/metrics.ts
2428
- var BASE7 = "/api/admin/metrics";
2429
- function metricsClient(t) {
2430
- async function list() {
2431
- return t.request("GET", BASE7);
2432
- }
2433
- async function resolve(idOrName) {
2434
- const all = await list();
2435
- const byId = all.find((m) => m.id === idOrName);
2436
- if (byId) return byId;
2437
- const byPrefix = all.filter((m) => m.id.startsWith(idOrName));
2438
- if (byPrefix.length === 1) return byPrefix[0];
2439
- if (byPrefix.length > 1) throw new ApiError(`Metric id prefix '${idOrName}' is ambiguous`, 400);
2440
- const byName = all.filter((m) => m.name === idOrName);
2441
- if (byName.length === 1) return byName[0];
2442
- if (byName.length > 1)
2443
- throw new ApiError(`Metric name '${idOrName}' is ambiguous \u2014 pass an id`, 400);
2444
- throw new ApiError(`Metric '${idOrName}' not found`, 404);
2445
- }
2446
- return {
2447
- list,
2448
- resolve,
2449
- get: (id) => t.request("GET", `${BASE7}/${id}`),
2450
- create: (input) => t.request("POST", BASE7, input),
2451
- delete: (id) => t.request("DELETE", `${BASE7}/${id}`)
2452
- };
2453
- }
2454
-
2455
- // src/resources/events.ts
2456
- var BASE8 = "/api/admin/events";
2457
- function eventsClient(t) {
2458
- async function list() {
2459
- return t.request("GET", BASE8);
2460
- }
2461
- async function resolve(idOrName) {
2462
- const all = await list();
2463
- const byId = all.find((e) => e.id === idOrName);
2464
- if (byId) return byId;
2465
- const byPrefix = all.filter((e) => e.id.startsWith(idOrName));
2466
- if (byPrefix.length === 1) return byPrefix[0];
2467
- if (byPrefix.length > 1) throw new ApiError(`Event id prefix '${idOrName}' is ambiguous`, 400);
2468
- const byName = all.filter((e) => e.name === idOrName);
2469
- if (byName.length === 1) return byName[0];
2470
- if (byName.length > 1)
2471
- throw new ApiError(`Event name '${idOrName}' is ambiguous \u2014 pass an id`, 400);
2472
- throw new ApiError(`Event '${idOrName}' not found`, 404);
2473
- }
2474
- return {
2475
- list,
2476
- resolve,
2477
- get: (id) => t.request("GET", `${BASE8}/${id}`),
2478
- create: (input) => t.request("POST", BASE8, input),
2479
- update: (id, input) => t.request("PATCH", `${BASE8}/${id}`, input),
2480
- approve: (id, input = {}) => t.request("POST", `${BASE8}/${id}/approve`, input),
2481
- delete: (id) => t.request("DELETE", `${BASE8}/${id}`)
2482
- };
2483
- }
2484
-
2485
- // src/resources/ops.ts
2486
- var FEEDBACK = "/api/admin/feedback";
2487
- var NOTIFY = "/api/admin/notifications";
2488
- var CREATE_PATH = {
2489
- bug: "/api/admin/bugs",
2490
- feature_request: "/api/admin/feature-requests"
2491
- };
2492
- function itemPath(handle) {
2493
- return `${FEEDBACK}/${encodeURIComponent(handle)}`;
2494
- }
2495
- function opsClient(t) {
2496
- return {
2497
- list: (query = {}) => {
2498
- const q = {};
2499
- if (query.type) q.type = query.type;
2500
- if (query.status) q.status = query.status;
2501
- if (query.limit !== void 0) q.limit = String(query.limit);
2502
- return t.request("GET", FEEDBACK, void 0, q);
2503
- },
2504
- get: (handle) => t.request("GET", itemPath(handle)),
2505
- create: ({ type, ...body }) => t.request("POST", CREATE_PATH[type], body),
2506
- update: (handle, input) => t.request("PATCH", itemPath(handle), input),
2507
- linkPr: (handle, input) => t.request("POST", `${itemPath(handle)}/link-pr`, input),
2508
- notify: (input) => t.request("POST", NOTIFY, {
2509
- title: input.title,
2510
- summary: input.summary,
2511
- steps: input.steps ?? [],
2512
- ...input.href ? { href: input.href } : {},
2513
- ...input.dedupeKey ? { dedupeKey: input.dedupeKey } : {}
2514
- }),
2515
- channels: () => t.request("GET", "/api/admin/slack/channels")
2516
- };
2517
- }
2518
-
2519
- // src/resources/projects.ts
2520
- var BASE9 = "/api/admin/projects";
2521
- function projectsClient(t) {
2522
- return {
2523
- upsert: (input) => t.request("POST", `${BASE9}/upsert`, {
2524
- domain: input.domain,
2525
- ...input.name ? { name: input.name } : {}
2526
- }),
2527
- current: () => t.request("GET", `${BASE9}/current`)
2528
- };
2529
- }
2530
-
2531
- // src/resources/i18n.ts
2532
- var PROFILES = "/api/admin/i18n/profiles";
2533
- var KEYS = "/api/admin/i18n/keys";
2534
- var DRAFTS = "/api/admin/i18n/drafts";
2535
- function i18nClient(t) {
2536
- async function listProfiles() {
2537
- return t.request("GET", PROFILES);
2538
- }
2539
- async function resolveProfile(name) {
2540
- const all = await listProfiles();
2541
- const found = all.find((p) => p.name === name);
2542
- if (!found)
2543
- throw new ApiError(
2544
- `Profile '${name}' not found. Existing: ${all.map((p) => p.name).join(", ") || "(none)"}`,
2545
- 404
2546
- );
2547
- return found;
2548
- }
2549
- async function listKeys(query = {}) {
2550
- const q = {};
2551
- if (query.profile_id) q.profile_id = query.profile_id;
2552
- if (query.prefix) q.prefix = query.prefix;
2553
- if (query.limit !== void 0) q.limit = String(query.limit);
2554
- const res = await t.request("GET", KEYS, void 0, q);
2555
- return Array.isArray(res) ? res : res.keys;
2556
- }
2557
- function updateKey(id, input) {
2558
- return t.request("PUT", `${KEYS}/${id}`, input);
2559
- }
2560
- return {
2561
- listProfiles,
2562
- resolveProfile,
2563
- listKeys,
2564
- updateKey,
2565
- createProfile: (input) => t.request("POST", PROFILES, input),
2566
- pushKeys: (input) => t.request("POST", KEYS, input),
2567
- updateKeyByName: async (profileId, key, input) => {
2568
- const rows = await listKeys({ profile_id: profileId, prefix: key, limit: 500 });
2569
- const match = rows.find((k) => k.key === key);
2570
- if (!match)
2571
- throw new ApiError(`Key '${key}' not found in profile. Add it first with i18n push.`, 404);
2572
- return updateKey(match.id, input);
2573
- },
2574
- listDrafts: () => t.request("GET", DRAFTS),
2575
- publish: (profileId, input) => t.request("POST", `${PROFILES}/${profileId}/publish`, { chunk: input.chunk ?? "default" })
2576
- };
2577
- }
2578
-
2579
- // src/resources/attributes.ts
2580
- var BASE10 = "/api/admin/attributes";
2581
- function attributesClient(t) {
2582
- return {
2583
- list: () => t.request("GET", BASE10)
2584
- };
2585
- }
2586
-
2587
- // src/resources/index.ts
2588
- function createAdminClient(transport) {
2589
- return {
2590
- transport,
2591
- gates: gatesClient(transport),
2592
- experiments: experimentsClient(transport),
2593
- configs: configsClient(transport),
2594
- universes: universesClient(transport),
2595
- killswitches: killswitchesClient(transport),
2596
- alertRules: alertRulesClient(transport),
2597
- metrics: metricsClient(transport),
2598
- events: eventsClient(transport),
2599
- ops: opsClient(transport),
2600
- projects: projectsClient(transport),
2601
- i18n: i18nClient(transport),
2602
- attributes: attributesClient(transport)
2603
- };
2604
- }
2605
- var RESOURCE_REGISTRY = [
2606
- gatesResource,
2607
- experimentsResource,
2608
- configsResource,
2609
- universesResource,
2610
- killswitchesResource
2611
- ];
2612
-
2613
- // src/operations/types.ts
2614
- var opId = (op) => [...op.group, op.name].join(".");
2615
- var opMcpName = (op) => [...op.group, op.name].join("_");
2616
- var opCli = (op) => [...op.group, op.name].join(" ");
2617
-
2618
- // src/operations/coerce.ts
2619
- function coerceValue(param, raw) {
2620
- if (raw === void 0 || raw === null) return void 0;
2621
- switch (param.type) {
2622
- case "number": {
2623
- const n = typeof raw === "number" ? raw : Number(raw);
2624
- if (!Number.isFinite(n)) throw new ApiError(`--${param.name} must be a number`, 400);
2625
- return n;
2626
- }
2627
- case "boolean":
2628
- return raw === true || raw === "true" || raw === "1";
2629
- case "json":
2630
- if (typeof raw !== "string") return raw;
2631
- try {
2632
- return JSON.parse(raw);
2633
- } catch {
2634
- throw new ApiError(`--${param.name} must be valid JSON`, 400);
2635
- }
2636
- case "string":
2637
- default: {
2638
- const s = String(raw);
2639
- if (param.enum && !param.enum.includes(s)) {
2640
- throw new ApiError(`--${param.name} must be one of: ${param.enum.join(", ")}`, 400);
2641
- }
2642
- return s;
2643
- }
2644
- }
2645
- }
2646
- function coerceInput(op, raw) {
2647
- const out = {};
2648
- for (const param of op.params) {
2649
- const value = coerceValue(param, raw[param.name]);
2650
- if (value === void 0) {
2651
- if (param.required) throw new ApiError(`${param.name} is required`, 400);
2652
- continue;
2653
- }
2654
- out[param.name] = value;
2655
- }
2656
- return out;
2657
- }
2658
-
2659
- // src/operations/cli-adapter.ts
2660
- var toKebab = (s) => s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
2661
- function commandSignature(op) {
2662
- const positionals = op.params.filter((p) => p.positional).map((p) => p.required === false ? `[${p.name}]` : `<${p.name}>`);
2663
- return [op.name, ...positionals].join(" ");
2664
- }
2665
- function mountOperations(group, ops, ctx) {
2666
- for (const op of ops) {
2667
- const positionals = op.params.filter((p) => p.positional);
2668
- const flags = op.params.filter((p) => !p.positional);
2669
- const cmd = group.command(commandSignature(op)).description(op.summary);
2670
- for (const p of flags) {
2671
- cmd.option(`--${toKebab(p.name)} <value>`, p.description);
2672
- }
2673
- cmd.option("--json", "Output as JSON");
2674
- cmd.option("--project <id>", "Project ID override");
2675
- cmd.action(async (...argv) => {
2676
- try {
2677
- const opts = argv.find((a) => a && typeof a === "object" && !Array.isArray(a)) ?? {};
2678
- const raw = { ...opts };
2679
- positionals.forEach((p, i) => {
2680
- raw[p.name] = argv[i];
2681
- });
2682
- const input = coerceInput(op, raw);
2683
- const client = ctx.getClient({
2684
- mutates: op.mutates,
2685
- projectOverride: opts.project
2686
- });
2687
- const data = await op.run(client, input);
2688
- if (opts.json) ctx.printJson(data);
2689
- else ctx.printResult(op, data, input);
2690
- } catch (e) {
2691
- ctx.onError(e);
2692
- }
2693
- });
2694
- }
2695
- }
2696
-
2697
- // src/operations/mcp-adapter.ts
2698
- function jsonType(t) {
2699
- switch (t) {
2700
- case "number":
2701
- return "number";
2702
- case "boolean":
2703
- return "boolean";
2704
- case "json":
2705
- case "string":
2706
- default:
2707
- return "string";
2708
- }
2709
- }
2710
- function operationsToMcpTools(ops) {
2711
- return ops.map((op) => {
2712
- const properties = {};
2713
- const required = [];
2714
- for (const p of op.params) {
2715
- properties[p.name] = { type: jsonType(p.type), description: p.description };
2716
- if (p.enum) properties[p.name].enum = p.enum;
2717
- if (p.required) required.push(p.name);
2718
- }
2719
- return {
2720
- name: opMcpName(op),
2721
- description: op.description,
2722
- inputSchema: { type: "object", ...required.length ? { required } : {}, properties }
2723
- };
2724
- });
2725
- }
2726
- function operationsToDispatch(ops) {
2727
- const map = {};
2728
- for (const op of ops) {
2729
- map[opMcpName(op)] = async (client, args) => op.run(client, coerceInput(op, args ?? {}));
2730
- }
2731
- return map;
2732
- }
2733
-
2734
- // src/operations/docs.ts
2735
- function renderOperationsMarkdown(ops, opts = {}) {
2736
- const lines = [];
2737
- if (opts.title) lines.push(`# ${opts.title}`, "");
2738
- for (const op of ops) {
2739
- lines.push(`## \`${opId(op)}\``, "");
2740
- lines.push(op.summary, "");
2741
- if (op.description && op.description !== op.summary) lines.push(op.description, "");
2742
- lines.push(
2743
- `- **CLI:** \`shipeasy ${opCli(op)}\` \xB7 **MCP tool:** \`${opMcpName(op)}\` \xB7 ${op.mutates ? "write (needs project binding)" : "read"}`,
2744
- ""
2745
- );
2746
- if (op.params.length) {
2747
- lines.push("| Param | Type | Required | Description |", "| --- | --- | --- | --- |");
2748
- for (const p of op.params) {
2749
- const kind = p.positional ? `${p.type} (positional)` : p.type;
2750
- lines.push(`| \`${p.name}\` | ${kind} | ${p.required ? "yes" : "no"} | ${p.description} |`);
2751
- }
2752
- lines.push("");
2753
- }
2754
- if (op.examples?.length) {
2755
- lines.push("**Examples**", "");
2756
- for (const ex of op.examples) {
2757
- if (ex.note) lines.push(`- ${ex.note}:`);
2758
- lines.push("```sh", ex.run, "```");
2759
- }
2760
- lines.push("");
2761
- }
2762
- }
2763
- return lines.join("\n");
2764
- }
2765
-
2766
- // src/operations/util.ts
2767
- var num = (i, k) => i[k] === void 0 ? void 0 : i[k];
2768
- var str = (i, k) => i[k] === void 0 ? void 0 : i[k];
2769
- var bool = (i, k) => i[k] === void 0 ? void 0 : i[k];
2770
-
2771
- // src/operations/gates.ts
2772
- var ROLLOUT_DESC = "Rollout percentage, 0\u2013100 (converted to basis points internally).";
2773
- var RULES_DESC = `Targeting rules as a JSON array, e.g. '[{"attr":"plan","op":"eq","value":"pro"}]'. AND-combined.`;
2774
- 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.";
2775
- var gateOperations = [
2776
- {
2777
- group: ["release", "flags"],
2778
- name: "list",
2779
- mutates: false,
2780
- summary: "List all feature gates",
2781
- description: "Return every feature gate in the project (name, enabled, rollout %), ordered by most recently updated.",
2782
- params: [],
2783
- examples: [{ run: "shipeasy release flags list" }],
2784
- run: (client) => client.gates.listAll()
2785
- },
2786
- {
2787
- group: ["release", "flags"],
2788
- name: "create",
2789
- mutates: true,
2790
- summary: "Create a feature gate",
2791
- 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.",
2792
- params: [
2793
- { name: "name", type: "string", description: "Gate key (lowercase, digits, _/-).", required: true, positional: true },
2794
- { name: "rollout", type: "number", description: ROLLOUT_DESC },
2795
- { name: "rules", type: "json", description: RULES_DESC },
2796
- { name: "salt", type: "string", description: "Override hash salt (immutable after create)." },
2797
- { name: "stack", type: "json", description: STACK_DESC }
2798
- ],
2799
- examples: [
2800
- { note: "Off by default; flip it on later", run: "shipeasy release flags create checkout-v2" },
2801
- {
2802
- note: "Roll out to 25% of pro-plan users",
2803
- run: `shipeasy release flags create new-ui --rollout 25 --rules '[{"attr":"plan","op":"eq","value":"pro"}]'`
2804
- }
2805
- ],
2806
- run: (client, i) => client.gates.create({
2807
- name: i.name,
2808
- rollout_pct: Math.round((num(i, "rollout") ?? 0) * 100),
2809
- // Validated at runtime by gateCreateSchema.parse inside gates.create().
2810
- rules: i.rules ?? [],
2811
- ...i.salt ? { salt: i.salt } : {},
2812
- ...i.stack ? { stack: i.stack } : {}
2813
- })
2814
- },
2815
- {
2816
- group: ["release", "flags"],
2817
- name: "update",
2818
- mutates: true,
2819
- summary: "Update a gate's rollout, rules, stack, or enabled flag",
2820
- 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.",
2821
- params: [
2822
- { name: "name", type: "string", description: "Gate name to update.", required: true, positional: true },
2823
- { name: "rollout", type: "number", description: ROLLOUT_DESC },
2824
- { name: "rules", type: "json", description: RULES_DESC + " Replaces the existing rules wholesale." },
2825
- { name: "stack", type: "json", description: STACK_DESC + " Pass null to revert to flat evaluation." },
2826
- { name: "enabled", type: "boolean", description: "Master switch \u2014 false forces the gate off for every caller." }
2827
- ],
2828
- examples: [{ note: "Ramp to 50%", run: "shipeasy release flags update checkout-v2 --rollout 50" }],
2829
- run: async (client, i) => {
2830
- const gate = await client.gates.resolve(i.name);
2831
- const patch = {};
2832
- const rollout = num(i, "rollout");
2833
- if (rollout !== void 0) patch.rollout_pct = Math.round(rollout * 100);
2834
- if (i.rules !== void 0) patch.rules = i.rules;
2835
- if (i.stack !== void 0) patch.stack = i.stack;
2836
- if (i.enabled !== void 0) patch.enabled = i.enabled;
2837
- return client.gates.update(gate.id, patch);
2838
- }
2839
- },
2840
- {
2841
- group: ["release", "flags"],
2842
- name: "enable",
2843
- mutates: true,
2844
- summary: "Enable a feature gate",
2845
- description: "Set `enabled: true`, preserving the current rollout %.",
2846
- params: [{ name: "name", type: "string", description: "Gate name.", required: true, positional: true }],
2847
- examples: [{ run: "shipeasy release flags enable checkout-v2" }],
2848
- run: async (client, i) => {
2849
- const gate = await client.gates.resolve(i.name);
2850
- return client.gates.enable(gate.id);
2851
- }
2852
- },
2853
- {
2854
- group: ["release", "flags"],
2855
- name: "disable",
2856
- mutates: true,
2857
- summary: "Disable a feature gate (kill switch)",
2858
- description: "Set `enabled: false` so the gate evaluates to `false` for every caller regardless of rules/rollout.",
2859
- params: [{ name: "name", type: "string", description: "Gate name.", required: true, positional: true }],
2860
- examples: [{ run: "shipeasy release flags disable checkout-v2" }],
2861
- run: async (client, i) => {
2862
- const gate = await client.gates.resolve(i.name);
2863
- return client.gates.disable(gate.id);
2864
- }
2865
- },
2866
- {
2867
- group: ["release", "flags"],
2868
- name: "rollout",
2869
- mutates: true,
2870
- summary: "Set rollout percentage (0\u2013100)",
2871
- description: "Resolve the gate by name and set its rollout percentage (0\u2013100).",
2872
- params: [
2873
- { name: "name", type: "string", description: "Gate name.", required: true, positional: true },
2874
- { name: "pct", type: "number", description: "Rollout percentage, 0\u2013100.", required: true, positional: true }
2875
- ],
2876
- examples: [
2877
- { note: "Ramp to 50%", run: "shipeasy release flags rollout checkout-v2 50" },
2878
- { note: "Instant kill \u2014 set to 0", run: "shipeasy release flags rollout checkout-v2 0" }
2879
- ],
2880
- run: (client, i) => client.gates.setRollout(i.name, num(i, "pct"))
2881
- },
2882
- {
2883
- group: ["release", "flags"],
2884
- name: "archive",
2885
- mutates: true,
2886
- summary: "Archive a feature gate",
2887
- description: "Archive (soft-delete) a feature gate by name. Refuses if a running experiment references it as a targeting gate.",
2888
- params: [{ name: "name", type: "string", description: "Gate name.", required: true, positional: true }],
2889
- examples: [{ run: "shipeasy release flags archive checkout-v2" }],
2890
- run: async (client, i) => {
2891
- const gate = await client.gates.resolve(i.name);
2892
- await client.gates.delete(gate.id);
2893
- return { ok: true, archived: i.name };
2894
- }
2895
- }
2896
- ];
2897
-
2898
- // src/operations/killswitches.ts
2899
- var GROUP = ["release", "killswitch"];
2900
- var ENVS = ["dev", "staging", "prod"];
2901
- var killswitchOperations = [
2902
- {
2903
- group: GROUP,
2904
- name: "list",
2905
- mutates: false,
2906
- summary: "List all kill switches",
2907
- description: "Return every kill switch with its per-env default value and switch count.",
2908
- params: [],
2909
- examples: [{ run: "shipeasy release killswitch list" }],
2910
- run: (client) => client.killswitches.listAll()
2911
- },
2912
- {
2913
- group: GROUP,
2914
- name: "create",
2915
- mutates: true,
2916
- summary: "Create a kill switch",
2917
- 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.",
2918
- params: [
2919
- { name: "name", type: "string", description: "`folder.name` \u2014 two lowercase segments.", required: true, positional: true },
2920
- { name: "description", type: "string", description: "Optional description." },
2921
- { name: "value", type: "boolean", description: "Default value (true|false). Defaults to false." },
2922
- { name: "switches", type: "json", description: "JSON object of { switch_key: bool }; takes precedence over value." }
2923
- ],
2924
- examples: [
2925
- { note: "Default OFF", run: "shipeasy release killswitch create payments.stripe-gateway" },
2926
- { note: "Pre-seed per-key switches", run: `shipeasy release killswitch create payments.stripe-gateway --switches '{"refunds":true,"payouts":false}'` }
2927
- ],
2928
- run: (client, i) => client.killswitches.create({
2929
- name: i.name,
2930
- description: str(i, "description"),
2931
- value: bool(i, "value") ?? false,
2932
- switches: i.switches
2933
- })
2934
- },
2935
- {
2936
- group: GROUP,
2937
- name: "update",
2938
- mutates: true,
2939
- summary: "Update a kill switch's default value, switches map, or description",
2940
- description: "Patch a kill switch. `switches` replaces the map wholesale; pass an empty description to clear it.",
2941
- params: [
2942
- { name: "name", type: "string", description: "Kill switch name.", required: true, positional: true },
2943
- { name: "value", type: "boolean", description: "Default value (true|false)." },
2944
- { name: "switches", type: "json", description: "JSON { switch_key: bool } \u2014 replaces wholesale." },
2945
- { name: "description", type: "string", description: "Description (empty string clears it)." }
2946
- ],
2947
- examples: [{ note: "Flip the default ON", run: "shipeasy release killswitch update payments.stripe-gateway --value true" }],
2948
- run: async (client, i) => {
2949
- const k = await client.killswitches.resolve(i.name);
2950
- const patch = {};
2951
- if (i.value !== void 0) patch.value = i.value;
2952
- if (i.switches !== void 0) patch.switches = i.switches;
2953
- if (i.description !== void 0) patch.description = i.description === "" ? null : i.description;
2954
- return client.killswitches.update(k.id, patch);
2955
- }
2956
- },
2957
- {
2958
- group: GROUP,
2959
- name: "set",
2960
- mutates: true,
2961
- summary: "Set one switch entry on one env",
2962
- description: "Set or update a single named switch on one environment (default `prod`), leaving the rest untouched.",
2963
- params: [
2964
- { name: "name", type: "string", description: "Kill switch name.", required: true, positional: true },
2965
- { name: "switchKey", type: "string", description: "Switch key to set.", required: true, positional: true },
2966
- { name: "value", type: "boolean", description: "true|false.", required: true, positional: true },
2967
- { name: "env", type: "string", description: `Env: ${ENVS.join(" | ")} (default prod).`, enum: ENVS }
2968
- ],
2969
- examples: [
2970
- { note: "Kill refunds in prod", run: "shipeasy release killswitch set payments.stripe-gateway refunds true" },
2971
- { note: "Target staging instead", run: "shipeasy release killswitch set payments.stripe-gateway refunds true --env staging" }
2972
- ],
2973
- run: async (client, i) => {
2974
- const k = await client.killswitches.resolve(i.name);
2975
- return client.killswitches.setSwitch(k.id, {
2976
- env: str(i, "env") ?? "prod",
2977
- switchKey: i.switchKey,
2978
- value: i.value
2979
- });
2980
- }
2981
- },
2982
- {
2983
- group: GROUP,
2984
- name: "unset",
2985
- mutates: true,
2986
- summary: "Remove one switch entry from one env",
2987
- description: "Remove a single named switch from one environment (default `prod`), reverting it to the default.",
2988
- params: [
2989
- { name: "name", type: "string", description: "Kill switch name.", required: true, positional: true },
2990
- { name: "switchKey", type: "string", description: "Switch key to remove.", required: true, positional: true },
2991
- { name: "env", type: "string", description: `Env: ${ENVS.join(" | ")} (default prod).`, enum: ENVS }
2992
- ],
2993
- examples: [{ run: "shipeasy release killswitch unset payments.stripe-gateway refunds" }],
2994
- run: async (client, i) => {
2995
- const k = await client.killswitches.resolve(i.name);
2996
- return client.killswitches.unsetSwitch(k.id, {
2997
- env: str(i, "env") ?? "prod",
2998
- switchKey: i.switchKey
2999
- });
3000
- }
3001
- },
3002
- {
3003
- group: GROUP,
3004
- name: "archive",
3005
- mutates: true,
3006
- summary: "Archive a kill switch",
3007
- description: "Archive (soft-delete) a kill switch by name.",
3008
- params: [{ name: "name", type: "string", description: "Kill switch name.", required: true, positional: true }],
3009
- examples: [{ run: "shipeasy release killswitch archive payments.stripe-gateway" }],
3010
- run: async (client, i) => {
3011
- const k = await client.killswitches.resolve(i.name);
3012
- await client.killswitches.delete(k.id);
3013
- return { ok: true, archived: i.name };
3014
- }
3015
- }
3016
- ];
3017
-
3018
- // src/operations/configs.ts
3019
- var GROUP2 = ["release", "configs"];
3020
- var configOperations = [
3021
- {
3022
- group: GROUP2,
3023
- name: "list",
3024
- mutates: false,
3025
- summary: "List all configs",
3026
- description: "Return every dynamic config (name, schema, last updated).",
3027
- params: [],
3028
- examples: [{ run: "shipeasy release configs list" }],
3029
- run: (client) => client.configs.listAll()
3030
- },
3031
- {
3032
- group: GROUP2,
3033
- name: "get",
3034
- mutates: false,
3035
- summary: "Show a config by name",
3036
- description: "Resolve a config by name and return its full detail (schema + per-env values + drafts).",
3037
- params: [{ name: "name", type: "string", description: "Config name.", required: true, positional: true }],
3038
- examples: [{ run: "shipeasy release configs get pricing" }],
3039
- run: async (client, i) => {
3040
- const c = await client.configs.resolve(i.name);
3041
- return client.configs.get(c.id);
3042
- }
3043
- },
3044
- {
3045
- group: GROUP2,
3046
- name: "create",
3047
- mutates: true,
3048
- summary: "Create a config",
3049
- description: "Create a dynamic config. Configs are JSON objects validated against a JSON Schema (object-only).",
3050
- params: [
3051
- { name: "name", type: "string", description: "Config name.", required: true, positional: true },
3052
- { name: "schema", type: "json", description: "JSON Schema (top-level type=object). Defaults to a permissive object schema." },
3053
- { name: "value", type: "json", description: "Initial value as a JSON object. Defaults to {}." },
3054
- { name: "description", type: "string", description: "Human-readable description." }
3055
- ],
3056
- examples: [
3057
- { note: "Permissive schema, empty value", run: "shipeasy release configs create pricing" },
3058
- { note: "Seed an initial value", run: `shipeasy release configs create pricing --value '{"free_trial_days":14}'` }
3059
- ],
3060
- run: (client, i) => client.configs.create({
3061
- name: i.name,
3062
- schema: i.schema ?? { type: "object", properties: {}, additionalProperties: true },
3063
- value: i.value ?? {},
3064
- ...i.description ? { description: i.description } : {}
3065
- })
3066
- },
3067
- {
3068
- group: GROUP2,
3069
- name: "update",
3070
- mutates: true,
3071
- summary: "Update a config's value (flat, all envs)",
3072
- description: "Legacy flat value update across all envs. Use draft + publish for per-env staging.",
3073
- params: [
3074
- { name: "name", type: "string", description: "Config name.", required: true, positional: true },
3075
- { name: "value", type: "json", description: "New value (JSON object).", required: true }
3076
- ],
3077
- examples: [{ note: "Overwrite the value (all envs)", run: `shipeasy release configs update pricing --value '{"free_trial_days":30}'` }],
3078
- run: async (client, i) => {
3079
- const c = await client.configs.resolve(i.name);
3080
- return client.configs.update(c.id, { value: i.value });
3081
- }
3082
- },
3083
- {
3084
- group: GROUP2,
3085
- name: "draft",
3086
- mutates: true,
3087
- summary: "Save a draft value for one env",
3088
- description: "Stage a draft value for the given env, to publish later.",
3089
- params: [
3090
- { name: "name", type: "string", description: "Config name.", required: true, positional: true },
3091
- { name: "env", type: "string", description: "Target env (e.g. prod, staging, dev).", required: true },
3092
- { name: "value", type: "json", description: "Draft value (JSON object).", required: true }
3093
- ],
3094
- examples: [{ note: "Stage a prod value", run: `shipeasy release configs draft pricing --env prod --value '{"free_trial_days":30}'` }],
3095
- run: async (client, i) => {
3096
- const c = await client.configs.resolve(i.name);
3097
- return client.configs.saveDraft(c.id, { env: i.env, value: i.value });
3098
- }
3099
- },
3100
- {
3101
- group: GROUP2,
3102
- name: "publish",
3103
- mutates: true,
3104
- summary: "Publish the saved draft for one env",
3105
- description: "Promote the staged draft for the given env to live.",
3106
- params: [
3107
- { name: "name", type: "string", description: "Config name.", required: true, positional: true },
3108
- { name: "env", type: "string", description: "Target env to publish.", required: true }
3109
- ],
3110
- examples: [{ note: "Promote staged draft to prod", run: "shipeasy release configs publish pricing --env prod" }],
3111
- run: async (client, i) => {
3112
- const c = await client.configs.resolve(i.name);
3113
- return client.configs.publish(c.id, { env: str(i, "env") });
3114
- }
3115
- },
3116
- {
3117
- group: GROUP2,
3118
- name: "archive",
3119
- mutates: true,
3120
- summary: "Archive a config",
3121
- description: "Archive (soft-delete) a config by name.",
3122
- params: [{ name: "name", type: "string", description: "Config name.", required: true, positional: true }],
3123
- examples: [{ run: "shipeasy release configs archive pricing" }],
3124
- run: async (client, i) => {
3125
- const c = await client.configs.resolve(i.name);
3126
- await client.configs.delete(c.id);
3127
- return { ok: true, archived: i.name };
3128
- }
3129
- }
3130
- ];
3131
-
3132
- // src/operations/experiments.ts
3133
- var GROUP3 = ["release", "experiments"];
3134
- var DEFAULT_GROUPS = [
3135
- { name: "control", weight: 5e3, params: {} },
3136
- { name: "test", weight: 5e3, params: {} }
3137
- ];
3138
- var AGGREGATIONS = [
3139
- "count_users",
3140
- "count_events",
3141
- "retention_7d",
3142
- "retention_30d",
3143
- "sum",
3144
- "avg"
3145
- ];
3146
- function buildGoalMetric(i) {
3147
- const event = str(i, "successEvent");
3148
- if (!event) return void 0;
3149
- const agg = str(i, "successAggregation") ?? "count_users";
3150
- const value = str(i, "successValue");
3151
- switch (agg) {
3152
- case "count_users":
3153
- return { query: `count_users(${event})` };
3154
- case "count_events":
3155
- return { query: `count(${event})` };
3156
- case "retention_7d":
3157
- return { query: `retention_7d(${event})` };
3158
- case "retention_30d":
3159
- return { query: `retention_30d(${event})` };
3160
- case "sum":
3161
- case "avg":
3162
- if (!value)
3163
- throw new Error(
3164
- `successAggregation '${agg}' requires successValue \u2014 the numeric event property to ${agg} (e.g. "amount")`
3165
- );
3166
- return { query: `${agg}(${event}, ${value})` };
3167
- default:
3168
- throw new Error(`Unknown successAggregation '${agg}'`);
3169
- }
3170
- }
3171
- function parseGuardrails(raw) {
3172
- if (!Array.isArray(raw)) return [];
3173
- const out = [];
3174
- for (const entry of raw) {
3175
- if (typeof entry === "string") {
3176
- const query = entry.trim();
3177
- if (query) out.push({ query });
3178
- } else if (entry && typeof entry === "object") {
3179
- const o = entry;
3180
- const query = typeof o.query === "string" ? o.query.trim() : "";
3181
- if (query)
3182
- out.push(typeof o.name === "string" && o.name ? { query, name: o.name } : { query });
3183
- }
3184
- }
3185
- return out;
3186
- }
3187
- function computeVerdict(detail, results) {
3188
- if (results.some((r) => r.srm_detected === 1))
3189
- return { verdict: "invalid_srm", reason: "sample-ratio mismatch detected" };
3190
- if (detail.status !== "running") return { verdict: "not_running", reason: `status=${detail.status}` };
3191
- if (!results.length) return { verdict: "wait", reason: "no data yet" };
3192
- const withP = results.filter((r) => r.p_value !== null);
3193
- if (!withP.length) return { verdict: "wait", reason: "no p-values yet" };
3194
- const best = withP.reduce((a, b) => a.p_value < b.p_value ? a : b);
3195
- const threshold = detail.significance_threshold ?? 0.05;
3196
- if (best.p_value < threshold) return { verdict: "ship" };
3197
- const startedAt = detail.startedAt ?? detail.started_at;
3198
- const daysRunning = startedAt ? (Date.now() - new Date(startedAt).getTime()) / (1e3 * 60 * 60 * 24) : 0;
3199
- return daysRunning < (detail.min_runtime_days ?? 0) ? { verdict: "wait", reason: "min runtime not reached" } : { verdict: "hold" };
3200
- }
3201
- var experimentOperations = [
3202
- {
3203
- group: GROUP3,
3204
- name: "list",
3205
- mutates: false,
3206
- summary: "List experiments",
3207
- description: "Return every experiment (status, universe, allocation).",
3208
- params: [],
3209
- examples: [{ run: "shipeasy release experiments list" }],
3210
- run: (client) => client.experiments.listAll()
3211
- },
3212
- {
3213
- group: GROUP3,
3214
- name: "create",
3215
- mutates: true,
3216
- summary: "Create an experiment",
3217
- 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`.",
3218
- params: [
3219
- { name: "name", type: "string", description: "Experiment name.", required: true, positional: true },
3220
- { name: "universe", type: "string", description: "Universe name. Defaults to 'default'." },
3221
- { name: "allocation", type: "number", description: "Allocation percentage, 0\u2013100. Defaults to 100." },
3222
- { name: "groups", type: "json", description: "Groups as JSON [{name,weight,params}]. weight is basis points." },
3223
- { name: "params", type: "json", description: "Parameter schema {name: type}." },
3224
- { name: "targetingGate", type: "string", description: "Targeting gate name." },
3225
- { name: "bucketBy", type: "string", description: "Attribute to bucket by (defaults to the universe unit)." },
3226
- { name: "salt", type: "string", description: "Override hash salt." },
3227
- { name: "successEvent", type: "string", description: "Event name for the inline goal metric. Attaching one makes the draft startable." },
3228
- { name: "successAggregation", type: "string", description: `Goal-metric reducer. Defaults to count_users.`, enum: [...AGGREGATIONS] },
3229
- { name: "successValue", type: "string", description: "Numeric event property for sum/avg goal metrics." },
3230
- { name: "guardrailMetrics", type: "json", description: "Up to 10 guardrail metrics: JSON array of DSL query strings or {query,name}." },
3231
- { name: "significance", type: "number", description: "Significance threshold (0.0001\u20130.5). Defaults to 0.05." },
3232
- { name: "minRuntimeDays", type: "number", description: "Minimum runtime in days." },
3233
- { name: "minSampleSize", type: "number", description: "Minimum sample size." },
3234
- { name: "sequentialTesting", type: "boolean", description: "Enable sequential testing (always-valid p-values; Premium+)." },
3235
- { name: "description", type: "string", description: "Human-readable description." }
3236
- ],
3237
- examples: [
3238
- { note: "Default 50/50, full traffic", run: "shipeasy release experiments create pricing-page" },
3239
- {
3240
- note: "Attach a goal metric so it's startable",
3241
- run: "shipeasy release experiments create checkout-cta --universe web --success-event checkout_completed"
3242
- }
3243
- ],
3244
- run: (client, i) => {
3245
- const goal_metric = buildGoalMetric(i);
3246
- const guardrail_metrics = parseGuardrails(i.guardrailMetrics);
3247
- return client.experiments.create({
3248
- name: i.name,
3249
- universe: str(i, "universe") ?? "default",
3250
- allocation_pct: Math.round((num(i, "allocation") ?? 100) * 100),
3251
- groups: i.groups ?? DEFAULT_GROUPS,
3252
- params: i.params ?? {},
3253
- targeting_gate: str(i, "targetingGate") ?? null,
3254
- ...str(i, "bucketBy") ? { bucket_by: str(i, "bucketBy") } : {},
3255
- ...str(i, "description") !== void 0 ? { description: str(i, "description") } : {},
3256
- ...i.salt ? { salt: i.salt } : {},
3257
- ...num(i, "significance") !== void 0 ? { significance_threshold: num(i, "significance") } : {},
3258
- ...num(i, "minRuntimeDays") !== void 0 ? { min_runtime_days: num(i, "minRuntimeDays") } : {},
3259
- ...num(i, "minSampleSize") !== void 0 ? { min_sample_size: num(i, "minSampleSize") } : {},
3260
- ...bool(i, "sequentialTesting") !== void 0 ? { sequential_testing: bool(i, "sequentialTesting") } : {},
3261
- ...goal_metric ? { goal_metric } : {},
3262
- ...guardrail_metrics.length ? { guardrail_metrics } : {}
3263
- });
3264
- }
3265
- },
3266
- {
3267
- group: GROUP3,
3268
- name: "update",
3269
- mutates: true,
3270
- summary: "Update an experiment",
3271
- description: "Patch allocation, groups, params, targeting gate, or stats thresholds. Pass `targetingGate` = 'null' to clear it.",
3272
- params: [
3273
- { name: "name", type: "string", description: "Experiment name.", required: true, positional: true },
3274
- { name: "allocation", type: "number", description: "Allocation percentage, 0\u2013100." },
3275
- { name: "groups", type: "json", description: "Groups as JSON [{name,weight,params}]." },
3276
- { name: "params", type: "json", description: "Parameter schema {name: type}." },
3277
- { name: "targetingGate", type: "string", description: "Targeting gate name ('null' to clear)." },
3278
- { name: "bucketBy", type: "string", description: "Attribute to bucket by (empty string clears)." },
3279
- { name: "successEvent", type: "string", description: "Attach/replace the goal metric from this event." },
3280
- { name: "successAggregation", type: "string", description: "Goal-metric reducer (with successEvent).", enum: [...AGGREGATIONS] },
3281
- { name: "successValue", type: "string", description: "Numeric event property for sum/avg goal metrics." },
3282
- { name: "guardrailMetrics", type: "json", description: "Replace the guardrail set: JSON array of DSL queries or {query,name}." },
3283
- { name: "significance", type: "number", description: "Significance threshold (0.0001\u20130.5)." },
3284
- { name: "minRuntimeDays", type: "number", description: "Minimum runtime in days." },
3285
- { name: "minSampleSize", type: "number", description: "Minimum sample size." },
3286
- { name: "sequentialTesting", type: "boolean", description: "Enable sequential testing (always-valid p-values)." }
3287
- ],
3288
- examples: [
3289
- { note: "Dial allocation to 50%", run: "shipeasy release experiments update pricing-page --allocation 50" },
3290
- { note: "Clear targeting + tighten significance", run: "shipeasy release experiments update checkout-cta --targeting-gate null --significance 0.01" }
3291
- ],
3292
- run: async (client, i) => {
3293
- const e = await client.experiments.resolve(i.name);
3294
- const patch = {};
3295
- const allocation = num(i, "allocation");
3296
- if (allocation !== void 0) patch.allocation_pct = Math.round(allocation * 100);
3297
- if (i.groups !== void 0) patch.groups = i.groups;
3298
- if (i.params !== void 0) patch.params = i.params;
3299
- if (i.targetingGate !== void 0)
3300
- patch.targeting_gate = i.targetingGate === "null" ? null : i.targetingGate;
3301
- if (str(i, "bucketBy") !== void 0) patch.bucket_by = str(i, "bucketBy") || null;
3302
- if (i.significance !== void 0) patch.significance_threshold = i.significance;
3303
- if (i.minRuntimeDays !== void 0) patch.min_runtime_days = i.minRuntimeDays;
3304
- if (i.minSampleSize !== void 0) patch.min_sample_size = i.minSampleSize;
3305
- if (bool(i, "sequentialTesting") !== void 0) patch.sequential_testing = bool(i, "sequentialTesting");
3306
- if (i.guardrailMetrics !== void 0) patch.guardrail_metrics = parseGuardrails(i.guardrailMetrics);
3307
- const goal_metric = buildGoalMetric(i);
3308
- if (goal_metric) patch.goal_metric = goal_metric;
3309
- return client.experiments.update(e.id, patch);
3310
- }
3311
- },
3312
- {
3313
- group: GROUP3,
3314
- name: "start",
3315
- mutates: true,
3316
- summary: "Start an experiment",
3317
- description: "Move an experiment from draft to running.",
3318
- params: [{ name: "name", type: "string", description: "Experiment name.", required: true, positional: true }],
3319
- examples: [{ run: "shipeasy release experiments start pricing-page" }],
3320
- run: async (client, i) => {
3321
- const e = await client.experiments.resolve(i.name);
3322
- return client.experiments.start(e.id);
3323
- }
3324
- },
3325
- {
3326
- group: GROUP3,
3327
- name: "stop",
3328
- mutates: true,
3329
- summary: "Stop a running experiment",
3330
- description: "Stop a running experiment. Optionally name a `promoteGroup` to record an intent note (actual promotion happens in the dashboard).",
3331
- params: [
3332
- { name: "name", type: "string", description: "Experiment name.", required: true, positional: true },
3333
- { name: "promoteGroup", type: "string", description: "Group to note as the winner to promote." }
3334
- ],
3335
- examples: [{ run: "shipeasy release experiments stop pricing-page" }],
3336
- run: async (client, i) => {
3337
- const e = await client.experiments.resolve(i.name);
3338
- const result = await client.experiments.stop(e.id);
3339
- const promote = str(i, "promoteGroup");
3340
- return {
3341
- ...result,
3342
- ...promote ? {
3343
- promote_group_note: `Manual promotion needed: promote group '${promote}' in the dashboard or via PATCH /api/admin/experiments/${e.id}.`
3344
- } : {}
3345
- };
3346
- }
3347
- },
3348
- {
3349
- group: GROUP3,
3350
- name: "archive",
3351
- mutates: true,
3352
- summary: "Archive a stopped experiment",
3353
- description: "Archive (soft-delete) a stopped experiment, hiding it from the default list while preserving results.",
3354
- params: [{ name: "name", type: "string", description: "Experiment name.", required: true, positional: true }],
3355
- examples: [{ run: "shipeasy release experiments archive pricing-page" }],
3356
- run: async (client, i) => {
3357
- const e = await client.experiments.resolve(i.name);
3358
- return client.experiments.archive(e.id);
3359
- }
3360
- },
3361
- {
3362
- group: GROUP3,
3363
- name: "restore",
3364
- mutates: true,
3365
- summary: "Restore an archived experiment to draft",
3366
- 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.",
3367
- params: [{ name: "name", type: "string", description: "Experiment name.", required: true, positional: true }],
3368
- examples: [{ run: "shipeasy release experiments restore pricing-page" }],
3369
- run: async (client, i) => {
3370
- const e = await client.experiments.resolve(i.name);
3371
- return client.experiments.restore(e.id);
3372
- }
3373
- },
3374
- {
3375
- group: GROUP3,
3376
- name: "reanalyze",
3377
- mutates: true,
3378
- summary: "Re-run the analysis pass",
3379
- description: "Trigger a fresh analysis pass for an experiment.",
3380
- params: [{ name: "name", type: "string", description: "Experiment name.", required: true, positional: true }],
3381
- examples: [{ run: "shipeasy release experiments reanalyze pricing-page" }],
3382
- run: async (client, i) => {
3383
- const e = await client.experiments.resolve(i.name);
3384
- return client.experiments.reanalyze(e.id);
3385
- }
3386
- },
3387
- {
3388
- group: GROUP3,
3389
- name: "status",
3390
- mutates: false,
3391
- summary: "Show experiment status, verdict, and latest results",
3392
- 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.",
3393
- params: [{ name: "name", type: "string", description: "Experiment name.", required: true, positional: true }],
3394
- examples: [{ run: "shipeasy release experiments status pricing-page" }],
3395
- run: async (client, i) => {
3396
- const e = await client.experiments.resolve(i.name);
3397
- const detail = await client.experiments.get(e.id).catch(() => e);
3398
- const results = await client.experiments.results(e.id).catch(() => []);
3399
- const { verdict, reason } = computeVerdict(detail, results);
3400
- return { verdict, ...reason ? { reason } : {}, experiment: detail, results };
3401
- }
3402
- }
3403
- ];
3404
-
3405
- // src/operations/universes.ts
3406
- var GROUP4 = ["release", "experiments", "universes"];
3407
- function parseHoldout(raw) {
3408
- if (raw === void 0 || raw === "" || raw === "null") return null;
3409
- const [lo, hi] = raw.split(",").map((s) => Number(s.trim()));
3410
- if (!Number.isFinite(lo) || !Number.isFinite(hi)) {
3411
- throw new ApiError("holdout must be 'lo,hi' integers in [0,9999]", 400);
3412
- }
3413
- return [lo, hi];
3414
- }
3415
- var universeOperations = [
3416
- {
3417
- group: GROUP4,
3418
- name: "list",
3419
- mutates: false,
3420
- summary: "List universes",
3421
- description: "Return every experiment universe (allocation %, holdout %).",
3422
- params: [],
3423
- examples: [{ run: "shipeasy release experiments universes list" }],
3424
- run: (client) => client.universes.listAll()
3425
- },
3426
- {
3427
- group: GROUP4,
3428
- name: "create",
3429
- mutates: true,
3430
- summary: "Create a universe",
3431
- description: "Create an experiment universe (container + optional holdout).",
3432
- params: [
3433
- { name: "name", type: "string", description: "Universe name.", required: true, positional: true },
3434
- { name: "unitType", type: "string", description: "Unit type (e.g. user_id, account_id). Defaults to user_id." },
3435
- { name: "holdout", type: "string", description: "Holdout range as 'lo,hi' (0\u20139999); omit for no holdout." }
3436
- ],
3437
- examples: [
3438
- { note: "Default user_id unit, no holdout", run: "shipeasy release experiments universes create web" },
3439
- { note: "Holdout 0\u2013999, keyed on account_id", run: "shipeasy release experiments universes create web --unit-type account_id --holdout 0,999" }
3440
- ],
3441
- run: (client, i) => client.universes.create({
3442
- name: i.name,
3443
- unit_type: str(i, "unitType") ?? "user_id",
3444
- holdout_range: parseHoldout(str(i, "holdout"))
3445
- })
3446
- },
3447
- {
3448
- group: GROUP4,
3449
- name: "update",
3450
- mutates: true,
3451
- summary: "Update a universe's holdout range",
3452
- description: "Update the holdout range. Pass 'null' to clear the holdout.",
3453
- params: [
3454
- { name: "name", type: "string", description: "Universe name.", required: true, positional: true },
3455
- { name: "holdout", type: "string", description: "Holdout range as 'lo,hi' (0\u20139999), or 'null' to clear." }
3456
- ],
3457
- examples: [
3458
- { note: "Set a 5% holdout", run: "shipeasy release experiments universes update web --holdout 0,499" },
3459
- { note: "Clear the holdout", run: "shipeasy release experiments universes update web --holdout null" }
3460
- ],
3461
- run: async (client, i) => {
3462
- const x = await client.universes.resolve(i.name);
3463
- const patch = i.holdout === void 0 ? {} : { holdout_range: parseHoldout(str(i, "holdout")) };
3464
- return client.universes.update(x.id, patch);
3465
- }
3466
- },
3467
- {
3468
- group: GROUP4,
3469
- name: "archive",
3470
- mutates: true,
3471
- summary: "Archive a universe",
3472
- description: "Archive (soft-delete) a universe by name.",
3473
- params: [{ name: "name", type: "string", description: "Universe name.", required: true, positional: true }],
3474
- examples: [{ run: "shipeasy release experiments universes archive web" }],
3475
- run: async (client, i) => {
3476
- const x = await client.universes.resolve(i.name);
3477
- await client.universes.delete(x.id);
3478
- return { ok: true, archived: i.name };
3479
- }
3480
- }
3481
- ];
3482
-
3483
- // src/operations/metrics.ts
3484
- var GROUP5 = ["metrics"];
3485
- var METRIC_GRAMMAR = `Shipeasy metric query DSL
3486
- =========================
3487
-
3488
- Query := AggFunc "(" Selector ("," Identifier)? ")" GroupBy?
3489
- AggFunc := count_users | count | sum | avg | min | max | unique |
3490
- p50 | p75 | p90 | p95 | p99 | p999 |
3491
- retention_<N>d (N in 1..90)
3492
- Selector := Identifier ("{" Filter ("," Filter)* ","? "}")?
3493
- Filter := Identifier MatchOp StringLiteral
3494
- MatchOp := "=" | "!=" | "=~" | "!~"
3495
- StringLiteral := double-quoted, supports \\" and \\\\
3496
- GroupBy := ("by" | "without") "(" Identifier ("," Identifier)* ","? ")"
3497
-
3498
- Examples
3499
- --------
3500
- count_users(checkout_completed)
3501
- sum(purchase{country="US"}, amount)
3502
- p99(req_dur{route=~"/api/.*"}, ms) by (route, status)
3503
- retention_7d(session_start)
3504
- avg(req_dur{tier!="free"}, ms) without (region)
3505
-
3506
- Notes
3507
- -----
3508
- - One event selector per query (no formulas).
3509
- - "count" maps to count_events (per-row count); "count_users" is uniq-by-user.
3510
- - For experiments, "by (label)" is ignored \u2014 per-user reducer is applied.
3511
- - Filter values must be quoted strings, even for numeric labels (they are coerced).
3512
- - Filter labels and value labels must be declared on the source event.
3513
- `;
3514
- var metricOperations = [
3515
- {
3516
- group: GROUP5,
3517
- name: "grammar",
3518
- mutates: false,
3519
- summary: "Print the metric query DSL grammar",
3520
- 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.",
3521
- params: [],
3522
- examples: [{ run: "shipeasy metrics grammar" }],
3523
- run: async () => ({ grammar: METRIC_GRAMMAR })
3524
- },
3525
- {
3526
- group: GROUP5,
3527
- name: "list",
3528
- mutates: false,
3529
- summary: "List all metrics",
3530
- description: "Return every metric in the project (name, folder, source event, aggregation).",
3531
- params: [],
3532
- examples: [{ run: "shipeasy metrics list" }],
3533
- run: (client) => client.metrics.list()
3534
- },
3535
- {
3536
- group: GROUP5,
3537
- name: "show",
3538
- mutates: false,
3539
- summary: "Show one metric",
3540
- description: "Resolve a metric by id, id-prefix, or name and return its full detail.",
3541
- params: [{ name: "metric", type: "string", description: "Metric id or name.", required: true, positional: true }],
3542
- examples: [{ run: "shipeasy metrics show checkouts" }],
3543
- run: (client, i) => client.metrics.resolve(i.metric)
3544
- },
3545
- {
3546
- group: GROUP5,
3547
- name: "create",
3548
- mutates: true,
3549
- summary: "Create a metric",
3550
- 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.",
3551
- params: [
3552
- { name: "name", type: "string", description: "Metric name.", required: true, positional: true },
3553
- { name: "event", type: "string", description: "Source event name (must equal the query's event).", required: true },
3554
- { name: "query", type: "string", description: "Query DSL string, e.g. 'sum(purchase, amount)'." },
3555
- { name: "queryIr", type: "json", description: "Typed IR JSON (alternative to query)." },
3556
- { name: "folder", type: "string", description: "Folder to file the metric under." },
3557
- { name: "winsorize", type: "number", description: "Winsorize percentile (1\u201399). Defaults to 99." },
3558
- { name: "mde", type: "number", description: "Min detectable effect (0\u20131)." }
3559
- ],
3560
- examples: [
3561
- { note: "Unique users who completed checkout", run: "shipeasy metrics create checkouts --event checkout_completed --query 'count_users(checkout_completed)'" },
3562
- { note: "p99 latency grouped by route", run: `shipeasy metrics create api-latency --event req_dur --query 'p99(req_dur{route=~"/api/.*"}, ms) by (route)'` }
3563
- ],
3564
- run: (client, i) => client.metrics.create({
3565
- name: i.name,
3566
- event_name: i.event,
3567
- folder: str(i, "folder") ?? null,
3568
- winsorize_pct: num(i, "winsorize") ?? 99,
3569
- min_detectable_effect: num(i, "mde") ?? null,
3570
- ...i.query ? { query: i.query } : {},
3571
- ...i.queryIr ? { query_ir: i.queryIr } : {}
3572
- })
3573
- },
3574
- {
3575
- group: GROUP5,
3576
- name: "archive",
3577
- mutates: true,
3578
- summary: "Archive a metric",
3579
- description: "Archive (soft-delete) a metric by id or name.",
3580
- params: [{ name: "metric", type: "string", description: "Metric id or name.", required: true, positional: true }],
3581
- examples: [{ run: "shipeasy metrics archive checkouts" }],
3582
- run: async (client, i) => {
3583
- const m = await client.metrics.resolve(i.metric);
3584
- await client.metrics.delete(m.id);
3585
- return { ok: true, archived: m.name };
3586
- }
3587
- }
3588
- ];
3589
-
3590
- // src/operations/events.ts
3591
- var GROUP6 = ["events"];
3592
- 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.`;
3593
- function props(i) {
3594
- return i.props === void 0 ? void 0 : i.props;
3595
- }
3596
- var eventOperations = [
3597
- {
3598
- group: GROUP6,
3599
- name: "list",
3600
- mutates: false,
3601
- summary: "List catalogued events",
3602
- description: "List every catalogued event (including pending auto-discovered names). Pass `pending` to show only the unapproved queue.",
3603
- params: [{ name: "pending", type: "boolean", description: "Show only pending (unapproved) events." }],
3604
- examples: [
3605
- { run: "shipeasy metrics events list" },
3606
- { note: "Review the auto-discovered queue", run: "shipeasy metrics events list --pending true" }
3607
- ],
3608
- run: async (client, i) => {
3609
- const rows = await client.events.list();
3610
- return bool(i, "pending") ? rows.filter((e) => e.pending === 1) : rows;
3611
- }
3612
- },
3613
- {
3614
- group: GROUP6,
3615
- name: "get",
3616
- mutates: false,
3617
- summary: "Show one catalogued event",
3618
- description: "Resolve an event by id, id-prefix, or name and return its full detail.",
3619
- params: [{ name: "event", type: "string", description: "Event id or name.", required: true, positional: true }],
3620
- examples: [{ run: "shipeasy metrics events get checkout_completed" }],
3621
- run: (client, i) => client.events.resolve(i.event)
3622
- },
3623
- {
3624
- group: GROUP6,
3625
- name: "create",
3626
- mutates: true,
3627
- summary: "Register a new event",
3628
- description: "Register a new event name and (optionally) its typed properties.",
3629
- params: [
3630
- { name: "name", type: "string", description: "Event name.", required: true, positional: true },
3631
- { name: "folder", type: "string", description: "Folder to file it under." },
3632
- { name: "description", type: "string", description: "Human-readable description." },
3633
- { name: "props", type: "json", description: PROPS_DESC }
3634
- ],
3635
- examples: [
3636
- { run: "shipeasy metrics events create checkout_completed" },
3637
- { note: "With a typed, required property", run: `shipeasy metrics events create purchase --props '[{"name":"amount","type":"number","required":true}]'` }
3638
- ],
3639
- run: (client, i) => client.events.create({
3640
- name: i.name,
3641
- folder: str(i, "folder") ?? null,
3642
- ...i.description ? { description: i.description } : {},
3643
- properties: props(i) ?? []
3644
- })
3645
- },
3646
- {
3647
- group: GROUP6,
3648
- name: "update",
3649
- mutates: true,
3650
- summary: "Update an event",
3651
- description: "Update an event's folder, description, or properties (props replace the full set).",
3652
- params: [
3653
- { name: "event", type: "string", description: "Event id.", required: true, positional: true },
3654
- { name: "folder", type: "string", description: "New folder." },
3655
- { name: "description", type: "string", description: "New description." },
3656
- { name: "props", type: "json", description: PROPS_DESC }
3657
- ],
3658
- examples: [{ run: "shipeasy metrics events update evt_3f9a --folder checkout" }],
3659
- run: (client, i) => {
3660
- const patch = {};
3661
- if (i.folder !== void 0) patch.folder = i.folder;
3662
- if (i.description !== void 0) patch.description = i.description;
3663
- const p = props(i);
3664
- if (p) patch.properties = p;
3665
- return client.events.update(i.event, patch);
3666
- }
3667
- },
3668
- {
3669
- group: GROUP6,
3670
- name: "approve",
3671
- mutates: true,
3672
- summary: "Approve a pending event",
3673
- description: "Promote a pending (auto-discovered) event so metrics can query it. Optionally declare its folder/props on approval.",
3674
- params: [
3675
- { name: "event", type: "string", description: "Event id.", required: true, positional: true },
3676
- { name: "folder", type: "string", description: "Folder to file it under." },
3677
- { name: "description", type: "string", description: "Description." },
3678
- { name: "props", type: "json", description: PROPS_DESC }
3679
- ],
3680
- examples: [{ note: "Promote an auto-discovered event", run: "shipeasy metrics events approve evt_3f9a" }],
3681
- run: (client, i) => {
3682
- const body = {};
3683
- if (i.folder !== void 0) body.folder = i.folder;
3684
- if (i.description !== void 0) body.description = i.description;
3685
- const p = props(i);
3686
- if (p) body.properties = p;
3687
- return client.events.approve(i.event, body);
3688
- }
3689
- },
3690
- {
3691
- group: GROUP6,
3692
- name: "archive",
3693
- mutates: true,
3694
- summary: "Archive an event",
3695
- description: "Archive (remove) an event from the catalog by id.",
3696
- params: [{ name: "event", type: "string", description: "Event id.", required: true, positional: true }],
3697
- examples: [{ run: "shipeasy metrics events archive evt_3f9a" }],
3698
- run: async (client, i) => {
3699
- await client.events.delete(i.event);
3700
- return { ok: true, archived: i.event };
3701
- }
3702
- }
3703
- ];
3704
-
3705
- // src/operations/ops.ts
3706
- var QUEUE = ["ops"];
3707
- var ALERTS = ["ops", "alerts"];
3708
- var TYPES = ["bug", "feature_request", "error", "alert"];
3709
- var STATUSES = [
3710
- "open",
3711
- "triaged",
3712
- "in_progress",
3713
- "ready_for_qa",
3714
- "resolved",
3715
- "wont_fix"
3716
- ];
3717
- var PRIORITIES = ["nice_to_have", "medium", "high", "critical"];
3718
- var COMPARATORS = ["gt", "gte", "lt", "lte"];
3719
- var SEVERITIES = ["danger", "warn", "info"];
3720
- async function resolveSlackChannel(client, idOrName) {
3721
- const res = await client.ops.channels();
3722
- if (!res.connected)
3723
- throw new ApiError("No Slack connector \u2014 connect Slack in Settings \u2192 Notifications first.", 400);
3724
- const stripped = idOrName.replace(/^#/, "");
3725
- const byId = res.channels.find((c) => c.id === idOrName);
3726
- if (byId) return { id: byId.id, name: byId.name };
3727
- const byName = res.channels.filter((c) => c.name === stripped);
3728
- if (byName.length === 1) return { id: byName[0].id, name: byName[0].name };
3729
- if (byName.length > 1)
3730
- throw new ApiError(`Slack channel name '${stripped}' is ambiguous \u2014 pass a channel id`, 400);
3731
- throw new ApiError(`Slack channel '${idOrName}' not found (run 'ops alerts channels')`, 404);
3732
- }
3733
- var opsOperations = [
3734
- // ── unified queue ──────────────────────────────────────────────────────────
3735
- {
3736
- group: QUEUE,
3737
- name: "list",
3738
- mutates: false,
3739
- summary: "List the operational queue",
3740
- description: "List the unified queue \u2014 bugs, feature requests, and auto-filed error/alert tickets. Filter by `type`, `status`, or `priority`.",
3741
- params: [
3742
- { name: "type", type: "string", description: "Filter by type.", enum: [...TYPES, "all"] },
3743
- { name: "status", type: "string", description: "Filter by status.", enum: [...STATUSES, "all"] },
3744
- { name: "priority", type: "string", description: "Filter by priority.", enum: PRIORITIES },
3745
- { name: "limit", type: "number", description: "Max rows (default 200)." }
3746
- ],
3747
- examples: [
3748
- { note: "Open critical items only", run: "shipeasy ops list --priority critical" },
3749
- { note: "Just error tickets", run: "shipeasy ops list --type error" }
3750
- ],
3751
- run: async (client, i) => {
3752
- let items = await client.ops.list({
3753
- type: str(i, "type") ?? "all",
3754
- status: str(i, "status") ?? "open",
3755
- limit: num(i, "limit") ?? 200
3756
- });
3757
- const priority = str(i, "priority");
3758
- if (priority) items = items.filter((x) => x.priority === priority);
3759
- return items;
3760
- }
3761
- },
3762
- {
3763
- group: QUEUE,
3764
- name: "get",
3765
- mutates: false,
3766
- summary: "Show one queue item",
3767
- description: "Show one queue item by number (#7 \u2192 7) or id \u2014 any type.",
3768
- params: [{ name: "handle", type: "string", description: "Item number or id.", required: true, positional: true }],
3769
- examples: [{ note: "By queue number", run: "shipeasy ops get 7" }],
3770
- run: (client, i) => client.ops.get(i.handle)
3771
- },
3772
- {
3773
- group: QUEUE,
3774
- name: "create",
3775
- mutates: true,
3776
- summary: "File a bug or feature request",
3777
- 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.",
3778
- params: [
3779
- { name: "type", type: "string", description: "Item type.", required: true, enum: ["bug", "feature_request"] },
3780
- { name: "title", type: "string", description: "One-line title.", required: true, positional: true },
3781
- { name: "body", type: "string", description: "Detailed description." },
3782
- { name: "priority", type: "string", description: "Priority.", enum: PRIORITIES },
3783
- { name: "stepsToReproduce", type: "string", description: "Repro steps (bugs)." },
3784
- { name: "pageUrl", type: "string", description: "Page URL the item relates to." }
3785
- ],
3786
- examples: [
3787
- { note: "File a bug", run: 'shipeasy ops create "Checkout 500s on Safari" --type bug' },
3788
- { note: "File a feature request", run: 'shipeasy ops create "Dark mode" --type feature_request --priority medium' }
3789
- ],
3790
- run: (client, i) => client.ops.create({
3791
- type: i.type,
3792
- title: i.title,
3793
- ...i.body ? { body: i.body } : {},
3794
- ...i.priority ? { priority: i.priority } : {},
3795
- ...i.stepsToReproduce ? { stepsToReproduce: i.stepsToReproduce } : {},
3796
- ...i.pageUrl ? { pageUrl: i.pageUrl } : {}
3797
- })
3798
- },
3799
- {
3800
- group: QUEUE,
3801
- name: "update",
3802
- mutates: true,
3803
- summary: "Update a queue item's status/priority",
3804
- description: "Flip a queue item's status (any type) and optionally its priority.",
3805
- params: [
3806
- { name: "handle", type: "string", description: "Item number or id.", required: true, positional: true },
3807
- { name: "status", type: "string", description: "New status.", enum: STATUSES },
3808
- { name: "priority", type: "string", description: "New priority.", enum: PRIORITIES }
3809
- ],
3810
- examples: [
3811
- { note: "Resolve item #7", run: "shipeasy ops update 7 --status resolved" },
3812
- { note: "Bump priority", run: "shipeasy ops update 7 --priority high" }
3813
- ],
3814
- run: (client, i) => {
3815
- const patch = {};
3816
- if (i.status !== void 0) patch.status = i.status;
3817
- if (i.priority !== void 0) patch.priority = i.priority;
3818
- if (!Object.keys(patch).length)
3819
- throw new ApiError("Nothing to update \u2014 pass status and/or priority.", 400);
3820
- return client.ops.update(i.handle, patch);
3821
- }
3822
- },
3823
- {
3824
- group: QUEUE,
3825
- name: "link-pr",
3826
- mutates: true,
3827
- summary: "Link the PR that fixed an item",
3828
- description: "Link the PR that fixed a queue item (any type). Records connector_data.github.pr. Pass `remove` to unlink.",
3829
- params: [
3830
- { name: "handle", type: "string", description: "Item number or id.", required: true, positional: true },
3831
- { name: "prNumber", type: "number", description: "PR number.", positional: true },
3832
- { name: "url", type: "string", description: "Explicit PR URL (required for error/alert tickets)." },
3833
- { name: "remove", type: "boolean", description: "Unlink the PR instead of setting one." }
3834
- ],
3835
- examples: [
3836
- { note: "Link the fixing PR", run: "shipeasy ops link-pr 7 42 --url https://github.com/acme/app/pull/42" }
3837
- ],
3838
- run: (client, i) => {
3839
- if (i.remove) return client.ops.linkPr(i.handle, { prNumber: null });
3840
- const prNumber = num(i, "prNumber");
3841
- if (prNumber === void 0 || !Number.isInteger(prNumber) || prNumber <= 0)
3842
- throw new ApiError("Pass a positive PR number (or --remove to unlink).", 400);
3843
- return client.ops.linkPr(i.handle, {
3844
- prNumber,
3845
- ...i.url ? { prUrl: i.url } : {}
3846
- });
3847
- }
3848
- },
3849
- {
3850
- group: QUEUE,
3851
- name: "notify",
3852
- mutates: true,
3853
- summary: "Raise a 'needs your attention' bell notification",
3854
- 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`.",
3855
- params: [
3856
- { name: "title", type: "string", description: "One-line headline of what's blocked.", required: true },
3857
- { name: "summary", type: "string", description: "One sentence: why it can't be fixed in code.", required: true },
3858
- { name: "steps", type: "json", description: "Steps the human should take, as a JSON array of strings." },
3859
- { name: "href", type: "string", description: "Dashboard-relative deep link." },
3860
- { name: "item", type: "string", description: "Queue item number this is about (sets dedupe key feedback:<n>)." },
3861
- { name: "key", type: "string", description: "Explicit dedupe key (overrides item)." }
3862
- ],
3863
- examples: [
3864
- { 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"` }
3865
- ],
3866
- run: (client, i) => {
3867
- const item = str(i, "item");
3868
- const dedupeKey = str(i, "key") ?? (item ? `feedback:${item.replace(/^#/, "")}` : void 0);
3869
- return client.ops.notify({
3870
- title: i.title,
3871
- summary: i.summary,
3872
- steps: i.steps ?? [],
3873
- ...i.href ? { href: i.href } : {},
3874
- ...dedupeKey ? { dedupeKey } : {}
3875
- });
3876
- }
3877
- },
3878
- // ── alert rules (ops alerts …) ───────────────────────────────────────────────
3879
- {
3880
- group: ALERTS,
3881
- name: "list",
3882
- mutates: false,
3883
- summary: "List alert rules",
3884
- description: "List the project's metric-threshold alert rules the analysis cron evaluates.",
3885
- params: [],
3886
- examples: [{ run: "shipeasy ops alerts list" }],
3887
- run: (client) => client.alertRules.list()
3888
- },
3889
- {
3890
- group: ALERTS,
3891
- name: "channels",
3892
- mutates: false,
3893
- summary: "List the project's Slack channels",
3894
- description: "List the project's Slack channels (for the `slackChannel` alert target).",
3895
- params: [],
3896
- examples: [{ run: "shipeasy ops alerts channels" }],
3897
- run: (client) => client.ops.channels()
3898
- },
3899
- {
3900
- group: ALERTS,
3901
- name: "create",
3902
- mutates: true,
3903
- summary: "Create an alert rule",
3904
- 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.",
3905
- params: [
3906
- { name: "name", type: "string", description: "Rule name.", required: true, positional: true },
3907
- { name: "metric", type: "string", description: "Metric to evaluate (id or name).", required: true },
3908
- { name: "comparator", type: "string", description: "Comparison operator.", required: true, enum: COMPARATORS },
3909
- { name: "threshold", type: "number", description: "Threshold the metric value is compared against.", required: true },
3910
- { name: "window", type: "number", description: "Lookback window in whole hours (1\u2013720). Defaults to 24." },
3911
- { name: "severity", type: "string", description: "Severity. Defaults to warn.", enum: SEVERITIES },
3912
- { name: "disabled", type: "boolean", description: "Create the rule disabled." },
3913
- { name: "slackChannel", type: "string", description: "Slack channel to post to (id or name)." },
3914
- { name: "email", type: "string", description: "Email address to notify." }
3915
- ],
3916
- examples: [
3917
- { note: "Alert when error rate exceeds 50 / 24h", run: "shipeasy ops alerts create high-error-rate --metric api-errors --comparator gt --threshold 50" },
3918
- { 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" }
3919
- ],
3920
- run: async (client, i) => {
3921
- const metric = await client.metrics.resolve(i.metric);
3922
- const slackChannel = str(i, "slackChannel");
3923
- const email = str(i, "email");
3924
- const notify = slackChannel || email ? {
3925
- slackChannel: slackChannel ? await resolveSlackChannel(client, slackChannel) : null,
3926
- email: email ?? null
3927
- } : void 0;
3928
- return client.alertRules.create({
3929
- name: i.name,
3930
- metricId: metric.id,
3931
- comparator: i.comparator,
3932
- threshold: num(i, "threshold"),
3933
- windowHours: num(i, "window") ?? 24,
3934
- severity: str(i, "severity") ?? "warn",
3935
- enabled: !i.disabled,
3936
- ...notify ? { notify } : {}
3937
- });
3938
- }
3939
- },
3940
- {
3941
- group: ALERTS,
3942
- name: "update",
3943
- mutates: true,
3944
- summary: "Update an alert rule",
3945
- 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.",
3946
- params: [
3947
- { name: "rule", type: "string", description: "Rule id, id-prefix, or name.", required: true, positional: true },
3948
- { name: "name", type: "string", description: "New human label." },
3949
- { name: "comparator", type: "string", description: "Comparison operator.", enum: COMPARATORS },
3950
- { name: "threshold", type: "number", description: "New threshold." },
3951
- { name: "window", type: "number", description: "New lookback window in whole hours (1\u2013720)." },
3952
- { name: "severity", type: "string", description: "New severity.", enum: SEVERITIES },
3953
- { name: "enabled", type: "boolean", description: "Enable/disable the rule." },
3954
- { name: "slackChannel", type: "string", description: "Set the Slack channel target (id or name)." },
3955
- { name: "email", type: "string", description: "Set the email target." },
3956
- { name: "clearTarget", type: "boolean", description: "Clear the target back to the project default." }
3957
- ],
3958
- examples: [
3959
- { note: "Raise the threshold", run: "shipeasy ops alerts update high-error-rate --threshold 100" },
3960
- { note: "Disable a rule", run: "shipeasy ops alerts update high-error-rate --enabled false" }
3961
- ],
3962
- run: async (client, i) => {
3963
- const rule = await client.alertRules.resolve(i.rule);
3964
- const patch = {};
3965
- if (i.name !== void 0) patch.name = i.name;
3966
- if (i.comparator !== void 0) patch.comparator = i.comparator;
3967
- if (i.threshold !== void 0) patch.threshold = num(i, "threshold");
3968
- if (i.window !== void 0) patch.windowHours = num(i, "window");
3969
- if (i.severity !== void 0) patch.severity = i.severity;
3970
- if (i.enabled !== void 0) patch.enabled = i.enabled;
3971
- const slackChannel = str(i, "slackChannel");
3972
- const email = str(i, "email");
3973
- if (i.clearTarget) {
3974
- patch.notify = { slackChannel: null, email: null };
3975
- } else if (slackChannel || email) {
3976
- patch.notify = {
3977
- slackChannel: slackChannel ? await resolveSlackChannel(client, slackChannel) : rule.notify?.slackChannel ?? null,
3978
- email: email ?? rule.notify?.email ?? null
3979
- };
3980
- }
3981
- if (!Object.keys(patch).length)
3982
- throw new ApiError("Nothing to update \u2014 pass at least one field to change.", 400);
3983
- return client.alertRules.update(rule.id, patch);
3984
- }
3985
- },
3986
- {
3987
- group: ALERTS,
3988
- name: "archive",
3989
- mutates: true,
3990
- summary: "Archive an alert rule",
3991
- description: "Archive (delete) an alert rule by id, id-prefix, or unique name.",
3992
- params: [{ name: "rule", type: "string", description: "Rule id, id-prefix, or name.", required: true, positional: true }],
3993
- examples: [{ run: "shipeasy ops alerts archive high-error-rate" }],
3994
- run: async (client, i) => {
3995
- const rule = await client.alertRules.resolve(i.rule);
3996
- await client.alertRules.delete(rule.id);
3997
- return { ok: true, archived: rule.name };
3998
- }
3999
- }
4000
- ];
4001
-
4002
- // src/operations/projects.ts
4003
- var GROUP7 = ["projects"];
4004
- var projectOperations = [
4005
- {
4006
- group: GROUP7,
4007
- name: "current",
4008
- mutates: false,
4009
- summary: "Show the current project",
4010
- 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`.",
4011
- params: [],
4012
- examples: [{ run: "shipeasy projects current" }],
4013
- run: (client) => client.projects.current()
4014
- },
4015
- {
4016
- group: GROUP7,
4017
- name: "upsert",
4018
- mutates: false,
4019
- summary: "Find-or-create a project by domain",
4020
- 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.",
4021
- params: [
4022
- { name: "domain", type: "string", description: "Hostname-like project identifier (e.g. acme.com).", required: true },
4023
- { name: "name", type: "string", description: "Human-readable name (defaults to the domain on first create)." }
4024
- ],
4025
- examples: [
4026
- { note: "Find-or-create by domain", run: "shipeasy projects upsert --domain acme.com" },
4027
- { note: "Name it explicitly", run: 'shipeasy projects upsert --domain shouks.app --name "Shouks"' }
4028
- ],
4029
- run: (client, i) => client.projects.upsert({
4030
- domain: i.domain,
4031
- ...i.name ? { name: i.name } : {}
4032
- })
4033
- }
4034
- ];
4035
-
4036
- // src/operations/i18n.ts
4037
- var I18N = ["i18n"];
4038
- var PROFILES2 = ["i18n", "profiles"];
4039
- var DRAFTS2 = ["i18n", "drafts"];
4040
- var i18nOperations = [
4041
- {
4042
- group: PROFILES2,
4043
- name: "list",
4044
- mutates: false,
4045
- summary: "List i18n locale profiles",
4046
- description: "List the project's i18n locale profiles (name, default locale, locales).",
4047
- params: [],
4048
- examples: [{ run: "shipeasy i18n profiles list" }],
4049
- run: (client) => client.i18n.listProfiles()
4050
- },
4051
- {
4052
- group: PROFILES2,
4053
- name: "create",
4054
- mutates: true,
4055
- summary: "Create an i18n locale profile",
4056
- description: "Create an i18n profile (e.g. 'en:prod', 'fr:prod') with one or more locales.",
4057
- params: [
4058
- { name: "name", type: "string", description: "Profile name.", required: true, positional: true },
4059
- { name: "locales", type: "json", description: `Locales as a JSON array, e.g. '["fr","fr-CA"]'. Defaults to ["en"].` },
4060
- { name: "defaultLocale", type: "string", description: "Default locale (defaults to the first locale)." }
4061
- ],
4062
- examples: [
4063
- { run: "shipeasy i18n profiles create en:prod" },
4064
- { note: "Multiple locales with a default", run: `shipeasy i18n profiles create fr:prod --locales '["fr","fr-CA"]' --default-locale fr` }
4065
- ],
4066
- run: (client, i) => {
4067
- const locales = i.locales ?? ["en"];
4068
- return client.i18n.createProfile({
4069
- name: i.name,
4070
- locales,
4071
- default_locale: str(i, "defaultLocale") ?? locales[0]
4072
- });
4073
- }
4074
- },
4075
- {
4076
- group: I18N,
4077
- name: "push",
4078
- mutates: true,
4079
- summary: "Add new i18n keys (insert-only)",
4080
- 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}.",
4081
- params: [
4082
- { name: "profile", type: "string", description: "Profile name (e.g. 'en:prod').", required: true },
4083
- { name: "keys", type: "json", description: `Keys as a JSON array, e.g. '[{"key":"home.cta","value":"Get started"}]'.`, required: true },
4084
- { name: "chunk", type: "string", description: "Logical grouping for the keys. Defaults to 'default'." }
4085
- ],
4086
- examples: [
4087
- { note: "Add keys to a profile", run: `shipeasy i18n push --profile en:prod --keys '[{"key":"home.cta","value":"Get started"}]'` }
4088
- ],
4089
- run: async (client, i) => {
4090
- const profile = await client.i18n.resolveProfile(i.profile);
4091
- return client.i18n.pushKeys({
4092
- profile_id: profile.id,
4093
- chunk: str(i, "chunk") ?? "default",
4094
- keys: i.keys
4095
- });
4096
- }
4097
- },
4098
- {
4099
- group: I18N,
4100
- name: "update",
4101
- mutates: true,
4102
- summary: "Update one i18n key's value",
4103
- description: "Change the value of a single existing key (the only overwrite path; `push` only adds). One key per call.",
4104
- params: [
4105
- { name: "key", type: "string", description: "Key name.", required: true, positional: true },
4106
- { name: "value", type: "string", description: "New value.", required: true, positional: true },
4107
- { name: "profile", type: "string", description: "Profile name (e.g. 'en:prod').", required: true },
4108
- { name: "description", type: "string", description: "Optional description to store with the key." }
4109
- ],
4110
- examples: [
4111
- { note: "Change one key's value", run: "shipeasy i18n update home.cta 'Get started' --profile en:prod" }
4112
- ],
4113
- run: async (client, i) => {
4114
- const profile = await client.i18n.resolveProfile(i.profile);
4115
- return client.i18n.updateKeyByName(profile.id, i.key, {
4116
- value: i.value,
4117
- ...i.description !== void 0 ? { description: i.description } : {}
4118
- });
4119
- }
4120
- },
4121
- {
4122
- group: I18N,
4123
- name: "publish",
4124
- mutates: true,
4125
- summary: "Publish a profile chunk to the CDN",
4126
- description: "Publish a profile chunk (rebuilds the KV manifest and purges the CDN).",
4127
- params: [
4128
- { name: "profile", type: "string", description: "Profile name (e.g. 'en:prod').", required: true },
4129
- { name: "chunk", type: "string", description: "Chunk to publish. Defaults to 'default'." }
4130
- ],
4131
- examples: [
4132
- { note: "Publish the default chunk", run: "shipeasy i18n publish --profile en:prod" },
4133
- { note: "Publish a specific chunk", run: "shipeasy i18n publish --profile fr:prod --chunk marketing" }
4134
- ],
4135
- run: async (client, i) => {
4136
- const profile = await client.i18n.resolveProfile(i.profile);
4137
- return client.i18n.publish(profile.id, { chunk: str(i, "chunk") ?? "default" });
4138
- }
4139
- },
4140
- {
4141
- group: DRAFTS2,
4142
- name: "list",
4143
- mutates: false,
4144
- summary: "List staged i18n drafts",
4145
- description: "List staged translation drafts. Replaces the `drafts` kind of the deleted MCP generic read.",
4146
- params: [],
4147
- examples: [{ run: "shipeasy i18n drafts list" }],
4148
- run: (client) => client.i18n.listDrafts()
4149
- }
4150
- ];
4151
-
4152
- // src/operations/attributes.ts
4153
- var attributeOperations = [
4154
- {
4155
- group: ["attributes"],
4156
- name: "list",
4157
- mutates: false,
4158
- summary: "List targeting attributes",
4159
- description: "List the auto-inferred user-attribute schema available for gate/experiment targeting.",
4160
- params: [],
4161
- examples: [{ run: "shipeasy attributes list" }],
4162
- run: (client) => client.attributes.list()
4163
- }
4164
- ];
4165
-
4166
- // src/operations/sdk-docs.ts
4167
- var SDK_REPOS = {
4168
- typescript: { owner: "shipeasy-ai", repo: "sdk" },
4169
- javascript: { owner: "shipeasy-ai", repo: "sdk" },
4170
- node: { owner: "shipeasy-ai", repo: "sdk" },
4171
- ts: { owner: "shipeasy-ai", repo: "sdk" },
4172
- python: { owner: "shipeasy-ai", repo: "sdk-python" },
4173
- go: { owner: "shipeasy-ai", repo: "sdk-go" },
4174
- java: { owner: "shipeasy-ai", repo: "sdk-java" },
4175
- kotlin: { owner: "shipeasy-ai", repo: "sdk-kotlin" },
4176
- php: { owner: "shipeasy-ai", repo: "sdk-php" },
4177
- swift: { owner: "shipeasy-ai", repo: "sdk-swift" },
4178
- ruby: { owner: "shipeasy-ai", repo: "sdk-ruby" }
4179
- };
4180
- var SDK_ENUM = Object.keys(SDK_REPOS);
4181
- var DEFAULT_MANIFEST = {
4182
- schemaVersion: 2,
4183
- sdk: "default",
4184
- placeholders: ["RESOURCE_NAME"],
4185
- skill: "skill/SKILL.md",
4186
- pages: { overview: "pages/overview.md", installation: "pages/installation.md" },
4187
- snippets: { release: { flags: "snippets/release/flags.md" } }
4188
- };
4189
- var DEFAULT_CONTENT = {
4190
- "pages/overview.md": "# Shipeasy SDK\n\nThis SDK has not published its `/docs/` folder yet. See https://docs.shipeasy.ai for the full reference.",
4191
- "pages/installation.md": "# Installation\n\nInstall the Shipeasy SDK for your language, then call `shipeasy({ serverKey })` (server) or `shipeasy({ clientKey })` (browser).",
4192
- "snippets/release/flags.md": "```\nconst on = await client.checkGate(user, '{{RESOURCE_NAME}}');\n```",
4193
- "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."
4194
- };
4195
- function repoFor(sdk) {
4196
- const r = SDK_REPOS[sdk.toLowerCase()];
4197
- if (!r) throw new ApiError(`Unknown SDK '${sdk}'. One of: ${SDK_ENUM.join(", ")}`, 400);
4198
- return r;
4199
- }
4200
- function pagesBase(sdk) {
4201
- const { owner, repo } = repoFor(sdk);
4202
- return `https://${owner}.github.io/${repo}`;
4203
- }
4204
- async function fetchText(url) {
4205
- const res = await fetch(url);
4206
- if (!res.ok) return null;
4207
- return res.text();
4208
- }
4209
- async function loadManifest(sdk) {
4210
- const text = await fetchText(`${pagesBase(sdk)}/manifest.json`);
4211
- if (!text) return { manifest: DEFAULT_MANIFEST, fallback: true };
4212
- try {
4213
- return { manifest: JSON.parse(text), fallback: false };
4214
- } catch {
4215
- return { manifest: DEFAULT_MANIFEST, fallback: true };
4216
- }
4217
- }
4218
- function resolvePath(manifest, handle) {
4219
- if (handle.endsWith(".md")) return handle;
4220
- if (manifest.pages?.[handle]) return manifest.pages[handle];
4221
- if (handle.includes("/")) {
4222
- const [group, resource] = handle.split("/", 2);
4223
- const p = manifest.snippets?.[group]?.[resource];
4224
- if (p) return p;
4225
- }
4226
- throw new ApiError(
4227
- `Doc '${handle}' not found. Run 'docs list --sdk <lang>' to see available pages and snippets.`,
4228
- 404
4229
- );
4230
- }
4231
- function substitute(body, manifest, i) {
4232
- let out = body;
4233
- for (const ph of manifest.placeholders ?? []) {
4234
- const value = str(i, ph) ?? (ph === "RESOURCE_NAME" ? str(i, "name") : void 0) ?? (ph === "FRAMEWORK" ? str(i, "framework") : void 0);
4235
- if (value !== void 0) out = out.replaceAll(`{{${ph}}}`, value);
4236
- }
4237
- return out;
4238
- }
4239
- var docsOperations = [
4240
- {
4241
- group: ["docs"],
4242
- name: "list",
4243
- mutates: false,
4244
- summary: "List an SDK's documentation tree",
4245
- 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.",
4246
- params: [{ name: "sdk", type: "string", description: "SDK language.", required: true, enum: SDK_ENUM }],
4247
- examples: [{ run: "shipeasy docs list --sdk python" }],
4248
- run: async (_client, i) => {
4249
- const { manifest, fallback } = await loadManifest(i.sdk);
4250
- return {
4251
- sdk: i.sdk,
4252
- fallback,
4253
- pages: Object.keys(manifest.pages ?? {}),
4254
- snippets: Object.fromEntries(
4255
- Object.entries(manifest.snippets ?? {}).map(([g, r]) => [g, Object.keys(r)])
4256
- ),
4257
- skill: Boolean(manifest.skill)
4258
- };
4259
- }
4260
- },
4261
- {
4262
- group: ["docs"],
4263
- name: "get",
4264
- mutates: false,
4265
- summary: "Fetch one SDK doc page or snippet",
4266
- 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`.",
4267
- params: [
4268
- { name: "sdk", type: "string", description: "SDK language.", required: true, enum: SDK_ENUM },
4269
- { name: "path", type: "string", description: "Page key or snippet 'group/resource'.", required: true, positional: true },
4270
- { name: "framework", type: "string", description: "Framework hint (substitutes {{FRAMEWORK}})." },
4271
- { name: "name", type: "string", description: "Resource name (substitutes {{RESOURCE_NAME}})." }
4272
- ],
4273
- examples: [
4274
- { note: "A feature page", run: "shipeasy docs get --sdk python experiments" },
4275
- { note: "A nested snippet, with a resource name", run: "shipeasy docs get --sdk typescript release/flags --name checkout_v2" }
4276
- ],
4277
- run: async (_client, i) => {
4278
- const sdk = i.sdk;
4279
- const { manifest, fallback } = await loadManifest(sdk);
4280
- const path = resolvePath(manifest, i.path);
4281
- const body = fallback ? DEFAULT_CONTENT[path] : await fetchText(`${pagesBase(sdk)}/${path}`) ?? void 0;
4282
- if (body === void 0) throw new ApiError(`Doc '${path}' could not be fetched for '${sdk}'.`, 404);
4283
- return { sdk, path, fallback, content: substitute(body, manifest, i) };
4284
- }
4285
- },
4286
- {
4287
- group: ["docs"],
4288
- name: "skill",
4289
- mutates: false,
4290
- summary: "Fetch an SDK's installable LLM skill",
4291
- 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.",
4292
- params: [
4293
- { name: "sdk", type: "string", description: "SDK language.", required: true, enum: SDK_ENUM },
4294
- { name: "install", type: "boolean", description: "CLI only: write the skill to the local agent skills dir." }
4295
- ],
4296
- examples: [
4297
- { run: "shipeasy docs skill --sdk python" },
4298
- { note: "Install into the local skills dir", run: "shipeasy docs skill --sdk python --install true" }
4299
- ],
4300
- run: async (_client, i) => {
4301
- const sdk = i.sdk;
4302
- const { manifest, fallback } = await loadManifest(sdk);
4303
- const skillPath = manifest.skill ?? "skill/SKILL.md";
4304
- const content = fallback ? DEFAULT_CONTENT[skillPath] : await fetchText(`${pagesBase(sdk)}/${skillPath}`) ?? void 0;
4305
- if (content === void 0) throw new ApiError(`No skill published for '${sdk}'.`, 404);
4306
- return { sdk, skillPath, fallback, content };
4307
- }
4308
- }
4309
- ];
4310
-
4311
- // src/operations/index.ts
4312
- var RELEASE_OPERATIONS = [
4313
- ...gateOperations,
4314
- ...killswitchOperations,
4315
- ...configOperations,
4316
- ...experimentOperations,
4317
- ...universeOperations
4318
- ];
4319
- var ALL_OPERATIONS = [
4320
- ...RELEASE_OPERATIONS,
4321
- ...metricOperations,
4322
- ...eventOperations,
4323
- ...opsOperations,
4324
- ...projectOperations,
4325
- ...i18nOperations,
4326
- ...attributeOperations,
4327
- ...docsOperations
4328
- ];
4329
151
  export {
4330
- ALL_OPERATIONS,
4331
- ApiError,
4332
- DEFAULT_PAGE_SIZE,
4333
- MAX_PAGE_SIZE,
4334
- METRIC_GRAMMAR,
4335
- RELEASE_OPERATIONS,
4336
- RESOURCE_REGISTRY,
4337
- alertRulesClient,
4338
- attributeOperations,
4339
- attributesClient,
4340
- coerceInput,
4341
- configOperations,
4342
- configsClient,
4343
- configsResource,
4344
- createAdminClient,
4345
- createHttpTransport,
4346
- decodeCursor,
4347
- docsOperations,
4348
- encodeCursor,
4349
- eventOperations,
4350
- eventsClient,
4351
- experimentOperations,
4352
- experimentsClient,
4353
- experimentsResource,
4354
- gateOperations,
4355
- gatesClient,
4356
- gatesResource,
4357
- i18nClient,
4358
- i18nOperations,
4359
- killswitchOperations,
4360
- killswitchesClient,
4361
- killswitchesResource,
4362
- metricOperations,
4363
- metricsClient,
4364
- mountOperations,
4365
- opCli,
4366
- opId,
4367
- opMcpName,
4368
- operationsToDispatch,
4369
- operationsToMcpTools,
4370
- opsClient,
4371
- opsOperations,
4372
- pageQuerySchema,
4373
- projectOperations,
4374
- projectsClient,
4375
- renderOperationsMarkdown,
4376
- universeOperations,
4377
- universesClient,
4378
- universesResource
152
+ ERROR_CODES,
153
+ approveEvent,
154
+ client,
155
+ configure,
156
+ createAlertRule,
157
+ createAttribute,
158
+ createClient,
159
+ createConfig as createClientConfig,
160
+ createConfig2 as createConfig,
161
+ createConnector,
162
+ createEvent,
163
+ createExperiment,
164
+ createGate,
165
+ createGateTemplate,
166
+ createI18nDraft,
167
+ createI18nProfile,
168
+ createKey,
169
+ createKillswitch,
170
+ createMetric,
171
+ createOpsItem,
172
+ createTriggerConnector,
173
+ createUniverse,
174
+ deleteAlertRule,
175
+ deleteAttribute,
176
+ deleteConfig,
177
+ deleteConnector,
178
+ deleteEvent,
179
+ deleteExperiment,
180
+ deleteGate,
181
+ deleteGateTemplate,
182
+ deleteKillswitch,
183
+ deleteMetric,
184
+ deleteUniverse,
185
+ disableGate,
186
+ discardConfigDraft,
187
+ enableGate,
188
+ errorCodeOf,
189
+ fileErrorTicket,
190
+ fireConnector,
191
+ formatAuthFailure,
192
+ getAttribute,
193
+ getConfig,
194
+ getConnector,
195
+ getCurrentProject,
196
+ getError,
197
+ getErrorSeries,
198
+ getEvent,
199
+ getExperiment,
200
+ getExperimentResults,
201
+ getExperimentTimeseries,
202
+ getGateTemplate,
203
+ getKillswitch,
204
+ getMetric,
205
+ getOpsItem,
206
+ isApiErrorBody,
207
+ linkPrToOpsItem,
208
+ listAlertRules,
209
+ listAttributes,
210
+ listConfigActivity,
211
+ listConfigs,
212
+ listConnectors,
213
+ listErrors,
214
+ listEvents,
215
+ listExperiments,
216
+ listGateTemplates,
217
+ listGates,
218
+ listI18nDrafts,
219
+ listI18nKeys,
220
+ listI18nProfiles,
221
+ listKeys,
222
+ listKillswitches,
223
+ listMetrics,
224
+ listOpsItems,
225
+ listSlackChannels,
226
+ listUniverses,
227
+ notifyOps,
228
+ publishConfigDraft,
229
+ publishI18nProfile,
230
+ pushI18nKeys,
231
+ reanalyzeExperiment,
232
+ revokeKey,
233
+ saveConfigDraft,
234
+ searchResources,
235
+ setExperimentMetrics,
236
+ setExperimentStatus,
237
+ setI18nLabel,
238
+ setKillswitchSwitch,
239
+ setKillswitchValue,
240
+ testConnector,
241
+ unsetKillswitchSwitch,
242
+ updateAlertRule,
243
+ updateAttribute,
244
+ updateConfig,
245
+ updateConfigSchema,
246
+ updateConnector,
247
+ updateErrorStatus,
248
+ updateEvent,
249
+ updateExperiment,
250
+ updateGate,
251
+ updateGateTemplate,
252
+ updateI18nDraft,
253
+ updateI18nKey,
254
+ updateKillswitch,
255
+ updateMetric,
256
+ updateOpsItem,
257
+ updateProject,
258
+ updateTriggerConnector,
259
+ updateUniverse,
260
+ upsertProject
4379
261
  };
262
+ //# sourceMappingURL=index.js.map