@akanjs/devkit 2.2.10 → 2.2.12

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 CHANGED
@@ -1,5 +1,30 @@
1
1
  # @akanjs/devkit
2
2
 
3
+ ## 2.2.12
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [666e46c]
8
+ - Updated dependencies [666e46c]
9
+ - akanjs@2.2.12
10
+
11
+ ## 2.2.11
12
+
13
+ ### Patch Changes
14
+
15
+ - 8af7a9d: Add agent-oriented workspace context tooling, generated agent rule templates, and LLM documentation surfaces for Akan workspaces.
16
+ - 8190632: Add Akan server console support with CLI/build integration and documentation for console-oriented workflows.
17
+ - 4bce7f9: Add initial LLM discovery docs and stabilize Akan client/runtime behavior.
18
+
19
+ - Add `/llms.txt` documentation discovery for Akan docs.
20
+ - Add `wsConnect` support for automatic WebSocket connections.
21
+ - Delay client bootstrap module execution until the SSR fizz stream is ready.
22
+ - Improve route tree, HMR, fetch, store, and SSR/client runtime stability.
23
+
24
+ - Updated dependencies [8190632]
25
+ - Updated dependencies [4bce7f9]
26
+ - akanjs@2.2.11
27
+
3
28
  ## 2.2.7
4
29
 
5
30
  ### 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 + 1,
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
  }
@@ -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
@@ -1,6 +1,7 @@
1
1
  export * from "./aiEditor";
2
2
  export * from "./akanApp";
3
3
  export * from "./akanConfig";
4
+ export * from "./akanContext";
4
5
  export * from "./applicationBuildReporter";
5
6
  export * from "./applicationBuildRunner";
6
7
  export * from "./applicationReleasePackager";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akanjs/devkit",
3
- "version": "2.2.10",
3
+ "version": "2.2.12",
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.10",
35
+ "akanjs": "2.2.12",
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 = (await fsPromise.readdir(guidelineRoot)).filter(
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 getScanResult(exec: PkgExecutor) {
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.getScanResult(exec);
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;