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

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 (96) hide show
  1. package/abstractDoc.ts +5 -4
  2. package/akanApp/akanApp.host.ts +23 -9
  3. package/akanConfig/akanConfig.test.ts +55 -30
  4. package/akanConfig/akanConfig.ts +29 -17
  5. package/akanConfig/types.ts +1 -0
  6. package/applicationBuildRunner.test.ts +5 -3
  7. package/applicationBuildRunner.ts +33 -6
  8. package/artifact/implicitRootLayout.test.ts +6 -11
  9. package/artifact/implicitRootLayout.ts +0 -15
  10. package/artifact/routeSeedIndex.test.ts +7 -6
  11. package/biome.base.json +6 -1
  12. package/capacitorApp.test.ts +28 -2
  13. package/capacitorApp.ts +25 -4
  14. package/cloud/constants.ts +23 -0
  15. package/cloud/globalConfig.ts +13 -0
  16. package/codeAgent/agent/AkanCodeServices.ts +38 -12
  17. package/codeAgent/agent/CodeAgent.ts +94 -30
  18. package/codeAgent/agent/CodeAgentAsks.ts +3 -1
  19. package/codeAgent/agent/CodeAgentEventMapper.ts +9 -1
  20. package/codeAgent/agent/CodeAgentProxy.ts +91 -0
  21. package/codeAgent/agent/CodeAgentRpcHost.ts +29 -9
  22. package/codeAgent/agent/CodeAgentRpcListener.ts +117 -0
  23. package/codeAgent/agent/CodeAgentSuspended.ts +92 -0
  24. package/codeAgent/agent/akanCodeModel.ts +1 -1
  25. package/codeAgent/agent/akanCodePaths.ts +23 -3
  26. package/codeAgent/agent/akanContextFiles.test.ts +2 -1
  27. package/codeAgent/agent/akanSystemPrompt.ts +5 -0
  28. package/codeAgent/agent/codeAgentContract.test.ts +17 -0
  29. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  30. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  31. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  32. package/codeAgent/skills/akan-code/SKILL.md +122 -0
  33. package/codeAgent/skills/akan-module/SKILL.md +2 -2
  34. package/codeAgent/skills/akan-store/SKILL.md +6 -2
  35. package/codeAgent/tools/AkanCodePlugins.ts +19 -3
  36. package/codeAgent/tools/AskToolPack.ts +111 -0
  37. package/codeAgent/tools/McpClient.ts +51 -5
  38. package/codeAgent/tools/McpOAuth.ts +268 -0
  39. package/codeAgent/tools/McpOAuthLoopback.ts +77 -0
  40. package/codeAgent/tools/McpServerConfig.ts +69 -34
  41. package/codeAgent/tools/McpSignIn.ts +161 -0
  42. package/codeAgent/tools/McpTokenStore.ts +92 -0
  43. package/codeAgent/tools/McpToolPack.ts +16 -3
  44. package/codeAgent/tools/askToolPack.test.ts +119 -0
  45. package/codeAgent/tools/mcpOAuth.test.ts +324 -0
  46. package/codeAgent/tools/mcpServerConfig.test.ts +106 -12
  47. package/commandDecorators/command.ts +2 -0
  48. package/commandDecorators/commandDecorators.test.ts +1 -1
  49. package/commandDecorators/helpFormatter.ts +2 -1
  50. package/dependencyScanner.ts +4 -2
  51. package/devkitUtils.test.ts +1 -1
  52. package/executors.linkFallback.test.ts +103 -0
  53. package/executors.test.ts +18 -46
  54. package/executors.ts +31 -8
  55. package/frontendBuild/autoImportSync.test.ts +13 -0
  56. package/frontendBuild/autoImportSync.ts +19 -1
  57. package/frontendBuild/buildRouteClient.test.ts +52 -1
  58. package/frontendBuild/cssImportResolver.ts +1 -1
  59. package/frontendBuild/fontOptimizer.test.ts +1 -1
  60. package/frontendBuild/frontendBuild.test.ts +19 -17
  61. package/frontendBuild/hmrWatcher.test.ts +1 -1
  62. package/frontendBuild/routeClientBuilder.ts +27 -10
  63. package/frontendBuild/sourceMtimeIndex.test.ts +6 -5
  64. package/getDirname.test.ts +4 -2
  65. package/incrementalBuilder/devWatchBatch.test.ts +7 -6
  66. package/incrementalBuilder/incrementalBuilder.proc.ts +1 -0
  67. package/index.ts +0 -1
  68. package/integration/devStabilityHarness.test.ts +7 -2
  69. package/lint/gritRules.test.ts +2 -1
  70. package/lint/no-deep-internal-import.grit +2 -2
  71. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  72. package/linter.ts +4 -1
  73. package/package.json +2 -2
  74. package/platformTest/BunTestReport.test.ts +60 -0
  75. package/platformTest/BunTestReport.ts +65 -0
  76. package/platformTest/IdleWatchedProcess.test.ts +64 -0
  77. package/platformTest/IdleWatchedProcess.ts +77 -0
  78. package/platformTest/LinuxDockerTarget.ts +120 -0
  79. package/platformTest/PlatformTestRun.test.ts +68 -0
  80. package/platformTest/PlatformTestRun.ts +141 -0
  81. package/platformTest/PlatformTestTarget.ts +120 -0
  82. package/platformTest/TestSnapshot.test.ts +80 -0
  83. package/platformTest/TestSnapshot.ts +107 -0
  84. package/platformTest/WindowsSshTarget.ts +167 -0
  85. package/prompter.ts +0 -7
  86. package/qualityScanner.test.ts +3 -2
  87. package/qualityScanner.ts +1 -1
  88. package/routeSourceValidator.test.ts +0 -1
  89. package/routeSourceValidator.ts +0 -12
  90. package/scanInfo.ts +5 -2
  91. package/subspace.test.ts +5 -2
  92. package/subspace.ts +19 -8
  93. package/workflow/executor.ts +52 -0
  94. package/workflow/rolloutGate.test.ts +1 -1
  95. package/workflow/source.ts +19 -13
  96. package/guideline.ts +0 -19
@@ -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("/");
package/subspace.test.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
1
+ import { afterEach, beforeEach, describe, expect, setDefaultTimeout, test } from "bun:test";
2
2
  import { mkdir, mkdtemp, readdir, rm, writeFile } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
@@ -7,6 +7,9 @@ import { FileSys } from "./fileSys";
7
7
  import { formatSubspaceDiff, formatSubspacePushResults, formatSubspaceStatuses, Subspace } from "./subspace";
8
8
  import { SubspaceConfig } from "./subspaceConfig";
9
9
 
10
+ //? Each case drives several real git clones and pushes; on a 4-core Windows VM that alone lands at 2-5s.
11
+ setDefaultTimeout(20_000);
12
+
10
13
  const tempRoots: string[] = [];
11
14
  const originalEnv = { ...process.env };
12
15
 
@@ -96,7 +99,7 @@ const makeMirror = async (servedApp: string, privateApp: string, libName: string
96
99
  await write(path.join(wsRoot, "benchmarks/bench.ts"), "export {};\n");
97
100
  await write(
98
101
  path.join(wsRoot, "akan.subspace.ts"),
99
- `export default { pushableBranches: ["develop"], exclude: ["benchmarks"], subspaces: [{ name: "acme", repo: "${bareRoot}", apps: ["${servedApp}"] }] };\n`,
102
+ `export default { pushableBranches: ["develop"], exclude: ["benchmarks"], subspaces: [{ name: "acme", repo: ${JSON.stringify(bareRoot)}, apps: ["${servedApp}"] }] };\n`,
100
103
  );
101
104
 
102
105
  await makeSys(wsRoot, "apps", servedApp, {
package/subspace.ts CHANGED
@@ -296,15 +296,25 @@ export class Subspace {
296
296
  /**
297
297
  * `git archive` emits exactly the tracked files at HEAD, which is why the copy needs no exclude list of
298
298
  * its own: generated barrels, the `(libs)`/`public/libs` symlinks, env values, secrets and the lockfile
299
- * are all outside git and cannot enter the archive. Directory arguments only — a file list would put
300
- * route paths like `page/(docs)/…` through a shell.
299
+ * are all outside git and cannot enter the archive. Spawned without a shell, whose quoting differs between
300
+ * `sh` and `cmd.exe`.
301
301
  */
302
302
  async #extract(paths: string[], excludes: string[] = []) {
303
- const pathspec = paths.map((entry) => `'${entry}'`).join(" ");
304
- const excludeArgs = excludes.map((entry) => `--exclude='${entry}'`).join(" ");
305
- await this.#workspace.exec(
306
- `git archive HEAD -- ${pathspec} | tar -x -C '${this.#clonePath}' ${excludeArgs}`.trim(),
307
- );
303
+ // Relative, because GNU tar reads a drive-letter archive name (`C:\…`) as `host:path` on a remote host.
304
+ const archivePath = path.relative(this.#workspace.workspaceRoot, `${this.#clonePath}.tar`);
305
+ await this.#workspace.spawn("git", ["archive", "-o", archivePath, "HEAD", "--", ...paths]);
306
+ try {
307
+ await this.#workspace.spawn("tar", [
308
+ "-x",
309
+ "-f",
310
+ archivePath,
311
+ "-C",
312
+ this.#clonePath,
313
+ ...excludes.map((entry) => `--exclude=${entry}`),
314
+ ]);
315
+ } finally {
316
+ await rm(path.join(this.#workspace.workspaceRoot, archivePath), { force: true });
317
+ }
308
318
  }
309
319
 
310
320
  /**
@@ -399,7 +409,8 @@ export class Subspace {
399
409
  const gitignorePath = path.join(this.#clonePath, ".gitignore");
400
410
  if (!(await FileSys.fileExists(gitignorePath))) return;
401
411
  const [begin = "", end = ""] = Subspace.secretsMarkers;
402
- const lines = (await FileSys.readText(gitignorePath)).split("\n");
412
+ // `git archive` checks text out as CRLF under Windows Git's default `core.autocrlf=true`.
413
+ const lines = (await FileSys.readText(gitignorePath)).split(/\r?\n/);
403
414
  const beginIdx = lines.indexOf(begin);
404
415
  const endIdx = lines.indexOf(end);
405
416
  const filtered =
@@ -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
 
@@ -24,7 +24,7 @@ const workspacePackageManifestPaths = async () => {
24
24
  for (const workspace of workspaces) {
25
25
  const glob = new Bun.Glob(`${workspace}/package.json`);
26
26
  for await (const relativePath of glob.scan({ cwd: workspaceRoot, onlyFiles: true })) {
27
- paths.add(relativePath);
27
+ paths.add(relativePath.split(path.sep).join("/"));
28
28
  }
29
29
  }
30
30
  return [...paths].sort();
@@ -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
- }