@akanjs/devkit 3.0.0-beta.9 → 3.0.0
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/DEV_RUNTIME_KNOBS.md +44 -26
- package/README.ko.md +21 -7
- package/README.md +17 -8
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +6 -8
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +56 -3
- package/akanApp/akanApp.host.ts +218 -278
- package/akanApp/devBootLatch.test.ts +39 -0
- package/akanApp/devBootLatch.ts +39 -0
- package/akanApp/devHostPolicy.test.ts +81 -35
- package/akanApp/devHostPolicy.ts +41 -102
- package/akanConfig/akanBin.test.ts +62 -0
- package/akanConfig/akanBin.ts +78 -0
- package/akanConfig/akanConfig.test.ts +439 -141
- package/akanConfig/akanConfig.ts +359 -275
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +22 -8
- package/akanContext.ts +110 -160
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +41 -5
- package/applicationBuildRunner.ts +71 -63
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +32 -30
- package/artifact/implicitRootLayout.ts +58 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +35 -2
- package/biomeBase.ts +2 -5
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +77 -39
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +26 -5
- package/cloud/globalConfig.ts +71 -32
- package/codeAgent/agent/AkanCodeServices.ts +97 -0
- package/codeAgent/agent/AkanContextFiles.ts +94 -0
- package/codeAgent/agent/CodeAgent.ts +551 -0
- package/codeAgent/agent/CodeAgentAsks.ts +66 -0
- package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
- package/codeAgent/agent/CodeAgentGate.ts +93 -0
- package/codeAgent/agent/CodeAgentProxy.ts +82 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
- package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
- package/codeAgent/agent/CodeAgentUi.ts +47 -0
- package/codeAgent/agent/CodeMailbox.ts +148 -0
- package/codeAgent/agent/CodeSessionFork.ts +56 -0
- package/codeAgent/agent/CodeSessionIndex.ts +78 -0
- package/codeAgent/agent/SubagentPool.ts +214 -0
- package/codeAgent/agent/akanCodeModel.ts +47 -0
- package/codeAgent/agent/akanCodePaths.ts +31 -0
- package/codeAgent/agent/akanContextFiles.test.ts +114 -0
- package/codeAgent/agent/akanSystemPrompt.ts +46 -0
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +523 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
- package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
- package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
- package/codeAgent/agent/codeMailbox.test.ts +76 -0
- package/codeAgent/agent/codeSessionFork.test.ts +58 -0
- package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
- package/codeAgent/agent/consoleToStderr.ts +8 -0
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +82 -0
- package/codeAgent/feedback/PreviewChrome.ts +82 -0
- package/codeAgent/feedback/PreviewFeedback.ts +46 -0
- package/codeAgent/feedback/PreviewView.ts +95 -0
- package/codeAgent/feedback/TurnFeedback.ts +98 -0
- package/codeAgent/feedback/VerifyFeedback.ts +40 -0
- package/codeAgent/feedback/previewChrome.test.ts +73 -0
- package/codeAgent/index.ts +34 -0
- package/codeAgent/skills/akan-code/SKILL.md +130 -0
- package/codeAgent/skills/akan-debug/SKILL.md +42 -0
- package/codeAgent/skills/akan-deps/SKILL.md +50 -0
- package/codeAgent/skills/akan-err/SKILL.md +39 -0
- package/codeAgent/skills/akan-module/SKILL.md +45 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
- package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
- package/codeAgent/skills/akan-service/SKILL.md +34 -0
- package/codeAgent/skills/akan-store/SKILL.md +67 -0
- package/codeAgent/skills/akan-ui/SKILL.md +63 -0
- package/codeAgent/skills/akan-validate/SKILL.md +44 -0
- package/codeAgent/tools/AkanCli.ts +50 -0
- package/codeAgent/tools/AkanCodePlugins.ts +136 -0
- package/codeAgent/tools/AkanEditScope.ts +48 -0
- package/codeAgent/tools/AkanEnvKeys.ts +50 -0
- package/codeAgent/tools/AkanToolPack.ts +92 -0
- package/codeAgent/tools/AkanVerifier.ts +48 -0
- package/codeAgent/tools/AskToolPack.ts +99 -0
- package/codeAgent/tools/MailToolPack.ts +76 -0
- package/codeAgent/tools/McpClient.ts +187 -0
- package/codeAgent/tools/McpOAuth.ts +233 -0
- package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
- package/codeAgent/tools/McpServerConfig.ts +129 -0
- package/codeAgent/tools/McpSignIn.ts +158 -0
- package/codeAgent/tools/McpTokenStore.ts +81 -0
- package/codeAgent/tools/McpToolPack.ts +199 -0
- package/codeAgent/tools/SessionToolPack.ts +91 -0
- package/codeAgent/tools/WebToolPack.ts +147 -0
- package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
- package/codeAgent/tools/askToolPack.test.ts +116 -0
- package/codeAgent/tools/mcpOAuth.test.ts +612 -0
- package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +46 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +2 -10
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +68 -145
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/csrE2e/csrE2eHarness.fixture.ts +505 -0
- package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
- package/csrE2e/csrPageStack.e2e.test.ts +252 -0
- package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
- package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
- package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
- package/csrE2e/registryVendor.e2e.test.ts +221 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +31 -80
- package/devkitUtils.test.ts +48 -20
- package/executors.linkFallback.test.ts +103 -0
- package/executors.test.ts +176 -106
- package/executors.ts +212 -498
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +22 -9
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +27 -38
- package/frontendBuild/autoImportSync.ts +31 -65
- package/frontendBuild/buildRouteClient.test.ts +123 -37
- package/frontendBuild/bundleDefine.test.ts +12 -0
- package/frontendBuild/bundleDefine.ts +16 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
- package/frontendBuild/clientEntryDiscovery.ts +122 -110
- package/frontendBuild/csrArtifactBuilder.ts +9 -31
- package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
- package/frontendBuild/csrDevArtifactWriter.ts +307 -0
- package/frontendBuild/csrDevBundler.test.ts +66 -0
- package/frontendBuild/csrDevBundler.ts +95 -0
- package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
- package/frontendBuild/csrDevFactoryCheck.ts +39 -0
- package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
- package/frontendBuild/csrDevModuleCompiler.ts +422 -0
- package/frontendBuild/csrDevPatcher.test.ts +572 -0
- package/frontendBuild/csrDevPatcher.ts +548 -0
- package/frontendBuild/csrDevPaths.ts +145 -0
- package/frontendBuild/csrDevResolver.test.ts +239 -0
- package/frontendBuild/csrDevResolver.ts +363 -0
- package/frontendBuild/csrDevTypes.ts +61 -0
- package/frontendBuild/csrEntryFiles.ts +24 -0
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +31 -82
- package/frontendBuild/cssImportResolver.ts +6 -19
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/devRegistryBundler.ts +192 -0
- package/frontendBuild/fontOptimizer.test.ts +6 -14
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +57 -70
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +59 -32
- package/frontendBuild/hmrWatcher.ts +31 -74
- package/frontendBuild/index.ts +4 -0
- package/frontendBuild/pagesBundleBuilder.ts +28 -30
- package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
- package/frontendBuild/registryMetadataFingerprint.ts +28 -0
- package/frontendBuild/routeClientBuilder.ts +100 -62
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/serverGraphFile.test.ts +77 -0
- package/frontendBuild/serverGraphFile.ts +93 -0
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
- package/frontendBuild/ssrDevBundler.ts +85 -0
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +147 -63
- package/incrementalBuilder/buildBatchProtocol.ts +13 -38
- package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
- package/incrementalBuilder/buildBatchRunner.ts +26 -38
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
- package/incrementalBuilder/builderWorkQueue.ts +126 -0
- package/incrementalBuilder/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
- package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
- package/index.ts +5 -19
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +105 -199
- package/integration/devStabilityHarness.test.ts +13 -28
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
- package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
- package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
- package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
- package/lint/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-document-cookie.grit +29 -0
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/lint/no-web-only-api-outside-webkit.grit +44 -0
- package/lint/no-web-storage.grit +30 -0
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/desktopBin.test.ts +182 -0
- package/mobile/desktopBin.ts +144 -0
- package/mobile/desktopServerStage.test.ts +252 -0
- package/mobile/desktopServerStage.ts +178 -0
- package/mobile/index.ts +9 -0
- package/mobile/mobileTarget.test.ts +57 -8
- package/mobile/mobileTarget.ts +45 -26
- package/mobile/nativeApi.test.ts +30 -0
- package/mobile/nativeApi.ts +40 -0
- package/mobile/nativeApp.test.ts +184 -0
- package/mobile/nativeApp.ts +454 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +438 -0
- package/mobile/nativeConfig.ts +294 -0
- package/mobile/nativePluginFolders.test.ts +81 -0
- package/mobile/nativePluginFolders.ts +60 -0
- package/mobile/nativeWebDir.test.ts +80 -0
- package/mobile/nativeWebDir.ts +66 -0
- package/mobile/placeholderAppId.test.ts +13 -0
- package/mobile/placeholderAppId.ts +16 -0
- package/mobile/usageDescriptions.test.ts +27 -0
- package/mobile/usageDescriptions.ts +24 -0
- package/package.json +11 -11
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +46 -0
- package/platformTest/BunTestReport.test.ts +60 -0
- package/platformTest/BunTestReport.ts +65 -0
- package/platformTest/IdleWatchedProcess.test.ts +56 -0
- package/platformTest/IdleWatchedProcess.ts +73 -0
- package/platformTest/LinuxDockerTarget.ts +105 -0
- package/platformTest/PlatformTestRun.test.ts +64 -0
- package/platformTest/PlatformTestRun.ts +141 -0
- package/platformTest/PlatformTestTarget.ts +142 -0
- package/platformTest/TestSnapshot.test.ts +72 -0
- package/platformTest/TestSnapshot.ts +103 -0
- package/platformTest/WindowsSshTarget.ts +151 -0
- package/prompter.ts +3 -13
- package/qualityScanner.test.ts +9 -26
- package/qualityScanner.ts +200 -286
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.test.ts +0 -1
- package/routeSourceValidator.ts +7 -42
- package/scanInfo.ts +91 -225
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/stripAnsi.ts +3 -0
- package/subspace.test.ts +9 -25
- package/subspace.ts +48 -115
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +194 -0
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +42 -20
- package/transforms/serverEnvPlugin.test.ts +103 -0
- package/transforms/serverEnvPlugin.ts +41 -0
- package/transforms/transforms.test.ts +166 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +6 -23
- package/typeChecker.ts +7 -63
- package/types.ts +2 -0
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +101 -110
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/rolloutGate.test.ts +1 -1
- package/workflow/source.ts +52 -69
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.test.ts +18 -3
- package/workspaceLayout.ts +22 -23
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- package/applicationReleasePackager.ts +0 -209
- package/capacitorApp.test.ts +0 -372
- package/capacitorApp.ts +0 -1648
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/guideline.ts +0 -19
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { akanCodePaths } from "../agent/akanCodePaths";
|
|
6
|
+
import { McpServerConfig, type McpServerScope } from "./McpServerConfig";
|
|
7
|
+
|
|
8
|
+
const roots: string[] = [];
|
|
9
|
+
|
|
10
|
+
// Without this every add() writes into the real ~/.akan/code/mcp.json of whoever runs the suite.
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
const home = mkdtempSync(path.join(tmpdir(), "akan-home-"));
|
|
13
|
+
roots.push(home);
|
|
14
|
+
process.env.AKAN_CODE_HOME = home;
|
|
15
|
+
expect(akanCodePaths.globalMcpFile().startsWith(home)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const globalFile = (file: string) => {
|
|
19
|
+
const target = akanCodePaths.globalMcpFile();
|
|
20
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
21
|
+
writeFileSync(target, file);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const workspace = (file?: string) => {
|
|
25
|
+
const root = mkdtempSync(path.join(tmpdir(), "akan-mcp-"));
|
|
26
|
+
roots.push(root);
|
|
27
|
+
if (file === undefined) return root;
|
|
28
|
+
const target = McpServerConfig.file(root);
|
|
29
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
30
|
+
writeFileSync(target, file);
|
|
31
|
+
return root;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const written = (root: string) =>
|
|
35
|
+
JSON.parse(readFileSync(McpServerConfig.file(root), "utf8")) as Record<string, object>;
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
rmSync(akanCodePaths.globalMcpFile(), { force: true });
|
|
39
|
+
for (const root of roots.slice(1)) rmSync(root, { recursive: true, force: true });
|
|
40
|
+
roots.splice(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("MCP server config", () => {
|
|
44
|
+
test("an editor's own block is read as it is written", () => {
|
|
45
|
+
const root = workspace(
|
|
46
|
+
JSON.stringify({
|
|
47
|
+
mcpServers: {
|
|
48
|
+
github: { command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] },
|
|
49
|
+
linear: { url: "https://mcp.linear.app/sse" },
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
expect(McpServerConfig.read(root)).toEqual([
|
|
54
|
+
{ name: "github", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"] },
|
|
55
|
+
{ name: "linear", transport: "http", url: "https://mcp.linear.app/sse" },
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("a static token and a hand-issued oauth client travel from the file", () => {
|
|
60
|
+
const root = workspace(
|
|
61
|
+
JSON.stringify({
|
|
62
|
+
mcpServers: {
|
|
63
|
+
sentry: {
|
|
64
|
+
url: "https://mcp.sentry.dev/mcp",
|
|
65
|
+
headers: { "x-api-key": "from-the-file" },
|
|
66
|
+
oauth: { clientId: "abc", scope: "project:read" },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
expect(McpServerConfig.read(root)[0]).toMatchObject({
|
|
72
|
+
headers: { "x-api-key": "from-the-file" },
|
|
73
|
+
oauth: { clientId: "abc", scope: "project:read" },
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("a disabled server is declared but never connected", () => {
|
|
78
|
+
const root = workspace(JSON.stringify({ mcpServers: { off: { command: "x", disabled: true } } }));
|
|
79
|
+
expect(McpServerConfig.read(root)).toEqual([]);
|
|
80
|
+
expect(McpServerConfig.declared(root)).toEqual([
|
|
81
|
+
{ ref: { name: "off", transport: "stdio", command: "x" }, disabled: true, scope: "workspace" },
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("a url is an http server and anything else is a command with its argv", () => {
|
|
86
|
+
const root = workspace();
|
|
87
|
+
expect(McpServerConfig.add(root, "linear", ["https://mcp.linear.app/sse"], "workspace")).toEqual({
|
|
88
|
+
name: "linear",
|
|
89
|
+
transport: "http",
|
|
90
|
+
url: "https://mcp.linear.app/sse",
|
|
91
|
+
});
|
|
92
|
+
expect(McpServerConfig.add(root, "github", ["npx", "-y", "server-github"], "workspace")).toEqual({
|
|
93
|
+
name: "github",
|
|
94
|
+
transport: "stdio",
|
|
95
|
+
command: "npx",
|
|
96
|
+
args: ["-y", "server-github"],
|
|
97
|
+
});
|
|
98
|
+
expect(Object.keys(written(root).mcpServers ?? {})).toEqual(["linear", "github"]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("a VS Code file keeps its own spelling through a write", () => {
|
|
102
|
+
const root = workspace(JSON.stringify({ servers: { a: { command: "a" } } }));
|
|
103
|
+
McpServerConfig.add(root, "b", ["b"], "workspace");
|
|
104
|
+
const file = written(root);
|
|
105
|
+
expect(Object.keys(file)).toEqual(["servers"]);
|
|
106
|
+
expect(Object.keys(file.servers ?? {})).toEqual(["a", "b"]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("adding the same name twice replaces the entry rather than doubling it", () => {
|
|
110
|
+
const root = workspace();
|
|
111
|
+
McpServerConfig.add(root, "a", ["old"], "workspace");
|
|
112
|
+
McpServerConfig.add(root, "a", ["new", "--flag"], "workspace");
|
|
113
|
+
expect(McpServerConfig.read(root)).toEqual([{ name: "a", transport: "stdio", command: "new", args: ["--flag"] }]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("removing says whether it removed anything", () => {
|
|
117
|
+
const root = workspace(JSON.stringify({ mcpServers: { a: { command: "a" } } }));
|
|
118
|
+
expect(McpServerConfig.remove(root, "nope")).toBe(false);
|
|
119
|
+
expect(McpServerConfig.remove(root, "a")).toBe("workspace");
|
|
120
|
+
expect(McpServerConfig.read(root)).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("a server with nothing to reach it by is refused", () => {
|
|
124
|
+
expect(() => McpServerConfig.add(workspace(), "a", [], "workspace")).toThrow();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("a malformed file costs its servers and is named rather than thrown", () => {
|
|
128
|
+
const root = workspace("{ not json");
|
|
129
|
+
expect(McpServerConfig.read(root)).toEqual([]);
|
|
130
|
+
expect(McpServerConfig.problem(root)).toContain("not valid JSON");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("no file at all is not a problem", () => {
|
|
134
|
+
const root = workspace();
|
|
135
|
+
expect(McpServerConfig.read(root)).toEqual([]);
|
|
136
|
+
expect(McpServerConfig.problem(root)).toBeUndefined();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("a server declared in the home file is read from a workspace that declares nothing", () => {
|
|
140
|
+
globalFile(JSON.stringify({ mcpServers: { office: { url: "https://office.example/mcp" } } }));
|
|
141
|
+
const root = workspace();
|
|
142
|
+
expect(McpServerConfig.read(root)).toEqual([
|
|
143
|
+
{ name: "office", transport: "http", url: "https://office.example/mcp" },
|
|
144
|
+
]);
|
|
145
|
+
expect(McpServerConfig.declared(root)[0]?.scope).toBe("global");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("both files are merged, and the workspace wins the name they share", () => {
|
|
149
|
+
globalFile(JSON.stringify({ mcpServers: { office: { url: "https://home/mcp" }, shared: { command: "home" } } }));
|
|
150
|
+
const root = workspace(JSON.stringify({ mcpServers: { shared: { command: "repo" } } }));
|
|
151
|
+
expect(McpServerConfig.declared(root).map((entry) => [entry.ref.name, entry.scope])).toEqual([
|
|
152
|
+
["office", "global"],
|
|
153
|
+
["shared", "workspace"],
|
|
154
|
+
]);
|
|
155
|
+
expect(McpServerConfig.read(root).find((ref) => ref.name === "shared")?.command).toBe("repo");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("a write goes to the scope it was asked for, and leaves the other file alone", () => {
|
|
159
|
+
const root = workspace();
|
|
160
|
+
McpServerConfig.add(root, "office", ["https://office.example/mcp"]);
|
|
161
|
+
expect(McpServerConfig.declared(root)).toEqual([
|
|
162
|
+
{
|
|
163
|
+
ref: { name: "office", transport: "http", url: "https://office.example/mcp" },
|
|
164
|
+
disabled: false,
|
|
165
|
+
scope: "global",
|
|
166
|
+
},
|
|
167
|
+
]);
|
|
168
|
+
expect(existsSync(McpServerConfig.file(root, "workspace"))).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("removing finds the file the server is actually in", () => {
|
|
172
|
+
globalFile(JSON.stringify({ mcpServers: { office: { command: "x" } } }));
|
|
173
|
+
const root = workspace(JSON.stringify({ mcpServers: { repobot: { command: "y" } } }));
|
|
174
|
+
expect(McpServerConfig.remove(root, "office")).toBe("global");
|
|
175
|
+
expect(McpServerConfig.remove(root, "repobot")).toBe("workspace");
|
|
176
|
+
expect(McpServerConfig.read(root)).toEqual([]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("removing a name both files declare takes the workspace's, and the home file's then applies", () => {
|
|
180
|
+
globalFile(JSON.stringify({ mcpServers: { shared: { command: "home" } } }));
|
|
181
|
+
const root = workspace(JSON.stringify({ mcpServers: { shared: { command: "repo" } } }));
|
|
182
|
+
expect(McpServerConfig.remove(root, "shared")).toBe("workspace");
|
|
183
|
+
expect(McpServerConfig.declared(root)).toEqual([
|
|
184
|
+
{ ref: { name: "shared", transport: "stdio", command: "home" }, disabled: false, scope: "global" },
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("a remove that names a scope touches only that file", () => {
|
|
189
|
+
globalFile(JSON.stringify({ mcpServers: { office: { command: "x" }, shared: { command: "home" } } }));
|
|
190
|
+
const root = workspace(JSON.stringify({ mcpServers: { shared: { command: "repo" } } }));
|
|
191
|
+
expect(McpServerConfig.remove(root, "office", "workspace")).toBe(false);
|
|
192
|
+
expect(McpServerConfig.remove(root, "shared", "global")).toBe("global");
|
|
193
|
+
expect(McpServerConfig.declared(root).map((entry) => [entry.ref.name, entry.scope])).toEqual([
|
|
194
|
+
["office", "global"],
|
|
195
|
+
["shared", "workspace"],
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("a scope is what a caller names, never guessed from the shape", () => {
|
|
200
|
+
const root = workspace();
|
|
201
|
+
const scopes: McpServerScope[] = ["global", "workspace"];
|
|
202
|
+
for (const scope of scopes) McpServerConfig.add(root, `in-${scope}`, ["x"], scope);
|
|
203
|
+
expect(McpServerConfig.declared(root).map((entry) => entry.scope)).toEqual(["global", "workspace"]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("a server reads back as the one line it is reached at", () => {
|
|
207
|
+
expect(McpServerConfig.targetOf({ name: "a", transport: "stdio", command: "npx", args: ["-y", "x"] })).toBe(
|
|
208
|
+
"npx -y x",
|
|
209
|
+
);
|
|
210
|
+
expect(McpServerConfig.targetOf({ name: "a", transport: "http", url: "https://x" })).toBe("https://x");
|
|
211
|
+
});
|
|
212
|
+
});
|
package/codegenLock.test.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { readFile, stat, utimes, writeFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { CodegenLock } from "./codegenLock";
|
|
5
|
+
import { tempDirs } from "./testHelpers";
|
|
6
6
|
|
|
7
|
-
const
|
|
8
|
-
const makeRoot = async () => {
|
|
9
|
-
const root = await mkdtemp(path.join(tmpdir(), "akan-codegen-lock-"));
|
|
10
|
-
roots.push(root);
|
|
11
|
-
return root;
|
|
12
|
-
};
|
|
7
|
+
const makeRoot = tempDirs("akan-codegen-lock-");
|
|
13
8
|
const seedHolder = async (root: string, holder: unknown) => {
|
|
14
9
|
const lockPath = CodegenLock.pathIn(root);
|
|
15
10
|
await Bun.write(lockPath, typeof holder === "string" ? holder : JSON.stringify(holder));
|
|
@@ -18,10 +13,6 @@ const seedHolder = async (root: string, holder: unknown) => {
|
|
|
18
13
|
/** A pid that cannot be alive: `kill(0)` on it is ESRCH on every platform this runs on. */
|
|
19
14
|
const deadPid = 0x7ffffff;
|
|
20
15
|
|
|
21
|
-
afterEach(async () => {
|
|
22
|
-
for (const root of roots.splice(0)) await rm(root, { recursive: true, force: true });
|
|
23
|
-
});
|
|
24
|
-
|
|
25
16
|
describe("CodegenLock", () => {
|
|
26
17
|
test("serializes concurrent callers in the same process", async () => {
|
|
27
18
|
const root = await makeRoot();
|
package/codegenLock.ts
CHANGED
|
@@ -8,26 +8,12 @@ interface LockHolder {
|
|
|
8
8
|
label: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*
|
|
14
|
-
* `WatchRootResolver` narrows each dev server to its own app and its own lib dependencies, but two apps
|
|
15
|
-
* that share a lib still both watch it, so a save there reaches both builders and both regenerate the
|
|
16
|
-
* same barrel. Whichever watcher is mid-scan then reads a half-written file back as a user edit, which is
|
|
17
|
-
* a rebuild per rewrite. `scanSync` writes the same files at boot for every mounting app.
|
|
18
|
-
*
|
|
19
|
-
* A wait that expires proceeds *without* the lock rather than failing: this sits on the dev server's
|
|
20
|
-
* hot path, and stalling the file watcher is worse than the torn read `FileSys.writeTextAtomic` already
|
|
21
|
-
* prevents on its own.
|
|
22
|
-
*/
|
|
11
|
+
// Two dev servers sharing a lib both regenerate its barrels, and a mid-scan watcher reads a half-written one as a user
|
|
12
|
+
// edit. An expired wait proceeds unlocked: stalling the watcher is worse than the torn read `writeTextAtomic` prevents.
|
|
23
13
|
export class CodegenLock {
|
|
24
14
|
static readonly fileName = "codegen.lock";
|
|
25
15
|
static readonly waitTimeoutMs = 10_000;
|
|
26
|
-
/**
|
|
27
|
-
* How long an unreadable lock file is respected. It covers the window between the exclusive create
|
|
28
|
-
* and the holder write, where the file exists but names no pid yet — a young one is somebody else
|
|
29
|
-
* mid-acquire, not a corpse.
|
|
30
|
-
*/
|
|
16
|
+
/** A holder-less lock file younger than this is someone mid-acquire (between the exclusive create and the write). */
|
|
31
17
|
static readonly unknownHolderStaleMs = 60_000;
|
|
32
18
|
static readonly #pollMs = 25;
|
|
33
19
|
static readonly #logger = new Logger("CodegenLock");
|
|
@@ -83,11 +83,7 @@ export const normalizePrimitiveArgType = (type: PrimitiveArgType): NormalizedPri
|
|
|
83
83
|
export const App = createInternalArgToken<AppExecutor, "App">("App");
|
|
84
84
|
export type App = AppExecutor;
|
|
85
85
|
|
|
86
|
-
/**
|
|
87
|
-
* One or more apps, from a variadic positional (`akan start a b`, `akan start a,b`, `akan start all`)
|
|
88
|
-
* or a checkbox when none is named. Reach for it only where running several is meaningful — every other
|
|
89
|
-
* command takes `App`, whose single-select is unchanged.
|
|
90
|
-
*/
|
|
86
|
+
/** One or more apps: `akan start a b`, `a,b` or `all`, or a checkbox when none is named. */
|
|
91
87
|
export const Apps = createInternalArgToken<AppExecutor[], "Apps">("Apps");
|
|
92
88
|
export type Apps = AppExecutor[];
|
|
93
89
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const camelToKebabCase = (str: string) => str.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
@@ -10,11 +10,8 @@ import chalk from "chalk";
|
|
|
10
10
|
import { type Command, program } from "commander";
|
|
11
11
|
import { AppSelectionMemory } from "../appSelectionMemory";
|
|
12
12
|
import { AppExecutor, Executor, LibExecutor, ModuleExecutor, PkgExecutor, WorkspaceExecutor } from "../executors";
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
// every process that registers a command (measured: 236MB vs 3MB).
|
|
16
|
-
import { FileSys } from "../fileSys";
|
|
17
|
-
import { getDirname } from "../getDirname";
|
|
13
|
+
// Never the root barrel: it drags ink, ssh2 and the cloud stack into every command process.
|
|
14
|
+
import { FileSys, getDirname } from "../fileSys";
|
|
18
15
|
import type { PackageJson } from "../types";
|
|
19
16
|
import {
|
|
20
17
|
type ArgMeta,
|
|
@@ -24,11 +21,11 @@ import {
|
|
|
24
21
|
getArgMetas,
|
|
25
22
|
type InternalArgMeta,
|
|
26
23
|
} from "./argMeta";
|
|
24
|
+
import { camelToKebabCase } from "./camelToKebabCase";
|
|
27
25
|
import { CommandContainer } from "./dependencyBuilder";
|
|
28
26
|
import { formatCommandHelp, formatHelp } from "./helpFormatter";
|
|
29
27
|
import { type CommandCls, getTargetCommandNames, getTargetMetas } from "./targetMeta";
|
|
30
28
|
|
|
31
|
-
const camelToKebabCase = (str: string) => str.replace(/([A-Z])/g, "-$1").toLowerCase();
|
|
32
29
|
const loggedCliErrorObjects = new WeakSet<object>();
|
|
33
30
|
const loggedCliErrorMessages = new Set<string>();
|
|
34
31
|
|
|
@@ -71,6 +68,8 @@ const handleOption = (programCommand: Command, argMeta: ArgMeta) => {
|
|
|
71
68
|
`-${flag}, --${kebabName}${type === "boolean" ? " [boolean]" : ` <${kebabName}>`}`,
|
|
72
69
|
`${desc}${ask ? ` (${ask})` : ""}${example ? ` (example: ${example})` : ""}${choices ? ` (choices: ${choices.map((choice) => choice.name).join(", ")})` : ""}`,
|
|
73
70
|
);
|
|
71
|
+
if (type === "boolean" && argMeta.argsOption.default === true)
|
|
72
|
+
programCommand.option(`--no-${kebabName}`, `turn off --${kebabName}`);
|
|
74
73
|
return programCommand;
|
|
75
74
|
};
|
|
76
75
|
const handleArgument = (programCommand: Command, argMeta: ArgMeta) => {
|
|
@@ -95,30 +94,16 @@ const normalizeEnumChoices = (enumChoices: EnumChoices) =>
|
|
|
95
94
|
: { value: choice, name: choice.toString() },
|
|
96
95
|
);
|
|
97
96
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
*
|
|
101
|
-
* `runCommands` lives in this module, so a static import put `@inquirer/prompts` (~24MB) in the CLI
|
|
102
|
-
* entry's chunk closure — and `akan start` holds that process for the whole dev session while never
|
|
103
|
-
* asking a question, because every argument it needs is already on the command line.
|
|
104
|
-
*
|
|
105
|
-
* The wrappers are typed from the real prompts so no call site changes, and `import type` leaves no
|
|
106
|
-
* runtime edge for the bundler to follow (`entryModuleGraph.test.ts` asserts that).
|
|
107
|
-
*/
|
|
97
|
+
// Loaded on the first prompt: a static import keeps @inquirer/prompts (~24MB) resident for a whole `akan start`.
|
|
98
|
+
// The types come through `import type`, which leaves no runtime edge (entryModuleGraph.test.ts asserts that).
|
|
108
99
|
const prompts = async () => await import("@inquirer/prompts");
|
|
109
100
|
const select = ((config, context) => prompts().then((m) => m.select(config, context))) as typeof inquirerSelect;
|
|
110
101
|
const confirm = ((config, context) => prompts().then((m) => m.confirm(config, context))) as typeof inquirerConfirm;
|
|
111
102
|
const input = ((config, context) => prompts().then((m) => m.input(config, context))) as typeof inquirerInput;
|
|
112
103
|
const checkbox = ((config, context) => prompts().then((m) => m.checkbox(config, context))) as typeof inquirerCheckbox;
|
|
113
104
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
*
|
|
117
|
-
* Only a static choice list can be checked: a `DynamicEnum` resolves against the command context, which
|
|
118
|
-
* is not populated until the internal args are resolved, and it is the interactive `select` that consumes
|
|
119
|
-
* it. Comparison is stringly on purpose — the value still carries commander's raw string here, while a
|
|
120
|
-
* numeric choice list holds numbers.
|
|
121
|
-
*/
|
|
105
|
+
// A DynamicEnum is not checked: it resolves against a context the internal args have not populated yet.
|
|
106
|
+
// Compared as strings: the value is still commander's raw string while a numeric choice list holds numbers.
|
|
122
107
|
const assertEnumChoice = (argMeta: ArgMeta, value: unknown) => {
|
|
123
108
|
const enumChoices = argMeta.argsOption.enum;
|
|
124
109
|
if (!enumChoices || typeof enumChoices === "function") return;
|
|
@@ -131,17 +116,13 @@ const assertEnumChoice = (argMeta: ArgMeta, value: unknown) => {
|
|
|
131
116
|
);
|
|
132
117
|
};
|
|
133
118
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
if (!enumChoices) return null;
|
|
137
|
-
if (typeof enumChoices === "function") return await enumChoices(context);
|
|
138
|
-
return enumChoices;
|
|
139
|
-
};
|
|
119
|
+
const inputMessageOf = ({ name, argsOption: { desc, example, ask } }: ArgMeta) =>
|
|
120
|
+
ask ? `${ask}: ` : desc ? `${desc}: ` : `Enter the ${name} value${example ? ` (example: ${example})` : ""}: `;
|
|
140
121
|
|
|
141
122
|
export const getOptionValue = async (argMeta: ArgMeta, opt: Record<string, unknown>, context: CommandContext) => {
|
|
142
123
|
const {
|
|
143
124
|
name,
|
|
144
|
-
argsOption: { enum: enumChoices, default: defaultValue, type, desc, nullable,
|
|
125
|
+
argsOption: { enum: enumChoices, default: defaultValue, type, desc, nullable, ask },
|
|
145
126
|
} = argMeta;
|
|
146
127
|
if (opt[argMeta.name] !== undefined) {
|
|
147
128
|
assertEnumChoice(argMeta, opt[argMeta.name]);
|
|
@@ -149,42 +130,26 @@ export const getOptionValue = async (argMeta: ArgMeta, opt: Record<string, unkno
|
|
|
149
130
|
} else if (defaultValue !== undefined) return defaultValue;
|
|
150
131
|
|
|
151
132
|
if (enumChoices) {
|
|
152
|
-
const choices = normalizeEnumChoices(
|
|
133
|
+
const choices = normalizeEnumChoices(
|
|
134
|
+
(typeof enumChoices === "function" ? await enumChoices(context) : enumChoices) ?? [],
|
|
135
|
+
);
|
|
153
136
|
if (choices.length === 1) return choices[0]?.value;
|
|
154
|
-
|
|
155
|
-
return choice;
|
|
137
|
+
return await select({ message: ask ?? desc ?? `Select the ${name} value`, choices });
|
|
156
138
|
} else if (nullable) return null;
|
|
157
139
|
else if (type === "boolean") {
|
|
158
140
|
const message = ask ?? desc ?? `Do you want to set ${name}? ${desc ? ` (${desc})` : ""}: `;
|
|
159
141
|
return await confirm({ message });
|
|
160
|
-
} else {
|
|
161
|
-
const message = ask
|
|
162
|
-
? `${ask}: `
|
|
163
|
-
: desc
|
|
164
|
-
? `${desc}: `
|
|
165
|
-
: `Enter the ${name} value${example ? ` (example: ${example})` : ""}: `;
|
|
166
|
-
if (argMeta.argsOption.nullable) return await input({ message });
|
|
167
|
-
else return convertArgValue(await input({ message }), type ?? "string");
|
|
168
|
-
}
|
|
142
|
+
} else return convertArgValue(await input({ message: inputMessageOf(argMeta) }), type ?? "string");
|
|
169
143
|
};
|
|
170
144
|
|
|
171
145
|
export const getArgumentValue = async (argMeta: ArgMeta, value: string | undefined) => {
|
|
172
|
-
const {
|
|
173
|
-
name,
|
|
174
|
-
argsOption: { default: defaultValue, type, desc, nullable, example, ask },
|
|
175
|
-
} = argMeta;
|
|
146
|
+
const { default: defaultValue, type, nullable } = argMeta.argsOption;
|
|
176
147
|
if (value !== undefined) {
|
|
177
148
|
assertEnumChoice(argMeta, value);
|
|
178
149
|
return convertArgValue(value, type ?? "string");
|
|
179
150
|
} else if (defaultValue !== undefined) return defaultValue;
|
|
180
151
|
else if (nullable) return null;
|
|
181
|
-
|
|
182
|
-
const message = ask
|
|
183
|
-
? `${ask}: `
|
|
184
|
-
: desc
|
|
185
|
-
? `${desc}: `
|
|
186
|
-
: `Enter the ${name} value${example ? ` (example: ${example})` : ""}: `;
|
|
187
|
-
return convertArgValue(await input({ message }), type ?? "string");
|
|
152
|
+
return convertArgValue(await input({ message: inputMessageOf(argMeta) }), type ?? "string");
|
|
188
153
|
};
|
|
189
154
|
|
|
190
155
|
const assignCommandContext = (context: CommandContext, argMeta: ArgMeta | InternalArgMeta, value: unknown) => {
|
|
@@ -215,18 +180,13 @@ const assertCurrentDirectoryIsWorkspaceRoot = async () => {
|
|
|
215
180
|
);
|
|
216
181
|
};
|
|
217
182
|
|
|
218
|
-
/** A variadic positional arrives as an array, and each entry may itself be a comma-separated list. */
|
|
219
183
|
const parseAppNameList = (value: string | string[] | undefined): string[] =>
|
|
220
184
|
(Array.isArray(value) ? value : value === undefined ? [] : [value])
|
|
221
185
|
.flatMap((entry) => entry.split(","))
|
|
222
186
|
.map((entry) => entry.trim())
|
|
223
187
|
.filter(Boolean);
|
|
224
188
|
|
|
225
|
-
|
|
226
|
-
* The `Apps` token: named apps, `all`, or a checkbox when the command line names none. Kept out of
|
|
227
|
-
* `getInternalArgumentValue` because it is the only internal arg whose positional is variadic, so it is
|
|
228
|
-
* the only one whose raw value is an array.
|
|
229
|
-
*/
|
|
189
|
+
// Kept out of getInternalArgumentValue: Apps is the only internal arg whose positional is variadic (an array).
|
|
230
190
|
export const getAppsArgumentValue = async (
|
|
231
191
|
value: string | string[] | undefined,
|
|
232
192
|
workspace: WorkspaceExecutor,
|
|
@@ -263,32 +223,23 @@ export const getInternalArgumentValue = async (
|
|
|
263
223
|
if (argMeta.type === "Workspace") return workspace;
|
|
264
224
|
const sysType = argMeta.type.toLowerCase();
|
|
265
225
|
const [appNames, libNames, pkgNames] = await workspace.getExecs();
|
|
266
|
-
if (sysType === "sys") {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const execName = await select<string>({
|
|
284
|
-
message: `Select the App or Lib or Pkg name`,
|
|
285
|
-
choices: [...appNames, ...libNames, ...pkgNames],
|
|
286
|
-
});
|
|
287
|
-
if (appNames.includes(execName)) return AppExecutor.from(workspace, execName);
|
|
288
|
-
else if (libNames.includes(execName)) return LibExecutor.from(workspace, execName);
|
|
289
|
-
else if (pkgNames.includes(execName)) return PkgExecutor.from(workspace, execName);
|
|
290
|
-
else throw new Error(`Invalid system name: ${execName}`);
|
|
291
|
-
}
|
|
226
|
+
if (sysType === "sys" || sysType === "exec") {
|
|
227
|
+
const pkgChoices = sysType === "exec" ? pkgNames : [];
|
|
228
|
+
const execOf = (name: string) => {
|
|
229
|
+
if (appNames.includes(name)) return AppExecutor.from(workspace, name);
|
|
230
|
+
if (libNames.includes(name)) return LibExecutor.from(workspace, name);
|
|
231
|
+
if (pkgChoices.includes(name)) return PkgExecutor.from(workspace, name);
|
|
232
|
+
return null;
|
|
233
|
+
};
|
|
234
|
+
const found = value ? execOf(value) : null;
|
|
235
|
+
if (found) return found;
|
|
236
|
+
const name = await select<string>({
|
|
237
|
+
message: sysType === "exec" ? `Select the App or Lib or Pkg name` : `Select the App or Lib name`,
|
|
238
|
+
choices: [...appNames, ...libNames, ...pkgChoices],
|
|
239
|
+
});
|
|
240
|
+
const picked = execOf(name);
|
|
241
|
+
if (!picked) throw new Error(`Invalid system name: ${name}`);
|
|
242
|
+
return picked;
|
|
292
243
|
} else if (sysType === "app") {
|
|
293
244
|
if (value && appNames.includes(value)) return AppExecutor.from(workspace, value);
|
|
294
245
|
if (!value && appNames.length === 1 && appNames[0]) return AppExecutor.from(workspace, appNames[0]);
|
|
@@ -353,17 +304,10 @@ export const runCommands = async (...commands: CommandCls[]) => {
|
|
|
353
304
|
}
|
|
354
305
|
process.env.AKAN_VERSION = cliPackageJson?.version ?? "0.0.1";
|
|
355
306
|
|
|
356
|
-
// Custom help handling
|
|
357
307
|
const hasHelpFlag = process.argv.includes("--help") || process.argv.includes("-h");
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (hasHelpFlag || !hasCommand) {
|
|
362
|
-
if (process.argv.length === 2 || (process.argv.length === 3 && hasHelpFlag)) {
|
|
363
|
-
// Global help (no specific command)
|
|
364
|
-
Logger.rawLog(formatHelp(commands, process.env.AKAN_VERSION));
|
|
365
|
-
process.exit(0);
|
|
366
|
-
}
|
|
308
|
+
if (process.argv.length === 2 || (process.argv.length === 3 && hasHelpFlag)) {
|
|
309
|
+
Logger.rawLog(formatHelp(commands, process.env.AKAN_VERSION));
|
|
310
|
+
process.exit(0);
|
|
367
311
|
}
|
|
368
312
|
|
|
369
313
|
program.version(process.env.AKAN_VERSION).description("Akan CLI").configureHelp({
|
|
@@ -414,8 +358,6 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
|
|
|
414
358
|
}
|
|
415
359
|
}
|
|
416
360
|
programCommand = programCommand.option(`-v, --verbose [boolean]`, `verbose output`);
|
|
417
|
-
|
|
418
|
-
// Override help completely for each command
|
|
419
361
|
programCommand.helpInformation = () => {
|
|
420
362
|
return formatCommandHelp(command, targetMeta.key);
|
|
421
363
|
};
|
|
@@ -444,11 +386,9 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
|
|
|
444
386
|
cmdArgs[argMeta.idx] as string,
|
|
445
387
|
workspace,
|
|
446
388
|
);
|
|
447
|
-
// set app name to env
|
|
448
389
|
if (commandArgs[argMeta.idx] instanceof AppExecutor)
|
|
449
390
|
process.env.AKAN_PUBLIC_APP_NAME = (commandArgs[argMeta.idx] as AppExecutor).name;
|
|
450
|
-
//? Only
|
|
451
|
-
//? every env-derived answer in this process belong to whichever app happened to be last.
|
|
391
|
+
//? Only for a single app: with several, every env-derived answer would belong to whichever came last.
|
|
452
392
|
else if (Array.isArray(commandArgs[argMeta.idx])) {
|
|
453
393
|
const apps = commandArgs[argMeta.idx] as AppExecutor[];
|
|
454
394
|
if (apps.length === 1 && apps[0]) process.env.AKAN_PUBLIC_APP_NAME = apps[0].name;
|
|
@@ -469,5 +409,9 @@ It may cause unexpected behavior. Run \`akan update\` to update latest akanjs.`,
|
|
|
469
409
|
}
|
|
470
410
|
}
|
|
471
411
|
}
|
|
472
|
-
await
|
|
412
|
+
// Handled here, not by the handler above: a rejection reaching the entry's top-level await prints Bun's own trace.
|
|
413
|
+
await program.parseAsync(process.argv).catch((error: unknown) => {
|
|
414
|
+
printCliError(error);
|
|
415
|
+
process.exit(1);
|
|
416
|
+
});
|
|
473
417
|
};
|