@akanjs/devkit 2.3.11-rc.3 → 2.3.11-rc.5

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.
@@ -11,6 +11,7 @@ const BACKEND_RESTART_DEBOUNCE_MS = 120;
11
11
  const BACKEND_GRACEFUL_TIMEOUT_MS = 3000;
12
12
  const BACKEND_RECOVERY_BASE_DELAY_MS = 1_000;
13
13
  const BACKEND_RECOVERY_MAX_DELAY_MS = 30_000;
14
+ const BACKEND_STDERR_TAIL_LIMIT = 40;
14
15
  const BUILDER_READY_TIMEOUT_MS = 150000;
15
16
  const BUILDER_START_MAX_ATTEMPTS = 3;
16
17
  const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
@@ -270,6 +271,7 @@ export class AkanAppHost {
270
271
  #pendingRestartReason: BackendRestartReason | null = null;
271
272
  #backendStartStatus: { generation?: number; files: string[] } | null = null;
272
273
  #backendBuildStatusGeneration = 0;
274
+ #backendStderrTail: string[] = [];
273
275
  #lastGoodFrontend: LastGoodFrontendState = {};
274
276
  #buildStatusByPhase = new Map<BuildPhase, DevBuildStatus>();
275
277
  #pendingBuildStatusReplay: DevBuildStatus[] = [];
@@ -321,6 +323,7 @@ export class AkanAppHost {
321
323
  this.#backendStartStatus = startStatus;
322
324
  this.#setBackendLifecycleState("starting");
323
325
  this.#backendReady = false;
326
+ this.#backendStderrTail = [];
324
327
  const backend = Bun.spawn(["bun", `apps/${this.app.name}/main.ts`], {
325
328
  cwd: this.app.workspace.workspaceRoot,
326
329
  stdio: this.withInk ? ["ignore", "pipe", "pipe"] : ["inherit", "inherit", "inherit"],
@@ -351,6 +354,38 @@ export class AkanAppHost {
351
354
  });
352
355
  this.#backend = backend;
353
356
  this.logger.verbose(`backend spawned pid=${backend.pid}`);
357
+ if (this.withInk) {
358
+ // Ink mode pipes backend stdio to keep the TUI clean; drain the pipes and surface
359
+ // them through the logger so runtime errors are not silently swallowed.
360
+ void this.#forwardBackendStream(backend.stderr as unknown as ReadableStream<Uint8Array> | undefined, "stderr");
361
+ void this.#forwardBackendStream(backend.stdout as unknown as ReadableStream<Uint8Array> | undefined, "stdout");
362
+ }
363
+ }
364
+ #recordBackendStderr(chunk: string) {
365
+ const lines = chunk.split(/\r?\n/).filter((line) => line.length > 0);
366
+ if (lines.length === 0) return;
367
+ this.#backendStderrTail.push(...lines);
368
+ if (this.#backendStderrTail.length > BACKEND_STDERR_TAIL_LIMIT) {
369
+ this.#backendStderrTail.splice(0, this.#backendStderrTail.length - BACKEND_STDERR_TAIL_LIMIT);
370
+ }
371
+ }
372
+ async #forwardBackendStream(stream: ReadableStream<Uint8Array> | undefined | null, kind: "stdout" | "stderr") {
373
+ if (!stream) return;
374
+ const decoder = new TextDecoder();
375
+ try {
376
+ for await (const chunk of stream) {
377
+ const text = decoder.decode(chunk, { stream: true });
378
+ if (!text.trim()) continue;
379
+ if (kind === "stderr") {
380
+ this.#recordBackendStderr(text);
381
+ this.logger.warn(`[backend] ${text.trimEnd()}`);
382
+ } else {
383
+ this.logger.verbose(`[backend] ${text.trimEnd()}`);
384
+ }
385
+ }
386
+ } catch {
387
+ // The stream closes when the backend exits; nothing further to surface here.
388
+ }
354
389
  }
355
390
  #nextBackendBuildStatusGeneration(generation?: number): number {
356
391
  if (typeof generation === "number") {
@@ -482,6 +517,9 @@ export class AkanAppHost {
482
517
  this.logger.warn(
483
518
  `[backend-recovery] backend exited unexpectedly (${reason}); restarting in ${delay}ms (attempt ${this.#backendRecoveryAttempts})`,
484
519
  );
520
+ if (this.#backendStderrTail.length > 0) {
521
+ this.logger.warn(`[backend-recovery] recent backend stderr:\n${this.#backendStderrTail.join("\n")}`);
522
+ }
485
523
  this.#backendRecoveryTimer = setTimeout(() => {
486
524
  this.#backendRecoveryTimer = null;
487
525
  if (this.#backend) return;
@@ -32,9 +32,14 @@ export async function resolveSignalTestPreloadPath(target: SignalTestPreloadTarg
32
32
  path.resolve(import.meta.dir, "../../akanjs", SIGNAL_TEST_PRELOAD_PATH),
33
33
  );
34
34
 
35
- for (const candidate of [...new Set(candidates)]) {
35
+ const uniqueCandidates = [...new Set(candidates)];
36
+ for (const candidate of uniqueCandidates) {
36
37
  if (await Bun.file(candidate).exists()) return candidate;
37
38
  }
38
39
 
39
- throw new Error(`Failed to locate ${SIGNAL_TEST_PRELOAD_PATH} from ${target.cwdPath}`);
40
+ throw new Error(
41
+ `Failed to locate ${SIGNAL_TEST_PRELOAD_PATH} from ${target.cwdPath}.\nProbed paths:\n${uniqueCandidates
42
+ .map((candidate) => ` - ${candidate}`)
43
+ .join("\n")}`,
44
+ );
40
45
  }
package/executors.test.ts CHANGED
@@ -140,7 +140,7 @@ describe("Executor filesystem helpers", () => {
140
140
  expect(await readFile(path.join(root, "workspace/.env"), "utf8")).toContain("AKAN_PUBLIC_REPO_NAME");
141
141
  expect(await readFile(path.join(root, "workspace/.vscode/settings.json"), "utf8")).toContain("typescript.tsdk");
142
142
  expect(await readFile(path.join(root, "workspace/.cursor/rules/akan.mdc"), "utf8")).toContain(
143
- "Akan.js Workspace Rules",
143
+ "Akan workspace agent guide",
144
144
  );
145
145
  expect(await readFile(path.join(root, "workspace/AGENTS.md"), "utf8")).toContain("sample Agent Guide");
146
146
  expect(await readFile(path.join(root, "workspace/docs/AI-DEVELOPMENT.md"), "utf8")).toContain(
@@ -21,5 +21,15 @@ or {
21
21
  span = $source,
22
22
  message = "Module files should not import from two or more parent directories."
23
23
  )
24
+ },
25
+ JsModuleSource() as $source where {
26
+ $source <: within JsImport(),
27
+ not $filename <: r".*\.(?:test|spec)\.tsx?",
28
+ $filename <: r".*/lib/.+\.tsx",
29
+ $source <: r"\"\.\./.*\"",
30
+ register_diagnostic(
31
+ span = $source,
32
+ message = "Module UI (.tsx) files under lib must not use internal relative imports like ../cnst. Import from the package client/server entrypoint instead."
33
+ )
24
34
  }
25
35
  }
@@ -0,0 +1,26 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ // Every model already gets auto-generated CRUD endpoints in its slice fetch
5
+ // contract (see pkgs/akanjs/fetch/fetchType/sliceFetch.type.ts):
6
+ // <refName>, light<Model>, create<Model>, update<Model>, remove<Model>.
7
+ // Re-declaring one of them inside the `endpoint(...)` block of a
8
+ // `<model>.signal.ts` file shadows the framework contract and can pass
9
+ // sync/typecheck/build while failing at runtime, so flag it here.
10
+ `$key: $value` as $prop where {
11
+ $prop <: within `class $className extends $base {}` where {
12
+ $base <: contains `endpoint`,
13
+ $className <: r"([A-Z][a-zA-Z0-9]*)Endpoint"($Cap)
14
+ },
15
+ $filename <: r".*/([a-z][a-zA-Z0-9]*)\.signal\.ts"($model),
16
+ or {
17
+ $key <: $model,
18
+ $key <: r"(?:light|create|update|remove)([A-Z][a-zA-Z0-9]*)"($keyCap) where {
19
+ $keyCap <: $Cap
20
+ }
21
+ },
22
+ register_diagnostic(
23
+ span = $key,
24
+ message = "This endpoint name collides with an auto-generated CRUD endpoint (<model>, light/create/update/remove<Model>). Rename it or move the logic into the service; do not re-declare predefined endpoints."
25
+ )
26
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "2.3.11-rc.3",
3
+ "version": "2.3.11-rc.5",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -32,7 +32,7 @@
32
32
  "@langchain/openai": "^1.4.6",
33
33
  "@tailwindcss/node": "^4.3.0",
34
34
  "@trapezedev/project": "^7.1.4",
35
- "akanjs": "2.3.11-rc.3",
35
+ "akanjs": "2.3.11-rc.5",
36
36
  "chalk": "^5.6.2",
37
37
  "commander": "^14.0.3",
38
38
  "daisyui": "5.5.23",
package/scanInfo.ts CHANGED
@@ -52,6 +52,7 @@ const appRootAllowedFiles = new Set([
52
52
  "package.json",
53
53
  "server.ts",
54
54
  "tsconfig.json",
55
+ "tsconfig.tsbuildinfo",
55
56
  ]);
56
57
  const appRootAllowedDirs = new Set([
57
58
  ".akan",
@@ -403,6 +403,8 @@ export const createWorkflowStepRegistry = ({
403
403
  createUi,
404
404
  addField,
405
405
  addEnumField,
406
+ addMutation,
407
+ addSlice,
406
408
  }: WorkflowPrimitiveOperations): WorkflowStepRegistry => {
407
409
  const inspect = async () => undefined;
408
410
  const commandOnly = async () => undefined;
@@ -483,6 +485,24 @@ export const createWorkflowStepRegistry = ({
483
485
  ),
484
486
  [workflowStepKey("add-enum-field", "update-dictionary")]: inspect,
485
487
  [workflowStepKey("add-enum-field", "update-option")]: inspect,
488
+ [workflowStepKey("add-mutation", "update-service")]: async (_step, plan) =>
489
+ primitiveReportToWorkflowStepResult(
490
+ await addMutation({
491
+ app: workflowStringInput(plan.inputs.app),
492
+ module: workflowStringInput(plan.inputs.module),
493
+ mutation: workflowStringInput(plan.inputs.mutation),
494
+ }),
495
+ ),
496
+ [workflowStepKey("add-mutation", "update-signal")]: inspect,
497
+ [workflowStepKey("add-slice", "update-service-query")]: async (_step, plan) =>
498
+ primitiveReportToWorkflowStepResult(
499
+ await addSlice({
500
+ app: workflowStringInput(plan.inputs.app),
501
+ module: workflowStringInput(plan.inputs.module),
502
+ slice: workflowStringInput(plan.inputs.slice),
503
+ }),
504
+ ),
505
+ [workflowStepKey("add-slice", "update-signal-slice")]: inspect,
486
506
  };
487
507
  };
488
508
 
@@ -1,5 +1,12 @@
1
1
  import { describe, expect, test } from "bun:test";
2
- import { inspectDictionaryStructure } from "./source";
2
+ import {
3
+ hasClassMethod,
4
+ hasSignalFactoryEntry,
5
+ hasSourceParseErrors,
6
+ insertClassMethod,
7
+ insertSignalFactoryEntry,
8
+ inspectDictionaryStructure,
9
+ } from "./source";
3
10
 
4
11
  describe("inspectDictionaryStructure", () => {
5
12
  test("preserves protected dictionary chain order around the model object", () => {
@@ -60,3 +67,109 @@ export const dictionary = modelDictionary(["en", "ko"])
60
67
  expect(structure.chainMethods).toEqual(["modelDictionary", "slice", "model", "translate", "error"]);
61
68
  });
62
69
  });
70
+
71
+ describe("insertClassMethod", () => {
72
+ test("inserts a method into an empty service class body", () => {
73
+ const content = `import { serve } from "akanjs/service";
74
+ import * as db from "../db";
75
+ export class BannerService extends serve(db.banner, () => ({})) {}
76
+ `;
77
+ const next = insertClassMethod(content, "BannerService", " async archive() {\n return true;\n }");
78
+ expect(next).not.toBeNull();
79
+ expect(hasSourceParseErrors(next as string, "service.ts")).toBe(false);
80
+ expect(hasClassMethod(next as string, "BannerService", "archive")).toBe(true);
81
+ });
82
+
83
+ test("inserts a method into a populated service class body", () => {
84
+ const content = `import { serve } from "akanjs/service";
85
+ import * as db from "../db";
86
+ export class NotificationService extends serve(db.notification, () => ({})) {
87
+ async subscribe(token: string) {
88
+ return token;
89
+ }
90
+ }
91
+ `;
92
+ const next = insertClassMethod(content, "NotificationService", " async archive() {\n return true;\n }");
93
+ expect(next).not.toBeNull();
94
+ expect(hasSourceParseErrors(next as string, "service.ts")).toBe(false);
95
+ expect(hasClassMethod(next as string, "NotificationService", "archive")).toBe(true);
96
+ expect(hasClassMethod(next as string, "NotificationService", "subscribe")).toBe(true);
97
+ });
98
+
99
+ test("returns null when the class is not found", () => {
100
+ expect(insertClassMethod("export class Other {}", "MissingService", " x() {}")).toBeNull();
101
+ });
102
+ });
103
+
104
+ describe("insertSignalFactoryEntry", () => {
105
+ const mutationEntry =
106
+ "archive: mutation(Boolean)\n .exec(async function () {\n return await this.bannerService.archive();\n }),";
107
+
108
+ test("adds mutation param when the endpoint factory has no params", () => {
109
+ const content = `import { endpoint } from "akanjs/signal";
110
+ import * as srv from "../srv";
111
+ export class BannerEndpoint extends endpoint(srv.banner, () => ({})) {}
112
+ `;
113
+ const next = insertSignalFactoryEntry(content, "BannerEndpoint", "archive", mutationEntry, {
114
+ mode: "destructure",
115
+ name: "mutation",
116
+ });
117
+ expect(next).not.toBeNull();
118
+ expect(hasSourceParseErrors(next as string, "signal.ts")).toBe(false);
119
+ expect(hasSignalFactoryEntry(next as string, "BannerEndpoint", "archive")).toBe(true);
120
+ expect(next as string).toContain("{ mutation }");
121
+ });
122
+
123
+ test("extends an existing destructured param without dropping siblings", () => {
124
+ const content = `import { endpoint } from "akanjs/signal";
125
+ import * as srv from "../srv";
126
+ export class BannerEndpoint extends endpoint(srv.banner, ({ pubsub, query }) => ({
127
+ existing: query(String).exec(async function () {
128
+ return "x";
129
+ }),
130
+ })) {}
131
+ `;
132
+ const next = insertSignalFactoryEntry(content, "BannerEndpoint", "archive", mutationEntry, {
133
+ mode: "destructure",
134
+ name: "mutation",
135
+ });
136
+ expect(next).not.toBeNull();
137
+ expect(hasSourceParseErrors(next as string, "signal.ts")).toBe(false);
138
+ expect(next as string).toContain("{ pubsub, query, mutation }");
139
+ expect(hasSignalFactoryEntry(next as string, "BannerEndpoint", "archive")).toBe(true);
140
+ expect(hasSignalFactoryEntry(next as string, "BannerEndpoint", "existing")).toBe(true);
141
+ });
142
+
143
+ test("adds the init param to a slice factory and inserts the slice entry", () => {
144
+ const content = `import { slice, Admin, Public } from "akanjs/signal";
145
+ import * as srv from "../srv";
146
+ export class BannerSlice extends slice(srv.banner, { guards: { root: Admin, get: Public, cru: Admin } }, () => ({})) {}
147
+ `;
148
+ const sliceEntry =
149
+ "inPublic: init()\n .exec(function () {\n return this.bannerService.queryInPublic();\n }),";
150
+ const next = insertSignalFactoryEntry(content, "BannerSlice", "inPublic", sliceEntry, {
151
+ mode: "positional",
152
+ name: "init",
153
+ });
154
+ expect(next).not.toBeNull();
155
+ expect(hasSourceParseErrors(next as string, "signal.ts")).toBe(false);
156
+ expect(next as string).toContain("(init) =>");
157
+ expect(hasSignalFactoryEntry(next as string, "BannerSlice", "inPublic")).toBe(true);
158
+ });
159
+
160
+ test("is idempotent when the entry already exists", () => {
161
+ const content = `import { endpoint } from "akanjs/signal";
162
+ import * as srv from "../srv";
163
+ export class BannerEndpoint extends endpoint(srv.banner, ({ mutation }) => ({
164
+ archive: mutation(Boolean).exec(async function () {
165
+ return true;
166
+ }),
167
+ })) {}
168
+ `;
169
+ const next = insertSignalFactoryEntry(content, "BannerEndpoint", "archive", mutationEntry, {
170
+ mode: "destructure",
171
+ name: "mutation",
172
+ });
173
+ expect(next).toBe(content);
174
+ });
175
+ });
@@ -862,3 +862,134 @@ export const parseValues = (value: string | null) =>
862
862
  ?.split(",")
863
863
  .map((item) => item.trim())
864
864
  .filter(Boolean) ?? [];
865
+
866
+ // ---- Service / signal insertion (add-mutation, add-slice) ----
867
+
868
+ export const hasSourceParseErrors = (content: string, fileName = "source.ts") =>
869
+ hasParseDiagnostics(sourceFileFor(fileName, content));
870
+
871
+ const findClassDeclaration = (source: ts.SourceFile, className: string): ts.ClassDeclaration | null => {
872
+ let found: ts.ClassDeclaration | null = null;
873
+ const visit = (node: ts.Node) => {
874
+ if (found) return;
875
+ if (ts.isClassDeclaration(node) && node.name?.text === className) {
876
+ found = node;
877
+ return;
878
+ }
879
+ ts.forEachChild(node, visit);
880
+ };
881
+ ts.forEachChild(source, visit);
882
+ return found;
883
+ };
884
+
885
+ const firstHeritageCall = (node: ts.ClassDeclaration): ts.CallExpression | null => {
886
+ const expression = (node.heritageClauses?.flatMap((clause) => [...clause.types]) ?? [])[0]?.expression;
887
+ return expression && ts.isCallExpression(expression) ? expression : null;
888
+ };
889
+
890
+ const factoryArrowOf = (call: ts.CallExpression): ts.ArrowFunction | ts.FunctionExpression | null => {
891
+ const arg = call.arguments.find((argument) => ts.isArrowFunction(argument) || ts.isFunctionExpression(argument));
892
+ return arg && (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg)) ? arg : null;
893
+ };
894
+
895
+ export const hasClassMethod = (content: string, className: string, methodName: string) => {
896
+ const node = findClassDeclaration(sourceFileFor("service.ts", content), className);
897
+ return Boolean(
898
+ node?.members.some((member) => ts.isMethodDeclaration(member) && nodeName(member.name) === methodName),
899
+ );
900
+ };
901
+
902
+ export const insertClassMethod = (content: string, className: string, methodBlock: string): string | null => {
903
+ const source = sourceFileFor("service.ts", content);
904
+ const node = findClassDeclaration(source, className);
905
+ if (!node) return null;
906
+ const closeBrace = node.getEnd() - 1;
907
+ if (content[closeBrace] !== "}") return null;
908
+ const lead = content.slice(0, closeBrace).endsWith("\n") ? "" : "\n";
909
+ return spliceText(content, closeBrace, closeBrace, `${lead}${methodBlock}\n`);
910
+ };
911
+
912
+ export interface FactoryParamPlan {
913
+ mode: "destructure" | "positional";
914
+ name: string;
915
+ }
916
+
917
+ const arrowParamInnerRegion = (
918
+ content: string,
919
+ source: ts.SourceFile,
920
+ arrow: ts.ArrowFunction | ts.FunctionExpression,
921
+ ) => {
922
+ if (arrow.parameters.length > 0) {
923
+ return {
924
+ start: arrow.parameters[0].getStart(source),
925
+ end: arrow.parameters[arrow.parameters.length - 1].getEnd(),
926
+ };
927
+ }
928
+ const open = content.indexOf("(", arrow.getStart(source));
929
+ if (open < 0) return null;
930
+ const close = content.indexOf(")", open);
931
+ if (close < 0) return null;
932
+ return { start: open + 1, end: close };
933
+ };
934
+
935
+ const factoryParamEdit = (
936
+ content: string,
937
+ source: ts.SourceFile,
938
+ arrow: ts.ArrowFunction | ts.FunctionExpression,
939
+ plan: FactoryParamPlan,
940
+ ): { start: number; end: number; text: string } | "unchanged" | null => {
941
+ if (arrow.parameters.length > 1) return null;
942
+ const region = arrowParamInnerRegion(content, source, arrow);
943
+ if (!region) return null;
944
+ if (arrow.parameters.length === 0) {
945
+ return { ...region, text: plan.mode === "destructure" ? `{ ${plan.name} }` : plan.name };
946
+ }
947
+ const param = arrow.parameters[0];
948
+ if (plan.mode === "positional") return nodeName(param.name) === plan.name ? "unchanged" : null;
949
+ if (!ts.isObjectBindingPattern(param.name)) return null;
950
+ const names = param.name.elements.map((element) => (ts.isIdentifier(element.name) ? element.name.text : null));
951
+ if (names.some((name) => name === null)) return null;
952
+ if (names.includes(plan.name)) return "unchanged";
953
+ return {
954
+ start: param.getStart(source),
955
+ end: param.getEnd(),
956
+ text: `{ ${[...(names as string[]), plan.name].join(", ")} }`,
957
+ };
958
+ };
959
+
960
+ const factoryObjectOf = (source: ts.SourceFile, className: string) => {
961
+ const node = findClassDeclaration(source, className);
962
+ const call = node ? firstHeritageCall(node) : null;
963
+ const arrow = call ? factoryArrowOf(call) : null;
964
+ const object = arrow ? firstObjectReturnedByArrow(arrow) : null;
965
+ return arrow && object ? { arrow, object } : null;
966
+ };
967
+
968
+ export const hasSignalFactoryEntry = (content: string, className: string, entryName: string) => {
969
+ const located = factoryObjectOf(sourceFileFor("signal.ts", content), className);
970
+ return Boolean(located?.object.properties.some((property) => propertyName(property) === entryName));
971
+ };
972
+
973
+ export const insertSignalFactoryEntry = (
974
+ content: string,
975
+ className: string,
976
+ entryName: string,
977
+ entryLine: string,
978
+ param: FactoryParamPlan,
979
+ ): string | null => {
980
+ const source = sourceFileFor("signal.ts", content);
981
+ const located = factoryObjectOf(source, className);
982
+ if (!located) return null;
983
+ const locator = locatedObject(source, located.object);
984
+ if (locator.fields.some((field) => field.name === entryName)) return content;
985
+ // Compute the param edit first (its offsets precede the object), but apply it last so the
986
+ // object splice (at a higher offset) does not invalidate the param region positions.
987
+ const paramEdit = factoryParamEdit(content, source, located.arrow, param);
988
+ if (paramEdit === null) return null;
989
+ const withEntry = insertOrderedFieldLine(content, locator, entryName, entryLine, {
990
+ fieldIndent: " ",
991
+ closingIndent: "",
992
+ });
993
+ if (withEntry === content) return null;
994
+ return paramEdit === "unchanged" ? withEntry : spliceText(withEntry, paramEdit.start, paramEdit.end, paramEdit.text);
995
+ };
package/workflow/types.ts CHANGED
@@ -47,6 +47,14 @@ export interface AddEnumFieldInput extends PrimitiveTargetInput {
47
47
  includeInLight?: boolean | null;
48
48
  }
49
49
 
50
+ export interface AddMutationInput extends PrimitiveTargetInput {
51
+ mutation: string | null;
52
+ }
53
+
54
+ export interface AddSliceInput extends PrimitiveTargetInput {
55
+ slice: string | null;
56
+ }
57
+
50
58
  export interface WorkflowInputSpec {
51
59
  type: WorkflowInputType;
52
60
  required?: boolean;
@@ -472,4 +480,6 @@ export interface WorkflowPrimitiveOperations {
472
480
  createUi: (input: PrimitiveTargetInput & { surface: UiSurface }) => Promise<PrimitiveWriteReport>;
473
481
  addField: (input: AddFieldInput) => Promise<PrimitiveWriteReport>;
474
482
  addEnumField: (input: AddEnumFieldInput) => Promise<PrimitiveWriteReport>;
483
+ addMutation: (input: AddMutationInput) => Promise<PrimitiveWriteReport>;
484
+ addSlice: (input: AddSliceInput) => Promise<PrimitiveWriteReport>;
475
485
  }