@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
@@ -1,25 +1,39 @@
1
- import { capitalize } from "@akanjs/common";
1
+ import type { CapacitorConfig } from "@capacitor/cli";
2
2
  import { MobileProject } from "@trapezedev/project";
3
- import fs from "fs";
3
+ import type { AndroidProject } from "@trapezedev/project/dist/android/project";
4
+ import type { IosProject } from "@trapezedev/project/dist/ios/project";
5
+ import { capitalize } from "akanjs/common";
6
+ import { type AppExecutor, FileSys } from "@akanjs/devkit";
7
+
4
8
  import { FileEditor } from "./fileEditor";
5
- class CapacitorApp {
6
- constructor(app) {
7
- this.app = app;
9
+
10
+ interface RunConfig extends CapacitorConfig {
11
+ operation: "local" | "release";
12
+ version: string;
13
+ buildNum: number;
14
+ appId?: string;
15
+ host?: "local" | "debug" | "develop" | "main";
16
+ }
17
+
18
+ export class CapacitorApp {
19
+ project: MobileProject & { ios: IosProject; android: AndroidProject };
20
+ iosTargetName = "App";
21
+ constructor(private readonly app: AppExecutor) {
8
22
  this.project = new MobileProject(this.app.cwdPath, {
9
23
  android: { path: "android" },
10
- ios: { path: "ios/App" }
11
- });
24
+ ios: { path: "ios/App" },
25
+ }) as MobileProject & { ios: IosProject; android: AndroidProject };
12
26
  }
13
- project;
14
- iosTargetName = "App";
15
27
  async init() {
16
- const project = this.project;
28
+ const project = this.project as MobileProject;
17
29
  await this.project.load();
18
- if (!project.android) {
30
+ const hasAndroid = await FileSys.fileExists(`${this.app.cwdPath}/android/app/build.gradle`);
31
+ const hasIos = await FileSys.fileExists(`${this.app.cwdPath}/ios/App/App.xcodeproj/project.pbxproj`);
32
+ if (!project.android && !hasAndroid) {
19
33
  await this.app.spawn("npx", ["cap", "add", "android"]);
20
34
  await this.project.load();
21
35
  }
22
- if (!project.ios) {
36
+ if (!project.ios && !hasIos) {
23
37
  await this.app.spawn("npx", ["cap", "add", "ios"]);
24
38
  await this.project.load();
25
39
  }
@@ -29,12 +43,11 @@ class CapacitorApp {
29
43
  await this.project.commit();
30
44
  }
31
45
  async #prepareIos() {
32
- const isAdded = fs.existsSync(`${this.app.cwdPath}/ios/App/Podfile`);
46
+ const isAdded = await FileSys.fileExists(`${this.app.cwdPath}/ios/App/App.xcodeproj/project.pbxproj`);
33
47
  if (!isAdded) {
34
48
  await this.app.spawn("npx", ["cap", "add", "ios"]);
35
49
  await this.app.spawn("npx", ["@capacitor/assets", "generate"]);
36
- } else
37
- this.app.verbose(`iOS already added, skip adding process`);
50
+ } else this.app.verbose(`iOS already added, skip adding process`);
38
51
  this.app.verbose(`syncing iOS`);
39
52
  await this.app.spawn("npx", ["cap", "sync", "ios"]);
40
53
  this.app.verbose(`sync completed.`);
@@ -50,7 +63,7 @@ class CapacitorApp {
50
63
  async openIos() {
51
64
  await this.app.spawn("npx", ["cap", "open", "ios"]);
52
65
  }
53
- async runIos({ operation, appId, version = "0.0.1", buildNum = 1, host = "local" }) {
66
+ async runIos({ operation, appId, version = "0.0.1", buildNum = 1, host = "local" }: RunConfig) {
54
67
  const defaultAppId = `com.${this.app.name}.app`;
55
68
  await this.#prepareIos();
56
69
  this.project.ios.setBundleId("App", "Debug", appId ?? defaultAppId);
@@ -62,35 +75,29 @@ class CapacitorApp {
62
75
  await this.project.commit();
63
76
  await this.app.spawn(
64
77
  "npx",
65
- [
66
- "cross-env",
67
- `APP_OPERATION_MODE=${operation}`,
68
- `NEXT_PUBLIC_ENV=${host}`,
69
- "npx",
70
- "cap",
71
- "run",
72
- "ios",
73
- "--live-reload",
74
- operation === "release" ? "" : "--live-reload",
75
- operation === "release" ? "" : "--port",
76
- operation === "release" ? "" : "4201"
77
- ],
78
+ ["cross-env", `APP_OPERATION_MODE=${operation}`, `BUN_PUBLIC_ENV=${host}`, "npx", "cap", "run", "ios"],
78
79
  {
79
- stdio: "inherit"
80
- }
80
+ stdio: "inherit",
81
+ },
81
82
  );
83
+
84
+ // this.project.ios.incrementBuild("App", "Debug");
85
+ // this.project.ios.incrementBuild("App", "Release");
82
86
  }
87
+
83
88
  async #prepareAndroid() {
84
- const isAdded = fs.existsSync(`${this.app.cwdPath}/android/app/build.gradle`);
89
+ const isAdded = await Bun.file(`${this.app.cwdPath}/android/app/build.gradle`).exists();
85
90
  if (!isAdded) {
86
91
  await this.app.spawn("npx", ["cap", "add", "android"]);
87
- } else
88
- this.app.verbose(`Android already added, skip adding process`);
92
+ } else this.app.verbose(`Android already added, skip adding process`);
89
93
  await this.app.spawn("npx", ["@capacitor/assets", "generate"]);
90
94
  await this.app.spawn("npx", ["cap", "sync", "android"]);
91
95
  }
92
- #updateAndroidBuildTypes() {
93
- const appGradle = new FileEditor(`${this.app.cwdPath}/android/app/build.gradle`);
96
+
97
+ async #updateAndroidBuildTypes() {
98
+ //keystore 기본 설정 및 debug, release 설정
99
+
100
+ const appGradle = await FileEditor.create(`${this.app.cwdPath}/android/app/build.gradle`);
94
101
  const buildTypesBlock = `
95
102
  debug {
96
103
  applicationIdSuffix ".debug"
@@ -123,16 +130,18 @@ class CapacitorApp {
123
130
  if (appGradle.find(`applicationIdSuffix ".debug"`) === -1) {
124
131
  appGradle.insertAfter("buildTypes {", buildTypesBlock);
125
132
  }
126
- appGradle.save();
133
+ await appGradle.save();
127
134
  }
128
- async buildAndroid(assembleType) {
135
+ async buildAndroid(assembleType: "apk" | "aab") {
129
136
  await this.#prepareAndroid();
130
- this.#updateAndroidBuildTypes();
137
+ await this.#updateAndroidBuildTypes();
138
+ //윈도우는 gradlew.bat 사용
131
139
  const isWindows = process.platform === "win32";
132
140
  const gradleCommand = isWindows ? "gradlew.bat" : "./gradlew";
141
+
133
142
  await this.app.spawn(gradleCommand, [assembleType === "apk" ? "assembleRelease" : "bundleRelease"], {
134
143
  stdio: "inherit",
135
- cwd: `${this.app.cwdPath}/android`
144
+ cwd: `${this.app.cwdPath}/android`,
136
145
  });
137
146
  }
138
147
  async openAndroid() {
@@ -142,7 +151,7 @@ class CapacitorApp {
142
151
  await this.#prepareAndroid();
143
152
  this.app.log(`Sync Android Completed.`);
144
153
  }
145
- async runAndroid({ operation, appName, appId, version = "0.0.1", buildNum = 1, host = "local" }) {
154
+ async runAndroid({ operation, appName, appId, version = "0.0.1", buildNum = 1, host = "local" }: RunConfig) {
146
155
  const defaultAppId = `com.${this.app.name}.app`;
147
156
  const defaultAppName = this.app.name;
148
157
  await this.project.android.setVersionName(version);
@@ -153,28 +162,20 @@ class CapacitorApp {
153
162
  await this.project.android.setAppName(appName ?? defaultAppName);
154
163
  await this.project.commit();
155
164
  await this.#prepareAndroid();
165
+
156
166
  this.app.logger.info(`Running Android in ${operation} mode on ${host} host`);
157
167
  await this.app.spawn(
158
168
  "npx",
159
- [
160
- "cross-env",
161
- `NEXT_PUBLIC_ENV=${host}`,
162
- `APP_OPERATION_MODE=${operation}`,
163
- "npx",
164
- "cap",
165
- "run",
166
- "android",
167
- operation === "release" ? "" : "--live-reload",
168
- operation === "release" ? "" : "--port",
169
- operation === "release" ? "" : "4201"
170
- ],
169
+ ["cross-env", `BUN_PUBLIC_ENV=${host}`, `APP_OPERATION_MODE=${operation}`, "npx", "cap", "run", "android"],
171
170
  {
172
- stdio: "inherit"
173
- }
171
+ stdio: "inherit",
172
+ },
174
173
  );
175
174
  }
175
+
176
176
  //? 릴리즈시 buildNum +1 version 파라미터 받아서 업데이트
177
- async updateAndroidVersion(version, buildNum) {
177
+ async updateAndroidVersion(version: string, buildNum: number) {
178
+ //TODO: 테스트 확인 필요
178
179
  await this.project.android.setVersionName(version);
179
180
  await this.project.android.setVersionCode(buildNum);
180
181
  const versionName = await this.project.android.getVersionName();
@@ -182,99 +183,99 @@ class CapacitorApp {
182
183
  await this.project.commit();
183
184
  }
184
185
  async releaseIos() {
185
- const isAdded = fs.existsSync(`${this.app.cwdPath}/ios/App/Podfile`);
186
+ //TODO: 작업 필요
187
+ const isAdded = await Bun.file(`${this.app.cwdPath}/ios/App/App.xcodeproj/project.pbxproj`).exists();
186
188
  if (!isAdded) {
187
189
  await this.app.spawn("npx cap add ios");
188
190
  await this.app.spawn("npx @capacitor/assets generate");
189
- } else
190
- this.app.log(`iOS already added, skip adding process`);
191
+ } else this.app.log(`iOS already added, skip adding process`);
191
192
  await this.app.spawn("cross-env", ["APP_OPERATION_MODE=release", "npx", "cap", "sync", "ios"]);
192
193
  }
193
194
  async releaseAndroid() {
194
- const isAdded = fs.existsSync(`${this.app.cwdPath}/android/app/build.gradle`);
195
+ //TODO: 작업 필요
196
+ const isAdded = await Bun.file(`${this.app.cwdPath}/android/app/build.gradle`).exists();
195
197
  if (!isAdded) {
196
198
  await this.app.spawn("npx cap add android");
197
199
  await this.app.spawn("npx @capacitor/assets generate");
198
- } else
199
- this.app.log(`android already added, skip adding process`);
200
+ } else this.app.log(`android already added, skip adding process`);
200
201
  await this.app.spawn("cross-env", ["APP_OPERATION_MODE=release", "npx", "cap", "sync", "android"]);
201
202
  }
202
203
  async addCamera() {
203
204
  await this.#setPermissionInIos({
204
205
  cameraUsageDescription: "$(PRODUCT_NAME) requires access to the camera to take photos.",
205
206
  photoAddUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos.",
206
- photoUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos."
207
+ photoUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos.",
207
208
  });
208
209
  this.#setPermissionsInAndroid(["READ_MEDIA_IMAGES", "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"]);
209
210
  }
210
211
  async addContact() {
211
212
  await this.#setPermissionInIos({
212
- contactsUsageDescription: "$(PRODUCT_NAME) requires access to the contacts to add new contacts."
213
+ contactsUsageDescription: "$(PRODUCT_NAME) requires access to the contacts to add new contacts.",
213
214
  });
214
215
  this.#setPermissionsInAndroid(["READ_CONTACTS", "WRITE_CONTACTS"]);
215
216
  }
216
217
  async addLocation() {
217
218
  await this.#setPermissionInIos({
218
219
  locationAlwaysUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location.",
219
- locationWhenInUseUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location."
220
+ locationWhenInUseUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location.",
220
221
  });
221
222
  this.#setPermissionsInAndroid(["ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION"]);
222
223
  this.#setFeaturesInAndroid(["android.hardware.location.gps"]);
223
224
  }
224
- async #setPermissionInIos(permissions) {
225
+ async #setPermissionInIos(permissions: { [key: string]: string }) {
225
226
  const updateNs = Object.fromEntries(
226
- Object.entries(permissions).map(([key, value]) => [`NS${capitalize(key)}`, value])
227
+ Object.entries(permissions).map(([key, value]) => [`NS${capitalize(key)}`, value]),
227
228
  );
228
229
  await Promise.all([
229
230
  this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", updateNs),
230
- this.project.ios.updateInfoPlist(this.iosTargetName, "Release", updateNs)
231
+ this.project.ios.updateInfoPlist(this.iosTargetName, "Release", updateNs),
231
232
  ]);
232
233
  }
233
- #setFeaturesInAndroid(features) {
234
+ #setFeaturesInAndroid(features: string[]) {
234
235
  for (const feature of features) {
235
236
  if (this.#hasFeatureInAndroid(feature)) {
236
237
  this.app.logger.info(`${feature} already exists in android`);
237
238
  return this;
238
239
  }
239
240
  this.app.logger.info(`Adding ${feature} to android`);
240
- this.project.android.getAndroidManifest().injectFragment("manifest", `<uses-feature android:name="${feature}" />`);
241
+ this.project.android
242
+ .getAndroidManifest()
243
+ .injectFragment("manifest", `<uses-feature android:name="${feature}" />`);
241
244
  }
242
245
  return this;
243
246
  }
244
247
  #getFeaturesInAndroid() {
245
248
  const androidManifest = this.project.android.getAndroidManifest();
246
249
  const element = androidManifest.getDocumentElement();
247
- if (!element)
248
- throw new Error("manifest not found");
250
+ if (!element) throw new Error("manifest not found");
249
251
  const usesFeature = element.getElementsByTagName("uses-feature");
250
252
  return Array.from(usesFeature).map((feature) => feature.getAttribute("android:name"));
251
253
  }
252
- #hasFeatureInAndroid(feature) {
254
+ #hasFeatureInAndroid(feature: string) {
253
255
  return this.#getFeaturesInAndroid().includes(feature);
254
256
  }
255
- #setPermissionsInAndroid(permissions) {
257
+
258
+ #setPermissionsInAndroid(permissions: string[]) {
256
259
  for (const permission of permissions) {
257
260
  if (this.#hasPermissionInAndroid(permission)) {
258
261
  this.app.logger.info(`${permission} already exists in android`);
259
262
  return this;
260
263
  }
261
264
  this.app.logger.info(`Adding ${permission} to android`);
262
- this.project.android.getAndroidManifest().injectFragment("manifest", `<uses-permission android:name="android.permission.${permission}" />`);
265
+ this.project.android
266
+ .getAndroidManifest()
267
+ .injectFragment("manifest", `<uses-permission android:name="android.permission.${permission}" />`);
263
268
  }
264
269
  return this;
265
270
  }
266
271
  #getPermissionsInAndroid() {
267
272
  const androidManifest = this.project.android.getAndroidManifest();
268
273
  const element = androidManifest.getDocumentElement();
269
- if (!element)
270
- throw new Error("manifest not found");
274
+ if (!element) throw new Error("manifest not found");
271
275
  const usesPermission = element.getElementsByTagName("uses-permission");
272
276
  return Array.from(usesPermission).map((permission) => permission.getAttribute("android:name"));
273
277
  }
274
- #hasPermissionInAndroid(permission) {
278
+ #hasPermissionInAndroid(permission: string) {
275
279
  return this.#getPermissionsInAndroid().includes(permission);
276
280
  }
277
281
  }
278
- export {
279
- CapacitorApp
280
- };
package/sshTunnel.ts ADDED
@@ -0,0 +1,152 @@
1
+ import { type Channel, Client, type ConnectConfig } from "ssh2";
2
+
3
+ export interface SshTunnelOptions {
4
+ localHost?: string;
5
+ localPort: number;
6
+ srcHost?: string;
7
+ srcPort?: number;
8
+ dstHost: string;
9
+ dstPort: number;
10
+ sshOptions: ConnectConfig;
11
+ }
12
+
13
+ export interface SshTunnel {
14
+ close(): void;
15
+ }
16
+
17
+ interface TunnelSocketData {
18
+ stream?: Channel;
19
+ pending: Buffer[];
20
+ closed: boolean;
21
+ }
22
+
23
+ type BunServer = { stop(): void };
24
+ type BunSocket = Bun.Socket<TunnelSocketData>;
25
+
26
+ const closeQuietly = (close: () => void) => {
27
+ try {
28
+ close();
29
+ } catch {
30
+ // The opposite side may already have closed the tunnel.
31
+ }
32
+ };
33
+
34
+ const createSshClient = async (options: SshTunnelOptions) =>
35
+ new Promise<Client>((resolve, reject) => {
36
+ const client = new Client();
37
+
38
+ const cleanup = () => {
39
+ client.off("ready", onReady);
40
+ client.off("error", onError);
41
+ client.off("close", onClose);
42
+ };
43
+
44
+ const onReady = () => {
45
+ cleanup();
46
+ resolve(client);
47
+ };
48
+
49
+ const onError = (error: Error) => {
50
+ cleanup();
51
+ reject(error);
52
+ };
53
+
54
+ const onClose = () => {
55
+ cleanup();
56
+ reject(new Error("SSH tunnel closed before it was ready"));
57
+ };
58
+
59
+ client.once("ready", onReady);
60
+ client.once("error", onError);
61
+ client.once("close", onClose);
62
+ client.connect({ readyTimeout: 10000, ...options.sshOptions });
63
+ });
64
+
65
+ export const createSshTunnel = async (options: SshTunnelOptions): Promise<SshTunnel> => {
66
+ const client = await createSshClient(options);
67
+ const sockets = new Set<BunSocket>();
68
+ let closing = false;
69
+
70
+ const closeSocket = (socket: BunSocket, error?: Error) => {
71
+ sockets.delete(socket);
72
+ closeBunSocket(socket, error);
73
+ };
74
+
75
+ try {
76
+ const server = Bun.listen<TunnelSocketData>({
77
+ hostname: options.localHost ?? "127.0.0.1",
78
+ port: options.localPort,
79
+ socket: {
80
+ open(socket) {
81
+ sockets.add(socket);
82
+ socket.data = { pending: [], closed: false };
83
+
84
+ client.forwardOut(
85
+ options.srcHost ?? "127.0.0.1",
86
+ options.srcPort ?? options.localPort,
87
+ options.dstHost,
88
+ options.dstPort,
89
+ (error, stream) => {
90
+ if (error) {
91
+ closeSocket(socket, error);
92
+ return;
93
+ }
94
+
95
+ if (socket.data.closed) {
96
+ stream.destroy();
97
+ return;
98
+ }
99
+
100
+ socket.data.stream = stream;
101
+ stream.on("data", (chunk: Buffer) => socket.write(chunk));
102
+ stream.once("close", () => closeSocket(socket));
103
+ stream.once("error", (streamError: Error) => closeSocket(socket, streamError));
104
+
105
+ for (const chunk of socket.data.pending) stream.write(chunk);
106
+ socket.data.pending = [];
107
+ },
108
+ );
109
+ },
110
+ data(socket, chunk) {
111
+ const buffer = Buffer.from(chunk);
112
+ if (socket.data.stream) socket.data.stream.write(buffer);
113
+ else socket.data.pending.push(buffer);
114
+ },
115
+ close(socket) {
116
+ closeSocket(socket);
117
+ },
118
+ error(socket, error) {
119
+ closeSocket(socket, error);
120
+ },
121
+ },
122
+ });
123
+
124
+ const closeTunnel = (error?: Error) => {
125
+ if (closing) return;
126
+ closing = true;
127
+ for (const socket of sockets) closeSocket(socket, error);
128
+ closeQuietly(() => client.end());
129
+ closeBunServer(server);
130
+ };
131
+
132
+ client.once("error", (error) => closeTunnel(error));
133
+ client.once("close", () => closeTunnel());
134
+
135
+ return { close: closeTunnel };
136
+ } catch (error) {
137
+ closeQuietly(() => client.end());
138
+ throw error;
139
+ }
140
+ };
141
+
142
+ const closeBunSocket = (socket: BunSocket, error?: Error) => {
143
+ if (socket.data.closed) return;
144
+ socket.data.closed = true;
145
+ if (error) socket.data.stream?.destroy();
146
+ else socket.data.stream?.end();
147
+ closeQuietly(() => socket.end());
148
+ };
149
+
150
+ const closeBunServer = (server: BunServer) => {
151
+ server.stop();
152
+ };
@@ -1,18 +1,25 @@
1
1
  import { PromptTemplate } from "@langchain/core/prompts";
2
2
  import { RunnableSequence } from "@langchain/core/runnables";
3
3
  import { ChatOpenAI } from "@langchain/openai";
4
- const streamAi = async (question, callback = (chunk) => {
5
- process.stdout.write(chunk);
6
- }) => {
4
+
5
+ interface StreamResponse {
6
+ content: string;
7
+ chunk?: string;
8
+ }
9
+
10
+ export const streamAi = async (
11
+ question: string,
12
+ callback: (chunk: string) => void = (chunk) => {
13
+ process.stdout.write(chunk);
14
+ },
15
+ ): Promise<StreamResponse> => {
7
16
  const createStreamingModel = (apiKey = process.env.DEEPSEEK_API_KEY) => {
8
- if (!apiKey)
9
- throw new Error(`process.env.DEEPSEEK_API_KEY is not set`);
17
+ if (!apiKey) throw new Error(`process.env.DEEPSEEK_API_KEY is not set`);
10
18
  return new ChatOpenAI({
11
19
  modelName: "deepseek-reasoner",
12
20
  temperature: 0.7,
13
- streaming: true,
14
- // Enable streaming
15
- configuration: { baseURL: "https://api.deepseek.com/v1", apiKey }
21
+ streaming: true, // Enable streaming
22
+ configuration: { baseURL: "https://api.deepseek.com/v1", apiKey },
16
23
  });
17
24
  };
18
25
  const createProcessingChain = () => {
@@ -21,19 +28,18 @@ const streamAi = async (question, callback = (chunk) => {
21
28
  try {
22
29
  const chain = createProcessingChain();
23
30
  const stream = await chain.stream({ question });
31
+
24
32
  let fullResponse = "";
25
33
  for await (const chunk of stream) {
26
34
  const content = chunk.content;
27
35
  if (typeof content === "string") {
28
36
  fullResponse += content;
29
- callback(content);
37
+ callback(content); // Send individual chunks to callback
30
38
  }
31
39
  }
40
+
32
41
  return { content: fullResponse };
33
42
  } catch (error) {
34
43
  throw new Error("Failed to stream response");
35
44
  }
36
45
  };
37
- export {
38
- streamAi
39
- };