@akanjs/devkit 3.0.0-alpha.0 → 3.0.0-alpha.10

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/agentsIndex.ts CHANGED
@@ -19,6 +19,32 @@ const loadRecipeScanner = () => (recipeScannerLoad ??= import("./recipeScanner")
19
19
  export const AGENT_BLOCK_START = "<!-- akan:agent:start -->";
20
20
  export const AGENT_BLOCK_END = "<!-- akan:agent:end -->";
21
21
 
22
+ const AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
23
+
24
+ /**
25
+ * The generating release, stamped into the workspace block. Nothing re-runs `akan agent install` on its own, so
26
+ * without a stamp a workspace carrying conventions from four releases ago is indistinguishable from a current one —
27
+ * `akan doctor` compares this against the installed devkit and says so.
28
+ */
29
+ export const stampBlockVersion = (block: string, version: string): string =>
30
+ `${AGENT_VERSION_PREFIX}${version} -->\n\n${block}`;
31
+
32
+ /** The stamped version of a committed AGENTS.md, or null when it predates stamping or carries no block. */
33
+ export const extractBlockVersion = (content: string): string | null =>
34
+ content.match(/<!-- akan:agent:version ([^\s]+) -->/)?.[1] ?? null;
35
+
36
+ /** The running `@akanjs/devkit` version; null when its package.json is unreadable, which must not fail a doctor run. */
37
+ export const readDevkitVersion = async (): Promise<string | null> => {
38
+ const { readFile } = await import("node:fs/promises");
39
+ const { getDirname } = await import("./getDirname");
40
+ try {
41
+ const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
42
+ return (JSON.parse(raw) as { version?: string }).version ?? null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ };
47
+
22
48
  export interface AgentsIndexScope {
23
49
  type: "app" | "lib";
24
50
  name: string;
package/akanContext.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readdir } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { capitalize } from "akanjs/common";
4
+ import { extractBlockVersion, readDevkitVersion } from "./agentsIndex";
4
5
  import { AppExecutor, LibExecutor, type SysExecutor, type WorkspaceExecutor } from "./executors";
5
6
  import { FileSys } from "./fileSys";
6
7
  import { collectRecipeSources, findInlineRecipeDuplicates, scanRecipes } from "./recipeScanner";
@@ -14,6 +15,7 @@ import {
14
15
  workflowRunArtifactPath,
15
16
  workflowSyncDir,
16
17
  } from "./workflow";
18
+ import { appRootAllowedDirs, appRootAllowedFiles, isScannedAppRootEntry } from "./workspaceLayout";
17
19
 
18
20
  export type AkanContextFormat = "json" | "markdown";
19
21
  export type AkanModuleKind = "domain" | "service" | "scalar";
@@ -275,6 +277,8 @@ const validationCommands = [
275
277
  "akan test <app-or-lib-or-pkg>",
276
278
  "akan build <app-name>",
277
279
  "akan doctor --strict --format json",
280
+ "akan quality scan [--format json]",
281
+ "akan quality ssr [--format json]",
278
282
  ];
279
283
 
280
284
  const unknownGeneratedFilesFreshness: GeneratedFilesFreshness = {
@@ -313,36 +317,6 @@ const moduleShapeFiles = (module: AkanModuleContext) => {
313
317
  const constantFieldNames = (content: string) =>
314
318
  [...content.matchAll(/\b([A-Za-z_$][\w$]*)\s*:\s*field\(/g)].map((match) => match[1]).filter(Boolean);
315
319
 
316
- const appRootAllowFiles = new Set([
317
- // 스코프 에이전트 가이드 — scan(write) 이 유지 (agentsIndex.ts); scanInfo.ts 의 appRootAllowedFiles 와 동기
318
- "AGENTS.md",
319
- "CLAUDE.md",
320
- "akan.app.json",
321
- "akan.config.ts",
322
- "capacitor.config.ts",
323
- "client.ts",
324
- "main.ts",
325
- "package.json",
326
- "server.ts",
327
- "tsconfig.json",
328
- ]);
329
-
330
- const appRootAllowDirs = new Set([
331
- ".akan",
332
- "android",
333
- "common",
334
- "env",
335
- "ios",
336
- "lib",
337
- "page",
338
- "private",
339
- "public",
340
- "script",
341
- "srvkit",
342
- "ui",
343
- "webkit",
344
- ]);
345
-
346
320
  const safeReadDir = async (dirPath: string) => {
347
321
  try {
348
322
  return (await readdir(dirPath, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
@@ -647,6 +621,30 @@ export class AkanContextAnalyzer {
647
621
  };
648
622
  }
649
623
 
624
+ // The conventions in AGENTS.md ship with the package, but nothing re-renders them on `bun update` — a workspace
625
+ // keeps whichever release wrote its block until someone re-runs the install. Comparing the stamp against the
626
+ // running devkit is the only signal that the guide an agent is reading is older than the framework it describes.
627
+ static async #agentGuideDrift(workspace: WorkspaceExecutor) {
628
+ const installed = await readDevkitVersion();
629
+ if (!installed) return null;
630
+ const content = await safeReadText(path.join(workspace.workspaceRoot, "AGENTS.md"));
631
+ if (content === null) return null;
632
+ const stamped = extractBlockVersion(content);
633
+ if (stamped === installed) return null;
634
+ const hint = "Re-render the AGENTS.md managed block from the installed framework release.";
635
+ if (!stamped)
636
+ return {
637
+ code: "agent-guide-unstamped",
638
+ message: `AGENTS.md carries no generated-version stamp, so its conventions may predate @akanjs/devkit ${installed}`,
639
+ hint,
640
+ };
641
+ return {
642
+ code: "agent-guide-stale",
643
+ message: `AGENTS.md was generated by @akanjs/devkit ${stamped}, but ${installed} is installed`,
644
+ hint,
645
+ };
646
+ }
647
+
650
648
  static async doctor(
651
649
  workspace: WorkspaceExecutor,
652
650
  {
@@ -671,7 +669,8 @@ export class AkanContextAnalyzer {
671
669
  for (const app of context.apps) {
672
670
  const appPath = path.join(workspace.workspaceRoot, app.path);
673
671
  for (const entry of await safeReadDir(appPath)) {
674
- const allowed = entry.isDirectory() ? appRootAllowDirs.has(entry.name) : appRootAllowFiles.has(entry.name);
672
+ if (!isScannedAppRootEntry(entry.name)) continue;
673
+ const allowed = entry.isDirectory() ? appRootAllowedDirs.has(entry.name) : appRootAllowedFiles.has(entry.name);
675
674
  if (!allowed) {
676
675
  const action = repairAction(
677
676
  "module-shape",
@@ -691,6 +690,19 @@ export class AkanContextAnalyzer {
691
690
  }
692
691
  }
693
692
 
693
+ const agentDrift = await AkanContextAnalyzer.#agentGuideDrift(workspace);
694
+ if (agentDrift) {
695
+ const action = repairAction("generated", "akan agent install agents-md", agentDrift.hint, true);
696
+ diagnostics.push({
697
+ severity: "warning",
698
+ code: agentDrift.code,
699
+ path: "AGENTS.md",
700
+ message: agentDrift.message,
701
+ repairActions: [action],
702
+ });
703
+ repairActions.push(action);
704
+ }
705
+
694
706
  for (const sys of [...context.apps, ...context.libs]) {
695
707
  for (const module of sys.modules) {
696
708
  if (!module.abstract.exists) {
@@ -112,7 +112,7 @@ export class ApplicationBuildRunner {
112
112
  () => precompressArtifacts(this.#app),
113
113
  (result) =>
114
114
  result.files > 0
115
- ? `${result.files} files, ${ApplicationBuildRunner.formatBytes(result.inputBytes)} -> ${ApplicationBuildRunner.formatBytes(result.outputBytes)}`
115
+ ? `${result.files} files, ${ApplicationBuildRunner.formatBytes(result.inputBytes)} -> gzip ${ApplicationBuildRunner.formatBytes(result.outputBytes)} / br ${ApplicationBuildRunner.formatBytes(result.brotliBytes)}`
116
116
  : "no files",
117
117
  phaseOptions,
118
118
  );
@@ -0,0 +1,301 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.5.8/schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": false
7
+ },
8
+ "formatter": {
9
+ "enabled": true,
10
+ "indentStyle": "space",
11
+ "indentWidth": 2,
12
+ "lineWidth": 120
13
+ },
14
+ "linter": {
15
+ "enabled": true,
16
+ "rules": {
17
+ "preset": "recommended",
18
+ "suspicious": {
19
+ "noConsole": {
20
+ "level": "error",
21
+ "options": {
22
+ "allow": ["assert", "error", "info", "warn"]
23
+ }
24
+ },
25
+ "noArrayIndexKey": "off",
26
+ "noShadowRestrictedNames": "off",
27
+ "noUnnecessaryConditions": {
28
+ "level": "error"
29
+ }
30
+ },
31
+ "correctness": {
32
+ "noUnusedFunctionParameters": "off",
33
+ "noUnusedImports": {
34
+ "level": "error",
35
+ "fix": "safe"
36
+ },
37
+ "useParseIntRadix": "off",
38
+ "useExhaustiveDependencies": "off",
39
+ "useHookAtTopLevel": "off"
40
+ },
41
+ "nursery": {
42
+ "useSortedClasses": {
43
+ "level": "error",
44
+ "fix": "safe",
45
+ "options": {
46
+ "attributes": ["classList"],
47
+ "functions": ["cn"]
48
+ }
49
+ }
50
+ },
51
+ "a11y": "off",
52
+ "complexity": {
53
+ "noStaticOnlyClass": "off"
54
+ },
55
+ "style": {
56
+ "useTemplate": {
57
+ "level": "warn",
58
+ "fix": "safe"
59
+ }
60
+ }
61
+ },
62
+ "domains": {
63
+ "project": "recommended",
64
+ "react": "recommended",
65
+ "test": "recommended",
66
+ "types": "all"
67
+ }
68
+ },
69
+ "javascript": {
70
+ "parser": {
71
+ "unsafeParameterDecoratorsEnabled": true
72
+ },
73
+ "formatter": {
74
+ "quoteStyle": "double"
75
+ }
76
+ },
77
+ "assist": {
78
+ "enabled": true,
79
+ "actions": {
80
+ "source": {
81
+ "organizeImports": "on"
82
+ }
83
+ }
84
+ },
85
+ "overrides": [
86
+ {
87
+ "includes": [
88
+ "**/env.client.local.ts",
89
+ "**/env.client.debug.ts",
90
+ "**/env.client.develop.ts",
91
+ "**/env.client.main.ts",
92
+ "**/env.client.testing.ts",
93
+ "**/env.server.local.ts",
94
+ "**/env.server.develop.ts",
95
+ "**/env.server.main.ts",
96
+ "**/env.server.testing.ts",
97
+ "**/env.server.type.ts",
98
+ "apps/*/lib/cnst.ts",
99
+ "apps/*/lib/dict.ts",
100
+ "apps/*/lib/db.ts",
101
+ "apps/*/lib/srv.ts",
102
+ "apps/*/lib/st.ts",
103
+ "apps/*/lib/sig.ts",
104
+ "apps/*/lib/useClient.ts",
105
+ "apps/*/lib/useServer.ts",
106
+ "apps/*/client.ts",
107
+ "apps/*/server.ts",
108
+ "apps/*/lib/*/index.tsx",
109
+ "libs/*/lib/cnst.ts",
110
+ "libs/*/lib/dict.ts",
111
+ "libs/*/lib/db.ts",
112
+ "libs/*/lib/srv.ts",
113
+ "libs/*/lib/st.ts",
114
+ "libs/*/lib/sig.ts",
115
+ "libs/*/lib/useClient.ts",
116
+ "libs/*/lib/useServer.ts",
117
+ "libs/*/client.ts",
118
+ "libs/*/server.ts",
119
+ "libs/*/index.ts",
120
+ "libs/*/lib/*/index.tsx"
121
+ ],
122
+ "linter": { "enabled": false },
123
+ "formatter": { "enabled": false },
124
+ "assist": { "enabled": false }
125
+ },
126
+ {
127
+ "includes": [
128
+ "apps/**/*.ts",
129
+ "apps/**/*.tsx",
130
+ "libs/**/*.ts",
131
+ "libs/**/*.tsx",
132
+ "!**/*.test.ts",
133
+ "!**/*.test.tsx",
134
+ "!**/*.spec.ts",
135
+ "!**/*.spec.tsx",
136
+ "!**/*.constant.ts",
137
+ "!**/common/**",
138
+ "!**/env/**"
139
+ ],
140
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-throw-raw-error.grit"]
141
+ },
142
+ {
143
+ "includes": ["**/page/**/*.ts", "**/page/**/*.tsx", "**/*.Unit.tsx", "**/*.View.tsx"],
144
+ "plugins": [
145
+ "./node_modules/@akanjs/devkit/lint/no-import-client-functions.grit",
146
+ "./node_modules/@akanjs/devkit/lint/no-use-client-in-server.grit"
147
+ ]
148
+ },
149
+ {
150
+ "includes": ["**/page/**/*.ts", "**/page/**/*.tsx"],
151
+ "plugins": ["./node_modules/@akanjs/devkit/lint/non-scalar-props-restricted.grit"]
152
+ },
153
+ {
154
+ "includes": ["**/*.constant.ts", "**/*.document.ts", "**/*.service.ts", "**/*.store.ts"],
155
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-js-private-class-method.grit"]
156
+ },
157
+ {
158
+ "includes": ["**/*.store.ts"],
159
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-return-in-store-action.grit"]
160
+ },
161
+ {
162
+ "includes": ["**/*.signal.ts"],
163
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-redeclare-predefined-endpoint.grit"]
164
+ },
165
+ {
166
+ "includes": [
167
+ "**/ui/**/*.ts",
168
+ "**/ui/**/*.tsx",
169
+ "**/webkit/**/*.ts",
170
+ "**/webkit/**/*.tsx",
171
+ "**/common/**/*.ts",
172
+ "**/page/**/*.tsx",
173
+ "**/*.constant.ts",
174
+ "**/*.store.ts",
175
+ "**/*.Template.tsx",
176
+ "**/*.Unit.tsx",
177
+ "**/*.Util.tsx",
178
+ "**/*.View.tsx",
179
+ "**/*.Zone.tsx",
180
+ "!**/*.test.ts",
181
+ "!**/*.test.tsx",
182
+ "!**/*.spec.ts",
183
+ "!**/*.spec.tsx"
184
+ ],
185
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-bang-comment-in-client.grit"]
186
+ },
187
+ {
188
+ "includes": [
189
+ "**/*.constant.ts",
190
+ "**/*.dictionary.ts",
191
+ "**/*.document.ts",
192
+ "**/*.service.ts",
193
+ "**/*.signal.ts",
194
+ "**/*.store.ts",
195
+ "**/*.Template.tsx",
196
+ "**/*.Unit.tsx",
197
+ "**/*.Util.tsx",
198
+ "**/*.View.tsx",
199
+ "**/*.Zone.tsx"
200
+ ],
201
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-deep-internal-import.grit"]
202
+ },
203
+ {
204
+ "includes": [
205
+ "**/page/**/*.ts",
206
+ "**/page/**/*.tsx",
207
+ "**/index.ts",
208
+ "**/index.tsx",
209
+ "**/cnst.ts",
210
+ "**/db.ts",
211
+ "**/dict.ts",
212
+ "**/option.ts",
213
+ "**/sig.ts",
214
+ "**/srv.ts",
215
+ "**/st.ts",
216
+ "**/*.constant.ts",
217
+ "**/*.dictionary.ts",
218
+ "**/*.document.ts",
219
+ "**/*.service.ts",
220
+ "**/*.signal.ts",
221
+ "**/*.signal.test.ts",
222
+ "**/*.service.test.ts",
223
+ "**/*.store.ts",
224
+ "**/*.Template.tsx",
225
+ "**/*.Unit.tsx",
226
+ "**/*.Util.tsx",
227
+ "**/*.View.tsx",
228
+ "**/*.Zone.tsx"
229
+ ],
230
+ "plugins": [
231
+ "./node_modules/@akanjs/devkit/lint/no-import-external-library.grit",
232
+ "./node_modules/@akanjs/devkit/lint/no-deep-internal-import.grit"
233
+ ]
234
+ },
235
+ {
236
+ "includes": [
237
+ "apps/**/*.tsx",
238
+ "libs/**/*.tsx",
239
+ "apps/**/ui/**/*.ts",
240
+ "libs/**/ui/**/*.ts",
241
+ "apps/**/webkit/**/*.ts",
242
+ "libs/**/webkit/**/*.ts",
243
+ "apps/**/page/**/*.ts",
244
+ "libs/**/page/**/*.ts",
245
+ "apps/**/common/**/*.ts",
246
+ "libs/**/common/**/*.ts",
247
+ "apps/**/*.store.ts",
248
+ "libs/**/*.store.ts",
249
+ "apps/**/*.constant.ts",
250
+ "libs/**/*.constant.ts",
251
+ "!**/*.test.ts",
252
+ "!**/*.test.tsx",
253
+ "!**/*.spec.ts",
254
+ "!**/*.spec.tsx"
255
+ ],
256
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-import-server-in-client.grit"]
257
+ },
258
+ {
259
+ "includes": [
260
+ "apps/**/*.document.ts",
261
+ "libs/**/*.document.ts",
262
+ "apps/**/*.dictionary.ts",
263
+ "libs/**/*.dictionary.ts",
264
+ "apps/**/*.service.ts",
265
+ "libs/**/*.service.ts",
266
+ "apps/**/*.signal.ts",
267
+ "libs/**/*.signal.ts",
268
+ "apps/**/srvkit/**/*.ts",
269
+ "libs/**/srvkit/**/*.ts",
270
+ "apps/**/common/**/*.ts",
271
+ "libs/**/common/**/*.ts",
272
+ "apps/**/*.constant.ts",
273
+ "libs/**/*.constant.ts",
274
+ "!**/*.test.ts",
275
+ "!**/*.test.tsx",
276
+ "!**/*.spec.ts",
277
+ "!**/*.spec.tsx"
278
+ ],
279
+ "plugins": ["./node_modules/@akanjs/devkit/lint/no-import-client-in-server.grit"]
280
+ },
281
+ {
282
+ "includes": [
283
+ "apps/**/*.ts",
284
+ "apps/**/*.tsx",
285
+ "libs/**/*.ts",
286
+ "libs/**/*.tsx",
287
+ "!**/*.test.ts",
288
+ "!**/*.test.tsx",
289
+ "!**/*.spec.ts",
290
+ "!**/*.spec.tsx"
291
+ ],
292
+ "plugins": [
293
+ "./node_modules/@akanjs/devkit/lint/no-raw-palette-class.grit",
294
+ "./node_modules/@akanjs/devkit/lint/no-arbitrary-color.grit",
295
+ "./node_modules/@akanjs/devkit/lint/no-daisyui-legacy-class.grit",
296
+ "./node_modules/@akanjs/devkit/lint/no-inline-color.grit",
297
+ "./node_modules/@akanjs/devkit/lint/no-interpolated-arbitrary-class.grit"
298
+ ]
299
+ }
300
+ ]
301
+ }
package/biomeBase.ts ADDED
@@ -0,0 +1,9 @@
1
+ /** `extends` target for a workspace `biome.json`; Biome resolves it through node_modules. */
2
+ export const biomeBaseConfig = "@akanjs/devkit/biome.base.json";
3
+
4
+ // Biome moves rules between groups across minors — `noUnnecessaryConditions` sat in `nursery` at 2.4 and moved to
5
+ // `suspicious` at 2.5 — and the stale position is a hard "unknown key" error, not a warning. A workspace whose
6
+ // Biome disagrees with the shipped base config therefore fails to load it at all, which is why the version is
7
+ // pinned here instead of resolved to latest at create time. Bump this and `biome.base.json` in one commit, and run
8
+ // `biome migrate --write` in the workspace root and in `pkgs/@akanjs/devkit` so both configs move together.
9
+ export const biomeVersion = "2.5.8";
@@ -11,6 +11,7 @@ import {
11
11
  clearRootCapacitorConfigs,
12
12
  formatAndroidReleaseSigningError,
13
13
  getAdbDeviceStateIssues,
14
+ getAndroidLocalServerHost,
14
15
  getMissingAndroidReleaseSigningKeys,
15
16
  isPlaceholderAppId,
16
17
  materializeCapacitorConfig,
@@ -339,6 +340,13 @@ describe("Android signing diagnostics", () => {
339
340
  "Android device abc123 is unauthorized. Confirm USB debugging authorization on the device.",
340
341
  "Android device xyz is offline. Reconnect the device or restart adb.",
341
342
  ]);
343
+ expect(getAndroidLocalServerHost("List of devices attached\nemulator-5554 device\n", "192.168.0.5")).toBe(
344
+ "10.0.2.2",
345
+ );
346
+ expect(getAndroidLocalServerHost("List of devices attached\nabc123 device\n", "192.168.0.5")).toBe("192.168.0.5");
347
+ expect(
348
+ getAndroidLocalServerHost("List of devices attached\nemulator-5554 device\nabc123 device\n", "192.168.0.5"),
349
+ ).toBe("192.168.0.5");
342
350
  });
343
351
  });
344
352