@akanjs/devkit 3.1.2-rc.0 → 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.
- package/akanMcpContract.ts +2 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +1 -0
- package/package.json +3 -3
- package/workflow/executor.ts +11 -0
- package/workflow/index.ts +1 -0
- package/workflow/keyRename.ts +78 -0
- package/workflow/plan.ts +22 -0
- package/workflow/types.ts +7 -0
package/akanMcpContract.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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",
|
package/workflow/executor.ts
CHANGED
|
@@ -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
|
}
|