@akanjs/devkit 3.0.0-beta.2 → 3.0.0-beta.20

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 (265) hide show
  1. package/README.md +1 -0
  2. package/abstractDoc.test.ts +6 -15
  3. package/abstractDoc.ts +6 -8
  4. package/agentsIndex.test.ts +4 -7
  5. package/agentsIndex.ts +6 -22
  6. package/akanApp/BackendImportGraph.test.ts +8 -22
  7. package/akanApp/BackendImportGraph.ts +1 -20
  8. package/akanApp/akanApp.host.test.ts +1 -3
  9. package/akanApp/akanApp.host.ts +82 -254
  10. package/akanApp/devHostPolicy.test.ts +10 -34
  11. package/akanApp/devHostPolicy.ts +12 -98
  12. package/akanConfig/akanConfig.test.ts +85 -69
  13. package/akanConfig/akanConfig.ts +69 -153
  14. package/akanConfig/types.ts +1 -0
  15. package/akanContext.ts +106 -159
  16. package/akanMcpContract.ts +28 -73
  17. package/appSelectionMemory.ts +1 -4
  18. package/applicationBuildReporter.ts +37 -5
  19. package/applicationBuildRunner.test.ts +5 -3
  20. package/applicationBuildRunner.ts +54 -61
  21. package/applicationReleasePackager.ts +24 -44
  22. package/applicationTestPreload.ts +3 -12
  23. package/artifact/implicitRootLayout.test.ts +9 -30
  24. package/artifact/implicitRootLayout.ts +34 -106
  25. package/artifact/routeSeedIndex.test.ts +7 -6
  26. package/artifact/routeSeedIndex.ts +11 -44
  27. package/biome.base.json +6 -1
  28. package/biomeBase.ts +2 -5
  29. package/capacitorApp.test.ts +32 -16
  30. package/capacitorApp.ts +110 -238
  31. package/cloud/cloudApi.test.ts +202 -0
  32. package/cloud/cloudApi.ts +77 -39
  33. package/cloud/configLock.test.ts +64 -0
  34. package/cloud/configLock.ts +96 -0
  35. package/cloud/constants.ts +26 -5
  36. package/cloud/globalConfig.ts +70 -32
  37. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  38. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  39. package/codeAgent/agent/CodeAgent.ts +551 -0
  40. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  41. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  42. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  43. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  44. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  45. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  46. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  47. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  48. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  49. package/codeAgent/agent/CodeMailbox.ts +148 -0
  50. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  51. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  52. package/codeAgent/agent/SubagentPool.ts +214 -0
  53. package/codeAgent/agent/akanCodeModel.ts +47 -0
  54. package/codeAgent/agent/akanCodePaths.ts +31 -0
  55. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  56. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  57. package/codeAgent/agent/codeAgent.test.ts +178 -0
  58. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  59. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  60. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  61. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  62. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  63. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  64. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  65. package/codeAgent/agent/consoleToStderr.ts +8 -0
  66. package/codeAgent/agent/jsonLines.ts +29 -0
  67. package/codeAgent/agent/stringArg.ts +5 -0
  68. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  69. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  70. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  71. package/codeAgent/feedback/PreviewView.ts +95 -0
  72. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  73. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  74. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  75. package/codeAgent/index.ts +34 -0
  76. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  77. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  78. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  79. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  80. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  81. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  82. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  83. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  84. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  85. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  86. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  87. package/codeAgent/tools/AkanCli.ts +50 -0
  88. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  89. package/codeAgent/tools/AkanEditScope.ts +48 -0
  90. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  91. package/codeAgent/tools/AkanToolPack.ts +92 -0
  92. package/codeAgent/tools/AkanVerifier.ts +48 -0
  93. package/codeAgent/tools/AskToolPack.ts +99 -0
  94. package/codeAgent/tools/MailToolPack.ts +76 -0
  95. package/codeAgent/tools/McpClient.ts +187 -0
  96. package/codeAgent/tools/McpOAuth.ts +233 -0
  97. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  98. package/codeAgent/tools/McpServerConfig.ts +129 -0
  99. package/codeAgent/tools/McpSignIn.ts +158 -0
  100. package/codeAgent/tools/McpTokenStore.ts +81 -0
  101. package/codeAgent/tools/McpToolPack.ts +199 -0
  102. package/codeAgent/tools/SessionToolPack.ts +91 -0
  103. package/codeAgent/tools/WebToolPack.ts +147 -0
  104. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  105. package/codeAgent/tools/askToolPack.test.ts +116 -0
  106. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  107. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  108. package/codegenLock.test.ts +4 -13
  109. package/codegenLock.ts +3 -17
  110. package/commandDecorators/argMeta.ts +1 -5
  111. package/commandDecorators/camelToKebabCase.ts +1 -0
  112. package/commandDecorators/command.ts +46 -102
  113. package/commandDecorators/commandBuilder.ts +30 -61
  114. package/commandDecorators/commandDecorators.test.ts +2 -10
  115. package/commandDecorators/dependencyBuilder.ts +10 -1
  116. package/commandDecorators/helpFormatter.ts +68 -145
  117. package/commandDecorators/index.ts +0 -2
  118. package/commandDecorators/targetMeta.ts +9 -7
  119. package/createTunnel.ts +7 -14
  120. package/dependencyScanner.test.ts +4 -16
  121. package/dependencyScanner.ts +31 -80
  122. package/devkitUtils.test.ts +48 -20
  123. package/executors.linkFallback.test.ts +103 -0
  124. package/executors.test.ts +132 -106
  125. package/executors.ts +194 -484
  126. package/fileEditor.ts +9 -37
  127. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  128. package/fileSys.ts +6 -8
  129. package/formSetterScanner.ts +3 -16
  130. package/frontendBuild/allRoutesBuilder.ts +7 -21
  131. package/frontendBuild/autoImportSync.test.ts +27 -38
  132. package/frontendBuild/autoImportSync.ts +31 -65
  133. package/frontendBuild/buildRouteClient.test.ts +62 -37
  134. package/frontendBuild/bundleDefine.ts +12 -0
  135. package/frontendBuild/clientBuildTypes.ts +5 -36
  136. package/frontendBuild/clientEntriesBundler.ts +9 -40
  137. package/frontendBuild/clientEntryDiscovery.ts +6 -41
  138. package/frontendBuild/csrArtifactBuilder.ts +6 -25
  139. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  140. package/frontendBuild/cssCandidateCache.ts +6 -41
  141. package/frontendBuild/cssCompiler.ts +29 -78
  142. package/frontendBuild/cssImportResolver.ts +6 -19
  143. package/frontendBuild/devChangePlanner.ts +7 -10
  144. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  145. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  146. package/frontendBuild/fontOptimizer.test.ts +6 -14
  147. package/frontendBuild/fontOptimizer.ts +15 -72
  148. package/frontendBuild/fontPruner.test.ts +5 -10
  149. package/frontendBuild/fontPruner.ts +5 -20
  150. package/frontendBuild/frontendBuild.test.ts +46 -69
  151. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  152. package/frontendBuild/hmrWatcher.test.ts +8 -32
  153. package/frontendBuild/hmrWatcher.ts +14 -71
  154. package/frontendBuild/pagesBundleBuilder.ts +4 -29
  155. package/frontendBuild/precompressArtifacts.ts +5 -18
  156. package/frontendBuild/routeClientBuilder.ts +23 -46
  157. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  158. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  159. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  160. package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
  161. package/frontendBuild/styleContract.ts +1 -8
  162. package/frontendBuild/themeValidator.ts +4 -26
  163. package/frontendBuild/watchRootResolver.test.ts +23 -42
  164. package/frontendBuild/watchRootResolver.ts +5 -27
  165. package/getRelatedCnsts.ts +12 -92
  166. package/incrementalBuilder/buildBatch.proc.ts +22 -57
  167. package/incrementalBuilder/buildBatchProtocol.ts +8 -36
  168. package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
  169. package/incrementalBuilder/buildBatchRunner.ts +10 -31
  170. package/incrementalBuilder/builderChannel.test.ts +24 -49
  171. package/incrementalBuilder/builderChannel.ts +6 -37
  172. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  173. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  174. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  175. package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
  176. package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
  177. package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
  178. package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
  179. package/index.ts +5 -17
  180. package/integration/devResourceProbe.ts +50 -56
  181. package/integration/devStability.integration.test.ts +47 -194
  182. package/integration/devStabilityHarness.test.ts +13 -28
  183. package/integration/devStabilityHarness.ts +65 -330
  184. package/integration/ssrMemoryProbe.ts +41 -167
  185. package/libSource.test.ts +4 -15
  186. package/libSource.ts +19 -31
  187. package/lint/gritRules.test.ts +8 -24
  188. package/lint/no-deep-internal-import.grit +2 -2
  189. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  190. package/linter.test.ts +1 -2
  191. package/linter.ts +26 -116
  192. package/mobile/mobileTarget.ts +14 -13
  193. package/package.json +10 -5
  194. package/packageExports.test.ts +3 -18
  195. package/packageExportsMap.ts +3 -19
  196. package/piDependencyPins.test.ts +46 -0
  197. package/platformTest/BunTestReport.test.ts +60 -0
  198. package/platformTest/BunTestReport.ts +65 -0
  199. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  200. package/platformTest/IdleWatchedProcess.ts +73 -0
  201. package/platformTest/LinuxDockerTarget.ts +105 -0
  202. package/platformTest/PlatformTestRun.test.ts +64 -0
  203. package/platformTest/PlatformTestRun.ts +141 -0
  204. package/platformTest/PlatformTestTarget.ts +142 -0
  205. package/platformTest/TestSnapshot.test.ts +72 -0
  206. package/platformTest/TestSnapshot.ts +103 -0
  207. package/platformTest/WindowsSshTarget.ts +151 -0
  208. package/prompter.ts +3 -13
  209. package/qualityScanner.test.ts +9 -26
  210. package/qualityScanner.ts +197 -285
  211. package/recipeScanner.test.ts +7 -12
  212. package/recipeScanner.ts +9 -38
  213. package/repoIdentity.ts +2 -9
  214. package/routeSourceValidator.test.ts +0 -1
  215. package/routeSourceValidator.ts +7 -42
  216. package/scanInfo.ts +87 -216
  217. package/selectModel.ts +1 -2
  218. package/semver.ts +3 -5
  219. package/slicePlanner.test.ts +6 -24
  220. package/slicePlanner.ts +11 -32
  221. package/spinner.test.ts +3 -8
  222. package/spinner.ts +4 -16
  223. package/sshTunnel.ts +1 -6
  224. package/ssrScanner.test.ts +1 -6
  225. package/ssrScanner.ts +103 -113
  226. package/stripAnsi.ts +3 -0
  227. package/subspace.test.ts +30 -25
  228. package/subspace.ts +81 -115
  229. package/subspaceConfig.ts +11 -9
  230. package/testHelpers.ts +194 -0
  231. package/transforms/asyncDefaultExportDetector.ts +3 -11
  232. package/transforms/barrelAnalyzer.ts +43 -135
  233. package/transforms/barrelImportsPlugin.ts +38 -175
  234. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  235. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  236. package/transforms/moduleSyntax.ts +32 -0
  237. package/transforms/rscUseClientTransform.ts +35 -20
  238. package/transforms/transforms.test.ts +153 -44
  239. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  240. package/transforms/useClientBundlePlugin.ts +2 -22
  241. package/typeChecker.ts +7 -63
  242. package/types.ts +1 -0
  243. package/uploadRelease.ts +10 -25
  244. package/workflow/artifacts.ts +18 -0
  245. package/workflow/executor.test.ts +1 -12
  246. package/workflow/executor.ts +101 -110
  247. package/workflow/index.ts +0 -1
  248. package/workflow/moduleIndex.test.ts +4 -15
  249. package/workflow/moduleIndex.ts +9 -49
  250. package/workflow/plan.ts +18 -25
  251. package/workflow/render.ts +80 -96
  252. package/workflow/rolloutGate.test.ts +1 -1
  253. package/workflow/source.ts +52 -69
  254. package/workflow/sourceAst.ts +50 -0
  255. package/workspaceLayout.ts +4 -19
  256. package/abstractCompactor.ts +0 -112
  257. package/aiEditor.test.ts +0 -60
  258. package/aiEditor.ts +0 -387
  259. package/commandDecorators/commandMeta.ts +0 -7
  260. package/commandDecorators/types.ts +0 -10
  261. package/getDirname.ts +0 -4
  262. package/guideline.ts +0 -19
  263. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  264. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  265. package/workflow/primitive.ts +0 -59
@@ -0,0 +1,103 @@
1
+ import { mkdir, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export interface SnapshotDrift {
5
+ changed: string[];
6
+ added: string[];
7
+ removed: string[];
8
+ }
9
+
10
+ //? Per-file hashes are kept so the tree is re-checked before publishing: a run gates only if nothing changed under it.
11
+ export class TestSnapshot {
12
+ static readonly #secretPatterns = [
13
+ /(^|\/)\.env(\.(?!(template|example|sample)$)[^/]+)?$/,
14
+ /(^|\/)env\/env\.(client|server)\.(?!(type|example)\.ts$)[^/]+\.ts$/,
15
+ ];
16
+
17
+ readonly root: string;
18
+ readonly tarPath: string;
19
+ readonly digest: string;
20
+ readonly #hashes: Map<string, string>;
21
+
22
+ constructor(root: string, tarPath: string, hashes: Map<string, string>) {
23
+ this.root = root;
24
+ this.tarPath = tarPath;
25
+ this.#hashes = hashes;
26
+ this.digest = TestSnapshot.#digestOf(hashes);
27
+ }
28
+
29
+ get fileCount() {
30
+ return this.#hashes.size;
31
+ }
32
+
33
+ static async create(root: string, outDir: string) {
34
+ const hashes = await TestSnapshot.#hashTree(root);
35
+ //? A tracked file is already in the repository; only an untracked one nobody ignored can leak by being copied.
36
+ const untracked = await TestSnapshot.#gitList(root, ["--others", "--exclude-standard"]);
37
+ const secrets = untracked.filter((file) => TestSnapshot.#secretPatterns.some((re) => re.test(file)));
38
+ if (secrets.length)
39
+ throw new Error(
40
+ `Refusing to snapshot env files git does not ignore — they would be copied to every test target: ${secrets.join(", ")}`,
41
+ );
42
+ await mkdir(outDir, { recursive: true });
43
+ const listPath = path.join(outDir, "files.txt");
44
+ const tarPath = path.join(outDir, "src.tar");
45
+ await writeFile(listPath, `${[...hashes.keys()].join("\n")}\n`);
46
+ const tar = Bun.spawn(
47
+ ["tar", ...(process.platform === "darwin" ? ["--no-xattrs"] : []), "-cf", tarPath, "-T", listPath],
48
+ { cwd: root, env: { ...process.env, COPYFILE_DISABLE: "1" }, stdout: "ignore", stderr: "pipe" },
49
+ );
50
+ if ((await tar.exited) !== 0) throw new Error(`tar failed: ${await new Response(tar.stderr).text()}`);
51
+ return new TestSnapshot(root, tarPath, hashes);
52
+ }
53
+
54
+ async drift(): Promise<SnapshotDrift> {
55
+ const current = await TestSnapshot.#hashTree(this.root);
56
+ const changed = [...current].filter(([file, hash]) => this.#hashes.has(file) && this.#hashes.get(file) !== hash);
57
+ return {
58
+ changed: changed.map(([file]) => file),
59
+ added: [...current.keys()].filter((file) => !this.#hashes.has(file)),
60
+ removed: [...this.#hashes.keys()].filter((file) => !current.has(file)),
61
+ };
62
+ }
63
+
64
+ static async #gitList(root: string, args: string[]) {
65
+ const proc = Bun.spawn(["git", "ls-files", ...args, "-z"], { cwd: root, stdout: "pipe", stderr: "pipe" });
66
+ const [stdout, stderr, exitCode] = await Promise.all([
67
+ new Response(proc.stdout).text(),
68
+ new Response(proc.stderr).text(),
69
+ proc.exited,
70
+ ]);
71
+ if (exitCode !== 0) throw new Error(`git ls-files failed in ${root}: ${stderr.trim()}`);
72
+ return [...new Set(stdout.split("\0").filter(Boolean))].sort();
73
+ }
74
+
75
+ static async #listFiles(root: string) {
76
+ const listed = await TestSnapshot.#gitList(root, ["--cached", "--others", "--exclude-standard"]);
77
+ //? `--cached` still lists a tracked file deleted from the working tree, which tar would then fail on.
78
+ const present = await Promise.all(
79
+ listed.map(async (file) => ((await stat(path.join(root, file)).catch(() => null))?.isFile() ? file : null)),
80
+ );
81
+ return present.filter((file): file is string => file !== null);
82
+ }
83
+
84
+ static async #hashTree(root: string) {
85
+ const files = await TestSnapshot.#listFiles(root);
86
+ const hashes = new Map<string, string>();
87
+ for (let start = 0; start < files.length; start += 64) {
88
+ const batch = files.slice(start, start + 64);
89
+ const batchHashes = await Promise.all(
90
+ batch.map(async (file) => Bun.hash(await Bun.file(path.join(root, file)).bytes()).toString(16)),
91
+ );
92
+ for (const [idx, file] of batch.entries()) hashes.set(file, batchHashes[idx] ?? "");
93
+ }
94
+ return hashes;
95
+ }
96
+
97
+ static #digestOf(hashes: Map<string, string>) {
98
+ return Bun.hash([...hashes].map(([file, hash]) => `${file}:${hash}`).join("\n"))
99
+ .toString(16)
100
+ .padStart(16, "0")
101
+ .slice(0, 12);
102
+ }
103
+ }
@@ -0,0 +1,151 @@
1
+ import { existsSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import type { PlatformGatePolicy, WindowsTestTargetConfig } from "../cloud/constants";
5
+ import { IdleWatchedProcess } from "./IdleWatchedProcess";
6
+ import { type PlatformRunContext, PlatformTestTarget } from "./PlatformTestTarget";
7
+
8
+ export class WindowsSshTarget extends PlatformTestTarget {
9
+ static readonly utmctlPath = "/Applications/UTM.app/Contents/MacOS/utmctl";
10
+ static readonly defaultWorkRoot = "C:\\akan-test";
11
+ //* arm64 has neither a sharp@0.32 prebuilt nor a Chrome for puppeteer's postinstall; x64 users get both.
12
+ static readonly defaultTolerateScriptFailures = ["sharp", "puppeteer"];
13
+ static readonly #runDirPattern = "^\\d{8}-\\d{6}$";
14
+ static readonly #keptRuns = 2;
15
+
16
+ readonly platform = "windows";
17
+ readonly policy: PlatformGatePolicy;
18
+ protected override readonly tolerateScriptFailures: string[];
19
+ readonly #config: WindowsTestTargetConfig;
20
+ #host: string;
21
+ #runDir: string | null = null;
22
+
23
+ constructor(config: WindowsTestTargetConfig) {
24
+ super();
25
+ this.#config = config;
26
+ this.#host = process.env.AKAN_TEST_WINDOWS_HOST ?? config.host;
27
+ this.policy = config.policy ?? "warn";
28
+ this.tolerateScriptFailures = config.tolerateScriptFailures ?? WindowsSshTarget.defaultTolerateScriptFailures;
29
+ }
30
+
31
+ get #workRoot() {
32
+ return this.#config.workRoot ?? WindowsSshTarget.defaultWorkRoot;
33
+ }
34
+
35
+ protected async prepare(context: PlatformRunContext) {
36
+ await this.#connect();
37
+ const bunVersion = (await this.#ssh("bun --version")).stdout.trim();
38
+ if (bunVersion !== Bun.version)
39
+ throw new Error(
40
+ `the Windows host runs Bun ${bunVersion || "(none)"}, this host ${Bun.version} — align them first`,
41
+ );
42
+ if ((await this.#ssh("git --version")).exitCode !== 0) throw new Error("git is not installed on the Windows host");
43
+ const runDir = `${this.#workRoot}\\${context.runId}`;
44
+ const mkdir = await this.#ssh(`New-Item -ItemType Directory -Force -Path '${runDir}\\w' | Out-Null`);
45
+ if (mkdir.exitCode !== 0) throw new Error(`could not create ${runDir}: ${mkdir.stderr.trim()}`);
46
+ this.#runDir = runDir;
47
+ for (const [local, remoteName] of [
48
+ [context.snapshot.tarPath, "src.tar"],
49
+ [context.envPath, "env"],
50
+ ] as const) {
51
+ const copied = await WindowsSshTarget.spawnText([
52
+ ...this.#scpBase(),
53
+ local,
54
+ this.#remoteScpPath(`${runDir}\\${remoteName}`),
55
+ ]);
56
+ if (copied.exitCode !== 0) throw new Error(`scp ${remoteName} failed: ${copied.stderr.trim()}`);
57
+ }
58
+ const extract = await this.exec(
59
+ "tar -xf ..\\src.tar && copy /Y ..\\env .env >nul && git init -q",
60
+ path.join(context.logDir, this.platform, "extract.log"),
61
+ PlatformTestTarget.installIdleMs,
62
+ );
63
+ if (extract.exitCode !== 0) throw new Error("extracting the snapshot on the Windows host failed");
64
+ }
65
+
66
+ protected async exec(command: string, logPath: string, idleMs: number) {
67
+ const runDir = this.#runDir;
68
+ if (!runDir) throw new Error("the Windows run directory was not prepared");
69
+ let remotePid: string | null = null;
70
+ const script = [
71
+ "$ProgressPreference = 'SilentlyContinue'",
72
+ "[Console]::OutputEncoding = [Text.Encoding]::UTF8",
73
+ "'PID=' + $PID",
74
+ ...Object.entries(PlatformTestTarget.gitIdentityEnv).map(([key, value]) => `$env:${key} = '${value}'`),
75
+ `Set-Location -LiteralPath '${runDir}\\w'`,
76
+ `cmd /c "${command} 2>&1"`,
77
+ "exit $LASTEXITCODE",
78
+ ].join("; ");
79
+ return await new IdleWatchedProcess([...this.#sshBase(), script], {
80
+ logPath,
81
+ idleMs,
82
+ onOutput: (text) => {
83
+ remotePid ??= /PID=(\d+)/.exec(text)?.[1] ?? null;
84
+ },
85
+ onIdle: async () => {
86
+ if (remotePid) await this.#ssh(`taskkill /F /T /PID ${remotePid} | Out-Null`);
87
+ },
88
+ }).run();
89
+ }
90
+
91
+ protected async cleanup() {
92
+ this.#runDir = null;
93
+ await this.#ssh(
94
+ [
95
+ `Get-ChildItem -LiteralPath '${this.#workRoot}' -Directory`,
96
+ `Where-Object { $_.Name -match '${WindowsSshTarget.#runDirPattern}' }`,
97
+ "Sort-Object Name -Descending",
98
+ `Select-Object -Skip ${WindowsSshTarget.#keptRuns}`,
99
+ "ForEach-Object { cmd /c rmdir /s /q $_.FullName }",
100
+ ].join(" | "),
101
+ );
102
+ }
103
+
104
+ async #connect() {
105
+ if ((await this.#ssh("exit 0")).exitCode === 0) return;
106
+ const vm = this.#config.utmVm;
107
+ if (!vm || !existsSync(WindowsSshTarget.utmctlPath))
108
+ throw new Error(`cannot reach ${this.#config.user}@${this.#host} over ssh`);
109
+ const status = (await WindowsSshTarget.spawnText([WindowsSshTarget.utmctlPath, "status", vm])).stdout.trim();
110
+ if (status !== "started") await WindowsSshTarget.spawnText([WindowsSshTarget.utmctlPath, "start", vm]);
111
+ for (let attempt = 0; attempt < 24; attempt++) {
112
+ const addresses = (await WindowsSshTarget.spawnText([WindowsSshTarget.utmctlPath, "ip-address", vm])).stdout;
113
+ const ipv4 = addresses.split(/\s+/).find((address) => /^\d+\.\d+\.\d+\.\d+$/.test(address));
114
+ if (ipv4) this.#host = ipv4;
115
+ if ((await this.#ssh("exit 0")).exitCode === 0) return;
116
+ await Bun.sleep(5_000);
117
+ }
118
+ throw new Error(`UTM VM "${vm}" did not answer ssh at ${this.#host}`);
119
+ }
120
+
121
+ #sshOptions() {
122
+ const expand = (file: string) => (file.startsWith("~/") ? path.join(os.homedir(), file.slice(2)) : file);
123
+ return [
124
+ "-i",
125
+ expand(this.#config.identityFile),
126
+ ...(this.#config.knownHostsFile ? ["-o", `UserKnownHostsFile=${expand(this.#config.knownHostsFile)}`] : []),
127
+ "-o",
128
+ "BatchMode=yes",
129
+ "-o",
130
+ "ConnectTimeout=8",
131
+ "-o",
132
+ "ServerAliveInterval=30",
133
+ ];
134
+ }
135
+
136
+ #sshBase() {
137
+ return ["ssh", ...this.#sshOptions(), `${this.#config.user}@${this.#host}`];
138
+ }
139
+
140
+ #scpBase() {
141
+ return ["scp", "-q", ...this.#sshOptions()];
142
+ }
143
+
144
+ #remoteScpPath(windowsPath: string) {
145
+ return `${this.#config.user}@${this.#host}:/${windowsPath.replaceAll("\\", "/")}`;
146
+ }
147
+
148
+ async #ssh(script: string) {
149
+ return await WindowsSshTarget.spawnText([...this.#sshBase(), script]);
150
+ }
151
+ }
package/prompter.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import fsPromise from "node:fs/promises";
2
2
  import { input, select } from "@inquirer/prompts";
3
3
 
4
- import { getDirname } from "./getDirname";
5
- import type { GuideGenerateJson } from "./guideline";
4
+ import { getDirname } from "./fileSys";
6
5
 
7
6
  interface FileUpdateRequestProps {
8
7
  context: string;
@@ -34,17 +33,8 @@ 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
- /**
44
- * The name is checked against the directory listing rather than interpolated straight into a path:
45
- * it arrives from an MCP tool argument, and an unknown one has to name the valid set instead of
46
- * surfacing an ENOENT that carries the host's own paths.
47
- */
36
+ // Checked against the listing, not interpolated into a path: the name is an MCP tool argument, and an unknown one
37
+ // must not surface an ENOENT carrying the host's paths.
48
38
  static async getInstruction(guideName: string): Promise<string> {
49
39
  const guideNames = await Prompter.listGuidelines();
50
40
  if (!guideNames.includes(guideName))
@@ -1,32 +1,23 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
1
+ import { describe, expect, test } from "bun:test";
4
2
  import path from "node:path";
5
3
  import { AbstractDoc } from "./abstractDoc";
6
4
  import { AkanQualityScanner, type QualityScanResult } from "./qualityScanner";
5
+ import { tempDirs, writeText } from "./testHelpers";
7
6
 
8
- const tempRoots: string[] = [];
7
+ const makeTempRoot = tempDirs("akan-quality-scanner-");
9
8
 
10
9
  const makeWorkspace = async (files: Record<string, string>) => {
11
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-quality-scanner-"));
12
- tempRoots.push(root);
13
- for (const [filePath, content] of Object.entries({ ".gitignore": "node_modules\n", ...files })) {
14
- const absolutePath = path.join(root, filePath);
15
- await mkdir(path.dirname(absolutePath), { recursive: true });
16
- await writeFile(absolutePath, content);
17
- }
10
+ const root = await makeTempRoot();
11
+ for (const [filePath, content] of Object.entries({ ".gitignore": "node_modules\n", ...files }))
12
+ await writeText(path.join(root, filePath), content);
18
13
  return root;
19
14
  };
20
15
 
21
16
  const abstractOf = (lineNum: number) =>
22
17
  ["# post Abstract", ...Array.from({ length: lineNum - 1 }, (_, idx) => `- rule ${idx}`)].join("\n");
23
18
 
24
- afterEach(async () => {
25
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
26
- });
27
-
28
19
  describe("AkanQualityScanner abstract rule", () => {
29
- test("warns on an abstract over the line limit and points at akan compact", async () => {
20
+ test("warns on an abstract over the line limit and says how to trim it", async () => {
30
21
  const root = await makeWorkspace({
31
22
  "apps/demo/lib/post/post.abstract.md": abstractOf(AbstractDoc.maxLines + 1),
32
23
  "apps/demo/lib/post/post.constant.ts": "export class Post {}\n",
@@ -41,7 +32,8 @@ describe("AkanQualityScanner abstract rule", () => {
41
32
  expect(warnings).toHaveLength(1);
42
33
  expect(warnings[0]?.file).toBe("apps/demo/lib/post/post.abstract.md");
43
34
  expect(warnings[0]?.message).toContain(`${AbstractDoc.maxLines + 1} lines`);
44
- expect(warnings[0]?.fix).toContain("akan compact");
35
+ expect(warnings[0]?.fix).toContain("## Rules");
36
+ expect(warnings[0]?.fix).not.toContain("akan compact");
45
37
  });
46
38
  });
47
39
 
@@ -184,15 +176,6 @@ describe("AkanQualityScanner ssr rules", () => {
184
176
  });
185
177
  });
186
178
 
187
- const signalOf = (entries: string) =>
188
- [
189
- `import { endpoint, slice } from "akanjs/signal";`,
190
- `export class PostSlice extends slice(srv.post, { guards: {}, mcp: { get: true } }, (init) => ({`,
191
- entries,
192
- `})) {}`,
193
- "",
194
- ].join("\n");
195
-
196
179
  describe("AkanQualityScanner layout rules", () => {
197
180
  test("flags an unknown app root file but not a facet entrypoint", async () => {
198
181
  const root = await makeWorkspace({