@akanjs/devkit 2.1.1-rc.0 → 2.1.1-rc.2

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/aiEditor.ts CHANGED
@@ -12,14 +12,17 @@ import { ChatOpenAI } from "@langchain/openai";
12
12
  import { Logger } from "akanjs/common";
13
13
  import chalk from "chalk";
14
14
 
15
- import { getAkanGlobalConfig, setAkanGlobalConfig } from "./auth";
15
+ import { GlobalConfig } from "./cloud";
16
16
  import type { Executor, WorkspaceExecutor } from "./executors";
17
17
  import { Spinner } from "./spinner";
18
18
  import type { FileContent } from "./types";
19
19
 
20
20
  const MAX_ASK_TRY = 300;
21
21
 
22
- export const supportedLlmModels = ["deepseek-chat", "deepseek-reasoner"] as const;
22
+ export const supportedLlmModels = [
23
+ "deepseek-chat",
24
+ "deepseek-reasoner",
25
+ ] as const;
23
26
  export type SupportedLlmModel = (typeof supportedLlmModels)[number];
24
27
 
25
28
  interface EditOptions {
@@ -33,15 +36,30 @@ interface EditOptions {
33
36
  export class AiSession {
34
37
  static #cacheDir = "node_modules/.cache/akan/aiSession";
35
38
  static #chat: ChatDeepSeek | ChatOpenAI | null = null;
36
- static async init({ temperature = 0, useExisting = true }: { temperature?: number; useExisting?: boolean } = {}) {
39
+ static async init({
40
+ temperature = 0,
41
+ useExisting = true,
42
+ }: {
43
+ temperature?: number;
44
+ useExisting?: boolean;
45
+ } = {}) {
37
46
  if (useExisting) {
38
47
  const llmConfig = await AiSession.getLlmConfig();
39
48
  if (llmConfig) {
40
49
  AiSession.#setChatModel(llmConfig.model, llmConfig.apiKey);
41
- Logger.rawLog(chalk.dim(`🤖akan editor uses existing LLM config (${llmConfig.model})`));
50
+ Logger.rawLog(
51
+ chalk.dim(
52
+ `🤖akan editor uses existing LLM config (${llmConfig.model})`,
53
+ ),
54
+ );
42
55
  return AiSession;
43
56
  }
44
- } else Logger.rawLog(chalk.yellow("🤖akan-editor is not initialized. LLM configuration should be set first."));
57
+ } else
58
+ Logger.rawLog(
59
+ chalk.yellow(
60
+ "🤖akan-editor is not initialized. LLM configuration should be set first.",
61
+ ),
62
+ );
45
63
 
46
64
  const llmConfig = await AiSession.#requestLlmConfig();
47
65
  const { model, apiKey } = llmConfig;
@@ -51,7 +69,11 @@ export class AiSession {
51
69
  await session.setLlmConfig({ model, apiKey });
52
70
  return session;
53
71
  }
54
- static #setChatModel(model: SupportedLlmModel, apiKey: string, { temperature = 0 }: { temperature?: number } = {}) {
72
+ static #setChatModel(
73
+ model: SupportedLlmModel,
74
+ apiKey: string,
75
+ { temperature = 0 }: { temperature?: number } = {},
76
+ ) {
55
77
  AiSession.#chat = new ChatDeepSeek({
56
78
  modelName: model,
57
79
  temperature,
@@ -62,22 +84,26 @@ export class AiSession {
62
84
  return AiSession;
63
85
  }
64
86
  static async getLlmConfig() {
65
- const akanConfig = await getAkanGlobalConfig();
66
- return akanConfig.llm ?? null;
87
+ return await GlobalConfig.getLlmConfig();
67
88
  }
68
- static async setLlmConfig(llmConfig: { model: SupportedLlmModel; apiKey: string } | null) {
69
- const akanConfig = await getAkanGlobalConfig();
70
- akanConfig.llm = llmConfig;
71
- await setAkanGlobalConfig(akanConfig);
89
+ static async setLlmConfig(
90
+ llmConfig: { model: SupportedLlmModel; apiKey: string } | null,
91
+ ) {
92
+ await GlobalConfig.setLlmConfig(llmConfig);
72
93
  return AiSession;
73
94
  }
74
95
  static async #requestLlmConfig() {
75
- const model = await select<SupportedLlmModel>({ message: "Select a LLM model", choices: supportedLlmModels });
96
+ const model = await select<SupportedLlmModel>({
97
+ message: "Select a LLM model",
98
+ choices: supportedLlmModels,
99
+ });
76
100
  const apiKey = await input({ message: "Enter your API key" });
77
101
  return { model, apiKey };
78
102
  }
79
103
  static async #validateApiKey(modelName: SupportedLlmModel, apiKey: string) {
80
- const spinner = new Spinner("Validating LLM API key...", { prefix: `🤖akan-editor` }).start();
104
+ const spinner = new Spinner("Validating LLM API key...", {
105
+ prefix: `🤖akan-editor`,
106
+ }).start();
81
107
  const chat = new ChatOpenAI({
82
108
  modelName,
83
109
  temperature: 0,
@@ -107,7 +133,15 @@ export class AiSession {
107
133
  workspace: WorkspaceExecutor;
108
134
  constructor(
109
135
  type: string,
110
- { workspace, cacheKey, isContinued }: { workspace: WorkspaceExecutor; cacheKey?: string; isContinued?: boolean },
136
+ {
137
+ workspace,
138
+ cacheKey,
139
+ isContinued,
140
+ }: {
141
+ workspace: WorkspaceExecutor;
142
+ cacheKey?: string;
143
+ isContinued?: boolean;
144
+ },
111
145
  ) {
112
146
  this.workspace = workspace;
113
147
  this.sessionKey = `${type}${cacheKey ? `-${cacheKey}` : ""}`;
@@ -126,7 +160,10 @@ export class AiSession {
126
160
  }
127
161
  async #saveCache() {
128
162
  const cacheFilePath = `${AiSession.#cacheDir}/${this.sessionKey}.json`;
129
- await this.workspace.writeJson(cacheFilePath, mapChatMessagesToStoredMessages(this.messageHistory));
163
+ await this.workspace.writeJson(
164
+ cacheFilePath,
165
+ mapChatMessagesToStoredMessages(this.messageHistory),
166
+ );
130
167
  }
131
168
  async ask(
132
169
  question: string,
@@ -142,7 +179,8 @@ export class AiSession {
142
179
  if (!AiSession.#chat) await AiSession.init();
143
180
  if (this.#cacheLoadPromise) await this.#cacheLoadPromise;
144
181
 
145
- if (!AiSession.#chat) throw new Error("Failed to initialize the AI session");
182
+ if (!AiSession.#chat)
183
+ throw new Error("Failed to initialize the AI session");
146
184
  const loader = new Spinner(`${AiSession.#chat.model} is thinking...`, {
147
185
  prefix: `🤖akan-editor`,
148
186
  }).start();
@@ -154,10 +192,13 @@ export class AiSession {
154
192
  fullResponse = "",
155
193
  tokenIdx = 0;
156
194
  for await (const chunk of stream) {
157
- if (loader.isSpinning()) loader.succeed(`${AiSession.#chat.model} responded`);
195
+ if (loader.isSpinning())
196
+ loader.succeed(`${AiSession.#chat.model} responded`);
158
197
 
159
198
  if (!fullResponse.length) {
160
- const reasoningContent = (chunk.additional_kwargs as { reasoning_content?: string }).reasoning_content ?? "";
199
+ const reasoningContent =
200
+ (chunk.additional_kwargs as { reasoning_content?: string })
201
+ .reasoning_content ?? "";
161
202
  if (reasoningContent.length) {
162
203
  reasoningResponse += reasoningContent;
163
204
  onReasoning(reasoningContent);
@@ -184,7 +225,16 @@ export class AiSession {
184
225
  throw new Error("Failed to stream response");
185
226
  }
186
227
  }
187
- async edit(question: string, { onChunk, onReasoning, maxTry = MAX_ASK_TRY, validate, approve }: EditOptions = {}) {
228
+ async edit(
229
+ question: string,
230
+ {
231
+ onChunk,
232
+ onReasoning,
233
+ maxTry = MAX_ASK_TRY,
234
+ validate,
235
+ approve,
236
+ }: EditOptions = {},
237
+ ) {
188
238
  for (let tryCount = 0; tryCount < maxTry; tryCount++) {
189
239
  let response = await this.ask(question, { onChunk, onReasoning });
190
240
  if (validate?.length && tryCount === 0) {
@@ -226,35 +276,51 @@ ${validate.map((v) => `- ${v}`).join("\n")}`;
226
276
  // const toolMessages = messages.map(
227
277
  // (message) => new ToolMessage({ content: message.content, tool_call_id: message.type })
228
278
  // );
229
- const toolMessages = messages.map((message) => new HumanMessage(message.content));
279
+ const toolMessages = messages.map(
280
+ (message) => new HumanMessage(message.content),
281
+ );
230
282
  this.messageHistory.push(...toolMessages);
231
283
  return this;
232
284
  }
233
- async writeTypescripts(question: string, executor: Executor, options: EditOptions = {}) {
285
+ async writeTypescripts(
286
+ question: string,
287
+ executor: Executor,
288
+ options: EditOptions = {},
289
+ ) {
234
290
  const content = await this.edit(question, options);
235
291
  const writes = this.#getTypescriptCodes(content);
236
- for (const write of writes) await executor.writeFile(write.filePath, write.content);
292
+ for (const write of writes)
293
+ await executor.writeFile(write.filePath, write.content);
237
294
  return await this.#tryFixTypescripts(writes, executor, options);
238
295
  }
239
296
  async #editTypescripts(question: string, options: EditOptions = {}) {
240
297
  const content = await this.edit(question, options);
241
298
  return this.#getTypescriptCodes(content);
242
299
  }
243
- async #tryFixTypescripts(writes: FileContent[], executor: Executor, options: EditOptions = {}) {
300
+ async #tryFixTypescripts(
301
+ writes: FileContent[],
302
+ executor: Executor,
303
+ options: EditOptions = {},
304
+ ) {
244
305
  const MAX_EDIT_TRY = 5;
245
306
  for (let tryCount = 0; tryCount < MAX_EDIT_TRY; tryCount++) {
246
- const loader = new Spinner(`Type checking and linting...`, { prefix: `🤖akan-editor` }).start();
307
+ const loader = new Spinner(`Type checking and linting...`, {
308
+ prefix: `🤖akan-editor`,
309
+ }).start();
247
310
  const fileChecks = await Promise.all(
248
311
  writes.map(async ({ filePath }) => {
249
312
  const typeCheckResult = executor.typeCheck(filePath);
250
313
  const lintResult = await executor.lint(filePath);
251
- const needFix = !!typeCheckResult.fileErrors.length || !!lintResult.errors.length;
314
+ const needFix =
315
+ !!typeCheckResult.fileErrors.length || !!lintResult.errors.length;
252
316
  return { filePath, typeCheckResult, lintResult, needFix };
253
317
  }),
254
318
  );
255
319
  const needFix = fileChecks.some((fileCheck) => fileCheck.needFix);
256
320
  if (needFix) {
257
- loader.fail("Type checking and linting has some errors, try to fix them");
321
+ loader.fail(
322
+ "Type checking and linting has some errors, try to fix them",
323
+ );
258
324
  fileChecks.forEach((fileCheck) => {
259
325
  Logger.rawLog(
260
326
  `TypeCheck Result \n${fileCheck.typeCheckResult.message}\nLint Result \n${fileCheck.lintResult.message}`,
@@ -264,12 +330,16 @@ ${validate.map((v) => `- ${v}`).join("\n")}`;
264
330
  { type: "eslint", content: fileCheck.lintResult.message },
265
331
  ]);
266
332
  });
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);
333
+ writes = await this.#editTypescripts(
334
+ "Fix the typescript and eslint errors",
335
+ {
336
+ ...options,
337
+ validate: undefined,
338
+ approve: true,
339
+ },
340
+ );
341
+ for (const write of writes)
342
+ await executor.writeFile(write.filePath, write.content);
273
343
  } else {
274
344
  loader.succeed("Type checking and linting has no errors");
275
345
  return writes;
@@ -285,7 +355,9 @@ ${validate.map((v) => `- ${v}`).join("\n")}`;
285
355
  if (!content) return null;
286
356
  const filePath = /\/\/ File: (.*?)(?:\n|$)/.exec(content)?.[1]?.trim();
287
357
  if (!filePath) return null;
288
- const contentWithoutFilepath = content.replace(`// File: ${filePath}\n`, "").trim();
358
+ const contentWithoutFilepath = content
359
+ .replace(`// File: ${filePath}\n`, "")
360
+ .trim();
289
361
  return { filePath, content: contentWithoutFilepath };
290
362
  });
291
363
  return result.filter((code) => code !== null) as FileContent[];
package/cloud/cloudApi.ts CHANGED
@@ -1,23 +1,45 @@
1
- interface AccessToken {
2
- jwt: string;
3
- refreshToken: string;
4
- expiresAt: Date;
5
- }
1
+ import {
2
+ type AccessToken,
3
+ type AccessTokenDto,
4
+ akanCloudHost,
5
+ type HostConfig,
6
+ } from "./constants";
7
+ import { GlobalConfig } from "./globalConfig";
6
8
 
7
9
  class HttpClient {
8
10
  readonly baseUrl: string;
9
- constructor(baseUrl: string) {
11
+ readonly headers: Record<string, string> = {};
12
+ constructor(baseUrl: string, headers: Record<string, string> = {}) {
10
13
  this.baseUrl = baseUrl;
14
+ this.headers = headers;
11
15
  }
12
16
  async get<T>(
13
17
  url: string,
14
18
  { headers }: { headers?: Record<string, string> } = {},
15
19
  ): Promise<T> {
16
20
  const response = await fetch(`${this.baseUrl}${url}`, {
17
- headers: { "Content-Type": "application/json", ...headers },
21
+ headers: {
22
+ "Content-Type": "application/json",
23
+ ...this.headers,
24
+ ...headers,
25
+ },
18
26
  });
19
27
  return response.json();
20
28
  }
29
+ async getFile(
30
+ url: string,
31
+ localPath: string,
32
+ headers?: Record<string, string>,
33
+ ): Promise<void> {
34
+ const response = await fetch(`${this.baseUrl}${url}`, {
35
+ headers: { ...this.headers, ...headers },
36
+ });
37
+ if (!response.ok)
38
+ throw new Error(
39
+ `Failed to download file: ${response.status} ${response.statusText}`,
40
+ );
41
+ await Bun.write(localPath, response);
42
+ }
21
43
  async post<T>(
22
44
  url: string,
23
45
  data: unknown,
@@ -28,74 +50,86 @@ class HttpClient {
28
50
  method: "POST",
29
51
  body: isFormData ? data : JSON.stringify(data),
30
52
  headers: isFormData
31
- ? headers
32
- : { "Content-Type": "application/json", ...headers },
53
+ ? { ...this.headers, ...headers }
54
+ : { "Content-Type": "application/json", ...this.headers, ...headers },
33
55
  });
34
56
  return response.json();
35
57
  }
58
+ setHeaders(headers: Record<string, string>) {
59
+ Object.assign(this.headers, headers);
60
+ return this;
61
+ }
36
62
  }
37
63
 
38
64
  export class CloudApi {
39
- readonly api: HttpClient;
65
+ readonly #api: HttpClient;
40
66
  #accessToken: AccessToken | null = null;
41
- constructor(
42
- host: string,
43
- { accessToken }: { accessToken?: AccessToken } = {},
44
- ) {
45
- this.api = new HttpClient(`${host}/api`);
46
- this.#accessToken = accessToken ?? null;
67
+
68
+ static async fromHost(host?: string) {
69
+ const hostConfig = await GlobalConfig.getHostConfig(host);
70
+ return new CloudApi(hostConfig);
71
+ }
72
+ constructor(hostConfig: HostConfig) {
73
+ const host = akanCloudHost;
74
+ this.#api = new HttpClient(`${host}/api`);
75
+ this.#accessToken = hostConfig.auth?.accessToken ?? null;
76
+ if (this.#accessToken && !GlobalConfig.needRefreshToken(this.#accessToken))
77
+ this.#api.setHeaders({
78
+ Authorization: `Bearer ${this.#accessToken.jwt}`,
79
+ });
47
80
  }
48
81
 
49
- async uploadEnv(
50
- devProjectId: string,
51
- fileStream: ReadableStream,
52
- ): Promise<boolean> {
82
+ async uploadEnv(devProjectId: string, file: File): Promise<boolean> {
53
83
  const formData = new FormData();
54
84
  formData.append("devProjectId", devProjectId);
55
- formData.append("fileStream", await new Response(fileStream).blob());
56
- const response = await this.api.post<{ success: boolean }>(
85
+ formData.append("file", file);
86
+ const data = await this.#api.post<boolean>(
57
87
  `/uploadEnv/${devProjectId}`,
58
88
  formData,
59
89
  );
60
- return response.success;
90
+ return data;
61
91
  }
62
- async downloadEnv(devProjectId: string): Promise<boolean> {
63
- const response = await this.api.get<{ success: boolean }>(
64
- `/downloadEnv/${devProjectId}`,
65
- );
66
- return response.success;
92
+ async downloadEnv(devProjectId: string, localPath: string): Promise<void> {
93
+ await this.#api.getFile(`/downloadEnv/${devProjectId}`, localPath);
67
94
  }
68
- async getRemoteAuthToken(remoteId: string): Promise<AccessToken> {
69
- if (this.#needRefreshToken()) return await this.refreshAuthToken();
70
- else if (this.#accessToken) return this.#accessToken;
71
- const accessToken = await this.api.get<AccessToken>(
72
- `/getRemoteAuthToken/${remoteId}`,
73
- );
74
- this.#accessToken = {
75
- jwt: accessToken.jwt,
76
- refreshToken: accessToken.refreshToken,
77
- expiresAt: new Date(accessToken.expiresAt),
78
- };
79
- return accessToken;
95
+ async getRemoteAuthToken(remoteId: string): Promise<AccessToken | null> {
96
+ try {
97
+ if (this.#accessToken) {
98
+ if (GlobalConfig.needRefreshToken(this.#accessToken))
99
+ return await this.refreshAuthToken();
100
+ else return await this.refreshAuthToken();
101
+ }
102
+ const accessToken = await this.#api.get<AccessTokenDto>(
103
+ `/getRemoteAuthToken/${remoteId}`,
104
+ );
105
+ this.#accessToken = GlobalConfig.toAccessToken(accessToken);
106
+ this.#api.setHeaders({
107
+ Authorization: `Bearer ${this.#accessToken.jwt}`,
108
+ });
109
+ return this.#accessToken;
110
+ } catch (_) {
111
+ return null;
112
+ }
80
113
  }
81
114
  async refreshAuthToken(): Promise<AccessToken> {
82
- const response = await this.api.post<AccessToken>(
115
+ const response = await this.#api.post<AccessTokenDto>(
83
116
  `/refreshRemoteAuthToken`,
84
117
  {
85
118
  refreshToken: this.#accessToken?.refreshToken,
86
119
  },
87
120
  );
88
- this.#accessToken = {
89
- jwt: response.jwt,
90
- refreshToken: response.refreshToken,
91
- expiresAt: new Date(response.expiresAt),
92
- };
93
- return response;
121
+ this.#accessToken = GlobalConfig.toAccessToken(response);
122
+ this.#api.setHeaders({ Authorization: `Bearer ${this.#accessToken.jwt}` });
123
+ return this.#accessToken;
94
124
  }
95
- #needRefreshToken(): boolean {
96
- return !!(
97
- this.#accessToken?.expiresAt &&
98
- this.#accessToken.expiresAt.getTime() < Date.now() - 1000 * 60 * 60
99
- );
125
+ async getRemoteSelf(): Promise<{ id: string; nickname: string } | null> {
126
+ try {
127
+ const data = await this.#api.get<{ id: string; nickname: string }>(
128
+ `/getRemoteSelf`,
129
+ );
130
+ return data;
131
+ } catch {
132
+ return null;
133
+ }
100
134
  }
101
135
  }
@@ -0,0 +1,48 @@
1
+ import type { Dayjs } from "dayjs";
2
+ import type { SupportedLlmModel } from "../aiEditor";
3
+
4
+ export const basePath = `${Bun.env.HOME ?? Bun.env.USERPROFILE}/.akan`;
5
+ export const configPath = `${basePath}/config.json`;
6
+ export const akanCloudHost =
7
+ process.env.AKAN_PUBLIC_OPERATION_MODE === "local" ? "http://localhost" : "https://cloud.akanjs.com";
8
+ export const akanCloudUrl = `${akanCloudHost}${process.env.AKAN_PUBLIC_OPERATION_MODE === "local" ? ":8282" : ""}/api`;
9
+
10
+ export interface HostConfig {
11
+ auth?: {
12
+ accessToken?: AccessToken;
13
+ self?: { id: string; nickname: string };
14
+ };
15
+ }
16
+ export interface HostConfigDto {
17
+ auth?: {
18
+ accessToken?: AccessTokenDto;
19
+ self?: { id: string; nickname: string };
20
+ };
21
+ }
22
+ export const defaultHostConfig: HostConfig = {};
23
+ export interface RemoteEnvServerConfig {
24
+ host: string;
25
+ username?: string;
26
+ port?: number;
27
+ }
28
+ export interface AkanGlobalConfig {
29
+ cloudHost: { [key: string]: HostConfigDto };
30
+ remoteEnvServers: Record<string, RemoteEnvServerConfig>;
31
+ llm: { model: SupportedLlmModel; apiKey: string } | null;
32
+ }
33
+ export const defaultAkanGlobalConfig: AkanGlobalConfig = {
34
+ cloudHost: {},
35
+ remoteEnvServers: {},
36
+ llm: null,
37
+ };
38
+
39
+ export interface AccessTokenDto {
40
+ jwt: string;
41
+ refreshToken: string | null;
42
+ expiresAt: string | null;
43
+ }
44
+ export interface AccessToken {
45
+ jwt: string;
46
+ refreshToken: string | null;
47
+ expiresAt: Dayjs | null;
48
+ }
@@ -0,0 +1,109 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import dayjs from "dayjs";
3
+ import { FileSys } from "../fileSys";
4
+ import {
5
+ type AccessToken,
6
+ type AccessTokenDto,
7
+ type AkanGlobalConfig,
8
+ akanCloudHost,
9
+ basePath,
10
+ configPath,
11
+ defaultAkanGlobalConfig,
12
+ defaultHostConfig,
13
+ type HostConfig,
14
+ type HostConfigDto,
15
+ type RemoteEnvServerConfig,
16
+ } from "./constants";
17
+
18
+ export class GlobalConfig {
19
+ static async #getAkanGlobalConfig(): Promise<AkanGlobalConfig> {
20
+ const exists = await FileSys.fileExists(configPath);
21
+ const akanConfig = exists ? await FileSys.readJson<Partial<AkanGlobalConfig>>(configPath) : {};
22
+ return {
23
+ ...defaultAkanGlobalConfig,
24
+ ...akanConfig,
25
+ cloudHost: akanConfig.cloudHost ?? defaultAkanGlobalConfig.cloudHost,
26
+ remoteEnvServers: akanConfig.remoteEnvServers ?? defaultAkanGlobalConfig.remoteEnvServers,
27
+ };
28
+ }
29
+ static async #setAkanGlobalConfig(akanConfig: AkanGlobalConfig) {
30
+ await mkdir(basePath, { recursive: true });
31
+ await Bun.write(configPath, JSON.stringify(akanConfig, null, 2));
32
+ }
33
+ static async getHostConfig(host = akanCloudHost): Promise<HostConfig> {
34
+ const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
35
+ return GlobalConfig.toHostConfig(akanConfig.cloudHost[host] ?? defaultHostConfig);
36
+ }
37
+ static async setHostConfig(host = akanCloudHost, config: HostConfig = {}) {
38
+ const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
39
+ akanConfig.cloudHost[host] = GlobalConfig.toHostConfigDto(config);
40
+ await GlobalConfig.#setAkanGlobalConfig(akanConfig);
41
+ }
42
+ static async getLlmConfig(): Promise<AkanGlobalConfig["llm"]> {
43
+ const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
44
+ return akanConfig.llm ?? null;
45
+ }
46
+ static async setLlmConfig(llmConfig: AkanGlobalConfig["llm"]) {
47
+ const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
48
+ await GlobalConfig.#setAkanGlobalConfig({ ...akanConfig, llm: llmConfig });
49
+ }
50
+ static async getRemoteEnvServers(): Promise<AkanGlobalConfig["remoteEnvServers"]> {
51
+ const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
52
+ return akanConfig.remoteEnvServers;
53
+ }
54
+ static async setRemoteEnvServer(name: string, config: RemoteEnvServerConfig) {
55
+ const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
56
+ await GlobalConfig.#setAkanGlobalConfig({
57
+ ...akanConfig,
58
+ remoteEnvServers: {
59
+ ...akanConfig.remoteEnvServers,
60
+ [name]: config,
61
+ },
62
+ });
63
+ }
64
+ static async removeRemoteEnvServer(name: string) {
65
+ const akanConfig = await GlobalConfig.#getAkanGlobalConfig();
66
+ const { [name]: _, ...remoteEnvServers } = akanConfig.remoteEnvServers;
67
+ await GlobalConfig.#setAkanGlobalConfig({
68
+ ...akanConfig,
69
+ remoteEnvServers,
70
+ });
71
+ }
72
+ static needRefreshToken(accessToken: AccessToken): boolean {
73
+ return !!accessToken?.expiresAt?.isBefore(dayjs().add(1, "hour"));
74
+ }
75
+ static toAccessToken(accessToken: AccessTokenDto): AccessToken {
76
+ return {
77
+ jwt: accessToken.jwt,
78
+ refreshToken: accessToken.refreshToken ?? null,
79
+ expiresAt: accessToken.expiresAt ? dayjs(accessToken.expiresAt) : null,
80
+ };
81
+ }
82
+ static toAccessTokenDto(accessToken: AccessToken): AccessTokenDto {
83
+ return {
84
+ jwt: accessToken.jwt,
85
+ refreshToken: accessToken.refreshToken ?? null,
86
+ expiresAt: accessToken.expiresAt?.toString() ?? null,
87
+ };
88
+ }
89
+ static toHostConfigDto(hostConfig: HostConfig): HostConfigDto {
90
+ return {
91
+ auth: {
92
+ accessToken: hostConfig.auth?.accessToken
93
+ ? GlobalConfig.toAccessTokenDto(hostConfig.auth.accessToken)
94
+ : undefined,
95
+ self: hostConfig.auth?.self,
96
+ },
97
+ };
98
+ }
99
+ static toHostConfig(hostConfigDto: HostConfigDto): HostConfig {
100
+ return {
101
+ auth: {
102
+ accessToken: hostConfigDto.auth?.accessToken
103
+ ? GlobalConfig.toAccessToken(hostConfigDto.auth.accessToken)
104
+ : undefined,
105
+ self: hostConfigDto.auth?.self,
106
+ },
107
+ };
108
+ }
109
+ }
package/cloud/index.ts CHANGED
@@ -1 +1,3 @@
1
1
  export * from "./cloudApi";
2
+ export * from "./constants";
3
+ export * from "./globalConfig";
package/executors.ts CHANGED
@@ -261,10 +261,30 @@ export class Executor {
261
261
  });
262
262
  return new Promise((resolve, reject) => {
263
263
  proc.on("error", (error) => {
264
- reject(new CommandExecutionError({ command, cwd, code: null, signal: null, stdout, stderr, cause: error }));
264
+ reject(
265
+ new CommandExecutionError({
266
+ command,
267
+ cwd,
268
+ code: null,
269
+ signal: null,
270
+ stdout,
271
+ stderr,
272
+ cause: error,
273
+ }),
274
+ );
265
275
  });
266
276
  proc.on("exit", (code, signal) => {
267
- if (!!code || signal) reject(new CommandExecutionError({ command, cwd, code, signal, stdout, stderr }));
277
+ if (!!code || signal)
278
+ reject(
279
+ new CommandExecutionError({
280
+ command,
281
+ cwd,
282
+ code,
283
+ signal,
284
+ stdout,
285
+ stderr,
286
+ }),
287
+ );
268
288
  else resolve({ code, signal });
269
289
  });
270
290
  });
@@ -293,12 +313,31 @@ export class Executor {
293
313
  return new Promise((resolve, reject) => {
294
314
  proc.on("error", (error) => {
295
315
  reject(
296
- new CommandExecutionError({ command, args, cwd, code: null, signal: null, stdout, stderr, cause: error }),
316
+ new CommandExecutionError({
317
+ command,
318
+ args,
319
+ cwd,
320
+ code: null,
321
+ signal: null,
322
+ stdout,
323
+ stderr,
324
+ cause: error,
325
+ }),
297
326
  );
298
327
  });
299
328
  proc.on("close", (code, signal) => {
300
329
  if (code !== 0 || signal)
301
- reject(new CommandExecutionError({ command, args, cwd, code, signal, stdout, stderr }));
330
+ reject(
331
+ new CommandExecutionError({
332
+ command,
333
+ args,
334
+ cwd,
335
+ code,
336
+ signal,
337
+ stdout,
338
+ stderr,
339
+ }),
340
+ );
302
341
  else resolve(stdout);
303
342
  });
304
343
  });
@@ -345,7 +384,17 @@ export class Executor {
345
384
  });
346
385
  proc.on("exit", (code, signal) => {
347
386
  if (!!code || signal)
348
- reject(new CommandExecutionError({ command: modulePath, args, cwd, code, signal, stdout, stderr }));
387
+ reject(
388
+ new CommandExecutionError({
389
+ command: modulePath,
390
+ args,
391
+ cwd,
392
+ code,
393
+ signal,
394
+ stdout,
395
+ stderr,
396
+ }),
397
+ );
349
398
  else resolve({ code, signal });
350
399
  });
351
400
  });
@@ -490,7 +539,10 @@ export class Executor {
490
539
  const result = {
491
540
  ...extendsTsconfig,
492
541
  ...tsconfig,
493
- compilerOptions: { ...extendsTsconfig.compilerOptions, ...tsconfig.compilerOptions },
542
+ compilerOptions: {
543
+ ...extendsTsconfig.compilerOptions,
544
+ ...tsconfig.compilerOptions,
545
+ },
494
546
  } as TsConfigJson;
495
547
  this.#tsconfig = result;
496
548
  return result;
@@ -571,7 +623,9 @@ export class Executor {
571
623
  content,
572
624
  );
573
625
  this.logger.verbose(`Apply template ${templatePath} to ${convertedTargetPath}`);
574
- return this.writeFile(convertedTargetPath, convertedContent, { overwrite });
626
+ return this.writeFile(convertedTargetPath, convertedContent, {
627
+ overwrite,
628
+ });
575
629
  } else if (staticTemplateFileExtensions.has(path.extname(targetPath).toLowerCase())) {
576
630
  const convertedTargetPath = Object.entries(dict).reduce(
577
631
  (path, [key, value]) => path.replace(new RegExp(`__${key}__`, "g"), value),
@@ -606,7 +660,12 @@ export class Executor {
606
660
  if ((await stat(prefixTemplatePath)).isFile()) {
607
661
  const filename = path.basename(prefixTemplatePath);
608
662
  const fileContent = await this.#applyTemplateFile(
609
- { templatePath: prefixTemplatePath, targetPath: path.join(basePath, filename), scanInfo, overwrite },
663
+ {
664
+ templatePath: prefixTemplatePath,
665
+ targetPath: path.join(basePath, filename),
666
+ scanInfo,
667
+ overwrite,
668
+ },
610
669
  dict,
611
670
  options,
612
671
  );
@@ -619,7 +678,12 @@ export class Executor {
619
678
  const subpath = path.join(templatePath, subdir);
620
679
  if ((await stat(subpath)).isFile()) {
621
680
  const fileContent = await this.#applyTemplateFile(
622
- { templatePath: subpath, targetPath: path.join(basePath, subdir), scanInfo, overwrite },
681
+ {
682
+ templatePath: subpath,
683
+ targetPath: path.join(basePath, subdir),
684
+ scanInfo,
685
+ overwrite,
686
+ },
623
687
  dict,
624
688
  options,
625
689
  );
@@ -690,7 +754,10 @@ export class Executor {
690
754
  }> {
691
755
  const path = this.getPath(filePath);
692
756
  const linter = this.getLinter();
693
- const { results, errors, warnings } = await linter.lint(path, { fix, dryRun });
757
+ const { results, errors, warnings } = await linter.lint(path, {
758
+ fix,
759
+ dryRun,
760
+ });
694
761
  const message = linter.formatLintResults(results);
695
762
  return { results, message, errors, warnings };
696
763
  }
@@ -726,6 +793,7 @@ export class WorkspaceExecutor extends Executor {
726
793
 
727
794
  const appName = sourceEnv.AKAN_PUBLIC_APP_NAME;
728
795
  const workspaceRoot = sourceEnv.AKAN_WORKSPACE_ROOT;
796
+ const workspaceId = sourceEnv.AKAN_WORKSPACE_ID;
729
797
 
730
798
  const repoName = sourceEnv.AKAN_PUBLIC_REPO_NAME;
731
799
  if (!repoName) throw new Error("AKAN_PUBLIC_REPO_NAME is not set");
@@ -743,7 +811,16 @@ export class WorkspaceExecutor extends Executor {
743
811
  | "local"
744
812
  | undefined;
745
813
  if (!env) throw new Error("AKAN_PUBLIC_ENV is not set");
746
- return { ...(appName ? { appName } : {}), workspaceRoot, repoName, serveDomain, env, portOffset };
814
+ return { ...(appName ? { appName } : {}), workspaceRoot, repoName, serveDomain, env, portOffset, workspaceId };
815
+ }
816
+ getWorkspaceId<AllowEmpty extends boolean = false>({
817
+ allowEmpty,
818
+ }: {
819
+ allowEmpty?: AllowEmpty;
820
+ } = {}): AllowEmpty extends true ? string | undefined : string {
821
+ const { workspaceId } = WorkspaceExecutor.getBaseDevEnv();
822
+ if (!workspaceId && !allowEmpty) throw new Error("Workspace ID is not found");
823
+ return workspaceId as AllowEmpty extends true ? string | undefined : string;
747
824
  }
748
825
  async scan(): Promise<WorkspaceInfo> {
749
826
  return await WorkspaceInfo.fromExecutor(this);
@@ -976,8 +1053,12 @@ export class SysExecutor extends Executor {
976
1053
  if (this.#scanInfo && !refresh) return this.#scanInfo;
977
1054
  const scanInfo =
978
1055
  this.type === "app"
979
- ? await AppInfo.fromExecutor(this as unknown as AppExecutor, { refresh })
980
- : await LibInfo.fromExecutor(this as unknown as LibExecutor, { refresh });
1056
+ ? await AppInfo.fromExecutor(this as unknown as AppExecutor, {
1057
+ refresh,
1058
+ })
1059
+ : await LibInfo.fromExecutor(this as unknown as LibExecutor, {
1060
+ refresh,
1061
+ });
981
1062
  if (write) {
982
1063
  await Promise.all(this.#getScanTemplateTasks(scanInfo));
983
1064
  await this.writeJson(`akan.${this.type}.json`, scanInfo.getScanResult());
@@ -1147,7 +1228,11 @@ export class SysExecutor extends Executor {
1147
1228
  ),
1148
1229
  };
1149
1230
  const scanInfo = await this.scan();
1150
- const fileContents = await this._applyTemplate({ ...options, scanInfo, dict });
1231
+ const fileContents = await this._applyTemplate({
1232
+ ...options,
1233
+ scanInfo,
1234
+ dict,
1235
+ });
1151
1236
  await this.scan();
1152
1237
  return fileContents;
1153
1238
  }
@@ -1247,7 +1332,11 @@ export class AppExecutor extends SysExecutor {
1247
1332
  this.#pageKeys = [];
1248
1333
  return this.#pageKeys;
1249
1334
  }
1250
- for await (const rel of glob.scan({ cwd: pageDir, absolute: false, onlyFiles: true })) {
1335
+ for await (const rel of glob.scan({
1336
+ cwd: pageDir,
1337
+ absolute: false,
1338
+ onlyFiles: true,
1339
+ })) {
1251
1340
  const segments = rel.split(path.sep);
1252
1341
  if (segments.some((s) => s === "node_modules")) continue;
1253
1342
  const posix = segments.join("/");
@@ -1255,7 +1344,10 @@ export class AppExecutor extends SysExecutor {
1255
1344
  validatePageSourceFile(posix, { filePath: absPath });
1256
1345
  if (!isRouteSourceFile(posix)) continue;
1257
1346
  const key = `./${posix}`;
1258
- validateSubRoutePageKey(key, akanConfig.basePaths, { appName: this.name, filePath: absPath });
1347
+ validateSubRoutePageKey(key, akanConfig.basePaths, {
1348
+ appName: this.name,
1349
+ filePath: absPath,
1350
+ });
1259
1351
  const parsed = parseRouteModuleKey(key);
1260
1352
  if (parsed.isInternalRootLayout) {
1261
1353
  throw new Error(`[route-convention] __root_layout is reserved for Akan.js generated root layout: ${absPath}`);
@@ -1298,7 +1390,11 @@ export class AppExecutor extends SysExecutor {
1298
1390
  ]);
1299
1391
  }
1300
1392
  async scanSync({ refresh = false, write = true }: { refresh?: boolean; write?: boolean } = {}) {
1301
- const scanInfo = (await this.scan({ refresh, write, writeLib: write })) as AppInfo;
1393
+ const scanInfo = (await this.scan({
1394
+ refresh,
1395
+ write,
1396
+ writeLib: write,
1397
+ })) as AppInfo;
1302
1398
  if (write) await this.syncAssets(scanInfo.getScanResult().libDeps);
1303
1399
  return scanInfo;
1304
1400
  }
@@ -1365,7 +1461,10 @@ export class PkgExecutor extends Executor {
1365
1461
  return scanInfo;
1366
1462
  }
1367
1463
  async #getDependencyVersion(rootPackageJson: PackageJson, dep: string): Promise<string | undefined> {
1368
- const rootDeps = { ...rootPackageJson.dependencies, ...rootPackageJson.devDependencies };
1464
+ const rootDeps = {
1465
+ ...rootPackageJson.dependencies,
1466
+ ...rootPackageJson.devDependencies,
1467
+ };
1369
1468
  const rootVersion = rootDeps[dep];
1370
1469
  if (rootVersion) return rootVersion;
1371
1470
 
@@ -1426,7 +1525,14 @@ export class PkgExecutor extends Executor {
1426
1525
  const distPkgJson: PackageJson = {
1427
1526
  ...pkgJson,
1428
1527
  type: "module",
1429
- exports: { ...pkgJson.exports, ".": { import: "./index.ts", types: "./index.ts", default: "./index.ts" } },
1528
+ exports: {
1529
+ ...pkgJson.exports,
1530
+ ".": {
1531
+ import: "./index.ts",
1532
+ types: "./index.ts",
1533
+ default: "./index.ts",
1534
+ },
1535
+ },
1430
1536
  engines: { bun: ">=1.3.13" },
1431
1537
  ...dependencyMaps,
1432
1538
  };
package/index.ts CHANGED
@@ -6,11 +6,10 @@ export * from "./applicationBuildRunner";
6
6
  export * from "./applicationReleasePackager";
7
7
  export * from "./applicationTestPreload";
8
8
  export * from "./artifact";
9
- export * from "./auth";
10
9
  export * from "./builder";
11
10
  export * from "./capacitorApp";
11
+ export * from "./cloud";
12
12
  export * from "./commandDecorators";
13
- export * from "./constants";
14
13
  export * from "./createTunnel";
15
14
  export * from "./dependencyScanner";
16
15
  export * from "./executors";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "2.1.1-rc.0",
3
+ "version": "2.1.1-rc.2",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -32,10 +32,11 @@
32
32
  "@langchain/openai": "^1.4.6",
33
33
  "@tailwindcss/node": "^4.3.0",
34
34
  "@trapezedev/project": "^7.1.4",
35
- "akanjs": "2.1.1-rc.0",
35
+ "akanjs": "2.1.1-rc.2",
36
36
  "chalk": "^5.6.2",
37
37
  "commander": "^14.0.3",
38
38
  "daisyui": "^5.5.20",
39
+ "dayjs": "^1.11.20",
39
40
  "fontaine": "^0.8.0",
40
41
  "fonteditor-core": "^2.6.3",
41
42
  "ignore": "^7.0.5",
package/auth.ts DELETED
@@ -1,41 +0,0 @@
1
- import { mkdir } from "node:fs/promises";
2
-
3
- import {
4
- type AkanGlobalConfig,
5
- akanCloudHost,
6
- akanCloudUrl,
7
- basePath,
8
- configPath,
9
- defaultAkanGlobalConfig,
10
- defaultHostConfig,
11
- type HostConfig,
12
- } from "./constants";
13
- import { FileSys } from "./fileSys";
14
-
15
- export const getAkanGlobalConfig = async () => {
16
- const exists = await FileSys.fileExists(configPath);
17
- const akanConfig = exists ? await FileSys.readJson<AkanGlobalConfig>(configPath) : defaultAkanGlobalConfig;
18
- return akanConfig;
19
- };
20
- export const setAkanGlobalConfig = async (akanConfig: AkanGlobalConfig) => {
21
- await mkdir(basePath, { recursive: true });
22
- await Bun.write(configPath, JSON.stringify(akanConfig, null, 2));
23
- };
24
- export const getHostConfig = async (host = akanCloudHost) => {
25
- const akanConfig = await getAkanGlobalConfig();
26
- return akanConfig.cloudHost[host] ?? defaultHostConfig;
27
- };
28
- export const setHostConfig = async (host = akanCloudHost, config: HostConfig = {}) => {
29
- const akanConfig = await getAkanGlobalConfig();
30
- akanConfig.cloudHost[host] = config;
31
- await setAkanGlobalConfig(akanConfig);
32
- };
33
- export const getSelf = async (token: string) => {
34
- try {
35
- const res = await fetch(`${akanCloudUrl}/user/getSelf`, { headers: { Authorization: `Bearer ${token}` } });
36
- const user = (await res.json()) as { id: string; nickname: string };
37
- return user;
38
- } catch (e) {
39
- return null;
40
- }
41
- };
package/constants.ts DELETED
@@ -1,32 +0,0 @@
1
- import type { SupportedLlmModel } from "./aiEditor";
2
-
3
- export const basePath = `${Bun.env.HOME ?? Bun.env.USERPROFILE}/.akan`;
4
- export const configPath = `${basePath}/config.json`;
5
- export const akanCloudHost =
6
- process.env.AKAN_PUBLIC_OPERATION_MODE === "local"
7
- ? "http://localhost"
8
- : "https://cloud.akanjs.com";
9
- export const akanCloudUrl = `${akanCloudHost}${
10
- process.env.AKAN_PUBLIC_OPERATION_MODE === "local" ? ":8282" : ""
11
- }/api`;
12
-
13
- export interface HostConfig {
14
- auth?: {
15
- token: string;
16
- self: { id: string; nickname: string };
17
- };
18
- }
19
- export const defaultHostConfig: HostConfig = {};
20
- export interface AkanGlobalConfig {
21
- cloudHost: {
22
- [key: string]: HostConfig;
23
- };
24
- llm: {
25
- model: SupportedLlmModel;
26
- apiKey: string;
27
- } | null;
28
- }
29
- export const defaultAkanGlobalConfig: AkanGlobalConfig = {
30
- cloudHost: {},
31
- llm: null,
32
- };