@akanjs/devkit 2.2.10 → 2.2.11
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/CHANGELOG.md +17 -0
- package/akanContext.ts +360 -0
- package/applicationBuildRunner.ts +44 -2
- package/devkitUtils.test.ts +40 -0
- package/executors.test.ts +4 -0
- package/index.ts +1 -0
- package/package.json +2 -2
- package/prompter.ts +6 -7
- package/scanInfo.ts +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @akanjs/devkit
|
|
2
2
|
|
|
3
|
+
## 2.2.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8af7a9d: Add agent-oriented workspace context tooling, generated agent rule templates, and LLM documentation surfaces for Akan workspaces.
|
|
8
|
+
- 8190632: Add Akan server console support with CLI/build integration and documentation for console-oriented workflows.
|
|
9
|
+
- 4bce7f9: Add initial LLM discovery docs and stabilize Akan client/runtime behavior.
|
|
10
|
+
|
|
11
|
+
- Add `/llms.txt` documentation discovery for Akan docs.
|
|
12
|
+
- Add `wsConnect` support for automatic WebSocket connections.
|
|
13
|
+
- Delay client bootstrap module execution until the SSR fizz stream is ready.
|
|
14
|
+
- Improve route tree, HMR, fetch, store, and SSR/client runtime stability.
|
|
15
|
+
|
|
16
|
+
- Updated dependencies [8190632]
|
|
17
|
+
- Updated dependencies [4bce7f9]
|
|
18
|
+
- akanjs@2.2.11
|
|
19
|
+
|
|
3
20
|
## 2.2.7
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
package/akanContext.ts
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { capitalize } from "akanjs/common";
|
|
4
|
+
import { AppExecutor, LibExecutor, type SysExecutor, type WorkspaceExecutor } from "./executors";
|
|
5
|
+
import { FileSys } from "./fileSys";
|
|
6
|
+
import type { PackageJson } from "./types";
|
|
7
|
+
|
|
8
|
+
export type AkanContextFormat = "json" | "markdown";
|
|
9
|
+
export type AkanModuleKind = "domain" | "service" | "scalar";
|
|
10
|
+
export type AkanDiagnosticSeverity = "warning" | "error";
|
|
11
|
+
|
|
12
|
+
export interface AkanAbstractSummary {
|
|
13
|
+
path: string;
|
|
14
|
+
exists: boolean;
|
|
15
|
+
title?: string;
|
|
16
|
+
headings: string[];
|
|
17
|
+
content?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AkanModuleContext {
|
|
21
|
+
kind: AkanModuleKind;
|
|
22
|
+
name: string;
|
|
23
|
+
folderName: string;
|
|
24
|
+
sysName: string;
|
|
25
|
+
sysType: "app" | "lib";
|
|
26
|
+
path: string;
|
|
27
|
+
abstract: AkanAbstractSummary;
|
|
28
|
+
files: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AkanSysContext {
|
|
32
|
+
type: "app" | "lib";
|
|
33
|
+
name: string;
|
|
34
|
+
path: string;
|
|
35
|
+
hasConfig: boolean;
|
|
36
|
+
modules: AkanModuleContext[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AkanPackageContext {
|
|
40
|
+
name: string;
|
|
41
|
+
path: string;
|
|
42
|
+
version?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AkanWorkspaceContext {
|
|
46
|
+
schemaVersion: 1;
|
|
47
|
+
repoName: string;
|
|
48
|
+
root: string;
|
|
49
|
+
packageVersion?: string;
|
|
50
|
+
apps: AkanSysContext[];
|
|
51
|
+
libs: AkanSysContext[];
|
|
52
|
+
pkgs: AkanPackageContext[];
|
|
53
|
+
generatedFiles: string[];
|
|
54
|
+
validationCommands: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AkanDiagnostic {
|
|
58
|
+
severity: AkanDiagnosticSeverity;
|
|
59
|
+
code: string;
|
|
60
|
+
message: string;
|
|
61
|
+
path?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface AkanDoctorResult {
|
|
65
|
+
schemaVersion: 1;
|
|
66
|
+
strict: boolean;
|
|
67
|
+
diagnostics: AkanDiagnostic[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface AkanContextOptions {
|
|
71
|
+
app?: string | null;
|
|
72
|
+
module?: string | null;
|
|
73
|
+
includeAbstractContent?: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const generatedFiles = [
|
|
77
|
+
"cnst.ts",
|
|
78
|
+
"db.ts",
|
|
79
|
+
"dict.ts",
|
|
80
|
+
"option.ts",
|
|
81
|
+
"sig.ts",
|
|
82
|
+
"srv.ts",
|
|
83
|
+
"st.ts",
|
|
84
|
+
"useClient.ts",
|
|
85
|
+
"useServer.ts",
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const appRootAllowFiles = new Set([
|
|
89
|
+
"akan.app.json",
|
|
90
|
+
"akan.config.ts",
|
|
91
|
+
"capacitor.config.ts",
|
|
92
|
+
"client.ts",
|
|
93
|
+
"main.ts",
|
|
94
|
+
"package.json",
|
|
95
|
+
"server.ts",
|
|
96
|
+
"tsconfig.json",
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
const appRootAllowDirs = new Set([
|
|
100
|
+
".akan",
|
|
101
|
+
"android",
|
|
102
|
+
"common",
|
|
103
|
+
"env",
|
|
104
|
+
"ios",
|
|
105
|
+
"lib",
|
|
106
|
+
"page",
|
|
107
|
+
"private",
|
|
108
|
+
"public",
|
|
109
|
+
"script",
|
|
110
|
+
"srvkit",
|
|
111
|
+
"ui",
|
|
112
|
+
"webkit",
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const safeReadDir = async (dirPath: string) => {
|
|
116
|
+
try {
|
|
117
|
+
return (await readdir(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
118
|
+
} catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const safeReadText = async (filePath: string) => {
|
|
124
|
+
try {
|
|
125
|
+
return await FileSys.readText(filePath);
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const safeReadJson = async <T>(filePath: string) => {
|
|
132
|
+
try {
|
|
133
|
+
return await FileSys.readJson<T>(filePath);
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const parseAbstractSummary = (
|
|
140
|
+
relativePath: string,
|
|
141
|
+
content: string | null,
|
|
142
|
+
includeContent: boolean,
|
|
143
|
+
): AkanAbstractSummary => {
|
|
144
|
+
if (content === null) return { path: relativePath, exists: false, headings: [] };
|
|
145
|
+
const headings = content
|
|
146
|
+
.split(/\r?\n/)
|
|
147
|
+
.map((line) => line.trim())
|
|
148
|
+
.filter((line) => line.startsWith("#"))
|
|
149
|
+
.map((line) => line.replace(/^#+\s*/, "").trim())
|
|
150
|
+
.filter(Boolean);
|
|
151
|
+
return {
|
|
152
|
+
path: relativePath,
|
|
153
|
+
exists: true,
|
|
154
|
+
title: headings[0],
|
|
155
|
+
headings: headings.slice(0, 8),
|
|
156
|
+
...(includeContent ? { content } : {}),
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const readFiles = async (dirPath: string) =>
|
|
161
|
+
(await safeReadDir(dirPath))
|
|
162
|
+
.filter((entry) => entry.isFile())
|
|
163
|
+
.map((entry) => entry.name)
|
|
164
|
+
.sort();
|
|
165
|
+
|
|
166
|
+
const getRelative = (workspace: WorkspaceExecutor, absolutePath: string) =>
|
|
167
|
+
path.relative(workspace.workspaceRoot, absolutePath).replaceAll(path.sep, "/");
|
|
168
|
+
|
|
169
|
+
const createModuleContext = async (
|
|
170
|
+
workspace: WorkspaceExecutor,
|
|
171
|
+
sys: SysExecutor,
|
|
172
|
+
kind: AkanModuleKind,
|
|
173
|
+
folderName: string,
|
|
174
|
+
moduleName: string,
|
|
175
|
+
includeAbstractContent: boolean,
|
|
176
|
+
): Promise<AkanModuleContext> => {
|
|
177
|
+
const modulePath =
|
|
178
|
+
kind === "scalar"
|
|
179
|
+
? path.join(sys.cwdPath, "lib", "__scalar", moduleName)
|
|
180
|
+
: path.join(sys.cwdPath, "lib", folderName);
|
|
181
|
+
const relativePath = getRelative(workspace, modulePath);
|
|
182
|
+
const abstractPath = `${relativePath}/${moduleName}.abstract.md`;
|
|
183
|
+
const abstractContent = await safeReadText(path.join(workspace.workspaceRoot, abstractPath));
|
|
184
|
+
return {
|
|
185
|
+
kind,
|
|
186
|
+
name: moduleName,
|
|
187
|
+
folderName,
|
|
188
|
+
sysName: sys.name,
|
|
189
|
+
sysType: sys.type,
|
|
190
|
+
path: relativePath,
|
|
191
|
+
abstract: parseAbstractSummary(abstractPath, abstractContent, includeAbstractContent),
|
|
192
|
+
files: await readFiles(modulePath),
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const getSysModules = async (
|
|
197
|
+
workspace: WorkspaceExecutor,
|
|
198
|
+
sys: SysExecutor,
|
|
199
|
+
{
|
|
200
|
+
includeAbstractContent = false,
|
|
201
|
+
module: moduleFilter,
|
|
202
|
+
}: { includeAbstractContent?: boolean; module?: string | null } = {},
|
|
203
|
+
) => {
|
|
204
|
+
const libPath = path.join(sys.cwdPath, "lib");
|
|
205
|
+
const entries = await safeReadDir(libPath);
|
|
206
|
+
const modules: AkanModuleContext[] = [];
|
|
207
|
+
for (const entry of entries) {
|
|
208
|
+
if (!entry.isDirectory()) continue;
|
|
209
|
+
if (entry.name === "__scalar") continue;
|
|
210
|
+
if (entry.name.startsWith("__")) continue;
|
|
211
|
+
if (entry.name.startsWith("_")) {
|
|
212
|
+
const serviceName = entry.name.replace(/^_+/, "");
|
|
213
|
+
if (moduleFilter && moduleFilter !== serviceName && moduleFilter !== entry.name) continue;
|
|
214
|
+
if (!(await FileSys.fileExists(path.join(libPath, entry.name, `${serviceName}.service.ts`)))) continue;
|
|
215
|
+
modules.push(
|
|
216
|
+
await createModuleContext(workspace, sys, "service", entry.name, serviceName, includeAbstractContent),
|
|
217
|
+
);
|
|
218
|
+
} else {
|
|
219
|
+
if (moduleFilter && moduleFilter !== entry.name) continue;
|
|
220
|
+
if (!(await FileSys.fileExists(path.join(libPath, entry.name, `${entry.name}.constant.ts`)))) continue;
|
|
221
|
+
modules.push(await createModuleContext(workspace, sys, "domain", entry.name, entry.name, includeAbstractContent));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const scalarRoot = path.join(libPath, "__scalar");
|
|
226
|
+
for (const entry of await safeReadDir(scalarRoot)) {
|
|
227
|
+
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
228
|
+
if (moduleFilter && moduleFilter !== entry.name) continue;
|
|
229
|
+
if (!(await FileSys.fileExists(path.join(scalarRoot, entry.name, `${entry.name}.constant.ts`)))) continue;
|
|
230
|
+
modules.push(await createModuleContext(workspace, sys, "scalar", entry.name, entry.name, includeAbstractContent));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return modules.sort((a, b) => `${a.sysName}:${a.path}`.localeCompare(`${b.sysName}:${b.path}`));
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const getSysContext = async (
|
|
237
|
+
workspace: WorkspaceExecutor,
|
|
238
|
+
type: "app" | "lib",
|
|
239
|
+
name: string,
|
|
240
|
+
options: AkanContextOptions,
|
|
241
|
+
): Promise<AkanSysContext> => {
|
|
242
|
+
const sys = type === "app" ? AppExecutor.from(workspace, name) : LibExecutor.from(workspace, name);
|
|
243
|
+
return {
|
|
244
|
+
type,
|
|
245
|
+
name,
|
|
246
|
+
path: `${type}s/${name}`,
|
|
247
|
+
hasConfig: await FileSys.fileExists(path.join(sys.cwdPath, "akan.config.ts")),
|
|
248
|
+
modules: await getSysModules(workspace, sys, {
|
|
249
|
+
includeAbstractContent: options.includeAbstractContent,
|
|
250
|
+
module: options.module,
|
|
251
|
+
}),
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
export class AkanContextAnalyzer {
|
|
256
|
+
static async analyze(workspace: WorkspaceExecutor, options: AkanContextOptions = {}): Promise<AkanWorkspaceContext> {
|
|
257
|
+
const [appNames, libNames, pkgNames] = await workspace.getExecs();
|
|
258
|
+
const rootPackageJson = await safeReadJson<PackageJson>(path.join(workspace.workspaceRoot, "package.json"));
|
|
259
|
+
const filteredApps = options.app ? appNames.filter((name) => name === options.app) : appNames;
|
|
260
|
+
const [apps, libs, pkgs] = await Promise.all([
|
|
261
|
+
Promise.all(filteredApps.map((name) => getSysContext(workspace, "app", name, options))),
|
|
262
|
+
Promise.all(libNames.map((name) => getSysContext(workspace, "lib", name, options))),
|
|
263
|
+
Promise.all(
|
|
264
|
+
pkgNames.map(async (name) => {
|
|
265
|
+
const packageJson = await safeReadJson<PackageJson>(
|
|
266
|
+
path.join(workspace.workspaceRoot, "pkgs", name, "package.json"),
|
|
267
|
+
);
|
|
268
|
+
return {
|
|
269
|
+
name,
|
|
270
|
+
path: `pkgs/${name}`,
|
|
271
|
+
...(packageJson?.version ? { version: packageJson.version } : {}),
|
|
272
|
+
};
|
|
273
|
+
}),
|
|
274
|
+
),
|
|
275
|
+
]);
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
schemaVersion: 1,
|
|
279
|
+
repoName: workspace.repoName,
|
|
280
|
+
root: workspace.workspaceRoot,
|
|
281
|
+
packageVersion: rootPackageJson?.dependencies?.akanjs ?? rootPackageJson?.devDependencies?.["@akanjs/devkit"],
|
|
282
|
+
apps,
|
|
283
|
+
libs,
|
|
284
|
+
pkgs,
|
|
285
|
+
generatedFiles,
|
|
286
|
+
validationCommands: ["akan lint <app-or-lib-or-pkg>", "akan build <app-name>", "akan start <app-name>"],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
static async doctor(
|
|
291
|
+
workspace: WorkspaceExecutor,
|
|
292
|
+
{ strict = false }: { strict?: boolean } = {},
|
|
293
|
+
): Promise<AkanDoctorResult> {
|
|
294
|
+
const context = await AkanContextAnalyzer.analyze(workspace);
|
|
295
|
+
const diagnostics: AkanDiagnostic[] = [];
|
|
296
|
+
|
|
297
|
+
for (const app of context.apps) {
|
|
298
|
+
const appPath = path.join(workspace.workspaceRoot, app.path);
|
|
299
|
+
for (const entry of await safeReadDir(appPath)) {
|
|
300
|
+
const allowed = entry.isDirectory() ? appRootAllowDirs.has(entry.name) : appRootAllowFiles.has(entry.name);
|
|
301
|
+
if (!allowed) {
|
|
302
|
+
diagnostics.push({
|
|
303
|
+
severity: "warning",
|
|
304
|
+
code: "app-root-unknown-entry",
|
|
305
|
+
path: `${app.path}/${entry.name}`,
|
|
306
|
+
message: `Unexpected ${entry.isDirectory() ? "folder" : "file"} in app root: ${app.path}/${entry.name}`,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
for (const sys of [...context.apps, ...context.libs]) {
|
|
313
|
+
for (const module of sys.modules) {
|
|
314
|
+
if (!module.abstract.exists) {
|
|
315
|
+
diagnostics.push({
|
|
316
|
+
severity: strict ? "error" : "warning",
|
|
317
|
+
code: "module-abstract-missing",
|
|
318
|
+
path: module.abstract.path,
|
|
319
|
+
message: `${capitalize(module.kind)} module ${sys.name}:${module.name} should include ${module.abstract.path}`,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return { schemaVersion: 1, strict, diagnostics };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
static findModules(context: AkanWorkspaceContext, moduleName?: string | null) {
|
|
329
|
+
const modules = [...context.apps, ...context.libs].flatMap((sys) => sys.modules);
|
|
330
|
+
return moduleName
|
|
331
|
+
? modules.filter((module) => module.name === moduleName || module.folderName === moduleName)
|
|
332
|
+
: modules;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
static renderMarkdown(context: AkanWorkspaceContext, { module: moduleName }: { module?: string | null } = {}) {
|
|
336
|
+
const lines = [`# Akan Workspace Context`, "", `- Repo: ${context.repoName}`, `- Root: ${context.root}`];
|
|
337
|
+
if (context.packageVersion) lines.push(`- Akan version: ${context.packageVersion}`);
|
|
338
|
+
lines.push("", "## Apps", ...context.apps.map((app) => `- ${app.name}: ${app.modules.length} module(s)`));
|
|
339
|
+
lines.push("", "## Libraries", ...context.libs.map((lib) => `- ${lib.name}: ${lib.modules.length} module(s)`));
|
|
340
|
+
lines.push(
|
|
341
|
+
"",
|
|
342
|
+
"## Packages",
|
|
343
|
+
...context.pkgs.map((pkg) => `- ${pkg.name}${pkg.version ? ` (${pkg.version})` : ""}`),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const modules = AkanContextAnalyzer.findModules(context, moduleName);
|
|
347
|
+
lines.push("", "## Modules");
|
|
348
|
+
for (const module of modules) {
|
|
349
|
+
lines.push("", `### ${module.sysName}:${module.name} (${module.kind})`, `- Path: ${module.path}`);
|
|
350
|
+
lines.push(`- Abstract: ${module.abstract.exists ? module.abstract.path : "missing"}`);
|
|
351
|
+
if (module.abstract.exists && module.abstract.content) lines.push("", module.abstract.content.trim(), "");
|
|
352
|
+
else if (module.abstract.headings.length)
|
|
353
|
+
lines.push(`- Abstract headings: ${module.abstract.headings.join(", ")}`);
|
|
354
|
+
lines.push(`- Files: ${module.files.join(", ") || "none"}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
lines.push("", "## Validation", ...context.validationCommands.map((command) => `- \`${command}\``));
|
|
358
|
+
return `${lines.join("\n")}\n`;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -184,6 +184,7 @@ export class ApplicationBuildRunner {
|
|
|
184
184
|
outdir: this.#app.dist.cwdPath,
|
|
185
185
|
target: "bun",
|
|
186
186
|
minify: true,
|
|
187
|
+
naming: { entry: "[name].[ext]", chunk: "chunk-[hash].[ext]" },
|
|
187
188
|
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
188
189
|
plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
|
|
189
190
|
});
|
|
@@ -198,12 +199,45 @@ export class ApplicationBuildRunner {
|
|
|
198
199
|
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
199
200
|
plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
|
|
200
201
|
});
|
|
202
|
+
const consoleRuntimeResult = await this.#buildOrThrow("console-runtime", {
|
|
203
|
+
entrypoints: [this.#resolveConsoleRuntimeBuildEntry()],
|
|
204
|
+
outdir: this.#app.dist.cwdPath,
|
|
205
|
+
target: "bun",
|
|
206
|
+
minify: true,
|
|
207
|
+
naming: { entry: "console-runtime.[ext]", chunk: "chunk-[hash].[ext]" },
|
|
208
|
+
define: { "process.env.NODE_ENV": JSON.stringify("production") },
|
|
209
|
+
});
|
|
210
|
+
await this.#writeConsoleShim();
|
|
201
211
|
return {
|
|
202
|
-
entrypoints: backendEntryPoints.length +
|
|
203
|
-
outputs: backendResult.outputs.length + rscWorkerResult.outputs.length,
|
|
212
|
+
entrypoints: backendEntryPoints.length + 2,
|
|
213
|
+
outputs: backendResult.outputs.length + rscWorkerResult.outputs.length + consoleRuntimeResult.outputs.length + 1,
|
|
204
214
|
};
|
|
205
215
|
}
|
|
206
216
|
|
|
217
|
+
async #writeConsoleShim() {
|
|
218
|
+
await Bun.write(
|
|
219
|
+
path.join(this.#app.dist.cwdPath, "console.js"),
|
|
220
|
+
`import { cnst, db, dict, option, server, sig, srv } from "./server.js";
|
|
221
|
+
import { assertAkanConsoleAllowed, startAkanConsole } from "./console-runtime.js";
|
|
222
|
+
|
|
223
|
+
const run = async () => {
|
|
224
|
+
assertAkanConsoleAllowed(server.env);
|
|
225
|
+
await server.start({ listen: false, web: false });
|
|
226
|
+
try {
|
|
227
|
+
await startAkanConsole(server, { globals: { cnst, db, dict, option, sig, srv } });
|
|
228
|
+
} finally {
|
|
229
|
+
await server.stop();
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
void run().catch((error) => {
|
|
234
|
+
console.error(error);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
});
|
|
237
|
+
`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
207
241
|
#resolveRscWorkerBuildEntry(): string {
|
|
208
242
|
try {
|
|
209
243
|
return Bun.resolveSync("akanjs/server/rsc-worker", import.meta.dir);
|
|
@@ -212,6 +246,14 @@ export class ApplicationBuildRunner {
|
|
|
212
246
|
}
|
|
213
247
|
}
|
|
214
248
|
|
|
249
|
+
#resolveConsoleRuntimeBuildEntry(): string {
|
|
250
|
+
try {
|
|
251
|
+
return path.join(path.dirname(Bun.resolveSync("akanjs/server", import.meta.dir)), "console.ts");
|
|
252
|
+
} catch {
|
|
253
|
+
return path.join(this.#app.workspace.workspaceRoot, "pkgs/akanjs/server/console.ts");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
215
257
|
async #buildCsr() {
|
|
216
258
|
return await new CsrArtifactBuilder(this.#app, "build").build();
|
|
217
259
|
}
|
package/devkitUtils.test.ts
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { ApplicationBuildReporter } from "./applicationBuildReporter";
|
|
6
6
|
import { resolveSignalTestPreloadPath } from "./applicationTestPreload";
|
|
7
7
|
import { TypeScriptDependencyScanner } from "./dependencyScanner";
|
|
8
|
+
import { AppExecutor, WorkspaceExecutor } from "./executors";
|
|
8
9
|
import { extractDependencies } from "./extractDeps";
|
|
9
10
|
import { getModelFileData } from "./getModelFileData";
|
|
10
11
|
import type { PackageJson, TsConfigJson } from "./types";
|
|
@@ -212,6 +213,45 @@ describe("TypeScriptDependencyScanner", () => {
|
|
|
212
213
|
});
|
|
213
214
|
});
|
|
214
215
|
|
|
216
|
+
describe("scan convention", () => {
|
|
217
|
+
test("allows module abstract markdown files", async () => {
|
|
218
|
+
const root = await makeTempRoot();
|
|
219
|
+
const appName = "scanAbstractDemo";
|
|
220
|
+
const appDir = path.join(root, `apps/${appName}`);
|
|
221
|
+
await write(path.join(root, ".gitignore"), "");
|
|
222
|
+
await write(
|
|
223
|
+
path.join(root, ".env"),
|
|
224
|
+
["AKAN_PUBLIC_REPO_NAME=repo", 'AKAN_PUBLIC_SERVE_DOMAIN="localhost"', "AKAN_PUBLIC_ENV=local", ""].join("\n"),
|
|
225
|
+
);
|
|
226
|
+
await write(
|
|
227
|
+
path.join(root, "package.json"),
|
|
228
|
+
JSON.stringify({
|
|
229
|
+
name: "repo",
|
|
230
|
+
version: "1.0.0",
|
|
231
|
+
description: "repo",
|
|
232
|
+
dependencies: {},
|
|
233
|
+
devDependencies: {},
|
|
234
|
+
}),
|
|
235
|
+
);
|
|
236
|
+
await write(path.join(root, "tsconfig.json"), JSON.stringify({ compilerOptions: { target: "ESNext", paths: {} } }));
|
|
237
|
+
await write(path.join(appDir, "package.json"), JSON.stringify({ name: appName, version: "1.0.0" }));
|
|
238
|
+
await write(path.join(appDir, "tsconfig.json"), JSON.stringify({ compilerOptions: { target: "ESNext" } }));
|
|
239
|
+
await write(path.join(appDir, "akan.config.ts"), "export default {};\n");
|
|
240
|
+
await write(path.join(appDir, "main.ts"), "export {};\n");
|
|
241
|
+
await write(path.join(appDir, "lib/post/post.abstract.md"), "# Post Abstract\n");
|
|
242
|
+
await write(path.join(appDir, "lib/post/post.constant.ts"), "export class Post {}\n");
|
|
243
|
+
await write(path.join(appDir, "lib/_payment/payment.abstract.md"), "# Payment Service Abstract\n");
|
|
244
|
+
await write(path.join(appDir, "lib/_payment/payment.service.ts"), "export const payment = {};\n");
|
|
245
|
+
await write(path.join(appDir, "lib/__scalar/money/money.abstract.md"), "# Money Scalar Abstract\n");
|
|
246
|
+
await write(path.join(appDir, "lib/__scalar/money/money.constant.ts"), "export class Money {}\n");
|
|
247
|
+
|
|
248
|
+
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "repo" });
|
|
249
|
+
const app = AppExecutor.from(workspace, appName);
|
|
250
|
+
|
|
251
|
+
await expect(app.scan({ write: false })).resolves.toBeDefined();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
215
255
|
describe("getModelFileData", () => {
|
|
216
256
|
test("reads model files and derives imported local, scalar, and lib models", async () => {
|
|
217
257
|
const root = await makeTempRoot();
|
package/executors.test.ts
CHANGED
|
@@ -139,6 +139,10 @@ describe("Executor filesystem helpers", () => {
|
|
|
139
139
|
expect(await readFile(path.join(root, "workspace/.gitignore"), "utf8")).toContain("node_modules");
|
|
140
140
|
expect(await readFile(path.join(root, "workspace/.env"), "utf8")).toContain("AKAN_PUBLIC_REPO_NAME");
|
|
141
141
|
expect(await readFile(path.join(root, "workspace/.vscode/settings.json"), "utf8")).toContain("typescript.tsdk");
|
|
142
|
+
expect(await readFile(path.join(root, "workspace/.cursor/rules/akan.mdc"), "utf8")).toContain(
|
|
143
|
+
"Akan.js Workspace Rules",
|
|
144
|
+
);
|
|
145
|
+
expect(await readFile(path.join(root, "workspace/AGENTS.md"), "utf8")).toContain("sample Agent Guide");
|
|
142
146
|
expect(await readFile(path.join(root, "workspace/biome.json"), "utf8")).toContain(
|
|
143
147
|
"./node_modules/@akanjs/devkit/lint/no-import-client-functions.grit",
|
|
144
148
|
);
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.11",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@langchain/openai": "^1.4.6",
|
|
33
33
|
"@tailwindcss/node": "^4.3.0",
|
|
34
34
|
"@trapezedev/project": "^7.1.4",
|
|
35
|
-
"akanjs": "2.2.
|
|
35
|
+
"akanjs": "2.2.11",
|
|
36
36
|
"chalk": "^5.6.2",
|
|
37
37
|
"commander": "^14.0.3",
|
|
38
38
|
"daisyui": "^5.5.20",
|
package/prompter.ts
CHANGED
|
@@ -11,10 +11,7 @@ interface FileUpdateRequestProps {
|
|
|
11
11
|
export class Prompter {
|
|
12
12
|
static async #getGuidelineRoot() {
|
|
13
13
|
const dirname = getDirname(import.meta.url);
|
|
14
|
-
const candidates = [
|
|
15
|
-
`${dirname}/guidelines`,
|
|
16
|
-
`${dirname}/../cli/guidelines`,
|
|
17
|
-
];
|
|
14
|
+
const candidates = [`${dirname}/guidelines`, `${dirname}/../cli/guidelines`];
|
|
18
15
|
for (const candidate of candidates) {
|
|
19
16
|
try {
|
|
20
17
|
await fsPromise.access(candidate);
|
|
@@ -28,14 +25,16 @@ export class Prompter {
|
|
|
28
25
|
|
|
29
26
|
static async selectGuideline() {
|
|
30
27
|
const guidelineRoot = await Prompter.#getGuidelineRoot();
|
|
31
|
-
const guideNames =
|
|
32
|
-
(name) => !name.startsWith("_"),
|
|
33
|
-
);
|
|
28
|
+
const guideNames = await Prompter.listGuidelines();
|
|
34
29
|
return await select({
|
|
35
30
|
message: "Select a guideline",
|
|
36
31
|
choices: guideNames.map((name) => ({ name, value: name })),
|
|
37
32
|
});
|
|
38
33
|
}
|
|
34
|
+
static async listGuidelines() {
|
|
35
|
+
const guidelineRoot = await Prompter.#getGuidelineRoot();
|
|
36
|
+
return (await fsPromise.readdir(guidelineRoot)).filter((name) => !name.startsWith("_")).sort();
|
|
37
|
+
}
|
|
39
38
|
static async getGuideJson(guideName: string): Promise<GuideGenerateJson> {
|
|
40
39
|
const guidelineRoot = await Prompter.#getGuidelineRoot();
|
|
41
40
|
const filePath = `${guidelineRoot}/${guideName}/${guideName}.generate.json`;
|
package/scanInfo.ts
CHANGED
|
@@ -99,6 +99,10 @@ const isAllowedLibRootFile = (filename: string) =>
|
|
|
99
99
|
libRootAllowedFiles.has(filename) || rootSignalTestFilePattern.test(filename);
|
|
100
100
|
const getScanPath = (exec: AppExecutor | LibExecutor, relativePath: string) =>
|
|
101
101
|
path.posix.join(`${exec.type}s`, exec.name, relativePath.split(path.sep).join("/"));
|
|
102
|
+
const getModuleNameFromPath = (kind: ModuleKind, modulePath: string) => {
|
|
103
|
+
const dirname = path.basename(modulePath);
|
|
104
|
+
return kind === "service" ? dirname.replace(/^_+/, "") : dirname;
|
|
105
|
+
};
|
|
102
106
|
|
|
103
107
|
async function assertScanConvention(exec: AppExecutor | LibExecutor, libRoot: { files: string[]; dirs: string[] }) {
|
|
104
108
|
const violations: string[] = [];
|
|
@@ -158,6 +162,7 @@ async function validateModuleFiles(
|
|
|
158
162
|
modulePath: string,
|
|
159
163
|
) {
|
|
160
164
|
const { files, dirs } = await exec.getFilesAndDirs(modulePath);
|
|
165
|
+
const moduleName = getModuleNameFromPath(kind, modulePath);
|
|
161
166
|
dirs.forEach((dirname) => {
|
|
162
167
|
violations.push(`${getScanPath(exec, path.join(modulePath, dirname))}: unsupported module folder`);
|
|
163
168
|
});
|
|
@@ -165,6 +170,7 @@ async function validateModuleFiles(
|
|
|
165
170
|
files.forEach((filename) => {
|
|
166
171
|
const filePath = path.join(modulePath, filename);
|
|
167
172
|
if (filename === "index.ts" || filename === "index.tsx" || isAllowedTestFile(filename)) return;
|
|
173
|
+
if (filename === `${moduleName}.abstract.md`) return;
|
|
168
174
|
|
|
169
175
|
const uiMatch = filename.match(/\.([A-Z][A-Za-z0-9]*)\.tsx$/);
|
|
170
176
|
if (uiMatch) {
|
|
@@ -515,7 +521,7 @@ export class PkgInfo {
|
|
|
515
521
|
readonly name: string;
|
|
516
522
|
private scanResult: PkgScanResult;
|
|
517
523
|
|
|
518
|
-
static async
|
|
524
|
+
static async scanExecutor(exec: PkgExecutor) {
|
|
519
525
|
const [tsconfig, rootPackageJson] = await Promise.all([exec.getTsConfig(), exec.workspace.getPackageJson()]);
|
|
520
526
|
const scanner = await TypeScriptDependencyScanner.from(exec);
|
|
521
527
|
const npmSet = new Set(Object.keys({ ...rootPackageJson.dependencies, ...rootPackageJson.devDependencies }));
|
|
@@ -547,7 +553,7 @@ export class PkgInfo {
|
|
|
547
553
|
const existingPkgInfo = PkgInfo.#pkgInfos.get(exec.name);
|
|
548
554
|
if (existingPkgInfo && !options.refresh) return existingPkgInfo;
|
|
549
555
|
|
|
550
|
-
const scanResult = await PkgInfo.
|
|
556
|
+
const scanResult = await PkgInfo.scanExecutor(exec);
|
|
551
557
|
const pkgInfo = new PkgInfo(exec, scanResult);
|
|
552
558
|
PkgInfo.#pkgInfos.set(exec.name, pkgInfo);
|
|
553
559
|
return pkgInfo;
|