@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
@@ -0,0 +1,42 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ or {
5
+ JsMethodClassMember() as $method where {
6
+ $method <: contains JsPrivateClassMemberName() as $name,
7
+ $method <: r"async\s+#",
8
+ $method <: contains `async` as $asyncToken,
9
+ $name <: r"#(.*)"($bareName),
10
+ $privateName = join(list = ["_", $bareName], separator = ""),
11
+ register_diagnostic(
12
+ span = $method,
13
+ message = "JavaScript private class methods (#) should not be used in mixin-enabled modules. Use a TypeScript private method with an underscore prefix instead.",
14
+ severity = "error"
15
+ ),
16
+ $asyncToken => `private async`,
17
+ $name => $privateName
18
+ },
19
+ JsMethodClassMember() as $method where {
20
+ $method <: contains JsPrivateClassMemberName() as $name,
21
+ not $method <: r"async\s+#",
22
+ $name <: r"#(.*)"($bareName),
23
+ $privateName = join(list = ["private _", $bareName], separator = ""),
24
+ register_diagnostic(
25
+ span = $method,
26
+ message = "JavaScript private class methods (#) should not be used in mixin-enabled modules. Use a TypeScript private method with an underscore prefix instead.",
27
+ severity = "error"
28
+ ),
29
+ $name => $privateName
30
+ },
31
+ JsCallExpression() as $call where {
32
+ $call <: contains JsPrivateName() as $name,
33
+ $name <: r"#(.*)"($bareName),
34
+ $privateName = join(list = ["_", $bareName], separator = ""),
35
+ register_diagnostic(
36
+ span = $call,
37
+ message = "Calls to JavaScript private class methods (#) should use the underscore-prefixed TypeScript private method.",
38
+ severity = "error"
39
+ ),
40
+ $name => $privateName
41
+ }
42
+ }
@@ -0,0 +1,7 @@
1
+ `$directive` where {
2
+ $directive <: `"use client"`,
3
+ register_diagnostic(
4
+ span = $directive,
5
+ message = "`use client` should not be used at the top of a server file."
6
+ )
7
+ }
@@ -0,0 +1,13 @@
1
+ `$name={$value}` where {
2
+ or {
3
+ $value <: `($$$_) => $_`,
4
+ $value <: `$_ => $_`,
5
+ $value <: `function($$$_) { $$$_ }`,
6
+ $value <: `function $fnName($$$_) { $$$_ }`
7
+ },
8
+ not $name <: or { `loader`, `render`, `of` },
9
+ register_diagnostic(
10
+ span = $name,
11
+ message = "Non-scalar props (function expressions) should not be used in server components. Allowed: loader, render, of."
12
+ )
13
+ }
package/linter.ts ADDED
@@ -0,0 +1,271 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import * as path from "node:path";
3
+ import { Logger } from "akanjs/common";
4
+ import chalk from "chalk";
5
+ // import { ESLint, type Linter as ESLintLinter } from "eslint";
6
+
7
+ export class Linter {
8
+ #logger = new Logger("Linter");
9
+ // #eslint: any;
10
+ lintRoot: string;
11
+
12
+ constructor(cwdPath: string) {
13
+ this.lintRoot = this.#findEslintRootPath(cwdPath);
14
+ // this.#eslint = new ESLint({ cwd: this.lintRoot, errorOnUnmatchedPattern: false });
15
+ }
16
+ #findEslintRootPath(dir: string): string {
17
+ const configPath = path.join(dir, "eslint.config.ts");
18
+ if (existsSync(configPath)) return dir;
19
+ const parentDir = path.dirname(dir);
20
+ return this.#findEslintRootPath(parentDir);
21
+ }
22
+ async lint(filePath: string, { fix = false, dryRun = false }: { fix?: boolean; dryRun?: boolean } = {}) {
23
+ if (fix) return await this.fixFile(filePath, dryRun);
24
+ return await this.lintFile(filePath);
25
+ }
26
+
27
+ /**
28
+ * Lint a single file using ESLint
29
+ * @param filePath - Path to the file to lint
30
+ * @returns Array of ESLint results
31
+ */
32
+ async lintFile(filePath: string): Promise<{
33
+ fixed: boolean;
34
+ output?: string;
35
+ results: any[]; // ESLint.LintResult[];
36
+ errors: any[]; // ESLintLinter.LintMessage[];
37
+ warnings: any[]; // ESLintLinter.LintMessage[];
38
+ }> {
39
+ if (!existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
40
+ // const isIgnored = await this.#eslint.isPathIgnored(filePath);
41
+ // if (isIgnored) {
42
+ // this.#logger.warn(`File ${filePath} is ignored by ESLint configuration`);
43
+ // return { fixed: false, results: [], errors: [], warnings: [] };
44
+ // }
45
+ // const results = await this.#eslint.lintFiles([filePath]);
46
+ // const errors = results.flatMap((result) => result.messages.filter((message) => message.severity === 2));
47
+ // const warnings = results.flatMap((result) => result.messages.filter((message) => message.severity === 1));
48
+ return { fixed: false, results: [], errors: [], warnings: [] };
49
+ }
50
+
51
+ /**
52
+ * Format lint results for console output
53
+ * @param results - Array of ESLint results
54
+ * @returns Formatted string
55
+ */
56
+ formatLintResults(results: any[]): string {
57
+ // ESLint.LintResult[]
58
+ if (results.length === 0) return "No files to lint";
59
+
60
+ const output: string[] = [];
61
+ let totalErrors = 0;
62
+ let totalWarnings = 0;
63
+
64
+ results.forEach((result) => {
65
+ totalErrors += result.errorCount;
66
+ totalWarnings += result.warningCount;
67
+
68
+ if (result.messages.length > 0) {
69
+ output.push(`\n${chalk.cyan(result.filePath)}`);
70
+
71
+ // Read source file once
72
+ let sourceLines: string[] = [];
73
+ if (existsSync(result.filePath)) {
74
+ try {
75
+ const sourceContent = readFileSync(result.filePath, "utf8");
76
+ sourceLines = sourceContent.split("\n");
77
+ } catch (error) {
78
+ // Ignore read errors
79
+ }
80
+ }
81
+
82
+ result.messages.forEach((message: any) => {
83
+ const type = message.severity === 2 ? "error" : "warning";
84
+ const typeColor = message.severity === 2 ? chalk.red : chalk.yellow;
85
+ const icon = message.severity === 2 ? "❌" : "⚠️";
86
+ const ruleInfo = message.ruleId ? chalk.dim(` (${message.ruleId})`) : "";
87
+
88
+ output.push(`\n ${icon} ${typeColor(type)}: ${message.message}${ruleInfo}`);
89
+ output.push(` ${chalk.gray("at")} ${result.filePath}:${chalk.bold(`${message.line}:${message.column}`)}`);
90
+
91
+ // Show source line with underline
92
+ if (sourceLines.length > 0 && message.line <= sourceLines.length) {
93
+ const sourceLine = sourceLines[message.line - 1];
94
+ const lineNumber = message.line.toString().padStart(5, " ");
95
+
96
+ output.push(`\n${chalk.dim(`${lineNumber} |`)} ${sourceLine}`);
97
+
98
+ // Create underline
99
+ const underlinePrefix = " ".repeat(message.column - 1);
100
+ const underlineLength = message.endColumn ? message.endColumn - message.column : 1;
101
+ const underline = "^".repeat(Math.max(1, underlineLength));
102
+
103
+ output.push(`${chalk.dim(`${" ".repeat(lineNumber.length)} |`)} ${underlinePrefix}${typeColor(underline)}`);
104
+ }
105
+ });
106
+ }
107
+ });
108
+
109
+ if (totalErrors === 0 && totalWarnings === 0) return chalk.bold("✅ No ESLint errors or warnings found");
110
+
111
+ const errorText = totalErrors > 0 ? chalk.red(`${totalErrors} error(s)`) : "0 errors";
112
+ const warningText = totalWarnings > 0 ? chalk.yellow(`${totalWarnings} warning(s)`) : "0 warnings";
113
+ const summary = [`\n${errorText}, ${warningText} found`];
114
+
115
+ return summary.concat(output).join("\n");
116
+ }
117
+
118
+ /**
119
+ * Get detailed lint information
120
+ * @param filePath - Path to the file to lint
121
+ * @returns Object containing detailed lint information
122
+ */
123
+ async getDetailedLintInfo(filePath: string): Promise<{
124
+ results: any[]; // ESLint.LintResult[];
125
+ details: {
126
+ line: number;
127
+ column: number;
128
+ message: string;
129
+ ruleId: string | null;
130
+ severity: "error" | "warning";
131
+ fix?: {
132
+ range: [number, number];
133
+ text: string;
134
+ };
135
+ suggestions?: {
136
+ desc: string;
137
+ fix: {
138
+ range: [number, number];
139
+ text: string;
140
+ };
141
+ }[];
142
+ }[];
143
+ stats: {
144
+ errorCount: number;
145
+ warningCount: number;
146
+ fixableErrorCount: number;
147
+ fixableWarningCount: number;
148
+ };
149
+ }> {
150
+ const { results } = await this.lintFile(filePath);
151
+
152
+ const details = results.flatMap((result) =>
153
+ result.messages.map((message: any) => ({
154
+ line: message.line,
155
+ column: message.column,
156
+ message: message.message,
157
+ ruleId: message.ruleId,
158
+ severity: message.severity === 2 ? ("error" as const) : ("warning" as const),
159
+ fix: message.fix,
160
+ suggestions: message.suggestions,
161
+ })),
162
+ );
163
+
164
+ const stats = results.reduce(
165
+ (acc, result) => ({
166
+ errorCount: acc.errorCount + result.errorCount,
167
+ warningCount: acc.warningCount + result.warningCount,
168
+ fixableErrorCount: acc.fixableErrorCount + result.fixableErrorCount,
169
+ fixableWarningCount: acc.fixableWarningCount + result.fixableWarningCount,
170
+ }),
171
+ { errorCount: 0, warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0 },
172
+ );
173
+
174
+ return { results, details, stats };
175
+ }
176
+
177
+ /**
178
+ * Check if a file has lint errors
179
+ * @param filePath - Path to the file to check
180
+ * @returns true if there are no errors, false otherwise
181
+ */
182
+ async hasNoLintErrors(filePath: string): Promise<boolean> {
183
+ try {
184
+ const { results } = await this.lintFile(filePath);
185
+ return results.every((result) => result.errorCount === 0);
186
+ } catch (error) {
187
+ return false;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Get only error messages (excluding warnings)
193
+ * @param filePath - Path to the file to lint
194
+ * @returns Array of error messages
195
+ */
196
+ async getErrors(filePath: string): Promise<any[]> {
197
+ // ESLintLinter.LintMessage[]
198
+ const { results } = await this.lintFile(filePath);
199
+ return results.flatMap((result) => result.messages.filter((message: any) => message.severity === 2));
200
+ }
201
+
202
+ /**
203
+ * Get only warning messages
204
+ * @param filePath - Path to the file to lint
205
+ * @returns Array of warning messages
206
+ */
207
+ async getWarnings(filePath: string): Promise<any[]> {
208
+ // ESLintLinter.LintMessage[]
209
+ const { results } = await this.lintFile(filePath);
210
+ return results.flatMap((result) => result.messages.filter((message: any) => message.severity === 1));
211
+ }
212
+
213
+ /**
214
+ * Fix lint errors automatically
215
+ * @param filePath - Path to the file to fix
216
+ * @param dryRun - If true, returns the fixed content without writing to file
217
+ * @returns Fixed content and remaining issues
218
+ */
219
+ async fixFile(
220
+ filePath: string,
221
+ dryRun = false,
222
+ ): Promise<{
223
+ fixed: boolean;
224
+ output?: string;
225
+ results: any[]; // ESLint.LintResult[];
226
+ errors: any[]; // ESLintLinter.LintMessage[];
227
+ warnings: any[]; // ESLintLinter.LintMessage[];
228
+ }> {
229
+ if (!existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
230
+
231
+ // const eslint = new ESLint({ cwd: this.lintRoot, fix: true });
232
+ // const results = await eslint.lintFiles([filePath]);
233
+ // const errors = results.flatMap((result) => result.messages.filter((message) => message.severity === 2));
234
+ // const warnings = results.flatMap((result) => result.messages.filter((message) => message.severity === 1));
235
+ // if (!dryRun) await ESLint.outputFixes(results);
236
+
237
+ // const fixedResult = results[0];
238
+ // return { fixed: fixedResult.output !== undefined, output: fixedResult.output, results, errors, warnings };
239
+ return { fixed: false, output: undefined, results: [], errors: [], warnings: [] };
240
+ }
241
+
242
+ /**
243
+ * Get ESLint configuration for a file
244
+ * @param filePath - Path to the file
245
+ * @returns ESLint configuration object
246
+ */
247
+ async getConfigForFile(filePath: string): Promise<unknown> {
248
+ // const eslint = new ESLint();
249
+ // const config = (await eslint.calculateConfigForFile(filePath)) as unknown;
250
+ // return config;
251
+ return {};
252
+ }
253
+
254
+ /**
255
+ * Get rules that are causing errors in a file
256
+ * @param filePath - Path to the file to check
257
+ * @returns Object mapping rule IDs to their error counts
258
+ */
259
+ async getProblematicRules(filePath: string): Promise<Record<string, number>> {
260
+ const { results } = await this.lintFile(filePath);
261
+ const ruleCounts: Record<string, number> = {};
262
+
263
+ results.forEach((result) => {
264
+ result.messages.forEach((message: any) => {
265
+ if (message.ruleId) ruleCounts[message.ruleId] = (ruleCounts[message.ruleId] || 0) + 1;
266
+ });
267
+ });
268
+
269
+ return ruleCounts;
270
+ }
271
+ }
@@ -0,0 +1 @@
1
+ export * from "./mobileTarget";
@@ -0,0 +1,53 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { AkanMobileTargetConfig } from "../akanConfig";
3
+ import type { App } from "../commandDecorators";
4
+ import { getMobileTargetChoices, resolveMobilePath, resolveMobileTargets, targetHtmlFilename } from "./mobileTarget";
5
+
6
+ const target = {
7
+ name: "akanjs",
8
+ basePath: "akanjs",
9
+ appName: "Akanjs",
10
+ appId: "com.akanjs.app",
11
+ version: "1.0.0",
12
+ buildNum: 1,
13
+ } as AkanMobileTargetConfig;
14
+
15
+ describe("mobile target helpers", () => {
16
+ test("maps deep links into target base paths without duplicating prefixes", () => {
17
+ expect(resolveMobilePath(target, "/order/123")).toBe("/akanjs/order/123");
18
+ expect(resolveMobilePath(target, "/akanjs/order/123")).toBe("/akanjs/order/123");
19
+ expect(resolveMobilePath({ ...target, basePath: undefined }, "/order/123")).toBe("/order/123");
20
+ });
21
+
22
+ test("selects the self-contained html file for a target", () => {
23
+ expect(targetHtmlFilename(target)).toBe("akanjs.html");
24
+ expect(targetHtmlFilename({ ...target, basePath: undefined })).toBe("index.html");
25
+ });
26
+
27
+ test("lists route base paths when only one mobile target is configured", async () => {
28
+ const app = {
29
+ getConfig: async () => ({
30
+ basePaths: new Set(["akanjs", "soft", "office"]),
31
+ mobile: { targets: { akanjs: target } },
32
+ }),
33
+ } as unknown as App;
34
+
35
+ await expect(getMobileTargetChoices(app)).resolves.toEqual(["akanjs", "soft", "office"]);
36
+ });
37
+
38
+ test("resolves a route base path onto the default mobile target config", async () => {
39
+ const app = {
40
+ getConfig: async () => ({
41
+ basePaths: new Set(["akanjs", "soft", "office"]),
42
+ mobile: { targets: { akanjs: target } },
43
+ }),
44
+ } as unknown as App;
45
+
46
+ await expect(resolveMobileTargets(app, "soft")).resolves.toEqual([
47
+ {
48
+ name: "soft",
49
+ config: { ...target, name: "soft", basePath: "soft" },
50
+ },
51
+ ]);
52
+ });
53
+ });
@@ -0,0 +1,88 @@
1
+ import type { AkanMobileTargetConfig, MobileEnv } from "../akanConfig";
2
+ import type { App } from "../commandDecorators";
3
+
4
+ export type MobilePlatform = "ios" | "android";
5
+ export type MobileTargetSelection = string | "all" | undefined;
6
+
7
+ export interface ResolvedMobileTarget {
8
+ name: string;
9
+ config: AkanMobileTargetConfig;
10
+ }
11
+
12
+ export const MOBILE_ENVS = ["local", "debug", "develop", "main"] as const satisfies readonly MobileEnv[];
13
+
14
+ export const getMobileTargets = async (app: App): Promise<ResolvedMobileTarget[]> => {
15
+ const config = await app.getConfig();
16
+ return Object.entries(config.mobile.targets).map(([name, target]) => ({ name, config: target }));
17
+ };
18
+
19
+ export const getMobileTargetChoices = async (app: App): Promise<string[]> => {
20
+ const config = await app.getConfig();
21
+ const targetNames = Object.keys(config.mobile.targets);
22
+ if (targetNames.length > 1) return targetNames;
23
+ const basePaths = [...config.basePaths];
24
+ if (basePaths.length > 1) return basePaths;
25
+ if (targetNames.length > 0) return targetNames;
26
+ return basePaths;
27
+ };
28
+
29
+ const resolveMobileTargetByBasePath = (
30
+ targets: ResolvedMobileTarget[],
31
+ basePath: string,
32
+ ): ResolvedMobileTarget | undefined => {
33
+ const normalizedBasePath = basePath.replace(/^\/+|\/+$/g, "");
34
+ const byBasePath = targets.find((target) => target.config.basePath?.replace(/^\/+|\/+$/g, "") === normalizedBasePath);
35
+ if (byBasePath) return byBasePath;
36
+ const [template] = targets;
37
+ if (!template) return undefined;
38
+ return {
39
+ name: normalizedBasePath,
40
+ config: {
41
+ ...template.config,
42
+ name: normalizedBasePath,
43
+ basePath: normalizedBasePath,
44
+ },
45
+ };
46
+ };
47
+
48
+ export const resolveMobileTargets = async (
49
+ app: App,
50
+ selection: MobileTargetSelection,
51
+ ): Promise<ResolvedMobileTarget[]> => {
52
+ const config = await app.getConfig();
53
+ const targets = await getMobileTargets(app);
54
+ if (targets.length === 0) throw new Error(`No mobile targets configured for ${app.name}`);
55
+ if (!selection) {
56
+ const choices = await getMobileTargetChoices(app);
57
+ if (choices.length === 1) return resolveMobileTargets(app, choices[0]);
58
+ throw new Error(`Multiple mobile targets found for ${app.name}. Pass --target <${choices.join("|")}|all>.`);
59
+ }
60
+ if (selection === "all") {
61
+ if (Object.keys(config.mobile.targets).length > 1) return targets;
62
+ const basePaths = [...config.basePaths];
63
+ if (basePaths.length > 1) {
64
+ return basePaths.flatMap((basePath) => {
65
+ const resolved = resolveMobileTargetByBasePath(targets, basePath);
66
+ return resolved ? [resolved] : [];
67
+ });
68
+ }
69
+ return targets;
70
+ }
71
+ const target = targets.find((candidate) => candidate.name === selection);
72
+ if (target) return [target];
73
+ const basePathTarget = resolveMobileTargetByBasePath(targets, selection);
74
+ if (basePathTarget && config.basePaths.has(selection.replace(/^\/+|\/+$/g, ""))) return [basePathTarget];
75
+ const choices = await getMobileTargetChoices(app);
76
+ throw new Error(`Mobile target '${selection}' was not found. Available: ${choices.join(", ")}`);
77
+ };
78
+
79
+ export const resolveMobilePath = (target: AkanMobileTargetConfig, pathname: string) => {
80
+ const basePath = target.basePath?.replace(/^\/+|\/+$/g, "");
81
+ const normalizedPath = `/${pathname.replace(/^\/+/, "")}`;
82
+ if (!basePath) return normalizedPath;
83
+ if (normalizedPath === `/${basePath}` || normalizedPath.startsWith(`/${basePath}/`)) return normalizedPath;
84
+ return `/${basePath}${normalizedPath === "/" ? "" : normalizedPath}`;
85
+ };
86
+
87
+ export const targetHtmlFilename = (target: AkanMobileTargetConfig) =>
88
+ target.basePath?.replace(/^\/+|\/+$/g, "") ? `${target.basePath.replace(/^\/+|\/+$/g, "")}.html` : "index.html";
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "1.0.20",
3
+ "version": "2.1.0-rc.1",
4
4
  "sourceType": "module",
5
+ "type": "module",
5
6
  "publishConfig": {
6
7
  "access": "public"
7
8
  },
@@ -12,41 +13,57 @@
12
13
  "url": "https://github.com/akan-team/akanjs.git",
13
14
  "directory": "pkgs/@akanjs/devkit"
14
15
  },
15
- "main": "./index.js",
16
16
  "engines": {
17
- "node": ">=20"
17
+ "bun": ">=1.3.13"
18
+ },
19
+ "exports": {
20
+ ".": {
21
+ "import": "./index.ts",
22
+ "types": "./index.ts",
23
+ "default": "./index.ts"
24
+ },
25
+ "./package.json": "./package.json",
26
+ "./capacitor.base.config": {
27
+ "import": "./capacitor.base.config.ts",
28
+ "types": "./capacitor.base.config.ts",
29
+ "default": "./capacitor.base.config.ts"
30
+ },
31
+ "./*": "./*"
18
32
  },
19
33
  "dependencies": {
20
- "@inquirer/prompts": "^7.2.1",
21
- "@langchain/core": "^0.3.56",
22
- "@langchain/deepseek": "^0.0.1",
23
- "@langchain/openai": "^0.5.10",
24
- "@trapezedev/project": "^7.1.3",
25
- "axios": "^1.7.9",
26
- "chalk": "^5.4.1",
27
- "commander": "^14.0.2",
28
- "dotenv": "^16.4.7",
29
- "esbuild": "^0.19.2",
30
- "esbuild-plugin-d.ts": "^1.3.1",
31
- "eslint": "^9.0.0",
32
- "form-data": "^4.0.1",
34
+ "@inquirer/prompts": "^8.4.3",
35
+ "@langchain/core": "^1.1.47",
36
+ "@langchain/deepseek": "^1.0.26",
37
+ "@langchain/openai": "^1.4.6",
38
+ "@trapezedev/project": "^7.1.4",
39
+ "akanjs": "2.1.0-rc.0",
40
+ "chalk": "^5.6.2",
41
+ "commander": "^14.0.3",
42
+ "fontaine": "^0.8.0",
43
+ "fonteditor-core": "^2.6.3",
33
44
  "ignore": "^7.0.5",
34
- "ink": "^6.1.0",
35
- "js-yaml": "^4.1.0",
36
- "ora": "^3.4.0",
37
- "react": "19.2.3",
38
- "reflect-metadata": "^0.2.2",
39
- "tunnel-ssh": "^5.2.0",
40
- "typescript": "5.8.3"
45
+ "ink": "^6.8.0",
46
+ "js-yaml": "^4.1.1",
47
+ "ora": "^9.4.0",
48
+ "ssh2": "^1.17.0",
49
+ "subset-font": "^2.5.0",
50
+ "tailwindcss": "^4.3.0",
51
+ "typescript": "^6.0.3"
41
52
  },
42
- "exports": {
43
- ".": {
44
- "require": "./cjs/index.js",
45
- "import": "./esm/index.js",
46
- "types": "./index.d.ts"
53
+ "peerDependencies": {
54
+ "@capacitor/cli": "^8.3.4",
55
+ "react": "19.2.6"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "@capacitor/cli": {
59
+ "optional": true
60
+ },
61
+ "react": {
62
+ "optional": true
47
63
  }
48
64
  },
49
- "esbuild": {
50
- "platform": "node"
65
+ "devDependencies": {},
66
+ "bun": {
67
+ "platform": "bun"
51
68
  }
52
- }
69
+ }
package/prompter.ts ADDED
@@ -0,0 +1,72 @@
1
+ import fsPromise from "node:fs/promises";
2
+ import { input, select } from "@inquirer/prompts";
3
+
4
+ import { getDirname } from "./getDirname";
5
+ import type { GuideGenerateJson } from "./guideline";
6
+
7
+ interface FileUpdateRequestProps {
8
+ context: string;
9
+ request: string;
10
+ }
11
+ export class Prompter {
12
+ static async #getGuidelineRoot() {
13
+ const dirname = getDirname(import.meta.url);
14
+ const candidates = [`${dirname}/guidelines`, `${dirname}/../cli/guidelines`];
15
+ for (const candidate of candidates) {
16
+ try {
17
+ await fsPromise.access(candidate);
18
+ return candidate;
19
+ } catch {
20
+ // Try the next layout; source and bundled CLI resolve from different dirs.
21
+ }
22
+ }
23
+ return candidates[0];
24
+ }
25
+
26
+ static async selectGuideline() {
27
+ const guidelineRoot = await Prompter.#getGuidelineRoot();
28
+ const guideNames = (await fsPromise.readdir(guidelineRoot)).filter((name) => !name.startsWith("_"));
29
+ return await select({ message: "Select a guideline", choices: guideNames.map((name) => ({ name, value: name })) });
30
+ }
31
+ static async getGuideJson(guideName: string): Promise<GuideGenerateJson> {
32
+ const guidelineRoot = await Prompter.#getGuidelineRoot();
33
+ const filePath = `${guidelineRoot}/${guideName}/${guideName}.generate.json`;
34
+ const guideJson = await fsPromise.readFile(filePath, "utf-8");
35
+ return JSON.parse(guideJson) as GuideGenerateJson;
36
+ }
37
+ static async getInstruction(guideName: string): Promise<string> {
38
+ const guidelineRoot = await Prompter.#getGuidelineRoot();
39
+ const filePath = `${guidelineRoot}/${guideName}/${guideName}.instruction.md`;
40
+ const content = await fsPromise.readFile(filePath, "utf-8");
41
+ return content;
42
+ }
43
+ static async getUpdateRequest(guideName: string) {
44
+ return await input({ message: `What do you want to update in ${guideName}?` });
45
+ }
46
+
47
+ async makeTsFileUpdatePrompt({ context, request }: FileUpdateRequestProps) {
48
+ return `You are a senior developer writing TypeScript-based programs using Akan.js, an in-house framework. Here's an overview of the Akan.js framework:
49
+ ${await this.getDocumentation("framework")}
50
+ Please understand the following background information, write code that meets the requirements, verify that it satisfies the validation conditions, and return the result.
51
+
52
+ # Background Information
53
+ \`\`\`markdown
54
+ ${context}
55
+ \`\`\`
56
+
57
+ # Requirements
58
+ \`\`\`markdown
59
+ ${request}
60
+ \`\`\`
61
+ `;
62
+ }
63
+ async getDocumentation(guideName: string) {
64
+ const guidelineRoot = await Prompter.#getGuidelineRoot();
65
+ const filePath = `${guidelineRoot}/${guideName}/${guideName}.instruction.md`;
66
+ const document = await fsPromise.readFile(filePath, "utf-8");
67
+ return `\`\`\`markdown
68
+ ${document}
69
+ \`\`\`
70
+ `;
71
+ }
72
+ }