@akanjs/devkit 1.0.20 → 2.1.0-rc.1

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 (195) hide show
  1. package/README.ko.md +65 -0
  2. package/README.md +62 -6
  3. package/aiEditor.ts +304 -0
  4. package/akanApp/akanApp.host.ts +393 -0
  5. package/akanApp/index.ts +1 -0
  6. package/akanConfig/akanConfig.test.ts +236 -0
  7. package/akanConfig/akanConfig.ts +384 -0
  8. package/akanConfig/index.ts +2 -0
  9. package/akanConfig/types.ts +23 -0
  10. package/applicationBuildReporter.ts +69 -0
  11. package/applicationBuildRunner.ts +302 -0
  12. package/applicationReleasePackager.ts +206 -0
  13. package/artifact/implicitRootLayout.ts +155 -0
  14. package/artifact/index.ts +1 -0
  15. package/artifact/routeSeedIndex.test.ts +98 -0
  16. package/artifact/routeSeedIndex.ts +130 -0
  17. package/auth.ts +41 -0
  18. package/builder.ts +164 -0
  19. package/capacitor.base.config.ts +88 -0
  20. package/capacitorApp.ts +440 -0
  21. package/commandDecorators/argMeta.ts +102 -0
  22. package/commandDecorators/command.ts +351 -0
  23. package/commandDecorators/commandBuilder.ts +224 -0
  24. package/commandDecorators/commandDecorators.test.ts +212 -0
  25. package/commandDecorators/commandMeta.ts +7 -0
  26. package/commandDecorators/dependencyBuilder.ts +100 -0
  27. package/{esm/src/commandDecorators/helpFormatter.js → commandDecorators/helpFormatter.ts} +100 -47
  28. package/{esm/src/commandDecorators/index.js → commandDecorators/index.ts} +4 -2
  29. package/commandDecorators/targetMeta.ts +31 -0
  30. package/commandDecorators/types.ts +10 -0
  31. package/constants.ts +25 -0
  32. package/createTunnel.ts +36 -0
  33. package/dependencyScanner.ts +357 -0
  34. package/devkitUtils.test.ts +259 -0
  35. package/executors.test.ts +315 -0
  36. package/executors.ts +1390 -0
  37. package/{esm/src/extractDeps.js → extractDeps.ts} +26 -20
  38. package/{esm/src/fileEditor.js → fileEditor.ts} +51 -32
  39. package/fileSys.ts +39 -0
  40. package/frontendBuild/allRoutesBuilder.ts +103 -0
  41. package/frontendBuild/buildRouteClient.test.ts +190 -0
  42. package/frontendBuild/clientBuildTypes.ts +114 -0
  43. package/frontendBuild/clientEntriesBundler.ts +303 -0
  44. package/frontendBuild/clientEntryDiscovery.ts +199 -0
  45. package/frontendBuild/csrArtifactBuilder.ts +237 -0
  46. package/frontendBuild/cssCompiler.ts +286 -0
  47. package/frontendBuild/cssImportResolver.ts +116 -0
  48. package/frontendBuild/fontOptimizer.ts +427 -0
  49. package/frontendBuild/frontendBuild.test.ts +204 -0
  50. package/frontendBuild/hmrChangeClassifier.ts +28 -0
  51. package/frontendBuild/hmrWatcher.ts +102 -0
  52. package/frontendBuild/index.ts +18 -0
  53. package/frontendBuild/pagesBundleBuilder.ts +137 -0
  54. package/frontendBuild/pagesEntrySourceGenerator.ts +37 -0
  55. package/frontendBuild/precompressArtifacts.ts +59 -0
  56. package/frontendBuild/routeClientBuilder.ts +290 -0
  57. package/frontendBuild/routesManifestArtifactSerializer.ts +62 -0
  58. package/frontendBuild/ssrBaseArtifactBuilder.ts +139 -0
  59. package/frontendBuild/vendorSpecifiers.ts +16 -0
  60. package/frontendBuild/watchRootResolver.ts +28 -0
  61. package/getCredentials.ts +19 -0
  62. package/getDirname.ts +3 -0
  63. package/getModelFileData.ts +59 -0
  64. package/getRelatedCnsts.ts +313 -0
  65. package/guideline.ts +19 -0
  66. package/incrementalBuilder/incrementalBuilder.host.test.ts +51 -0
  67. package/incrementalBuilder/incrementalBuilder.host.ts +152 -0
  68. package/incrementalBuilder/incrementalBuilder.proc.ts +331 -0
  69. package/incrementalBuilder/index.ts +1 -0
  70. package/{esm/src/index.js → index.ts} +28 -15
  71. package/lint/no-deep-internal-import.grit +25 -0
  72. package/lint/no-import-client-functions.grit +32 -0
  73. package/lint/no-import-external-library.grit +21 -0
  74. package/lint/no-js-private-class-method.grit +42 -0
  75. package/lint/no-use-client-in-server.grit +7 -0
  76. package/lint/non-scalar-props-restricted.grit +13 -0
  77. package/linter.ts +271 -0
  78. package/mobile/index.ts +1 -0
  79. package/mobile/mobileTarget.test.ts +53 -0
  80. package/mobile/mobileTarget.ts +88 -0
  81. package/package.json +48 -31
  82. package/prompter.ts +72 -0
  83. package/scanInfo.ts +606 -0
  84. package/selectModel.ts +11 -0
  85. package/{esm/src/spinner.js → spinner.ts} +22 -28
  86. package/{esm/src/capacitorApp.js → src/capacitorApp.ts} +82 -81
  87. package/sshTunnel.ts +152 -0
  88. package/{esm/src/streamAi.js → streamAi.ts} +18 -12
  89. package/transforms/barrelAnalyzer.ts +278 -0
  90. package/transforms/barrelImportsPlugin.ts +504 -0
  91. package/transforms/externalizeFrameworkPlugin.ts +185 -0
  92. package/transforms/index.ts +5 -0
  93. package/transforms/rscUseClientTransform.ts +59 -0
  94. package/transforms/transforms.test.ts +208 -0
  95. package/transforms/useClientBundlePlugin.ts +47 -0
  96. package/tsconfig.json +37 -0
  97. package/typeChecker.ts +264 -0
  98. package/types.ts +44 -0
  99. package/ui/MultiScrollList.tsx +242 -0
  100. package/ui/ScrollList.tsx +107 -0
  101. package/ui/index.ts +2 -0
  102. package/{esm/src/uploadRelease.js → uploadRelease.ts} +50 -34
  103. package/{esm/src/useStdoutDimensions.js → useStdoutDimensions.ts} +5 -5
  104. package/cjs/index.js +0 -21
  105. package/cjs/src/aiEditor.js +0 -311
  106. package/cjs/src/auth.js +0 -72
  107. package/cjs/src/builder.js +0 -114
  108. package/cjs/src/capacitorApp.js +0 -313
  109. package/cjs/src/commandDecorators/argMeta.js +0 -88
  110. package/cjs/src/commandDecorators/command.js +0 -324
  111. package/cjs/src/commandDecorators/commandMeta.js +0 -30
  112. package/cjs/src/commandDecorators/helpFormatter.js +0 -211
  113. package/cjs/src/commandDecorators/index.js +0 -31
  114. package/cjs/src/commandDecorators/targetMeta.js +0 -57
  115. package/cjs/src/commandDecorators/types.js +0 -15
  116. package/cjs/src/constants.js +0 -46
  117. package/cjs/src/createTunnel.js +0 -49
  118. package/cjs/src/dependencyScanner.js +0 -220
  119. package/cjs/src/executors.js +0 -964
  120. package/cjs/src/extractDeps.js +0 -103
  121. package/cjs/src/fileEditor.js +0 -120
  122. package/cjs/src/getCredentials.js +0 -44
  123. package/cjs/src/getDirname.js +0 -38
  124. package/cjs/src/getModelFileData.js +0 -66
  125. package/cjs/src/getRelatedCnsts.js +0 -260
  126. package/cjs/src/guideline.js +0 -15
  127. package/cjs/src/index.js +0 -65
  128. package/cjs/src/linter.js +0 -238
  129. package/cjs/src/prompter.js +0 -85
  130. package/cjs/src/scanInfo.js +0 -491
  131. package/cjs/src/selectModel.js +0 -46
  132. package/cjs/src/spinner.js +0 -93
  133. package/cjs/src/streamAi.js +0 -62
  134. package/cjs/src/typeChecker.js +0 -207
  135. package/cjs/src/types.js +0 -15
  136. package/cjs/src/uploadRelease.js +0 -112
  137. package/cjs/src/useStdoutDimensions.js +0 -43
  138. package/esm/index.js +0 -1
  139. package/esm/src/aiEditor.js +0 -282
  140. package/esm/src/auth.js +0 -42
  141. package/esm/src/builder.js +0 -81
  142. package/esm/src/commandDecorators/argMeta.js +0 -54
  143. package/esm/src/commandDecorators/command.js +0 -290
  144. package/esm/src/commandDecorators/commandMeta.js +0 -7
  145. package/esm/src/commandDecorators/targetMeta.js +0 -33
  146. package/esm/src/commandDecorators/types.js +0 -0
  147. package/esm/src/constants.js +0 -17
  148. package/esm/src/createTunnel.js +0 -26
  149. package/esm/src/dependencyScanner.js +0 -187
  150. package/esm/src/executors.js +0 -928
  151. package/esm/src/getCredentials.js +0 -11
  152. package/esm/src/getDirname.js +0 -5
  153. package/esm/src/getModelFileData.js +0 -33
  154. package/esm/src/getRelatedCnsts.js +0 -221
  155. package/esm/src/guideline.js +0 -0
  156. package/esm/src/linter.js +0 -205
  157. package/esm/src/prompter.js +0 -51
  158. package/esm/src/scanInfo.js +0 -455
  159. package/esm/src/selectModel.js +0 -13
  160. package/esm/src/typeChecker.js +0 -174
  161. package/esm/src/types.js +0 -0
  162. package/index.d.ts +0 -1
  163. package/src/aiEditor.d.ts +0 -50
  164. package/src/auth.d.ts +0 -9
  165. package/src/builder.d.ts +0 -18
  166. package/src/capacitorApp.d.ts +0 -39
  167. package/src/commandDecorators/argMeta.d.ts +0 -67
  168. package/src/commandDecorators/command.d.ts +0 -2
  169. package/src/commandDecorators/commandMeta.d.ts +0 -2
  170. package/src/commandDecorators/helpFormatter.d.ts +0 -3
  171. package/src/commandDecorators/index.d.ts +0 -6
  172. package/src/commandDecorators/targetMeta.d.ts +0 -19
  173. package/src/commandDecorators/types.d.ts +0 -1
  174. package/src/constants.d.ts +0 -26
  175. package/src/createTunnel.d.ts +0 -8
  176. package/src/dependencyScanner.d.ts +0 -23
  177. package/src/executors.d.ts +0 -296
  178. package/src/extractDeps.d.ts +0 -7
  179. package/src/fileEditor.d.ts +0 -16
  180. package/src/getCredentials.d.ts +0 -12
  181. package/src/getDirname.d.ts +0 -1
  182. package/src/getModelFileData.d.ts +0 -16
  183. package/src/getRelatedCnsts.d.ts +0 -53
  184. package/src/guideline.d.ts +0 -19
  185. package/src/index.d.ts +0 -23
  186. package/src/linter.d.ts +0 -109
  187. package/src/prompter.d.ts +0 -14
  188. package/src/scanInfo.d.ts +0 -82
  189. package/src/selectModel.d.ts +0 -1
  190. package/src/spinner.d.ts +0 -20
  191. package/src/streamAi.d.ts +0 -6
  192. package/src/typeChecker.d.ts +0 -52
  193. package/src/types.d.ts +0 -31
  194. package/src/uploadRelease.d.ts +0 -10
  195. package/src/useStdoutDimensions.d.ts +0 -1
package/README.ko.md ADDED
@@ -0,0 +1,65 @@
1
+ # @akanjs/devkit
2
+
3
+ [문서](https://akanjs.com/docs) | [npm](https://www.npmjs.com/package/@akanjs/devkit) | [런타임](https://www.npmjs.com/package/akanjs)
4
+
5
+ Akan.js를 위한 development tooling primitive입니다.
6
+
7
+ `@akanjs/devkit`은 Akan CLI와 프레임워크 수준 tooling에서 사용하는 build runner, workspace
8
+ executor, config loader, dependency scanner, frontend artifact builder, command decorator, prompt,
9
+ release helper를 담고 있습니다. 애플리케이션 런타임 코드가 아니라 tooling과 package author를 위한
10
+ 패키지입니다.
11
+
12
+ ## 설치
13
+
14
+ 대부분의 사용자는 devkit 대신 CLI를 설치하면 됩니다.
15
+
16
+ ```bash
17
+ bun install -g @akanjs/cli@latest
18
+ ```
19
+
20
+ Akan-aware tooling을 직접 만들 때만 `@akanjs/devkit`을 설치하세요.
21
+
22
+ ```bash
23
+ bun add -d @akanjs/devkit
24
+ ```
25
+
26
+ ## 사용 예시
27
+
28
+ ```ts
29
+ import { ApplicationBuildRunner, WorkspaceExecutor } from "@akanjs/devkit";
30
+
31
+ const workspace = WorkspaceExecutor.fromRoot();
32
+ const app = await workspace.getApp("my-app");
33
+ const runner = new ApplicationBuildRunner(app);
34
+
35
+ await runner.build();
36
+ ```
37
+
38
+ ## 제공하는 것
39
+
40
+ - Workspace, app, library, package, module executor.
41
+ - `akan.config.ts` 로딩과 정규화.
42
+ - Application build, typecheck, SSR, CSR, release runner.
43
+ - Dependency scanning과 package metadata 생성 helper.
44
+ - Frontend build transform과 RSC/SSR artifact builder.
45
+ - `@akanjs/cli`가 사용하는 command/script decorator.
46
+ - AI prompt, guideline, code-generation 지원 utility.
47
+ - Capacitor와 mobile release helper.
48
+
49
+ ## 패키지 경계
50
+
51
+ - 런타임 코드는 `akanjs`에서 import해야 합니다. `AppConfig`, `LibConfig`, `AppInfo`, `LibInfo` 같은
52
+ 공유 config 타입도 `akanjs`에서 가져옵니다.
53
+ - CLI 사용자는 `@akanjs/cli`를 설치하면 됩니다. published CLI는 이 devkit을 내부에 번들링합니다.
54
+ - Tooling author는 Akan workspace introspection이나 build API가 필요할 때 `@akanjs/devkit`을 직접
55
+ import할 수 있습니다.
56
+
57
+ ## 요구사항
58
+
59
+ - [Bun](https://bun.sh) `>=1.3.13`
60
+ - TypeScript
61
+ - Optional peer는 Capacitor integration처럼 해당 기능을 사용할 때만 필요합니다.
62
+
63
+ ## 라이선스
64
+
65
+ MIT
package/README.md CHANGED
@@ -1,7 +1,63 @@
1
- > 🎉 **Akanjs v2 has been released!**
2
- >
3
- > This package is under maintenance for existing users, but we recommend using Akanjs v2 for new projects.
4
- >
5
- > For the unified and latest version package, please visit [akanjs](https://www.npmjs.com/package/akanjs).
1
+ # @akanjs/devkit
6
2
 
7
- This package is a sub-library of akanjs. For more information, please refer to [@akanjs/cli](https://www.npmjs.com/package/@akanjs/cli) and the [Akan.js Github](https://github.com/akan-team/akanjs).
3
+ [Docs](https://akanjs.com/docs) | [npm](https://www.npmjs.com/package/@akanjs/devkit) | [Runtime](https://www.npmjs.com/package/akanjs)
4
+
5
+ Development tooling primitives for Akan.js.
6
+
7
+ `@akanjs/devkit` contains the build runners, workspace executors, config loaders, dependency scanners,
8
+ frontend artifact builders, command decorators, prompts, and release helpers used by the Akan CLI and by
9
+ framework-level tooling. It is intended for tools and package authors, not for application runtime code.
10
+
11
+ ## Install
12
+
13
+ Most users should install the CLI instead:
14
+
15
+ ```bash
16
+ bun install -g @akanjs/cli@latest
17
+ ```
18
+
19
+ Install `@akanjs/devkit` directly only when building Akan-aware tooling:
20
+
21
+ ```bash
22
+ bun add -d @akanjs/devkit
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```ts
28
+ import { ApplicationBuildRunner, WorkspaceExecutor } from "@akanjs/devkit";
29
+
30
+ const workspace = WorkspaceExecutor.fromRoot();
31
+ const app = await workspace.getApp("my-app");
32
+ const runner = new ApplicationBuildRunner(app);
33
+
34
+ await runner.build();
35
+ ```
36
+
37
+ ## What It Provides
38
+
39
+ - Workspace, app, library, package, and module executors.
40
+ - `akan.config.ts` loading and normalization.
41
+ - Application build, typecheck, SSR, CSR, and release runners.
42
+ - Dependency scanning and package metadata generation helpers.
43
+ - Frontend build transforms and RSC/SSR artifact builders.
44
+ - Command/script decorators used by `@akanjs/cli`.
45
+ - AI prompt, guideline, and code-generation support utilities.
46
+ - Capacitor and mobile release helpers.
47
+
48
+ ## Package Boundary
49
+
50
+ - Runtime code should import from `akanjs`, including shared config types such as `AppConfig`, `LibConfig`,
51
+ `AppInfo`, and `LibInfo`.
52
+ - CLI users should install `@akanjs/cli`; the published CLI bundles this devkit internally.
53
+ - Tooling authors can import `@akanjs/devkit` directly when they need Akan workspace introspection or build APIs.
54
+
55
+ ## Requirements
56
+
57
+ - [Bun](https://bun.sh) `>=1.3.13`
58
+ - TypeScript
59
+ - Optional peers are only needed for the features that use them, such as Capacitor integration.
60
+
61
+ ## License
62
+
63
+ MIT
package/aiEditor.ts ADDED
@@ -0,0 +1,304 @@
1
+ import { input, select } from "@inquirer/prompts";
2
+ import {
3
+ AIMessage,
4
+ type BaseMessage,
5
+ HumanMessage,
6
+ mapChatMessagesToStoredMessages,
7
+ mapStoredMessagesToChatMessages,
8
+ type StoredMessage,
9
+ } from "@langchain/core/messages";
10
+ import { ChatDeepSeek } from "@langchain/deepseek";
11
+ import { ChatOpenAI } from "@langchain/openai";
12
+ import { Logger } from "akanjs/common";
13
+ import chalk from "chalk";
14
+
15
+ import { getAkanGlobalConfig, setAkanGlobalConfig } from "./auth";
16
+ import type { Executor, WorkspaceExecutor } from "./executors";
17
+ import { Spinner } from "./spinner";
18
+ import type { FileContent } from "./types";
19
+
20
+ const MAX_ASK_TRY = 300;
21
+
22
+ export const supportedLlmModels = ["deepseek-chat", "deepseek-reasoner"] as const;
23
+ export type SupportedLlmModel = (typeof supportedLlmModels)[number];
24
+
25
+ interface EditOptions {
26
+ onReasoning?: (reasoning: string) => void;
27
+ onChunk?: (chunk: string) => void;
28
+ maxTry?: number;
29
+ validate?: string[];
30
+ approve?: boolean;
31
+ }
32
+
33
+ export class AiSession {
34
+ static #cacheDir = "node_modules/.cache/akan/aiSession";
35
+ static #chat: ChatDeepSeek | ChatOpenAI | null = null;
36
+ static async init({ temperature = 0, useExisting = true }: { temperature?: number; useExisting?: boolean } = {}) {
37
+ if (useExisting) {
38
+ const llmConfig = await AiSession.getLlmConfig();
39
+ if (llmConfig) {
40
+ AiSession.#setChatModel(llmConfig.model, llmConfig.apiKey);
41
+ Logger.rawLog(chalk.dim(`🤖akan editor uses existing LLM config (${llmConfig.model})`));
42
+ return AiSession;
43
+ }
44
+ } else Logger.rawLog(chalk.yellow("🤖akan-editor is not initialized. LLM configuration should be set first."));
45
+
46
+ const llmConfig = await AiSession.#requestLlmConfig();
47
+ const { model, apiKey } = llmConfig;
48
+
49
+ await AiSession.#validateApiKey(model, apiKey);
50
+ const session = AiSession.#setChatModel(model, apiKey, { temperature });
51
+ await session.setLlmConfig({ model, apiKey });
52
+ return session;
53
+ }
54
+ static #setChatModel(model: SupportedLlmModel, apiKey: string, { temperature = 0 }: { temperature?: number } = {}) {
55
+ AiSession.#chat = new ChatDeepSeek({
56
+ modelName: model,
57
+ temperature,
58
+ streaming: true,
59
+ apiKey,
60
+ // configuration: { baseURL: "https://api.deepseek.com/v1", apiKey },
61
+ });
62
+ return AiSession;
63
+ }
64
+ static async getLlmConfig() {
65
+ const akanConfig = await getAkanGlobalConfig();
66
+ return akanConfig.llm ?? null;
67
+ }
68
+ static async setLlmConfig(llmConfig: { model: SupportedLlmModel; apiKey: string } | null) {
69
+ const akanConfig = await getAkanGlobalConfig();
70
+ akanConfig.llm = llmConfig;
71
+ await setAkanGlobalConfig(akanConfig);
72
+ return AiSession;
73
+ }
74
+ static async #requestLlmConfig() {
75
+ const model = await select<SupportedLlmModel>({ message: "Select a LLM model", choices: supportedLlmModels });
76
+ const apiKey = await input({ message: "Enter your API key" });
77
+ return { model, apiKey };
78
+ }
79
+ static async #validateApiKey(modelName: SupportedLlmModel, apiKey: string) {
80
+ const spinner = new Spinner("Validating LLM API key...", { prefix: `🤖akan-editor` }).start();
81
+ const chat = new ChatOpenAI({
82
+ modelName,
83
+ temperature: 0,
84
+ configuration: { baseURL: "https://api.deepseek.com/v1", apiKey },
85
+ });
86
+ try {
87
+ await chat.invoke("Hi, and just say 'ok'");
88
+ spinner.succeed("LLM API key is valid");
89
+ return true;
90
+ } catch (error) {
91
+ spinner.fail(
92
+ chalk.red(
93
+ `LLM API key is invalid. Please check your API key and try again. You can set it again by running "akan set-llm" or reset by running "akan reset-llm"`,
94
+ ),
95
+ );
96
+ throw error;
97
+ }
98
+ }
99
+ static async clearCache(workspaceRoot: string) {
100
+ const cacheDir = `${workspaceRoot}/${AiSession.#cacheDir}`;
101
+ await Bun.$`rm -rf ${cacheDir}`;
102
+ }
103
+
104
+ messageHistory: BaseMessage[] = [];
105
+ readonly sessionKey: string;
106
+ isCacheLoaded: boolean = false;
107
+ workspace: WorkspaceExecutor;
108
+ constructor(
109
+ type: string,
110
+ { workspace, cacheKey, isContinued }: { workspace: WorkspaceExecutor; cacheKey?: string; isContinued?: boolean },
111
+ ) {
112
+ this.workspace = workspace;
113
+ this.sessionKey = `${type}${cacheKey ? `-${cacheKey}` : ""}`;
114
+ if (isContinued) this.#cacheLoadPromise = this.#loadCache();
115
+ }
116
+ #cacheLoadPromise: Promise<void> | null = null;
117
+ async #loadCache() {
118
+ const cacheFile = `${AiSession.#cacheDir}/${this.sessionKey}.json`;
119
+ const isCacheExists = await this.workspace.exists(cacheFile);
120
+ if (isCacheExists)
121
+ this.messageHistory = mapStoredMessagesToChatMessages(
122
+ (await this.workspace.readJson(cacheFile)) as StoredMessage[],
123
+ );
124
+ else this.messageHistory = [];
125
+ this.isCacheLoaded = isCacheExists;
126
+ }
127
+ async #saveCache() {
128
+ const cacheFilePath = `${AiSession.#cacheDir}/${this.sessionKey}.json`;
129
+ await this.workspace.writeJson(cacheFilePath, mapChatMessagesToStoredMessages(this.messageHistory));
130
+ }
131
+ async ask(
132
+ question: string,
133
+ {
134
+ onReasoning = (reasoning: string) => {
135
+ Logger.raw(chalk.dim(reasoning));
136
+ },
137
+ onChunk = (chunk: string) => {
138
+ Logger.raw(chunk);
139
+ },
140
+ }: EditOptions = {},
141
+ ): Promise<{ content: string; messageHistory: BaseMessage[] }> {
142
+ if (!AiSession.#chat) await AiSession.init();
143
+ if (this.#cacheLoadPromise) await this.#cacheLoadPromise;
144
+
145
+ if (!AiSession.#chat) throw new Error("Failed to initialize the AI session");
146
+ const loader = new Spinner(`${AiSession.#chat.model} is thinking...`, {
147
+ prefix: `🤖akan-editor`,
148
+ }).start();
149
+ try {
150
+ const humanMessage = new HumanMessage(question);
151
+ this.messageHistory.push(humanMessage);
152
+ const stream = await AiSession.#chat.stream(this.messageHistory);
153
+ let reasoningResponse = "",
154
+ fullResponse = "",
155
+ tokenIdx = 0;
156
+ for await (const chunk of stream) {
157
+ if (loader.isSpinning()) loader.succeed(`${AiSession.#chat.model} responded`);
158
+
159
+ if (!fullResponse.length) {
160
+ const reasoningContent = (chunk.additional_kwargs as { reasoning_content?: string }).reasoning_content ?? "";
161
+ if (reasoningContent.length) {
162
+ reasoningResponse += reasoningContent;
163
+ onReasoning(reasoningContent);
164
+ continue;
165
+ } else if (chunk.content.length) {
166
+ reasoningResponse += "\n";
167
+ onReasoning(reasoningResponse);
168
+ }
169
+ }
170
+
171
+ const content = chunk.content;
172
+ if (typeof content === "string") {
173
+ fullResponse += content;
174
+ onChunk(content); // Send individual chunks to callback
175
+ }
176
+ tokenIdx++;
177
+ }
178
+ fullResponse += "\n";
179
+ onChunk("\n");
180
+ this.messageHistory.push(new AIMessage(fullResponse));
181
+ return { content: fullResponse, messageHistory: this.messageHistory };
182
+ } catch (error) {
183
+ loader.fail(`${AiSession.#chat.model} failed to respond`);
184
+ throw new Error("Failed to stream response");
185
+ }
186
+ }
187
+ async edit(question: string, { onChunk, onReasoning, maxTry = MAX_ASK_TRY, validate, approve }: EditOptions = {}) {
188
+ for (let tryCount = 0; tryCount < maxTry; tryCount++) {
189
+ let response = await this.ask(question, { onChunk, onReasoning });
190
+ if (validate?.length && tryCount === 0) {
191
+ const validateQuestion = `Double check if the response meets the requirements and conditions, and follow the instructions. If not, rewrite it.
192
+ ${validate.map((v) => `- ${v}`).join("\n")}`;
193
+ response = await this.ask(validateQuestion, { onChunk, onReasoning });
194
+ }
195
+ const isConfirmed = approve
196
+ ? true
197
+ : await select({
198
+ message: "Do you want to edit the response?",
199
+ choices: [
200
+ { name: "✅ Yes, confirm and apply this result", value: true },
201
+ { name: "🔄 No, I want to edit it more", value: false },
202
+ ],
203
+ });
204
+ if (isConfirmed) {
205
+ await this.#saveCache();
206
+ return response.content;
207
+ }
208
+ question = await input({ message: "What do you want to change?" });
209
+ tryCount++;
210
+ }
211
+ throw new Error("Failed to edit");
212
+ }
213
+ async editTypescript(question: string, options: EditOptions = {}) {
214
+ const content = await this.edit(question, options);
215
+ return this.#getTypescriptCode(content);
216
+ }
217
+ #getTypescriptCode(content: string) {
218
+ //! will be deprecated
219
+ const code = /```(typescript|tsx)([\s\S]*?)```/.exec(content);
220
+ // 2번째로 해야 반환되는데 모르겟음 아무튼 일단 이렇게 함.
221
+
222
+ return code?.[2] ?? content;
223
+ // return code ? code[1] : content;
224
+ }
225
+ addToolMessgaes(messages: { type: string; content: string }[]) {
226
+ // const toolMessages = messages.map(
227
+ // (message) => new ToolMessage({ content: message.content, tool_call_id: message.type })
228
+ // );
229
+ const toolMessages = messages.map((message) => new HumanMessage(message.content));
230
+ this.messageHistory.push(...toolMessages);
231
+ return this;
232
+ }
233
+ async writeTypescripts(question: string, executor: Executor, options: EditOptions = {}) {
234
+ const content = await this.edit(question, options);
235
+ const writes = this.#getTypescriptCodes(content);
236
+ for (const write of writes) await executor.writeFile(write.filePath, write.content);
237
+ return await this.#tryFixTypescripts(writes, executor, options);
238
+ }
239
+ async #editTypescripts(question: string, options: EditOptions = {}) {
240
+ const content = await this.edit(question, options);
241
+ return this.#getTypescriptCodes(content);
242
+ }
243
+ async #tryFixTypescripts(writes: FileContent[], executor: Executor, options: EditOptions = {}) {
244
+ const MAX_EDIT_TRY = 5;
245
+ for (let tryCount = 0; tryCount < MAX_EDIT_TRY; tryCount++) {
246
+ const loader = new Spinner(`Type checking and linting...`, { prefix: `🤖akan-editor` }).start();
247
+ const fileChecks = await Promise.all(
248
+ writes.map(async ({ filePath }) => {
249
+ const typeCheckResult = executor.typeCheck(filePath);
250
+ const lintResult = await executor.lint(filePath);
251
+ const needFix = !!typeCheckResult.fileErrors.length || !!lintResult.errors.length;
252
+ return { filePath, typeCheckResult, lintResult, needFix };
253
+ }),
254
+ );
255
+ const needFix = fileChecks.some((fileCheck) => fileCheck.needFix);
256
+ if (needFix) {
257
+ loader.fail("Type checking and linting has some errors, try to fix them");
258
+ fileChecks.forEach((fileCheck) => {
259
+ Logger.rawLog(
260
+ `TypeCheck Result \n${fileCheck.typeCheckResult.message}\nLint Result \n${fileCheck.lintResult.message}`,
261
+ );
262
+ this.addToolMessgaes([
263
+ { type: "typescript", content: fileCheck.typeCheckResult.message },
264
+ { type: "eslint", content: fileCheck.lintResult.message },
265
+ ]);
266
+ });
267
+ writes = await this.#editTypescripts("Fix the typescript and eslint errors", {
268
+ ...options,
269
+ validate: undefined,
270
+ approve: true,
271
+ });
272
+ for (const write of writes) await executor.writeFile(write.filePath, write.content);
273
+ } else {
274
+ loader.succeed("Type checking and linting has no errors");
275
+ return writes;
276
+ }
277
+ }
278
+ throw new Error("Failed to create scalar");
279
+ }
280
+ #getTypescriptCodes(text: string): FileContent[] {
281
+ const codes = text.match(/```(typescript|tsx)([\s\S]*?)```/g);
282
+ if (!codes) return [];
283
+ const result = codes.map((code) => {
284
+ const content = /```(typescript|tsx)([\s\S]*?)```/.exec(code)?.[2];
285
+ if (!content) return null;
286
+ const filePath = /\/\/ File: (.*?)(?:\n|$)/.exec(content)?.[1]?.trim();
287
+ if (!filePath) return null;
288
+ const contentWithoutFilepath = content.replace(`// File: ${filePath}\n`, "").trim();
289
+ return { filePath, content: contentWithoutFilepath };
290
+ });
291
+ return result.filter((code) => code !== null) as FileContent[];
292
+ }
293
+ async editMarkdown(request: string, options: EditOptions = {}) {
294
+ const content = await this.edit(request, options);
295
+ return this.#getMarkdownContent(content);
296
+ }
297
+ #getMarkdownContent(text: string) {
298
+ const searchText = "```markdown";
299
+ const firstIndex = text.indexOf("```markdown");
300
+ const lastIndex = text.lastIndexOf("```");
301
+ if (firstIndex === -1) return text;
302
+ else return text.slice(firstIndex + searchText.length, lastIndex).trim();
303
+ }
304
+ }