@akanjs/devkit 2.3.9-rc.0 → 2.3.9-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.test.ts +4 -12
- package/akanContext.ts +225 -12
- package/capacitorApp.test.ts +14 -8
- package/commandDecorators/command.ts +2 -1
- package/commandDecorators/commandDecorators.test.ts +13 -0
- package/executors.test.ts +21 -0
- package/executors.ts +5 -4
- package/index.ts +1 -0
- package/linter.ts +27 -74
- package/package.json +2 -2
- package/typecheck/typecheck.proc.ts +1 -2
- package/workflow/index.ts +1162 -0
package/aiEditor.test.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
parseTypescriptFileBlocks,
|
|
4
|
-
preserveTypescriptResponseContent,
|
|
5
|
-
} from "./aiEditor";
|
|
2
|
+
import { parseTypescriptFileBlocks, preserveTypescriptResponseContent } from "./aiEditor";
|
|
6
3
|
|
|
7
4
|
describe("parseTypescriptFileBlocks", () => {
|
|
8
5
|
test("parses TypeScript file blocks with common fence variants", () => {
|
|
@@ -39,12 +36,9 @@ export const CarUnit = () => null;
|
|
|
39
36
|
export const car = "car";
|
|
40
37
|
\`\`\`
|
|
41
38
|
`;
|
|
42
|
-
const nextContent =
|
|
43
|
-
"The generated file meets all specified requirements. No rewrite is necessary.";
|
|
39
|
+
const nextContent = "The generated file meets all specified requirements. No rewrite is necessary.";
|
|
44
40
|
|
|
45
|
-
expect(
|
|
46
|
-
preserveTypescriptResponseContent(previousContent, nextContent),
|
|
47
|
-
).toBe(previousContent);
|
|
41
|
+
expect(preserveTypescriptResponseContent(previousContent, nextContent)).toBe(previousContent);
|
|
48
42
|
});
|
|
49
43
|
|
|
50
44
|
test("uses next code response when validation rewrites with parseable files", () => {
|
|
@@ -61,8 +55,6 @@ export const car = "updated";
|
|
|
61
55
|
\`\`\`
|
|
62
56
|
`;
|
|
63
57
|
|
|
64
|
-
expect(
|
|
65
|
-
preserveTypescriptResponseContent(previousContent, nextContent),
|
|
66
|
-
).toBe(nextContent);
|
|
58
|
+
expect(preserveTypescriptResponseContent(previousContent, nextContent)).toBe(nextContent);
|
|
67
59
|
});
|
|
68
60
|
});
|
package/akanContext.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { capitalize } from "akanjs/common";
|
|
|
4
4
|
import { AppExecutor, LibExecutor, type SysExecutor, type WorkspaceExecutor } from "./executors";
|
|
5
5
|
import { FileSys } from "./fileSys";
|
|
6
6
|
import type { PackageJson } from "./types";
|
|
7
|
+
import type { RepairAction } from "./workflow";
|
|
7
8
|
|
|
8
9
|
export type AkanContextFormat = "json" | "markdown";
|
|
9
10
|
export type AkanModuleKind = "domain" | "service" | "scalar";
|
|
@@ -59,14 +60,94 @@ export interface AkanDiagnostic {
|
|
|
59
60
|
code: string;
|
|
60
61
|
message: string;
|
|
61
62
|
path?: string;
|
|
63
|
+
repairActions?: RepairAction[];
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
export interface AkanDoctorResult {
|
|
65
67
|
schemaVersion: 1;
|
|
68
|
+
repoName: string;
|
|
69
|
+
root: string;
|
|
66
70
|
strict: boolean;
|
|
71
|
+
status: "passed" | "failed";
|
|
67
72
|
diagnostics: AkanDiagnostic[];
|
|
73
|
+
generatedFiles: string[];
|
|
74
|
+
generatedFilesFreshness: {
|
|
75
|
+
status: "unknown";
|
|
76
|
+
message: string;
|
|
77
|
+
refreshCommand: string;
|
|
78
|
+
verifyingCommands: string[];
|
|
79
|
+
};
|
|
80
|
+
validationCommands: string[];
|
|
81
|
+
repairActions: RepairAction[];
|
|
68
82
|
}
|
|
69
83
|
|
|
84
|
+
export type JsonRpcRequest = {
|
|
85
|
+
jsonrpc?: "2.0";
|
|
86
|
+
id?: string | number | null;
|
|
87
|
+
method: string;
|
|
88
|
+
params?: Record<string, unknown>;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type McpFraming = "content-length" | "newline";
|
|
92
|
+
export type AkanMcpMode = "readonly" | "plan" | "apply";
|
|
93
|
+
|
|
94
|
+
export type CursorMcpConfig = {
|
|
95
|
+
mcpServers?: Record<string, unknown>;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const resourceList = [
|
|
99
|
+
{ uri: "akan://docs/framework", name: "Akan framework guide", mimeType: "text/markdown" },
|
|
100
|
+
{ uri: "akan://guidelines/framework", name: "Framework guideline", mimeType: "text/markdown" },
|
|
101
|
+
{ uri: "akan://guidelines/modelSignal", name: "Model signal guideline", mimeType: "text/markdown" },
|
|
102
|
+
{ uri: "akan://workspace/summary", name: "Workspace summary", mimeType: "application/json" },
|
|
103
|
+
{ uri: "akan://workspace/apps", name: "Workspace apps", mimeType: "application/json" },
|
|
104
|
+
{ uri: "akan://workspace/modules", name: "Workspace modules", mimeType: "application/json" },
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
export const cursorMcpConfigPath = ".cursor/mcp.json";
|
|
108
|
+
|
|
109
|
+
const cursorWorkspaceFolder = "$" + "{workspaceFolder}";
|
|
110
|
+
|
|
111
|
+
export const createAkanCursorMcpServer = (mode: AkanMcpMode = "readonly") => ({
|
|
112
|
+
type: "stdio",
|
|
113
|
+
command: "bash",
|
|
114
|
+
args: ["-lc", `cd "${cursorWorkspaceFolder}" && akan mcp --mode ${mode}`],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
export const akanCursorMcpServer = createAkanCursorMcpServer();
|
|
118
|
+
|
|
119
|
+
export const renderDoctorText = (result: AkanDoctorResult) => {
|
|
120
|
+
const lines = [`Akan doctor status: ${result.status}`];
|
|
121
|
+
if (result.diagnostics.length === 0) {
|
|
122
|
+
lines.push("", "No Akan workspace diagnostics found.");
|
|
123
|
+
} else {
|
|
124
|
+
lines.push(
|
|
125
|
+
"",
|
|
126
|
+
...result.diagnostics.map((diagnostic) =>
|
|
127
|
+
[
|
|
128
|
+
`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`,
|
|
129
|
+
diagnostic.path ? ` ${diagnostic.path}` : "",
|
|
130
|
+
]
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.join("\n"),
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
lines.push(
|
|
137
|
+
"",
|
|
138
|
+
"Generated file freshness:",
|
|
139
|
+
result.generatedFilesFreshness.message,
|
|
140
|
+
`Refresh: ${result.generatedFilesFreshness.refreshCommand}`,
|
|
141
|
+
"",
|
|
142
|
+
"Repair actions:",
|
|
143
|
+
...(result.repairActions.length ? result.repairActions.map((action) => `- ${action.command}`) : ["- none"]),
|
|
144
|
+
"",
|
|
145
|
+
"Validation commands:",
|
|
146
|
+
...result.validationCommands.map((command) => `- ${command}`),
|
|
147
|
+
);
|
|
148
|
+
return `${lines.join("\n")}\n`;
|
|
149
|
+
};
|
|
150
|
+
|
|
70
151
|
export interface AkanContextOptions {
|
|
71
152
|
app?: string | null;
|
|
72
153
|
module?: string | null;
|
|
@@ -74,17 +155,69 @@ export interface AkanContextOptions {
|
|
|
74
155
|
}
|
|
75
156
|
|
|
76
157
|
const generatedFiles = [
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
158
|
+
"apps/*/client.ts",
|
|
159
|
+
"apps/*/server.ts",
|
|
160
|
+
"*/lib/cnst.ts",
|
|
161
|
+
"*/lib/db.ts",
|
|
162
|
+
"*/lib/dict.ts",
|
|
163
|
+
"*/lib/option.ts",
|
|
164
|
+
"*/lib/sig.ts",
|
|
165
|
+
"*/lib/srv.ts",
|
|
166
|
+
"*/lib/st.ts",
|
|
167
|
+
"*/lib/useClient.ts",
|
|
168
|
+
"*/lib/useServer.ts",
|
|
169
|
+
"*/lib/**/index.ts",
|
|
170
|
+
"*/ui/index.ts",
|
|
171
|
+
"*/webkit/index.ts",
|
|
172
|
+
"*/srvkit/index.ts",
|
|
173
|
+
"*/common/index.ts",
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const validationCommands = [
|
|
177
|
+
"akan sync <app-or-lib>",
|
|
178
|
+
"akan lint <app-or-lib-or-pkg>",
|
|
179
|
+
"akan typecheck <app-name>",
|
|
180
|
+
"akan test <app-or-lib-or-pkg>",
|
|
181
|
+
"akan build <app-name>",
|
|
182
|
+
"akan doctor --strict --format json",
|
|
86
183
|
];
|
|
87
184
|
|
|
185
|
+
const generatedFilesFreshness = {
|
|
186
|
+
status: "unknown" as const,
|
|
187
|
+
message: "Run sync before validation so generated Akan files match the current source conventions.",
|
|
188
|
+
refreshCommand: "akan sync <app-or-lib>",
|
|
189
|
+
verifyingCommands: ["akan lint <app-or-lib-or-pkg>", "akan build <app-name>"],
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const repairAction = (
|
|
193
|
+
kind: RepairAction["kind"],
|
|
194
|
+
command: string,
|
|
195
|
+
reason: string,
|
|
196
|
+
safeToRun: boolean,
|
|
197
|
+
): RepairAction => ({
|
|
198
|
+
kind,
|
|
199
|
+
command,
|
|
200
|
+
reason,
|
|
201
|
+
safeToRun,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const moduleShapeFiles = (module: AkanModuleContext) => {
|
|
205
|
+
if (module.kind === "service") {
|
|
206
|
+
return [`${module.name}.dictionary.ts`, `${module.name}.service.ts`, `${module.name}.signal.ts`];
|
|
207
|
+
}
|
|
208
|
+
if (module.kind === "scalar") return [`${module.name}.constant.ts`, `${module.name}.dictionary.ts`];
|
|
209
|
+
return [
|
|
210
|
+
`${module.name}.constant.ts`,
|
|
211
|
+
`${module.name}.dictionary.ts`,
|
|
212
|
+
`${module.name}.service.ts`,
|
|
213
|
+
`${module.name}.store.ts`,
|
|
214
|
+
`${module.name}.signal.ts`,
|
|
215
|
+
];
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const constantFieldNames = (content: string) =>
|
|
219
|
+
[...content.matchAll(/\b([A-Za-z_$][\w$]*)\s*:\s*field\(/g)].map((match) => match[1]).filter(Boolean);
|
|
220
|
+
|
|
88
221
|
const appRootAllowFiles = new Set([
|
|
89
222
|
"akan.app.json",
|
|
90
223
|
"akan.config.ts",
|
|
@@ -283,7 +416,7 @@ export class AkanContextAnalyzer {
|
|
|
283
416
|
libs,
|
|
284
417
|
pkgs,
|
|
285
418
|
generatedFiles,
|
|
286
|
-
validationCommands
|
|
419
|
+
validationCommands,
|
|
287
420
|
};
|
|
288
421
|
}
|
|
289
422
|
|
|
@@ -293,18 +426,35 @@ export class AkanContextAnalyzer {
|
|
|
293
426
|
): Promise<AkanDoctorResult> {
|
|
294
427
|
const context = await AkanContextAnalyzer.analyze(workspace);
|
|
295
428
|
const diagnostics: AkanDiagnostic[] = [];
|
|
429
|
+
const repairActions: RepairAction[] = [
|
|
430
|
+
repairAction("generated", "akan repair generated --app <app-or-lib>", "Refresh generated Akan files.", true),
|
|
431
|
+
repairAction(
|
|
432
|
+
"format",
|
|
433
|
+
"akan repair format --target <app-or-lib-or-pkg>",
|
|
434
|
+
"Run the formatter/linter repair path.",
|
|
435
|
+
true,
|
|
436
|
+
),
|
|
437
|
+
];
|
|
296
438
|
|
|
297
439
|
for (const app of context.apps) {
|
|
298
440
|
const appPath = path.join(workspace.workspaceRoot, app.path);
|
|
299
441
|
for (const entry of await safeReadDir(appPath)) {
|
|
300
442
|
const allowed = entry.isDirectory() ? appRootAllowDirs.has(entry.name) : appRootAllowFiles.has(entry.name);
|
|
301
443
|
if (!allowed) {
|
|
444
|
+
const action = repairAction(
|
|
445
|
+
"module-shape",
|
|
446
|
+
`akan repair module-shape --app ${app.name}`,
|
|
447
|
+
"Review app root shape and remove or move the unknown entry.",
|
|
448
|
+
false,
|
|
449
|
+
);
|
|
302
450
|
diagnostics.push({
|
|
303
|
-
severity: "
|
|
451
|
+
severity: "error",
|
|
304
452
|
code: "app-root-unknown-entry",
|
|
305
453
|
path: `${app.path}/${entry.name}`,
|
|
306
454
|
message: `Unexpected ${entry.isDirectory() ? "folder" : "file"} in app root: ${app.path}/${entry.name}`,
|
|
455
|
+
repairActions: [action],
|
|
307
456
|
});
|
|
457
|
+
repairActions.push(action);
|
|
308
458
|
}
|
|
309
459
|
}
|
|
310
460
|
}
|
|
@@ -312,17 +462,80 @@ export class AkanContextAnalyzer {
|
|
|
312
462
|
for (const sys of [...context.apps, ...context.libs]) {
|
|
313
463
|
for (const module of sys.modules) {
|
|
314
464
|
if (!module.abstract.exists) {
|
|
465
|
+
const action = repairAction(
|
|
466
|
+
"module-shape",
|
|
467
|
+
`akan repair module-shape --app ${sys.name} --module ${module.name}`,
|
|
468
|
+
"Create the missing module abstract or inspect required source files.",
|
|
469
|
+
false,
|
|
470
|
+
);
|
|
315
471
|
diagnostics.push({
|
|
316
472
|
severity: strict ? "error" : "warning",
|
|
317
473
|
code: "module-abstract-missing",
|
|
318
474
|
path: module.abstract.path,
|
|
319
475
|
message: `${capitalize(module.kind)} module ${sys.name}:${module.name} should include ${module.abstract.path}`,
|
|
476
|
+
repairActions: [action],
|
|
477
|
+
});
|
|
478
|
+
repairActions.push(action);
|
|
479
|
+
}
|
|
480
|
+
const missingFiles = moduleShapeFiles(module).filter((filename) => !module.files.includes(filename));
|
|
481
|
+
if (missingFiles.length) {
|
|
482
|
+
const action = repairAction(
|
|
483
|
+
"module-shape",
|
|
484
|
+
`akan repair module-shape --app ${sys.name} --module ${module.name}`,
|
|
485
|
+
"Review missing required module source files.",
|
|
486
|
+
false,
|
|
487
|
+
);
|
|
488
|
+
diagnostics.push({
|
|
489
|
+
severity: "error",
|
|
490
|
+
code: "module-shape-invalid",
|
|
491
|
+
path: module.path,
|
|
492
|
+
message: `${capitalize(module.kind)} module ${sys.name}:${module.name} is missing required files: ${missingFiles.join(", ")}`,
|
|
493
|
+
repairActions: [action],
|
|
320
494
|
});
|
|
495
|
+
repairActions.push(action);
|
|
496
|
+
}
|
|
497
|
+
if (module.kind !== "service" && module.files.includes(`${module.name}.dictionary.ts`)) {
|
|
498
|
+
const constantPath = path.join(workspace.workspaceRoot, module.path, `${module.name}.constant.ts`);
|
|
499
|
+
const dictionaryPath = path.join(workspace.workspaceRoot, module.path, `${module.name}.dictionary.ts`);
|
|
500
|
+
const [constantContent, dictionaryContent] = await Promise.all([
|
|
501
|
+
safeReadText(constantPath),
|
|
502
|
+
safeReadText(dictionaryPath),
|
|
503
|
+
]);
|
|
504
|
+
if (constantContent && dictionaryContent) {
|
|
505
|
+
for (const fieldName of constantFieldNames(constantContent)) {
|
|
506
|
+
if (new RegExp(`\\b${fieldName}\\s*:`).test(dictionaryContent)) continue;
|
|
507
|
+
const action = repairAction(
|
|
508
|
+
"dictionary",
|
|
509
|
+
`akan repair dictionary --app ${sys.name} --module ${module.name}`,
|
|
510
|
+
"Add missing dictionary labels for source constant fields.",
|
|
511
|
+
false,
|
|
512
|
+
);
|
|
513
|
+
diagnostics.push({
|
|
514
|
+
severity: "warning",
|
|
515
|
+
code: "dictionary-label-missing",
|
|
516
|
+
path: `${module.path}/${module.name}.dictionary.ts`,
|
|
517
|
+
message: `Dictionary labels for ${sys.name}:${module.name}.${fieldName} were not found.`,
|
|
518
|
+
repairActions: [action],
|
|
519
|
+
});
|
|
520
|
+
repairActions.push(action);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
321
523
|
}
|
|
322
524
|
}
|
|
323
525
|
}
|
|
324
526
|
|
|
325
|
-
return {
|
|
527
|
+
return {
|
|
528
|
+
schemaVersion: 1,
|
|
529
|
+
repoName: context.repoName,
|
|
530
|
+
root: context.root,
|
|
531
|
+
strict,
|
|
532
|
+
status: diagnostics.some((diagnostic) => diagnostic.severity === "error") ? "failed" : "passed",
|
|
533
|
+
diagnostics,
|
|
534
|
+
generatedFiles: context.generatedFiles,
|
|
535
|
+
generatedFilesFreshness,
|
|
536
|
+
validationCommands: context.validationCommands,
|
|
537
|
+
repairActions,
|
|
538
|
+
};
|
|
326
539
|
}
|
|
327
540
|
|
|
328
541
|
static findModules(context: AkanWorkspaceContext, moduleName?: string | null) {
|
package/capacitorApp.test.ts
CHANGED
|
@@ -130,16 +130,14 @@ describe("materializeCapacitorConfig", () => {
|
|
|
130
130
|
describe("root capacitor config helpers", () => {
|
|
131
131
|
test("clears root configs before writing the temporary json config", async () => {
|
|
132
132
|
const root = await makeTempRoot();
|
|
133
|
-
await Promise.all(
|
|
134
|
-
rootCapacitorConfigFilenames.map((file) => writeFile(path.join(root, file), `old ${file}\n`)),
|
|
135
|
-
);
|
|
133
|
+
await Promise.all(rootCapacitorConfigFilenames.map((file) => writeFile(path.join(root, file), `old ${file}\n`)));
|
|
136
134
|
|
|
137
|
-
await writeRootCapacitorConfig(root,
|
|
135
|
+
await writeRootCapacitorConfig(root, '{\n "appId": "com.minimal.app"\n}\n');
|
|
138
136
|
|
|
139
137
|
expect(await Bun.file(path.join(root, "capacitor.config.ts")).exists()).toBe(false);
|
|
140
138
|
expect(await Bun.file(path.join(root, "capacitor.config.js")).exists()).toBe(false);
|
|
141
139
|
expect(await Bun.file(path.join(root, "capacitor.config.json")).text()).toBe(
|
|
142
|
-
|
|
140
|
+
'{\n "appId": "com.minimal.app"\n}\n',
|
|
143
141
|
);
|
|
144
142
|
|
|
145
143
|
await clearRootCapacitorConfigs(root);
|
|
@@ -186,7 +184,9 @@ describe("iOS native run helpers", () => {
|
|
|
186
184
|
},
|
|
187
185
|
}),
|
|
188
186
|
),
|
|
189
|
-
).toEqual([
|
|
187
|
+
).toEqual([
|
|
188
|
+
{ id: "11111111-2222-3333-4444-555555555555", name: "iPhone 16", kind: "simulator", state: "Shutdown" },
|
|
189
|
+
]);
|
|
190
190
|
});
|
|
191
191
|
|
|
192
192
|
test("builds xcodebuild destinations and output paths by target kind", () => {
|
|
@@ -198,7 +198,9 @@ describe("iOS native run helpers", () => {
|
|
|
198
198
|
});
|
|
199
199
|
expect(deviceCommand.xcodebuildArgs).toContain("id=device-1");
|
|
200
200
|
expect(deviceCommand.xcodebuildArgs).toContain("App QA");
|
|
201
|
-
expect(deviceCommand.appPath).toBe(
|
|
201
|
+
expect(deviceCommand.appPath).toBe(
|
|
202
|
+
"/repo/apps/minimal/ios/DerivedData/device-1/Build/Products/Debug-iphoneos/App.app",
|
|
203
|
+
);
|
|
202
204
|
|
|
203
205
|
const simulatorCommand = buildIosNativeRunCommand({
|
|
204
206
|
appRoot: "/repo/apps/minimal",
|
|
@@ -212,7 +214,11 @@ describe("iOS native run helpers", () => {
|
|
|
212
214
|
|
|
213
215
|
test("classifies iOS signing and device-state failures", () => {
|
|
214
216
|
expect(classifyIosRunFailure("There are no accounts registered with Xcode").kind).toBe("apple-account");
|
|
215
|
-
expect(
|
|
217
|
+
expect(
|
|
218
|
+
classifyIosRunFailure(
|
|
219
|
+
'Failed Registering Bundle Identifier: The app identifier "com.minimal.app" cannot be registered to your development team',
|
|
220
|
+
).kind,
|
|
221
|
+
).toBe("bundle-identifier");
|
|
216
222
|
expect(classifyIosRunFailure("No profiles for 'com.minimal.app' were found").kind).toBe("provisioning-profile");
|
|
217
223
|
expect(classifyIosRunFailure("Developer Mode is disabled on this device").kind).toBe("device-state");
|
|
218
224
|
expect(classifyIosRunFailure("arm64-apple-darwin20.0: error: unknown argument: '-index-store-path'").kind).toBe(
|
|
@@ -165,7 +165,7 @@ const assertCurrentDirectoryIsWorkspaceRoot = async () => {
|
|
|
165
165
|
);
|
|
166
166
|
};
|
|
167
167
|
|
|
168
|
-
const getInternalArgumentValue = async (
|
|
168
|
+
export const getInternalArgumentValue = async (
|
|
169
169
|
argMeta: InternalArgMeta,
|
|
170
170
|
value: string | undefined,
|
|
171
171
|
workspace: WorkspaceExecutor,
|
|
@@ -201,6 +201,7 @@ const getInternalArgumentValue = async (
|
|
|
201
201
|
}
|
|
202
202
|
} else if (sysType === "app") {
|
|
203
203
|
if (value && appNames.includes(value)) return AppExecutor.from(workspace, value);
|
|
204
|
+
if (!value && appNames.length === 1 && appNames[0]) return AppExecutor.from(workspace, appNames[0]);
|
|
204
205
|
const appName = await select<string>({ message: `Select the ${sysType} name`, choices: appNames });
|
|
205
206
|
return AppExecutor.from(workspace, appName);
|
|
206
207
|
} else if (sysType === "lib") {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { AppExecutor, WorkspaceExecutor } from "../executors";
|
|
2
3
|
import { App, getArgMetas, Lib, Module, Pkg, Sys, Workspace } from "./argMeta";
|
|
4
|
+
import { getInternalArgumentValue } from "./command";
|
|
3
5
|
import { command } from "./commandBuilder";
|
|
4
6
|
import {
|
|
5
7
|
assertUniqueDependencies,
|
|
@@ -127,6 +129,17 @@ describe("command helper metadata", () => {
|
|
|
127
129
|
expect(commandHelp).toContain("[default: true]");
|
|
128
130
|
expect(commandHelp).toContain("Fast, Full");
|
|
129
131
|
});
|
|
132
|
+
|
|
133
|
+
test("resolves the only app without prompting for selection", async () => {
|
|
134
|
+
const workspace = new WorkspaceExecutor({ workspaceRoot: "/workspace", repoName: "repo" });
|
|
135
|
+
workspace.getExecs = async () => [["single-command-test-app"], [], []];
|
|
136
|
+
|
|
137
|
+
const app = await getInternalArgumentValue({ key: "", idx: 0, type: "App" }, undefined, workspace);
|
|
138
|
+
|
|
139
|
+
expect(app).toBeInstanceOf(AppExecutor);
|
|
140
|
+
expect(app.name).toBe("single-command-test-app");
|
|
141
|
+
expect(app.workspace).toBe(workspace);
|
|
142
|
+
});
|
|
130
143
|
});
|
|
131
144
|
|
|
132
145
|
describe("command helper dependency injection", () => {
|
package/executors.test.ts
CHANGED
|
@@ -143,11 +143,32 @@ describe("Executor filesystem helpers", () => {
|
|
|
143
143
|
"Akan.js Workspace Rules",
|
|
144
144
|
);
|
|
145
145
|
expect(await readFile(path.join(root, "workspace/AGENTS.md"), "utf8")).toContain("sample Agent Guide");
|
|
146
|
+
expect(await readFile(path.join(root, "workspace/docs/AI-DEVELOPMENT.md"), "utf8")).toContain(
|
|
147
|
+
"AI Development Guide",
|
|
148
|
+
);
|
|
149
|
+
expect(await readFile(path.join(root, "workspace/docs/GENERATED.md"), "utf8")).toContain("Generated Akan Files");
|
|
146
150
|
expect(await readFile(path.join(root, "workspace/biome.json"), "utf8")).toContain(
|
|
147
151
|
"./node_modules/@akanjs/devkit/lint/no-import-client-functions.grit",
|
|
148
152
|
);
|
|
149
153
|
});
|
|
150
154
|
|
|
155
|
+
test("applies app sample signal test helpers", async () => {
|
|
156
|
+
const root = await makeTempRoot();
|
|
157
|
+
const exec = new Executor("fixture", root);
|
|
158
|
+
|
|
159
|
+
await exec.applyTemplate({
|
|
160
|
+
basePath: "app",
|
|
161
|
+
template: "appSample",
|
|
162
|
+
dict: { appName: "demo" },
|
|
163
|
+
options: { libs: [] },
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await expect(readFile(path.join(root, "app/lib/task/task.service.test.ts"), "utf8")).rejects.toThrow();
|
|
167
|
+
expect(await readFile(path.join(root, "app/lib/task/task.signal.spec.ts"), "utf8")).toContain("getCompletedTask");
|
|
168
|
+
expect(await readFile(path.join(root, "app/lib/task/task.signal.test.ts"), "utf8")).toContain("Task signal smoke");
|
|
169
|
+
expect(await readFile(path.join(root, "app/lib/task/task.document.ts"), "utf8")).toContain('action: "started"');
|
|
170
|
+
});
|
|
171
|
+
|
|
151
172
|
test("copies static files from CLI templates", async () => {
|
|
152
173
|
const root = await makeTempRoot();
|
|
153
174
|
const exec = new Executor("fixture", root);
|
package/executors.ts
CHANGED
|
@@ -503,7 +503,7 @@ export class Executor {
|
|
|
503
503
|
async writeFile(
|
|
504
504
|
filePath: string,
|
|
505
505
|
content: string | object,
|
|
506
|
-
{ overwrite = true }: { overwrite?: boolean } = {},
|
|
506
|
+
{ overwrite = true, silent = false }: { overwrite?: boolean; silent?: boolean } = {},
|
|
507
507
|
): Promise<FileContent> {
|
|
508
508
|
const writePath = this.getPath(filePath);
|
|
509
509
|
const dir = path.dirname(writePath);
|
|
@@ -521,7 +521,7 @@ export class Executor {
|
|
|
521
521
|
}
|
|
522
522
|
} else {
|
|
523
523
|
await FileSys.writeText(writePath, contentStr);
|
|
524
|
-
this.logger.rawLog(chalk.green(`File Create: ${filePath}`));
|
|
524
|
+
if (!silent) this.logger.rawLog(chalk.green(`File Create: ${filePath}`));
|
|
525
525
|
}
|
|
526
526
|
return { filePath: writePath, content: contentStr };
|
|
527
527
|
}
|
|
@@ -1357,14 +1357,15 @@ export class AppExecutor extends SysExecutor {
|
|
|
1357
1357
|
getCommandEnv(env: Record<string, string> = {}): Record<string, string> {
|
|
1358
1358
|
const basePort = 8282;
|
|
1359
1359
|
const portOffset = WorkspaceExecutor.getBaseDevEnv().portOffset;
|
|
1360
|
-
const PORT =
|
|
1360
|
+
const PORT = (basePort + portOffset).toString();
|
|
1361
1361
|
const AKAN_PUBLIC_SERVER_PORT = portOffset ? (8282 + portOffset).toString() : undefined;
|
|
1362
1362
|
return {
|
|
1363
1363
|
...process.env,
|
|
1364
1364
|
AKAN_PUBLIC_APP_NAME: this.name,
|
|
1365
1365
|
AKAN_WORKSPACE_ROOT: this.workspace.workspaceRoot,
|
|
1366
1366
|
NODE_NO_WARNINGS: "1",
|
|
1367
|
-
|
|
1367
|
+
PORT,
|
|
1368
|
+
AKAN_PUBLIC_CLIENT_PORT: PORT,
|
|
1368
1369
|
...(AKAN_PUBLIC_SERVER_PORT ? { AKAN_PUBLIC_SERVER_PORT } : {}),
|
|
1369
1370
|
...env,
|
|
1370
1371
|
};
|
package/index.ts
CHANGED