@akanjs/devkit 3.1.2-rc.1 → 3.1.2-rc.2

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.
@@ -188,6 +188,8 @@ export const defaultWorkflowPlanPath = (workflow: string, inputs: WorkflowPlanIn
188
188
  slugPart(inputs.surface),
189
189
  slugPart(inputs.mutation),
190
190
  slugPart(inputs.slice),
191
+ slugPart(inputs.query),
192
+ slugPart(inputs.to),
191
193
  ]
192
194
  .filter(Boolean)
193
195
  .join("-");
@@ -32,6 +32,7 @@ Then pick the workflow that matches the intent:
32
32
  | a new field with a closed set of values | `add-enum-field` |
33
33
  | a new state-changing action | `add-mutation` |
34
34
  | a filtered list, tab, or reusable query | `add-slice` |
35
+ | a slice or query key renamed, with every name derived from it | `rename-key` |
35
36
  | a View / Unit / Template / Zone / Util for an existing module | `create-ui` |
36
37
 
37
38
  ## The chain
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "3.1.2-rc.1",
3
+ "version": "3.1.2-rc.2",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -41,7 +41,7 @@
41
41
  "./*": "./*.ts"
42
42
  },
43
43
  "dependencies": {
44
- "@akanjs/language": "3.1.2-rc.1",
44
+ "@akanjs/language": "3.1.2-rc.2",
45
45
  "@earendil-works/pi-agent-core": "1.3.0",
46
46
  "@earendil-works/pi-ai": "1.3.0",
47
47
  "@earendil-works/pi-codemode": "1.3.0",
@@ -50,7 +50,7 @@
50
50
  "@earendil-works/pi-tui": "1.3.0",
51
51
  "@inquirer/prompts": "^8.7.3",
52
52
  "@tailwindcss/node": "^4.3.3",
53
- "akanjs": "3.1.2-rc.1",
53
+ "akanjs": "3.1.2-rc.2",
54
54
  "chalk": "^6.0.1",
55
55
  "commander": "^15.0.0",
56
56
  "dayjs": "^1.11.23",
@@ -404,6 +404,7 @@ export const createWorkflowStepRegistry = ({
404
404
  addEnumField,
405
405
  addMutation,
406
406
  addSlice,
407
+ renameKey,
407
408
  createInteraction,
408
409
  }: WorkflowPrimitiveOperations): WorkflowStepRegistry => {
409
410
  const inspect = async () => undefined;
@@ -520,6 +521,16 @@ export const createWorkflowStepRegistry = ({
520
521
  ),
521
522
  [workflowStepKey("add-slice", "update-signal-slice")]: inspect,
522
523
  [workflowStepKey("add-slice", "connect-view-surfaces")]: async (_step, plan) => addSliceViewSurfaceInspection(plan),
524
+ [workflowStepKey("rename-key", "rename-key")]: async (_step, plan) =>
525
+ primitiveReportToWorkflowStepResult(
526
+ await renameKey({
527
+ app: workflowStringInput(plan.inputs.app),
528
+ module: workflowStringInput(plan.inputs.module),
529
+ slice: workflowStringInput(plan.inputs.slice),
530
+ query: workflowStringInput(plan.inputs.query),
531
+ to: workflowStringInput(plan.inputs.to),
532
+ }),
533
+ ),
523
534
  };
524
535
  };
525
536
 
package/workflow/index.ts CHANGED
@@ -2,6 +2,7 @@ export * from "./artifacts";
2
2
  export * from "./executor";
3
3
  export * from "./guardCatalog";
4
4
  export * from "./interactionScaffold";
5
+ export * from "./keyRename";
5
6
  export * from "./moduleIndex";
6
7
  export * from "./mutationScaffold";
7
8
  export * from "./mutationSource";
@@ -0,0 +1,78 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { AkanIndex, type AkanRenameEdit } from "@akanjs/language";
4
+ import ts from "typescript";
5
+ import type { Workspace } from "../commandDecorators";
6
+ import { resolveWorkflowSys } from "./executor";
7
+ import { getSysRoot, moduleSourcePaths } from "./source";
8
+ import type { RenameKeyInput, ScaffoldFile, WorkflowDiagnostic } from "./types";
9
+
10
+ const indexedPath = /^(apps|libs)\/(?!(?:.*\/)?(?:node_modules|dist|\.akan)\/).*\.tsx?$/;
11
+ const keyPattern = /^[a-z][A-Za-z0-9]*$/;
12
+ const slashed = (fileName: string) => fileName.replace(/\\/g, "/");
13
+
14
+ const refuse = (code: string, input: string, message: string) => ({
15
+ diagnostics: [{ severity: "error", code, input, message }] satisfies WorkflowDiagnostic[],
16
+ files: [] as ScaffoldFile[],
17
+ });
18
+
19
+ export class KeyRename {
20
+ static async build(workspace: Workspace, input: RenameKeyInput) {
21
+ const sys = await resolveWorkflowSys(workspace, input.app);
22
+ if (!sys) return refuse("workflow-target-missing", "app", `No app or library named ${input.app ?? ""}.`);
23
+ if (!input.module) return refuse("workflow-input-missing", "module", "Module name is required.");
24
+ if (!input.slice === !input.query)
25
+ return refuse("rename-key-source-invalid", "slice", "Name exactly one key to rename, as slice or as query.");
26
+ const family = input.slice ? "slice" : "query";
27
+ const from = input.slice ?? input.query ?? "";
28
+ if (!input.to || !keyPattern.test(input.to))
29
+ return refuse(
30
+ "rename-key-name-invalid",
31
+ "to",
32
+ `New key "${input.to ?? ""}" must be camelCase (inFeed, byTitle).`,
33
+ );
34
+ if (input.to === from) return refuse("rename-key-name-same", "to", `New key is the same as ${from}.`);
35
+
36
+ const root = workspace.workspaceRoot;
37
+ const paths = moduleSourcePaths(input.module);
38
+ const sourceRelative = `${getSysRoot(sys)}/${family === "slice" ? paths.signal : paths.document}`;
39
+ const sourcePath = slashed(path.join(root, sourceRelative));
40
+ const texts = new Map<string, string>();
41
+ const index = new AkanIndex({ ts, root });
42
+ for (const relative of await workspace.getAllFiles("{apps,libs}/**/*.{ts,tsx}")) {
43
+ if (!indexedPath.test(slashed(relative))) continue;
44
+ const fileName = slashed(path.join(root, relative));
45
+ const text = await readFile(fileName, "utf8");
46
+ texts.set(fileName, text);
47
+ index.setFile(fileName, text);
48
+ }
49
+
50
+ const keys = index.sourceKeysOf(sourcePath).filter((key) => key.family === family);
51
+ const source = keys.find((key) => key.sourceKey === from);
52
+ if (!source)
53
+ return refuse("rename-key-source-missing", family, `${sourceRelative} declares no ${family} key ${from}.`);
54
+ if (keys.some((key) => key.sourceKey === input.to))
55
+ return refuse("rename-key-exists", "to", `${sourceRelative} already declares the ${family} key ${input.to}.`);
56
+ const edits = index.renameAt(sourcePath, source.span.start, input.to, (fileName) => texts.get(fileName)) ?? [];
57
+
58
+ const byFile = new Map<string, AkanRenameEdit[]>();
59
+ for (const edit of edits) byFile.set(edit.span.fileName, [...(byFile.get(edit.span.fileName) ?? []), edit]);
60
+ const files: ScaffoldFile[] = [...byFile].map(([fileName, fileEdits]) => {
61
+ let content = texts.get(fileName) ?? "";
62
+ for (const { span, newText } of fileEdits.toSorted((a, b) => b.span.start - a.span.start))
63
+ content = content.slice(0, span.start) + newText + content.slice(span.end);
64
+ const names = fileEdits.length === 1 ? "1 name changes" : `${fileEdits.length} names change`;
65
+ return {
66
+ path: slashed(path.relative(root, fileName)),
67
+ content,
68
+ action: "modify",
69
+ reason: `${names} with the ${family} key ${from} → ${input.to}.`,
70
+ };
71
+ });
72
+ return { diagnostics: [] as WorkflowDiagnostic[], files: files.toSorted((a, b) => a.path.localeCompare(b.path)) };
73
+ }
74
+
75
+ static targetsOf(files: readonly { path: string }[]) {
76
+ return [...new Set(files.map((file) => file.path.split("/").slice(0, 2).join("/")))];
77
+ }
78
+ }
package/workflow/plan.ts CHANGED
@@ -3,6 +3,7 @@ import type { Workspace } from "../commandDecorators";
3
3
  import { workflowPlanApproval } from "./artifacts";
4
4
  import { resolveWorkflowSys } from "./executor";
5
5
  import { InteractionScaffold } from "./interactionScaffold";
6
+ import { KeyRename } from "./keyRename";
6
7
  import { MutationScaffold } from "./mutationScaffold";
7
8
  import { coerceFieldDefault, moduleSourcePaths } from "./source";
8
9
  import type {
@@ -28,6 +29,8 @@ const nameInputPatterns: Record<string, RegExp> = {
28
29
  scalar: identifierPattern,
29
30
  field: identifierPattern,
30
31
  slice: identifierPattern,
32
+ query: identifierPattern,
33
+ to: identifierPattern,
31
34
  mutation: identifierPattern,
32
35
  interaction: identifierPattern,
33
36
  };
@@ -458,7 +461,26 @@ const planString = (inputs: Record<string, WorkflowInputValue>, name: string) =>
458
461
  const planStrings = (inputs: Record<string, WorkflowInputValue>, name: string) =>
459
462
  Array.isArray(inputs[name]) ? inputs[name] : null;
460
463
 
464
+ const inspectRenameKeyPlan = async (workspace: Workspace, plan: WorkflowPlan): Promise<WorkflowPlan> => {
465
+ if (plan.diagnostics.some((diagnostic) => diagnostic.severity === "error")) return plan;
466
+ const { diagnostics, files } = await KeyRename.build(workspace, {
467
+ app: planString(plan.inputs, "app"),
468
+ module: planString(plan.inputs, "module"),
469
+ slice: planString(plan.inputs, "slice"),
470
+ query: planString(plan.inputs, "query"),
471
+ to: planString(plan.inputs, "to"),
472
+ });
473
+ return {
474
+ ...plan,
475
+ diagnostics: [...plan.diagnostics, ...diagnostics],
476
+ predictedChanges: files.length
477
+ ? files.map((file) => ({ target: file.path, action: "modify", applyScope: "auto", reason: file.reason }))
478
+ : plan.predictedChanges,
479
+ };
480
+ };
481
+
461
482
  export const inspectWorkflowPlan = async (workspace: Workspace, plan: WorkflowPlan): Promise<WorkflowPlan> => {
483
+ if (plan.workflow === "rename-key") return await inspectRenameKeyPlan(workspace, plan);
462
484
  if (plan.workflow !== "add-mutation" && plan.workflow !== "create-interaction") return plan;
463
485
  const app = planString(plan.inputs, "app");
464
486
  if (!app) return plan;
package/workflow/types.ts CHANGED
@@ -63,6 +63,12 @@ export interface AddSliceInput extends PrimitiveTargetInput {
63
63
  slice: string | null;
64
64
  }
65
65
 
66
+ export interface RenameKeyInput extends PrimitiveTargetInput {
67
+ slice: string | null;
68
+ query: string | null;
69
+ to: string | null;
70
+ }
71
+
66
72
  export interface WorkflowInputSpec {
67
73
  type: WorkflowInputType;
68
74
  required?: boolean;
@@ -498,5 +504,6 @@ export interface WorkflowPrimitiveOperations {
498
504
  addEnumField: (input: AddEnumFieldInput) => Promise<PrimitiveWriteReport>;
499
505
  addMutation: (input: AddMutationInput) => Promise<PrimitiveWriteReport>;
500
506
  addSlice: (input: AddSliceInput) => Promise<PrimitiveWriteReport>;
507
+ renameKey: (input: RenameKeyInput) => Promise<PrimitiveWriteReport>;
501
508
  createInteraction: (input: CreateInteractionInput) => Promise<PrimitiveWriteReport>;
502
509
  }