@shipeasy/openapi 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/index.d.ts +24 -0
  2. package/dist/index.js +1924 -3
  3. package/dist/node-context.d.ts +37 -0
  4. package/dist/node-context.js +56 -0
  5. package/dist/operations/attributes.d.ts +33 -0
  6. package/dist/operations/attributes.js +16 -0
  7. package/dist/operations/cli-adapter.d.ts +35 -0
  8. package/dist/operations/cli-adapter.js +91 -0
  9. package/dist/operations/coerce.d.ts +32 -0
  10. package/dist/operations/coerce.js +53 -0
  11. package/dist/operations/configs.d.ts +28 -0
  12. package/dist/operations/configs.js +119 -0
  13. package/dist/operations/docs.d.ts +37 -0
  14. package/dist/operations/docs.js +39 -0
  15. package/dist/operations/events.d.ts +28 -0
  16. package/dist/operations/events.js +121 -0
  17. package/dist/operations/experiments.d.ts +28 -0
  18. package/dist/operations/experiments.js +280 -0
  19. package/dist/operations/gates.d.ts +28 -0
  20. package/dist/operations/gates.js +132 -0
  21. package/dist/operations/i18n.d.ts +28 -0
  22. package/dist/operations/i18n.js +121 -0
  23. package/dist/operations/index.d.ts +63 -0
  24. package/dist/operations/index.js +1751 -0
  25. package/dist/operations/killswitches.d.ts +28 -0
  26. package/dist/operations/killswitches.js +126 -0
  27. package/dist/operations/mcp-adapter.d.ts +35 -0
  28. package/dist/operations/mcp-adapter.js +94 -0
  29. package/dist/operations/metrics.d.ts +30 -0
  30. package/dist/operations/metrics.js +114 -0
  31. package/dist/operations/ops.d.ts +28 -0
  32. package/dist/operations/ops.js +313 -0
  33. package/dist/operations/projects.d.ts +28 -0
  34. package/dist/operations/projects.js +36 -0
  35. package/dist/operations/sdk-docs.d.ts +28 -0
  36. package/dist/operations/sdk-docs.js +160 -0
  37. package/dist/operations/types.d.ts +138 -0
  38. package/dist/operations/types.js +9 -0
  39. package/dist/operations/universes.d.ts +28 -0
  40. package/dist/operations/universes.js +93 -0
  41. package/dist/operations/util.d.ts +33 -0
  42. package/dist/operations/util.js +9 -0
  43. package/dist/resources/alert-rules.d.ts +3 -1
  44. package/dist/resources/alert-rules.js +11 -2
  45. package/dist/resources/attributes.d.ts +19 -0
  46. package/dist/resources/attributes.js +10 -0
  47. package/dist/resources/events.d.ts +61 -0
  48. package/dist/resources/events.js +42 -0
  49. package/dist/resources/i18n.d.ts +84 -0
  50. package/dist/resources/i18n.js +60 -0
  51. package/dist/resources/index.d.ts +22 -4
  52. package/dist/resources/index.js +184 -3
  53. package/dist/resources/metrics.d.ts +54 -0
  54. package/dist/resources/metrics.js +40 -0
  55. package/dist/resources/ops.d.ts +88 -0
  56. package/dist/resources/ops.js +36 -0
  57. package/dist/resources/projects.d.ts +54 -0
  58. package/dist/resources/projects.js +14 -0
  59. package/dist/schemas/alert-rules.d.ts +23 -1
  60. package/dist/schemas/alert-rules.js +13 -3
  61. package/dist/schemas/index.d.ts +1 -1
  62. package/dist/schemas/index.js +12 -2
  63. package/package.json +10 -1
@@ -0,0 +1,138 @@
1
+ import { AdminClient } from '../resources/index.js';
2
+ import 'zod/v4/core';
3
+ import 'zod';
4
+ import '../resources/attributes.js';
5
+ import '../transport.js';
6
+ import '../resources/i18n.js';
7
+ import '../resources/projects.js';
8
+ import '../resources/ops.js';
9
+ import '../resources/events.js';
10
+ import '../resources/metrics.js';
11
+ import '../resources/alert-rules.js';
12
+ import '../schemas/alert-rules.js';
13
+ import '../resources/killswitches.js';
14
+ import '../schemas/killswitches.js';
15
+ import '../schemas/pagination.js';
16
+ import '../resources/universes.js';
17
+ import '../schemas/universes.js';
18
+ import '../resources/configs.js';
19
+ import '../schemas/configs.js';
20
+ import '../resources/experiments.js';
21
+ import '../schemas/experiments.js';
22
+ import '../resources/gates.js';
23
+ import '../schemas/gates.js';
24
+
25
+ /**
26
+ * Operation registry — the single source of truth that drives BOTH
27
+ * `@shipeasy/cli` (commander subcommands) and `@shipeasy/mcp` (MCP tools),
28
+ * plus their generated docs.
29
+ *
30
+ * Today each surface is hand-maintained: the CLI hand-builds commander
31
+ * commands in `packages/cli/src/commands/*`, the MCP server hand-writes tool
32
+ * JSON Schemas in `packages/mcp/src/tools/schema.ts` and a 60-branch dispatch
33
+ * in `server.ts`, and the percent→basis-points + `JSON.parse(rules)` mapping
34
+ * is written *identically* in both. An Operation collapses that into one
35
+ * definition: the params describe the ergonomic facade once, `run` performs
36
+ * the facade→wire mapping once, and the adapters in this folder project it
37
+ * onto each surface.
38
+ */
39
+ /** Param value kinds. `json` arrives as a JSON string on both surfaces and is parsed by the framework. */
40
+ type ParamType = "string" | "number" | "boolean" | "json";
41
+ interface Param {
42
+ /** Field name — also the CLI flag (`--<name>`) or positional, and the MCP property key. */
43
+ name: string;
44
+ type: ParamType;
45
+ /** Shown in CLI `--help`, the MCP tool input schema, and generated docs. */
46
+ description: string;
47
+ required?: boolean;
48
+ /** CLI only: render as a positional `<name>` argument instead of a `--flag`. Ignored by MCP. */
49
+ positional?: boolean;
50
+ /** Restrict to a fixed set (MCP `enum`, CLI validated in coercion). */
51
+ enum?: string[];
52
+ }
53
+ interface OpExample {
54
+ /** Why you'd run this. */
55
+ note?: string;
56
+ /** A copy-pasteable command line. */
57
+ run: string;
58
+ }
59
+ /** Coerced facade input handed to `run` — keys are param names, values coerced by `ParamType`. */
60
+ type OpInput = Record<string, unknown>;
61
+ interface Operation<O = unknown> {
62
+ /**
63
+ * Command-path segments BEFORE the verb, e.g. `["release", "flags"]`.
64
+ * This is the exact CLI nesting; nested resources just add segments
65
+ * (`["release", "experiments", "universes"]`). Drives grouping + naming.
66
+ */
67
+ group: string[];
68
+ /**
69
+ * The verb, e.g. `create`. Authored ONCE — every surface name derives from
70
+ * `group` + `name` so they can never drift:
71
+ * id = `release.flags.create` (opId — `.`-joined)
72
+ * CLI = `release flags create` (opCli — space-joined, the real invocation)
73
+ * MCP = `release_flags_create` (opMcpName — `_`-joined; MCP names can't have spaces)
74
+ */
75
+ name: string;
76
+ /** True for writes — the CLI/MCP adapter enforces `.shipeasy` binding before running. */
77
+ mutates: boolean;
78
+ /** One-liner — CLI `.description()` and the docs heading. */
79
+ summary: string;
80
+ /** Long-form — the MCP tool `description` and the docs body. */
81
+ description: string;
82
+ params: Param[];
83
+ examples?: OpExample[];
84
+ /**
85
+ * The single source of behaviour: coerced facade input → typed AdminClient
86
+ * call → raw data. Returns data (never prints) so every surface can format
87
+ * it its own way (CLI table, MCP JSON envelope, …).
88
+ */
89
+ run: (client: AdminClient, input: OpInput) => Promise<O>;
90
+ }
91
+ /**
92
+ * Structural subset of commander's `Command`. The CLI adapter targets this so
93
+ * `@shipeasy/openapi` never has to depend on `commander` — commander's real
94
+ * `Command` satisfies it.
95
+ */
96
+ interface CommandLike {
97
+ command(nameAndArgs: string): CommandLike;
98
+ description(text: string): CommandLike;
99
+ option(flags: string, description?: string, defaultValue?: unknown): CommandLike;
100
+ action(fn: (...args: unknown[]) => void | Promise<void>): CommandLike;
101
+ }
102
+ /** Everything the CLI surface injects so the adapter stays free of process/chalk/IO. */
103
+ interface CliContext {
104
+ /** Resolve the typed admin client; `mutates` triggers the `requireBinding` guard. */
105
+ getClient: (o: {
106
+ mutates: boolean;
107
+ projectOverride?: string;
108
+ }) => AdminClient;
109
+ /** Print machine output (`--json`). */
110
+ printJson: (data: unknown) => void;
111
+ /** Print human output for a successful op (table / one-liner). */
112
+ printResult: (op: Operation, data: unknown, input: OpInput) => void;
113
+ /** Terminal error handler (prints + exits). */
114
+ onError: (e: unknown) => void;
115
+ }
116
+ type Named = Pick<Operation, "group" | "name">;
117
+ /** Stable id, e.g. `release.flags.create`. */
118
+ declare const opId: (op: Named) => string;
119
+ /** MCP tool name, e.g. `release_flags_create` — the CLI path with `_` for each space. */
120
+ declare const opMcpName: (op: Named) => string;
121
+ /** CLI invocation, e.g. `release flags create` (full command path). */
122
+ declare const opCli: (op: Named) => string;
123
+ /** Plain MCP `Tool` shape — structurally compatible with `@modelcontextprotocol/sdk`'s `Tool`. */
124
+ interface McpTool {
125
+ name: string;
126
+ description: string;
127
+ inputSchema: {
128
+ type: "object";
129
+ required?: string[];
130
+ properties: Record<string, {
131
+ type: string;
132
+ description?: string;
133
+ enum?: string[];
134
+ }>;
135
+ };
136
+ }
137
+
138
+ export { type CliContext, type CommandLike, type McpTool, type OpExample, type OpInput, type Operation, type Param, type ParamType, opCli, opId, opMcpName };
@@ -0,0 +1,9 @@
1
+ // src/operations/types.ts
2
+ var opId = (op) => [...op.group, op.name].join(".");
3
+ var opMcpName = (op) => [...op.group, op.name].join("_");
4
+ var opCli = (op) => [...op.group, op.name].join(" ");
5
+ export {
6
+ opCli,
7
+ opId,
8
+ opMcpName
9
+ };
@@ -0,0 +1,28 @@
1
+ import { Operation } from './types.js';
2
+ import '../resources/index.js';
3
+ import 'zod/v4/core';
4
+ import 'zod';
5
+ import '../resources/attributes.js';
6
+ import '../transport.js';
7
+ import '../resources/i18n.js';
8
+ import '../resources/projects.js';
9
+ import '../resources/ops.js';
10
+ import '../resources/events.js';
11
+ import '../resources/metrics.js';
12
+ import '../resources/alert-rules.js';
13
+ import '../schemas/alert-rules.js';
14
+ import '../resources/killswitches.js';
15
+ import '../schemas/killswitches.js';
16
+ import '../schemas/pagination.js';
17
+ import '../resources/universes.js';
18
+ import '../schemas/universes.js';
19
+ import '../resources/configs.js';
20
+ import '../schemas/configs.js';
21
+ import '../resources/experiments.js';
22
+ import '../schemas/experiments.js';
23
+ import '../resources/gates.js';
24
+ import '../schemas/gates.js';
25
+
26
+ declare const universeOperations: Operation[];
27
+
28
+ export { universeOperations };
@@ -0,0 +1,93 @@
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;
9
+ };
10
+
11
+ // src/operations/util.ts
12
+ var str = (i, k) => i[k] === void 0 ? void 0 : i[k];
13
+
14
+ // src/operations/universes.ts
15
+ var GROUP = ["release", "experiments", "universes"];
16
+ function parseHoldout(raw) {
17
+ if (raw === void 0 || raw === "" || raw === "null") return null;
18
+ const [lo, hi] = raw.split(",").map((s) => Number(s.trim()));
19
+ if (!Number.isFinite(lo) || !Number.isFinite(hi)) {
20
+ throw new ApiError("holdout must be 'lo,hi' integers in [0,9999]", 400);
21
+ }
22
+ return [lo, hi];
23
+ }
24
+ var universeOperations = [
25
+ {
26
+ group: GROUP,
27
+ name: "list",
28
+ mutates: false,
29
+ summary: "List universes",
30
+ description: "Return every experiment universe (allocation %, holdout %).",
31
+ params: [],
32
+ examples: [{ run: "shipeasy release experiments universes list" }],
33
+ run: (client) => client.universes.listAll()
34
+ },
35
+ {
36
+ group: GROUP,
37
+ name: "create",
38
+ mutates: true,
39
+ summary: "Create a universe",
40
+ description: "Create an experiment universe (container + optional holdout).",
41
+ params: [
42
+ { name: "name", type: "string", description: "Universe name.", required: true, positional: true },
43
+ { name: "unitType", type: "string", description: "Unit type (e.g. user_id, account_id). Defaults to user_id." },
44
+ { name: "holdout", type: "string", description: "Holdout range as 'lo,hi' (0\u20139999); omit for no holdout." }
45
+ ],
46
+ examples: [
47
+ { note: "Default user_id unit, no holdout", run: "shipeasy release experiments universes create web" },
48
+ { note: "Holdout 0\u2013999, keyed on account_id", run: "shipeasy release experiments universes create web --unit-type account_id --holdout 0,999" }
49
+ ],
50
+ run: (client, i) => client.universes.create({
51
+ name: i.name,
52
+ unit_type: str(i, "unitType") ?? "user_id",
53
+ holdout_range: parseHoldout(str(i, "holdout"))
54
+ })
55
+ },
56
+ {
57
+ group: GROUP,
58
+ name: "update",
59
+ mutates: true,
60
+ summary: "Update a universe's holdout range",
61
+ description: "Update the holdout range. Pass 'null' to clear the holdout.",
62
+ params: [
63
+ { name: "name", type: "string", description: "Universe name.", required: true, positional: true },
64
+ { name: "holdout", type: "string", description: "Holdout range as 'lo,hi' (0\u20139999), or 'null' to clear." }
65
+ ],
66
+ examples: [
67
+ { note: "Set a 5% holdout", run: "shipeasy release experiments universes update web --holdout 0,499" },
68
+ { note: "Clear the holdout", run: "shipeasy release experiments universes update web --holdout null" }
69
+ ],
70
+ run: async (client, i) => {
71
+ const x = await client.universes.resolve(i.name);
72
+ const patch = i.holdout === void 0 ? {} : { holdout_range: parseHoldout(str(i, "holdout")) };
73
+ return client.universes.update(x.id, patch);
74
+ }
75
+ },
76
+ {
77
+ group: GROUP,
78
+ name: "archive",
79
+ mutates: true,
80
+ summary: "Archive a universe",
81
+ description: "Archive (soft-delete) a universe by name.",
82
+ params: [{ name: "name", type: "string", description: "Universe name.", required: true, positional: true }],
83
+ examples: [{ run: "shipeasy release experiments universes archive web" }],
84
+ run: async (client, i) => {
85
+ const x = await client.universes.resolve(i.name);
86
+ await client.universes.delete(x.id);
87
+ return { ok: true, archived: i.name };
88
+ }
89
+ }
90
+ ];
91
+ export {
92
+ universeOperations
93
+ };
@@ -0,0 +1,33 @@
1
+ import { OpInput } from './types.js';
2
+ import '../resources/index.js';
3
+ import 'zod/v4/core';
4
+ import 'zod';
5
+ import '../resources/attributes.js';
6
+ import '../transport.js';
7
+ import '../resources/i18n.js';
8
+ import '../resources/projects.js';
9
+ import '../resources/ops.js';
10
+ import '../resources/events.js';
11
+ import '../resources/metrics.js';
12
+ import '../resources/alert-rules.js';
13
+ import '../schemas/alert-rules.js';
14
+ import '../resources/killswitches.js';
15
+ import '../schemas/killswitches.js';
16
+ import '../schemas/pagination.js';
17
+ import '../resources/universes.js';
18
+ import '../schemas/universes.js';
19
+ import '../resources/configs.js';
20
+ import '../schemas/configs.js';
21
+ import '../resources/experiments.js';
22
+ import '../schemas/experiments.js';
23
+ import '../resources/gates.js';
24
+ import '../schemas/gates.js';
25
+
26
+ /** Read a coerced number param (already `Number()`-ed by the framework), or undefined. */
27
+ declare const num: (i: OpInput, k: string) => number | undefined;
28
+ /** Read a string param, or undefined. */
29
+ declare const str: (i: OpInput, k: string) => string | undefined;
30
+ /** Read a boolean param, or undefined. */
31
+ declare const bool: (i: OpInput, k: string) => boolean | undefined;
32
+
33
+ export { bool, num, str };
@@ -0,0 +1,9 @@
1
+ // src/operations/util.ts
2
+ var num = (i, k) => i[k] === void 0 ? void 0 : i[k];
3
+ var str = (i, k) => i[k] === void 0 ? void 0 : i[k];
4
+ var bool = (i, k) => i[k] === void 0 ? void 0 : i[k];
5
+ export {
6
+ bool,
7
+ num,
8
+ str
9
+ };
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { alertRuleCreateSchema, alertRuleUpdateSchema } from '../schemas/alert-rules.js';
2
+ import { NotificationTarget, alertRuleCreateSchema, alertRuleUpdateSchema } from '../schemas/alert-rules.js';
3
3
  import { Transport } from '../transport.js';
4
4
 
5
5
  type AlertRuleCreateInput = z.input<typeof alertRuleCreateSchema>;
@@ -24,6 +24,8 @@ interface AlertRule {
24
24
  windowHours: number;
25
25
  severity: "danger" | "warn" | "info";
26
26
  enabled: boolean;
27
+ /** Delivery target for this rule's alert; null = use project default. */
28
+ notify: NotificationTarget | null;
27
29
  createdAt: string;
28
30
  updatedAt: string;
29
31
  }
@@ -2,6 +2,13 @@
2
2
  import { z } from "zod";
3
3
  var ALERT_COMPARATORS = ["gt", "gte", "lt", "lte"];
4
4
  var ALERT_SEVERITIES = ["danger", "warn", "info"];
5
+ var notificationTargetSchema = z.object({
6
+ slackChannel: z.object({
7
+ id: z.string().min(1).describe("Slack channel id (e.g. C0123ABCD)."),
8
+ name: z.string().min(1).describe("Slack channel name (without the leading #).")
9
+ }).nullish().describe("Slack channel to post this rule's alert to. Requires a Slack connector."),
10
+ email: z.string().email().nullish().describe("Email address to notify for this rule (overrides the default recipient).")
11
+ }).strict();
5
12
  var alertRuleCreateSchema = z.object({
6
13
  name: z.string().min(1).max(120).describe("Human label for the rule, shown on the alert and the rules list."),
7
14
  metricId: z.string().min(1).describe("Id of the metric to evaluate."),
@@ -9,7 +16,8 @@ var alertRuleCreateSchema = z.object({
9
16
  threshold: z.number().finite().describe("Threshold the metric value is compared against."),
10
17
  windowHours: z.number().int().min(1).max(720).default(24).describe("Lookback window (hours) the metric is aggregated over. 1\u2013720."),
11
18
  severity: z.enum(ALERT_SEVERITIES).default("warn").describe("Severity of the raised alert."),
12
- enabled: z.boolean().default(true).describe("Whether the rule is evaluated by the cron.")
19
+ enabled: z.boolean().default(true).describe("Whether the rule is evaluated by the cron."),
20
+ notify: notificationTargetSchema.nullish().describe("Where to deliver this rule's alert (Slack channel and/or email target).")
13
21
  }).strict();
14
22
  var alertRuleUpdateSchema = z.object({
15
23
  name: z.string().min(1).max(120),
@@ -17,7 +25,8 @@ var alertRuleUpdateSchema = z.object({
17
25
  threshold: z.number().finite(),
18
26
  windowHours: z.number().int().min(1).max(720),
19
27
  severity: z.enum(ALERT_SEVERITIES),
20
- enabled: z.boolean()
28
+ enabled: z.boolean(),
29
+ notify: notificationTargetSchema.nullish()
21
30
  }).partial().strict();
22
31
 
23
32
  // src/transport.ts
@@ -0,0 +1,19 @@
1
+ import { Transport } from '../transport.js';
2
+
3
+ /**
4
+ * Targeting attributes — the auto-inferred user-attribute schema the dashboard
5
+ * surfaces for gate/experiment targeting. Read-only; this exists so the typed
6
+ * `attributes list` op can replace the `attributes` kind of the old MCP-only
7
+ * "generic read" (`list_resources`), which is being deleted (see doc 21 §A4.3).
8
+ */
9
+ interface Attribute {
10
+ name: string;
11
+ type?: string;
12
+ [key: string]: unknown;
13
+ }
14
+ interface AttributesClient {
15
+ list(): Promise<Attribute[]>;
16
+ }
17
+ declare function attributesClient(t: Transport): AttributesClient;
18
+
19
+ export { type Attribute, type AttributesClient, attributesClient };
@@ -0,0 +1,10 @@
1
+ // src/resources/attributes.ts
2
+ var BASE = "/api/admin/attributes";
3
+ function attributesClient(t) {
4
+ return {
5
+ list: () => t.request("GET", BASE)
6
+ };
7
+ }
8
+ export {
9
+ attributesClient
10
+ };
@@ -0,0 +1,61 @@
1
+ import { Transport } from '../transport.js';
2
+
3
+ /**
4
+ * Event catalog — the registry of event names (and their typed properties) that
5
+ * metric queries reference. `/collect` auto-discovers unknown names as
6
+ * `pending` rows; `approve` promotes them to usable. Property `name:type`
7
+ * parsing is a CLI/MCP facade concern; the resource takes the already-parsed
8
+ * `properties` array.
9
+ */
10
+ interface EventProperty {
11
+ name: string;
12
+ type: "string" | "number" | "boolean";
13
+ required: boolean;
14
+ description?: string;
15
+ }
16
+ interface CatalogEvent {
17
+ id: string;
18
+ name: string;
19
+ folder: string | null;
20
+ description: string | null;
21
+ properties: EventProperty[];
22
+ pending: number;
23
+ createdAt: string;
24
+ [key: string]: unknown;
25
+ }
26
+ interface EventCreateInput {
27
+ name: string;
28
+ folder?: string | null;
29
+ description?: string;
30
+ properties?: EventProperty[];
31
+ }
32
+ interface EventUpdateInput {
33
+ folder?: string | null;
34
+ description?: string;
35
+ /** Replaces the full property set (no merge). */
36
+ properties?: EventProperty[];
37
+ }
38
+ interface EventsClient {
39
+ list(): Promise<CatalogEvent[]>;
40
+ get(id: string): Promise<CatalogEvent>;
41
+ create(input: EventCreateInput): Promise<{
42
+ id: string;
43
+ name: string;
44
+ }>;
45
+ update(id: string, input: EventUpdateInput): Promise<{
46
+ id: string;
47
+ }>;
48
+ /** Promote a pending (auto-discovered) event so metrics can query it. */
49
+ approve(id: string, input?: EventUpdateInput): Promise<{
50
+ id: string;
51
+ }>;
52
+ /** Soft-delete (the user-facing verb is `archive`). */
53
+ delete(id: string): Promise<{
54
+ ok: true;
55
+ }>;
56
+ /** Resolve by exact id, unique id-prefix, or exact (unique) name. */
57
+ resolve(idOrName: string): Promise<CatalogEvent>;
58
+ }
59
+ declare function eventsClient(t: Transport): EventsClient;
60
+
61
+ export { type CatalogEvent, type EventCreateInput, type EventProperty, type EventUpdateInput, type EventsClient, eventsClient };
@@ -0,0 +1,42 @@
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;
9
+ };
10
+
11
+ // src/resources/events.ts
12
+ var BASE = "/api/admin/events";
13
+ function eventsClient(t) {
14
+ async function list() {
15
+ return t.request("GET", BASE);
16
+ }
17
+ async function resolve(idOrName) {
18
+ const all = await list();
19
+ const byId = all.find((e) => e.id === idOrName);
20
+ if (byId) return byId;
21
+ const byPrefix = all.filter((e) => e.id.startsWith(idOrName));
22
+ if (byPrefix.length === 1) return byPrefix[0];
23
+ if (byPrefix.length > 1) throw new ApiError(`Event id prefix '${idOrName}' is ambiguous`, 400);
24
+ const byName = all.filter((e) => e.name === idOrName);
25
+ if (byName.length === 1) return byName[0];
26
+ if (byName.length > 1)
27
+ throw new ApiError(`Event name '${idOrName}' is ambiguous \u2014 pass an id`, 400);
28
+ throw new ApiError(`Event '${idOrName}' not found`, 404);
29
+ }
30
+ return {
31
+ list,
32
+ resolve,
33
+ get: (id) => t.request("GET", `${BASE}/${id}`),
34
+ create: (input) => t.request("POST", BASE, input),
35
+ update: (id, input) => t.request("PATCH", `${BASE}/${id}`, input),
36
+ approve: (id, input = {}) => t.request("POST", `${BASE}/${id}/approve`, input),
37
+ delete: (id) => t.request("DELETE", `${BASE}/${id}`)
38
+ };
39
+ }
40
+ export {
41
+ eventsClient
42
+ };
@@ -0,0 +1,84 @@
1
+ import { Transport } from '../transport.js';
2
+
3
+ /**
4
+ * i18n (String Manager) — the API-only parts that are worker-safe and shared
5
+ * between CLI and MCP: locale profiles, the insert-only key push, single-key
6
+ * update, chunk publish, and the read-only key/draft listings.
7
+ *
8
+ * The fs/AST parts (`scan`, `validate`, `install-loader`, `codemod`) stay
9
+ * hand-written in the fs-having consumers and are NEVER added here — the
10
+ * registry is bundled into a Worker that can't load `node:fs` or the TS AST.
11
+ */
12
+ interface I18nProfile {
13
+ id: string;
14
+ name: string;
15
+ locales?: string[];
16
+ default_locale?: string;
17
+ }
18
+ interface I18nKey {
19
+ id: string;
20
+ key: string;
21
+ value?: string;
22
+ [key: string]: unknown;
23
+ }
24
+ interface I18nDraft {
25
+ id: string;
26
+ [key: string]: unknown;
27
+ }
28
+ interface I18nPushResult {
29
+ added?: string[];
30
+ skipped?: string[];
31
+ pushed_count?: number;
32
+ skipped_count?: number;
33
+ }
34
+ interface I18nClient {
35
+ listProfiles(): Promise<I18nProfile[]>;
36
+ createProfile(input: {
37
+ name: string;
38
+ locales?: string[];
39
+ default_locale?: string;
40
+ }): Promise<{
41
+ id: string;
42
+ name: string;
43
+ }>;
44
+ /** Resolve a profile by exact name (the user-facing handle). */
45
+ resolveProfile(name: string): Promise<I18nProfile>;
46
+ /** Insert-only key push (existing keys are left untouched). */
47
+ pushKeys(input: {
48
+ profile_id: string;
49
+ chunk?: string;
50
+ keys: {
51
+ key: string;
52
+ value: string;
53
+ }[];
54
+ }): Promise<I18nPushResult>;
55
+ /** List keys for a profile (optionally a name prefix). */
56
+ listKeys(query?: {
57
+ profile_id?: string;
58
+ prefix?: string;
59
+ limit?: number;
60
+ }): Promise<I18nKey[]>;
61
+ /** Update one existing key's value (the only overwrite path). */
62
+ updateKey(id: string, input: {
63
+ value: string;
64
+ description?: string;
65
+ }): Promise<{
66
+ id: string;
67
+ }>;
68
+ /** Resolve a key by exact name within a profile, then PUT its new value. */
69
+ updateKeyByName(profileId: string, key: string, input: {
70
+ value: string;
71
+ description?: string;
72
+ }): Promise<{
73
+ id: string;
74
+ }>;
75
+ /** List staged drafts (the read-only `drafts` kind from the old generic read). */
76
+ listDrafts(): Promise<I18nDraft[]>;
77
+ /** Publish a profile chunk (rebuild KV manifest + purge CDN). */
78
+ publish(profileId: string, input: {
79
+ chunk?: string;
80
+ }): Promise<unknown>;
81
+ }
82
+ declare function i18nClient(t: Transport): I18nClient;
83
+
84
+ export { type I18nClient, type I18nDraft, type I18nKey, type I18nProfile, type I18nPushResult, i18nClient };
@@ -0,0 +1,60 @@
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;
9
+ };
10
+
11
+ // src/resources/i18n.ts
12
+ var PROFILES = "/api/admin/i18n/profiles";
13
+ var KEYS = "/api/admin/i18n/keys";
14
+ var DRAFTS = "/api/admin/i18n/drafts";
15
+ function i18nClient(t) {
16
+ async function listProfiles() {
17
+ return t.request("GET", PROFILES);
18
+ }
19
+ async function resolveProfile(name) {
20
+ const all = await listProfiles();
21
+ const found = all.find((p) => p.name === name);
22
+ if (!found)
23
+ throw new ApiError(
24
+ `Profile '${name}' not found. Existing: ${all.map((p) => p.name).join(", ") || "(none)"}`,
25
+ 404
26
+ );
27
+ return found;
28
+ }
29
+ async function listKeys(query = {}) {
30
+ const q = {};
31
+ if (query.profile_id) q.profile_id = query.profile_id;
32
+ if (query.prefix) q.prefix = query.prefix;
33
+ if (query.limit !== void 0) q.limit = String(query.limit);
34
+ const res = await t.request("GET", KEYS, void 0, q);
35
+ return Array.isArray(res) ? res : res.keys;
36
+ }
37
+ function updateKey(id, input) {
38
+ return t.request("PUT", `${KEYS}/${id}`, input);
39
+ }
40
+ return {
41
+ listProfiles,
42
+ resolveProfile,
43
+ listKeys,
44
+ updateKey,
45
+ createProfile: (input) => t.request("POST", PROFILES, input),
46
+ pushKeys: (input) => t.request("POST", KEYS, input),
47
+ updateKeyByName: async (profileId, key, input) => {
48
+ const rows = await listKeys({ profile_id: profileId, prefix: key, limit: 500 });
49
+ const match = rows.find((k) => k.key === key);
50
+ if (!match)
51
+ throw new ApiError(`Key '${key}' not found in profile. Add it first with i18n push.`, 404);
52
+ return updateKey(match.id, input);
53
+ },
54
+ listDrafts: () => t.request("GET", DRAFTS),
55
+ publish: (profileId, input) => t.request("POST", `${PROFILES}/${profileId}/publish`, { chunk: input.chunk ?? "default" })
56
+ };
57
+ }
58
+ export {
59
+ i18nClient
60
+ };