@akanjs/devkit 3.0.0-beta.16 → 3.0.0-beta.18
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/abstractDoc.ts +1 -3
- package/applicationBuildRunner.ts +4 -3
- package/artifact/implicitRootLayout.test.ts +6 -11
- package/artifact/implicitRootLayout.ts +0 -15
- package/biome.base.json +6 -1
- package/capacitorApp.test.ts +26 -0
- package/capacitorApp.ts +25 -4
- package/codeAgent/agent/AkanCodeServices.ts +43 -27
- package/codeAgent/agent/AkanContextFiles.ts +117 -0
- package/codeAgent/agent/CodeAgent.ts +87 -19
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -0
- package/codeAgent/agent/SubagentPool.ts +55 -4
- package/codeAgent/agent/akanCodeModel.ts +8 -8
- package/codeAgent/agent/akanCodePaths.ts +23 -3
- package/codeAgent/agent/akanContextFiles.test.ts +115 -0
- package/codeAgent/agent/akanSystemPrompt.ts +5 -0
- package/codeAgent/index.ts +0 -1
- package/codeAgent/skills/akan-code/SKILL.md +122 -0
- package/codeAgent/skills/akan-module/SKILL.md +2 -2
- package/codeAgent/skills/akan-store/SKILL.md +6 -2
- package/codeAgent/tools/AkanCodePlugins.ts +26 -3
- package/codeAgent/tools/AkanEnvKeys.ts +5 -3
- package/codeAgent/tools/AskToolPack.ts +111 -0
- package/codeAgent/tools/McpClient.ts +51 -5
- package/codeAgent/tools/McpOAuth.ts +268 -0
- package/codeAgent/tools/McpOAuthLoopback.ts +77 -0
- package/codeAgent/tools/McpServerConfig.ts +148 -0
- package/codeAgent/tools/McpSignIn.ts +161 -0
- package/codeAgent/tools/McpTokenStore.ts +92 -0
- package/codeAgent/tools/McpToolPack.ts +42 -49
- package/codeAgent/tools/akanCodePlugins.test.ts +36 -3
- package/codeAgent/tools/askToolPack.test.ts +119 -0
- package/codeAgent/tools/mcpOAuth.test.ts +320 -0
- package/codeAgent/tools/mcpServerConfig.test.ts +205 -0
- package/commandDecorators/command.ts +2 -0
- package/commandDecorators/commandDecorators.test.ts +1 -1
- package/commandDecorators/helpFormatter.ts +2 -1
- package/executors.test.ts +10 -46
- package/index.ts +0 -1
- package/package.json +8 -6
- package/piDependencyPins.test.ts +4 -2
- package/prompter.ts +0 -7
- package/qualityScanner.test.ts +3 -2
- package/qualityScanner.ts +1 -1
- package/routeSourceValidator.test.ts +0 -1
- package/routeSourceValidator.ts +0 -12
- package/scanInfo.ts +5 -2
- package/workflow/executor.ts +52 -0
- package/workflow/source.ts +19 -13
- package/codeAgent/agent/AkanAuthStorageBackend.ts +0 -105
- package/guideline.ts +0 -19
package/executors.test.ts
CHANGED
|
@@ -727,7 +727,7 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
727
727
|
});
|
|
728
728
|
});
|
|
729
729
|
|
|
730
|
-
test("accepts
|
|
730
|
+
test("accepts head route exports during page key discovery", async () => {
|
|
731
731
|
const root = await makeTempRoot();
|
|
732
732
|
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
733
733
|
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
@@ -737,24 +737,19 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
737
737
|
await writeFile(path.join(root, "apps/demo/akan.config.ts"), "export default {};\n");
|
|
738
738
|
await writeFile(
|
|
739
739
|
path.join(root, "apps/demo/page/_layout.tsx"),
|
|
740
|
-
[
|
|
741
|
-
"export const head = null;",
|
|
742
|
-
"export const metadata = { title: 'Root' };",
|
|
743
|
-
"export default function Layout({ children }) { return children; }",
|
|
744
|
-
"",
|
|
745
|
-
].join("\n"),
|
|
740
|
+
["export const head = null;", "export default function Layout({ children }) { return children; }", ""].join("\n"),
|
|
746
741
|
);
|
|
747
742
|
await writeFile(
|
|
748
743
|
path.join(root, "apps/demo/page/docs/_layout.tsx"),
|
|
749
744
|
[
|
|
750
|
-
"export async function
|
|
745
|
+
"export async function generateHead() { return null; }",
|
|
751
746
|
"export default function Layout({ children }) { return children; }",
|
|
752
747
|
"",
|
|
753
748
|
].join("\n"),
|
|
754
749
|
);
|
|
755
750
|
await writeFile(
|
|
756
751
|
path.join(root, "apps/demo/page/docs/intro.tsx"),
|
|
757
|
-
["export const
|
|
752
|
+
["export const head = null;", "export default function Page() { return null; }", ""].join("\n"),
|
|
758
753
|
);
|
|
759
754
|
|
|
760
755
|
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
@@ -767,7 +762,7 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
767
762
|
]);
|
|
768
763
|
});
|
|
769
764
|
|
|
770
|
-
test("rejects
|
|
765
|
+
test("rejects a metadata route export during page key discovery", async () => {
|
|
771
766
|
const root = await makeTempRoot();
|
|
772
767
|
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
773
768
|
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
@@ -776,47 +771,16 @@ describe("Workspace and app executor environment contracts", () => {
|
|
|
776
771
|
await mkdir(path.join(root, "apps/demo/page"), { recursive: true });
|
|
777
772
|
await writeFile(path.join(root, "apps/demo/akan.config.ts"), "export default {};\n");
|
|
778
773
|
await writeFile(
|
|
779
|
-
path.join(root, "apps/demo/page/
|
|
780
|
-
[
|
|
781
|
-
"
|
|
782
|
-
|
|
783
|
-
"export default function Page() { return null; }",
|
|
784
|
-
"",
|
|
785
|
-
].join("\n"),
|
|
774
|
+
path.join(root, "apps/demo/page/legacy.tsx"),
|
|
775
|
+
["export const metadata = { title: 'Legacy' };", "export default function Page() { return null; }", ""].join(
|
|
776
|
+
"\n",
|
|
777
|
+
),
|
|
786
778
|
);
|
|
787
779
|
|
|
788
780
|
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
789
781
|
const app = AppExecutor.from(workspace, "demo");
|
|
790
782
|
|
|
791
|
-
await expect(app.getPageKeys({ refresh: true })).rejects.toThrow(
|
|
792
|
-
"metadata and generateMetadata cannot both be exported",
|
|
793
|
-
);
|
|
794
|
-
});
|
|
795
|
-
|
|
796
|
-
test("rejects mixed head and metadata route export channels during page key discovery", async () => {
|
|
797
|
-
const root = await makeTempRoot();
|
|
798
|
-
process.env.AKAN_PUBLIC_REPO_NAME = "repo";
|
|
799
|
-
process.env.AKAN_PUBLIC_SERVE_DOMAIN = "example.com";
|
|
800
|
-
process.env.AKAN_PUBLIC_ENV = "local";
|
|
801
|
-
await writeJson(path.join(root, "package.json"), rootPackageJson());
|
|
802
|
-
await mkdir(path.join(root, "apps/demo/page"), { recursive: true });
|
|
803
|
-
await writeFile(path.join(root, "apps/demo/akan.config.ts"), "export default {};\n");
|
|
804
|
-
await writeFile(
|
|
805
|
-
path.join(root, "apps/demo/page/mixed.tsx"),
|
|
806
|
-
[
|
|
807
|
-
"export const head = null;",
|
|
808
|
-
"export const metadata = { title: 'Mixed' };",
|
|
809
|
-
"export default function Page() { return null; }",
|
|
810
|
-
"",
|
|
811
|
-
].join("\n"),
|
|
812
|
-
);
|
|
813
|
-
|
|
814
|
-
const workspace = new WorkspaceExecutor({ workspaceRoot: root, repoName: "repo" });
|
|
815
|
-
const app = AppExecutor.from(workspace, "demo");
|
|
816
|
-
|
|
817
|
-
await expect(app.getPageKeys({ refresh: true })).rejects.toThrow(
|
|
818
|
-
"head/generateHead and metadata/generateMetadata cannot both be exported",
|
|
819
|
-
);
|
|
783
|
+
await expect(app.getPageKeys({ refresh: true })).rejects.toThrow('unsupported export "metadata"');
|
|
820
784
|
});
|
|
821
785
|
|
|
822
786
|
test("assigns start command ports from sorted app order", async () => {
|
package/index.ts
CHANGED
|
@@ -32,7 +32,6 @@ export type * from "./fileSys";
|
|
|
32
32
|
export type * from "./frontendBuild";
|
|
33
33
|
export type * from "./getDirname";
|
|
34
34
|
export type * from "./getRelatedCnsts";
|
|
35
|
-
export type * from "./guideline";
|
|
36
35
|
export type * from "./incrementalBuilder";
|
|
37
36
|
export type * from "./mobile";
|
|
38
37
|
export type * from "./packageExportsMap";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.18",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -39,14 +39,16 @@
|
|
|
39
39
|
"./*": "./*.ts"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@earendil-works/
|
|
43
|
-
"@earendil-works/pi-
|
|
44
|
-
"@earendil-works/pi-
|
|
45
|
-
"@earendil-works/pi-
|
|
42
|
+
"@earendil-works/chord": "0.86.1",
|
|
43
|
+
"@earendil-works/pi-agent-core": "0.86.1",
|
|
44
|
+
"@earendil-works/pi-ai": "0.86.1",
|
|
45
|
+
"@earendil-works/pi-coding-agent": "0.86.1",
|
|
46
|
+
"@earendil-works/pi-telemetry": "0.86.1",
|
|
47
|
+
"@earendil-works/pi-tui": "0.86.1",
|
|
46
48
|
"@inquirer/prompts": "^8.4.3",
|
|
47
49
|
"@tailwindcss/node": "^4.3.0",
|
|
48
50
|
"@trapezedev/project": "^7.1.4",
|
|
49
|
-
"akanjs": "3.0.0-beta.
|
|
51
|
+
"akanjs": "3.0.0-beta.18",
|
|
50
52
|
"chalk": "^5.6.2",
|
|
51
53
|
"commander": "^14.0.3",
|
|
52
54
|
"dayjs": "^1.11.20",
|
package/piDependencyPins.test.ts
CHANGED
|
@@ -28,9 +28,11 @@ const readJson = async <T>(file: string) => (await Bun.file(file).json()) as T;
|
|
|
28
28
|
|
|
29
29
|
const shrinkwrappedPins = async () => {
|
|
30
30
|
const { packages } = await readJson<Shrinkwrap>(`${piDir}/npm-shrinkwrap.json`);
|
|
31
|
+
// Top-level entries only: a key with a second `node_modules/` in it is a *nested placement* of some other
|
|
32
|
+
// package's dependency, which a consumer's resolver reaches on its own and we have no business pinning.
|
|
31
33
|
const entries = Object.entries(packages)
|
|
32
|
-
.
|
|
33
|
-
.
|
|
34
|
+
.filter(([key]) => /^node_modules\/@earendil-works\/[^/]+$/.test(key))
|
|
35
|
+
.map(([key, entry]) => [key.replace(/^node_modules\//, ""), entry.version ?? ""] as const);
|
|
34
36
|
return Object.fromEntries(entries);
|
|
35
37
|
};
|
|
36
38
|
|
package/prompter.ts
CHANGED
|
@@ -2,7 +2,6 @@ import fsPromise from "node:fs/promises";
|
|
|
2
2
|
import { input, select } from "@inquirer/prompts";
|
|
3
3
|
|
|
4
4
|
import { getDirname } from "./getDirname";
|
|
5
|
-
import type { GuideGenerateJson } from "./guideline";
|
|
6
5
|
|
|
7
6
|
interface FileUpdateRequestProps {
|
|
8
7
|
context: string;
|
|
@@ -34,12 +33,6 @@ export class Prompter {
|
|
|
34
33
|
const guidelineRoot = await Prompter.#getGuidelineRoot();
|
|
35
34
|
return (await fsPromise.readdir(guidelineRoot)).filter((name) => !name.startsWith("_")).sort();
|
|
36
35
|
}
|
|
37
|
-
static async getGuideJson(guideName: string): Promise<GuideGenerateJson> {
|
|
38
|
-
const guidelineRoot = await Prompter.#getGuidelineRoot();
|
|
39
|
-
const filePath = `${guidelineRoot}/${guideName}/${guideName}.generate.json`;
|
|
40
|
-
const guideJson = await fsPromise.readFile(filePath, "utf-8");
|
|
41
|
-
return JSON.parse(guideJson) as GuideGenerateJson;
|
|
42
|
-
}
|
|
43
36
|
/**
|
|
44
37
|
* The name is checked against the directory listing rather than interpolated straight into a path:
|
|
45
38
|
* it arrives from an MCP tool argument, and an unknown one has to name the valid set instead of
|
package/qualityScanner.test.ts
CHANGED
|
@@ -26,7 +26,7 @@ afterEach(async () => {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
describe("AkanQualityScanner abstract rule", () => {
|
|
29
|
-
test("warns on an abstract over the line limit and
|
|
29
|
+
test("warns on an abstract over the line limit and says how to trim it", async () => {
|
|
30
30
|
const root = await makeWorkspace({
|
|
31
31
|
"apps/demo/lib/post/post.abstract.md": abstractOf(AbstractDoc.maxLines + 1),
|
|
32
32
|
"apps/demo/lib/post/post.constant.ts": "export class Post {}\n",
|
|
@@ -41,7 +41,8 @@ describe("AkanQualityScanner abstract rule", () => {
|
|
|
41
41
|
expect(warnings).toHaveLength(1);
|
|
42
42
|
expect(warnings[0]?.file).toBe("apps/demo/lib/post/post.abstract.md");
|
|
43
43
|
expect(warnings[0]?.message).toContain(`${AbstractDoc.maxLines + 1} lines`);
|
|
44
|
-
expect(warnings[0]?.fix).toContain("
|
|
44
|
+
expect(warnings[0]?.fix).toContain("## Rules");
|
|
45
|
+
expect(warnings[0]?.fix).not.toContain("akan compact");
|
|
45
46
|
});
|
|
46
47
|
});
|
|
47
48
|
|
package/qualityScanner.ts
CHANGED
|
@@ -114,7 +114,7 @@ const RULE_FIXES: Record<string, string> = {
|
|
|
114
114
|
"Split the file by responsibility — move Zones, Utils, or subcomponents into sibling files.",
|
|
115
115
|
"akan.file.max-lines": "Break the file into smaller focused modules; keep one primary responsibility per file.",
|
|
116
116
|
"akan.file.abstract-max-lines":
|
|
117
|
-
"
|
|
117
|
+
"Trim the abstract by hand to a title, one sentence naming what the module owns, a `## Rules` list of the invariants the source cannot show, and an optional workflow chain.",
|
|
118
118
|
"akan.file.placeholder-export":
|
|
119
119
|
"Remove the placeholder export; generated indexes should only re-export real modules.",
|
|
120
120
|
"akan.file.bang-comment-in-client":
|
|
@@ -15,7 +15,6 @@ describe("RouteSourceValidator", () => {
|
|
|
15
15
|
"export const reconnect = true;",
|
|
16
16
|
"export const wsConnect = true;",
|
|
17
17
|
"export const layoutStyle = {};",
|
|
18
|
-
'export const gaTrackingId = "G-1";',
|
|
19
18
|
].join("\n");
|
|
20
19
|
|
|
21
20
|
expect(() => validate(source, "layout", true)).not.toThrow();
|
package/routeSourceValidator.ts
CHANGED
|
@@ -86,18 +86,6 @@ export class RouteSourceValidator {
|
|
|
86
86
|
if (exported.has("head") && exported.has("generateHead")) {
|
|
87
87
|
throw new Error(`[route-convention] head and generateHead cannot both be exported in ${filePath}`);
|
|
88
88
|
}
|
|
89
|
-
if (
|
|
90
|
-
!options.rootLayout &&
|
|
91
|
-
(exported.has("head") || exported.has("generateHead")) &&
|
|
92
|
-
(exported.has("metadata") || exported.has("generateMetadata"))
|
|
93
|
-
) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
`[route-convention] head/generateHead and metadata/generateMetadata cannot both be exported in ${filePath}`,
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
if (exported.has("metadata") && exported.has("generateMetadata")) {
|
|
99
|
-
throw new Error(`[route-convention] metadata and generateMetadata cannot both be exported in ${filePath}`);
|
|
100
|
-
}
|
|
101
89
|
const chain = RouteSourceValidator.#readChain(sourceFile, filePath);
|
|
102
90
|
if (!chain) return { devOnly: RouteSourceValidator.#readDevOnly(sourceFile, filePath) };
|
|
103
91
|
const named = [...exported].filter((name) => name !== "default");
|
package/scanInfo.ts
CHANGED
|
@@ -524,16 +524,19 @@ export class PkgInfo {
|
|
|
524
524
|
const [tsconfig, rootPackageJson] = await Promise.all([exec.getTsConfig(), exec.workspace.getPackageJson()]);
|
|
525
525
|
const scanner = await createDependencyScanner(exec);
|
|
526
526
|
const npmSet = new Set(Object.keys({ ...rootPackageJson.dependencies, ...rootPackageJson.devDependencies }));
|
|
527
|
+
const workspacePathOf = (resolve: string) => resolve.replace(/^\.\//, "");
|
|
527
528
|
const pkgPathSet = new Set(
|
|
528
529
|
Object.keys(tsconfig.compilerOptions.paths ?? {})
|
|
529
|
-
.filter((path) =>
|
|
530
|
+
.filter((path) =>
|
|
531
|
+
tsconfig.compilerOptions.paths?.[path]?.some((resolve) => workspacePathOf(resolve).startsWith("pkgs/")),
|
|
532
|
+
)
|
|
530
533
|
.map((path) => path.replace("/*", "")),
|
|
531
534
|
);
|
|
532
535
|
const [npmDepSet, pkgPathDepSet] = await scanner.getImportSets([npmSet, pkgPathSet]);
|
|
533
536
|
const pkgDeps = [...pkgPathDepSet]
|
|
534
537
|
.map((path) => {
|
|
535
538
|
const pathSplitLength = path.split("/").length;
|
|
536
|
-
return (tsconfig.compilerOptions.paths?.[path]?.[0] ?? "*")
|
|
539
|
+
return workspacePathOf(tsconfig.compilerOptions.paths?.[path]?.[0] ?? "*")
|
|
537
540
|
.split("/")
|
|
538
541
|
.slice(1, 1 + pathSplitLength)
|
|
539
542
|
.join("/");
|
package/workflow/executor.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { capitalize } from "akanjs/common";
|
|
1
2
|
import ts from "typescript";
|
|
2
3
|
import type { AkanModuleContext } from "../akanContext";
|
|
3
4
|
import type { Sys, Workspace } from "../commandDecorators";
|
|
@@ -396,6 +397,54 @@ const addFieldUiSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult =>
|
|
|
396
397
|
};
|
|
397
398
|
};
|
|
398
399
|
|
|
400
|
+
const addMutationActionSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult => {
|
|
401
|
+
const module = workflowStringInput(plan.inputs.module) ?? "<module>";
|
|
402
|
+
const mutation = workflowStringInput(plan.inputs.mutation) ?? "<mutation>";
|
|
403
|
+
const moduleClassName = moduleComponentName(module);
|
|
404
|
+
return {
|
|
405
|
+
recommendations: [
|
|
406
|
+
{
|
|
407
|
+
code: "add-mutation-action-surface-review",
|
|
408
|
+
kind: "manual-action",
|
|
409
|
+
target: `*/${moduleSourcePaths(module).store}`,
|
|
410
|
+
action: `Name the endpoint's guards and add its dictionary .endpoint() entry with a .desc(). After sync, fetch.${mutation} exists; write a store action only for a toast, an optimistic update, or a multi-field write, and put the control that calls it in ${moduleClassName}.Util.tsx.`,
|
|
411
|
+
confidence: "medium",
|
|
412
|
+
message: `Workflow apply does not write store or UI code for ${module}.${mutation}; review whether a screen should call it.`,
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
nextActions: [
|
|
416
|
+
{
|
|
417
|
+
command: `akan workflow explain ${plan.workflow}`,
|
|
418
|
+
reason: "Review action surface guidance before manually editing store or UI files.",
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
};
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const addSliceViewSurfaceInspection = (plan: WorkflowPlan): WorkflowStepResult => {
|
|
425
|
+
const module = workflowStringInput(plan.inputs.module) ?? "<module>";
|
|
426
|
+
const slice = workflowStringInput(plan.inputs.slice) ?? "<slice>";
|
|
427
|
+
const moduleClassName = moduleComponentName(module);
|
|
428
|
+
return {
|
|
429
|
+
recommendations: [
|
|
430
|
+
{
|
|
431
|
+
code: "add-slice-view-surface-review",
|
|
432
|
+
kind: "manual-action",
|
|
433
|
+
target: `*/${moduleSourcePaths(module).zone}`,
|
|
434
|
+
action: `Fill in the service query stub and add the dictionary .slice() entry. After sync, load fetch.init${moduleClassName}${capitalize(slice)}() in the page and pass the result to a ${moduleClassName}.Zone as an init prop.`,
|
|
435
|
+
confidence: "medium",
|
|
436
|
+
message: `Workflow apply does not write page or Zone code for ${module}.${slice}; review where the list should render.`,
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
nextActions: [
|
|
440
|
+
{
|
|
441
|
+
command: `akan workflow explain ${plan.workflow}`,
|
|
442
|
+
reason: "Review view surface guidance before manually editing Zone or page files.",
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
};
|
|
446
|
+
};
|
|
447
|
+
|
|
399
448
|
export const createWorkflowStepRegistry = ({
|
|
400
449
|
workspace,
|
|
401
450
|
createModule,
|
|
@@ -494,6 +543,8 @@ export const createWorkflowStepRegistry = ({
|
|
|
494
543
|
}),
|
|
495
544
|
),
|
|
496
545
|
[workflowStepKey("add-mutation", "update-signal")]: inspect,
|
|
546
|
+
[workflowStepKey("add-mutation", "update-action-surfaces")]: async (_step, plan) =>
|
|
547
|
+
addMutationActionSurfaceInspection(plan),
|
|
497
548
|
[workflowStepKey("add-slice", "update-service-query")]: async (_step, plan) =>
|
|
498
549
|
primitiveReportToWorkflowStepResult(
|
|
499
550
|
await addSlice({
|
|
@@ -503,6 +554,7 @@ export const createWorkflowStepRegistry = ({
|
|
|
503
554
|
}),
|
|
504
555
|
),
|
|
505
556
|
[workflowStepKey("add-slice", "update-signal-slice")]: inspect,
|
|
557
|
+
[workflowStepKey("add-slice", "connect-view-surfaces")]: async (_step, plan) => addSliceViewSurfaceInspection(plan),
|
|
506
558
|
};
|
|
507
559
|
};
|
|
508
560
|
|
package/workflow/source.ts
CHANGED
|
@@ -160,21 +160,27 @@ export const normalizeFieldType = (typeName: string) => {
|
|
|
160
160
|
return normalizedTypes[typeName.toLowerCase()] ?? typeName;
|
|
161
161
|
};
|
|
162
162
|
|
|
163
|
-
export const
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const nextNames = [...baseImport.names, typeName].sort();
|
|
163
|
+
export const ensureNamedImport = (content: string, moduleSpecifier: string, name: string) => {
|
|
164
|
+
const source = sourceFileFor("source.ts", content);
|
|
165
|
+
const namedImport = findNamedImport(source, moduleSpecifier);
|
|
166
|
+
if (namedImport) {
|
|
167
|
+
if (namedImport.names.includes(name)) return content;
|
|
168
|
+
const nextNames = [...namedImport.names, name].sort();
|
|
170
169
|
return spliceText(
|
|
171
170
|
content,
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
namedImport.namedBindingsStart,
|
|
172
|
+
namedImport.namedBindingsEnd,
|
|
174
173
|
`{ ${nextNames.join(", ")} }`,
|
|
175
174
|
);
|
|
176
175
|
}
|
|
177
|
-
return `import { ${
|
|
176
|
+
return `import { ${name} } from "${moduleSpecifier}";\n${content}`;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const ensureBaseImport = (content: string, name: string) => ensureNamedImport(content, "akanjs/base", name);
|
|
180
|
+
|
|
181
|
+
export const ensureBaseTypeImport = (content: string, typeName: string) => {
|
|
182
|
+
if (typeName !== "Int" && typeName !== "Float") return content;
|
|
183
|
+
return ensureBaseImport(content, typeName);
|
|
178
184
|
};
|
|
179
185
|
|
|
180
186
|
export type FieldDefaultValue = string | number | boolean | null;
|
|
@@ -200,12 +206,12 @@ const booleanDefault = (rawDefault: FieldDefaultValue): string | null => {
|
|
|
200
206
|
};
|
|
201
207
|
|
|
202
208
|
const dateDefault = (rawDefault: FieldDefaultValue): string | null => {
|
|
203
|
-
if (typeof rawDefault === "number" && Number.isFinite(rawDefault)) return `
|
|
209
|
+
if (typeof rawDefault === "number" && Number.isFinite(rawDefault)) return `() => dayjs(${rawDefault})`;
|
|
204
210
|
if (typeof rawDefault !== "string") return null;
|
|
205
211
|
const trimmed = rawDefault.trim();
|
|
206
212
|
if (!trimmed) return null;
|
|
207
|
-
if (trimmed === "now") return "
|
|
208
|
-
if (!Number.isNaN(Date.parse(trimmed))) return `
|
|
213
|
+
if (trimmed === "now") return "() => dayjs()";
|
|
214
|
+
if (!Number.isNaN(Date.parse(trimmed))) return `() => dayjs(${JSON.stringify(trimmed)})`;
|
|
209
215
|
return null;
|
|
210
216
|
};
|
|
211
217
|
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
closeSync,
|
|
3
|
-
existsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
openSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
renameSync,
|
|
8
|
-
unlinkSync,
|
|
9
|
-
writeFileSync,
|
|
10
|
-
} from "node:fs";
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import type { AuthStorageBackend } from "@earendil-works/pi-coding-agent";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Credential store for `akan code`, in akan's own file rather than the engine's.
|
|
16
|
-
*
|
|
17
|
-
* The engine only asks for a locked read-modify-write over one opaque string, so satisfying its interface with
|
|
18
|
-
* our own file keeps every key under `~/.akan/` — nothing lands in `~/.pi/` and a user who removes akan takes
|
|
19
|
-
* their credentials with them.
|
|
20
|
-
*
|
|
21
|
-
* The lock is an exclusive-create sentinel rather than an advisory flock: two `akan code` processes refreshing
|
|
22
|
-
* an OAuth token at the same moment would otherwise each write a token the other invalidates.
|
|
23
|
-
*/
|
|
24
|
-
export class AkanAuthStorageBackend implements AuthStorageBackend {
|
|
25
|
-
readonly #authPath: string;
|
|
26
|
-
readonly #lockPath: string;
|
|
27
|
-
|
|
28
|
-
constructor(authPath: string) {
|
|
29
|
-
this.#authPath = authPath;
|
|
30
|
-
this.#lockPath = `${authPath}.lock`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
withLock<T>(fn: (current: string | undefined) => { result: T; next?: string }) {
|
|
34
|
-
this.#acquire();
|
|
35
|
-
try {
|
|
36
|
-
const { result, next } = fn(this.#read());
|
|
37
|
-
if (next !== undefined) this.#write(next);
|
|
38
|
-
return result;
|
|
39
|
-
} finally {
|
|
40
|
-
this.#release();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async withLockAsync<T>(fn: (current: string | undefined) => Promise<{ result: T; next?: string }>) {
|
|
45
|
-
this.#acquire();
|
|
46
|
-
try {
|
|
47
|
-
const { result, next } = await fn(this.#read());
|
|
48
|
-
if (next !== undefined) this.#write(next);
|
|
49
|
-
return result;
|
|
50
|
-
} finally {
|
|
51
|
-
this.#release();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
#read() {
|
|
56
|
-
if (!existsSync(this.#authPath)) return undefined;
|
|
57
|
-
const text = readFileSync(this.#authPath, "utf8");
|
|
58
|
-
return text.trim() ? text : undefined;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
#write(next: string) {
|
|
62
|
-
mkdirSync(path.dirname(this.#authPath), { recursive: true, mode: 0o700 });
|
|
63
|
-
const temp = `${this.#authPath}.${process.pid}.tmp`;
|
|
64
|
-
writeFileSync(temp, next, { mode: 0o600 });
|
|
65
|
-
renameSync(temp, this.#authPath);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
#acquire() {
|
|
69
|
-
mkdirSync(path.dirname(this.#lockPath), { recursive: true, mode: 0o700 });
|
|
70
|
-
const deadline = Date.now() + 5_000;
|
|
71
|
-
for (;;) {
|
|
72
|
-
try {
|
|
73
|
-
closeSync(openSync(this.#lockPath, "wx"));
|
|
74
|
-
return;
|
|
75
|
-
} catch {
|
|
76
|
-
// A lock left behind by a killed process would block every later run, so it expires.
|
|
77
|
-
if (this.#lockIsStale() || Date.now() > deadline) {
|
|
78
|
-
try {
|
|
79
|
-
unlinkSync(this.#lockPath);
|
|
80
|
-
} catch {
|
|
81
|
-
// Another process won the race to clear it; retry the create.
|
|
82
|
-
}
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
#lockIsStale() {
|
|
91
|
-
try {
|
|
92
|
-
return Date.now() - Bun.file(this.#lockPath).lastModified > 30_000;
|
|
93
|
-
} catch {
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
#release() {
|
|
99
|
-
try {
|
|
100
|
-
unlinkSync(this.#lockPath);
|
|
101
|
-
} catch {
|
|
102
|
-
// Already gone — a stale-lock sweep by another process.
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
package/guideline.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export interface GuideScan {
|
|
2
|
-
type: "example" | "source" | "usage" | (string & {});
|
|
3
|
-
description: string;
|
|
4
|
-
path: string;
|
|
5
|
-
filterText?: string;
|
|
6
|
-
sample?: number;
|
|
7
|
-
}
|
|
8
|
-
export interface GuideUpdate {
|
|
9
|
-
filePath: string;
|
|
10
|
-
contents: string[];
|
|
11
|
-
rules: string[];
|
|
12
|
-
}
|
|
13
|
-
export interface GuideGenerateJson {
|
|
14
|
-
title: string;
|
|
15
|
-
description: string;
|
|
16
|
-
scans: GuideScan[];
|
|
17
|
-
update: GuideUpdate;
|
|
18
|
-
page?: string;
|
|
19
|
-
}
|