@akanjs/devkit 3.0.0-beta.17 → 3.0.0-beta.18

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 (41) hide show
  1. package/abstractDoc.ts +1 -3
  2. package/applicationBuildRunner.ts +4 -3
  3. package/artifact/implicitRootLayout.test.ts +6 -11
  4. package/artifact/implicitRootLayout.ts +0 -15
  5. package/biome.base.json +6 -1
  6. package/capacitorApp.test.ts +26 -0
  7. package/capacitorApp.ts +25 -4
  8. package/codeAgent/agent/AkanCodeServices.ts +16 -9
  9. package/codeAgent/agent/CodeAgent.ts +62 -13
  10. package/codeAgent/agent/akanCodePaths.ts +23 -3
  11. package/codeAgent/agent/akanSystemPrompt.ts +5 -0
  12. package/codeAgent/skills/akan-code/SKILL.md +122 -0
  13. package/codeAgent/skills/akan-module/SKILL.md +2 -2
  14. package/codeAgent/skills/akan-store/SKILL.md +6 -2
  15. package/codeAgent/tools/AkanCodePlugins.ts +19 -3
  16. package/codeAgent/tools/AskToolPack.ts +111 -0
  17. package/codeAgent/tools/McpClient.ts +51 -5
  18. package/codeAgent/tools/McpOAuth.ts +268 -0
  19. package/codeAgent/tools/McpOAuthLoopback.ts +77 -0
  20. package/codeAgent/tools/McpServerConfig.ts +69 -34
  21. package/codeAgent/tools/McpSignIn.ts +161 -0
  22. package/codeAgent/tools/McpTokenStore.ts +92 -0
  23. package/codeAgent/tools/McpToolPack.ts +16 -3
  24. package/codeAgent/tools/askToolPack.test.ts +119 -0
  25. package/codeAgent/tools/mcpOAuth.test.ts +320 -0
  26. package/codeAgent/tools/mcpServerConfig.test.ts +106 -12
  27. package/commandDecorators/command.ts +2 -0
  28. package/commandDecorators/commandDecorators.test.ts +1 -1
  29. package/commandDecorators/helpFormatter.ts +2 -1
  30. package/executors.test.ts +10 -46
  31. package/index.ts +0 -1
  32. package/package.json +2 -2
  33. package/prompter.ts +0 -7
  34. package/qualityScanner.test.ts +3 -2
  35. package/qualityScanner.ts +1 -1
  36. package/routeSourceValidator.test.ts +0 -1
  37. package/routeSourceValidator.ts +0 -12
  38. package/scanInfo.ts +5 -2
  39. package/workflow/executor.ts +52 -0
  40. package/workflow/source.ts +19 -13
  41. package/guideline.ts +0 -19
package/prompter.ts CHANGED
@@ -2,7 +2,6 @@ import fsPromise from "node:fs/promises";
2
2
  import { input, select } from "@inquirer/prompts";
3
3
 
4
4
  import { getDirname } from "./getDirname";
5
- import type { GuideGenerateJson } from "./guideline";
6
5
 
7
6
  interface FileUpdateRequestProps {
8
7
  context: string;
@@ -34,12 +33,6 @@ export class Prompter {
34
33
  const guidelineRoot = await Prompter.#getGuidelineRoot();
35
34
  return (await fsPromise.readdir(guidelineRoot)).filter((name) => !name.startsWith("_")).sort();
36
35
  }
37
- static async getGuideJson(guideName: string): Promise<GuideGenerateJson> {
38
- const guidelineRoot = await Prompter.#getGuidelineRoot();
39
- const filePath = `${guidelineRoot}/${guideName}/${guideName}.generate.json`;
40
- const guideJson = await fsPromise.readFile(filePath, "utf-8");
41
- return JSON.parse(guideJson) as GuideGenerateJson;
42
- }
43
36
  /**
44
37
  * The name is checked against the directory listing rather than interpolated straight into a path:
45
38
  * it arrives from an MCP tool argument, and an unknown one has to name the valid set instead of
@@ -26,7 +26,7 @@ afterEach(async () => {
26
26
  });
27
27
 
28
28
  describe("AkanQualityScanner abstract rule", () => {
29
- test("warns on an abstract over the line limit and points at akan compact", async () => {
29
+ test("warns on an abstract over the line limit and says how to trim it", async () => {
30
30
  const root = await makeWorkspace({
31
31
  "apps/demo/lib/post/post.abstract.md": abstractOf(AbstractDoc.maxLines + 1),
32
32
  "apps/demo/lib/post/post.constant.ts": "export class Post {}\n",
@@ -41,7 +41,8 @@ describe("AkanQualityScanner abstract rule", () => {
41
41
  expect(warnings).toHaveLength(1);
42
42
  expect(warnings[0]?.file).toBe("apps/demo/lib/post/post.abstract.md");
43
43
  expect(warnings[0]?.message).toContain(`${AbstractDoc.maxLines + 1} lines`);
44
- expect(warnings[0]?.fix).toContain("akan compact");
44
+ expect(warnings[0]?.fix).toContain("## Rules");
45
+ expect(warnings[0]?.fix).not.toContain("akan compact");
45
46
  });
46
47
  });
47
48
 
package/qualityScanner.ts CHANGED
@@ -114,7 +114,7 @@ const RULE_FIXES: Record<string, string> = {
114
114
  "Split the file by responsibility — move Zones, Utils, or subcomponents into sibling files.",
115
115
  "akan.file.max-lines": "Break the file into smaller focused modules; keep one primary responsibility per file.",
116
116
  "akan.file.abstract-max-lines":
117
- "Run `akan compact <app-or-lib>` to rewrite the abstract with the AI editor, keeping only the invariants and workflows the source files cannot show.",
117
+ "Trim the abstract by hand to a title, one sentence naming what the module owns, a `## Rules` list of the invariants the source cannot show, and an optional workflow chain.",
118
118
  "akan.file.placeholder-export":
119
119
  "Remove the placeholder export; generated indexes should only re-export real modules.",
120
120
  "akan.file.bang-comment-in-client":
@@ -15,7 +15,6 @@ describe("RouteSourceValidator", () => {
15
15
  "export const reconnect = true;",
16
16
  "export const wsConnect = true;",
17
17
  "export const layoutStyle = {};",
18
- 'export const gaTrackingId = "G-1";',
19
18
  ].join("\n");
20
19
 
21
20
  expect(() => validate(source, "layout", true)).not.toThrow();
@@ -86,18 +86,6 @@ export class RouteSourceValidator {
86
86
  if (exported.has("head") && exported.has("generateHead")) {
87
87
  throw new Error(`[route-convention] head and generateHead cannot both be exported in ${filePath}`);
88
88
  }
89
- if (
90
- !options.rootLayout &&
91
- (exported.has("head") || exported.has("generateHead")) &&
92
- (exported.has("metadata") || exported.has("generateMetadata"))
93
- ) {
94
- throw new Error(
95
- `[route-convention] head/generateHead and metadata/generateMetadata cannot both be exported in ${filePath}`,
96
- );
97
- }
98
- if (exported.has("metadata") && exported.has("generateMetadata")) {
99
- throw new Error(`[route-convention] metadata and generateMetadata cannot both be exported in ${filePath}`);
100
- }
101
89
  const chain = RouteSourceValidator.#readChain(sourceFile, filePath);
102
90
  if (!chain) return { devOnly: RouteSourceValidator.#readDevOnly(sourceFile, filePath) };
103
91
  const named = [...exported].filter((name) => name !== "default");
package/scanInfo.ts CHANGED
@@ -524,16 +524,19 @@ export class PkgInfo {
524
524
  const [tsconfig, rootPackageJson] = await Promise.all([exec.getTsConfig(), exec.workspace.getPackageJson()]);
525
525
  const scanner = await createDependencyScanner(exec);
526
526
  const npmSet = new Set(Object.keys({ ...rootPackageJson.dependencies, ...rootPackageJson.devDependencies }));
527
+ const workspacePathOf = (resolve: string) => resolve.replace(/^\.\//, "");
527
528
  const pkgPathSet = new Set(
528
529
  Object.keys(tsconfig.compilerOptions.paths ?? {})
529
- .filter((path) => tsconfig.compilerOptions.paths?.[path]?.some((resolve) => resolve.startsWith("pkgs/")))
530
+ .filter((path) =>
531
+ tsconfig.compilerOptions.paths?.[path]?.some((resolve) => workspacePathOf(resolve).startsWith("pkgs/")),
532
+ )
530
533
  .map((path) => path.replace("/*", "")),
531
534
  );
532
535
  const [npmDepSet, pkgPathDepSet] = await scanner.getImportSets([npmSet, pkgPathSet]);
533
536
  const pkgDeps = [...pkgPathDepSet]
534
537
  .map((path) => {
535
538
  const pathSplitLength = path.split("/").length;
536
- return (tsconfig.compilerOptions.paths?.[path]?.[0] ?? "*")
539
+ return workspacePathOf(tsconfig.compilerOptions.paths?.[path]?.[0] ?? "*")
537
540
  .split("/")
538
541
  .slice(1, 1 + pathSplitLength)
539
542
  .join("/");
@@ -1,3 +1,4 @@
1
+ import { capitalize } from "akanjs/common";
1
2
  import ts from "typescript";
2
3
  import type { AkanModuleContext } from "../akanContext";
3
4
  import type { Sys, Workspace } from "../commandDecorators";
@@ -396,6 +397,54 @@ const addFieldUiSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult =>
396
397
  };
397
398
  };
398
399
 
400
+ const addMutationActionSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult => {
401
+ const module = workflowStringInput(plan.inputs.module) ?? "<module>";
402
+ const mutation = workflowStringInput(plan.inputs.mutation) ?? "<mutation>";
403
+ const moduleClassName = moduleComponentName(module);
404
+ return {
405
+ recommendations: [
406
+ {
407
+ code: "add-mutation-action-surface-review",
408
+ kind: "manual-action",
409
+ target: `*/${moduleSourcePaths(module).store}`,
410
+ action: `Name the endpoint's guards and add its dictionary .endpoint() entry with a .desc(). After sync, fetch.${mutation} exists; write a store action only for a toast, an optimistic update, or a multi-field write, and put the control that calls it in ${moduleClassName}.Util.tsx.`,
411
+ confidence: "medium",
412
+ message: `Workflow apply does not write store or UI code for ${module}.${mutation}; review whether a screen should call it.`,
413
+ },
414
+ ],
415
+ nextActions: [
416
+ {
417
+ command: `akan workflow explain ${plan.workflow}`,
418
+ reason: "Review action surface guidance before manually editing store or UI files.",
419
+ },
420
+ ],
421
+ };
422
+ };
423
+
424
+ const addSliceViewSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult => {
425
+ const module = workflowStringInput(plan.inputs.module) ?? "<module>";
426
+ const slice = workflowStringInput(plan.inputs.slice) ?? "<slice>";
427
+ const moduleClassName = moduleComponentName(module);
428
+ return {
429
+ recommendations: [
430
+ {
431
+ code: "add-slice-view-surface-review",
432
+ kind: "manual-action",
433
+ target: `*/${moduleSourcePaths(module).zone}`,
434
+ action: `Fill in the service query stub and add the dictionary .slice() entry. After sync, load fetch.init${moduleClassName}${capitalize(slice)}() in the page and pass the result to a ${moduleClassName}.Zone as an init prop.`,
435
+ confidence: "medium",
436
+ message: `Workflow apply does not write page or Zone code for ${module}.${slice}; review where the list should render.`,
437
+ },
438
+ ],
439
+ nextActions: [
440
+ {
441
+ command: `akan workflow explain ${plan.workflow}`,
442
+ reason: "Review view surface guidance before manually editing Zone or page files.",
443
+ },
444
+ ],
445
+ };
446
+ };
447
+
399
448
  export const createWorkflowStepRegistry = ({
400
449
  workspace,
401
450
  createModule,
@@ -494,6 +543,8 @@ export const createWorkflowStepRegistry = ({
494
543
  }),
495
544
  ),
496
545
  [workflowStepKey("add-mutation", "update-signal")]: inspect,
546
+ [workflowStepKey("add-mutation", "update-action-surfaces")]: async (_step, plan) =>
547
+ addMutationActionSurfaceInspection(plan),
497
548
  [workflowStepKey("add-slice", "update-service-query")]: async (_step, plan) =>
498
549
  primitiveReportToWorkflowStepResult(
499
550
  await addSlice({
@@ -503,6 +554,7 @@ export const createWorkflowStepRegistry = ({
503
554
  }),
504
555
  ),
505
556
  [workflowStepKey("add-slice", "update-signal-slice")]: inspect,
557
+ [workflowStepKey("add-slice", "connect-view-surfaces")]: async (_step, plan) => addSliceViewSurfaceInspection(plan),
506
558
  };
507
559
  };
508
560
 
@@ -160,21 +160,27 @@ export const normalizeFieldType = (typeName: string) => {
160
160
  return normalizedTypes[typeName.toLowerCase()] ?? typeName;
161
161
  };
162
162
 
163
- export const ensureBaseTypeImport = (content: string, typeName: string) => {
164
- if (typeName !== "Int" && typeName !== "Float") return content;
165
- const source = sourceFileFor("constant.ts", content);
166
- const baseImport = findNamedImport(source, "akanjs/base");
167
- if (baseImport) {
168
- if (baseImport.names.includes(typeName)) return content;
169
- const nextNames = [...baseImport.names, typeName].sort();
163
+ export const ensureNamedImport = (content: string, moduleSpecifier: string, name: string) => {
164
+ const source = sourceFileFor("source.ts", content);
165
+ const namedImport = findNamedImport(source, moduleSpecifier);
166
+ if (namedImport) {
167
+ if (namedImport.names.includes(name)) return content;
168
+ const nextNames = [...namedImport.names, name].sort();
170
169
  return spliceText(
171
170
  content,
172
- baseImport.namedBindingsStart,
173
- baseImport.namedBindingsEnd,
171
+ namedImport.namedBindingsStart,
172
+ namedImport.namedBindingsEnd,
174
173
  `{ ${nextNames.join(", ")} }`,
175
174
  );
176
175
  }
177
- return `import { ${typeName} } from "akanjs/base";\n${content}`;
176
+ return `import { ${name} } from "${moduleSpecifier}";\n${content}`;
177
+ };
178
+
179
+ export const ensureBaseImport = (content: string, name: string) => ensureNamedImport(content, "akanjs/base", name);
180
+
181
+ export const ensureBaseTypeImport = (content: string, typeName: string) => {
182
+ if (typeName !== "Int" && typeName !== "Float") return content;
183
+ return ensureBaseImport(content, typeName);
178
184
  };
179
185
 
180
186
  export type FieldDefaultValue = string | number | boolean | null;
@@ -200,12 +206,12 @@ const booleanDefault = (rawDefault: FieldDefaultValue): string | null => {
200
206
  };
201
207
 
202
208
  const dateDefault = (rawDefault: FieldDefaultValue): string | null => {
203
- if (typeof rawDefault === "number" && Number.isFinite(rawDefault)) return `new Date(${rawDefault})`;
209
+ if (typeof rawDefault === "number" && Number.isFinite(rawDefault)) return `() => dayjs(${rawDefault})`;
204
210
  if (typeof rawDefault !== "string") return null;
205
211
  const trimmed = rawDefault.trim();
206
212
  if (!trimmed) return null;
207
- if (trimmed === "now") return "new Date()";
208
- if (!Number.isNaN(Date.parse(trimmed))) return `new Date(${JSON.stringify(trimmed)})`;
213
+ if (trimmed === "now") return "() => dayjs()";
214
+ if (!Number.isNaN(Date.parse(trimmed))) return `() => dayjs(${JSON.stringify(trimmed)})`;
209
215
  return null;
210
216
  };
211
217
 
package/guideline.ts DELETED
@@ -1,19 +0,0 @@
1
- export interface GuideScan {
2
- type: "example" | "source" | "usage" | (string & {});
3
- description: string;
4
- path: string;
5
- filterText?: string;
6
- sample?: number;
7
- }
8
- export interface GuideUpdate {
9
- filePath: string;
10
- contents: string[];
11
- rules: string[];
12
- }
13
- export interface GuideGenerateJson {
14
- title: string;
15
- description: string;
16
- scans: GuideScan[];
17
- update: GuideUpdate;
18
- page?: string;
19
- }