@akanjs/cli 2.3.11-rc.0 → 2.3.11-rc.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/incrementalBuilder.proc.js +678 -57
- package/index.js +1073 -100
- package/package.json +2 -2
- package/templates/appSample/lib/task/Task.Zone.tsx +1 -3
- package/templates/appSample/lib/task/task.service.ts +1 -1
- package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -2
- package/templates/crudPages/[__model__Id]/page.tsx +3 -3
- package/templates/crudPages/page.tsx +2 -2
- package/templates/crudSinglePage/page.tsx +3 -3
- package/templates/module/__Model__.Template.tsx +4 -5
- package/templates/module/__Model__.Unit.tsx +1 -1
- package/templates/module/__Model__.View.tsx +1 -1
- package/templates/module/__Model__.Zone.tsx +5 -3
- package/templates/module/__model__.constant.ts +2 -1
- package/templates/module/__model__.dictionary.ts +3 -1
- package/templates/workspaceRoot/.cursor/rules/akan.mdc.template +4 -33
- package/templates/workspaceRoot/.cursor/rules/application-test-commands.mdc.template +1 -1
- package/templates/workspaceRoot/.cursor/rules/typescript-imports.mdc.template +1 -1
- package/templates/workspaceRoot/.gitignore.template +4 -1
- package/templates/workspaceRoot/AGENTS.md.template +143 -8
- package/templates/workspaceRoot/biome.json.template +20 -1
- package/templates/workspaceRoot/docs/GENERATED.md.template +0 -1
- package/templates/workspaceRoot/package.json.template +3 -3
package/index.js
CHANGED
|
@@ -664,6 +664,7 @@ import {
|
|
|
664
664
|
import { readFileSync as readFileSync3 } from "fs";
|
|
665
665
|
import { copyFile, mkdir as mkdir2, readdir as readDirEntries, stat as stat2 } from "fs/promises";
|
|
666
666
|
import path7 from "path";
|
|
667
|
+
import { pathToFileURL } from "url";
|
|
667
668
|
import {
|
|
668
669
|
capitalize,
|
|
669
670
|
isRouteSourceFile,
|
|
@@ -1105,6 +1106,63 @@ var decreaseBuildNum = async (app) => {
|
|
|
1105
1106
|
const akanConfigContent = akanConfig.replace(`buildNum: ${appConfig.mobile.buildNum}`, `buildNum: ${appConfig.mobile.buildNum - 1}`);
|
|
1106
1107
|
fs.writeFileSync(akanConfigPath, akanConfigContent);
|
|
1107
1108
|
};
|
|
1109
|
+
// pkgs/@akanjs/devkit/firebaseMessagingSw.ts
|
|
1110
|
+
var FIREBASE_WEB_SDK_VERSION = "12.13.0";
|
|
1111
|
+
function normalizeFirebaseClientConfig(config) {
|
|
1112
|
+
if (!config || typeof config !== "object")
|
|
1113
|
+
return null;
|
|
1114
|
+
const value = config;
|
|
1115
|
+
if (typeof value.apiKey !== "string" || typeof value.projectId !== "string" || typeof value.messagingSenderId !== "string" || typeof value.appId !== "string") {
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
return {
|
|
1119
|
+
apiKey: value.apiKey,
|
|
1120
|
+
...typeof value.authDomain === "string" ? { authDomain: value.authDomain } : {},
|
|
1121
|
+
projectId: value.projectId,
|
|
1122
|
+
...typeof value.storageBucket === "string" ? { storageBucket: value.storageBucket } : {},
|
|
1123
|
+
messagingSenderId: value.messagingSenderId,
|
|
1124
|
+
appId: value.appId
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function createFirebaseMessagingServiceWorker(config) {
|
|
1128
|
+
const configJson = JSON.stringify(config);
|
|
1129
|
+
return `/* Generated by Akan.js. Do not edit. */
|
|
1130
|
+
const firebaseConfig = ${configJson};
|
|
1131
|
+
|
|
1132
|
+
if (firebaseConfig) {
|
|
1133
|
+
importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-app-compat.js");
|
|
1134
|
+
importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-messaging-compat.js");
|
|
1135
|
+
|
|
1136
|
+
firebase.initializeApp(firebaseConfig);
|
|
1137
|
+
const messaging = firebase.messaging();
|
|
1138
|
+
|
|
1139
|
+
const notificationUrl = (payload) =>
|
|
1140
|
+
payload?.data?.url || payload?.fcmOptions?.link || payload?.notification?.click_action;
|
|
1141
|
+
|
|
1142
|
+
messaging.onBackgroundMessage((payload) => {
|
|
1143
|
+
const title = payload?.notification?.title || "";
|
|
1144
|
+
const options = {
|
|
1145
|
+
body: payload?.notification?.body,
|
|
1146
|
+
icon: payload?.notification?.icon,
|
|
1147
|
+
image: payload?.notification?.image,
|
|
1148
|
+
data: {
|
|
1149
|
+
url: notificationUrl(payload),
|
|
1150
|
+
FCM_MSG: payload,
|
|
1151
|
+
},
|
|
1152
|
+
};
|
|
1153
|
+
self.registration.showNotification(title, options);
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
self.addEventListener("notificationclick", (event) => {
|
|
1158
|
+
const url = event.notification?.data?.url || event.notification?.data?.FCM_MSG?.data?.url;
|
|
1159
|
+
event.notification?.close();
|
|
1160
|
+
if (!url) return;
|
|
1161
|
+
event.waitUntil(clients.openWindow(url));
|
|
1162
|
+
});
|
|
1163
|
+
`;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1108
1166
|
// pkgs/@akanjs/devkit/getDirname.ts
|
|
1109
1167
|
import path2 from "path";
|
|
1110
1168
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -1390,6 +1448,7 @@ ${errorText}, ${warningText} found`];
|
|
|
1390
1448
|
|
|
1391
1449
|
// pkgs/@akanjs/devkit/scanInfo.ts
|
|
1392
1450
|
import path5 from "path";
|
|
1451
|
+
import { rm } from "fs/promises";
|
|
1393
1452
|
|
|
1394
1453
|
// pkgs/@akanjs/devkit/dependencyScanner.ts
|
|
1395
1454
|
import { builtinModules } from "module";
|
|
@@ -1708,8 +1767,10 @@ var appRootAllowedFiles = new Set([
|
|
|
1708
1767
|
"main.ts",
|
|
1709
1768
|
"package.json",
|
|
1710
1769
|
"server.ts",
|
|
1711
|
-
"tsconfig.json"
|
|
1770
|
+
"tsconfig.json",
|
|
1771
|
+
"tsconfig.tsbuildinfo"
|
|
1712
1772
|
]);
|
|
1773
|
+
var generatedRootCapacitorConfigFiles = ["capacitor.config.js", "capacitor.config.json"];
|
|
1713
1774
|
var appRootAllowedDirs = new Set([
|
|
1714
1775
|
".akan",
|
|
1715
1776
|
"android",
|
|
@@ -1753,11 +1814,17 @@ var rootSignalTestFilePattern = /^[A-Za-z][A-Za-z0-9_-]*\.signal\.(test|spec)\.(
|
|
|
1753
1814
|
var isAllowedTestFile = (filename) => testFilePattern.test(filename);
|
|
1754
1815
|
var isAllowedLibRootFile = (filename) => libRootAllowedFiles.has(filename) || rootSignalTestFilePattern.test(filename);
|
|
1755
1816
|
var getScanPath = (exec, relativePath) => path5.posix.join(`${exec.type}s`, exec.name, relativePath.split(path5.sep).join("/"));
|
|
1817
|
+
async function clearGeneratedRootCapacitorConfigs(exec) {
|
|
1818
|
+
if (exec.type !== "app")
|
|
1819
|
+
return;
|
|
1820
|
+
await Promise.all(generatedRootCapacitorConfigFiles.map((filename) => rm(exec.getPath(filename), { force: true })));
|
|
1821
|
+
}
|
|
1756
1822
|
var getModuleNameFromPath = (kind, modulePath) => {
|
|
1757
1823
|
const dirname = path5.basename(modulePath);
|
|
1758
1824
|
return kind === "service" ? dirname.replace(/^_+/, "") : dirname;
|
|
1759
1825
|
};
|
|
1760
1826
|
async function assertScanConvention(exec, libRoot) {
|
|
1827
|
+
await clearGeneratedRootCapacitorConfigs(exec);
|
|
1761
1828
|
const violations = [];
|
|
1762
1829
|
const addViolation = (relativePath, reason) => {
|
|
1763
1830
|
violations.push(`${getScanPath(exec, relativePath)}: ${reason}`);
|
|
@@ -2622,6 +2689,31 @@ function validateRouteSourceExports(source, filePath, kind, options = {}) {
|
|
|
2622
2689
|
throw new Error(`[route-convention] metadata and generateMetadata cannot both be exported in ${filePath}`);
|
|
2623
2690
|
}
|
|
2624
2691
|
}
|
|
2692
|
+
function validateOverridesSourceExports(source, filePath) {
|
|
2693
|
+
const sourceFile = ts3.createSourceFile(filePath, source, ts3.ScriptTarget.Latest, true, ts3.ScriptKind.TSX);
|
|
2694
|
+
const fail = (message) => {
|
|
2695
|
+
throw new Error(`[route-convention] ${message}: ${filePath}`);
|
|
2696
|
+
};
|
|
2697
|
+
let defaultOverride = null;
|
|
2698
|
+
for (const statement of sourceFile.statements) {
|
|
2699
|
+
if (ts3.isExpressionStatement(statement) && ts3.isStringLiteral(statement.expression))
|
|
2700
|
+
continue;
|
|
2701
|
+
if (ts3.isImportDeclaration(statement))
|
|
2702
|
+
continue;
|
|
2703
|
+
if (ts3.isInterfaceDeclaration(statement) || ts3.isTypeAliasDeclaration(statement))
|
|
2704
|
+
continue;
|
|
2705
|
+
if (ts3.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
2706
|
+
defaultOverride = statement;
|
|
2707
|
+
continue;
|
|
2708
|
+
}
|
|
2709
|
+
fail(`_overrides.tsx may only contain imports and a single "export default override({ ... })"`);
|
|
2710
|
+
}
|
|
2711
|
+
if (!defaultOverride)
|
|
2712
|
+
fail(`_overrides.tsx must "export default override({ ... })"`);
|
|
2713
|
+
const expression = defaultOverride.expression;
|
|
2714
|
+
if (!ts3.isCallExpression(expression) || !ts3.isIdentifier(expression.expression) || expression.expression.text !== "override")
|
|
2715
|
+
fail(`_overrides.tsx default export must be a call to "override", e.g. "export default override({ Modal: BrandModal })"`);
|
|
2716
|
+
}
|
|
2625
2717
|
|
|
2626
2718
|
class Executor {
|
|
2627
2719
|
static verbose = false;
|
|
@@ -3574,6 +3666,8 @@ class AppExecutor extends SysExecutor {
|
|
|
3574
3666
|
...routeEnv,
|
|
3575
3667
|
...devPort ? { PORT: devPort, AKAN_PUBLIC_CLIENT_PORT: devPort, AKAN_PUBLIC_SERVER_PORT: devPort } : {}
|
|
3576
3668
|
});
|
|
3669
|
+
if (type === "build")
|
|
3670
|
+
Object.assign(process.env, env);
|
|
3577
3671
|
return { env };
|
|
3578
3672
|
}
|
|
3579
3673
|
#publicEnv = null;
|
|
@@ -3643,7 +3737,11 @@ class AppExecutor extends SysExecutor {
|
|
|
3643
3737
|
throw new Error(`[route-convention] __root_layout is reserved for Akan.js generated root layout: ${absPath}`);
|
|
3644
3738
|
}
|
|
3645
3739
|
const isRootLayout = parsed.kind === "layout" && parsed.moduleSegments.at(-1) === "_layout";
|
|
3646
|
-
|
|
3740
|
+
const routeSource = await Bun.file(absPath).text();
|
|
3741
|
+
if (parsed.kind === "overrides")
|
|
3742
|
+
validateOverridesSourceExports(routeSource, absPath);
|
|
3743
|
+
else
|
|
3744
|
+
validateRouteSourceExports(routeSource, absPath, parsed.kind, { rootLayout: isRootLayout });
|
|
3647
3745
|
pageKeys.push(key);
|
|
3648
3746
|
}
|
|
3649
3747
|
pageKeys.sort();
|
|
@@ -3684,8 +3782,30 @@ class AppExecutor extends SysExecutor {
|
|
|
3684
3782
|
});
|
|
3685
3783
|
if (write)
|
|
3686
3784
|
await this.syncAssets(scanInfo.getScanResult().libDeps);
|
|
3785
|
+
if (write)
|
|
3786
|
+
await this.#syncFirebaseMessagingSw();
|
|
3687
3787
|
return scanInfo;
|
|
3688
3788
|
}
|
|
3789
|
+
async#syncFirebaseMessagingSw() {
|
|
3790
|
+
const swRelPath = "public/firebase-messaging-sw.js";
|
|
3791
|
+
if (await FileSys.fileExists(this.getPath(swRelPath)))
|
|
3792
|
+
return;
|
|
3793
|
+
const envClientPath = path7.join(this.cwdPath, "env", "env.client.ts");
|
|
3794
|
+
if (!await FileSys.fileExists(envClientPath))
|
|
3795
|
+
return;
|
|
3796
|
+
let firebaseConfig = null;
|
|
3797
|
+
try {
|
|
3798
|
+
const envUrl = pathToFileURL(envClientPath);
|
|
3799
|
+
envUrl.searchParams.set("t", String(Date.now()));
|
|
3800
|
+
const envModule = await import(envUrl.href);
|
|
3801
|
+
firebaseConfig = normalizeFirebaseClientConfig(envModule.env?.firebase);
|
|
3802
|
+
} catch {
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3805
|
+
if (!firebaseConfig)
|
|
3806
|
+
return;
|
|
3807
|
+
await this.writeFile(swRelPath, createFirebaseMessagingServiceWorker(firebaseConfig), { overwrite: false });
|
|
3808
|
+
}
|
|
3689
3809
|
async increaseBuildNum() {
|
|
3690
3810
|
await increaseBuildNum(this);
|
|
3691
3811
|
}
|
|
@@ -4168,6 +4288,7 @@ var BACKEND_RESTART_DEBOUNCE_MS = 120;
|
|
|
4168
4288
|
var BACKEND_GRACEFUL_TIMEOUT_MS = 3000;
|
|
4169
4289
|
var BACKEND_RECOVERY_BASE_DELAY_MS = 1000;
|
|
4170
4290
|
var BACKEND_RECOVERY_MAX_DELAY_MS = 30000;
|
|
4291
|
+
var BACKEND_STDERR_TAIL_LIMIT = 40;
|
|
4171
4292
|
var BUILDER_READY_TIMEOUT_MS = 150000;
|
|
4172
4293
|
var BUILDER_START_MAX_ATTEMPTS = 3;
|
|
4173
4294
|
var SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
@@ -4370,6 +4491,7 @@ class AkanAppHost {
|
|
|
4370
4491
|
#pendingRestartReason = null;
|
|
4371
4492
|
#backendStartStatus = null;
|
|
4372
4493
|
#backendBuildStatusGeneration = 0;
|
|
4494
|
+
#backendStderrTail = [];
|
|
4373
4495
|
#lastGoodFrontend = {};
|
|
4374
4496
|
#buildStatusByPhase = new Map;
|
|
4375
4497
|
#pendingBuildStatusReplay = [];
|
|
@@ -4421,6 +4543,7 @@ class AkanAppHost {
|
|
|
4421
4543
|
this.#backendStartStatus = startStatus;
|
|
4422
4544
|
this.#setBackendLifecycleState("starting");
|
|
4423
4545
|
this.#backendReady = false;
|
|
4546
|
+
this.#backendStderrTail = [];
|
|
4424
4547
|
const backend = Bun.spawn(["bun", `apps/${this.app.name}/main.ts`], {
|
|
4425
4548
|
cwd: this.app.workspace.workspaceRoot,
|
|
4426
4549
|
stdio: this.withInk ? ["ignore", "pipe", "pipe"] : ["inherit", "inherit", "inherit"],
|
|
@@ -4454,6 +4577,37 @@ class AkanAppHost {
|
|
|
4454
4577
|
});
|
|
4455
4578
|
this.#backend = backend;
|
|
4456
4579
|
this.logger.verbose(`backend spawned pid=${backend.pid}`);
|
|
4580
|
+
if (this.withInk) {
|
|
4581
|
+
this.#forwardBackendStream(backend.stderr, "stderr");
|
|
4582
|
+
this.#forwardBackendStream(backend.stdout, "stdout");
|
|
4583
|
+
}
|
|
4584
|
+
}
|
|
4585
|
+
#recordBackendStderr(chunk) {
|
|
4586
|
+
const lines = chunk.split(/\r?\n/).filter((line) => line.length > 0);
|
|
4587
|
+
if (lines.length === 0)
|
|
4588
|
+
return;
|
|
4589
|
+
this.#backendStderrTail.push(...lines);
|
|
4590
|
+
if (this.#backendStderrTail.length > BACKEND_STDERR_TAIL_LIMIT) {
|
|
4591
|
+
this.#backendStderrTail.splice(0, this.#backendStderrTail.length - BACKEND_STDERR_TAIL_LIMIT);
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
async#forwardBackendStream(stream, kind) {
|
|
4595
|
+
if (!stream)
|
|
4596
|
+
return;
|
|
4597
|
+
const decoder = new TextDecoder;
|
|
4598
|
+
try {
|
|
4599
|
+
for await (const chunk of stream) {
|
|
4600
|
+
const text = decoder.decode(chunk, { stream: true });
|
|
4601
|
+
if (!text.trim())
|
|
4602
|
+
continue;
|
|
4603
|
+
if (kind === "stderr") {
|
|
4604
|
+
this.#recordBackendStderr(text);
|
|
4605
|
+
this.logger.warn(`[backend] ${text.trimEnd()}`);
|
|
4606
|
+
} else {
|
|
4607
|
+
this.logger.verbose(`[backend] ${text.trimEnd()}`);
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
} catch {}
|
|
4457
4611
|
}
|
|
4458
4612
|
#nextBackendBuildStatusGeneration(generation) {
|
|
4459
4613
|
if (typeof generation === "number") {
|
|
@@ -4578,6 +4732,11 @@ class AkanAppHost {
|
|
|
4578
4732
|
});
|
|
4579
4733
|
this.#sendOrQueueBuildStatus(failureStatus);
|
|
4580
4734
|
this.logger.warn(`[backend-recovery] backend exited unexpectedly (${reason}); restarting in ${delay}ms (attempt ${this.#backendRecoveryAttempts})`);
|
|
4735
|
+
if (this.#backendStderrTail.length > 0) {
|
|
4736
|
+
this.logger.warn(`[backend-recovery] recent backend stderr:
|
|
4737
|
+
${this.#backendStderrTail.join(`
|
|
4738
|
+
`)}`);
|
|
4739
|
+
}
|
|
4581
4740
|
this.#backendRecoveryTimer = setTimeout(() => {
|
|
4582
4741
|
this.#backendRecoveryTimer = null;
|
|
4583
4742
|
if (this.#backend)
|
|
@@ -4954,7 +5113,6 @@ var workflowSyncStatePath = (target) => `${workflowSyncDir}/${syncStateSlug(targ
|
|
|
4954
5113
|
var generatedFilePathsForTarget = (targetRoot, reason = "Generated files were refreshed by sync.") => [
|
|
4955
5114
|
{ path: `${targetRoot}/lib/cnst.ts`, action: "sync", reason },
|
|
4956
5115
|
{ path: `${targetRoot}/lib/dict.ts`, action: "sync", reason },
|
|
4957
|
-
{ path: `${targetRoot}/lib/option.ts`, action: "sync", reason },
|
|
4958
5116
|
{ path: `${targetRoot}/lib/index.ts`, action: "sync", reason }
|
|
4959
5117
|
];
|
|
4960
5118
|
var writeGeneratedSyncState = async (workspace, state) => {
|
|
@@ -5943,6 +6101,117 @@ ${values.map((value) => ` ${value}: t([${JSON.stringify(titleize(value))}, ${
|
|
|
5943
6101
|
return `${content.slice(0, insertBeforeIndex)}${enumBlock}${content.slice(insertBeforeIndex)}`;
|
|
5944
6102
|
};
|
|
5945
6103
|
var parseValues = (value) => value?.split(",").map((item) => item.trim()).filter(Boolean) ?? [];
|
|
6104
|
+
var hasSourceParseErrors = (content, fileName = "source.ts") => hasParseDiagnostics(sourceFileFor(fileName, content));
|
|
6105
|
+
var findClassDeclaration = (source, className) => {
|
|
6106
|
+
let found = null;
|
|
6107
|
+
const visit = (node) => {
|
|
6108
|
+
if (found)
|
|
6109
|
+
return;
|
|
6110
|
+
if (ts4.isClassDeclaration(node) && node.name?.text === className) {
|
|
6111
|
+
found = node;
|
|
6112
|
+
return;
|
|
6113
|
+
}
|
|
6114
|
+
ts4.forEachChild(node, visit);
|
|
6115
|
+
};
|
|
6116
|
+
ts4.forEachChild(source, visit);
|
|
6117
|
+
return found;
|
|
6118
|
+
};
|
|
6119
|
+
var firstHeritageCall = (node) => {
|
|
6120
|
+
const expression = (node.heritageClauses?.flatMap((clause) => [...clause.types]) ?? [])[0]?.expression;
|
|
6121
|
+
return expression && ts4.isCallExpression(expression) ? expression : null;
|
|
6122
|
+
};
|
|
6123
|
+
var factoryArrowOf = (call) => {
|
|
6124
|
+
const arg = call.arguments.find((argument) => ts4.isArrowFunction(argument) || ts4.isFunctionExpression(argument));
|
|
6125
|
+
return arg && (ts4.isArrowFunction(arg) || ts4.isFunctionExpression(arg)) ? arg : null;
|
|
6126
|
+
};
|
|
6127
|
+
var hasClassMethod = (content, className, methodName) => {
|
|
6128
|
+
const node = findClassDeclaration(sourceFileFor("service.ts", content), className);
|
|
6129
|
+
return Boolean(node?.members.some((member) => ts4.isMethodDeclaration(member) && nodeName(member.name) === methodName));
|
|
6130
|
+
};
|
|
6131
|
+
var insertClassMethod = (content, className, methodBlock) => {
|
|
6132
|
+
const source = sourceFileFor("service.ts", content);
|
|
6133
|
+
const node = findClassDeclaration(source, className);
|
|
6134
|
+
if (!node)
|
|
6135
|
+
return null;
|
|
6136
|
+
const closeBrace = node.getEnd() - 1;
|
|
6137
|
+
if (content[closeBrace] !== "}")
|
|
6138
|
+
return null;
|
|
6139
|
+
const lead = content.slice(0, closeBrace).endsWith(`
|
|
6140
|
+
`) ? "" : `
|
|
6141
|
+
`;
|
|
6142
|
+
return spliceText(content, closeBrace, closeBrace, `${lead}${methodBlock}
|
|
6143
|
+
`);
|
|
6144
|
+
};
|
|
6145
|
+
var arrowParamInnerRegion = (content, source, arrow) => {
|
|
6146
|
+
if (arrow.parameters.length > 0) {
|
|
6147
|
+
return {
|
|
6148
|
+
start: arrow.parameters[0].getStart(source),
|
|
6149
|
+
end: arrow.parameters[arrow.parameters.length - 1].getEnd()
|
|
6150
|
+
};
|
|
6151
|
+
}
|
|
6152
|
+
const open = content.indexOf("(", arrow.getStart(source));
|
|
6153
|
+
if (open < 0)
|
|
6154
|
+
return null;
|
|
6155
|
+
const close = content.indexOf(")", open);
|
|
6156
|
+
if (close < 0)
|
|
6157
|
+
return null;
|
|
6158
|
+
return { start: open + 1, end: close };
|
|
6159
|
+
};
|
|
6160
|
+
var factoryParamEdit = (content, source, arrow, plan) => {
|
|
6161
|
+
if (arrow.parameters.length > 1)
|
|
6162
|
+
return null;
|
|
6163
|
+
const region = arrowParamInnerRegion(content, source, arrow);
|
|
6164
|
+
if (!region)
|
|
6165
|
+
return null;
|
|
6166
|
+
if (arrow.parameters.length === 0) {
|
|
6167
|
+
return { ...region, text: plan.mode === "destructure" ? `{ ${plan.name} }` : plan.name };
|
|
6168
|
+
}
|
|
6169
|
+
const param = arrow.parameters[0];
|
|
6170
|
+
if (plan.mode === "positional")
|
|
6171
|
+
return nodeName(param.name) === plan.name ? "unchanged" : null;
|
|
6172
|
+
if (!ts4.isObjectBindingPattern(param.name))
|
|
6173
|
+
return null;
|
|
6174
|
+
const names = param.name.elements.map((element) => ts4.isIdentifier(element.name) ? element.name.text : null);
|
|
6175
|
+
if (names.some((name) => name === null))
|
|
6176
|
+
return null;
|
|
6177
|
+
if (names.includes(plan.name))
|
|
6178
|
+
return "unchanged";
|
|
6179
|
+
return {
|
|
6180
|
+
start: param.getStart(source),
|
|
6181
|
+
end: param.getEnd(),
|
|
6182
|
+
text: `{ ${[...names, plan.name].join(", ")} }`
|
|
6183
|
+
};
|
|
6184
|
+
};
|
|
6185
|
+
var factoryObjectOf = (source, className) => {
|
|
6186
|
+
const node = findClassDeclaration(source, className);
|
|
6187
|
+
const call = node ? firstHeritageCall(node) : null;
|
|
6188
|
+
const arrow = call ? factoryArrowOf(call) : null;
|
|
6189
|
+
const object = arrow ? firstObjectReturnedByArrow(arrow) : null;
|
|
6190
|
+
return arrow && object ? { arrow, object } : null;
|
|
6191
|
+
};
|
|
6192
|
+
var hasSignalFactoryEntry = (content, className, entryName) => {
|
|
6193
|
+
const located = factoryObjectOf(sourceFileFor("signal.ts", content), className);
|
|
6194
|
+
return Boolean(located?.object.properties.some((property) => propertyName(property) === entryName));
|
|
6195
|
+
};
|
|
6196
|
+
var insertSignalFactoryEntry = (content, className, entryName, entryLine, param) => {
|
|
6197
|
+
const source = sourceFileFor("signal.ts", content);
|
|
6198
|
+
const located = factoryObjectOf(source, className);
|
|
6199
|
+
if (!located)
|
|
6200
|
+
return null;
|
|
6201
|
+
const locator = locatedObject(source, located.object);
|
|
6202
|
+
if (locator.fields.some((field) => field.name === entryName))
|
|
6203
|
+
return content;
|
|
6204
|
+
const paramEdit = factoryParamEdit(content, source, located.arrow, param);
|
|
6205
|
+
if (paramEdit === null)
|
|
6206
|
+
return null;
|
|
6207
|
+
const withEntry = insertOrderedFieldLine(content, locator, entryName, entryLine, {
|
|
6208
|
+
fieldIndent: " ",
|
|
6209
|
+
closingIndent: ""
|
|
6210
|
+
});
|
|
6211
|
+
if (withEntry === content)
|
|
6212
|
+
return null;
|
|
6213
|
+
return paramEdit === "unchanged" ? withEntry : spliceText(withEntry, paramEdit.start, paramEdit.end, paramEdit.text);
|
|
6214
|
+
};
|
|
5946
6215
|
|
|
5947
6216
|
// pkgs/@akanjs/devkit/workflow/moduleIndex.ts
|
|
5948
6217
|
var indexFileKinds = [
|
|
@@ -6660,7 +6929,9 @@ var createWorkflowStepRegistry = ({
|
|
|
6660
6929
|
createScalar,
|
|
6661
6930
|
createUi,
|
|
6662
6931
|
addField,
|
|
6663
|
-
addEnumField
|
|
6932
|
+
addEnumField,
|
|
6933
|
+
addMutation,
|
|
6934
|
+
addSlice
|
|
6664
6935
|
}) => {
|
|
6665
6936
|
const inspect = async () => {
|
|
6666
6937
|
return;
|
|
@@ -6736,7 +7007,19 @@ var createWorkflowStepRegistry = ({
|
|
|
6736
7007
|
defaultValue: workflowStringInput(plan.inputs.default)
|
|
6737
7008
|
})),
|
|
6738
7009
|
[workflowStepKey("add-enum-field", "update-dictionary")]: inspect,
|
|
6739
|
-
[workflowStepKey("add-enum-field", "update-option")]: inspect
|
|
7010
|
+
[workflowStepKey("add-enum-field", "update-option")]: inspect,
|
|
7011
|
+
[workflowStepKey("add-mutation", "update-service")]: async (_step, plan) => primitiveReportToWorkflowStepResult(await addMutation({
|
|
7012
|
+
app: workflowStringInput(plan.inputs.app),
|
|
7013
|
+
module: workflowStringInput(plan.inputs.module),
|
|
7014
|
+
mutation: workflowStringInput(plan.inputs.mutation)
|
|
7015
|
+
})),
|
|
7016
|
+
[workflowStepKey("add-mutation", "update-signal")]: inspect,
|
|
7017
|
+
[workflowStepKey("add-slice", "update-service-query")]: async (_step, plan) => primitiveReportToWorkflowStepResult(await addSlice({
|
|
7018
|
+
app: workflowStringInput(plan.inputs.app),
|
|
7019
|
+
module: workflowStringInput(plan.inputs.module),
|
|
7020
|
+
slice: workflowStringInput(plan.inputs.slice)
|
|
7021
|
+
})),
|
|
7022
|
+
[workflowStepKey("add-slice", "update-signal-slice")]: inspect
|
|
6740
7023
|
};
|
|
6741
7024
|
};
|
|
6742
7025
|
class WorkflowExecutor {
|
|
@@ -7483,13 +7766,59 @@ var resourceList = [
|
|
|
7483
7766
|
{ uri: "akan://workspace/modules", name: "Workspace modules", mimeType: "application/json" }
|
|
7484
7767
|
];
|
|
7485
7768
|
var cursorMcpConfigPath = ".cursor/mcp.json";
|
|
7769
|
+
var claudeMcpConfigPath = ".mcp.json";
|
|
7770
|
+
var codexMcpConfigPath = ".codex/config.toml";
|
|
7771
|
+
var akanMcpInstallTargets = ["cursor", "claude", "codex"];
|
|
7772
|
+
var akanMcpInstallConfigPaths = {
|
|
7773
|
+
cursor: cursorMcpConfigPath,
|
|
7774
|
+
claude: claudeMcpConfigPath,
|
|
7775
|
+
codex: codexMcpConfigPath
|
|
7776
|
+
};
|
|
7486
7777
|
var cursorWorkspaceFolder = "$" + "{workspaceFolder}";
|
|
7778
|
+
var claudeProjectDir = "$CLAUDE_PROJECT_DIR";
|
|
7779
|
+
var akanMcpCommand = (mode, { cd } = {}) => cd ? `cd "${cd}" && akan mcp --mode ${mode}` : `akan mcp --mode ${mode}`;
|
|
7487
7780
|
var createAkanCursorMcpServer = (mode = "readonly") => ({
|
|
7488
7781
|
type: "stdio",
|
|
7489
7782
|
command: "bash",
|
|
7490
|
-
args: ["-lc",
|
|
7783
|
+
args: ["-lc", akanMcpCommand(mode, { cd: cursorWorkspaceFolder })]
|
|
7491
7784
|
});
|
|
7785
|
+
var createAkanClaudeMcpServer = (mode = "readonly") => ({
|
|
7786
|
+
type: "stdio",
|
|
7787
|
+
command: "bash",
|
|
7788
|
+
args: ["-lc", akanMcpCommand(mode, { cd: claudeProjectDir })]
|
|
7789
|
+
});
|
|
7790
|
+
var createAkanMcpServer = (target, mode = "readonly") => target === "cursor" ? createAkanCursorMcpServer(mode) : createAkanClaudeMcpServer(mode);
|
|
7492
7791
|
var akanCursorMcpServer = createAkanCursorMcpServer();
|
|
7792
|
+
var codexMcpServerTableHeader = "[mcp_servers.akan]";
|
|
7793
|
+
var createAkanCodexMcpServerBlock = (mode = "readonly") => `${codexMcpServerTableHeader}
|
|
7794
|
+
command = "bash"
|
|
7795
|
+
args = ["-lc", "${akanMcpCommand(mode)}"]
|
|
7796
|
+
`;
|
|
7797
|
+
var codexAkanTablePattern = /^\[mcp_servers\.akan\][^\n]*\n(?:(?!\[)[^\n]*(?:\n|$))*/m;
|
|
7798
|
+
var upsertCodexMcpServerBlock = (existing, block, { force = false } = {}) => {
|
|
7799
|
+
const nextBlock = block.endsWith(`
|
|
7800
|
+
`) ? block : `${block}
|
|
7801
|
+
`;
|
|
7802
|
+
const match = existing.match(codexAkanTablePattern);
|
|
7803
|
+
if (!match) {
|
|
7804
|
+
if (!existing.trim())
|
|
7805
|
+
return nextBlock;
|
|
7806
|
+
return `${existing.replace(/\s*$/, "")}
|
|
7807
|
+
|
|
7808
|
+
${nextBlock}`;
|
|
7809
|
+
}
|
|
7810
|
+
if (match[0].trim() === nextBlock.trim())
|
|
7811
|
+
return existing;
|
|
7812
|
+
if (!force)
|
|
7813
|
+
throw new Error(`${codexMcpConfigPath} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
|
|
7814
|
+
const start = match.index ?? 0;
|
|
7815
|
+
const before = existing.slice(0, start);
|
|
7816
|
+
const after = existing.slice(start + match[0].length);
|
|
7817
|
+
const separator = after && !after.startsWith(`
|
|
7818
|
+
`) ? `
|
|
7819
|
+
` : "";
|
|
7820
|
+
return `${before}${nextBlock}${separator}${after}`;
|
|
7821
|
+
};
|
|
7493
7822
|
var renderDoctorText = (result) => {
|
|
7494
7823
|
const lines = [`Akan doctor status: ${result.status}`];
|
|
7495
7824
|
if (result.diagnostics.length === 0) {
|
|
@@ -7512,7 +7841,6 @@ var generatedFiles = [
|
|
|
7512
7841
|
"*/lib/cnst.ts",
|
|
7513
7842
|
"*/lib/db.ts",
|
|
7514
7843
|
"*/lib/dict.ts",
|
|
7515
|
-
"*/lib/option.ts",
|
|
7516
7844
|
"*/lib/sig.ts",
|
|
7517
7845
|
"*/lib/srv.ts",
|
|
7518
7846
|
"*/lib/st.ts",
|
|
@@ -8506,7 +8834,7 @@ Caused by: ${ApplicationBuildReporter.formatError(error.cause)}` : "";
|
|
|
8506
8834
|
}
|
|
8507
8835
|
}
|
|
8508
8836
|
// pkgs/@akanjs/devkit/applicationBuildRunner.ts
|
|
8509
|
-
import { mkdir as mkdir8, rm as
|
|
8837
|
+
import { mkdir as mkdir8, rm as rm4 } from "fs/promises";
|
|
8510
8838
|
import path37 from "path";
|
|
8511
8839
|
|
|
8512
8840
|
// pkgs/@akanjs/devkit/frontendBuild/allRoutesBuilder.ts
|
|
@@ -8521,6 +8849,25 @@ async function appHasStModule(appCwdPath) {
|
|
|
8521
8849
|
}
|
|
8522
8850
|
var IMPLICIT_LAYOUT_DIR = path13.join(".akan", "generated", "root-layouts");
|
|
8523
8851
|
var IMPLICIT_DICT_DIR = path13.join(".akan", "generated", "dict");
|
|
8852
|
+
var IMPLICIT_OVERRIDES_DIR = path13.join(".akan", "generated", "overrides");
|
|
8853
|
+
var OVERRIDES_KEY_RE = /^\.\/(.+\/)?_overrides\.(tsx|ts|jsx|js)$/;
|
|
8854
|
+
async function writeGeneratedOverridesLayoutFile(opts) {
|
|
8855
|
+
const filename = `${opts.key.replace(/^\.\//, "").replace(/[^a-zA-Z0-9]+/g, "_")}.tsx`;
|
|
8856
|
+
const absPath = path13.join(path13.resolve(opts.appCwdPath), IMPLICIT_OVERRIDES_DIR, filename);
|
|
8857
|
+
const userRel = path13.relative(path13.dirname(absPath), opts.userAbsPath).split(path13.sep).join("/");
|
|
8858
|
+
const userSpecifier = userRel.startsWith(".") ? userRel : `./${userRel}`;
|
|
8859
|
+
const source2 = `"use client";
|
|
8860
|
+
import { UiOverrideProvider } from "akanjs/ui";
|
|
8861
|
+
import { createElement, type ReactNode } from "react";
|
|
8862
|
+
import value from ${JSON.stringify(userSpecifier)};
|
|
8863
|
+
|
|
8864
|
+
export default function AkanUiOverridesLayout({ children }: { children?: ReactNode }) {
|
|
8865
|
+
return createElement(UiOverrideProvider, { value }, children);
|
|
8866
|
+
}
|
|
8867
|
+
`;
|
|
8868
|
+
await Bun.write(absPath, source2);
|
|
8869
|
+
return absPath;
|
|
8870
|
+
}
|
|
8524
8871
|
function getRootBoundarySegments(key) {
|
|
8525
8872
|
const match = LAYOUT_KEY_RE.exec(key);
|
|
8526
8873
|
if (!match)
|
|
@@ -8713,9 +9060,17 @@ async function resolveSsrPageEntries(opts) {
|
|
|
8713
9060
|
const segments = getRootBoundarySegments(key);
|
|
8714
9061
|
return segments !== null && isRootBoundarySegments(segments, basePaths);
|
|
8715
9062
|
}));
|
|
8716
|
-
const base = opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map((key) =>
|
|
8717
|
-
key
|
|
8718
|
-
|
|
9063
|
+
const base = await Promise.all(opts.pageKeys.filter((key) => !rootLayoutKeys.has(key)).map(async (key) => {
|
|
9064
|
+
const userAbsPath = path13.resolve(absPageDir, key);
|
|
9065
|
+
if (OVERRIDES_KEY_RE.test(key)) {
|
|
9066
|
+
const moduleAbsPath = await writeGeneratedOverridesLayoutFile({
|
|
9067
|
+
appCwdPath: opts.appCwdPath,
|
|
9068
|
+
key,
|
|
9069
|
+
userAbsPath
|
|
9070
|
+
});
|
|
9071
|
+
return { key, moduleAbsPath, seedAbsPaths: [userAbsPath] };
|
|
9072
|
+
}
|
|
9073
|
+
return { key, moduleAbsPath: userAbsPath };
|
|
8719
9074
|
}));
|
|
8720
9075
|
const generated = await Promise.all(rootBoundaries.map(async (boundary) => ({
|
|
8721
9076
|
key: implicitRootLayoutKey(boundary.segments),
|
|
@@ -8747,7 +9102,7 @@ function computeRouteSeedIndex(pageEntries) {
|
|
|
8747
9102
|
for (const { key, moduleAbsPath, seedAbsPaths } of pageEntries) {
|
|
8748
9103
|
const parsed = parseRouteModuleKey2(key);
|
|
8749
9104
|
const files = [path14.resolve(moduleAbsPath), ...(seedAbsPaths ?? []).map((seed) => path14.resolve(seed))];
|
|
8750
|
-
if (parsed.kind === "layout") {
|
|
9105
|
+
if (parsed.kind === "layout" || parsed.kind === "overrides") {
|
|
8751
9106
|
const prefix = parsed.routeSegments.join("/");
|
|
8752
9107
|
const prev = layoutsByPrefix.get(prefix) ?? [];
|
|
8753
9108
|
layoutsByPrefix.set(prefix, [...prev, ...files]);
|
|
@@ -10264,7 +10619,7 @@ class AllRoutesBuilder {
|
|
|
10264
10619
|
}
|
|
10265
10620
|
}
|
|
10266
10621
|
// pkgs/@akanjs/devkit/frontendBuild/csrArtifactBuilder.ts
|
|
10267
|
-
import { mkdir as mkdir5, rm, unlink } from "fs/promises";
|
|
10622
|
+
import { mkdir as mkdir5, rm as rm2, unlink } from "fs/promises";
|
|
10268
10623
|
import path24 from "path";
|
|
10269
10624
|
|
|
10270
10625
|
// pkgs/@akanjs/devkit/frontendBuild/pagesEntrySourceGenerator.ts
|
|
@@ -10394,7 +10749,7 @@ class CsrArtifactBuilder {
|
|
|
10394
10749
|
const artifact = await this.#loadCsrArtifact();
|
|
10395
10750
|
const csrBasePaths = [...akanConfig2.basePaths];
|
|
10396
10751
|
const htmlEntries = csrBasePaths.length > 0 ? csrBasePaths : ["index"];
|
|
10397
|
-
await
|
|
10752
|
+
await rm2(this.#outputDir, { recursive: true, force: true });
|
|
10398
10753
|
await mkdir5(path24.join(this.#app.cwdPath, ".akan/generated/csr"), { recursive: true });
|
|
10399
10754
|
const generatedHtmlFiles = Object.fromEntries(htmlEntries.map((basePath2) => this.#createHtmlFile(basePath2)));
|
|
10400
10755
|
const result = await Bun.build({
|
|
@@ -11128,7 +11483,7 @@ class DevChangePlanner {
|
|
|
11128
11483
|
}
|
|
11129
11484
|
var uniqueResolved = (files) => [...new Set(files.map((file) => path27.resolve(file)))].sort();
|
|
11130
11485
|
// pkgs/@akanjs/devkit/frontendBuild/devGeneratedIndexSync.ts
|
|
11131
|
-
import { mkdir as mkdir6, readdir as readdir2, readFile, rm as
|
|
11486
|
+
import { mkdir as mkdir6, readdir as readdir2, readFile, rm as rm3, stat as stat3, writeFile } from "fs/promises";
|
|
11132
11487
|
import path28 from "path";
|
|
11133
11488
|
var BARREL_FACETS2 = new Set(["common", "srvkit", "ui", "webkit"]);
|
|
11134
11489
|
var FACET_SOURCE_FILE_RE = /\.(ts|tsx)$/;
|
|
@@ -11211,7 +11566,7 @@ class DevGeneratedIndexSync {
|
|
|
11211
11566
|
if (content === null) {
|
|
11212
11567
|
if (!await exists(indexPath))
|
|
11213
11568
|
return false;
|
|
11214
|
-
await
|
|
11569
|
+
await rm3(indexPath, { force: true });
|
|
11215
11570
|
return true;
|
|
11216
11571
|
}
|
|
11217
11572
|
const current = await readFile(indexPath, "utf8").catch(() => null);
|
|
@@ -12358,7 +12713,7 @@ class ApplicationBuildRunner {
|
|
|
12358
12713
|
await this.#app.getPageKeys({ refresh: true });
|
|
12359
12714
|
const { typecheckDir, tsconfigPath } = await this.#writeTypecheckTsconfig({ incremental });
|
|
12360
12715
|
if (clean)
|
|
12361
|
-
await
|
|
12716
|
+
await rm4(path37.join(typecheckDir, "tsconfig.tsbuildinfo"), { force: true });
|
|
12362
12717
|
await this.#checkProjectInChildProcess(tsconfigPath);
|
|
12363
12718
|
}
|
|
12364
12719
|
async#runPhase(id, label, task, summarize, options = {}) {
|
|
@@ -12558,7 +12913,7 @@ void run().catch((error) => {
|
|
|
12558
12913
|
}
|
|
12559
12914
|
}
|
|
12560
12915
|
// pkgs/@akanjs/devkit/applicationReleasePackager.ts
|
|
12561
|
-
import { cp, mkdir as mkdir9, rm as
|
|
12916
|
+
import { cp, mkdir as mkdir9, rm as rm5 } from "fs/promises";
|
|
12562
12917
|
import path38 from "path";
|
|
12563
12918
|
|
|
12564
12919
|
// pkgs/@akanjs/devkit/uploadRelease.ts
|
|
@@ -12665,7 +13020,7 @@ class ApplicationReleasePackager {
|
|
|
12665
13020
|
const platformVersion = akanConfig2.mobile.version;
|
|
12666
13021
|
const buildRoot = `${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}`;
|
|
12667
13022
|
if (await FileSys.dirExists(buildRoot))
|
|
12668
|
-
await
|
|
13023
|
+
await rm5(buildRoot, { recursive: true, force: true });
|
|
12669
13024
|
await mkdir9(buildRoot, { recursive: true });
|
|
12670
13025
|
if (rebuild || !await FileSys.dirExists(`${this.#app.dist.cwdPath}/backend`))
|
|
12671
13026
|
await this.#build();
|
|
@@ -12674,7 +13029,7 @@ class ApplicationReleasePackager {
|
|
|
12674
13029
|
await mkdir9(buildPath, { recursive: true });
|
|
12675
13030
|
await cp(`${this.#app.dist.cwdPath}/backend`, `${buildPath}/backend`, { recursive: true });
|
|
12676
13031
|
await cp(this.#app.dist.cwdPath, buildRoot, { recursive: true });
|
|
12677
|
-
await
|
|
13032
|
+
await rm5(`${buildRoot}/frontend/.next`, { recursive: true, force: true });
|
|
12678
13033
|
const releaseRoot = this.#app.workspace.workspaceRoot;
|
|
12679
13034
|
const releaseArchivePath = path38.relative(releaseRoot, `${releaseRoot}/releases/builds/${this.#app.name}-release.tar.gz`).split(path38.sep).join("/");
|
|
12680
13035
|
await this.#app.workspace.spawn("tar", ["-zcf", releaseArchivePath, "-C", buildRoot, "./"], { cwd: releaseRoot });
|
|
@@ -12690,7 +13045,7 @@ class ApplicationReleasePackager {
|
|
|
12690
13045
|
`${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}-appBuild.zip`,
|
|
12691
13046
|
"./csr"
|
|
12692
13047
|
]);
|
|
12693
|
-
await
|
|
13048
|
+
await rm5("./csr", { recursive: true, force: true });
|
|
12694
13049
|
}
|
|
12695
13050
|
async#writeSourceArchive({ readme }) {
|
|
12696
13051
|
const sourceRoot = `${this.#app.workspace.workspaceRoot}/releases/sources/${this.#app.name}`;
|
|
@@ -12701,7 +13056,7 @@ class ApplicationReleasePackager {
|
|
|
12701
13056
|
await Promise.all([".next", "ios", "android", "public/libs"].map(async (path39) => {
|
|
12702
13057
|
const targetPath = `${sourceRoot}/apps/${this.#app.name}/${path39}`;
|
|
12703
13058
|
if (await FileSys.dirExists(targetPath))
|
|
12704
|
-
await
|
|
13059
|
+
await rm5(targetPath, { recursive: true, force: true });
|
|
12705
13060
|
}));
|
|
12706
13061
|
const syncPaths = [".husky", ".gitignore", "package.json"];
|
|
12707
13062
|
await Promise.all(syncPaths.map((path39) => cp(`${this.#app.workspace.cwdPath}/${path39}`, `${sourceRoot}/${path39}`, { recursive: true })));
|
|
@@ -12716,7 +13071,7 @@ class ApplicationReleasePackager {
|
|
|
12716
13071
|
const maxRetry = 3;
|
|
12717
13072
|
for (let i = 0;i < maxRetry; i++) {
|
|
12718
13073
|
try {
|
|
12719
|
-
await
|
|
13074
|
+
await rm5(sourceRoot, { recursive: true, force: true });
|
|
12720
13075
|
} catch {}
|
|
12721
13076
|
}
|
|
12722
13077
|
}
|
|
@@ -12805,11 +13160,15 @@ async function resolveSignalTestPreloadPath(target) {
|
|
|
12805
13160
|
addResolvedPackageCandidate(path39.dirname(Bun.main));
|
|
12806
13161
|
addResolvedPackageCandidate(import.meta.dir);
|
|
12807
13162
|
candidates.push(path39.join(target.cwdPath, "../../node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH), path39.join(target.cwdPath, "../../pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH), path39.join(process.cwd(), "node_modules/akanjs", SIGNAL_TEST_PRELOAD_PATH), path39.join(process.cwd(), "pkgs/akanjs", SIGNAL_TEST_PRELOAD_PATH), path39.join(path39.dirname(Bun.main), "../../akanjs", SIGNAL_TEST_PRELOAD_PATH), path39.resolve(import.meta.dir, "../../akanjs", SIGNAL_TEST_PRELOAD_PATH));
|
|
12808
|
-
|
|
13163
|
+
const uniqueCandidates = [...new Set(candidates)];
|
|
13164
|
+
for (const candidate of uniqueCandidates) {
|
|
12809
13165
|
if (await Bun.file(candidate).exists())
|
|
12810
13166
|
return candidate;
|
|
12811
13167
|
}
|
|
12812
|
-
throw new Error(`Failed to locate ${SIGNAL_TEST_PRELOAD_PATH} from ${target.cwdPath}
|
|
13168
|
+
throw new Error(`Failed to locate ${SIGNAL_TEST_PRELOAD_PATH} from ${target.cwdPath}.
|
|
13169
|
+
Probed paths:
|
|
13170
|
+
${uniqueCandidates.map((candidate) => ` - ${candidate}`).join(`
|
|
13171
|
+
`)}`);
|
|
12813
13172
|
}
|
|
12814
13173
|
// pkgs/@akanjs/devkit/builder.ts
|
|
12815
13174
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -12945,7 +13304,7 @@ class Builder {
|
|
|
12945
13304
|
}
|
|
12946
13305
|
}
|
|
12947
13306
|
// pkgs/@akanjs/devkit/capacitorApp.ts
|
|
12948
|
-
import { cp as cp2, mkdir as mkdir11, readFile as readFile2, rm as
|
|
13307
|
+
import { cp as cp2, mkdir as mkdir11, readFile as readFile2, rm as rm6, writeFile as writeFile2 } from "fs/promises";
|
|
12949
13308
|
import os from "os";
|
|
12950
13309
|
import path41 from "path";
|
|
12951
13310
|
import { select as select2 } from "@inquirer/prompts";
|
|
@@ -13156,7 +13515,7 @@ var rootCapacitorConfigFilenames = [
|
|
|
13156
13515
|
];
|
|
13157
13516
|
var rootCapacitorConfigPaths = (appRoot) => rootCapacitorConfigFilenames.map((file) => path41.join(appRoot, file));
|
|
13158
13517
|
async function clearRootCapacitorConfigs(appRoot) {
|
|
13159
|
-
await Promise.all(rootCapacitorConfigPaths(appRoot).map((file) =>
|
|
13518
|
+
await Promise.all(rootCapacitorConfigPaths(appRoot).map((file) => rm6(file, { force: true })));
|
|
13160
13519
|
}
|
|
13161
13520
|
async function writeRootCapacitorConfig(appRoot, content) {
|
|
13162
13521
|
await clearRootCapacitorConfigs(appRoot);
|
|
@@ -13177,6 +13536,7 @@ var getLocalIP = () => {
|
|
|
13177
13536
|
var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13178
13537
|
var asString = (value) => typeof value === "string" ? value : undefined;
|
|
13179
13538
|
var firstString = (...values) => values.find((value) => typeof value === "string");
|
|
13539
|
+
var resolveIosApnsEnvironment = ({ operation }) => operation === "release" ? "production" : "development";
|
|
13180
13540
|
var scoreIosDeviceTarget = (target) => {
|
|
13181
13541
|
const state = target.state?.toLowerCase() ?? "";
|
|
13182
13542
|
return (state.includes("available") ? 4 : 0) + (state.includes("wired") ? 2 : 0) + (state.includes("paired") ? 1 : 0);
|
|
@@ -13482,6 +13842,8 @@ function materializeCapacitorConfig(target, { operation, localServerUrl, localIp
|
|
|
13482
13842
|
const experimentalConfig = isRecord2(experimental) ? experimental : undefined;
|
|
13483
13843
|
const pluginsConfig = isRecord2(plugins) ? plugins : {};
|
|
13484
13844
|
const keyboardPluginConfig = isRecord2(pluginsConfig.Keyboard) ? pluginsConfig.Keyboard : {};
|
|
13845
|
+
const pushNotificationsPluginConfig = isRecord2(pluginsConfig.PushNotifications) ? pluginsConfig.PushNotifications : {};
|
|
13846
|
+
const usesPushNotifications = target.permissions?.includes("push") ?? false;
|
|
13485
13847
|
const config = {
|
|
13486
13848
|
...capacitorConfig,
|
|
13487
13849
|
appId,
|
|
@@ -13490,6 +13852,12 @@ function materializeCapacitorConfig(target, { operation, localServerUrl, localIp
|
|
|
13490
13852
|
plugins: {
|
|
13491
13853
|
CapacitorCookies: { enabled: true },
|
|
13492
13854
|
...pluginsConfig,
|
|
13855
|
+
...usesPushNotifications || isRecord2(pluginsConfig.PushNotifications) ? {
|
|
13856
|
+
PushNotifications: {
|
|
13857
|
+
...usesPushNotifications ? { presentationOptions: ["badge", "sound", "alert"] } : {},
|
|
13858
|
+
...pushNotificationsPluginConfig
|
|
13859
|
+
}
|
|
13860
|
+
} : {},
|
|
13493
13861
|
Keyboard: {
|
|
13494
13862
|
resize: "none",
|
|
13495
13863
|
...keyboardPluginConfig
|
|
@@ -13561,9 +13929,9 @@ class CapacitorApp {
|
|
|
13561
13929
|
await mkdir11(this.targetRoot, { recursive: true });
|
|
13562
13930
|
if (regenerate) {
|
|
13563
13931
|
if (!platform || platform === "ios")
|
|
13564
|
-
await
|
|
13932
|
+
await rm6(path41.join(this.app.cwdPath, this.iosRootPath), { recursive: true, force: true });
|
|
13565
13933
|
if (!platform || platform === "android")
|
|
13566
|
-
await
|
|
13934
|
+
await rm6(path41.join(this.app.cwdPath, this.androidRootPath), { recursive: true, force: true });
|
|
13567
13935
|
}
|
|
13568
13936
|
const project = this.project;
|
|
13569
13937
|
await this.project.load();
|
|
@@ -13585,7 +13953,7 @@ class CapacitorApp {
|
|
|
13585
13953
|
await this.#prepareTargetAssets();
|
|
13586
13954
|
await this.#prepareExternalFiles("ios");
|
|
13587
13955
|
await this.#applyIosMetadata();
|
|
13588
|
-
await this.#applyPermissions();
|
|
13956
|
+
await this.#applyPermissions({ operation, env });
|
|
13589
13957
|
await this.#applyDeepLinks("ios", { operation, env });
|
|
13590
13958
|
await this.project.commit();
|
|
13591
13959
|
await this.#generateAssets({ operation, env });
|
|
@@ -13728,7 +14096,7 @@ ${error.message}`;
|
|
|
13728
14096
|
await this.#prepareTargetAssets();
|
|
13729
14097
|
await this.#prepareExternalFiles("android");
|
|
13730
14098
|
await this.#applyAndroidMetadata();
|
|
13731
|
-
await this.#applyPermissions();
|
|
14099
|
+
await this.#applyPermissions({ operation, env });
|
|
13732
14100
|
await this.#applyDeepLinks("android", { operation, env });
|
|
13733
14101
|
await this.project.commit();
|
|
13734
14102
|
await this.#generateAssets({ operation, env });
|
|
@@ -13862,7 +14230,7 @@ ${error.message}`;
|
|
|
13862
14230
|
const htmlSource = path41.join(this.app.dist.cwdPath, "csr", targetHtmlFilename(this.target));
|
|
13863
14231
|
if (!await Bun.file(htmlSource).exists())
|
|
13864
14232
|
throw new Error(`CSR html for mobile target '${this.target.name}' not found: ${htmlSource}`);
|
|
13865
|
-
await
|
|
14233
|
+
await rm6(this.targetWebRoot, { recursive: true, force: true });
|
|
13866
14234
|
await mkdir11(this.targetWebRoot, { recursive: true });
|
|
13867
14235
|
await Bun.write(path41.join(this.targetWebRoot, "index.html"), this.#injectMobileTargetMeta(await Bun.file(htmlSource).text()));
|
|
13868
14236
|
}
|
|
@@ -13943,7 +14311,7 @@ ${error.message}`;
|
|
|
13943
14311
|
await this.project.android.setVersionCode(this.target.buildNum);
|
|
13944
14312
|
await this.project.android.setAppName(this.target.appName);
|
|
13945
14313
|
}
|
|
13946
|
-
async#applyPermissions() {
|
|
14314
|
+
async#applyPermissions({ operation, env }) {
|
|
13947
14315
|
for (const permission of this.target.permissions ?? []) {
|
|
13948
14316
|
if (permission === "camera")
|
|
13949
14317
|
await this.addCamera();
|
|
@@ -13952,7 +14320,7 @@ ${error.message}`;
|
|
|
13952
14320
|
else if (permission === "location")
|
|
13953
14321
|
await this.addLocation();
|
|
13954
14322
|
else if (permission === "push")
|
|
13955
|
-
await this.addPush();
|
|
14323
|
+
await this.addPush({ operation, env });
|
|
13956
14324
|
}
|
|
13957
14325
|
}
|
|
13958
14326
|
async#applyDeepLinks(platform, { operation, env }) {
|
|
@@ -13971,7 +14339,7 @@ ${error.message}`;
|
|
|
13971
14339
|
await this.#setUrlSchemesInIos(schemes);
|
|
13972
14340
|
}
|
|
13973
14341
|
if (domains.length > 0)
|
|
13974
|
-
await this.#setAssociatedDomainsInIos(domains);
|
|
14342
|
+
await this.#setAssociatedDomainsInIos(domains, { operation, env });
|
|
13975
14343
|
return;
|
|
13976
14344
|
}
|
|
13977
14345
|
if (platform === "android") {
|
|
@@ -14065,12 +14433,64 @@ ${error.message}`;
|
|
|
14065
14433
|
this.#setPermissionsInAndroid(["ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION"]);
|
|
14066
14434
|
this.#setFeaturesInAndroid(["android.hardware.location.gps"]);
|
|
14067
14435
|
}
|
|
14068
|
-
async addPush() {
|
|
14436
|
+
async addPush({ operation, env }) {
|
|
14069
14437
|
await this.#setPermissionInIos({
|
|
14070
14438
|
userNotificationsUsageDescription: "$(PRODUCT_NAME) uses notifications to keep you updated."
|
|
14071
14439
|
});
|
|
14440
|
+
await Promise.all([
|
|
14441
|
+
this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", { UIBackgroundModes: ["remote-notification"] }),
|
|
14442
|
+
this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { UIBackgroundModes: ["remote-notification"] }),
|
|
14443
|
+
this.#writeEntitlementsInIos(this.target.deepLinks?.domains ?? [], { operation, env }),
|
|
14444
|
+
this.#ensurePushAppDelegateInIos()
|
|
14445
|
+
]);
|
|
14072
14446
|
this.#setPermissionsInAndroid(["POST_NOTIFICATIONS"]);
|
|
14073
14447
|
}
|
|
14448
|
+
async#ensurePushAppDelegateInIos() {
|
|
14449
|
+
const appDelegatePath = path41.join(this.app.cwdPath, this.iosProjectPath, "App/AppDelegate.swift");
|
|
14450
|
+
if (!await Bun.file(appDelegatePath).exists())
|
|
14451
|
+
return;
|
|
14452
|
+
const editor = await FileEditor.create(appDelegatePath);
|
|
14453
|
+
let content = editor.getContent();
|
|
14454
|
+
if (!content.includes("import FirebaseCore")) {
|
|
14455
|
+
content = content.replace("import Capacitor", `import Capacitor
|
|
14456
|
+
import FirebaseCore`);
|
|
14457
|
+
}
|
|
14458
|
+
if (!content.includes("import FirebaseMessaging")) {
|
|
14459
|
+
content = content.replace("import FirebaseCore", `import FirebaseCore
|
|
14460
|
+
import FirebaseMessaging`);
|
|
14461
|
+
}
|
|
14462
|
+
if (!content.includes("FirebaseApp.configure()")) {
|
|
14463
|
+
content = content.replace(/\n(\s*)return true/, `
|
|
14464
|
+
$1FirebaseApp.configure()
|
|
14465
|
+
|
|
14466
|
+
$1return true`);
|
|
14467
|
+
}
|
|
14468
|
+
if (!content.includes("didRegisterForRemoteNotificationsWithDeviceToken")) {
|
|
14469
|
+
const delegateMethods = `
|
|
14470
|
+
func application(_ application: UIApplication,
|
|
14471
|
+
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
|
14472
|
+
Messaging.messaging().apnsToken = deviceToken
|
|
14473
|
+
NotificationCenter.default.post(
|
|
14474
|
+
name: .capacitorDidRegisterForRemoteNotifications,
|
|
14475
|
+
object: deviceToken
|
|
14476
|
+
)
|
|
14477
|
+
}
|
|
14478
|
+
|
|
14479
|
+
func application(_ application: UIApplication,
|
|
14480
|
+
didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
|
14481
|
+
NotificationCenter.default.post(
|
|
14482
|
+
name: .capacitorDidFailToRegisterForRemoteNotifications,
|
|
14483
|
+
object: error
|
|
14484
|
+
)
|
|
14485
|
+
}
|
|
14486
|
+
`;
|
|
14487
|
+
content = content.replace(`
|
|
14488
|
+
func applicationWillResignActive`, `${delegateMethods}
|
|
14489
|
+
func applicationWillResignActive`);
|
|
14490
|
+
}
|
|
14491
|
+
if (content !== editor.getContent())
|
|
14492
|
+
await editor.setContent(content).save();
|
|
14493
|
+
}
|
|
14074
14494
|
async#setPermissionInIos(permissions) {
|
|
14075
14495
|
const updateNs = Object.fromEntries(Object.entries(permissions).map(([key, value]) => [`NS${capitalize5(key)}`, value]));
|
|
14076
14496
|
await Promise.all([
|
|
@@ -14088,25 +14508,35 @@ ${error.message}`;
|
|
|
14088
14508
|
this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { CFBundleURLTypes: urlTypes })
|
|
14089
14509
|
]);
|
|
14090
14510
|
}
|
|
14091
|
-
async#setAssociatedDomainsInIos(domains) {
|
|
14511
|
+
async#setAssociatedDomainsInIos(domains, { operation, env }) {
|
|
14512
|
+
await this.#writeEntitlementsInIos(domains, { operation, env });
|
|
14513
|
+
}
|
|
14514
|
+
async#writeEntitlementsInIos(domains, runConfig) {
|
|
14092
14515
|
const entitlementsRelPath = "App/App.entitlements";
|
|
14093
14516
|
const entitlementsPath = path41.join(this.app.cwdPath, this.iosProjectPath, entitlementsRelPath);
|
|
14094
14517
|
const values = domains.map((domain) => `applinks:${domain}`);
|
|
14518
|
+
const usesPush = this.target.permissions?.includes("push") ?? false;
|
|
14519
|
+
const apsEnvironment = resolveIosApnsEnvironment(runConfig);
|
|
14095
14520
|
const body = [
|
|
14096
14521
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
14097
14522
|
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
14098
14523
|
'<plist version="1.0">',
|
|
14099
14524
|
"<dict>",
|
|
14100
|
-
" <key>
|
|
14101
|
-
|
|
14102
|
-
|
|
14103
|
-
|
|
14525
|
+
...usesPush ? [" <key>aps-environment</key>", ` <string>${apsEnvironment}</string>`] : [],
|
|
14526
|
+
...values.length > 0 ? [
|
|
14527
|
+
" <key>com.apple.developer.associated-domains</key>",
|
|
14528
|
+
" <array>",
|
|
14529
|
+
...values.map((value) => ` <string>${value}</string>`),
|
|
14530
|
+
" </array>"
|
|
14531
|
+
] : [],
|
|
14104
14532
|
"</dict>",
|
|
14105
14533
|
"</plist>",
|
|
14106
14534
|
""
|
|
14107
14535
|
].join(`
|
|
14108
14536
|
`);
|
|
14109
|
-
await
|
|
14537
|
+
const currentBody = await Bun.file(entitlementsPath).exists() ? await Bun.file(entitlementsPath).text() : undefined;
|
|
14538
|
+
if (currentBody !== body)
|
|
14539
|
+
await writeFile2(entitlementsPath, body);
|
|
14110
14540
|
await this.#setCodeSignEntitlementsInIos(entitlementsRelPath);
|
|
14111
14541
|
}
|
|
14112
14542
|
async#setCodeSignEntitlementsInIos(entitlementsRelPath) {
|
|
@@ -14210,7 +14640,7 @@ ${filter}$1`);
|
|
|
14210
14640
|
for (const permission of permissions) {
|
|
14211
14641
|
if (this.#hasPermissionInAndroid(permission)) {
|
|
14212
14642
|
this.app.logger.info(`${permission} already exists in android`);
|
|
14213
|
-
|
|
14643
|
+
continue;
|
|
14214
14644
|
}
|
|
14215
14645
|
this.app.logger.info(`Adding ${permission} to android`);
|
|
14216
14646
|
this.project.android.getAndroidManifest().injectFragment("manifest", `<uses-permission android:name="android.permission.${permission}" />`);
|
|
@@ -14226,7 +14656,7 @@ ${filter}$1`);
|
|
|
14226
14656
|
return Array.from(usesPermission).map((permission) => permission.getAttribute("android:name"));
|
|
14227
14657
|
}
|
|
14228
14658
|
#hasPermissionInAndroid(permission) {
|
|
14229
|
-
return this.#getPermissionsInAndroid().includes(permission);
|
|
14659
|
+
return this.#getPermissionsInAndroid().includes(`android.permission.${permission}`);
|
|
14230
14660
|
}
|
|
14231
14661
|
}
|
|
14232
14662
|
// pkgs/@akanjs/devkit/commandDecorators/targetMeta.ts
|
|
@@ -15322,6 +15752,42 @@ var CONVENTION_SUFFIXES = [
|
|
|
15322
15752
|
".signal.ts",
|
|
15323
15753
|
".store.ts"
|
|
15324
15754
|
];
|
|
15755
|
+
var PAGE_RESERVED_EXPORTS = new Set([
|
|
15756
|
+
"pageConfig",
|
|
15757
|
+
"head",
|
|
15758
|
+
"metadata",
|
|
15759
|
+
"generateHead",
|
|
15760
|
+
"generateMetadata",
|
|
15761
|
+
"fonts",
|
|
15762
|
+
"manifest",
|
|
15763
|
+
"theme",
|
|
15764
|
+
"reconnect",
|
|
15765
|
+
"wsConnect",
|
|
15766
|
+
"layoutStyle",
|
|
15767
|
+
"gaTrackingId"
|
|
15768
|
+
]);
|
|
15769
|
+
var RULE_FIXES = {
|
|
15770
|
+
"akan.global.duplicate-exported-function-name": "Rename one of the exports, or if they are the same thing, extract it into one shared module and import it in both places.",
|
|
15771
|
+
"akan.global.duplicate-exported-function-body": "Extract the shared implementation into a single exported helper and import it, instead of copying the body.",
|
|
15772
|
+
"akan.file.recommended-max-lines": "Split the file by responsibility \u2014 move Zones, Utils, or subcomponents into sibling files.",
|
|
15773
|
+
"akan.file.max-lines": "Break the file into smaller focused modules; keep one primary responsibility per file.",
|
|
15774
|
+
"akan.file.placeholder-export": "Remove the placeholder export; generated indexes should only re-export real modules.",
|
|
15775
|
+
"akan.file.dictionary-stale-text": "Replace the scaffold text with real localized copy for this dictionary entry.",
|
|
15776
|
+
"akan.file.global-declaration": "Move the global declaration into an approved low-level integration file (e.g. webkit) and keep it isolated.",
|
|
15777
|
+
"akan.file.window-augmentation": "Move the Window augmentation into an approved browser integration file and keep it isolated.",
|
|
15778
|
+
"akan.file.prototype-mutation": "Avoid prototype mutation, or isolate it in an approved low-level integration file.",
|
|
15779
|
+
"akan.file.class-export-global-declaration": "Move the helper to a sibling file and import it into the class module.",
|
|
15780
|
+
"akan.file.component-internal-declaration": "Move the type or helper to a type/util file in ui/, webkit/, or common/ by purpose. If it is the component's props, declare it as `interface <Component>Props`.",
|
|
15781
|
+
"akan.file.component-export": "Move the value or type to a util/constant/type file in ui/, webkit/, or common/ and import it. Adding `export` is not a valid fix \u2014 only PascalCase components and their `<Component>Props` interface belong here.",
|
|
15782
|
+
"akan.layout.app-root-file": "Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
|
|
15783
|
+
"akan.layout.lib-root-file": "Move the file into a domain module folder under lib/; keep lib root limited to generated support facets.",
|
|
15784
|
+
"akan.layout.module-ui-file": "Rename the file to an allowed module UI name, or move it to ui/ if it is not a module component."
|
|
15785
|
+
};
|
|
15786
|
+
function getRuleFix(rule) {
|
|
15787
|
+
if (rule.startsWith("akan.convention"))
|
|
15788
|
+
return "Keep only the model's allowed declarations in this file; move other logic to the matching domain file (service, document, store, etc.).";
|
|
15789
|
+
return RULE_FIXES[rule];
|
|
15790
|
+
}
|
|
15325
15791
|
|
|
15326
15792
|
class AkanQualityScanner {
|
|
15327
15793
|
async scan(workspaceRoot) {
|
|
@@ -15330,13 +15796,14 @@ class AkanQualityScanner {
|
|
|
15330
15796
|
const warnings = [
|
|
15331
15797
|
...this.#scanGlobalQuality(sourceFiles),
|
|
15332
15798
|
...sourceFiles.flatMap((sourceFile2) => this.#scanSingleFileQuality(sourceFile2)),
|
|
15799
|
+
...sourceFiles.flatMap((sourceFile2) => this.#scanComponentQuality(sourceFile2)),
|
|
15333
15800
|
...sourceFiles.flatMap((sourceFile2) => this.#scanConventionQuality(sourceFile2)),
|
|
15334
15801
|
...sourceFiles.flatMap((sourceFile2) => this.#scanLayoutQuality(sourceFile2))
|
|
15335
15802
|
];
|
|
15336
15803
|
return {
|
|
15337
15804
|
workspaceRoot,
|
|
15338
15805
|
scannedFiles: sourceFiles.length,
|
|
15339
|
-
warnings: warnings.sort(compareWarnings),
|
|
15806
|
+
warnings: warnings.map((warning) => ({ ...warning, fix: warning.fix ?? getRuleFix(warning.rule) })).sort(compareWarnings),
|
|
15340
15807
|
suggestedRules: SUGGESTED_RULES
|
|
15341
15808
|
};
|
|
15342
15809
|
}
|
|
@@ -15386,7 +15853,8 @@ class AkanQualityScanner {
|
|
|
15386
15853
|
#scanGlobalQuality(sourceFiles) {
|
|
15387
15854
|
const exportedFunctionLikes = sourceFiles.flatMap((sourceFile2) => getExportedFunctionLikes(sourceFile2));
|
|
15388
15855
|
const warnings = [];
|
|
15389
|
-
|
|
15856
|
+
const nameCheckedDeclarations = exportedFunctionLikes.filter((declaration) => !declaration.duplicateNameExempt);
|
|
15857
|
+
for (const [name, declarations] of groupBy(nameCheckedDeclarations, (declaration) => declaration.name)) {
|
|
15390
15858
|
if (declarations.length < 2)
|
|
15391
15859
|
continue;
|
|
15392
15860
|
warnings.push({
|
|
@@ -15457,6 +15925,49 @@ class AkanQualityScanner {
|
|
|
15457
15925
|
}
|
|
15458
15926
|
return warnings;
|
|
15459
15927
|
}
|
|
15928
|
+
#scanComponentQuality(sourceFile2) {
|
|
15929
|
+
if (!isComponentDeclarationFile(sourceFile2.file))
|
|
15930
|
+
return [];
|
|
15931
|
+
const isPage = isPageRouteFile(sourceFile2.file);
|
|
15932
|
+
const declarations = getComponentFileDeclarations(sourceFile2.sourceFile);
|
|
15933
|
+
const compoundComponentNames = getCompoundComponentNames(sourceFile2.sourceFile);
|
|
15934
|
+
const componentNames = declarations.filter((declaration) => declaration.exported && isComponentValueKind(declaration.kind)).filter((declaration) => isPascalCaseName(declaration.name)).map((declaration) => declaration.name);
|
|
15935
|
+
const allowedComponentNames = new Set([...componentNames, ...compoundComponentNames]);
|
|
15936
|
+
const allowedPropsInterfaces = new Set([...allowedComponentNames].map((name) => `${name}Props`));
|
|
15937
|
+
const warnings = [];
|
|
15938
|
+
for (const declaration of declarations) {
|
|
15939
|
+
if (declaration.isDefaultExport)
|
|
15940
|
+
continue;
|
|
15941
|
+
if (declaration.kind === "interface" && allowedPropsInterfaces.has(declaration.name))
|
|
15942
|
+
continue;
|
|
15943
|
+
if (declaration.exported) {
|
|
15944
|
+
if (isAllowedComponentExport(declaration, isPage))
|
|
15945
|
+
continue;
|
|
15946
|
+
warnings.push({
|
|
15947
|
+
rule: "akan.file.component-export",
|
|
15948
|
+
scope: "file",
|
|
15949
|
+
severity: "warning",
|
|
15950
|
+
file: sourceFile2.file,
|
|
15951
|
+
line: declaration.line,
|
|
15952
|
+
message: `Component file exports ${declaration.kind} "${declaration.name}", which is not a PascalCase component${isPage ? " or reserved route export" : ""}.`
|
|
15953
|
+
});
|
|
15954
|
+
continue;
|
|
15955
|
+
}
|
|
15956
|
+
if (isComponentValueKind(declaration.kind) && compoundComponentNames.has(declaration.name))
|
|
15957
|
+
continue;
|
|
15958
|
+
if (isRestrictedInternalKind(declaration.kind)) {
|
|
15959
|
+
warnings.push({
|
|
15960
|
+
rule: "akan.file.component-internal-declaration",
|
|
15961
|
+
scope: "file",
|
|
15962
|
+
severity: "warning",
|
|
15963
|
+
file: sourceFile2.file,
|
|
15964
|
+
line: declaration.line,
|
|
15965
|
+
message: `Component file declares non-exported ${declaration.kind} "${declaration.name}". Only "interface <Component>Props" may stay internal.`
|
|
15966
|
+
});
|
|
15967
|
+
}
|
|
15968
|
+
}
|
|
15969
|
+
return warnings;
|
|
15970
|
+
}
|
|
15460
15971
|
#scanConventionQuality(sourceFile2) {
|
|
15461
15972
|
const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile2.file.endsWith(candidate));
|
|
15462
15973
|
if (!suffix)
|
|
@@ -15532,6 +16043,8 @@ function formatQualityWarnings(warnings) {
|
|
|
15532
16043
|
if (warning.locations?.length) {
|
|
15533
16044
|
lines.push(...warning.locations.map(({ file, line }) => ` note: related location ${formatQualityLocation(file, line)}`));
|
|
15534
16045
|
}
|
|
16046
|
+
if (warning.fix)
|
|
16047
|
+
lines.push(` fix: ${warning.fix}`);
|
|
15535
16048
|
return lines;
|
|
15536
16049
|
});
|
|
15537
16050
|
}
|
|
@@ -15540,6 +16053,7 @@ function formatQualityLocation(file, line) {
|
|
|
15540
16053
|
}
|
|
15541
16054
|
function getExportedFunctionLikes(sourceFile2) {
|
|
15542
16055
|
const declarations = [];
|
|
16056
|
+
const nameExempt = isPageRouteFile(sourceFile2.file) || isUiComponentFile(sourceFile2.file);
|
|
15543
16057
|
for (const statement of sourceFile2.sourceFile.statements) {
|
|
15544
16058
|
if (ts11.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
15545
16059
|
declarations.push({
|
|
@@ -15547,7 +16061,8 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15547
16061
|
kind: "function",
|
|
15548
16062
|
file: sourceFile2.file,
|
|
15549
16063
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15550
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body)
|
|
16064
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement.body),
|
|
16065
|
+
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15551
16066
|
});
|
|
15552
16067
|
}
|
|
15553
16068
|
if (ts11.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
@@ -15556,7 +16071,8 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15556
16071
|
kind: "class",
|
|
15557
16072
|
file: sourceFile2.file,
|
|
15558
16073
|
line: getLine(sourceFile2.sourceFile, statement),
|
|
15559
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement)
|
|
16074
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, statement),
|
|
16075
|
+
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, isEnumClassStatement(sourceFile2.sourceFile, statement))
|
|
15560
16076
|
});
|
|
15561
16077
|
}
|
|
15562
16078
|
if (ts11.isVariableStatement(statement) && isExported(statement)) {
|
|
@@ -15568,16 +16084,127 @@ function getExportedFunctionLikes(sourceFile2) {
|
|
|
15568
16084
|
kind: "function-variable",
|
|
15569
16085
|
file: sourceFile2.file,
|
|
15570
16086
|
line: getLine(sourceFile2.sourceFile, declaration),
|
|
15571
|
-
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer)
|
|
16087
|
+
bodyFingerprint: getBodyFingerprint(sourceFile2.sourceFile, declaration.initializer),
|
|
16088
|
+
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile2.file, false)
|
|
15572
16089
|
});
|
|
15573
16090
|
}
|
|
15574
16091
|
}
|
|
15575
16092
|
}
|
|
15576
16093
|
return declarations;
|
|
15577
16094
|
}
|
|
16095
|
+
function isPageRouteFile(file) {
|
|
16096
|
+
const segments = file.split("/");
|
|
16097
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "page";
|
|
16098
|
+
}
|
|
16099
|
+
function isUiComponentFile(file) {
|
|
16100
|
+
const segments = file.split("/");
|
|
16101
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments[2] === "ui";
|
|
16102
|
+
}
|
|
16103
|
+
function isConventionDuplicateNameExempt(file, isEnumClass) {
|
|
16104
|
+
if (!isInLibModule(file))
|
|
16105
|
+
return false;
|
|
16106
|
+
if (file.endsWith(".tsx"))
|
|
16107
|
+
return true;
|
|
16108
|
+
if (file.endsWith(".document.ts") || file.endsWith(".service.ts") || file.endsWith(".signal.ts") || file.endsWith(".store.ts"))
|
|
16109
|
+
return true;
|
|
16110
|
+
if (file.endsWith(".constant.ts"))
|
|
16111
|
+
return !isEnumClass;
|
|
16112
|
+
return false;
|
|
16113
|
+
}
|
|
16114
|
+
function isInLibModule(file) {
|
|
16115
|
+
const segments = file.split("/");
|
|
16116
|
+
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
16117
|
+
}
|
|
16118
|
+
function isEnumClassStatement(sourceFile2, statement) {
|
|
16119
|
+
if (!ts11.isClassDeclaration(statement))
|
|
16120
|
+
return false;
|
|
16121
|
+
const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts11.SyntaxKind.ExtendsKeyword);
|
|
16122
|
+
const expression = heritageClause?.types[0]?.expression;
|
|
16123
|
+
return !!expression && expression.getText(sourceFile2).startsWith("enumOf(");
|
|
16124
|
+
}
|
|
15578
16125
|
function getExportedClassNames(sourceFile2) {
|
|
15579
16126
|
return sourceFile2.statements.filter((statement) => ts11.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
|
|
15580
16127
|
}
|
|
16128
|
+
function isComponentDeclarationFile(file) {
|
|
16129
|
+
if (!file.endsWith(".tsx"))
|
|
16130
|
+
return false;
|
|
16131
|
+
const segments = file.split("/");
|
|
16132
|
+
const [root, , area] = segments;
|
|
16133
|
+
if (root !== "apps" && root !== "libs")
|
|
16134
|
+
return false;
|
|
16135
|
+
if (area === "lib" || area === "ui")
|
|
16136
|
+
return true;
|
|
16137
|
+
return root === "apps" && area === "page";
|
|
16138
|
+
}
|
|
16139
|
+
function getComponentFileDeclarations(sourceFile2) {
|
|
16140
|
+
const reExportedNames = new Set;
|
|
16141
|
+
for (const statement of sourceFile2.statements) {
|
|
16142
|
+
if (ts11.isExportDeclaration(statement) && statement.exportClause && ts11.isNamedExports(statement.exportClause)) {
|
|
16143
|
+
for (const element of statement.exportClause.elements)
|
|
16144
|
+
reExportedNames.add((element.propertyName ?? element.name).text);
|
|
16145
|
+
}
|
|
16146
|
+
}
|
|
16147
|
+
const declarations = [];
|
|
16148
|
+
for (const statement of sourceFile2.statements) {
|
|
16149
|
+
const isDefaultExport = isDefaultExportStatement(statement);
|
|
16150
|
+
const inlineExported = isExported(statement);
|
|
16151
|
+
const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
|
|
16152
|
+
if (ts11.isInterfaceDeclaration(statement))
|
|
16153
|
+
add(statement.name.text, "interface", getLine(sourceFile2, statement));
|
|
16154
|
+
else if (ts11.isTypeAliasDeclaration(statement))
|
|
16155
|
+
add(statement.name.text, "type", getLine(sourceFile2, statement));
|
|
16156
|
+
else if (ts11.isEnumDeclaration(statement))
|
|
16157
|
+
add(statement.name.text, "enum", getLine(sourceFile2, statement));
|
|
16158
|
+
else if (ts11.isFunctionDeclaration(statement) && statement.name)
|
|
16159
|
+
add(statement.name.text, "function", getLine(sourceFile2, statement));
|
|
16160
|
+
else if (ts11.isClassDeclaration(statement) && statement.name)
|
|
16161
|
+
add(statement.name.text, "class", getLine(sourceFile2, statement));
|
|
16162
|
+
else if (ts11.isVariableStatement(statement)) {
|
|
16163
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
16164
|
+
if (!ts11.isIdentifier(declaration.name))
|
|
16165
|
+
continue;
|
|
16166
|
+
const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
|
|
16167
|
+
add(declaration.name.text, kind, getLine(sourceFile2, declaration));
|
|
16168
|
+
}
|
|
16169
|
+
}
|
|
16170
|
+
}
|
|
16171
|
+
return declarations;
|
|
16172
|
+
}
|
|
16173
|
+
function getCompoundComponentNames(sourceFile2) {
|
|
16174
|
+
const names = new Set;
|
|
16175
|
+
for (const statement of sourceFile2.statements) {
|
|
16176
|
+
if (!ts11.isExpressionStatement(statement))
|
|
16177
|
+
continue;
|
|
16178
|
+
const { expression } = statement;
|
|
16179
|
+
if (!ts11.isBinaryExpression(expression) || expression.operatorToken.kind !== ts11.SyntaxKind.EqualsToken)
|
|
16180
|
+
continue;
|
|
16181
|
+
if (!ts11.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
|
|
16182
|
+
continue;
|
|
16183
|
+
names.add(expression.left.name.text);
|
|
16184
|
+
if (ts11.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
|
|
16185
|
+
names.add(expression.right.text);
|
|
16186
|
+
}
|
|
16187
|
+
return names;
|
|
16188
|
+
}
|
|
16189
|
+
function isComponentValueKind(kind) {
|
|
16190
|
+
return kind === "variable" || kind === "function" || kind === "class";
|
|
16191
|
+
}
|
|
16192
|
+
function isRestrictedInternalKind(kind) {
|
|
16193
|
+
return kind === "interface" || kind === "type" || kind === "function";
|
|
16194
|
+
}
|
|
16195
|
+
function isAllowedComponentExport(declaration, isPage) {
|
|
16196
|
+
if (isComponentValueKind(declaration.kind) && isPascalCaseName(declaration.name))
|
|
16197
|
+
return true;
|
|
16198
|
+
return isPage && PAGE_RESERVED_EXPORTS.has(declaration.name);
|
|
16199
|
+
}
|
|
16200
|
+
function isPascalCaseName(name) {
|
|
16201
|
+
return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
|
|
16202
|
+
}
|
|
16203
|
+
function isDefaultExportStatement(statement) {
|
|
16204
|
+
if (ts11.isExportAssignment(statement))
|
|
16205
|
+
return !statement.isExportEquals;
|
|
16206
|
+
return !!(ts11.getCombinedModifierFlags(statement) & ts11.ModifierFlags.Default);
|
|
16207
|
+
}
|
|
15581
16208
|
function getPlaceholderExportWarnings(sourceFile2) {
|
|
15582
16209
|
if (!sourceFile2.file.endsWith("/index.ts") && !sourceFile2.file.endsWith("/index.tsx"))
|
|
15583
16210
|
return [];
|
|
@@ -15593,11 +16220,7 @@ function getPlaceholderExportWarnings(sourceFile2) {
|
|
|
15593
16220
|
function getDictionaryTextWarnings(sourceFile2) {
|
|
15594
16221
|
if (!sourceFile2.file.endsWith(".dictionary.ts"))
|
|
15595
16222
|
return [];
|
|
15596
|
-
const stalePatterns = [
|
|
15597
|
-
{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" },
|
|
15598
|
-
{ pattern: /settting/, label: "misspelling: settting" },
|
|
15599
|
-
{ pattern: /\uBC30\uB108 \uC218/, label: "stale copied Korean domain noun: \uBC30\uB108 \uC218" }
|
|
15600
|
-
];
|
|
16223
|
+
const stalePatterns = [{ pattern: /\b[A-Z][A-Za-z0-9]* description\b/, label: "scaffold description text" }];
|
|
15601
16224
|
return stalePatterns.flatMap(({ pattern, label }) => findPatternLines(sourceFile2.content, pattern).map((line) => ({
|
|
15602
16225
|
rule: "akan.file.dictionary-stale-text",
|
|
15603
16226
|
scope: "file",
|
|
@@ -15718,10 +16341,8 @@ function getConventionDescription(suffix, modelName) {
|
|
|
15718
16341
|
}
|
|
15719
16342
|
function getLibRootFile(file) {
|
|
15720
16343
|
const segments = file.split("/");
|
|
15721
|
-
if (segments[0] === "libs" && segments.length === 4 && segments[2] === "lib")
|
|
16344
|
+
if ((segments[0] === "libs" || segments[0] === "apps") && segments.length === 4 && segments[2] === "lib")
|
|
15722
16345
|
return segments[3];
|
|
15723
|
-
if (segments[0] === "apps" && segments.length === 5 && segments[2] === "lib")
|
|
15724
|
-
return segments[4];
|
|
15725
16346
|
return null;
|
|
15726
16347
|
}
|
|
15727
16348
|
function getModuleUiWarning(file) {
|
|
@@ -15862,26 +16483,61 @@ var targetPaths = {
|
|
|
15862
16483
|
"agents-md": "AGENTS.md",
|
|
15863
16484
|
claude: "CLAUDE.md"
|
|
15864
16485
|
};
|
|
15865
|
-
var
|
|
15866
|
-
|
|
15867
|
-
|
|
15868
|
-
|
|
16486
|
+
var AGENT_BLOCK_START = "<!-- akan:agent:start -->";
|
|
16487
|
+
var AGENT_BLOCK_END = "<!-- akan:agent:end -->";
|
|
16488
|
+
var SAMPLE_ARTIFACTS = [
|
|
16489
|
+
{ probe: "lib/task/task.constant.ts", target: "lib/task", label: "sample database module" },
|
|
16490
|
+
{ probe: "lib/_noti/noti.service.ts", target: "lib/_noti", label: "sample service module" },
|
|
16491
|
+
{
|
|
16492
|
+
probe: "lib/__scalar/workHistory/workHistory.dictionary.ts",
|
|
16493
|
+
target: "lib/__scalar/workHistory",
|
|
16494
|
+
label: "sample scalar module"
|
|
16495
|
+
},
|
|
16496
|
+
{ probe: "page/task/_index.tsx", target: "page/task", label: "sample task pages" }
|
|
16497
|
+
];
|
|
16498
|
+
var DEFAULT_INDEX_MARKER = "Akan.js template";
|
|
16499
|
+
var renderSampleCleanup = async (workspace, appNames) => {
|
|
16500
|
+
const items = [];
|
|
16501
|
+
for (const appName of appNames) {
|
|
16502
|
+
for (const artifact2 of SAMPLE_ARTIFACTS) {
|
|
16503
|
+
if (await workspace.exists(`apps/${appName}/${artifact2.probe}`)) {
|
|
16504
|
+
items.push(`- \`apps/${appName}/${artifact2.target}\` \u2014 ${artifact2.label}; delete it once you no longer need the reference.`);
|
|
16505
|
+
}
|
|
16506
|
+
}
|
|
16507
|
+
const indexPath = `apps/${appName}/page/_index.tsx`;
|
|
16508
|
+
if (await workspace.exists(indexPath)) {
|
|
16509
|
+
const content = await workspace.readFile(indexPath).catch(() => "");
|
|
16510
|
+
if (content.includes(DEFAULT_INDEX_MARKER)) {
|
|
16511
|
+
items.push(`- \`${indexPath}\` \u2014 default Akan landing page; replace it with your own home page.`);
|
|
16512
|
+
}
|
|
16513
|
+
}
|
|
16514
|
+
}
|
|
16515
|
+
if (items.length === 0)
|
|
16516
|
+
return "";
|
|
16517
|
+
return `## Start Clean (Remove Scaffolded Samples)
|
|
16518
|
+
|
|
16519
|
+
This workspace was scaffolded with reference samples so the Akan conventions are visible in real code. They are
|
|
16520
|
+
not part of your product. Before building real features, remove the samples below and run \`akan sync <app>\`:
|
|
16521
|
+
|
|
16522
|
+
${items.join(`
|
|
16523
|
+
`)}
|
|
16524
|
+
|
|
16525
|
+
Keep a sample only while you are still learning its pattern; delete it once your own modules cover the same ground.
|
|
16526
|
+
|
|
16527
|
+
`;
|
|
15869
16528
|
};
|
|
15870
|
-
var
|
|
16529
|
+
var renderManagedBlock = async (workspace) => {
|
|
15871
16530
|
const context = await AkanContextAnalyzer.analyze(workspace);
|
|
15872
16531
|
const frameworkGuide = await Prompter.getInstruction("framework");
|
|
15873
|
-
|
|
15874
|
-
|
|
15875
|
-
This file is generated by \`akan agent install ${target}\`.
|
|
15876
|
-
|
|
15877
|
-
## Workspace
|
|
16532
|
+
const sampleCleanup = await renderSampleCleanup(workspace, context.apps.map((app) => app.name));
|
|
16533
|
+
return `## Workspace
|
|
15878
16534
|
|
|
15879
16535
|
- Repo: ${context.repoName}
|
|
15880
16536
|
- Apps: ${context.apps.map((app) => app.name).join(", ") || "none"}
|
|
15881
16537
|
- Libraries: ${context.libs.map((lib) => lib.name).join(", ") || "none"}
|
|
15882
16538
|
- Packages: ${context.pkgs.map((pkg) => pkg.name).join(", ") || "none"}
|
|
15883
16539
|
|
|
15884
|
-
## Akan Module Abstracts
|
|
16540
|
+
${sampleCleanup}## Akan Module Abstracts
|
|
15885
16541
|
|
|
15886
16542
|
- Before changing a domain, service, or scalar module, read its \`*.abstract.md\` file first.
|
|
15887
16543
|
- Update the abstract when business invariants, workflows, or public behavior change.
|
|
@@ -15903,6 +16559,7 @@ If generated output is stale or broken, update the owning source file and run \`
|
|
|
15903
16559
|
- After \`apply_workflow\`, run \`run_validation\` with \`validationTarget\` when present; otherwise use \`applyReportPath\`.
|
|
15904
16560
|
- If no workflow exists, or apply reports unsupported/no-op/failed diagnostics that require manual action, keep edits scoped to owning source files and never patch generated files directly.
|
|
15905
16561
|
- For compound requests, split the request into workflows and apply each \`planPath\` in order, such as \`create-module\` followed by \`add-field\`.
|
|
16562
|
+
- **CLI-only fallback (MCP not connected):** \`akan mcp\` starts a stdio MCP server, so the \`list_workflows\`/\`plan_workflow\`/\`apply_workflow\` tools exist only when your agent is wired to it as an MCP client. When they are unavailable, the CLI is a first-class equivalent: \`akan workflow list\` / \`explain <name>\` / \`plan <name> ... --format json --out <planPath>\` / \`apply <planPath> --format json\`, \`akan doctor --strict --format json\` for validation, and \`akan repair generated|imports|module-shape --app <app> --format json\` for repairs. Scaffolding primitives (\`create-module\`/\`create-scalar\`/\`create-service\` take the target app/lib as a POSITIONAL arg; \`add-field\`/\`add-enum-field\` use \`--app\`/\`--module\` flags) call the same code the workflows do.
|
|
15906
16563
|
|
|
15907
16564
|
## Validation
|
|
15908
16565
|
|
|
@@ -15911,20 +16568,75 @@ ${context.validationCommands.map((command3) => `- \`${command3}\``).join(`
|
|
|
15911
16568
|
|
|
15912
16569
|
## Framework Guide
|
|
15913
16570
|
|
|
15914
|
-
${frameworkGuide.trim()}
|
|
16571
|
+
${frameworkGuide.trim()}`;
|
|
16572
|
+
};
|
|
16573
|
+
var upsertManagedBlock = (existing, block) => {
|
|
16574
|
+
const managed = `${AGENT_BLOCK_START}
|
|
16575
|
+
${block}
|
|
16576
|
+
${AGENT_BLOCK_END}`;
|
|
16577
|
+
const startIndex = existing.indexOf(AGENT_BLOCK_START);
|
|
16578
|
+
const endIndex = existing.indexOf(AGENT_BLOCK_END);
|
|
16579
|
+
if (startIndex >= 0 && endIndex > startIndex) {
|
|
16580
|
+
return `${existing.slice(0, startIndex)}${managed}${existing.slice(endIndex + AGENT_BLOCK_END.length)}`;
|
|
16581
|
+
}
|
|
16582
|
+
return `${existing.replace(/\s*$/, "")}
|
|
16583
|
+
|
|
16584
|
+
${managed}
|
|
16585
|
+
`;
|
|
16586
|
+
};
|
|
16587
|
+
var renderAgentsMd = async (workspace, existing) => {
|
|
16588
|
+
const block = await renderManagedBlock(workspace);
|
|
16589
|
+
if (existing?.trim())
|
|
16590
|
+
return upsertManagedBlock(existing, block);
|
|
16591
|
+
const context = await AkanContextAnalyzer.analyze(workspace);
|
|
16592
|
+
return `# ${context.repoName} Agent Guide
|
|
16593
|
+
|
|
16594
|
+
This file is the single source of truth for coding agents in this workspace. Claude Code reads it through
|
|
16595
|
+
\`CLAUDE.md\` (\`@AGENTS.md\`) and Cursor through \`.cursor/rules/akan.mdc\`. The section between the
|
|
16596
|
+
\`akan:agent\` markers is regenerated by \`akan agent install\`; edit anything outside the markers freely.
|
|
16597
|
+
|
|
16598
|
+
${AGENT_BLOCK_START}
|
|
16599
|
+
${block}
|
|
16600
|
+
${AGENT_BLOCK_END}
|
|
16601
|
+
`;
|
|
16602
|
+
};
|
|
16603
|
+
var renderClaudeMd = async (workspace) => {
|
|
16604
|
+
const context = await AkanContextAnalyzer.analyze(workspace);
|
|
16605
|
+
return `# ${context.repoName} \u2014 Claude Code Guide
|
|
16606
|
+
|
|
16607
|
+
@AGENTS.md
|
|
15915
16608
|
`;
|
|
15916
16609
|
};
|
|
16610
|
+
var renderCursorRule = () => `---
|
|
16611
|
+
description: Akan workspace agent guide
|
|
16612
|
+
alwaysApply: true
|
|
16613
|
+
---
|
|
16614
|
+
|
|
16615
|
+
Follow the workspace agent guide, which is the single source of truth for Akan conventions,
|
|
16616
|
+
generated-file rules, and the MCP workflow policy.
|
|
16617
|
+
|
|
16618
|
+
@AGENTS.md
|
|
16619
|
+
`;
|
|
16620
|
+
var renderTarget = async (workspace, target, existing) => {
|
|
16621
|
+
if (target === "agents-md")
|
|
16622
|
+
return await renderAgentsMd(workspace, existing);
|
|
16623
|
+
if (target === "claude")
|
|
16624
|
+
return await renderClaudeMd(workspace);
|
|
16625
|
+
return renderCursorRule();
|
|
16626
|
+
};
|
|
15917
16627
|
|
|
15918
16628
|
class AgentRunner extends runner("agent") {
|
|
15919
16629
|
async install(workspace, targets, { force = false } = {}) {
|
|
15920
16630
|
const written = [];
|
|
15921
16631
|
for (const target of targets) {
|
|
15922
16632
|
const filePath = targetPaths[target];
|
|
15923
|
-
|
|
16633
|
+
const exists2 = await workspace.exists(filePath);
|
|
16634
|
+
if (exists2 && !force && target !== "agents-md") {
|
|
15924
16635
|
throw new Error(`${filePath} already exists. Re-run with --force to overwrite it.`);
|
|
15925
16636
|
}
|
|
15926
|
-
const
|
|
15927
|
-
await workspace
|
|
16637
|
+
const existing = exists2 ? await workspace.readFile(filePath) : null;
|
|
16638
|
+
const content = await renderTarget(workspace, target, existing);
|
|
16639
|
+
await workspace.writeFile(filePath, content, { overwrite: true });
|
|
15928
16640
|
written.push(filePath);
|
|
15929
16641
|
}
|
|
15930
16642
|
return written;
|
|
@@ -17807,8 +18519,7 @@ var addEnumFieldWorkflowSpec = {
|
|
|
17807
18519
|
predictedChanges: [
|
|
17808
18520
|
{ target: "*/lib/<module>/<module>.constant.ts", action: "modify", reason: "Enum field shape is added." },
|
|
17809
18521
|
{ target: "*/lib/<module>/<module>.dictionary.ts", action: "modify", reason: "Enum labels are added." },
|
|
17810
|
-
{ target: "*/lib/<module>/<module>.option.ts", action: "modify", reason: "Enum options may be added." }
|
|
17811
|
-
{ target: "*/lib/option.ts", action: "sync", reason: "Generated option barrel may change after sync." }
|
|
18522
|
+
{ target: "*/lib/<module>/<module>.option.ts", action: "modify", reason: "Enum options may be added." }
|
|
17812
18523
|
],
|
|
17813
18524
|
validation: [
|
|
17814
18525
|
...baseValidation,
|
|
@@ -17999,8 +18710,18 @@ var addMutationWorkflowSpec = {
|
|
|
17999
18710
|
}
|
|
18000
18711
|
],
|
|
18001
18712
|
predictedChanges: [
|
|
18002
|
-
{
|
|
18003
|
-
|
|
18713
|
+
{
|
|
18714
|
+
target: "*/lib/<module>/<module>.service.ts",
|
|
18715
|
+
action: "modify",
|
|
18716
|
+
applyScope: "auto",
|
|
18717
|
+
reason: "Service mutation method stub is added."
|
|
18718
|
+
},
|
|
18719
|
+
{
|
|
18720
|
+
target: "*/lib/<module>/<module>.signal.ts",
|
|
18721
|
+
action: "modify",
|
|
18722
|
+
applyScope: "auto",
|
|
18723
|
+
reason: "Signal endpoint mutation is added."
|
|
18724
|
+
},
|
|
18004
18725
|
{ target: "*/lib/srv.ts", action: "sync", reason: "Generated service barrel may change after sync." },
|
|
18005
18726
|
{ target: "*/lib/sig.ts", action: "sync", reason: "Generated signal barrel may change after sync." }
|
|
18006
18727
|
],
|
|
@@ -18072,9 +18793,24 @@ var addSliceWorkflowSpec = {
|
|
|
18072
18793
|
}
|
|
18073
18794
|
],
|
|
18074
18795
|
predictedChanges: [
|
|
18075
|
-
{
|
|
18076
|
-
|
|
18077
|
-
|
|
18796
|
+
{
|
|
18797
|
+
target: "*/lib/<module>/<module>.service.ts",
|
|
18798
|
+
action: "modify",
|
|
18799
|
+
applyScope: "auto",
|
|
18800
|
+
reason: "Query helper method stub is added."
|
|
18801
|
+
},
|
|
18802
|
+
{
|
|
18803
|
+
target: "*/lib/<module>/<module>.signal.ts",
|
|
18804
|
+
action: "modify",
|
|
18805
|
+
applyScope: "auto",
|
|
18806
|
+
reason: "Signal slice entry is added."
|
|
18807
|
+
},
|
|
18808
|
+
{
|
|
18809
|
+
target: "*/lib/<module>/<Module>.Zone.tsx",
|
|
18810
|
+
action: "modify",
|
|
18811
|
+
applyScope: "manual-review",
|
|
18812
|
+
reason: "Zone may connect slice state."
|
|
18813
|
+
},
|
|
18078
18814
|
{ target: "*/lib/sig.ts", action: "sync", reason: "Generated signal barrel may change after sync." }
|
|
18079
18815
|
],
|
|
18080
18816
|
validation: [
|
|
@@ -18188,8 +18924,7 @@ var createScalarWorkflowSpec = {
|
|
|
18188
18924
|
}
|
|
18189
18925
|
],
|
|
18190
18926
|
predictedChanges: [
|
|
18191
|
-
{ target: "*/lib/__scalar/<scalar>/*", action: "create", reason: "New scalar source files are scaffolded." }
|
|
18192
|
-
{ target: "*/lib/option.ts", action: "sync", reason: "Generated option barrel may include scalar options." }
|
|
18927
|
+
{ target: "*/lib/__scalar/<scalar>/*", action: "create", reason: "New scalar source files are scaffolded." }
|
|
18193
18928
|
],
|
|
18194
18929
|
validation: baseValidation,
|
|
18195
18930
|
completionCriteria: ["Scalar files exist under __scalar.", "Generated files are refreshed.", "Lint passes."]
|
|
@@ -19386,6 +20121,12 @@ class PrimitiveScript extends script("primitive", [ModuleScript]) {
|
|
|
19386
20121
|
code: "primitive-field-type-unsupported",
|
|
19387
20122
|
message: `Field type "${input7.type}" is ambiguous in Akan. Use Int for integer fields or Float for decimal fields.`,
|
|
19388
20123
|
input: "type"
|
|
20124
|
+
} : null,
|
|
20125
|
+
input7.type && input7.type.toLowerCase() === "upload" ? {
|
|
20126
|
+
severity: "error",
|
|
20127
|
+
code: "primitive-field-type-upload-misuse",
|
|
20128
|
+
message: "Upload is not a model field type. Declare an image/file field as a relation to the File model (e.g. field(File)); Upload is only valid in a { fileUpload: true } signal body. Note the File model is provided by the shared file library.",
|
|
20129
|
+
input: "type"
|
|
19389
20130
|
} : null
|
|
19390
20131
|
]);
|
|
19391
20132
|
if (!sys3 || !input7.module || !input7.field || !input7.type || diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
@@ -19584,6 +20325,206 @@ class PrimitiveScript extends script("primitive", [ModuleScript]) {
|
|
|
19584
20325
|
nextActions: nextActionsForTarget(sys3.name)
|
|
19585
20326
|
});
|
|
19586
20327
|
}
|
|
20328
|
+
async addMutation(workspace, input7) {
|
|
20329
|
+
const moduleClassName = input7.module ? moduleComponentName(input7.module) : "";
|
|
20330
|
+
const serviceRef = lowerlize(moduleClassName);
|
|
20331
|
+
const name = input7.mutation;
|
|
20332
|
+
return await this.#writeServiceSignalEntry(workspace, {
|
|
20333
|
+
command: "add-mutation",
|
|
20334
|
+
app: input7.app,
|
|
20335
|
+
module: input7.module,
|
|
20336
|
+
entryKind: "mutation",
|
|
20337
|
+
entryName: name,
|
|
20338
|
+
requiredInput: "mutation",
|
|
20339
|
+
serviceMethod: name ? {
|
|
20340
|
+
name,
|
|
20341
|
+
block: [
|
|
20342
|
+
` async ${name}() {`,
|
|
20343
|
+
` // TODO(service): implement ${moduleClassName}Service.${name}`,
|
|
20344
|
+
` return true;`,
|
|
20345
|
+
` }`
|
|
20346
|
+
].join(`
|
|
20347
|
+
`)
|
|
20348
|
+
} : null,
|
|
20349
|
+
signal: name ? {
|
|
20350
|
+
className: `${moduleClassName}Endpoint`,
|
|
20351
|
+
entryLine: [
|
|
20352
|
+
`${name}: mutation(Boolean)`,
|
|
20353
|
+
` .exec(async function () {`,
|
|
20354
|
+
` return await this.${serviceRef}Service.${name}();`,
|
|
20355
|
+
` }),`
|
|
20356
|
+
].join(`
|
|
20357
|
+
`),
|
|
20358
|
+
param: { mode: "destructure", name: "mutation" }
|
|
20359
|
+
} : null
|
|
20360
|
+
});
|
|
20361
|
+
}
|
|
20362
|
+
async addSlice(workspace, input7) {
|
|
20363
|
+
const moduleClassName = input7.module ? moduleComponentName(input7.module) : "";
|
|
20364
|
+
const serviceRef = lowerlize(moduleClassName);
|
|
20365
|
+
const name = input7.slice;
|
|
20366
|
+
const queryName = name ? `query${capitalize12(name)}` : null;
|
|
20367
|
+
return await this.#writeServiceSignalEntry(workspace, {
|
|
20368
|
+
command: "add-slice",
|
|
20369
|
+
app: input7.app,
|
|
20370
|
+
module: input7.module,
|
|
20371
|
+
entryKind: "slice",
|
|
20372
|
+
entryName: name,
|
|
20373
|
+
requiredInput: "slice",
|
|
20374
|
+
serviceMethod: name && queryName ? {
|
|
20375
|
+
name: queryName,
|
|
20376
|
+
block: [
|
|
20377
|
+
` ${queryName}() {`,
|
|
20378
|
+
` // TODO(service): return a QueryOf for the ${name} slice`,
|
|
20379
|
+
` return {};`,
|
|
20380
|
+
` }`
|
|
20381
|
+
].join(`
|
|
20382
|
+
`)
|
|
20383
|
+
} : null,
|
|
20384
|
+
signal: name && queryName ? {
|
|
20385
|
+
className: `${moduleClassName}Slice`,
|
|
20386
|
+
entryLine: [
|
|
20387
|
+
`${name}: init()`,
|
|
20388
|
+
` .exec(function () {`,
|
|
20389
|
+
` return this.${serviceRef}Service.${queryName}();`,
|
|
20390
|
+
` }),`
|
|
20391
|
+
].join(`
|
|
20392
|
+
`),
|
|
20393
|
+
param: { mode: "positional", name: "init" }
|
|
20394
|
+
} : null
|
|
20395
|
+
});
|
|
20396
|
+
}
|
|
20397
|
+
async#writeServiceSignalEntry(workspace, spec) {
|
|
20398
|
+
const sys3 = await this.resolveSys(workspace, spec.app);
|
|
20399
|
+
const diagnostics = compactDiagnostics([
|
|
20400
|
+
!sys3 && { severity: "error", code: "primitive-target-missing", message: "Target app or library was not found." },
|
|
20401
|
+
!spec.module && {
|
|
20402
|
+
severity: "error",
|
|
20403
|
+
code: "primitive-input-missing",
|
|
20404
|
+
message: "Module is required.",
|
|
20405
|
+
input: "module"
|
|
20406
|
+
},
|
|
20407
|
+
!spec.entryName && {
|
|
20408
|
+
severity: "error",
|
|
20409
|
+
code: "primitive-input-missing",
|
|
20410
|
+
message: `${capitalize12(spec.requiredInput)} name is required.`,
|
|
20411
|
+
input: spec.requiredInput
|
|
20412
|
+
}
|
|
20413
|
+
]);
|
|
20414
|
+
if (!sys3 || !spec.module || !spec.entryName || !spec.serviceMethod || !spec.signal) {
|
|
20415
|
+
return createPrimitiveWriteReport({
|
|
20416
|
+
command: spec.command,
|
|
20417
|
+
changedFiles: [],
|
|
20418
|
+
generatedFiles: [],
|
|
20419
|
+
validationCommands: [],
|
|
20420
|
+
diagnostics,
|
|
20421
|
+
nextActions: []
|
|
20422
|
+
});
|
|
20423
|
+
}
|
|
20424
|
+
const paths = moduleSourcePaths(spec.module);
|
|
20425
|
+
const servicePath = paths.service;
|
|
20426
|
+
const signalPath = paths.signal;
|
|
20427
|
+
const changedFiles = [];
|
|
20428
|
+
const generatedFiles2 = generatedFilesForSync(sys3);
|
|
20429
|
+
const [hasServiceFile, hasSignalFile] = await Promise.all([sys3.exists(servicePath), sys3.exists(signalPath)]);
|
|
20430
|
+
if (!hasServiceFile) {
|
|
20431
|
+
diagnostics.push({
|
|
20432
|
+
severity: "error",
|
|
20433
|
+
code: "primitive-source-missing",
|
|
20434
|
+
message: `Service source file was not found: ${servicePath}.`
|
|
20435
|
+
});
|
|
20436
|
+
}
|
|
20437
|
+
if (!hasSignalFile) {
|
|
20438
|
+
diagnostics.push({
|
|
20439
|
+
severity: "error",
|
|
20440
|
+
code: "primitive-source-missing",
|
|
20441
|
+
message: `Signal source file was not found: ${signalPath}.`
|
|
20442
|
+
});
|
|
20443
|
+
}
|
|
20444
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
20445
|
+
return createPrimitiveWriteReport({
|
|
20446
|
+
command: spec.command,
|
|
20447
|
+
changedFiles,
|
|
20448
|
+
generatedFiles: generatedFiles2,
|
|
20449
|
+
validationCommands: validationCommandsForTarget(sys3.name),
|
|
20450
|
+
diagnostics,
|
|
20451
|
+
nextActions: nextActionsForTarget(sys3.name)
|
|
20452
|
+
});
|
|
20453
|
+
}
|
|
20454
|
+
const serviceClassName = `${moduleComponentName(spec.module)}Service`;
|
|
20455
|
+
const serviceContent = await sys3.readFile(servicePath);
|
|
20456
|
+
const signalContent = await sys3.readFile(signalPath);
|
|
20457
|
+
const serviceMethodExists = hasClassMethod(serviceContent, serviceClassName, spec.serviceMethod.name);
|
|
20458
|
+
if (serviceMethodExists) {
|
|
20459
|
+
diagnostics.push({
|
|
20460
|
+
severity: "error",
|
|
20461
|
+
code: "primitive-service-method-exists",
|
|
20462
|
+
input: spec.requiredInput,
|
|
20463
|
+
message: `Method "${spec.serviceMethod.name}" already exists in ${serviceClassName}.`
|
|
20464
|
+
});
|
|
20465
|
+
}
|
|
20466
|
+
if (hasSignalFactoryEntry(signalContent, spec.signal.className, spec.entryName)) {
|
|
20467
|
+
diagnostics.push({
|
|
20468
|
+
severity: "error",
|
|
20469
|
+
code: "primitive-signal-entry-exists",
|
|
20470
|
+
input: spec.requiredInput,
|
|
20471
|
+
message: `Entry "${spec.entryName}" already exists in ${spec.signal.className}.`
|
|
20472
|
+
});
|
|
20473
|
+
}
|
|
20474
|
+
const nextServiceContent = serviceMethodExists ? serviceContent : insertClassMethod(serviceContent, serviceClassName, spec.serviceMethod.block);
|
|
20475
|
+
const nextSignalContent = insertSignalFactoryEntry(signalContent, spec.signal.className, spec.entryName, spec.signal.entryLine, spec.signal.param);
|
|
20476
|
+
if (!nextServiceContent) {
|
|
20477
|
+
diagnostics.push({
|
|
20478
|
+
severity: "error",
|
|
20479
|
+
code: "primitive-service-shape-unsupported",
|
|
20480
|
+
message: `Could not find ${serviceClassName} class body in ${servicePath}.`
|
|
20481
|
+
});
|
|
20482
|
+
}
|
|
20483
|
+
if (!nextSignalContent) {
|
|
20484
|
+
diagnostics.push({
|
|
20485
|
+
severity: "error",
|
|
20486
|
+
code: "primitive-signal-shape-unsupported",
|
|
20487
|
+
message: `Could not find a safe insertion point in ${spec.signal.className} within ${signalPath}. Ensure the class exists and its factory returns an object literal.`
|
|
20488
|
+
});
|
|
20489
|
+
}
|
|
20490
|
+
if (nextServiceContent && hasSourceParseErrors(nextServiceContent, "service.ts")) {
|
|
20491
|
+
diagnostics.push({
|
|
20492
|
+
severity: "error",
|
|
20493
|
+
code: "primitive-post-edit-service-parse-failed",
|
|
20494
|
+
failureScope: "source-change",
|
|
20495
|
+
message: `Edited ${servicePath} did not parse cleanly; refusing to write source.`
|
|
20496
|
+
});
|
|
20497
|
+
}
|
|
20498
|
+
if (nextSignalContent && hasSourceParseErrors(nextSignalContent, "signal.ts")) {
|
|
20499
|
+
diagnostics.push({
|
|
20500
|
+
severity: "error",
|
|
20501
|
+
code: "primitive-post-edit-signal-parse-failed",
|
|
20502
|
+
failureScope: "source-change",
|
|
20503
|
+
message: `Edited ${signalPath} did not parse cleanly; refusing to write source.`
|
|
20504
|
+
});
|
|
20505
|
+
}
|
|
20506
|
+
if (nextSignalContent && !hasSignalFactoryEntry(nextSignalContent, spec.signal.className, spec.entryName)) {
|
|
20507
|
+
diagnostics.push({
|
|
20508
|
+
severity: "error",
|
|
20509
|
+
code: "primitive-post-edit-signal-verify-failed",
|
|
20510
|
+
failureScope: "source-change",
|
|
20511
|
+
message: `Edited ${signalPath} did not contain entry "${spec.entryName}" in ${spec.signal.className}.`
|
|
20512
|
+
});
|
|
20513
|
+
}
|
|
20514
|
+
if (!diagnostics.some((diagnostic) => diagnostic.severity === "error") && nextServiceContent && nextSignalContent) {
|
|
20515
|
+
await sys3.writeFile(servicePath, nextServiceContent);
|
|
20516
|
+
await sys3.writeFile(signalPath, nextSignalContent);
|
|
20517
|
+
changedFiles.push(sourceFile(sys3, servicePath, "modify", `Service ${spec.entryKind} was added.`), sourceFile(sys3, signalPath, "modify", `Signal ${spec.entryKind} was added.`));
|
|
20518
|
+
}
|
|
20519
|
+
return createPrimitiveWriteReport({
|
|
20520
|
+
command: spec.command,
|
|
20521
|
+
changedFiles,
|
|
20522
|
+
generatedFiles: generatedFiles2,
|
|
20523
|
+
validationCommands: validationCommandsForTarget(sys3.name),
|
|
20524
|
+
diagnostics,
|
|
20525
|
+
nextActions: nextActionsForTarget(sys3.name)
|
|
20526
|
+
});
|
|
20527
|
+
}
|
|
19587
20528
|
}
|
|
19588
20529
|
|
|
19589
20530
|
// pkgs/@akanjs/cli/scalar/scalar.prompt.ts
|
|
@@ -19801,7 +20742,9 @@ var createCliWorkflowStepRegistry = (workspace) => createWorkflowStepRegistry({
|
|
|
19801
20742
|
createScalar: (sys3, scalar) => CommandContainer.get(ScalarScript).createScalar(sys3, scalar),
|
|
19802
20743
|
createUi: (input8) => CommandContainer.get(PrimitiveScript).createUi(workspace, input8),
|
|
19803
20744
|
addField: (input8) => CommandContainer.get(PrimitiveScript).addField(workspace, input8),
|
|
19804
|
-
addEnumField: (input8) => CommandContainer.get(PrimitiveScript).addEnumField(workspace, input8)
|
|
20745
|
+
addEnumField: (input8) => CommandContainer.get(PrimitiveScript).addEnumField(workspace, input8),
|
|
20746
|
+
addMutation: (input8) => CommandContainer.get(PrimitiveScript).addMutation(workspace, input8),
|
|
20747
|
+
addSlice: (input8) => CommandContainer.get(PrimitiveScript).addSlice(workspace, input8)
|
|
19805
20748
|
});
|
|
19806
20749
|
|
|
19807
20750
|
// pkgs/@akanjs/cli/context/context.runner.ts
|
|
@@ -19852,14 +20795,18 @@ class ContextRunner extends runner("context") {
|
|
|
19852
20795
|
return await Prompter.getInstruction(name);
|
|
19853
20796
|
}
|
|
19854
20797
|
async installMcp(workspace, target, { force = false, mode = "readonly" } = {}) {
|
|
19855
|
-
if (target
|
|
19856
|
-
|
|
19857
|
-
|
|
20798
|
+
if (target === "codex")
|
|
20799
|
+
return await this.#installCodexMcp(workspace, { force, mode });
|
|
20800
|
+
return await this.#installJsonMcp(workspace, target, { force, mode });
|
|
20801
|
+
}
|
|
20802
|
+
async#installJsonMcp(workspace, target, { force, mode }) {
|
|
20803
|
+
const configPath2 = akanMcpInstallConfigPaths[target];
|
|
20804
|
+
const existing = await workspace.exists(configPath2) ? await workspace.readJson(configPath2) : {};
|
|
19858
20805
|
const mcpServers = existing.mcpServers ?? {};
|
|
19859
20806
|
const currentAkanServer = mcpServers.akan;
|
|
19860
|
-
const nextAkanServer =
|
|
20807
|
+
const nextAkanServer = createAkanMcpServer(target, mode);
|
|
19861
20808
|
if (currentAkanServer && !force && JSON.stringify(currentAkanServer) !== JSON.stringify(nextAkanServer)) {
|
|
19862
|
-
throw new Error(`${
|
|
20809
|
+
throw new Error(`${configPath2} already has an "akan" MCP server. Re-run with --force to overwrite it.`);
|
|
19863
20810
|
}
|
|
19864
20811
|
const nextConfig = {
|
|
19865
20812
|
...existing,
|
|
@@ -19868,9 +20815,15 @@ class ContextRunner extends runner("context") {
|
|
|
19868
20815
|
akan: nextAkanServer
|
|
19869
20816
|
}
|
|
19870
20817
|
};
|
|
19871
|
-
await workspace.writeFile(
|
|
20818
|
+
await workspace.writeFile(configPath2, `${JSON.stringify(nextConfig, null, 2)}
|
|
19872
20819
|
`);
|
|
19873
|
-
return
|
|
20820
|
+
return configPath2;
|
|
20821
|
+
}
|
|
20822
|
+
async#installCodexMcp(workspace, { force, mode }) {
|
|
20823
|
+
const existing = await workspace.exists(codexMcpConfigPath) ? await workspace.readFile(codexMcpConfigPath) : "";
|
|
20824
|
+
const merged = upsertCodexMcpServerBlock(existing, createAkanCodexMcpServerBlock(mode), { force });
|
|
20825
|
+
await workspace.writeFile(codexMcpConfigPath, merged);
|
|
20826
|
+
return codexMcpConfigPath;
|
|
19874
20827
|
}
|
|
19875
20828
|
listMcpTools(mode = "readonly") {
|
|
19876
20829
|
return listAkanMcpTools(mode);
|
|
@@ -20162,13 +21115,26 @@ ${payload}`);
|
|
|
20162
21115
|
agent: "`akan agent install <target>` writes editor-specific agent rules with overwrite protection.",
|
|
20163
21116
|
mcp: "`akan mcp --mode readonly|plan|apply` starts the Akan MCP server over stdio with an explicit permission mode.",
|
|
20164
21117
|
"mcp-call": '`akan mcp-call <tool> --mode plan --args \'{"workflow":"add-field","inputs":{"app":"demo"}}\'` calls one MCP tool through the same runner path for debugging.',
|
|
20165
|
-
"mcp-install": "`akan mcp-install cursor --mode readonly|plan|apply` installs the Akan MCP server config for Cursor."
|
|
21118
|
+
"mcp-install": "`akan mcp-install [cursor|claude|codex|all] --mode readonly|plan|apply` installs the Akan MCP server config for Cursor (.cursor/mcp.json), Claude Code (.mcp.json), and Codex (.codex/config.toml). Defaults to all targets."
|
|
20166
21119
|
};
|
|
20167
21120
|
return explanations[command3] ?? `No detailed explanation is available for ${command3}. Run \`akan --help\` for command help.`;
|
|
20168
21121
|
}
|
|
20169
21122
|
}
|
|
20170
21123
|
|
|
20171
21124
|
// pkgs/@akanjs/cli/context/context.script.ts
|
|
21125
|
+
var resolveMcpInstallTargets = (target) => {
|
|
21126
|
+
if (!target || target === "all")
|
|
21127
|
+
return [...akanMcpInstallTargets];
|
|
21128
|
+
if (akanMcpInstallTargets.includes(target))
|
|
21129
|
+
return [target];
|
|
21130
|
+
throw new Error(`Unknown MCP install target: ${target}. Use cursor, claude, codex, or all.`);
|
|
21131
|
+
};
|
|
21132
|
+
var mcpTargetLabels = {
|
|
21133
|
+
cursor: "Cursor",
|
|
21134
|
+
claude: "Claude Code",
|
|
21135
|
+
codex: "Codex"
|
|
21136
|
+
};
|
|
21137
|
+
|
|
20172
21138
|
class ContextScript extends script("context", [ContextRunner]) {
|
|
20173
21139
|
async context(workspace, options = {}) {
|
|
20174
21140
|
Logger17.rawLog(await this.contextRunner.getContext(workspace, options));
|
|
@@ -20177,11 +21143,15 @@ class ContextScript extends script("context", [ContextRunner]) {
|
|
|
20177
21143
|
Logger17.rawLog(await this.contextRunner.doctor(workspace, options));
|
|
20178
21144
|
}
|
|
20179
21145
|
async mcpInstall(workspace, target, { force = false, mode = "readonly" } = {}) {
|
|
20180
|
-
|
|
20181
|
-
|
|
20182
|
-
const
|
|
20183
|
-
|
|
20184
|
-
|
|
21146
|
+
const targets = resolveMcpInstallTargets(target);
|
|
21147
|
+
const written = [];
|
|
21148
|
+
for (const t of targets) {
|
|
21149
|
+
const configPath2 = await this.contextRunner.installMcp(workspace, t, { force, mode });
|
|
21150
|
+
written.push(`${mcpTargetLabels[t]}: ${configPath2}`);
|
|
21151
|
+
}
|
|
21152
|
+
Logger17.rawLog(`Akan MCP server installed (${mode} mode):
|
|
21153
|
+
${written.map((line) => `- ${line}`).join(`
|
|
21154
|
+
`)}`);
|
|
20185
21155
|
}
|
|
20186
21156
|
async mcp(workspace, { mode = "readonly" } = {}) {
|
|
20187
21157
|
await this.contextRunner.runMcp(workspace, { mode });
|
|
@@ -20220,7 +21190,7 @@ class ContextCommand extends command("context", [ContextScript], ({ public: targ
|
|
|
20220
21190
|
}).option("strict", Boolean, { desc: "treat recommended conventions as errors", default: false }).with(Workspace).exec(async function(format, strict, workspace) {
|
|
20221
21191
|
await this.contextScript.doctor(workspace, { format, strict });
|
|
20222
21192
|
}),
|
|
20223
|
-
mcpInstall: target({ desc: "Install the Akan MCP server config for Cursor" }).arg("target", String, { desc: "cursor", nullable: true }).option("force", Boolean, { desc: "overwrite an existing Akan MCP server entry", default: false }).option("mode", String, {
|
|
21193
|
+
mcpInstall: target({ desc: "Install the Akan MCP server config for Cursor, Claude Code, and Codex" }).arg("target", String, { desc: "cursor, claude, codex, or all", nullable: true }).option("force", Boolean, { desc: "overwrite an existing Akan MCP server entry", default: false }).option("mode", String, {
|
|
20224
21194
|
desc: "MCP permission mode",
|
|
20225
21195
|
default: "readonly",
|
|
20226
21196
|
enum: ["readonly", "plan", "apply"]
|
|
@@ -20605,7 +21575,7 @@ class LibraryCommand extends command("library", [LibraryScript], ({ public: targ
|
|
|
20605
21575
|
}
|
|
20606
21576
|
|
|
20607
21577
|
// pkgs/@akanjs/cli/localRegistry/localRegistry.runner.ts
|
|
20608
|
-
import { mkdir as mkdir13, rm as
|
|
21578
|
+
import { mkdir as mkdir13, rm as rm7 } from "fs/promises";
|
|
20609
21579
|
import path47 from "path";
|
|
20610
21580
|
import { Logger as Logger19 } from "akanjs/common";
|
|
20611
21581
|
var defaultLocalRegistryUrl = "http://127.0.0.1:4873";
|
|
@@ -20648,13 +21618,13 @@ class LocalRegistryRunner extends runner("localRegistry") {
|
|
|
20648
21618
|
try {
|
|
20649
21619
|
await workspace.spawn("docker", ["rm", "-f", containerName], { stdio: "inherit" });
|
|
20650
21620
|
} catch {}
|
|
20651
|
-
await
|
|
21621
|
+
await rm7(path47.join(workspace.workspaceRoot, ".akan/verdaccio"), { recursive: true, force: true });
|
|
20652
21622
|
Logger19.info("Local registry storage has been reset");
|
|
20653
21623
|
}
|
|
20654
21624
|
async smoke(workspace, { registryUrl } = {}) {
|
|
20655
21625
|
const registry = this.getRegistryUrl(registryUrl);
|
|
20656
21626
|
const smokeRoot = path47.join(workspace.workspaceRoot, ".akan/e2e");
|
|
20657
|
-
await
|
|
21627
|
+
await rm7(path47.join(smokeRoot, smokeRepoName), { recursive: true, force: true });
|
|
20658
21628
|
await workspace.spawn(process.execPath, [
|
|
20659
21629
|
"dist/pkgs/create-akan-workspace/index.js",
|
|
20660
21630
|
smokeRepoName,
|
|
@@ -20992,7 +21962,9 @@ class WorkflowScript extends script("workflow", [WorkflowRunner, ModuleScript, S
|
|
|
20992
21962
|
createScalar: (sys3, scalar) => this.scalarScript.createScalar(sys3, scalar),
|
|
20993
21963
|
createUi: (input8) => this.primitiveScript.createUi(workspace, input8),
|
|
20994
21964
|
addField: (input8) => this.primitiveScript.addField(workspace, input8),
|
|
20995
|
-
addEnumField: (input8) => this.primitiveScript.addEnumField(workspace, input8)
|
|
21965
|
+
addEnumField: (input8) => this.primitiveScript.addEnumField(workspace, input8),
|
|
21966
|
+
addMutation: (input8) => this.primitiveScript.addMutation(workspace, input8),
|
|
21967
|
+
addSlice: (input8) => this.primitiveScript.addSlice(workspace, input8)
|
|
20996
21968
|
})
|
|
20997
21969
|
}));
|
|
20998
21970
|
return;
|
|
@@ -21038,6 +22010,7 @@ import path48 from "path";
|
|
|
21038
22010
|
var defaultWorkspacePeerDependencies = new Set([
|
|
21039
22011
|
"@react-spring/web",
|
|
21040
22012
|
"@use-gesture/react",
|
|
22013
|
+
"chance",
|
|
21041
22014
|
"croner",
|
|
21042
22015
|
"daisyui",
|
|
21043
22016
|
"react",
|